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¶

In [1]:
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¶

In [2]:
# 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.

In [3]:
# print first 5 rows of the dataframe
raw_data.head()
Out[3]:
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.

In [4]:
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.

In [5]:
raw_data.isnull().sum()
Out[5]:
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.

In [6]:
raw_data[raw_data.duplicated(keep=False)]
Out[6]:
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.

In [7]:
raw_data.describe(include='all')
Out[7]:
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.

In [8]:
# upload all numeric attributes
numeric_data = raw_data.select_dtypes(include=[np.number])

Alter¶

In [9]:
sns.distplot(numeric_data["Age"])
Out[9]:
<AxesSubplot:xlabel='Age', ylabel='Density'>
No description has been provided for this image
  • 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
In [10]:
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)
Out[10]:
<AxesSubplot:xlabel='Age', ylabel='Density'>
No description has been provided for this image
  • 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.
In [11]:
plt.boxplot(numeric_data["Age"])
plt.show()
No description has been provided for this image

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¶

In [12]:
sns.distplot(numeric_data["Driving_License"])
Out[12]:
<AxesSubplot:xlabel='Driving_License', ylabel='Density'>
No description has been provided for this image
  • 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¶

In [13]:
sns.distplot(numeric_data["Region_Code"])
Out[13]:
<AxesSubplot:xlabel='Region_Code', ylabel='Density'>
No description has been provided for this image
  • Alle Regionen sind mit einer Nummer gekennzeichnet
  • Der Regionalcode bezeichnet einen eindeutigen Code für eine bestimmte Region
In [14]:
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)
Out[14]:
<AxesSubplot:xlabel='Region_Code', ylabel='Density'>
No description has been provided for this image
  • 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¶

In [15]:
sns.distplot(numeric_data["Previously_Insured"])
Out[15]:
<AxesSubplot:xlabel='Previously_Insured', ylabel='Density'>
No description has been provided for this image
  • Kategoriales Attribut, da 1 eine "Ja"- und 0 eine "Nein"-Antwort darstellt.
In [16]:
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)
Out[16]:
<AxesSubplot:xlabel='Previously_Insured', ylabel='Density'>
No description has been provided for this image
  • Kunden, die bereits eine Kfz-Versicherung haben, zeigen in der Regel kein Interesse an dem zusätzlichen Angebot.

Jahresprämie¶

In [17]:
sns.distplot(numeric_data["Annual_Premium"])
Out[17]:
<AxesSubplot:xlabel='Annual_Premium', ylabel='Density'>
No description has been provided for this image
  • 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.
In [18]:
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)
Out[18]:
<AxesSubplot:xlabel='Annual_Premium', ylabel='Density'>
No description has been provided for this image

Policy_Sales_Channel¶

In [19]:
sns.distplot(numeric_data["Policy_Sales_Channel"])
Out[19]:
<AxesSubplot:xlabel='Policy_Sales_Channel', ylabel='Density'>
No description has been provided for this image
  • 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
In [20]:
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)
Out[20]:
<AxesSubplot:xlabel='Policy_Sales_Channel', ylabel='Density'>
No description has been provided for this image
  • Die Kunden mit dem größten Interesse an einer zusätzlichen Kfz-Versicherung werden über die Kanäle 25 und 125 erreicht.

Vintage¶

In [21]:
sns.distplot(numeric_data["Vintage"])
Out[21]:
<AxesSubplot:xlabel='Vintage', ylabel='Density'>
No description has been provided for this image
  • Keine Normalverteilung erkennbar
  • Keine Ausreißer erkennbar
  • Kunden sind potentiell gleichmäßig über die einzelnen Tage verteilt.
In [22]:
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)
Out[22]:
<AxesSubplot:xlabel='Vintage', ylabel='Density'>
No description has been provided for this image
  • Gleichmäßige Verteilung
  • Kein Trend zu Zinsen erkennbar.

Response¶

