1. Business Understanding¶
InsurTech-Unternehmen beschäftigen sich mit dem das Thema der Modernisierung der Versicherungsbranche. Das Ziel ist es, die Versicherungen günstiger, attraktiver, "effizienter" zu machen durch moderne "Versicherungsmodelle, digitalisierte Prozesse, neue Beratungs- und Managementmethoden" gestaltet werden. Die zunehmenden Herausforderungen erschweren es den Versicherern, sich im Wettbewerb zu behaupten und am Markt zu bestehen. Bestehend aus adaptiven Kundenansprüchen, "Digitalisierung, strengerer Regulierung und Bürokratie", werden die Versicherer unter dem Aspekt des wirtschaftlichen Ergebnisses deutlich auf die Probe gestellt. Gleichzeitig ergeben sich Chancen im Geschäftsmodell, die noch ungenutzt sind. ungenutzt bleiben. Eines dieser Modelle ist das Prinzip des Cross-Selling. Damit dieses Modell umgesetzt werden kann, ist die ist die Meinung der bestehenden Kunden entscheidend. Das Geschäftsmodell sollte auf der Grundlage des bestehenden Kundeninteresses antizipiert werden. Die Analyse soll dem Versicherer zeigen, welches Potenzial in welcher Höhe tatsächlich vorhanden ist. Die Analyse wird sich mit der Frage beschäftigen, ob es Kunden gibt, die sich für eine Kfz-Versicherung interessieren. Die Antwort auf diese Frage soll gleichzeitig auch eine Darstellung des Interesses liefern. So lässt sich abschätzen, ob sich eine aktive Kundenansprache überhaupt lohnt.
2. Daten und Datenverständnis¶
Der Datensatz heißt Janatahack Cross-sell Prediction und wurde in einen Trainings- und einen Testdatensatz aufgeteilt. Das Attribut "Response" ist die Zielvariable und drückt das Interesse eines Kunden an einer Kfz-Versicherung aus. Da diese wichtige Spalte jedoch merkwürdigerweise im Testdatensatz fehlt, wurde nur der Trainingsdatensatz für das Modell verwendet. Dieser wurde also für Training und Test verwendet. Der Datensatz besteht aus 12 Spalten und 381109 Zeilen. Jede Zeile steht für eine Person.
2.1. Import von relevanten Modulen¶
import numpy as np
import pandas as pd
import statsmodels.api as sm
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.linear_model import LinearRegression
sns.set()
import warnings
warnings.filterwarnings('ignore')
# Zusätzlich für Logistische Regression benötigt:
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.linear_model import LogisticRegression
from sklearn import metrics
from sklearn.metrics import confusion_matrix, classification_report
from statsmodels.stats.outliers_influence import variance_inflation_factor
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
from sklearn import metrics
2.2. Daten einlesen¶
# Upload data
raw_data = pd.read_csv('https://storage.googleapis.com/ml-service-repository-datastorage/Prediction_Interest_for_car_insurance_data.csv')
Zu Beginn wird ein Überblick über die Daten gegeben. Es werden nur die ersten 5 Dateneinträge oder Zeilen berücksichtigt.
# print first 5 rows of the dataframe
raw_data.head()
id | Gender | Age | Driving_License | Region_Code | Previously_Insured | Vehicle_Age | Vehicle_Damage | Annual_Premium | Policy_Sales_Channel | Vintage | Response | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 1 | Male | 44 | 1 | 28.0 | 0 | > 2 Years | Yes | 40454.0 | 26.0 | 217 | 1 |
1 | 2 | Male | 76 | 1 | 3.0 | 0 | 1-2 Year | No | 33536.0 | 26.0 | 183 | 0 |
2 | 3 | Male | 47 | 1 | 28.0 | 0 | > 2 Years | Yes | 38294.0 | 26.0 | 27 | 1 |
3 | 4 | Male | 21 | 1 | 11.0 | 1 | < 1 Year | No | 28619.0 | 152.0 | 203 | 0 |
4 | 5 | Female | 29 | 1 | 41.0 | 1 | < 1 Year | No | 27496.0 | 152.0 | 39 | 0 |
Nachdem wir nun einen ersten Überblick über die Daten erhalten haben, wollen wir uns nun die Datentypen der einzelnen Spalten ansehen.
raw_data.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 381109 entries, 0 to 381108 Data columns (total 12 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 id 381109 non-null int64 1 Gender 381109 non-null object 2 Age 381109 non-null int64 3 Driving_License 381109 non-null int64 4 Region_Code 381109 non-null float64 5 Previously_Insured 381109 non-null int64 6 Vehicle_Age 381109 non-null object 7 Vehicle_Damage 381109 non-null object 8 Annual_Premium 381109 non-null float64 9 Policy_Sales_Channel 381109 non-null float64 10 Vintage 381109 non-null int64 11 Response 381109 non-null int64 dtypes: float64(3), int64(6), object(3) memory usage: 34.9+ MB
Die Datentypen stimmen mit den angegebenen Spalten überein. Die Gefahr der Erzeugung von NULL-Werten ist nicht gegeben. Daher ist keine Korrektur erforderlich.
2.3. Datenbereinigung¶
Zunächst werden alle leeren Zellen aus dem Datensatz summiert, in welchem Bereich mit welcher Gesamtzahl von Variablen sie fehlen.
raw_data.isnull().sum()
id 0 Gender 0 Age 0 Driving_License 0 Region_Code 0 Previously_Insured 0 Vehicle_Age 0 Vehicle_Damage 0 Annual_Premium 0 Policy_Sales_Channel 0 Vintage 0 Response 0 dtype: int64
Es werden keine NULL-Werte angegeben. Daher ist weder die Ersetzung durch synthetische Daten noch die Löschung bestimmter Zeilen erforderlich.
Nachdem die Tabelle auf NULL-Werte geprüft wurde, folgt die Prüfung auf Duplikate.
raw_data[raw_data.duplicated(keep=False)]
id | Gender | Age | Driving_License | Region_Code | Previously_Insured | Vehicle_Age | Vehicle_Damage | Annual_Premium | Policy_Sales_Channel | Vintage | Response |
---|
Auch hier wurden keine Duplikate festgestellt.
2.4. Deskriptive Analyse¶
Nach der Überprüfung auf NULL-Werte und Duplikate wird nun die deskriptive Statistik auf die Daten angewendet. Dies sollte einen ersten Eindruck davon vermitteln, wie die Daten im Moment strukturiert sind.
raw_data.describe(include='all')
id | Gender | Age | Driving_License | Region_Code | Previously_Insured | Vehicle_Age | Vehicle_Damage | Annual_Premium | Policy_Sales_Channel | Vintage | Response | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
count | 381109.000000 | 381109 | 381109.000000 | 381109.000000 | 381109.000000 | 381109.000000 | 381109 | 381109 | 381109.000000 | 381109.000000 | 381109.000000 | 381109.000000 |
unique | NaN | 2 | NaN | NaN | NaN | NaN | 3 | 2 | NaN | NaN | NaN | NaN |
top | NaN | Male | NaN | NaN | NaN | NaN | 1-2 Year | Yes | NaN | NaN | NaN | NaN |
freq | NaN | 206089 | NaN | NaN | NaN | NaN | 200316 | 192413 | NaN | NaN | NaN | NaN |
mean | 190555.000000 | NaN | 38.822584 | 0.997869 | 26.388807 | 0.458210 | NaN | NaN | 30564.389581 | 112.034295 | 154.347397 | 0.122563 |
std | 110016.836208 | NaN | 15.511611 | 0.046110 | 13.229888 | 0.498251 | NaN | NaN | 17213.155057 | 54.203995 | 83.671304 | 0.327936 |
min | 1.000000 | NaN | 20.000000 | 0.000000 | 0.000000 | 0.000000 | NaN | NaN | 2630.000000 | 1.000000 | 10.000000 | 0.000000 |
25% | 95278.000000 | NaN | 25.000000 | 1.000000 | 15.000000 | 0.000000 | NaN | NaN | 24405.000000 | 29.000000 | 82.000000 | 0.000000 |
50% | 190555.000000 | NaN | 36.000000 | 1.000000 | 28.000000 | 0.000000 | NaN | NaN | 31669.000000 | 133.000000 | 154.000000 | 0.000000 |
75% | 285832.000000 | NaN | 49.000000 | 1.000000 | 35.000000 | 1.000000 | NaN | NaN | 39400.000000 | 152.000000 | 227.000000 | 0.000000 |
max | 381109.000000 | NaN | 85.000000 | 1.000000 | 52.000000 | 1.000000 | NaN | NaN | 540165.000000 | 163.000000 | 299.000000 | 1.000000 |
Nach der Analyse der deskriptiven Statistiken sticht die Spalte "Jahresprämie" hervor. Der Durchschnitt (Mittelwert) weist einen Wert von 30564,389581 auf. Auffallend ist hier der Maximalwert von 540165, der deutlich hervorsticht. Dies deutet bereits auf einen Ausreißer beim Maximalwert hin.
Der Datensatz besteht aus 381109 Zeilen, jeweils eine Zeile zur Beschreibung des Kunden und 12 Spalten zur Beschreibung der Eigenschaften des Kunden. Mit diesen Daten wird versucht zu klassifizieren, ob ein Kunde an einer zusätzlichen Kfz-Versicherung interessiert ist oder nicht. Zu diesem Zweck enthalten die historischen Daten die Zielvariable "Antwort", die Auskunft darüber gibt, ob ein Kunde interessiert ist.
2.4.1. Numerische Attribute¶
Wir beginnen mit den numerischen Attributen. Zunächst werden die Verteilungen der numerischen Attribute einzeln untersucht und in einem zweiten Schritt werden die kategorischen Attribute mit der Zielvariablen in Beziehung gesetzt.
# upload all numeric attributes
numeric_data = raw_data.select_dtypes(include=[np.number])
Alter¶
sns.distplot(numeric_data["Age"])
<AxesSubplot:xlabel='Age', ylabel='Density'>
- Die Mehrheit der Kunden ist zwischen 20 und 30 Jahre alt
- Die Kurve flacht nach hinten stark ab
- Fraglich, ob es Ausreißer im hinteren Teil gibt --> Boxplot anwenden
sns.distplot(raw_data[raw_data.Response == 0]["Age"],
bins=10,
color='orange',
label='No',
kde=True)
sns.distplot(raw_data[raw_data.Response == 1]["Age"],
bins=10,
color='blue',
label='Yes',
kde=True)
<AxesSubplot:xlabel='Age', ylabel='Density'>
- Kunden im Alter von 20 bis 30 Jahren haben eher kein Interesse am Abschluss einer Zusatzversicherung.
- Das größte Interesse besteht bei Kunden zwischen 35 und 55 Jahren.
plt.boxplot(numeric_data["Age"])
plt.show()
Mit Hilfe des Boxplots konnte sichergestellt werden, dass keine Ausreißer zu finden waren. Sollten Ausreißer auftreten, würden sie als Punkte oberhalb der orangefarbenen Linie erscheinen.
Führerschein¶
sns.distplot(numeric_data["Driving_License"])
<AxesSubplot:xlabel='Driving_License', ylabel='Density'>
- Spalte besteht nur aus 0 und 1
- Ein großer Teil der Kunden hat einen Führerschein
- Das Balkendiagramm kann verwendet werden, um eine klarere Darstellung zu erhalten.
Regionalcode¶
sns.distplot(numeric_data["Region_Code"])
<AxesSubplot:xlabel='Region_Code', ylabel='Density'>
- Alle Regionen sind mit einer Nummer gekennzeichnet
- Der Regionalcode bezeichnet einen eindeutigen Code für eine bestimmte Region
sns.distplot(raw_data[raw_data.Response == 0]["Region_Code"],
bins=10,
color='orange',
label='No',
kde=True)
sns.distplot(raw_data[raw_data.Response == 1]["Region_Code"],
bins=10,
color='blue',
label='Yes',
kde=True)
<AxesSubplot:xlabel='Region_Code', ylabel='Density'>
- Die Mehrheit der Kunden ist zwischen 25 und 30 Jahre alt.
- Kunden aus Regionen mit dem Code zwischen 25 und 30 haben ein großes Interesse an Zusatzversicherungen.
- Kunden aus Regionen mit einem Code zwischen 5 und 10 sind nicht sehr an einer Zusatzversicherung interessiert.
Previously_Insured¶
sns.distplot(numeric_data["Previously_Insured"])
<AxesSubplot:xlabel='Previously_Insured', ylabel='Density'>
- Kategoriales Attribut, da 1 eine "Ja"- und 0 eine "Nein"-Antwort darstellt.
sns.distplot(raw_data[raw_data.Response == 0]["Previously_Insured"],
bins=10,
color='orange',
label='No',
kde=True)
sns.distplot(raw_data[raw_data.Response == 1]["Previously_Insured"],
bins=10,
color='blue',
label='Yes',
kde=True)
<AxesSubplot:xlabel='Previously_Insured', ylabel='Density'>
- Kunden, die bereits eine Kfz-Versicherung haben, zeigen in der Regel kein Interesse an dem zusätzlichen Angebot.
Jahresprämie¶
sns.distplot(numeric_data["Annual_Premium"])
<AxesSubplot:xlabel='Annual_Premium', ylabel='Density'>
- Normalverteilung erkennbar
- Ausreißer erkennbar, Kurve flacht extrem stark nach rechts ab
- Der Kostenbereich, den ein Kunde für eine Premium-Mitgliedschaft bezahlt, liegt zwischen 0 und 100000.
sns.distplot(raw_data[raw_data.Response == 0]["Annual_Premium"],
bins=10,
color='orange',
label='No',
kde=True)
sns.distplot(raw_data[raw_data.Response == 1]["Annual_Premium"],
bins=10,
color='blue',
label='Yes',
kde=True)
<AxesSubplot:xlabel='Annual_Premium', ylabel='Density'>
Policy_Sales_Channel¶
sns.distplot(numeric_data["Policy_Sales_Channel"])
<AxesSubplot:xlabel='Policy_Sales_Channel', ylabel='Density'>
- Policy Sales Channel ist die Wahl des Kanals, über den der Kunde erreicht wird (z. B. per Telefon).
- Code 150 ist der Kanal mit der höchsten Kundenreichweite
sns.distplot(raw_data[raw_data.Response == 0]["Policy_Sales_Channel"],
bins=10,
color='orange',
label='No',
kde=True)
sns.distplot(raw_data[raw_data.Response == 1]["Policy_Sales_Channel"],
bins=10,
color='blue',
label='Yes',
kde=True)
<AxesSubplot:xlabel='Policy_Sales_Channel', ylabel='Density'>
- Die Kunden mit dem größten Interesse an einer zusätzlichen Kfz-Versicherung werden über die Kanäle 25 und 125 erreicht.
Vintage¶
sns.distplot(numeric_data["Vintage"])
<AxesSubplot:xlabel='Vintage', ylabel='Density'>
- Keine Normalverteilung erkennbar
- Keine Ausreißer erkennbar
- Kunden sind potentiell gleichmäßig über die einzelnen Tage verteilt.
sns.distplot(raw_data[raw_data.Response == 0]["Vintage"],
bins=10,
color='orange',
label='No',
kde=True)
sns.distplot(raw_data[raw_data.Response == 1]["Vintage"],
bins=10,
color='blue',
label='Yes',
kde=True)
<AxesSubplot:xlabel='Vintage', ylabel='Density'>
- Gleichmäßige Verteilung
- Kein Trend zu Zinsen erkennbar.
Response¶
sns.distplot(numeric_data["Response"])
<AxesSubplot:xlabel='Response', ylabel='Density'>
- Kategoriale Variable, verfügbar als numerische Variable (nur 0 und 1).
- 1 steht für Interesse, 0 steht für kein Interesse
- Ein großer Teil der Kunden hat kein Interesse an einer weiteren Versicherung
Korrelationsmatrix der numerischen Variablen¶
feature_corr = numeric_data.drop("Response", axis=1).corr()
sns.heatmap(feature_corr, annot=True, cmap='coolwarm')
<AxesSubplot:>
Die Matrix weist keine auffallend starken Korrelationen auf. Die Korrelationen Previously_Insured/Policy_Sales_Channel und Age/Annual_Premium sind mäßig korreliert.
2.4.2 Kategorische Attribute¶
Im Folgenden werden die kategorialen Attribute untersucht. Auch hier sollten die Attribute mit der Zielvariablen in Verbindung stehen. Aus dem vorangegangenen Kapitel wurden numerische Attribute ermittelt, die kategoriale Attribute enthalten (z.B. Response). Diese werden auch in dieser Analyse berücksichtigt.
Response (Ziel)¶
Zunächst wird die Verteilung der Zielvariablen "Response" überprüft.
# Generate pie chart for response
# Generate percentages
response_rate = numeric_data.Response.value_counts() / len(numeric_data.Response)
# Prepare plot
labels = 'no interest', 'interest'
fig, ax = plt.subplots()
ax.pie(response_rate, labels=labels, autopct='%.f%%')
ax.set_title('Interest in car insurance vs. no interest')
Text(0.5, 1.0, 'Interest in car insurance vs. no interest')
- Das Interesse an einer Kfz-Versicherung entspricht etwa 12 % des gesamten Datensatzes, während das Desinteresse etwa 88 % ausmacht.
- Es handelt sich also um einen unausgewogenen Datensatz und in der Bewertungsphase muss eine andere Metrik verwendet werden.
Geschlecht¶
sns.countplot(x="Gender", hue="Response", data=raw_data)
plt.show()
- 0 = "kein Interesse", 1 = "Interesse".
- Das Interesse zwischen den Geschlechtern ist fast gleich groß.
Vehicle_Age¶
sns.countplot(x="Vehicle_Age", hue="Response", data=raw_data)
plt.show()
- Betrachtet man das Verhältnis der Grafik, so erkennt man, dass das größte Interesse bei Fahrzeughaltern besteht, deren Auto älter als 2 Jahre ist.
Vehicle_Damage¶
sns.countplot(x="Vehicle_Damage", hue="Response", data=raw_data)
plt.show()
- Die Grafik zeigt, dass Kraftfahrzeughalter, deren Auto bereits einen Schaden hatte, stärker an einer Kfz-Versicherung interessiert sind.
- Besitzer von unfallfreien Fahrzeugen sind dagegen überhaupt nicht an einem Angebot interessiert.
3. Datenaufbereitung¶
3.1 Kunden-ID reduzieren¶
Zu Beginn kann die Spalte "id" bereits gelöscht werden. Eine Identifikationsnummer zur Einschätzung des Interesses einer Kundengruppe ist nicht notwendig. Daher kann diese direkt entfernt werden
# delete customer id
data_prep1 = raw_data.drop("id", axis = 1)
3.2 Kodierung von numerischen Variablen¶
Im Datensatz wurde festgestellt, dass bestimmte Spalten nur aus den numerischen Variablen 0 und 1 bestehen. Da es sich um Kategorien handelt (1 = Ja, 0 = Nein), muss die numerische Variable für eine neue Datenübersicht in eine kategorische Variable umgewandelt werden. Zu diesem Zweck wird die Funktion map() verwendet.
# Conversion of numeric variables into categorical variables
bin_var = ["Driving_License","Previously_Insured","Response"]
# Creation of the "categorical_convert" functoin to reference the numeric variables to categorical attributes.
def kategorisch_umwandeln(x):
return x.map({1:'Yes',0:'No'})
# Calling the "data_prep" table and adjusting the defined columns
data_prep1[bin_var]=data_prep1[bin_var].apply(kategorisch_umwandeln)
# View of the newly created table
data_prep1.head()
Gender | Age | Driving_License | Region_Code | Previously_Insured | Vehicle_Age | Vehicle_Damage | Annual_Premium | Policy_Sales_Channel | Vintage | Response | |
---|---|---|---|---|---|---|---|---|---|---|---|
0 | Male | 44 | Yes | 28.0 | No | > 2 Years | Yes | 40454.0 | 26.0 | 217 | Yes |
1 | Male | 76 | Yes | 3.0 | No | 1-2 Year | No | 33536.0 | 26.0 | 183 | No |
2 | Male | 47 | Yes | 28.0 | No | > 2 Years | Yes | 38294.0 | 26.0 | 27 | Yes |
3 | Male | 21 | Yes | 11.0 | Yes | < 1 Year | No | 28619.0 | 152.0 | 203 | No |
4 | Female | 29 | Yes | 41.0 | Yes | < 1 Year | No | 27496.0 | 152.0 | 39 | No |
3.2 Umgang mit Ausreißern¶
sns.distplot(numeric_data["Annual_Premium"])
<AxesSubplot:xlabel='Annual_Premium', ylabel='Density'>
# Deletion of 1 % of the maximum value
q = data_prep1['Annual_Premium'].quantile(0.99)
data_1 = data_prep1[data_prep1['Annual_Premium']<q]
# Output corrected distribution function
sns.distplot(data_1["Annual_Premium"])
<AxesSubplot:xlabel='Annual_Premium', ylabel='Density'>
- Anschließend werden 1 % von links entfernt, da viele Werte auf 0 hinweisen.
- Kosten von 0 sind also möglich, da auch viele Kunden über ein Angebot einer einjährigen, kostenlosen Mitgliedschaft verfügen können.
q = data_1['Annual_Premium'].quantile(0.01)
data_2 = data_1[data_1['Annual_Premium']>q]
# Output corrected distribution function
sns.distplot(data_2["Annual_Premium"])
<AxesSubplot:xlabel='Annual_Premium', ylabel='Density'>
Verteilungsfunktion ist erkennbar.
- Die Mehrheit der Kunden zahlt 30.000,00 Euro pro Jahr für das Abonnement.
3.3 Berücksichtigung der neuen kategorialen Attribute "Response", "Driving License" und "Previously Insured"¶
response_rate = data_2.Response.value_counts() / len(data_2.Response)
# Prepare plot
labels = 'no interest', 'interest'
fig, ax = plt.subplots()
ax.pie(response_rate, labels=labels, autopct='%.f%%')
ax.set_title('Interest in car insurance vs. no interest')
Text(0.5, 1.0, 'Interest in car insurance vs. no interest')
Das Balkendiagramm für "Driving License" wird nun berücksichtigt.
sns.countplot(x="Driving_License", hue="Response", data=data_2)
plt.show()
Aus diesem Diagramm geht hervor, dass nur Kunden von Interesse sind, die auch im Besitz eines Führerscheins sind.
Im folgenden Diagramm betrachten wir Kunden, die bereits versichert sind.
sns.countplot(x="Previously_Insured", hue="Response", data=data_2)
plt.show()
Das Diagramm zeigt, dass Kunden, die bereits versichert sind, kein Interesse an zusätzlicher Versicherung haben.
Erneute Überprüfung der Korrelationsmatrix¶
# upload all numeric attributes
numeric_data = data_2.select_dtypes(include=[np.number])
feature_corr = numeric_data.corr()
sns.heatmap(feature_corr, annot=True, cmap='coolwarm')
<AxesSubplot:>
- Mit dem erneuten Blick werden nun mäßig starke Korrelationen untersucht.
- Es liegen keine starken Korrelationen vor.
3.4 Test auf Linearität¶
Nun wird die Linearität überprüft. Vintage wird als neue erklärende Variable hinzugefügt. Wir möchten die Dauer der Bindung an das Unternehmen mit anderen Variablen vergleichen.
sns.scatterplot(data=data_2, x="Vintage", y="Annual_Premium", hue="Response")
<AxesSubplot:xlabel='Vintage', ylabel='Annual_Premium'>
- Keine Linearität.
- Es scheint, dass Kunden, die schon länger beim Unternehmen sind und hohe Kosten haben, ein höheres Interesse an einer Zusatzversicherung haben (was auf wohlhabende Personen hindeutet).
sns.scatterplot(data=data_2, x="Vintage", y="Age", hue="Response")
<AxesSubplot:xlabel='Vintage', ylabel='Age'>
Hier ist zu sehen, dass Kunden zwischen 30 und 50 Jahren ein höheres Interesse aufrechterhalten. Vor allem Kunden, die seit 0 bis 100 Tagen beim Unternehmen sind, scheinen interessiert zu sein. Auch hier ist keine Linearität erkennbar.
sns.scatterplot(data=data_2, x="Vintage", y="Policy_Sales_Channel", hue="Response")
<AxesSubplot:xlabel='Vintage', ylabel='Policy_Sales_Channel'>
- Keine Linearität.
- Kunden, die über Vertriebskanal 40 und 120 erreicht wurden, zeigen signifikant höheres Interesse. Für Vertriebskanal 40 ist eine blaue Masse im Bereich zwischen 200 und 300 zu sehen.
sns.scatterplot(data=data_2, x="Vintage", y="Region_Code", hue="Response")
<AxesSubplot:xlabel='Vintage', ylabel='Region_Code'>
- Keine erkennbare Linearität.
- Regionen zwischen 30 und 40 mit Zugehörigkeit zwischen 100 und 200 Tagen zeigen häufiger Interesse.
Es wurde eine Logarithmisierung durchgeführt, um zu sehen, ob eine engere Linearität erzeugt werden kann. Dies war ein Testlauf und beschrieb keine Linearität in den folgenden Daten.
# create table for testing
newTest = data_2.copy()
# generate values
log_vintage = np.log(newTest['Vintage'])
# add column in newTest
newTest['log_vintage'] = log_vintage
# show table
newTest.head()
Gender | Age | Driving_License | Region_Code | Previously_Insured | Vehicle_Age | Vehicle_Damage | Annual_Premium | Policy_Sales_Channel | Vintage | Response | log_vintage | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | Male | 44 | Yes | 28.0 | No | > 2 Years | Yes | 40454.0 | 26.0 | 217 | Yes | 5.379897 |
1 | Male | 76 | Yes | 3.0 | No | 1-2 Year | No | 33536.0 | 26.0 | 183 | No | 5.209486 |
2 | Male | 47 | Yes | 28.0 | No | > 2 Years | Yes | 38294.0 | 26.0 | 27 | Yes | 3.295837 |
3 | Male | 21 | Yes | 11.0 | Yes | < 1 Year | No | 28619.0 | 152.0 | 203 | No | 5.313206 |
4 | Female | 29 | Yes | 41.0 | Yes | < 1 Year | No | 27496.0 | 152.0 | 39 | No | 3.663562 |
sns.scatterplot(data=newTest, x="log_vintage", y="Age", hue="Response")
<AxesSubplot:xlabel='log_vintage', ylabel='Age'>
sns.scatterplot(data=newTest, x="log_vintage", y="Annual_Premium", hue="Response")
<AxesSubplot:xlabel='log_vintage', ylabel='Annual_Premium'>
sns.scatterplot(data=newTest, x="log_vintage", y="Policy_Sales_Channel", hue="Response")
<AxesSubplot:xlabel='log_vintage', ylabel='Policy_Sales_Channel'>
sns.scatterplot(data=newTest, x="log_vintage", y="Region_Code", hue="Response")
<AxesSubplot:xlabel='log_vintage', ylabel='Region_Code'>
3.5 Zurücksetzen des Index¶
Nachdem die Ausreißer entfernt wurden, soll eine neue Tabelle bzw. ein neuer Index erstellt werden, um die entfernten Werte aus der Tabelle zu bereinigen. Mit drop=True
wird schließlich ein neuer Index festgelegt, wobei der alte Index entfernt wird.
# safe data_2 as NewDataframeName
NewDataframeName = data_2.reset_index(drop = True)
# show new descriptive statistic of NewDataframeName
NewDataframeName.describe(include='all')
Gender | Age | Driving_License | Region_Code | Previously_Insured | Vehicle_Age | Vehicle_Damage | Annual_Premium | Policy_Sales_Channel | Vintage | Response | |
---|---|---|---|---|---|---|---|---|---|---|---|
count | 312419 | 312419.000000 | 312419 | 312419.00000 | 312419 | 312419 | 312419 | 312419.000000 | 312419.000000 | 312419.000000 | 312419 |
unique | 2 | NaN | 2 | NaN | 2 | 3 | 2 | NaN | NaN | NaN | 2 |
top | Male | NaN | Yes | NaN | No | 1-2 Year | No | NaN | NaN | NaN | No |
freq | 167009 | NaN | 311745 | NaN | 162899 | 156656 | 160072 | NaN | NaN | NaN | 274776 |
mean | NaN | 38.318009 | NaN | 26.44119 | NaN | NaN | NaN | 35572.496465 | 111.836918 | 154.257916 | NaN |
std | NaN | 15.572843 | NaN | 12.92607 | NaN | NaN | NaN | 10001.929798 | 53.874091 | 83.672545 | NaN |
min | NaN | 20.000000 | NaN | 0.00000 | NaN | NaN | NaN | 6098.000000 | 1.000000 | 10.000000 | NaN |
25% | NaN | 24.000000 | NaN | 15.00000 | NaN | NaN | NaN | 28418.000000 | 29.000000 | 82.000000 | NaN |
50% | NaN | 35.000000 | NaN | 28.00000 | NaN | NaN | NaN | 33844.000000 | 125.000000 | 154.000000 | NaN |
75% | NaN | 49.000000 | NaN | 35.00000 | NaN | NaN | NaN | 40984.000000 | 152.000000 | 227.000000 | NaN |
max | NaN | 85.000000 | NaN | 52.00000 | NaN | NaN | NaN | 72959.000000 | 163.000000 | 299.000000 | NaN |
3.6 Umcodierung kategorialer Variablen¶
Für das weitere Vorgehen zur Überprüfung der Multikollinearität wird eine Codierung der numerischen Attribute durchgeführt.
# Convert binary variables to 1 and 0 with Yes and No
bin_var = ["Driving_License","Previously_Insured","Response"]
def binaer_umwandeln(x):
return x.map({'Yes':1,'No':0})
NewDataframeName[bin_var]=NewDataframeName[bin_var].apply(binaer_umwandeln)
NewDataframeName.head()
NewDataframeName.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 312419 entries, 0 to 312418 Data columns (total 11 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 Gender 312419 non-null object 1 Age 312419 non-null int64 2 Driving_License 312419 non-null int64 3 Region_Code 312419 non-null float64 4 Previously_Insured 312419 non-null int64 5 Vehicle_Age 312419 non-null object 6 Vehicle_Damage 312419 non-null object 7 Annual_Premium 312419 non-null float64 8 Policy_Sales_Channel 312419 non-null float64 9 Vintage 312419 non-null int64 10 Response 312419 non-null int64 dtypes: float64(3), int64(5), object(3) memory usage: 26.2+ MB
# create dummy-variables
data_enc = pd.get_dummies(NewDataframeName, drop_first=True)
data_enc.head()
Age | Driving_License | Region_Code | Previously_Insured | Annual_Premium | Policy_Sales_Channel | Vintage | Response | Gender_Male | Vehicle_Age_< 1 Year | Vehicle_Age_> 2 Years | Vehicle_Damage_Yes | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 44 | 1 | 28.0 | 0 | 40454.0 | 26.0 | 217 | 1 | 1 | 0 | 1 | 1 |
1 | 76 | 1 | 3.0 | 0 | 33536.0 | 26.0 | 183 | 0 | 1 | 0 | 0 | 0 |
2 | 47 | 1 | 28.0 | 0 | 38294.0 | 26.0 | 27 | 1 | 1 | 0 | 1 | 1 |
3 | 21 | 1 | 11.0 | 1 | 28619.0 | 152.0 | 203 | 0 | 1 | 1 | 0 | 0 |
4 | 29 | 1 | 41.0 | 1 | 27496.0 | 152.0 | 39 | 0 | 0 | 1 | 0 | 0 |
data_enc.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 312419 entries, 0 to 312418 Data columns (total 12 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 Age 312419 non-null int64 1 Driving_License 312419 non-null int64 2 Region_Code 312419 non-null float64 3 Previously_Insured 312419 non-null int64 4 Annual_Premium 312419 non-null float64 5 Policy_Sales_Channel 312419 non-null float64 6 Vintage 312419 non-null int64 7 Response 312419 non-null int64 8 Gender_Male 312419 non-null uint8 9 Vehicle_Age_< 1 Year 312419 non-null uint8 10 Vehicle_Age_> 2 Years 312419 non-null uint8 11 Vehicle_Damage_Yes 312419 non-null uint8 dtypes: float64(3), int64(5), uint8(4) memory usage: 20.3 MB
In der aktualisierten Ansicht werden jetzt keine Objekte mehr angezeigt, die Umwandlung in Dummy-Variablen wurde abgeschlossen.
3.7 Test auf Multikollinearität¶
Um sicherzustellen, dass die spätere Regression korrekt funktioniert, dürfen keine Multikollinearitäten zwischen den Variablen vorhanden sein. Die Anwesenheit solcher wird mithilfe der Bibliothek Statsmodel überprüft.
# independent variables
vif_test = data_enc.drop("Response", axis=1)
# VIF dataframe
vif_data = pd.DataFrame()
vif_data["feature"] = vif_test.columns
# VIF for each Feature
vif_data["VIF"] = [variance_inflation_factor(vif_test.values, i)
for i in range(len(vif_test.columns))]
print(vif_data)
feature VIF 0 Age 19.804783 1 Driving_License 62.984075 2 Region_Code 5.164535 3 Previously_Insured 6.556592 4 Annual_Premium 14.201152 5 Policy_Sales_Channel 9.017606 6 Vintage 4.371791 7 Gender_Male 2.211746 8 Vehicle_Age_< 1 Year 5.939466 9 Vehicle_Age_> 2 Years 1.133583 10 Vehicle_Damage_Yes 6.709365
Die Variable "Driving_License" hat den höchsten VIF-Wert und wird aus dem Datensatz entfernt.
data_enc.drop("Driving_License", axis=1, inplace=True)
# unabhängige Variablen
vif_test = data_enc.drop("Response", axis=1)
# VIF dataframe
vif_data = pd.DataFrame()
vif_data["feature"] = vif_test.columns
# VIF für jedes Feature
vif_data["VIF"] = [variance_inflation_factor(vif_test.values, i)
for i in range(len(vif_test.columns))]
print(vif_data)
feature VIF 0 Age 12.623381 1 Region_Code 4.864616 2 Previously_Insured 5.866290 3 Annual_Premium 12.116569 4 Policy_Sales_Channel 7.628331 5 Vintage 4.201412 6 Gender_Male 2.182362 7 Vehicle_Age_< 1 Year 4.934602 8 Vehicle_Age_> 2 Years 1.128678 9 Vehicle_Damage_Yes 5.795290
Die Variable "Age" hat den höchsten VIF-Wert und wird aus dem Datensatz entfernt.Age has high multicollinearity (VIF>10) and, as a result, is also removed.
data_enc.drop("Age", axis=1, inplace=True)
--------------------------------------------------------------------------- KeyError Traceback (most recent call last) <ipython-input-66-bc47fec320bd> in <module> ----> 1 data_enc.drop("Age", axis=1, inplace=True) ~\Anaconda3\lib\site-packages\pandas\core\frame.py in drop(self, labels, axis, index, columns, level, inplace, errors) 4161 weight 1.0 0.8 4162 """ -> 4163 return super().drop( 4164 labels=labels, 4165 axis=axis, ~\Anaconda3\lib\site-packages\pandas\core\generic.py in drop(self, labels, axis, index, columns, level, inplace, errors) 3885 for axis, labels in axes.items(): 3886 if labels is not None: -> 3887 obj = obj._drop_axis(labels, axis, level=level, errors=errors) 3888 3889 if inplace: ~\Anaconda3\lib\site-packages\pandas\core\generic.py in _drop_axis(self, labels, axis, level, errors) 3919 new_axis = axis.drop(labels, level=level, errors=errors) 3920 else: -> 3921 new_axis = axis.drop(labels, errors=errors) 3922 result = self.reindex(**{axis_name: new_axis}) 3923 ~\Anaconda3\lib\site-packages\pandas\core\indexes\base.py in drop(self, labels, errors) 5280 if mask.any(): 5281 if errors != "ignore": -> 5282 raise KeyError(f"{labels[mask]} not found in axis") 5283 indexer = indexer[~mask] 5284 return self.delete(indexer) KeyError: "['Age'] not found in axis"
# unabhängige Variablen
vif_test = data_enc.drop("Response", axis=1)
# VIF dataframe
vif_data = pd.DataFrame()
vif_data["feature"] = vif_test.columns
# VIF für jedes Feature
vif_data["VIF"] = [variance_inflation_factor(vif_test.values, i)
for i in range(len(vif_test.columns))]
print(vif_data)
feature VIF 0 Region_Code 4.593110 1 Previously_Insured 5.080591 2 Annual_Premium 9.093076 3 Policy_Sales_Channel 7.576390 4 Vintage 4.058008 5 Gender_Male 2.143373 6 Vehicle_Age_< 1 Year 3.376007 7 Vehicle_Age_> 2 Years 1.118402 8 Vehicle_Damage_Yes 5.107305
Keine der Variablen hat nun einen VIF größer als 10
3.8 Merkmalsskalierung¶
y = data_enc["Response"]
X = data_enc.drop(labels = ["Response"], axis = 1)
# scaling the variables
num_features = ['Vintage', 'Region_Code', 'Annual_Premium', 'Policy_Sales_Channel']
scaler = StandardScaler()
X[num_features] = scaler.fit_transform(X[num_features])
X.head()
Region_Code | Previously_Insured | Annual_Premium | Policy_Sales_Channel | Vintage | Gender_Male | Vehicle_Age_< 1 Year | Vehicle_Age_> 2 Years | Vehicle_Damage_Yes | |
---|---|---|---|---|---|---|---|---|---|
0 | 0.120594 | 0 | 0.488057 | -1.59329 | 0.749854 | 1 | 0 | 1 | 1 |
1 | -1.813484 | 0 | -0.203611 | -1.59329 | 0.343507 | 1 | 0 | 0 | 0 |
2 | 0.120594 | 0 | 0.272098 | -1.59329 | -1.520907 | 1 | 0 | 1 | 1 |
3 | -1.194579 | 1 | -0.695217 | 0.74550 | 0.582535 | 1 | 1 | 0 | 0 |
4 | 1.126316 | 1 | -0.807495 | 0.74550 | -1.377490 | 0 | 1 | 0 | 0 |
3.9 Trainings- & Testdaten erzeugen¶
# Split data into test and training data set
# The default value of 80% to 20% is used
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=110)
4. Modellierung und Evaluation¶
Die Modellierung wurde mithilfe des Jupyter-Notebooks "E11_Churn_Solution" durchgeführt. Die Interpretationen wurden individuell vorgenommen.
4.1 Logistische Regression¶
Die logistische Regression ist ein nicht-lineares Modell. Sie arbeitet mit kategorialen Zielwerten (abhängige Variable). Sie wird verwendet, um "Ja"/"Nein" (0/1)-Entscheidungen vorherzusagen.
4.1.1 Training und Vorhersage¶
# add constant
X_const = sm.add_constant(X_train)
# create model
log_reg = sm.Logit(y_train, X_const).fit()
print(log_reg.summary())
Optimization terminated successfully. Current function value: 0.269500 Iterations 11 Logit Regression Results ============================================================================== Dep. Variable: Response No. Observations: 234314 Model: Logit Df Residuals: 234304 Method: MLE Df Model: 9 Date: Fri, 05 Nov 2021 Pseudo R-squ.: 0.2666 Time: 15:08:20 Log-Likelihood: -63148. converged: True LL-Null: -86106. Covariance Type: nonrobust LLR p-value: 0.000 ========================================================================================= coef std err z P>|z| [0.025 0.975] ----------------------------------------------------------------------------------------- const -3.1757 0.049 -65.161 0.000 -3.271 -3.080 Region_Code -0.0060 0.008 -0.790 0.430 -0.021 0.009 Previously_Insured -3.9362 0.107 -36.695 0.000 -4.146 -3.726 Annual_Premium -0.0251 0.007 -3.582 0.000 -0.039 -0.011 Policy_Sales_Channel -0.0726 0.008 -9.634 0.000 -0.087 -0.058 Vintage 0.0080 0.007 1.153 0.249 -0.006 0.022 Gender_Male 0.0965 0.014 6.750 0.000 0.069 0.125 Vehicle_Age_< 1 Year -0.6888 0.020 -33.883 0.000 -0.729 -0.649 Vehicle_Age_> 2 Years 0.1093 0.024 4.620 0.000 0.063 0.156 Vehicle_Damage_Yes 2.1595 0.048 44.976 0.000 2.065 2.254 =========================================================================================
Wenn das P-Wert (P>|z|) für eine Variable größer als 0,05 ist und es sich nicht um die Konstante handelt, bedeutet dies, dass die Variable statistisch nicht signifikant ist. Die statistisch nicht relevanten Merkmale werden entfernt.
# Removing the statistically non-significant features (P>|z|> 0.05)
insignificant_features = ["Region_Code","Vintage"]
X_train.drop(insignificant_features, axis=1, inplace=True)
X_test.drop(insignificant_features, axis=1, inplace=True)
Ein zweites Modell kreieren:
# New model
X_const = sm.add_constant(X_train)
log_reg2 = sm.Logit(y_train, X_const).fit()
print(log_reg2.summary())
Optimization terminated successfully. Current function value: 0.269504 Iterations 11 Logit Regression Results ============================================================================== Dep. Variable: Response No. Observations: 234314 Model: Logit Df Residuals: 234306 Method: MLE Df Model: 7 Date: Fri, 05 Nov 2021 Pseudo R-squ.: 0.2666 Time: 15:09:13 Log-Likelihood: -63149. converged: True LL-Null: -86106. Covariance Type: nonrobust LLR p-value: 0.000 ========================================================================================= coef std err z P>|z| [0.025 0.975] ----------------------------------------------------------------------------------------- const -3.1759 0.049 -65.166 0.000 -3.271 -3.080 Previously_Insured -3.9361 0.107 -36.695 0.000 -4.146 -3.726 Annual_Premium -0.0248 0.007 -3.551 0.000 -0.039 -0.011 Policy_Sales_Channel -0.0724 0.008 -9.614 0.000 -0.087 -0.058 Gender_Male 0.0967 0.014 6.761 0.000 0.069 0.125 Vehicle_Age_< 1 Year -0.6885 0.020 -33.872 0.000 -0.728 -0.649 Vehicle_Age_> 2 Years 0.1092 0.024 4.617 0.000 0.063 0.156 Vehicle_Damage_Yes 2.1593 0.048 44.974 0.000 2.065 2.253 =========================================================================================
Es gibt keine weiteren statistisch nicht signifikanten Variablen mehr. Das endgültige Modell wurde modelliert.
# final model
X_const = sm.add_constant(X_train)
log_reg_final = sm.Logit(y_train, X_const).fit()
print(log_reg_final.summary())
Optimization terminated successfully. Current function value: 0.269504 Iterations 11 Logit Regression Results ============================================================================== Dep. Variable: Response No. Observations: 234314 Model: Logit Df Residuals: 234306 Method: MLE Df Model: 7 Date: Fri, 05 Nov 2021 Pseudo R-squ.: 0.2666 Time: 15:10:06 Log-Likelihood: -63149. converged: True LL-Null: -86106. Covariance Type: nonrobust LLR p-value: 0.000 ========================================================================================= coef std err z P>|z| [0.025 0.975] ----------------------------------------------------------------------------------------- const -3.1759 0.049 -65.166 0.000 -3.271 -3.080 Previously_Insured -3.9361 0.107 -36.695 0.000 -4.146 -3.726 Annual_Premium -0.0248 0.007 -3.551 0.000 -0.039 -0.011 Policy_Sales_Channel -0.0724 0.008 -9.614 0.000 -0.087 -0.058 Gender_Male 0.0967 0.014 6.761 0.000 0.069 0.125 Vehicle_Age_< 1 Year -0.6885 0.020 -33.872 0.000 -0.728 -0.649 Vehicle_Age_> 2 Years 0.1092 0.024 4.617 0.000 0.063 0.156 Vehicle_Damage_Yes 2.1593 0.048 44.974 0.000 2.065 2.253 =========================================================================================
# Prediction
y_hat = log_reg_final.predict(sm.add_constant(X_test))
# Statsmodel only gives the probabilities, therefore rounding is required.
prediction = list(map(round, y_hat))
4.2 Evaluation¶
Um mehrere Metriken für die Bewertung zu verwenden, kann das Modell bequemer mit Scikit-Learn erstellt werden. Daher wird das identische Modell wie mit Statsmodels erneut in Scikit-Learn generiert.
4.2.1 Training und Vorhersage¶
logistic_model = LogisticRegression(random_state=0, C=1e8)
# prediction with testdata
result = logistic_model.fit(X_train,y_train)
prediction_test = logistic_model.predict(X_test)
prediction_train = logistic_model.predict(X_train)
4.2.2 Evaluation¶
# Accuracy Score
acc = metrics.accuracy_score(y_test, prediction_test)
print('Accuracy with testdata: {}'.format(acc))
Accuracy with testdata: 0.8788809935343448
Die Genauigkeit deutet auf ein überdurchschnittliches Modell hin. Allerdings handelt es sich um ein unausgeglichenes Datenset. Daher müssen weitere Metriken analysiert werden.
# classification report
print("Training:")
print(classification_report(y_train,prediction_train))
print("Test:")
print(classification_report(y_test,prediction_test))
Training: precision recall f1-score support 0 0.88 1.00 0.94 206131 1 0.00 0.00 0.00 28183 accuracy 0.88 234314 macro avg 0.44 0.50 0.47 234314 weighted avg 0.77 0.88 0.82 234314 Test: precision recall f1-score support 0 0.88 1.00 0.94 68645 1 0.00 0.00 0.00 9460 accuracy 0.88 78105 macro avg 0.44 0.50 0.47 78105 weighted avg 0.77 0.88 0.82 78105
Die Genauigkeiten sind für das Trainings- und Testdatenset identisch. Insgesamt sind die Werte für die Test- und Trainingsdatensets sehr ähnlich. Daher kann weder Überanpassung noch Unteranpassung angenommen werden.
# Confusion matrix testdata
cm = confusion_matrix(y_test,prediction_test)
df_cm = pd.DataFrame(cm, index=['No Interest','Interest'], columns=['No Interest', 'Interest'],)
fig = plt.figure(figsize=[10,7])
heatmap = sns.heatmap(df_cm, annot=True, fmt="d")
heatmap.yaxis.set_ticklabels(heatmap.yaxis.get_ticklabels(), rotation=0, ha='right', fontsize=14)
heatmap.xaxis.set_ticklabels(heatmap.xaxis.get_ticklabels(), rotation=45, ha='right', fontsize=14)
plt.ylabel('True label')
plt.xlabel('Predicted label')
Text(0.5, 39.5, 'Predicted label')
# metrics of confusion matrix
tn, fp, fn, tp = cm.ravel()
recall = tp/(fn+tp)
precision = tp/(tp+fp)
print("True Negatives: " + str(tn))
print("False Positives: " + str(fp))
print("False Negatives: " + str(fn))
print("True Positives: " + str(tp))
print("Recall: " + str(recall))
print("Precision: " + str(precision))
True Negatives: 68645 False Positives: 0 False Negatives: 9460 True Positives: 0 Recall: 0.0 Precision: nan
Der Rückrufwert ist zu niedrig und bietet daher keine aussagekräftige Darstellung. Um die Präzision und die Rückrufwerte anzeigen und bewerten zu können, ist eine Verbesserung mithilfe der Schwellenwerte erforderlich. Die Rückrufwerte müssen erhöht werden.
Es scheint, dass das Modul 'sklearn.metrics' keine 'plot_roc_curve'-Attribute hat. -
- Wenn dieser Fehler auftritt, überprüfen Sie bitte die Versionierung.
- Leider brachte die Fehleranalyse keinen Erfolg.
- Aufgrund des massiven Aufwands und des Risikos von Datenverlust wurde der Fehler nicht behoben.
# ROC curve, AUC
fig, ax = plt.subplots(figsize=(8,6))
ax.set_title('ROC curve')
plot = metrics.plot_roc_curve(logistic_model, X_test, y_test, ax=ax);
ax.plot([0,1], [0,1], '--');
4.3 Interpretation¶
Zuerst werden wir jedoch die Ergebnisse für das Geschäft illustrieren und klären, welche Interesse bei einem Kunden wecken und welches dieses verringern.
# Read out regression coefficients and thus find out importance of individual attributes
weights = pd.Series(logistic_model.coef_[0],
index=X_train.columns.values)
weights.sort_values(ascending = False)
Vehicle_Damage_Yes 2.159262 Vehicle_Age_> 2 Years 0.109212 Gender_Male 0.096729 Annual_Premium -0.024813 Policy_Sales_Channel -0.072448 Vehicle_Age_< 1 Year -0.688410 Previously_Insured -3.936097 dtype: float64
# Graphical presentation of the most important features that increase the interest of an additional motor vehicle insurance:
weights = pd.Series(logistic_model.coef_[0],
index=X_train.columns.values)
print (weights.sort_values(ascending = False)[:4].plot(kind='bar'))
AxesSubplot(0.125,0.125;0.775x0.755)
Die drei Merkmale, die das Interesse des Kunden steigern, sind:
- Wenn der Kunde bereits einen Unfall hatte (Fahrzeug_Schaden_Ja)
- Wenn der Kunde männlich ist (Geschlecht_Männlich)
- Wenn das Auto des Kunden älter als 2 Jahre ist (Fahrzeug_Alt_>2Jahre)
# Most important features that reduce interest
print(weights.sort_values(ascending = False)[-3:].plot(kind='bar'))
AxesSubplot(0.125,0.125;0.775x0.755)
Die drei Merkmale, die das Interesse des Kunden verringern, sind:
- Vertriebskanal, über den der Kunde angesprochen wird (Policy_Sales_Channel).
- Wenn das Auto weniger als 1 Jahr alt ist (Fahrzeug_Alt_<1Jahr)
- Wenn der Kunde bereits eine Versicherungspolice hat (Zuvor_Versichert)
4.4. Modell Optimierung¶
Die Rückrufrate ist als Zielmetrik zu niedrig und muss daher erhöht werden. Daher werden die Metriken bei verschiedenen Schwellenwerten der logistischen Regression analysiert.
# Testing the metrics at different thresholds
threshold_list = [0.05,0.1,0.15,0.2,0.25,0.3,0.35,0.4,0.45,0.5,0.55,0.6,0.65,.7,.75,.8,.85,.9,.95,.99]
pred_proba_df = y_hat
for i in threshold_list:
print ('\n******** For a threshold value of {} ******'.format(i))
# Round up if value is above threshold
y_test_pred = pred_proba_df.apply(lambda x: 1 if x>i else 0)
# print metrics
test_accuracy = metrics.accuracy_score(y_test, y_test_pred)
print("Accuracy: {}".format(test_accuracy))
# confusion matrix
c = confusion_matrix(y_test, y_test_pred)
tn, fp, fn, tp = c.ravel()
recall = tp/(fn+tp)
precision = tp/(tp+fp)
# print relevant metrics
print("True Negatives: " + str(tn))
print("False Positives: " + str(fp))
print("False Negatives: " + str(fn))
print("True Positives: " + str(tp))
print("Recall: " + str(recall))
print("Precision: " + str(precision))
******** For a threshold value of 0.05 ****** Accuracy: 0.6497023237948915 True Negatives: 41462 False Positives: 27183 False Negatives: 177 True Positives: 9283 Recall: 0.9812896405919662 Precision: 0.25456589699994514 ******** For a threshold value of 0.1 ****** Accuracy: 0.6512515203892196 True Negatives: 41587 False Positives: 27058 False Negatives: 181 True Positives: 9279 Recall: 0.9808668076109937 Precision: 0.25535955087101303 ******** For a threshold value of 0.15 ****** Accuracy: 0.6876640419947506 True Negatives: 44877 False Positives: 23768 False Negatives: 627 True Positives: 8833 Recall: 0.9337209302325581 Precision: 0.2709426091224196 ******** For a threshold value of 0.2 ****** Accuracy: 0.7325523333973497 True Negatives: 49380 False Positives: 19265 False Negatives: 1624 True Positives: 7836 Recall: 0.8283298097251586 Precision: 0.28914062211726504 ******** For a threshold value of 0.25 ****** Accuracy: 0.733781448050701 True Negatives: 49540 False Positives: 19105 False Negatives: 1688 True Positives: 7772 Recall: 0.8215644820295983 Precision: 0.28916917810767573 ******** For a threshold value of 0.3 ****** Accuracy: 0.8367198002688688 True Negatives: 62809 False Positives: 5836 False Negatives: 6917 True Positives: 2543 Recall: 0.268816067653277 Precision: 0.3034968373314238 ******** For a threshold value of 0.35 ****** Accuracy: 0.8788809935343448 True Negatives: 68645 False Positives: 0 False Negatives: 9460 True Positives: 0 Recall: 0.0 Precision: nan ******** For a threshold value of 0.4 ****** Accuracy: 0.8788809935343448 True Negatives: 68645 False Positives: 0 False Negatives: 9460 True Positives: 0 Recall: 0.0 Precision: nan ******** For a threshold value of 0.45 ****** Accuracy: 0.8788809935343448 True Negatives: 68645 False Positives: 0 False Negatives: 9460 True Positives: 0 Recall: 0.0 Precision: nan ******** For a threshold value of 0.5 ****** Accuracy: 0.8788809935343448 True Negatives: 68645 False Positives: 0 False Negatives: 9460 True Positives: 0 Recall: 0.0 Precision: nan ******** For a threshold value of 0.55 ****** Accuracy: 0.8788809935343448 True Negatives: 68645 False Positives: 0 False Negatives: 9460 True Positives: 0 Recall: 0.0 Precision: nan ******** For a threshold value of 0.6 ****** Accuracy: 0.8788809935343448 True Negatives: 68645 False Positives: 0 False Negatives: 9460 True Positives: 0 Recall: 0.0 Precision: nan ******** For a threshold value of 0.65 ****** Accuracy: 0.8788809935343448 True Negatives: 68645 False Positives: 0 False Negatives: 9460 True Positives: 0 Recall: 0.0 Precision: nan ******** For a threshold value of 0.7 ****** Accuracy: 0.8788809935343448 True Negatives: 68645 False Positives: 0 False Negatives: 9460 True Positives: 0 Recall: 0.0 Precision: nan ******** For a threshold value of 0.75 ****** Accuracy: 0.8788809935343448 True Negatives: 68645 False Positives: 0 False Negatives: 9460 True Positives: 0 Recall: 0.0 Precision: nan ******** For a threshold value of 0.8 ****** Accuracy: 0.8788809935343448 True Negatives: 68645 False Positives: 0 False Negatives: 9460 True Positives: 0 Recall: 0.0 Precision: nan ******** For a threshold value of 0.85 ****** Accuracy: 0.8788809935343448 True Negatives: 68645 False Positives: 0 False Negatives: 9460 True Positives: 0 Recall: 0.0 Precision: nan ******** For a threshold value of 0.9 ****** Accuracy: 0.8788809935343448 True Negatives: 68645 False Positives: 0 False Negatives: 9460 True Positives: 0 Recall: 0.0 Precision: nan ******** For a threshold value of 0.95 ****** Accuracy: 0.8788809935343448 True Negatives: 68645 False Positives: 0 False Negatives: 9460 True Positives: 0 Recall: 0.0 Precision: nan ******** For a threshold value of 0.99 ****** Accuracy: 0.8788809935343448 True Negatives: 68645 False Positives: 0 False Negatives: 9460 True Positives: 0 Recall: 0.0 Precision: nan
Ein Schwellenwert von 0,25 bietet ein besseres Ergebnis für die Anwendung. Er erhöht den Recall auf ein zufriedenstellendes Niveau von 73,37 % auf Kosten der Präzision. Allerdings ist die Präzision vernachlässigbar.
Das resultiert in die folgenden Werten:
# Threshold of 0.25 see above
y_test_pred = pred_proba_df.apply(lambda x: 1 if x>0.25 else 0)
test_accuracy = metrics.accuracy_score(y_test, y_test_pred)
c = confusion_matrix(y_test, y_test_pred)
# Read values from confusion matrix
tn, fp, fn, tp = c.ravel()
recall = tp/(fn+tp)
precision = tp/(tp+fp)
print(classification_report(y_test,y_test_pred))
# create confusion matrix
print("confusion matrix with new threshold:")
df_cm = pd.DataFrame(c, index=['No Interest','Interest'], columns=['No Interest', 'Interest'],)
fig = plt.figure(figsize=[10,7])
heatmap = sns.heatmap(df_cm, annot=True, fmt="d")
heatmap.yaxis.set_ticklabels(heatmap.yaxis.get_ticklabels(), rotation=0, ha='right', fontsize=14)
heatmap.xaxis.set_ticklabels(heatmap.xaxis.get_ticklabels(), rotation=45, ha='right', fontsize=14)
plt.ylabel('True label')
plt.xlabel('Predicted label')
plt.show()
print(" ")
# print metrics
print("Metrics for the new threshold:")
print("Accuracy: {}".format(test_accuracy))
print("True Negatives: " + str(tn))
print("False Positives: " + str(fp))
print("False Negatives: " + str(fn))
print("True Positives: " + str(tp))
print("Recall: " + str(recall))
print("Precision: " + str(precision))
precision recall f1-score support 0 0.97 0.72 0.83 68645 1 0.29 0.82 0.43 9460 accuracy 0.73 78105 macro avg 0.63 0.77 0.63 78105 weighted avg 0.88 0.73 0.78 78105 confusion matrix with new threshold:
Metrics for the new threshold: Accuracy: 0.733781448050701 True Negatives: 49540 False Positives: 19105 False Negatives: 1688 True Positives: 7772 Recall: 0.8215644820295983 Precision: 0.28916917810767573
Wie erwartet steigt die Rate der Kunden, die fälschlicherweise als interessiert eingestuft werden. Gleichzeitig steigt jedoch auch die Anzahl der Kunden, die korrekt als abwanderungswillig vorhergesagt werden (True Positives). Wie in der Seminararbeit erläutert, ist dies entscheidend, denn im Zweifelsfall würde ein Kunde fälschlicherweise vom Service-Team kontaktiert und könnte diesen Anruf sogar als guten Service wahrnehmen und langfristig an das Unternehmen gebunden werden.
5. Deployment¶
# Separate individual (scaled) customer
customer_df = X_test.iloc[42357]
# Overview of selected customers
customer_df
Previously_Insured 0.000000 Annual_Premium -0.424768 Policy_Sales_Channel -1.593290 Gender_Male 1.000000 Vehicle_Age_< 1 Year 0.000000 Vehicle_Age_> 2 Years 1.000000 Vehicle_Damage_Yes 1.000000 Name: 128831, dtype: float64
# Run Prediction
cust_pred = logistic_model.predict([customer_df])
# Interpret result
def check_prediction(pred):
if pred[0] == 1:
print("The customer is probably interested! Inform Customer Relationship Management!")
else:
print("The customer is probably not interested.")
check_prediction(cust_pred)
The customer is probably not interested.