In [23]:
sns.distplot(numeric_data["Response"])
Out[23]:
<AxesSubplot:xlabel='Response', ylabel='Density'>
No description has been provided for this image
  • 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¶

In [24]:
feature_corr = numeric_data.drop("Response", axis=1).corr()
sns.heatmap(feature_corr, annot=True, cmap='coolwarm')
Out[24]:
<AxesSubplot:>
No description has been provided for this image

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.

In [25]:
# 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')
Out[25]:
Text(0.5, 1.0, 'Interest in car insurance vs. no interest')
No description has been provided for this image
  • 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¶

In [26]:
sns.countplot(x="Gender", hue="Response", data=raw_data)
plt.show()
No description has been provided for this image
  • 0 = "kein Interesse", 1 = "Interesse".
  • Das Interesse zwischen den Geschlechtern ist fast gleich groß.

Vehicle_Age¶

In [27]:
sns.countplot(x="Vehicle_Age", hue="Response", data=raw_data)
plt.show()
No description has been provided for this image
  • 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¶

In [28]:
sns.countplot(x="Vehicle_Damage", hue="Response", data=raw_data)
plt.show()
No description has been provided for this image
  • 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

In [29]:
# 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.

In [30]:
# Conversion of numeric variables into categorical variables
bin_var = ["Driving_License","Previously_Insured","Response"]
In [31]:
# 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'})
In [32]:
# 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()
Out[32]:
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¶

In [33]:
sns.distplot(numeric_data["Annual_Premium"])
Out[33]:
<AxesSubplot:xlabel='Annual_Premium', ylabel='Density'>
No description has been provided for this image
In [34]:
# Deletion of 1 % of the maximum value
q = data_prep1['Annual_Premium'].quantile(0.99)
data_1 = data_prep1[data_prep1['Annual_Premium']<q]
In [35]:
# Output corrected distribution function
sns.distplot(data_1["Annual_Premium"])
Out[35]:
<AxesSubplot:xlabel='Annual_Premium', ylabel='Density'>
No description has been provided for this image
  • 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.
In [36]:
q = data_1['Annual_Premium'].quantile(0.01)
data_2 = data_1[data_1['Annual_Premium']>q]
In [37]:
# Output corrected distribution function
sns.distplot(data_2["Annual_Premium"])
Out[37]:
<AxesSubplot:xlabel='Annual_Premium', ylabel='Density'>
No description has been provided for this image

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"¶

In [38]:
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')
Out[38]:
Text(0.5, 1.0, 'Interest in car insurance vs. no interest')
No description has been provided for this image

Das Balkendiagramm für "Driving License" wird nun berücksichtigt.

In [39]:
sns.countplot(x="Driving_License", hue="Response", data=data_2)
plt.show()
No description has been provided for this image

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.

In [40]:
sns.countplot(x="Previously_Insured", hue="Response", data=data_2)
plt.show()
No description has been provided for this image

Das Diagramm zeigt, dass Kunden, die bereits versichert sind, kein Interesse an zusätzlicher Versicherung haben.

Erneute Überprüfung der Korrelationsmatrix¶

In [41]:
# upload all numeric attributes
numeric_data = data_2.select_dtypes(include=[np.number])
In [42]:
feature_corr = numeric_data.corr()
sns.heatmap(feature_corr, annot=True, cmap='coolwarm')
Out[42]:
<AxesSubplot:>
No description has been provided for this image
  • 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.

In [43]:
sns.scatterplot(data=data_2, x="Vintage", y="Annual_Premium", hue="Response")
Out[43]:
<AxesSubplot:xlabel='Vintage', ylabel='Annual_Premium'>
No description has been provided for this image
  • 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).
In [44]:
sns.scatterplot(data=data_2, x="Vintage", y="Age", hue="Response")
Out[44]:
<AxesSubplot:xlabel='Vintage', ylabel='Age'>
No description has been provided for this image

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.

In [45]:
sns.scatterplot(data=data_2, x="Vintage", y="Policy_Sales_Channel", hue="Response")
Out[45]:
<AxesSubplot:xlabel='Vintage', ylabel='Policy_Sales_Channel'>
No description has been provided for this image
  • 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.
In [46]:
sns.scatterplot(data=data_2, x="Vintage", y="Region_Code", hue="Response")
Out[46]:
<AxesSubplot:xlabel='Vintage', ylabel='Region_Code'>
No description has been provided for this image
  • 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.

In [47]:
# 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()
Out[47]:
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
In [48]:
sns.scatterplot(data=newTest, x="log_vintage", y="Age", hue="Response")
Out[48]:
<AxesSubplot:xlabel='log_vintage', ylabel='Age'>
No description has been provided for this image
In [49]:
sns.scatterplot(data=newTest, x="log_vintage", y="Annual_Premium", hue="Response")
Out[49]:
<AxesSubplot:xlabel='log_vintage', ylabel='Annual_Premium'>
No description has been provided for this image
In [50]:
sns.scatterplot(data=newTest, x="log_vintage", y="Policy_Sales_Channel", hue="Response")
Out[50]:
<AxesSubplot:xlabel='log_vintage', ylabel='Policy_Sales_Channel'>
No description has been provided for this image
In [51]:
sns.scatterplot(data=newTest, x="log_vintage", y="Region_Code", hue="Response")
Out[51]:
<AxesSubplot:xlabel='log_vintage', ylabel='Region_Code'>
No description has been provided for this image

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.

In [52]:
# safe data_2 as NewDataframeName
NewDataframeName = data_2.reset_index(drop = True)
#  show new descriptive statistic of NewDataframeName
NewDataframeName.describe(include='all')
Out[52]:
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.

In [53]:
# Convert binary variables to 1 and 0 with Yes and No
bin_var = ["Driving_License","Previously_Insured","Response"]
In [54]:
def binaer_umwandeln(x):
    return x.map({'Yes':1,'No':0})
In [55]:
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
In [56]:
# create dummy-variables
data_enc = pd.get_dummies(NewDataframeName, drop_first=True)
data_enc.head()
Out[56]:
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
In [57]:
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.

In [58]:
# 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.

In [59]:
data_enc.drop("Driving_License", axis=1, inplace=True)
In [60]:
  
# 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.

In [66]:
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"
In [65]:
# 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¶

In [67]:
y = data_enc["Response"]
X = data_enc.drop(labels = ["Response"], axis = 1)
In [68]:
# 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()
Out[68]:
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¶

In [69]:
# 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¶

In [71]:
# 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.

In [72]:
# 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:

In [73]:
# 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.

In [75]:
# 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
=========================================================================================
In [76]:
# 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¶

In [77]:
logistic_model = LogisticRegression(random_state=0, C=1e8)
In [78]:
# 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¶

In [80]:
# 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.

In [81]:
# 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.

In [82]:
# 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')
Out[82]:
Text(0.5, 39.5, 'Predicted label')
No description has been provided for this image
In [83]:
# 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.
In [84]:
# 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], '--');
No description has been provided for this image

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.

In [85]:
# 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)
Out[85]:
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
In [86]:
# 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)
No description has been provided for this image

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)
In [87]:
# Most important features that reduce interest
print(weights.sort_values(ascending = False)[-3:].plot(kind='bar'))
AxesSubplot(0.125,0.125;0.775x0.755)
No description has been provided for this image

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.

In [89]:
# 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:

In [90]:
# 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:
No description has been provided for this image
 
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¶

In [91]:
# Separate individual (scaled) customer
customer_df = X_test.iloc[42357]
In [92]:
# Overview of selected customers
customer_df
Out[92]:
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
In [93]:
# Run Prediction
cust_pred = logistic_model.predict([customer_df])
In [95]:
# 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.")
In [96]:
check_prediction(cust_pred)
The customer is probably not interested.