Inhaltsverzeichnis

1. Business Understanding

1.1 Projektbeschreibung

Das Forschungsteam ProInsurance wird damit beauftragt, dass Projekt Cross-Selling-Prediction für den Kunden NextGen Insurance durchzuführen. Der Kunde benötigt Hilfe bei der Erstellung eines Modells, mit dem sich vorhersagen lässt, ob die Versicherungsnehmer des letzten Jahres auch an einer angebotenen Kfz-Versicherung interessiert sein werden. Der Kunde wünscht die Durchführung des Projektes innerhalb eines knapp kalkulierten Zeitraums.

Zu diesem Zweck erhält das Forschungsteam von ihrem Auftraggeber einen Datenbestand bestehend aus > 350.000 Datensätzen. Zusätzlich ein Data Dictionary, welches eine kurze Beschreibung der Daten liefert.

Die NextGen Insurance hat mehrere Forschungsteams beauftragt an einer Lösung zu arbeiten, damit Sie sich nach Ende der Präsentationen für die beste Alternative entscheiden können.

1.2 Data Dictionary des "train"- Datensatz

Unser Auftraggeber die NextGen Insurance stellt uns folgendes Data Dictionary und damit verbunden folgende Beschreibungen der einzelnen Variablen zur Verfügung:

Variable Definition
id Unique ID for the customer
gender Gender of the customer
age Age of the customer
driving_license 0 : Customer doesn't have DL, 1 : Customer has DL
region_code Unique code for the region of the customer
previously_insured 0 : Customer doesn't have Vehicle Insurance, 1 : Customer has Vehicle Insurance
vehicle_age Age of the Vehicle
vehicle_damage 1 : Customer got his/her vehicle damaged in the past. 0 : Customer didn't get his/her vehicle damaged in the past.
annual_premium The amount customer needs to pay as premium in the year for Health insurance
policy_sales_channel Anonymized Code for the channel of outreaching to the customer ie. Different Agents, Over Mail, Over Phone, In Person, etc.
vintage Number of Days customer has been associated with the company
response 1 : Customer is interested, 0 : Customer is not interested

2. Data Understanding

2.1 Pakete importieren

In [506]:
import pandas as pd
from pandas.api.types import CategoricalDtype
import numpy as np
from matplotlib import pyplot as plt
import seaborn as sns

# Sklearn Packages
from sklearn.impute import SimpleImputer
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression, LinearRegression
from sklearn.impute import KNNImputer
from sklearn.neural_network import MLPClassifier
from sklearn.preprocessing import StandardScaler
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.experimental import enable_halving_search_cv
from sklearn.model_selection import HalvingGridSearchCV
from sklearn import metrics
from sklearn.metrics import PrecisionRecallDisplay
from sklearn.metrics import RocCurveDisplay
from sklearn.metrics import roc_curve

# Undersamling / Oversampling
from imblearn.under_sampling import RandomUnderSampler
from imblearn.over_sampling import RandomOverSampler

import warnings
In [507]:
warnings.filterwarnings("ignore")

2.2 Daten einlesen

Der Datensatz wurde von der NextGen Insurance bereitgestellt.

Der Datensatz "train_dataset" wird zur Analyse eingelesen:

  • Entfernung des Trennzeichen "$".
  • Umwandlung von Zelleninhalten in Wahrheitswerte (Yes, yes, 1; No, no, 0).
  • Einrücken des Datensatzes.

Der Datensatz "real_dataset" wird zur Analyse eingelesen:

  • Entfernung der Trennzeichen "$" und ",".
  • Umwandlung von Zelleninhalten in Wahrheitswerte (Yes, yes, 1; No, no, 0).
  • Einrücken des Datensatzes.
In [508]:
# Read train.csv
train_dataset = pd.read_csv(
    "train.csv",
    sep="$",
    true_values=["Yes", "yes", "1"],
    false_values=["No", "no", "0"],
    index_col=False,
    low_memory=False,
)

#Read test.csv
real_dataset = pd.read_csv(
    "test.csv",
    sep="\$|,",  # this csv uses 2 different separators
    true_values=["Yes", "yes", "1"],
    false_values=["No", "no", "0"],
    index_col=False,
    engine="python"  # c engine does not support regex or multiple separators
)

2.3 Datensatz Anzeigen

Zur Betrachtung der Variablen aus dem Datensatz werden die ersten zwanzig Einträge angezeigt:

In [509]:
train_dataset.head(20)
Out[509]:
Unnamed: 0 id Gender Age Driving_License Region_Code Previously_Insured Vehicle_Age Vehicle_Damage Annual_Premium Policy_Sales_Channel Vintage Response
0 0 1 Male 44 False 28.0 False > 2 Years True 40454.0 26.0 217 True
1 1 2 Male 76 False 3.0 False 1-2 Year False 33536.0 26.0 183 False
2 2 3 Male 47 False 28.0 False > 2 Years True 38294.0 26.0 27 True
3 3 4 Male 21 True 11.0 True < 1 Year False 28619.0 152.0 203 False
4 4 5 Female 29 True 41.0 True < 1 Year False 27496.0 152.0 39 False
5 5 6 Female 24 False 33.0 False < 1 Year True 2630.0 160.0 176 False
6 6 7 Male 23 False 11.0 False < 1 Year True 23367.0 152.0 249 False
7 7 8 Female 56 False 28.0 False 1-2 Year True 32031.0 26.0 72 True
8 8 9 Female 24 True 3.0 True < 1 Year False 27619.0 152.0 28 False
9 9 10 Female 32 True 6.0 True < 1 Year False 28771.0 152.0 80 False
10 10 11 Female 47 False 35.0 False 1-2 Year True 47576.0 124.0 46 True
11 11 12 Female 24 True 50.0 True < 1 Year False 48699.0 152.0 289 False
12 12 13 NaN 41 True 15.0 True 1-2 Year False 31409.0 14.0 221 False
13 13 14 Male 76 False 28.0 False 1-2 Year True 36770.0 13.0 15 False
14 14 15 Male 71 True 28.0 True 1-2 Year False 46818.0 30.0 58 False
15 15 16 Male 37 False 6.0 False 1-2 Year True 2630.0 156.0 147 True
16 16 17 Female 25 False 45.0 False < 1 Year True 26218.0 160.0 256 False
17 17 18 Female 25 True 35.0 True < 1 Year False 46622.0 152.0 299 False
18 18 19 Male 42 False 28.0 False 1-2 Year True 33667.0 124.0 158 False
19 19 20 Female 60 False 33.0 False 1-2 Year True 32363.0 124.0 102 True
In [510]:
real_dataset.head(20)
Out[510]:
id Gender Age Driving_License Region_Code Previously_Insured Vehicle_Age Vehicle__Damage Annual__Premium Policy_Sales_Channel Vintage
0 381000 Male 68 1 28.0 1 1-2 Year False 53066.0 12.0 195
1 381001 Male 78 1 8.0 0 1-2 Year True 28301.0 124.0 195
2 381002 Male 22 1 28.0 1 < 1 Year False 2630.0 153.0 170
3 381003 Female 20 1 28.0 0 1-2 Year True 38627.0 124.0 62
4 381004 Female 44 1 28.0 1 1-2 Year False 24984.0 152.0 255
5 381005 Male 29 1 35.0 1 < 1 Year False 2630.0 152.0 109
6 381006 Female 44 1 28.0 0 1-2 Year True 40435.0 26.0 246
7 381007 Male 55 1 28.0 0 1-2 Year True 2630.0 156.0 174
8 381008 Female 24 1 28.0 1 < 1 Year False 36371.0 152.0 74
9 381009 Female 22 1 28.0 0 1-2 Year True 49611.0 154.0 138
10 381010 Male 27 1 28.0 0 < 1 Year True 52067.0 152.0 283
11 381011 Female 60 1 38.0 1 < 1 Year True 2630.0 1.0 46
12 381012 Female 34 1 28.0 0 1-2 Year True 34973.0 154.0 143
13 381013 Female 31 1 28.0 0 < 1 Year True 39877.0 26.0 124
14 381014 Female 54 1 30.0 0 1-2 Year True 26815.0 124.0 184
15 381015 Female 21 1 46.0 1 < 1 Year False 61335.0 152.0 250
16 381016 Female 23 1 46.0 1 < 1 Year False 19460.0 152.0 104
17 381017 Male 71 1 28.0 1 1-2 Year False 47653.0 122.0 85
18 381018 Male 55 1 13.0 0 > 2 Years True 29850.0 157.0 133
19 381019 Female 49 1 43.0 0 1-2 Year True 39618.0 124.0 215

2.4 Spaltennamen und Datentypen

Um eventuelle Korrekturen vorzunehmen betrachten wir die Datentypen der im Datensatz enthaltenen Variablen.

In [511]:
train_dataset.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 380999 entries, 0 to 380998
Data columns (total 13 columns):
 #   Column                Non-Null Count   Dtype  
---  ------                --------------   -----  
 0   Unnamed: 0            380999 non-null  int64  
 1   id                    380999 non-null  int64  
 2   Gender                379948 non-null  object 
 3   Age                   370107 non-null  object 
 4   Driving_License       380948 non-null  object 
 5   Region_Code           380999 non-null  object 
 6   Previously_Insured    380948 non-null  object 
 7   Vehicle_Age           380948 non-null  object 
 8   Vehicle_Damage        380948 non-null  object 
 9   Annual_Premium        380999 non-null  float64
 10  Policy_Sales_Channel  380999 non-null  object 
 11  Vintage               380948 non-null  object 
 12  Response              380999 non-null  bool   
dtypes: bool(1), float64(1), int64(2), object(9)
memory usage: 35.2+ MB
  • Die Spalten Driving_License, Previously_Insured, und Vehicle_Damage wurden nicht in den booleschen Datentypen gecastet. Dies ist ein Indikator dafür das diese Spalten invalide oder fehlende Werte enthalten.
  • Die Spalte Age wurde nicht in einen Integer oder Float gecastet, auch hier ist dies ein Indikator dafür, dass diese Spalte invalide oder fehlende Werte enthält.
In [512]:
real_dataset = real_dataset.rename(columns={"Vehicle__Damage": "Vehicle_Damage", "Annual__Premium": "Annual_Premium"})
real_dataset.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 127147 entries, 0 to 127146
Data columns (total 11 columns):
 #   Column                Non-Null Count   Dtype  
---  ------                --------------   -----  
 0   id                    127147 non-null  int64  
 1   Gender                127147 non-null  object 
 2   Age                   127147 non-null  int64  
 3   Driving_License       127147 non-null  int64  
 4   Region_Code           127147 non-null  float64
 5   Previously_Insured    127147 non-null  int64  
 6   Vehicle_Age           127147 non-null  object 
 7   Vehicle_Damage        127147 non-null  bool   
 8   Annual_Premium        127147 non-null  float64
 9   Policy_Sales_Channel  127147 non-null  float64
 10  Vintage               127147 non-null  int64  
dtypes: bool(1), float64(3), int64(5), object(2)
memory usage: 9.8+ MB

2.5 Datentypen anpassen

  • Die zum Pandas Modul zugehörige Funktion ".unique()" ermöglicht die Ausgabe aller einzigartigen Werte. Dies erleichtert das Nachvollziehen von Eingabefehlern um diese zu korrigieren.
  • Der Numpy-Datentyp int64 unterstützt keine nullable Values (NaN), deshalb wird der Pandas-Datentyp Int64 verwendet.

2.5.1 Variable Age

In [513]:
train_dataset["Age"].unique()
Out[513]:
array(['44', '76', '47', '21', '29', '24', '23', '56', '32', '41', '71',
       '37', '25', '42', '60', '65', '49', '34', '51', '26', '57', '79',
       '48', '45', '72', '30', '54', '27', '38', '22', '78', '20', '39',
       '62', '58', '59', '63', '50', '67', '77', '28', '69', '52', '31',
       '33', '43', '36', '53', '70', '46', '55', '40', '61', '75', '64',
       '35', '66', '68', '74', '73', '84', '83', '81', '80', '133', '171',
       '163', '144', '187', '203', '143', '123', '183', '167', '129',
       '127', '142', '82', '175', '128', '150', '196', '154', '198',
       '116', '152', '161', '114', '166', '124', '134', '173', '106',
       '199', '162', '157', '132', '156', '119', '159', '85', '192',
       '201', '177', '121', '160', '136', '148', '158', '113', '184',
       '182', '122', '190', '174', '176', '195', '147', '189', nan, '181',
       '188', '107', '145', '149', '137', '139', '126', '130', '202',
       '138', '193', '109', '179', '155', '125', '172', '200', '135',
       '205', '204', '180', '186', '194', '170', '146', '185', '197',
       '169', '151', '131', '191', '141', '105', '112', '115', '153',
       '118', '164', '140', '165', '111', '117', '120', '108', '168',
       '178', '22..', '21..', '23..', '41..', '29..', '24..', '27..',
       '44..', '64..', '46..', '45..', '30..', '77..', '32..', '25..',
       '35..', '26..', '40..', '39..', '47..', '61..', '38..', '75..',
       '50..', '28..', '57..', '42..', '51..', '55..', '59..', '33..',
       '71..', '53..', '34..', '43..', '31..', '20..', '49..', '67..',
       '72..', '54..', '69..', '73..', '52..', '68..', '48..', '74..',
       '60..', '79..', '62..', '36..', '70..', '76..', '63..'],
      dtype=object)

Aus dieser Ausgabe kann man sehen, dass einige fehlerhaften Eingaben getätigt wurden (z.B. "29.."). Da die Werte dieser Datensätze aber inhaltlich richtig sein könnten, sollen sie behalten werden.

In [514]:
# convert to string
train_dataset["Age"] = train_dataset["Age"].astype(pd.StringDtype())

# remove .. as this is what prevents us from propper type conversion
train_dataset["Age"] = train_dataset["Age"].str.replace(".", "")

# convert to int (no decimals observed in train data)
train_dataset["Age"] = train_dataset["Age"].astype("Int64")

Durch das Casten in den String-Datentyp können die fehlerhaften Sonderzeichen (..) entfernt werden. Anschließend wird die Variable in den gewünschten Integer-Datentypen gecastet.

2.5.2 Variablen Driving_License, Previously_Insured und Vehicle_Damage

In [515]:
# train_dataset
print("Driving_License:", train_dataset["Driving_License"].unique())

print("Previously_Insured:", train_dataset["Previously_Insured"].unique())

print("Vehicle_Damage:", train_dataset["Vehicle_Damage"].unique())
Driving_License: [False True nan]
Previously_Insured: [False True nan]
Vehicle_Damage: [True False nan]

Die Ausgabe weist darauf hin, dass diese Variablen richtig einglesen werden konnten und es keine (inhaltlich) falschen Ausprägungen gibt.

  • Es gibt nur True, False und Missing Values(NaN).
In [516]:
# convert each column
# no cleanup required
# train_dataset
train_dataset["Driving_License"] = train_dataset["Driving_License"].astype(pd.BooleanDtype())

train_dataset["Previously_Insured"] = train_dataset["Previously_Insured"].astype(pd.BooleanDtype())
    
train_dataset["Vehicle_Damage"] = train_dataset["Vehicle_Damage"].astype(pd.BooleanDtype())

###

# real_dataset
real_dataset["Driving_License"] = real_dataset["Driving_License"].astype(pd.BooleanDtype())

real_dataset["Previously_Insured"] = real_dataset["Previously_Insured"].astype(pd.BooleanDtype())

real_dataset["Vehicle_Damage"] = real_dataset["Vehicle_Damage"].astype(pd.BooleanDtype())

Anschließend wird die Variable in den gewünschten Boolean-Datentyp gecastet.

2.5.3 Variable Gender

In [517]:
train_dataset["Gender"].unique()
Out[517]:
array(['Male', 'Female', nan], dtype=object)

Die Ausgabe weist darauf hin, dass diese Variablen richtig einglesen werden konnten und es keine (inhaltlich) falschen Ausprägungen gibt.

  • Es gibt nur die zwei Kategorien Male, Female und Missing Values(NaN).
In [518]:
# no cleanup required
# train_dataset
train_dataset["Gender"] = train_dataset["Gender"].astype(pd.CategoricalDtype())

###

# real_dataset
real_dataset["Gender"] = real_dataset["Gender"].astype(pd.CategoricalDtype())

Anschließend wird die Variable in den gewünschten Category-Datentyp gecastet.

2.5.4 Variable Region Code

In [519]:
train_dataset["Region_Code"].unique()
Out[519]:
array(['28.0', '3.0', '11.0', '41.0', '33.0', '6.0', '35.0', '50.0',
       '15.0', '45.0', '8.0', '36.0', '30.0', '26.0', '16.0', '47.0',
       '48.0', '19.0', '39.0', '23.0', '37.0', '5.0', '17.0', '2.0',
       '7.0', '29.0', '46.0', '27.0', '25.0', '13.0', '18.0', '20.0',
       '49.0', '22.0', '44.0', '0.0', '9.0', '31.0', '12.0', '34.0',
       '21.0', '10.0', '14.0', '38.0', '24.0', '40.0', '43.0', '32.0',
       '4.0', '51.0', '42.0', '1.0', '52.0', '41.0##'], dtype=object)

Aus dieser Ausgabe kann man sehen, dass eine fehlerhafte Eingabe getätigt wurde ("41.0##"). Da dieser Wert des Datensatz aber inhaltlich richtig sein könnte, soll dieser behalten werden.

In [520]:
# convert to string
train_dataset["Region_Code"] = train_dataset["Region_Code"].astype(pd.StringDtype())

# remove ## as this is what prevents us from propper type conversion
train_dataset["Region_Code"] = train_dataset["Region_Code"].str.replace("#", "")

# convert to category as the region codes are similar to postal codes and have no order
train_dataset["Region_Code"] = train_dataset["Region_Code"].astype(pd.CategoricalDtype())

###

# real_dataset
real_dataset["Region_Code"] = real_dataset["Region_Code"].astype(pd.CategoricalDtype())

Durch das Casten in den String-Datentyp kann das fehlerhafte Sonderzeichen (##) entfernt werden. Anschließend wird die Variable in den gewünschten Category-Datentypen gecastet.

2.5.5 Variable Vehicle_Age

In [521]:
train_dataset["Vehicle_Age"].unique()
Out[521]:
array(['> 2 Years', '1-2 Year', '< 1 Year', nan], dtype=object)

Die Ausgabe weist darauf hin, dass diese Variablen richtig einglesen werden konnten und es keine (inhaltlich) falschen Ausprägungen gibt.

  • Es gibt nur die drei Kategorien > 2 Years, 1-2 Year, < 1 Year und Missing Values(NaN).
In [522]:
# no cleanup required
# train_dataset
train_dataset["Vehicle_Age"] = train_dataset["Vehicle_Age"].astype(pd.CategoricalDtype())

###

# real_dataset
real_dataset["Vehicle_Age"] = real_dataset["Vehicle_Age"].astype(pd.CategoricalDtype())

Anschließend wird die Variable in den gewünschten Category-Datentyp gecastet.

2.5.6 Variable Policy_Sales_Channel

In [523]:
train_dataset["Policy_Sales_Channel"].unique()
Out[523]:
array(['26.0', '152.0', '160.0', '124.0', '14.0', '13.0', '30.0', '156.0',
       '163.0', '157.0', '122.0', '19.0', '22.0', '15.0', '154.0', '16.0',
       '52.0', '155.0', '11.0', '151.0', '125.0', '25.0', '61.0', '1.0',
       '86.0', '31.0', '150.0', '23.0', '60.0', '21.0', '121.0', '3.0',
       '139.0', '12.0', '29.0', '55.0', '7.0', '47.0', '127.0', '153.0',
       '78.0', '158.0', '89.0', '32.0', '8.0', '10.0', '120.0', '65.0',
       '4.0', '42.0', '83.0', '136.0', '24.0', '18.0', '56.0', '48.0',
       '106.0', '54.0', '93.0', '116.0', '91.0', '45.0', '9.0', '145.0',
       '147.0', '44.0', '109.0', '37.0', '140.0', '107.0', '128.0',
       '131.0', '114.0', '118.0', '159.0', '119.0', '105.0', '135.0',
       '62.0', '138.0', '129.0', '88.0', '92.0', '111.0', '113.0', '73.0',
       '36.0', '28.0', '35.0', '59.0', '53.0', '148.0', '133.0', '108.0',
       '64.0', '39.0', '94.0', '132.0', '46.0', '81.0', '103.0', '90.0',
       '51.0', '27.0', '146.0', '63.0', '96.0', '40.0', '66.0', '100.0',
       '95.0', '123.0', '98.0', '75.0', '69.0', '130.0', '134.0', '49.0',
       '97.0', '38.0', '17.0', '110.0', '80.0', '71.0', '26.0##', '117.0',
       '58.0', '20.0', '76.0', '104.0', '87.0', '84.0', '137.0', '126.0',
       '68.0', '67.0', '101.0', '115.0', '57.0', '82.0', '79.0', '112.0',
       '99.0', '70.0', '2.0', '34.0', '33.0', '74.0', '102.0', '149.0',
       '43.0', '6.0', '50.0', '144.0', '143.0', '41.0'], dtype=object)

Aus dieser Ausgabe kann man sehen, dass eine fehlerhafte Eingabe getätigt wurde ("26.0##"). Da dieser Wert des Datensatz aber inhaltlich richtig sein könnte, soll dieser behalten werden.

In [524]:
# convert to string
train_dataset["Policy_Sales_Channel"] = train_dataset["Policy_Sales_Channel"].astype(pd.StringDtype())

# remove ## as this is what prevents us from propper type conversion
train_dataset["Policy_Sales_Channel"] = train_dataset["Policy_Sales_Channel"].str.replace("#", "")

# convert to category as the Policy Sales Channels is a anonymized Code for the channel of outreaching to the customer
train_dataset["Policy_Sales_Channel"] = train_dataset["Policy_Sales_Channel"].astype(pd.CategoricalDtype())

###

# real_dataset
real_dataset["Policy_Sales_Channel"] = real_dataset["Policy_Sales_Channel"].astype(pd.CategoricalDtype())

Durch das Casten in den String-Datentyp kann das fehlerhafte Sonderzeichen (##) entfernt werden. Anschließend wird die Variable in den gewünschten Category-Datentypen gecastet.

2.5.7 Variable Vintage

In [525]:
train_dataset["Vintage"].unique()
Out[525]:
array(['217', '183', '27', '203', '39', '176', '249', '72', '28', '80',
       '46', '289', '221', '15', '58', '147', '256', '299', '158', '102',
       '116', '177', '232', '60', '180', '49', '57', '223', '136', '222',
       '149', '169', '88', '253', '107', '264', '233', '45', '184', '251',
       '153', '186', '71', '34', '83', '12', '246', '141', '216', '130',
       '282', '73', '171', '283', '295', '165', '30', '218', '22', '36',
       '79', '81', '100', '63', '242', '277', '61', '111', '167', '74',
       '235', '131', '243', '248', '114', '281', '62', '189', '139',
       '138', '209', '254', '291', '68', '92', '52', '78', '156', '247',
       '275', '77', '181', '229', '166', '16', '23', '31', '293', '219',
       '50', '155', '66', '260', '19', '258', '117', '193', '204', '212',
       '144', '234', '206', '228', '125', '29', '18', '84', '230', '54',
       '123', '101', '86', '13', '237', '85', '98', '67', '128', '95',
       '89', '99', '208', '134', '135', '268', '284', '119', '226', '105',
       '142', '207', '272', '263', '64', '40', '245', '163', '24', '265',
       '202', '259', '91', '106', '190', '162', '33', '194', '287', '292',
       '69', '239', '132', '255', '152', '121', '150', '143', '198',
       '103', '127', '285', '214', '151', '199', '56', '59', '215', '104',
       '238', '120', '21', '32', '270', '211', '200', '197', '11', '213',
       '93', '113', '178', '10', '290', '94', '231', '296', '47', '122',
       '271', '278', '276', '96', '240', '172', '257', '224', '173',
       '220', '185', '90', '51', '205', '70', '160', '137', '168', '87',
       '118', '288', '126', '241', '82', '227', '115', '164', '236',
       '286', '244', '108', '274', '201', '97', '25', '174', '182', '154',
       '48', '20', '53', '17', '261', '41', '266', '35', '140', '269',
       '146', '145', '65', '298', '133', '195', '55', '188', '75', '38',
       '43', '110', '37', '129', '170', '109', '267', '279', '112', '280',
       '76', '191', '26', '161', '179', '175', '252', '42', '124', '187',
       '148', '294', '44', '157', '192', '262', '159', '210', '250', '14',
       '273', '297', '225', '196', '81##', nan], dtype=object)

Aus dieser Ausgabe kann man sehen, dass eine fehlerhafte Eingabe getätigt wurde ("81##"). Da dieser Wert des Datensatz aber inhaltlich richtig sein könnte, soll dieser behalten werden.

In [526]:
# convert to string
train_dataset["Vintage"] = train_dataset["Vintage"].astype(pd.StringDtype())

# remove ## as this is what prevents us from propper type conversion
train_dataset["Vintage"] = train_dataset["Vintage"].str.replace("#", "")

# convert to int (no decimals observed in train data)
train_dataset["Vintage"] = train_dataset["Vintage"].astype("Int64")

Durch das Casten in den String-Datentyp können die fehlerhaften Sonderzeichen (##) entfernt werden. Anschließend wird die Variable in den gewünschten Integer-Datentypen gecastet.

2.5.8 Variable Unnamed: 0

In [527]:
train_dataset.drop("Unnamed: 0", axis="columns", inplace=True)

Diese Spalte beinhaltet keine Informationen und wird aus dem "train_dataset" Datensatz entfernt.

2.5.9 Angepasste Datentypen anzeigen

In [528]:
train_dataset.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 380999 entries, 0 to 380998
Data columns (total 12 columns):
 #   Column                Non-Null Count   Dtype   
---  ------                --------------   -----   
 0   id                    380999 non-null  int64   
 1   Gender                379948 non-null  category
 2   Age                   370107 non-null  Int64   
 3   Driving_License       380948 non-null  boolean 
 4   Region_Code           380999 non-null  category
 5   Previously_Insured    380948 non-null  boolean 
 6   Vehicle_Age           380948 non-null  category
 7   Vehicle_Damage        380948 non-null  boolean 
 8   Annual_Premium        380999 non-null  float64 
 9   Policy_Sales_Channel  380999 non-null  category
 10  Vintage               380948 non-null  Int64   
 11  Response              380999 non-null  bool    
dtypes: Int64(2), bool(1), boolean(3), category(4), float64(1), int64(1)
memory usage: 16.7 MB

2.6 Deskriptive Analyse

2.6.1 Kennzahlen zur Beschreibung des Datensatz

Folgende statistische Kennzahlen werden verwenden:

In [529]:
train_dataset.describe(include="all").transpose()
Out[529]:
count unique top freq mean std min 25% 50% 75% max
id 380999 NaN NaN NaN 190500 109985 1 95250.5 190500 285750 380999
Gender 379948 2 Male 205447 NaN NaN NaN NaN NaN NaN NaN
Age 370107 NaN NaN NaN 38.8521 15.6322 20 25 36 49 205
Driving_License 380948 2 False 206635 NaN NaN NaN NaN NaN NaN NaN
Region_Code 380999 53 28.0 106372 NaN NaN NaN NaN NaN NaN NaN
Previously_Insured 380948 2 False 206635 NaN NaN NaN NaN NaN NaN NaN
Vehicle_Age 380948 3 1-2 Year 200228 NaN NaN NaN NaN NaN NaN NaN
Vehicle_Damage 380948 2 True 192328 NaN NaN NaN NaN NaN NaN NaN
Annual_Premium 380999 NaN NaN NaN 30527.7 17243 -9997 24371 31656 39390 540165
Policy_Sales_Channel 380999 155 152.0 134747 NaN NaN NaN NaN NaN NaN NaN
Vintage 380948 NaN NaN NaN 154.344 83.6731 10 82 154 227 299
Response 380999 2 False 334297 NaN NaN NaN NaN NaN NaN NaN

Auffälligkeiten einzelner Variablen anhand der statistischen Kennzahlen werden im nachfolgenden näher erläutert:

Variable Beschreibung
id - Beginnt bei 1 und endet bei 380.999
- weißt keine Auffälligkeiten auf
Gender - Das Geschlecht "Male" kommt am häufigsten vor mit 205.447 Datensätzen
- 2 verschiedene Ausprägungen
- 1051 Datensätze fehlen (Vergleich von 379.948 zu 380.999 Datensätzen)
Age - min. = 20 Jahre alt nicht auffällig
- Im Durchschnitt 39 Jahre alt
- max. = 205 Jahre alt
- 10.892 Datensätze fehlen (Vergleich von 370.107 zu 380.999 Datensätzen)
Driving_License - Mehr Personen haben keinen Führerschein mit 206.635 Datensätzen als das Sie einen Führerschein haben
- 2 verschiedene Ausprägungen
- 51 Datensätze fehlen (Vergleich von 380.948 zu 380.999 Datensätzen)
Region_Code - Die PLZ 28.0 kommt am häufigsten vor mit 106.372 Datensätzen
- 53 verschiedene Ausprägungen
Previously_Insured - Mehr Personen haben keine Versicherung mit 206.635 Datensätzen als das Sie eine Versicherung haben
- 2 verschiedene Ausprägungen
- 51 Datensätze fehlen (Vergleich von 380.948 zu 380.999 Datensätzen)
Vehicle_Age - Das Alter des Fahrzeugs beläuft sich auf bei den meisten Personen auf 1-2 Jahre mit 380.948 Datensätzen
- 3 verschiedene Ausprägungen
- 51 Datensätze fehlen (Vergleich von 380.948 zu 380.999 Datensätzen)
Vehicle_Damage - Bei mehr Personen, 192.328 Datensätze, ist es zu einem Schadensfall gekommen
- 2 verschiedene Ausprägungen
- 51 Datensätze fehlen (Vergleich von 380.948 zu 380.999 Datensätzen)
Annual_Premium - min. = -9997.0 Rs auffällig, da der Betrag den die Kunden zahlen müssen nicht negativ sein kann.
- Im Durchschnitt 30.527.71 Rs
- max. = 540.165 Rs auffällig, da der Betrag deutlich zu hoch ist
Policy_Sales_Channel - 155 verschiedene Ausprägungen
- Der Verkaufskanal 152.0 kommt mit 134.747 Datensätzen am häufigsten vor
Vintage - min. = 10 Tage
- Im Durchschnitt 154 Tage
- max. = 299 Tage
- 51 Datensätze fehlen (Vergleich von 380.948 zu 380.999 Datensätzen)
Response - Mehr Personen sind nicht interessiert mit 334.297 Datensätzen
- 2 verschiedene Ausprägungen

2.6.2 Prüfung auf Missing Values

Die zum Pandas Modul zugehörige Funktion ".isna()" ermöglicht die Ausgabe aller Missing Values(NaN) und die Funktion ".sum()" summiert die Missing Values der einzelnen Spalten auf.

In [530]:
train_dataset.isna().sum()
Out[530]:
id                          0
Gender                   1051
Age                     10892
Driving_License            51
Region_Code                 0
Previously_Insured         51
Vehicle_Age                51
Vehicle_Damage             51
Annual_Premium              0
Policy_Sales_Channel        0
Vintage                    51
Response                    0
dtype: int64

Die Überprüfung auf Missing Values zeigt, dass vor allem für die Variable Age Werte imputiert werden sollten. In der Spalte Gender fehlen rund 1000 Werte. Weiter sieht man, dass in den Spalten Driving_License, Previously_Insured, Vehicle_Age, Vehicle_Damage und Vintage genau 51 Werte fehlen. Das deutet darauf hin, dass diese Missing Values zu den selben Datensätzen gehören, was nachfolgend überprüft wird.

In [531]:
NaN_in_selected_columns = train_dataset.loc[
                  train_dataset["Vintage"].isna()
                & train_dataset["Vehicle_Damage"].isna()
                & train_dataset["Vehicle_Age"].isna()
                & train_dataset["Previously_Insured"].isna()
                & train_dataset["Driving_License"].isna()
]

print(f"Datensätze mit Vintage, Vehicle_Damage, Vehicle_Age, Previously_Insured und Driving_License fehlen: {len(NaN_in_selected_columns)}")
Datensätze mit Vintage, Vehicle_Damage, Vehicle_Age, Previously_Insured und Driving_License fehlen: 51

Mithilfe einer Und(&)-Verbindung wird geprüft, ob die Missing Values alle von den selben Datensätzen stammen. Die Annahme wurde bestätigt. Der Test ergab 51 Treffer. Da nur wenige Informationen zu diesen Datensätzen verfügbar sind und eine Imputation daher nur eingeschränkt möglich ist, werden die Datensätze im Verlauf der Data Preparation entfernt. Hierdurch wird die Modellgüte nicht ausschlaggebend beeinträchtigt, da 51 Datensätze in der Gesamtheit der Daten (>350.000 Datensätze) keinen signifikanten Einfluss haben.

Nachfolgend wurde überprüft, woher diese fehlerhaften Datensätze kommen. Unter verdacht standen die Vertriebskanäle Policy_Sales_Channel und Region_Code was auf fehlerhafte Eingaben in einer speziellen Filiale zurückzuführen wäre.

In [532]:
# cast Region_Code to Category using only the options that appear in the data frame
NaN_in_selected_columns["Region_Code"] = NaN_in_selected_columns["Region_Code"].astype(
    pd.CategoricalDtype(NaN_in_selected_columns["Region_Code"].unique())
)

sns.catplot(
    data=NaN_in_selected_columns, x="Region_Code", kind="count", height=10, aspect=2 / 1
);
In [533]:
NaN_in_selected_columns_grpd = NaN_in_selected_columns.groupby("Policy_Sales_Channel").count()

NaN_in_selected_columns_grpd = NaN_in_selected_columns_grpd.loc[NaN_in_selected_columns_grpd["id"] > 0]

# reset index to re-include groupby counts (this resets all dtypes)
NaN_in_selected_columns_grpd = NaN_in_selected_columns_grpd.reset_index()

# reset PSC to categorial dtype
NaN_in_selected_columns_grpd["Policy_Sales_Channel"] = NaN_in_selected_columns_grpd["Policy_Sales_Channel"].astype(
    pd.CategoricalDtype(NaN_in_selected_columns_grpd["Policy_Sales_Channel"].unique())
)

sns.catplot(
    data=NaN_in_selected_columns_grpd,
    x="Policy_Sales_Channel",
    y="id",
    height=10,
    aspect=2 / 1,
    kind="bar",
);

Es gibt zwar Hinweise darauf, dass manche Regionen und Sales Channel fehleranfälliger sind als andere, der Verdacht, dass die fehlerhaften Datensätze auf eine Datenquelle zurückzuführen sind, konnte nicht bestätigt werden.

2.7 Korrelation der Variablen

In [534]:
# remove id from correlation matrix as it does not provide any usefull information
def correlation_matrix_table(train_dataset):
    correlation = train_dataset.drop(columns=["id"]).corr()
    return correlation


correlation_matrix_table(train_dataset)
Out[534]:
Age Driving_License Previously_Insured Vehicle_Damage Annual_Premium Vintage Response
Age 1.000000 -0.254485 -0.254485 0.265097 0.066933 -0.001123 0.109969
Driving_License -0.254485 1.000000 1.000000 -0.823370 0.003981 0.002446 -0.340751
Previously_Insured -0.254485 1.000000 1.000000 -0.823370 0.003981 0.002446 -0.340751
Vehicle_Damage 0.265097 -0.823370 -0.823370 1.000000 0.009428 -0.002034 0.354438
Annual_Premium 0.066933 0.003981 0.003981 0.009428 1.000000 -0.000592 0.022631
Vintage -0.001123 0.002446 0.002446 -0.002034 -0.000592 1.000000 -0.001031
Response 0.109969 -0.340751 -0.340751 0.354438 0.022631 -0.001031 1.000000
In [535]:
def correlation_matrix_plot(train_dataset, x, y, show_labels, col_map, method=""):
    correlation = train_dataset.corr(method=method)
    plt.figure(figsize=(x, y))
    sns.heatmap(
        correlation, annot=show_labels, linewidths=1, linecolor="black", cmap=col_map, vmin=-1, vmax=1
    )
    plt.title(f"Korrelationsmatrix ({method})", fontsize=18, weight="bold")


correlation_matrix_plot(train_dataset, 12, 6, True, "seismic", "pearson")
correlation_matrix_plot(train_dataset, 12, 6, True, "seismic", "spearman")
  • Es fällt auf, dass Previously_Insured und Driving_License die höchste Korrelation, undzwar von 1, aufweisen. Das liegt daran, dass jeder KFZ-Besitzer eine KFZ-Versicherung haben muss sofern das KFZ angemeldet ist.
  • Die geringste Korrelation weisen die Variablen Driving_License und Vintage, sowie Previously_Insured und Vintage auf, mit einer Korrelation von 0,0024.
  • Hohe negative Korrelation zwischen Vehicle_Damage und Previously_Insured.
  • Korrelation von 0,35 zwischen Vehicle_Damage und Response. Wenn ich in der Vergangenheit einen Schadensfall hatte, bin ich eher dazu geneigt eine Versicherung abzuschließen.
  • Die Korrelationen von Vintage liege nahe an 0.
In [536]:
print(f'In wie vielen Fällen ist Driving_License != Previously_Insured?\n -> {len(train_dataset.loc[train_dataset["Driving_License"] != train_dataset["Previously_Insured"]])}')
# Observation was confirmed!
# Columns Driving_License and Previously_Insured are equals!
In wie vielen Fällen ist Driving_License != Previously_Insured?
 -> 0

Beobachtungen:

  • Die Spalten Driving_License und Previously_Insured beinhalten die gleichen Daten.

2.8 Interpretation der Variablen

2.8.1 Interpretation der Variable Gender

Die Variable Gender beschreibt das Geschlecht der Kunden. Diese ist eine kategoriale Variable mit den zwei Ausprägungen Male und Female.

In [537]:
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 14))
colors = sns.color_palette('pastel')[0:2]
l = ["Response: True", "Response: False"]
fig.suptitle("Kreisdiagramm der Variable Gender in Zusammenhang mit Response", fontsize=20, weight="bold")
plt.subplots_adjust(top=1.32)


# MALE PIE CHART
male = train_dataset.loc[train_dataset["Gender"] == "Male"]
d_m = [len(male.loc[male["Response"] == True]),
       len(male.loc[male["Response"] == False])]
ax1.pie(d_m, labels=l, colors=colors, autopct='%.0f%%')
ax1.set_title("Male Responses", weight="bold", fontsize=14)

# FEMALE PIE CHART
female = train_dataset.loc[train_dataset["Gender"] == "Female"]
d_f = [len(female.loc[female["Response"] == True]),
       len(female.loc[female["Response"] == False])]
ax2.pie(d_f, labels=l, colors=colors, autopct='%.0f%%')
ax2.set_title("Female Responses", weight="bold", fontsize=14); # ";" prevents output in console

Beobachtungen:

  • Keine signifikanten unterschiede im Interesse an KFZ-Versicherungen bei Männern und Frauen.

2.8.2 Interpretation der Variable Age

Die Variable Age beschreibt das Alter der Kunden.

Erwartungen:

  • Plotten der Altersverteilung gibt Rückschlüsse zur Datenqualität bzw. zur Datenherkunft
    • Es ist eine pyramiedenförmige Altersverteilung zu erwarten, da der Datensatz aus Indien stammt
  • Ältere und damit erfahrenere Kunden sind eher an einer Versicherung interessiert
In [538]:
sns.set(rc={"figure.figsize": (22, 10)})
histplot_age = sns.histplot(train_dataset, x="Age", binwidth=5)
histplot_age.set_title("Histogram der Variable Age", fontsize=30, weight='bold')
histplot_age.set_xlabel("Age", fontsize=20, weight='bold')
histplot_age.set_ylabel("Count", fontsize=20, weight='bold');

Beobachtungen:

  • Aus der Fallbeschreibung konnte entnommen werden, dass es sich um einen Datensatz aus Indien handelt. Es wurde von der Währung Rs (Indische Rupie) gesprochen. Die Altersverteilung kommt der pyramidenförmigen demografischen Verteilung von Indien deutlich näher als der Urnenform von Deutschland. Die geplottete Altersverteilung bestätigt zusätzlich die Datenherkunft und Datengüte, da die erwartete Verteilung, bis auf einen Sattelpunkt zwischen 30 Jahre und 40 Jahre, ausgegeben wurde.
  • Es gibt keine Werte unter 20 Jahre.
In [539]:
sns.set(rc={"figure.figsize": (25, 10)})
boxplot = sns.boxplot(data=train_dataset, y="Gender", x="Age", orient="horizontal")
boxplot.set_xlabel("Age", fontsize=20, weight='bold')
boxplot.set_ylabel("Gender", fontsize=20, weight='bold')
boxplot.set_title("Boxplot der Variable Age in Zusammenhang mit Gender" +
                  "\n", fontsize=30, weight='bold')
plt.tick_params(axis="both", labelsize=18)

Beobachtungen:

  • Es gibt unrealistisch hohe Alterswerte.
  • Männer sind im Schnitt älter als Frauen
In [540]:
print(f'Durchschnittsalter von Männern: {train_dataset.loc[train_dataset["Gender"] == "Male"].mean().round()["Age"]}')
print(f'Durchschnittsalter von Frauen: {train_dataset.loc[train_dataset["Gender"] == "Female"].mean().round()["Age"]}')
Durchschnittsalter von Männern: 41.0
Durchschnittsalter von Frauen: 36.0

Alle Datensätze bei denen das Alter über 100 Jahren liegt, sind nicht realitätsnah und werden genauer betrachtet:

In [541]:
train_dataset.loc[(train_dataset.Age >= 100)]
Out[541]:
id Gender Age Driving_License Region_Code Previously_Insured Vehicle_Age Vehicle_Damage Annual_Premium Policy_Sales_Channel Vintage Response
2444 2445 Female 133 True 12.0 True < 1 Year False 29183.0 152.0 44 False
3734 3735 Male 171 False 28.0 False 1-2 Year True 27966.0 163.0 48 True
4805 4806 Male 163 True 28.0 True 1-2 Year False 2630.0 124.0 200 False
4858 4859 Male 144 True 26.0 True < 1 Year False 30869.0 152.0 288 False
5635 5636 Male 187 False 33.0 False < 1 Year True 35397.0 124.0 96 True
... ... ... ... ... ... ... ... ... ... ... ... ...
191789 191790 Male 117 False 28.0 False > 2 Years True 47282.0 26.0 62 True
191977 191978 Male 120 False 5.0 False < 1 Year True 27514.0 26.0 31 False
192630 192631 Female 108 True 11.0 True < 1 Year False 28031.0 152.0 22 False
195149 195150 Male 168 True 10.0 True < 1 Year False 2630.0 152.0 26 False
196966 196967 Female 178 True 28.0 True 1-2 Year False 2630.0 124.0 227 False

100 rows × 12 columns

Die Daten mit unrealistisch hohen Alterswerten sind möglicherweise alte Datensätze, die nicht gepflegt bzw. im Fall der Vertragsauflösung nicht gelöscht wurden.

2.8.3 Interpretation der Variable Driving_License

Die Variable Driving_License beschreibt ob ein Kunde einen Führerschein besitzt.

Erwartungen:

  • Kunden, die keinen Führerschein besitzen, haben keine Verwendung für eine KFZ-Versicherung
    • Außer sie planen kurzfristig den Erwerb eines Führerscheins
  • Führerscheinbesitzer haben möglicherweise schon eine KFZ-Versicherung
In [542]:
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 14))
colors = sns.color_palette('pastel')[0:4]
fig.suptitle("Kreisdiagramm der Variable Driving_License in Zusammenhang mit Response", fontsize=20, weight="bold")
plt.subplots_adjust(top=1.32)

dl_true = train_dataset.loc[train_dataset["Driving_License"] == True]
dl_false=train_dataset.loc[train_dataset["Driving_License"] == False]

# dl_true PIE CHART
d_true = [len(dl_true.loc[dl_true["Response"] == True]),
       len(dl_true.loc[dl_true["Response"] == False])]
l_true = ["Response: True", "Response: False"]
ax1.pie(d_true, labels=l_true, colors=colors, autopct='%.0f%%')
ax1.set_title("Driving License Owner", weight="bold", fontsize=14)

# dl_false PIE CHART
d_false = [len(dl_false.loc[dl_false["Response"] == True]),
       len(dl_false.loc[dl_false["Response"] == False])]
l_false = l_true

ax2.pie(d_false, labels=l_false, colors=colors, autopct='%.0f%%')
ax2.set_title("Not Driving License Owner", weight="bold", fontsize=14);

Beobachtungen:

  • Kein Führerscheinbesitzer ist an einer KFZ-Versicherung interessiert. Möglicherweise weil Führerscheinbesitzer ein Auto und deswegen auch eine KFZ-Versicherung besitzen.
  • 23% aller Führerscheinlosen haben Interesse an einer KFZ-Versicherung bekundet.

2.8.4 Interpretation der Variable Region_Code

Die Variable Region_Code beschreibt den Wohnort der Kunden.

Erwartungen:

  • In einer guten Wohngegend können sich die Versicherungsnehmer eher eine KFZ-Versicherung leisten oder besitzen ein teureres Auto, für das sich eine Versicherung lohnt
  • Analog dazu verzichten Kunden aus ärmeren Regionen aus finanziellen Gründen eher auf eine KFZ-Versicherung
  • Bestimmte Verkaufskanäle konzentrieren sich auf bestimmte Regionen, andere agieren flächendeckend
In [543]:
len(train_dataset["Region_Code"].unique())
Out[543]:
53
In [544]:
p_data = train_dataset.groupby("Region_Code").count()

p_data = p_data.loc[p_data["id"] > 0]

# reset index to re-include groupby counts (this resets all dtypes)
p_data = p_data.reset_index()

# reset PSC to categorial dtype
p_data["Region_Code"] = p_data["Region_Code"].astype(
    pd.CategoricalDtype(p_data["Region_Code"].unique())
)

plot = sns.catplot(
    data=p_data,
    x="Region_Code",
    y="id",
    height=10,
    aspect=2 / 1,
    kind="bar"
)

Bei dieser Variable handelt es sich um eine kategoriale Variable mit 53 Ausprägungen. Sie kann analog zur Postleitzahl verstanden werden.

In [545]:
sns.set(rc={"figure.figsize": (22, 10)})
countplot_region_code = sns.countplot(data=train_dataset, x="Region_Code", hue="Response") 
countplot_region_code.set_title("Balkendiagramm der Variable Region_Code in Zusammenhang mit Response", fontsize=30, weight='bold')
countplot_region_code.set_xlabel("Region_Code", fontsize=20, weight='bold')
countplot_region_code.set_ylabel("Count", fontsize=20, weight='bold'); 

Beobachtungen:

  • Es fällt auf das die meisten Kunden aus dem Verkaufskanal 28.0 kommen und dementsprechend die Nachfrage nach einer KFZ-Versicherung am größten ist. Das könnte vielleicht daran liegen das es sich um eine gute Wohngegend handelt und sich die Versicherungsnehmer eher ein Fahrzeug und somit eine KFZ-Versicherung leisten können.
In [546]:
catplot_region_code = sns.catplot(data=train_dataset, x="Region_Code", y="Annual_Premium", jitter=False, height=10, aspect= 2/1)
catplot_region_code.fig.subplots_adjust(top=0.93)
catplot_region_code.fig.suptitle("Streudiagramm der Variable Region_Code in Zusammenhang mit Annual_Premium", fontsize=30, weight='bold')
catplot_region_code.set_xlabels("Region_Code", fontsize=20, weight='bold')
catplot_region_code.set_ylabels("Annual_Premium", fontsize=20, weight='bold');

Beobachtungen:

  • In einigen Verkaufskanälen ist die jährliche Zahlung an die Krankenversicherung unrealistisch hoch. Zudem haben einige Verkaufskanäle negative jährliche Zahlungen, was deutlich einen Fehler darstellt da die Versicherungsgesellschaft sonst den Kunden bezahlen würde.

2.8.5 Interpretation der Variable Previously_Insured

Die Variable Previously_Insured beschreibt ob ein Kunde eine KFZ-Versicherung besitzt.

In [547]:
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 14))
colors = sns.color_palette('pastel')[0:2]
l = ["Driving_License: True", "Driving_License: False"]
fig.suptitle("Kreisdiagramm der Variable Previously_Insured in Zusammenhang mit Driving_License", fontsize=20, weight="bold")
plt.subplots_adjust(top=1.32)

d_1 = train_dataset.loc[train_dataset["Previously_Insured"] == True]
p_1 = [len(d_1.loc[d_1["Driving_License"] == True]),
       len(d_1.loc[d_1["Driving_License"] == False])]
ax1.pie(p_1, labels=l, colors=colors, autopct='%.0f%%', startangle = 45)
ax1.set_title("Previously insured", weight="bold", fontsize=14)

d_2 = train_dataset.loc[train_dataset["Previously_Insured"] == False]
p_2 = [len(d_2.loc[d_2["Driving_License"] == True]),
       len(d_2.loc[d_2["Driving_License"] == False])]
ax2.pie(p_2, labels=l, colors=colors, autopct='%.0f%%', startangle = 45)
ax2.set_title("Not previously insured", weight="bold", fontsize=14);

Beobachtungen:

  • Kunden die eine KFZ-Versicherung abgeschlossen haben besitzen einen Führerschein.
  • Kunden die keine KFZ-Versicherung abgeschlossen haben besitzen keinen Führerschein.
In [548]:
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 14))
colors = sns.color_palette('pastel')[0:2]
l = ["Response: True", "Response: False"]
fig.suptitle("Kreisdiagramm der Variable Previously_Insured in Zusammenhang mit Response", fontsize=20, weight="bold")
plt.subplots_adjust(top=1.32)

d_1 = train_dataset.loc[train_dataset["Previously_Insured"] == True]
p_1 = [len(d_1.loc[d_1["Response"] == True]),
       len(d_1.loc[d_1["Response"] == False])]
ax1.pie(p_1, labels=l, colors=colors, autopct='%.0f%%')
ax1.set_title("Previously insured", weight="bold", fontsize=14)

d_2 = train_dataset.loc[train_dataset["Previously_Insured"] == False]
p_2 = [len(d_2.loc[d_2["Response"] == True]),
       len(d_2.loc[d_2["Response"] == False])]
ax2.pie(p_2, labels=l, colors=colors, autopct='%.0f%%')
ax2.set_title("Not previously insured", weight="bold", fontsize=14);

Beobachtungen:

  • Kunden die eine KFZ-Versicherung haben, sind an keiner KFZ-Versicherung interessiert.
  • 77% der Kunden die keine KFZ-Versicherung haben sind auch an keiner KFZ-Versicherung interessiert. 23% der Kunden die keine KFZ-Versicherung haben sind an einer KFZ-Versicherung interessiert.

2.8.6 Interpretation der Variable Vehicle_Age

Die Variable Vehicle_Age beschreibt das Alter des Fahrzeugs. Diese ist eine kategoriale Variable mit den drei Ausprägungen < 1 Year, 1-2 Year und > 2 Years.

Erwartungen:

  • Besitzer neuer KFZs wollen ihre Neuanschaffung eher versichern
In [549]:
catplot_region_code = sns.catplot(data=train_dataset, x="Vehicle_Age", kind="count", height=10, aspect= 2/1)
catplot_region_code.fig.subplots_adjust(top=0.93)
catplot_region_code.fig.suptitle("Balkendiagramm der Variable Vehicle_Age", fontsize=30, weight='bold')
catplot_region_code.set_xlabels("Vehicle_Age", fontsize=20, weight='bold')
catplot_region_code.set_ylabels("Count", fontsize=20, weight='bold');

Beobachtungen:

  • Kunden die ein Fahrzeug besitzen das 1-2 Jahre Alt ist, sind am häufigsten vorhanden mit ca. 200.000 Datensätzen. Fahrzeuge die unter einem Jahr alt sind, sind ebenfalls häufig vorhanden mit ca. 165.000 Datensätzen. Fahrzeuge die über 2 Jahre alt sind, sind nicht sehr häufig vorhanden mit ca. 20.000 Datensätzen.
In [550]:
catplot_region_code = sns.catplot(data=train_dataset, x="Vehicle_Age", y="Annual_Premium", jitter=False, height=10, aspect= 2/1)
catplot_region_code.fig.subplots_adjust(top=0.93)
catplot_region_code.fig.suptitle("Streudiagramm der Variable Vehicle_Age in Zusammenhang mit Annual_Premium", fontsize=30, weight='bold')
catplot_region_code.set_xlabels("Vehicle_Age", fontsize=20, weight='bold')
catplot_region_code.set_ylabels("Annual_Premium", fontsize=20, weight='bold');

Beobachtungen:

  • Kunden deren Fahrzeuge 1-2 Jahre alt und unter einem Jahr sind haben die höchsten zu zahlenden jährlichen Beträge an die Krankenversicherung. Dies könnte auf einen Fehler hindeuten, da die Beträge für die Krankenversicherung deutlich zu hoch ausfallen.
In [551]:
print(f'Durchschnittszahlungen an die Krankenversicherung wenn das Auto <1 Jahr alt ist: {train_dataset.loc[train_dataset["Vehicle_Age"] == "< 1 Year"].mean().round(2)["Annual_Premium"]} Rs')
print(f'Durchschnittszahlungen an die Krankenversicherung wenn das Auto 1-2 Jahre alt ist: {train_dataset.loc[train_dataset["Vehicle_Age"] == "1-2 Year"].mean().round(2)["Annual_Premium"]} Rs')
print(f'Durchschnittszahlungen an die Krankenversicherung wenn das Auto mehr als 2 Jahre alt ist: {train_dataset.loc[train_dataset["Vehicle_Age"] == "> 2 Years"].mean().round(2)["Annual_Premium"]} Rs')
Durchschnittszahlungen an die Krankenversicherung wenn das Auto <1 Jahr alt ist: 30082.84 Rs
Durchschnittszahlungen an die Krankenversicherung wenn das Auto 1-2 Jahre alt ist: 30487.86 Rs
Durchschnittszahlungen an die Krankenversicherung wenn das Auto mehr als 2 Jahre alt ist: 35613.25 Rs
In [552]:
fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(20, 10))
colors = sns.color_palette('pastel')[0:2]
fig.suptitle("Kreisdiagramm der Variable Vehicle_Age in Zusammenhang mit Response", fontsize=20, weight="bold")
plt.subplots_adjust(top=1.2)

dl_1 = train_dataset.loc[train_dataset["Vehicle_Age"] == "< 1 Year"]
dl_2 = train_dataset.loc[train_dataset["Vehicle_Age"] == "1-2 Year"]
dl_3 = train_dataset.loc[train_dataset["Vehicle_Age"] == "> 2 Years"]

l = ["Response: True", "Response: False"]
# dl_1 PIE CHART
d_1 = [len(dl_1.loc[dl_1["Response"] == True]),
       len(dl_1.loc[dl_1["Response"] == False])]
ax1.pie(d_1, labels=l, colors=colors, autopct='%.0f%%')
ax1.set_title("Vehicle_Age < 1 Year", weight="bold", fontsize=14)

# dl_2 PIE CHART
d_2 = [len(dl_2.loc[dl_2["Response"] == True]),
       len(dl_2.loc[dl_2["Response"] == False])]
ax2.pie(d_2, labels=l, colors=colors, autopct='%.0f%%')
ax2.set_title("Vehicle_Age 1-2 Year", weight="bold", fontsize=14)

# dl_3 PIE CHART
d_3 = [len(dl_3.loc[dl_3["Response"] == True]),
       len(dl_3.loc[dl_3["Response"] == False])]
ax3.pie(d_3, labels=l, colors=colors, autopct='%.0f%%')
ax3.set_title("Vehicle_Age > 2 Years", weight="bold", fontsize=14);

Beobachtungen:

  • Je Älter das Auto, desto interessierter sind die Kunden an einer KFZ-Versicherung.

2.8.7 Interpretation der Variable Vehicle_Damage

Die Variable Vehicle_Damage beschreibt, ob es an einem Fahrzeug schonmal einen Schadensfall gab.

Erwartung:

  • Jemand, der bereits einen Schaden hatte, hat aus der Erfahrung gelernt, dass es Vorteilhaft sein kann eine Versicherung zu haben
In [553]:
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 14))
colors = sns.color_palette('pastel')[0:2]
fig.suptitle("Kreisdiagramm der Variable Vehicle_Damage in Zusammenhang mit Response", fontsize=20, weight="bold")
plt.subplots_adjust(top=1.32)

dl_1 = train_dataset.loc[train_dataset["Vehicle_Damage"] ==True]
dl_2 = train_dataset.loc[train_dataset["Vehicle_Damage"] == False]

l = ["Response: True", "Response: False"]
# dl_1 PIE CHART
d_1 = [len(dl_1.loc[dl_1["Response"] == True]),
       len(dl_1.loc[dl_1["Response"] == False])]
ax1.pie(d_1, labels=l, colors=colors, autopct='%.0f%%')
ax1.set_title("Vehicle_Damage: True", weight="bold", fontsize=14)

# dl_2 PIE CHART
d_2 = [len(dl_2.loc[dl_2["Response"] == True]),
       len(dl_2.loc[dl_2["Response"] == False])]
ax2.pie(d_2, labels=l, colors=colors, autopct='%.0f%%')
ax2.set_title("Vehicle_Damage: False", weight="bold", fontsize=14);

Beobachtungen:

  • Es sind 24% an einer KFZ-Versicherung interessiert die bereits einen Schadensfall erlitten haben.
  • 1% sind an einer KFZ-Versicherung interessiert die bisher keinen Schadensfall erlitten haben.

2.8.8 Interpretation der Variable Annual_Premium

Die Variable Annual_Premium beschreibt die Höhe des jährlichen Versicherungsbeitrag der Krankenversicherung des Kunden.

Erwartungen:

  • Ein Kunde, der mit seiner Krankenversicherung zufrieden ist, etwa weil der Beitrag niedrig ist, ist eher verleitet, bei der selben Versicherung ein weiteres Produkt zu kaufen
  • Das Annual_Premium ist abhängig vom Alter des Versicherten.
In [554]:
train_dataset["Annual_Premium"].describe()
Out[554]:
count    380999.000000
mean      30527.700690
std       17242.997675
min       -9997.000000
25%       24371.000000
50%       31656.000000
75%       39390.000000
max      540165.000000
Name: Annual_Premium, dtype: float64
  • Der durchschnittliche Annual_Premium liegt bei rund 30.500 Rs.
  • Das Minimun ist negativ, was auf mindestens einen fehlerhaften Wert hindeutet.
  • Das Maximum liegt bei ca. 540.000 Rs, was auf einen Fehler hindeuten könnte.
In [555]:
sns.set(rc={"figure.figsize": (22, 10)})
histplot_annual_premium = sns.histplot(train_dataset, x="Annual_Premium", binwidth=2500)
histplot_annual_premium.set_title("Histogram der Variable Annual_Premium", fontsize=30, weight='bold')
histplot_age.set_xlabel("Age", fontsize=20, weight='bold')
histplot_age.set_ylabel("Count", fontsize=20, weight='bold');
In [556]:
print(f"Anzahl der Datensätze bei ca. 2500 Rs : {len(train_dataset.loc[(train_dataset['Annual_Premium'] >= 0) & (train_dataset['Annual_Premium'] < 3000)])} Datensätze")
print(f"Anzahl der Datensätze zwischen 3000 Rs und 100.000 Rs: {len(train_dataset.loc[(train_dataset['Annual_Premium'] >= 3000) & (train_dataset['Annual_Premium'] < 100000)])} Datensätze")
print(f"Anzahl der Datensätze ab 100.000 Rs: {len(train_dataset.loc[train_dataset['Annual_Premium'] >= 100000])} Datensätze")
print(f"Anzahl der Datensätze negativer Beträge: {len(train_dataset.loc[train_dataset['Annual_Premium'] < 0])} Datensätze")
Anzahl der Datensätze bei ca. 2500 Rs : 64805 Datensätze
Anzahl der Datensätze zwischen 3000 Rs und 100.000 Rs: 315052 Datensätze
Anzahl der Datensätze ab 100.000 Rs: 775 Datensätze
Anzahl der Datensätze negativer Beträge: 367 Datensätze
In [557]:
sns.set(rc={"figure.figsize": (22, 10)})
relplot = sns.relplot(data=train_dataset, x="Age", y="Annual_Premium",
                col="Response", hue="Vehicle_Age")
relplot.fig.subplots_adjust(top=0.8)
relplot.fig.suptitle("Streudiagramm der Variable Annual_Premium in Zusammenhang mit Age, Vehicle_Age und Response", fontsize=16, weight='bold')
relplot.set_xlabels("Age", fontsize=12, weight='bold')
relplot.set_ylabels("Annual_Premium", fontsize=12, weight='bold');

Beobachtungen:

  • Kunden die kein Interesse an einer KFZ-Versicherung haben zahlen höhere jährliche Beträge an die Krankenkasse.
  • Jüngere Kunden haben überwiegend ein Fahrzeug das unter einem Jahr alt ist.
  • Kunden zwischen 50 Jahren und 80 Jahren haben Fahrzeuge die über 2 Jahre alt sind.
  • Kunden deren Fahrzeug zwischen 1-2 Jahre alt sind, sind in allen Altersgruppen verteilt.
  • Mehr Kunden sind nicht an einer KFZ-Versicherung interessiert. Zudem lässt sich erkennen das die Kunden dazu geneigt sind eine KFZ-Versicherung abzuschließen wenn der jährlich zu zahlende Betrag an die Krankenversicherung nicht so hoch ist.
In [558]:
sns.set(rc={"figure.figsize": (20, 10)})

isolate_annual_premium = train_dataset.loc[(train_dataset["Annual_Premium"] > 0) &
                      (train_dataset["Annual_Premium"] < 70000)]

histplot_annual_premium = sns.histplot(isolate_annual_premium, x="Annual_Premium", binwidth=1000)
histplot_annual_premium.set_title("Betrachtung des realistischen Datenbereichs der Variable Annual_Premium", fontsize=30, weight='bold');
histplot_annual_premium.set_xlabel("Annual_Premium", fontsize=20, weight='bold')
histplot_annual_premium.set_ylabel("Count", fontsize=20, weight='bold');

Beobachtungen:

  • rechtsschiefe Verteilung um 30.000 Rs.
  • Ausreißer bei rund 2.500 Rs. Das ist möglicherweise ein besonderer Versicherungstarif, z.B. ein pauschaler Tarif.
In [559]:
sns.set(rc={"figure.figsize": (30, 10)})
boxplot = sns.boxplot(data=train_dataset, y="Gender",
                      x="Annual_Premium", orient="horizontal")
boxplot.set_xlabel("Annual_Premium", fontsize=20, weight='bold')
boxplot.set_ylabel("Gender", fontsize=20, weight='bold')

boxplot.set_xlim(0, 550000)
boxplot.set_xticks(range(0, 550000, 25000))

boxplot.set_title("Boxplot der Variable Annual_Premium in Zusammenhang mit Gender." +
                  "\n", fontsize=30, weight='bold')
plt.tick_params(axis="both", labelsize=18)
In [560]:
print(f'Durchschnittszahlungen an die Krankenversicherung von Männern: {train_dataset.loc[train_dataset["Gender"] == "Male"].mean().round(2)["Annual_Premium"]} Rs')
print(f'Durchschnittszahlungen an die Krankenversicherung von Frauen: {train_dataset.loc[train_dataset["Gender"] == "Female"].mean().round()["Annual_Premium"]} Rs')
Durchschnittszahlungen an die Krankenversicherung von Männern: 30586.97 Rs
Durchschnittszahlungen an die Krankenversicherung von Frauen: 30459.0 Rs

Beobachtungen:

  • Es gibt unrealistisch hohe jährliche Zahlungen an die Krankenversicherung. Zudem bei den Frauen unrealistisch niedrige Zahlungen.
  • Männer und Frauen zahlen im Schnitt gleiche jährliche Zahlungen an die Krankenversicherung.

2.8.9 Interpretation der Variable Policy_Sales_Channel

Die Variable Policy_Sales_Channel beschreibt den Verkaufskanal, über den die bestehende Krankenversicherung abgeschlossen wurde.

Erwartungen:

  • Bei bestimmten Verkaufskanälen gibt es ein höheres Interesse an KFZ-Versicherungen
In [561]:
catplot_region_code = sns.catplot(x="Policy_Sales_Channel",y="Response", data=train_dataset, ci=None, aspect=4, kind="bar")
catplot_region_code.fig.subplots_adjust(top=0.88)
catplot_region_code.fig.suptitle("Balkendiagramm der Variable Policy_Sales_Channel in Zusammenhang mit Response", fontsize=30, weight='bold')
catplot_region_code.set_xticklabels([]) # is categorical variable, no information to gain from labels
catplot_region_code.set_xlabels("Policy_Sales_Channel", fontsize=20, weight='bold')
catplot_region_code.set_ylabels("Response in %", fontsize=20, weight='bold');

Beobachtungen:

  • Es gibt deutliche Unterschiede zwischen den Vertriebskanälen
    • Allerdings haben Vertriebskanäle mit wenigen Kunden extremere Werte, da die Stichprobengröße kleiner ist

Es müssen weitere Untersuchungen von prozentualer positiver Rückmeldung und Anzahl der Kunden für jeden Vertriebskanal vorgenommen werden.

Nachfolgend werden die Daten pro Vertriebskanal zusammengefasst, um deren Positivrückmeldungsrate im Vergleich zur Anzahl der betreuten Kunden einordnen zu können.

In [562]:
# get percentage of True response
percent =  train_dataset.groupby("Policy_Sales_Channel").sum() / train_dataset.groupby("Policy_Sales_Channel").count()
percent = percent.reset_index()
percent["Policy_Sales_Channel"] = percent["Policy_Sales_Channel"].astype(pd.CategoricalDtype(percent["Policy_Sales_Channel"].unique()))

# get count of all response
count = train_dataset.groupby("Policy_Sales_Channel").count()
count = count.reset_index()
count["Policy_Sales_Channel"] = count["Policy_Sales_Channel"].astype(pd.CategoricalDtype(count["Policy_Sales_Channel"].unique()))

# join results
combined = pd.merge(percent, count, how="inner", on=["Policy_Sales_Channel","Policy_Sales_Channel"], suffixes=["_percent", "_count"])
combined = combined.sort_values("Response_percent", ascending=False)

# trim useless columns
combined = combined[["Policy_Sales_Channel", "Response_percent", "Response_count"]]

no_positive_responses = combined.loc[combined["Response_percent"] == 0]
combined
# remove sales channels with no customers
combined = combined.loc[combined["Response_count"] > 0]
combined
p = sns.regplot(x="Response_count", y="Response_percent" ,data=combined)
p.set_xscale("log")
p.set_xlabel("Anzahl der Kunden (logarithmische Skala zur besseren Darstellung)", size=20, weight="bold")
p.set_ylabel("Anteil der True Responses in %", size=20, weight="bold")
p.set_title("Erfolgsquote und Anzahl der Kunden pro Vertriebskanal (1 Punkt je Kanal)", size=30, weight="bold");
In [563]:
combined
Out[563]:
Policy_Sales_Channel Response_percent Response_count
97 43.0 1.000000 1
27 123.0 1.000000 1
80 28.0 0.333333 3
79 27.0 0.333333 3
89 36.0 0.326923 52
... ... ... ...
131 76.0 0.000000 4
133 79.0 0.000000 6
53 149.0 0.000000 1
104 50.0 0.000000 2
154 99.0 0.000000 7

155 rows × 3 columns

In [564]:
print(f"Anzahl der Vertriebskanäle ohne positive Response: {len(no_positive_responses)}")
Anzahl der Vertriebskanäle ohne positive Response: 34

Beobachtungen:

  • Die Vertriebskanäle sind unabhängig von ihrer Größe mehr oder weniger erfolgreich
  • Es gibt große Abweichungen vom Mittelwert unabhängig von der Anzahl der Kunden
  • Wie erwartet haben die Vertriebskanäle mit 100% Positivrückmeldungsquote nur einen einzigen Kunden
    • Dennoch gibt es auch Vertriebskanäle mit über 1000 Kunden und rund 33% Positivquote
  • Die meisten Vertriebskanäle haben weniger als 1000 Kunden
  • Es gibt 34 Vertriebskanäle ohne positive Rückmeldung

2.8.10 Interpretation der Variable Vintage

Die Variable Vintage beschreibt die Dauer des Versicherungsverhältnisses im letzten Jahr.

Erwartung:

  • Besondere Salesevents oder Aktionen mit limitierter Laufzeit können als Peak erkannt werden
In [565]:
sns.set(rc={"figure.figsize": (22, 10)})
histplot = sns.histplot(data=train_dataset, x="Vintage", binwidth=7)      # binwidth = 7 days = 1 week
histplot.set_title("Histogram der Variable Vintage", fontsize=30, weight='bold')
histplot.set_xlabel("Vintage", fontsize=20, weight='bold')
histplot.set_ylabel("Count", fontsize=20, weight='bold');

Beobachtungen:

  • Beinahe Gleichverteilung von Vintage. Es scheinen keine Verkaufsaktionen stattgefunden zu haben, oder sie sind ohne Erfolg geblieben.
In [566]:
# Look at cheap contracts
d = train_dataset.loc[(train_dataset["Annual_Premium"] > 0) & (train_dataset["Annual_Premium"] < 3000)]

#d = d.loc[d["Response"] == True]

sns.set(rc={"figure.figsize": (22, 10)})
scatterplot = sns.scatterplot(data=d, x="Vintage", y="Annual_Premium", hue="Response")
scatterplot.set_xlabel("Vintage", fontsize=20, weight='bold')
scatterplot.set_title("Scatterplot der günstigen Tarife im Zusammenhang mit Vintage", fontsize=30, weight='bold')
scatterplot.set_ylabel("Count", fontsize=20, weight='bold');

Beobachtungen:

  • Es gibt keine besonderen Zeiträume, in denen der günstige Pauschaltarif besonders häufg abgeschlossen wird
  • Es gibt keine besonderen Zeiträume, in denen die Response besonders gut ist

2.8.11 Interpretation der Variable Response

Die Variable Response beschreibt das Interesse der Kunden an einer KFZ-Versicherung. Es ist die Zielvariable, die mithilfe eines Modells vorhergesagt werdern soll.

In [567]:
sns.set(rc={"figure.figsize": (22, 10)})
histplot = sns.histplot(data=train_dataset, x="Age", binwidth=5, hue="Response")
histplot.set_title("Histogramm der Variable Response in Zusammenhang mit Age ", fontsize=30, weight='bold')
histplot.set_xlabel("Age", fontsize=20, weight='bold')
histplot.set_ylabel("Count", fontsize=20, weight='bold');

Beobachtung:

  • Kunden im mittleren Alter (zwischen 30 Jahre und 60 Jahre) haben ein vergleichsweise höheres Interesse an einer Versicherung.
  • Ältere und jüngere Kunden haben ein überproportional geringes Interesse.
In [568]:
fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(20, 10))
colors = sns.color_palette('pastel')[0:2]
fig.suptitle("Kreisdiagramm der Variable Response in Zusammenhang mit Age", fontsize=20, weight="bold")
plt.subplots_adjust(top=1.2)

d_1 = train_dataset.loc[train_dataset["Age"] < 30]
d_2 = train_dataset.loc[(train_dataset["Age"] >= 30) & (train_dataset["Age"] < 60)]
d_3 = train_dataset.loc[(train_dataset["Age"] >= 60) & (train_dataset["Age"] < 100)] # remove false data

l = ["Response: True", "Response: False"]

p_1 = [len(d_1.loc[d_1["Response"] == True]),
       len(d_1.loc[d_1["Response"] == False])]
ax1.pie(p_1, labels=l, colors=colors, autopct='%.0f%%')
ax1.set_title("Age: < 30", weight="bold", fontsize=14)

p_2 = [len(d_2.loc[d_2["Response"] == True]),
       len(d_2.loc[d_2["Response"] == False])]
ax2.pie(p_2, labels=l, colors=colors, autopct='%.0f%%')
ax2.set_title("Age: >=30 und < 60", weight="bold", fontsize=14)

p_3 = [len(d_3.loc[d_3["Response"] == True]),
       len(d_3.loc[d_3["Response"] == False])]
ax3.pie(p_3, labels=l, colors=colors, autopct='%.0f%%')
ax3.set_title("Age: >= 60", weight="bold", fontsize=14);

Beobachtung:

  • Besonders in der Altersgruppe 30 Jahre bis 60 Jahre ist ein besonders großes Interesse an einer KFZ-Versicherung zu erkennen

3. Data Preparation

Die Erkenntnisse, die im Kapitel Data Understanding gewonnen wurden, werden nachfolgend angewandt, um invalide Daten zu entfernen und die Datenqualität zu erhöhen.

3.1 Ausreißer behandeln

3.1.1 Ausreißer innerhalb der Variable Age

  • Ab dem Alter >100 Jahre werden alle Werte in Missing Values umgewandelt, da dieses Alter nicht realitätsnah ist.
  • Diese Grenze wurde als großzügige Einschätzung des zu erwartenden Lebensalters festgelegt.
In [569]:
print(f"Es sind {len(train_dataset.loc[train_dataset['Age']> 100])} Datensätze von dieser Änderung betroffen.")
Es sind 100 Datensätze von dieser Änderung betroffen.
In [570]:
train_dataset.loc[train_dataset["Age"] > 100, "Age"] = np.NaN
train_dataset.loc[train_dataset["Age"] < 18, "Age"] = np.NaN

3.1.2 Ausreißer innerhalb der Variable Annual_Premium

In [571]:
train_dataset["Annual_Premium"].describe()
Out[571]:
count    380999.000000
mean      30527.700690
std       17242.997675
min       -9997.000000
25%       24371.000000
50%       31656.000000
75%       39390.000000
max      540165.000000
Name: Annual_Premium, dtype: float64

Negative Werte für Annual_Premium sind nicht valide. Es würde bedeuten, dass die Versicherungsgesellschaft den Kunden bezahlt.

In [572]:
print(f"Es sind {len(train_dataset.loc[train_dataset['Annual_Premium']< 0])} Datensätze von dieser Änderung betroffen.")
Es sind 367 Datensätze von dieser Änderung betroffen.
In [573]:
# remove negative values
train_dataset.loc[train_dataset["Annual_Premium"] < 0, "Annual_Premium"] = np.NaN

3.2 Analyse der nicht vorhandenen Werte

3.2.1 Löschen der 51 fehlerhaften Datensätze

Wie im Abschnitt 2.6.2 beschrieben wurden 51 Datensätze in den Spalten Driving_License, Previously_Insured, Vehicle_Age, Vehicle_Damage und Vintage mit Missing Values gefunden, die zum selben Datensatz gehören. Da diese keinen signifikanten Einfluss auf das Modell haben werden, werden sie entfernt.

In [574]:
# remove 51 data sets with missing values
# NaN_in_selected_columns was generated before (in section 2.6.2) and contains 51 data sets that we want to remove
train_dataset = train_dataset.loc[~train_dataset["id"].isin(NaN_in_selected_columns["id"].to_numpy())]  # ~ = not
In [575]:
train_dataset.isna().sum()
Out[575]:
id                          0
Gender                   1000
Age                     10941
Driving_License             0
Region_Code                 0
Previously_Insured          0
Vehicle_Age                 0
Vehicle_Damage              0
Annual_Premium            367
Policy_Sales_Channel        0
Vintage                     0
Response                    0
dtype: int64

3.3 Train/Test-Split

Für den Split unterteilen wir die Daten aus der "train.csv" in Trainingsdaten und Testdaten. Hier wird ein 70/30-Split genutzt.

  • Es wird eine Teilung in 70% Trainingsdaten und 30% Testdaten vorgenommen.
  • Der Algorithmus lernt aus den Trainingsdaten und dient zum Trainieren des Modells.
  • Die Testdaten sind unabhängig von den Trainingsdaten und werden beim Training des Modells nicht benutzt.
In [576]:
#Features
X = train_dataset.copy(deep=True)
X.drop("Response", axis="columns", inplace=True)
X.drop("id", axis="columns", inplace=True)

#labels (X_train = all columns except Response / y_train = only Response //// X_test = all columns except Response / y_test = only Response)
y = train_dataset['Response'].copy(deep=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)

# help IDE understand that we are still dealing with data frames
X_train = pd.DataFrame(X_train)
X_test = pd.DataFrame(X_test)

3.4 Imputation der fehlenden Werte

Imputationsstrategie:
Die Imputation erfolgt anhand der nachfolgenden Prozedur. Es werden verschiedene Imputationsstrategien (mean, median, hot_code_locf und most_frequent) ausprobiert.

  • Die Imputation der fehlenden Werte wird für die Trainingsdaten "X_train" und für die Testdaten "X_test" separat gemacht.
In [577]:
def random_imputation(data, col_name):
    number_missing = data[col_name].isna().sum()
    existing_vals = data.loc[data[col_name].notna(), col_name]

    data.loc[data[col_name].isna(), col_name +
             "_imp"] = np.random.choice(existing_vals, number_missing, replace=True)
    # print(data)
    return data


# strategies: mean, median, hot_code_locf, most_frequent, KNN, regression
def impute_data(data: pd.DataFrame, col_name, strategy, y_data=None):
    if(strategy == "hot_code_locf"):
        return data[col_name].fillna(method="ffill")

    elif(strategy == "KNN"):
        # prepare data
        ref_data = pd.concat([data, pd.get_dummies(
            data["Gender"], prefix="Gender_is_")], axis=1)
        ref_data = pd.concat([data, pd.get_dummies(
            data["Vehicle_Age"], prefix="Vehicle_Age_is_")], axis=1)
        ref_data = data.loc[:, ~data.columns.duplicated()]

        # KNN imputation
        imputer = KNNImputer(missing_values=np.nan, n_neighbors=2)
        col_data = imputer.fit_transform(
            ref_data.select_dtypes(["number", "boolean"]), y_data)

        # restore column names
        col_data = pd.DataFrame(col_data, columns=ref_data.columns)

        return col_data[col_name]

    elif(strategy == "regression"):

        type = data[col_name].dtype.name
        # col_data = pd.DataFrame(columns=["Missing_" + col_name], dtype=type)
        # col_data["Missing_" + col_name] = data[col_name + "_imp"]

        # prepare non numerical cols
        data = pd.concat([data, pd.get_dummies(
            data["Gender"], prefix="Gender_is_")], axis=1)
        data = pd.concat([data, pd.get_dummies(
            data["Vehicle_Age"], prefix="Vehicle_Age_is_")], axis=1)
        data = data.loc[:, ~data.columns.duplicated()]

        # remove non numeric
        d = data.select_dtypes(include=["number"], exclude=["bool"])

        # t-t-split
        test = d.loc[d["Age"].isna()]
        y_test =test["Age"]
        test = test.drop(columns=["Age"])

        train = d.loc[d["Age"].notna()]

        # remove A_P NaNs        
        train = train.loc[train["Annual_Premium"].notna()]
        y_train = train["Age"]
        train = train.drop(columns=["Age"])
     
        # model
        clf = LinearRegression()
        fit = clf.fit(X=train, y=y_train)

        prediction = clf.predict(test)
        prediction = prediction.astype(type)

        unique, counts = np.unique(prediction, return_counts=True)
        print(dict(zip(unique, counts)))
   
        d[col_name].loc[d[col_name].isna()] = prediction
        return d[col_name]

    else:
        imputer = SimpleImputer(strategy=strategy, missing_values=np.NaN)
        fit = imputer.fit(data[[col_name]])
        col_data = fit.transform(data[[col_name]])
        return col_data

3.4.1 Ersetzung der fehlenden Werte numerischer Variablen

3.4.1.1 Imputation der Variable Age

In [578]:
#Trainingsdaten
X_train["Age"] = impute_data(X_train, "Age", "regression")

#Testdaten
X_test["Age"] = impute_data(X_test, "Age", "regression")
{23: 443, 24: 1585, 25: 1127, 26: 95, 27: 12, 28: 3, 29: 1, 47: 326, 48: 1269, 49: 2083, 50: 288, 51: 31, 52: 7, 53: 57, 54: 66, 55: 171, 56: 31, 58: 2, 62: 1}
{23: 180, 24: 771, 25: 462, 26: 34, 27: 4, 28: 2, 30: 1, 31: 1, 47: 138, 48: 622, 49: 863, 50: 104, 51: 8, 52: 1, 53: 31, 54: 35, 55: 75, 56: 11}

3.4.1.2 Imputation der Variable Annual_Premium

In [579]:
#Trainingsdaten
X_train["Annual_Premium"] = impute_data(X_train, "Annual_Premium", "median")

#Testdaten
X_test["Annual_Premium"] = impute_data(X_test, "Annual_Premium", "median")

3.4.2 Ersetzung der fehlenden Werte kategorialer Variablen

3.4.2.1 Imputation der Variable Gender

In [580]:
#Trainingsdaten
X_train["Gender"] = impute_data(X_train, "Gender", "most_frequent")

#Testdaten
X_test["Gender"] = impute_data(X_test, "Gender", "most_frequent")

#cast Gender to category Datatype again:
#Trainingsdaten
X_train["Gender"] = X_train["Gender"].astype(pd.CategoricalDtype())

#Testdaten
X_test["Gender"] = X_test["Gender"].astype(pd.CategoricalDtype())

3.4.3 Überprüfung der Imputationen

In [581]:
print(f'Missing Values in der Spalte Age: Test = {X_test["Age"].isna().sum()}, Training = {X_train["Age"].isna().sum()}')
print(f'Missing Values in der Spalte Annual_Premium: Test = {X_test["Annual_Premium"].isna().sum()}, Training = {X_train["Annual_Premium"].isna().sum()}')
print(f'Missing Values in der Spalte Gender: Test = {X_test["Gender"].isna().sum()}, Training = {X_train["Gender"].isna().sum()}')
Missing Values in der Spalte Age: Test = 0, Training = 0
Missing Values in der Spalte Annual_Premium: Test = 0, Training = 0
Missing Values in der Spalte Gender: Test = 0, Training = 0

Die Imputation war erfolgreich und alle Missing Values in den Trainingsdaten und Testdaten wurden ersetzt.

3.5. Sampling

Zunächst muss die Zielvariable Response wieder zu den Trainingsdaten und Testdaten hinzugefügt werden, da wir beim Sampling die Zielvariable betrachten.

  • Insgesamter Datensatz der Zielvariable im X_train beträgt: 266.663 Datensätze
  • Davon macht True 12% des Datensatzes aus. Dies ist die minority Class
  • Davon macht False 88% des Datensatzes aus. Dies ist die majority Class

  • Insgesamter Datensatz der Zielvariable im X_test beträgt: 114.285 Datensätze
  • Davon macht True 12% des Datensatzes aus. Dies ist die minority Class
  • Davon macht False 88% des Datessatzes aus. Dies ist die majority Class

Mit der Methode des zufälligen Oversamplings werden Datensätze aus der Minderheitsklasse, in dem Fall Response True, zufällig ausgewählt und dupliziert und dem Trainingsdatensatz hinzugefügt. Beim zufälligen Undersampling werden Datensätze aus der Mehrheitsklasse, in dem Fall Response False, zufällig ausgewählt und aus dem Trainingsdatensatz entfernt.

  • Aus einer unbalancierten Klassenverteilung wird zwischen der minority und majority class ein Gleichgewicht hergestellt.
In [582]:
# re-add Response

#insert in X_train --> y_train 
X_train.insert(len(X_train.columns), value=y_train, column="Response")

#insert in X_test --> y_test
X_test.insert(len(X_test.columns), value=y_test, column="Response")
In [583]:
# functions for visualization

# plot output
def plot_prop_of_split(train, test, col_name, sub_heading=""):
    fig, ax = plt.subplots(1, 2, figsize=(20, 10))

    #Plot for Trainingdata
    cp_1 = sns.countplot(data=train, x=col_name, ax=ax[0])
    cp_1.set_title("Trainingsdaten", weight="bold", fontsize=14)
    cp_1.set_xlabel("Response", fontsize=20, weight='bold')
    cp_1.set_ylabel("Count", fontsize=20, weight='bold');

    #Plot for Testdata
    cp_2 = sns.countplot(data=test, x=col_name, ax=ax[1])
    cp_2.set_title("Testdaten", weight="bold", fontsize=14)
    cp_2.set_xlabel("Response", fontsize=20, weight='bold')
    cp_2.set_ylabel("Count", fontsize=20, weight='bold');

   #Title over both charts
    fig.suptitle(f"Verteilung der Variable {col_name} in Trainingsdaten und Testdaten\n {sub_heading}", weight="bold", fontsize=30)
    fig.show()


# console output
def print_class_len_and_ratio(data: pd.DataFrame, col_name):

    # minority_class
    minority_class_len = len(data[data[col_name] == True])
    print(f"Die Variable {col_name} enthält {minority_class_len} Datensätze die den Wert True enthalten.")

    # majority_class
    majority_class_len = len(data[data[col_name] == False])
    print(f"Die Variable {col_name} enthält {majority_class_len} Datensätze die den Wert False enthalten.")

    # ratio
    print(train_dataset["Response"].value_counts(normalize=True))
In [584]:
plot_prop_of_split(X_train, X_test, "Response", "(vor Sampling)")
In [585]:
print_class_len_and_ratio(X_train, "Response")
print("-"*50)
print_class_len_and_ratio(X_test, "Response")
Die Variable Response enthält 32926 Datensätze die den Wert True enthalten.
Die Variable Response enthält 233737 Datensätze die den Wert False enthalten.
False    0.877424
True     0.122576
Name: Response, dtype: float64
--------------------------------------------------
Die Variable Response enthält 13769 Datensätze die den Wert True enthalten.
Die Variable Response enthält 100516 Datensätze die den Wert False enthalten.
False    0.877424
True     0.122576
Name: Response, dtype: float64

3.5.1. Undersampling

In [586]:
def undersample(data: pd.DataFrame, col_name):
    # Variable values count as integer
    response_false_count, response_true_count = data[col_name].value_counts()

    # Seperate in bool values (True and False values)
    seperate_response_false = data[data[col_name] == False]
    seperate_response_true = data[data[col_name] == True]

    # Undersampling to balance imbalanced datasets --> deleting samples from the majority class
    response_false_undersampling = seperate_response_false.sample(response_true_count, random_state=42)
    undersampling = pd.concat([response_false_undersampling, seperate_response_true], axis=0)

    return undersampling

Datensätze aus der majority_class werden zufällig entfernt.

  • Daraus entsteht eine identische Anzahl an Datensätzen für die Zielvariable Response mit den Ausprägungen True und False.
  • Der Datensatz wird balanciert, indem die gleiche Anzahl an Datensätzen von True zufällig für False gezogen wird.
In [587]:
# X_train run through the undersample function from section 3.5.1
X_train_undersampling = undersample(X_train, "Response")

# After the undersampling process X_train_undersampling will be plotted (plot function from section 3.5)
plot_prop_of_split(X_train_undersampling, X_test, "Response", "(nach Undersampling)")

# console output from X_train_Undersampling
print_class_len_and_ratio(X_train_undersampling, "Response")
Die Variable Response enthält 32926 Datensätze die den Wert True enthalten.
Die Variable Response enthält 32926 Datensätze die den Wert False enthalten.
False    0.877424
True     0.122576
Name: Response, dtype: float64

3.5.2. Oversampling

In [588]:
def oversample(data: pd.DataFrame, col_name):
    # Variable values count as integer
    response_false_count, response_true_count = data[col_name].value_counts()

    # Seperate in bool values (True and False values)
    seperate_response_false = data[data[col_name] == False]
    seperate_response_true = data[data[col_name] == True]

    # Oversampling to balance imbalanced datasets --> generate samples from the minority class
    response_true_oversampling = seperate_response_true.sample(response_false_count, replace=True, random_state=42)
    oversampling = pd.concat([response_true_oversampling, seperate_response_false], axis=0)

    return oversampling  

Datensätze aus der minority_class werden durch Generierung künstlicher Beispiele aufgestockt.

  • Daraus entsteht eine identische Anzahl an Datensätzen für die Zielvariable Response mit den Ausprägungen True und False.
  • Der Datensatz wird balanciert, indem die gleiche Anzahl an Datensätzen von False künstlich für True erzeugt wird.
In [589]:
# X_train run through the oversample function from section 3.5.2
X_train_oversampling = oversample(X_train, "Response")

# new rows were added to X_train by Oversampling, but they dont have an index yet
X_train_oversampling = X_train_oversampling.reset_index()

# After the oversampling process X_train_oversampling will be plotted (plot function from section 3.5)
plot_prop_of_split(X_train_oversampling, X_test, "Response", "(nach Oversampling)")

# console output from X_train_oversampling
print_class_len_and_ratio(X_train_oversampling, "Response")
Die Variable Response enthält 233737 Datensätze die den Wert True enthalten.
Die Variable Response enthält 233737 Datensätze die den Wert False enthalten.
False    0.877424
True     0.122576
Name: Response, dtype: float64

3.5.3. Cleanup

Die gesplitteten Trainingsdaten und Testdaten mussten für das Over- und Undersampling wieder mit der Zielvariable Response verknüpft werden. Im Fall von Oversampling wurden neue Datensätze erzeugt und im Fall von Undersampling wurden Datensätze entfernt, um ein Gleichgewicht der Klassen zu schaffen. Beides führt dazu, dass der gesampelte Datensatz X_train (Undersampling oder Oversampling) nicht mehr zu dem ursprünglichen Datensatz der Zielvariable y_train passt, da Länge und Zuordnung der Werte des jeweiligen gesampelten Datensatzes nicht mehr übereinstimmt. Deshalb wird nur die Spalte mit der gesampelten (Undersamling oder Oversampling) Zielvariable einer neuen Variable zugewiesen damit wir diese neue Variable nachfolgend verwenden können.

In [590]:
# nach dem Undersampling wird die Zielvariable Response der Variable "y_train_undersampling" zugewiesen
y_train_undersampling = X_train_undersampling["Response"]

# nach dem Oversampling wird die Zielvariable Response der Variable "y_train_oversampling" zugewiesen
y_train_oversampling = X_train_oversampling["Response"]

Anschließend wird die Zielvariable wieder von den Trainingsdaten und Testdaten entfernt.

In [591]:
# Remove Response from the undersampled dataset
X_train_undersampling = X_train_undersampling.drop("Response", axis="columns")

# Remove Response from the oversampled dataset
X_train_oversampling = X_train_oversampling.drop("Response", axis="columns")

3.5.4. Under- vs. Oversampling

Bei einem stark unausgeglichenen Datensatz kann das Oversampling dazu führen das die Minterheitsklasse überangepasst wird, da die Wahrscheinlichkeit größer ist das exakte Kopien der Datensätze für die Minderheitsklasse erstellt werden.

Im vorliegenden Datensatz stehen die Minderheiten- und Mehrheitenklasse im Verhältnis 12:88, daher verwenden wir Undersampling.

In [592]:
# set to undersampling to work with this dataset. 
# X_train = the undersampled dataset without response
# y_train = the undersampled dataset only with response

X_train = X_train_undersampling
y_train = y_train_undersampling
print("Using Undersampling")
Using Undersampling

3.6. Feature Engineering

3.6.1. Altersklassen als Feature

  • Eingrenzung der Variable Age in Oktile (q=8) um die Intervalle festzustellen.
  • Das Ergebnis der Funktion ".qcut" ist eine Variable des Datentypes "Category" da jedes Intervall (bin) einer Kategorie entspricht.
  • Der Kategorien der Variable Age_bin für den X_train und X_test Datensatz sind:
    • [20.0 - 23.0[ < [23.0 - 25.0[ < [25.0 - 28.0[ < [28.0 - 36.0[ < [36.0 - 43.0[ < [43.0 - 49.0[ < [49.0 - 59.0[ < [59.0 - 85.0[
In [593]:
# Trainigsdaten
X_train['Age_bins'] = pd.qcut(train_dataset["Age"], q=8)
print(f"Age Bins for Trainingdata:\n{X_train['Age_bins'].value_counts().sort_index()}\n")

# Testdaten
X_test['Age_bins'] = pd.qcut(train_dataset["Age"], q=8)
print(f"Age Bins for Testdata:\n{X_test['Age_bins'].value_counts().sort_index()}\n")

###

# Dataset from the test.csv
real_dataset["Age_bins"] = pd.qcut(train_dataset["Age"], q=8)
print(f"Age Bins for test.csv:\n{real_dataset['Age_bins'].value_counts().sort_index()}")
Age Bins for Trainingdata:
(19.999, 23.0]     7828
(23.0, 25.0]       5391
(25.0, 28.0]       4122
(28.0, 36.0]       8862
(36.0, 43.0]      10891
(43.0, 49.0]       9995
(49.0, 59.0]       9682
(59.0, 85.0]       7173
Name: Age_bins, dtype: int64

Age Bins for Testdata:
(19.999, 23.0]    20007
(23.0, 25.0]      13675
(25.0, 28.0]       9682
(28.0, 36.0]      12809
(36.0, 43.0]      14295
(43.0, 49.0]      13151
(49.0, 59.0]      13801
(59.0, 85.0]      13522
Name: Age_bins, dtype: int64

Age Bins for test.csv:
(19.999, 23.0]    22584
(23.0, 25.0]      15478
(25.0, 28.0]      11294
(28.0, 36.0]      14678
(36.0, 43.0]      16185
(43.0, 49.0]      15305
(49.0, 59.0]      15977
(59.0, 85.0]      15529
Name: Age_bins, dtype: int64
In [594]:
# split Age in 8 categories --> octiles
octiles_list = [0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1]
octiles = X_train["Age"].quantile(octiles_list)

# Plot Age as histplot
plt.figure(figsize= (22, 10));
fig, ax = plt.subplots();
X_train['Age'].hist(bins=65, color='#A9C5D3', 
                             edgecolor='black', grid=False)

# Plot the octiles as axvplot over the histplot
for quantile in octiles:
    axvlineplot = plt.axvline(quantile, color='r')
ax.legend([axvlineplot], ['Oktil'], fontsize=16)

ax.set_title('Age Histogramm mit Oktile', 
             fontsize=24, weight="bold")
ax.set_xlabel('Age', fontsize=14, weight="bold")
ax.set_ylabel('Count', fontsize=14, weight="bold")

# start graph just before 20 (no smaller values)
ax.set_xlim(19, 90)

# show ticks at octiles and steps of 10
ax.set_xticks([*octiles.to_numpy(), *range(20,90,10)]);
<Figure size 1584x720 with 0 Axes>

Beobachtung:

  • Der Peak bei 48-50 ist auf die Imputation durch Regression zurückzuführen. Da nur wenige überwiegend kategoriale Variablen zur Verfügung stehen, werden die Werte für Age relativ grob klassifiziert.
In [595]:
# define labels
def labels():
    q = train_dataset["Age"].quantile(octiles_list).to_numpy()
    i = 0
    labels = []
    while i < len(q)-1:
        labels.append(f"[{q[i]} - {q[i+1]}[")
        i+=1

    return labels

# function for numerical_binning
def numerical_binning(data):
        data['Age_bins'] = pd.qcut(
            data.Age, q=8, labels=labels()
            )
        return data['Age_bins'].value_counts().sort_index()


# Trainingsdaten
numerical_binning(X_train)

# Testdaten
numerical_binning(X_test)


###

# Dataset from the test.csv
numerical_binning(real_dataset);
In [596]:
sns.set(rc={"figure.figsize": (22, 10)})
histplot =sns.histplot(X_train, x="Age", hue="Age_bins", bins=65)

histplot.set_title('Histogram der Variable Age in Zusammenhang mit Age_bins', 
             fontsize=24, weight="bold")
histplot.set_xlabel('Age', fontsize=14, weight="bold")
histplot.set_ylabel('Count', fontsize=14, weight="bold")
histplot.set_xlim(19, 85)
histplot.set_xticks(range(20,90,1));

3.6.2 Features durch Aggregationen, Differenzen und Verhältnisse

In [597]:
# Create features function
def feature_encoding(data, col_name):
    selected_categorical_columns = data.select_dtypes(include=["category"])
    for categorical_columns in selected_categorical_columns:

        # mean encoding using numeric variable//
        # group by categorical variables and obtain the mean over the numeric variable
        mean = data.groupby(categorical_columns)[col_name].agg(['mean'])
        mean.columns = [f'mean_{col_name}_by_' + categorical_columns]

        # joining the column to the main dataset 
        data = pd.merge(data, mean, left_on=categorical_columns,
                        right_index=True, how='left')

        # difference between the numerical variable and the mean grouped by the categorical variables over the numeric one.
        data[f"diff_{col_name}_mean_by_" + categorical_columns] = data[col_name] - \
            data[f"mean_{col_name}_by_" + categorical_columns]

        # percentage of the difference
        perc = data[f"diff_{col_name}_mean_by_" + categorical_columns].abs() / data[f'mean_{col_name}_by_' + categorical_columns]
        data[f"prop_{col_name}_mean_by_" + categorical_columns] = perc
                                                                       
    return data
In [598]:
# Trainingsdaten
print("Creating features for Trainingdata:")
for col in X_train.select_dtypes(include=["number"], exclude=["bool", "boolean"]):
    print(f"Creating features for {col}")
    X_train = feature_encoding(X_train, col)
Creating features for Trainingdata:
Creating features for Age
Creating features for Annual_Premium
Creating features for Vintage
In [599]:
# Testdaten
print("Creating features for Testdata:")
for col in X_test.select_dtypes(include=["number"], exclude=["bool", "boolean"]):
    print(f"Creating features for {col}")
    X_test = feature_encoding(X_test, col)
Creating features for Testdata:
Creating features for Age
Creating features for Annual_Premium
Creating features for Vintage
In [600]:
# Dataset from the test.csv
print("Creating features for the test.csv:")
for col in real_dataset.select_dtypes(include=["number"], exclude=["bool", "boolean"]):
    print(f"Creating features for {col}")
    real_dataset = feature_encoding(real_dataset, col)
Creating features for the test.csv:
Creating features for id
Creating features for Age
Creating features for Annual_Premium
Creating features for Vintage

3.6.3 One-Hot-Encoding für kategoriale Variablen

Konvertieren von kategorialen Variablen in Dummy/Indikator-Variablen:

In [601]:
# Trainingsdaten
X_train = pd.concat([X_train, pd.get_dummies(X_train["Gender"], prefix="Gender_is_")], axis=1)
X_train = pd.concat([X_train, pd.get_dummies(X_train["Vehicle_Age"], prefix="Vehicle_Age_is_")], axis=1)
X_train = pd.concat([X_train, pd.get_dummies(X_train["Age_bins"], prefix="Age_bins_is")], axis=1)
X_train = X_train.loc[:,~X_train.columns.duplicated()]
print(f"Wir starten mit {len(X_train.columns)} Features in die Featureselektion")

# Testdaten
X_test = pd.concat([X_test, pd.get_dummies(X_test["Gender"], prefix="Gender_is_")], axis=1)
X_test = pd.concat([X_test, pd.get_dummies(X_test["Vehicle_Age"], prefix="Vehicle_Age_is_")], axis=1)
X_test = pd.concat([X_test, pd.get_dummies(X_test["Age_bins"], prefix="Age_bins_is")], axis=1)
X_test = X_test.loc[:,~X_test.columns.duplicated()]

###

# Dataset from the test.csv
real_dataset = pd.concat([real_dataset, pd.get_dummies(real_dataset["Gender"], prefix="Gender_is_")], axis=1)
real_dataset = pd.concat([real_dataset, pd.get_dummies(real_dataset["Vehicle_Age"], prefix="Vehicle_Age_is_")], axis=1)
real_dataset = pd.concat([real_dataset, pd.get_dummies(real_dataset["Age_bins"], prefix="Age_bins_is")], axis=1)
real_dataset = real_dataset.loc[:,~real_dataset.columns.duplicated()]
Wir starten mit 69 Features in die Featureselektion

3.7 Feature Selection

  • Bei der Feature Selection wählen wir die Features aus, die für den machine-learning-process verwendet werden.
  • Hierbei ist darauf zu achten, dass nur relevante Features zur Modellbildung verwendet werden sollten, da sonst eine Überanpassung des Modells stattfinden kann.
  • Durch die neu hinzugekommenen Features aus dem Feature Engineering umfasst der Datensatz nun 69 Spalten.
In [602]:
pd.set_option('display.max_columns', 100)
X_train
Out[602]:
Gender Age Driving_License Region_Code Previously_Insured Vehicle_Age Vehicle_Damage Annual_Premium Policy_Sales_Channel Vintage Age_bins mean_Age_by_Gender diff_Age_mean_by_Gender prop_Age_mean_by_Gender mean_Age_by_Region_Code diff_Age_mean_by_Region_Code prop_Age_mean_by_Region_Code mean_Age_by_Vehicle_Age diff_Age_mean_by_Vehicle_Age prop_Age_mean_by_Vehicle_Age mean_Age_by_Policy_Sales_Channel diff_Age_mean_by_Policy_Sales_Channel prop_Age_mean_by_Policy_Sales_Channel mean_Age_by_Age_bins diff_Age_mean_by_Age_bins prop_Age_mean_by_Age_bins mean_Annual_Premium_by_Gender diff_Annual_Premium_mean_by_Gender prop_Annual_Premium_mean_by_Gender mean_Annual_Premium_by_Region_Code diff_Annual_Premium_mean_by_Region_Code prop_Annual_Premium_mean_by_Region_Code mean_Annual_Premium_by_Vehicle_Age diff_Annual_Premium_mean_by_Vehicle_Age prop_Annual_Premium_mean_by_Vehicle_Age mean_Annual_Premium_by_Policy_Sales_Channel diff_Annual_Premium_mean_by_Policy_Sales_Channel prop_Annual_Premium_mean_by_Policy_Sales_Channel mean_Annual_Premium_by_Age_bins diff_Annual_Premium_mean_by_Age_bins prop_Annual_Premium_mean_by_Age_bins mean_Vintage_by_Gender diff_Vintage_mean_by_Gender prop_Vintage_mean_by_Gender mean_Vintage_by_Region_Code diff_Vintage_mean_by_Region_Code prop_Vintage_mean_by_Region_Code mean_Vintage_by_Vehicle_Age diff_Vintage_mean_by_Vehicle_Age prop_Vintage_mean_by_Vehicle_Age mean_Vintage_by_Policy_Sales_Channel diff_Vintage_mean_by_Policy_Sales_Channel prop_Vintage_mean_by_Policy_Sales_Channel mean_Vintage_by_Age_bins diff_Vintage_mean_by_Age_bins prop_Vintage_mean_by_Age_bins Gender_is__Female Gender_is__Male Vehicle_Age_is__1-2 Year Vehicle_Age_is__< 1 Year Vehicle_Age_is__> 2 Years Age_bins_is_[20.0 - 23.0[ Age_bins_is_[23.0 - 25.0[ Age_bins_is_[25.0 - 28.0[ Age_bins_is_[28.0 - 36.0[ Age_bins_is_[36.0 - 43.0[ Age_bins_is_[43.0 - 49.0[ Age_bins_is_[49.0 - 59.0[ Age_bins_is_[59.0 - 85.0[
356104 Female 27 False 27.0 False < 1 Year False 2630.0 152.0 72 [23.0 - 25.0[ 38.669326 -11.669326 0.301772 32.260047 -5.260047 0.163051 25.235889 1.764111 0.069905 26.255352 0.744648 0.028362 25.765911 1.234089 0.047896 30813.541851 -28183.541851 0.914648 22735.016548 -20105.016548 0.884319 29947.585203 -27317.585203 0.912180 30735.522440 -28105.522440 0.914431 30452.658925 -27822.658925 0.913636 153.764211 -81.764211 0.531751 150.957447 -78.957447 0.523044 153.252407 -81.252407 0.530187 153.562613 -81.562613 0.531136 154.478603 -82.478603 0.533916 1 0 0 1 0 0 1 0 0 0 0 0 0
141427 Female 53 False 31.0 False 1-2 Year True 2630.0 26.0 238 [49.0 - 59.0[ 38.669326 14.330674 0.370595 43.481481 9.518519 0.218910 47.250798 5.749202 0.121674 49.339464 3.660536 0.074191 54.072548 -1.072548 0.019835 30813.541851 -28183.541851 0.914648 2630.000000 0.000000 0.000000 30811.480444 -28181.480444 0.914642 34754.981894 -32124.981894 0.924327 32675.824088 -30045.824088 0.919512 153.764211 84.235789 0.547824 154.950617 83.049383 0.535973 153.236031 84.763969 0.553160 153.491551 84.508449 0.550574 153.665126 84.334874 0.548822 1 0 1 0 0 0 0 0 0 0 0 1 0
235662 Male 22 False 3.0 False < 1 Year True 29624.0 160.0 122 [20.0 - 23.0[ 42.454045 -20.454045 0.481793 38.471230 -16.471230 0.428144 25.235889 -3.235889 0.128226 23.567320 -1.567320 0.066504 22.556359 -0.556359 0.024665 31104.720510 -1480.720510 0.047604 24313.514234 5310.485766 0.218417 29947.585203 -323.585203 0.010805 24737.126719 4886.873281 0.197552 29889.498303 -265.498303 0.008883 152.985388 -30.985388 0.202538 153.121139 -31.121139 0.203245 153.252407 -31.252407 0.203928 151.522718 -29.522718 0.194840 153.090121 -31.090121 0.203084 0 1 0 1 0 1 0 0 0 0 0 0 0
63651 Female 24 False 14.0 False < 1 Year True 36948.0 152.0 94 [20.0 - 23.0[ 38.669326 -14.669326 0.379353 35.467974 -11.467974 0.323333 25.235889 -1.235889 0.048973 26.255352 -2.255352 0.085901 22.556359 1.443641 0.064001 30813.541851 6134.458149 0.199083 25093.322876 11854.677124 0.472424 29947.585203 7000.414797 0.233756 30735.522440 6212.477560 0.202127 29889.498303 7058.501697 0.236153 153.764211 -59.764211 0.388674 154.500654 -60.500654 0.391588 153.252407 -59.252407 0.386633 153.562613 -59.562613 0.387872 153.090121 -59.090121 0.385983 1 0 0 1 0 1 0 0 0 0 0 0 0
367760 Male 54 False 15.0 False 1-2 Year True 31437.0 152.0 72 [49.0 - 59.0[ 42.454045 11.545955 0.271964 34.345196 19.654804 0.572272 47.250798 6.749202 0.142838 26.255352 27.744648 1.056724 54.072548 -0.072548 0.001342 31104.720510 332.279490 0.010683 28613.426029 2823.573971 0.098680 30811.480444 625.519556 0.020302 30735.522440 701.477560 0.022823 32675.824088 -1238.824088 0.037913 152.985388 -80.985388 0.529367 155.221149 -83.221149 0.536146 153.236031 -81.236031 0.530137 153.562613 -81.562613 0.531136 153.665126 -81.665126 0.531449 0 1 1 0 0 0 0 0 0 0 0 1 0
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
328998 Female 47 False 28.0 False 1-2 Year True 41927.0 26.0 150 [43.0 - 49.0[ 38.669326 8.330674 0.215434 46.023225 0.976775 0.021224 47.250798 -0.250798 0.005308 49.339464 -2.339464 0.047416 47.964471 -0.964471 0.020108 30813.541851 11113.458149 0.360668 38850.760213 3076.239787 0.079181 30811.480444 11115.519556 0.360759 34754.981894 7172.018106 0.206359 32295.475637 9631.524363 0.298231 153.764211 -3.764211 0.024480 153.449473 -3.449473 0.022480 153.236031 -3.236031 0.021118 153.491551 -3.491551 0.022748 154.206970 -4.206970 0.027281 1 0 1 0 0 0 0 0 0 0 1 0 0
262964 Male 42 False 28.0 False 1-2 Year True 39506.0 163.0 34 [36.0 - 43.0[ 42.454045 -0.454045 0.010695 46.023225 -4.023225 0.087417 47.250798 -5.250798 0.111126 38.495157 3.504843 0.091046 43.511334 -1.511334 0.034734 31104.720510 8401.279490 0.270097 38850.760213 655.239787 0.016866 30811.480444 8694.519556 0.282184 24449.302663 15056.697337 0.615833 32098.317351 7407.682649 0.230781 152.985388 -118.985388 0.777757 153.449473 -119.449473 0.778429 153.236031 -119.236031 0.778120 151.543584 -117.543584 0.775642 152.552823 -118.552823 0.777126 0 1 1 0 0 0 0 0 0 1 0 0 0
64820 Male 30 False 30.0 False < 1 Year True 26651.0 154.0 50 [25.0 - 28.0[ 42.454045 -12.454045 0.293354 33.744451 -3.744451 0.110965 25.235889 4.764111 0.188783 39.676451 -9.676451 0.243884 31.448053 -1.448053 0.046046 31104.720510 -4453.720510 0.143185 24654.484348 1996.515652 0.080980 29947.585203 -3296.585203 0.110078 28411.532290 -1760.532290 0.061965 27587.091619 -936.091619 0.033932 152.985388 -102.985388 0.673171 152.242459 -102.242459 0.671577 153.252407 -103.252407 0.673741 151.813438 -101.813438 0.670648 152.421828 -102.421828 0.671963 0 1 0 1 0 0 0 1 0 0 0 0 0
374922 Female 54 False 36.0 False 1-2 Year True 2630.0 26.0 189 [49.0 - 59.0[ 38.669326 15.330674 0.396456 33.950833 20.049167 0.590535 47.250798 6.749202 0.142838 49.339464 4.660536 0.094459 54.072548 -0.072548 0.001342 30813.541851 -28183.541851 0.914648 28486.157018 -25856.157018 0.907674 30811.480444 -28181.480444 0.914642 34754.981894 -32124.981894 0.924327 32675.824088 -30045.824088 0.919512 153.764211 35.235789 0.229155 153.222046 35.777954 0.233504 153.236031 35.763969 0.233391 153.491551 35.508449 0.231338 153.665126 35.334874 0.229947 1 0 1 0 0 0 0 0 0 0 0 1 0
131983 Male 25 False 15.0 False < 1 Year True 38308.0 152.0 264 [23.0 - 25.0[ 42.454045 -17.454045 0.411128 34.345196 -9.345196 0.272096 25.235889 -0.235889 0.009347 26.255352 -1.255352 0.047813 25.765911 -0.765911 0.029726 31104.720510 7203.279490 0.231582 28613.426029 9694.573971 0.338812 29947.585203 8360.414797 0.279168 30735.522440 7572.477560 0.246375 30452.658925 7855.341075 0.257953 152.985388 111.014612 0.725655 155.221149 108.778851 0.700799 153.252407 110.747593 0.722648 153.562613 110.437387 0.719168 154.478603 109.521397 0.708975 0 1 0 1 0 0 1 0 0 0 0 0 0

65852 rows × 69 columns

  • Um sich eine Übersicht über die Variablen zu verschaffen wird ein Korrelationsplot genutzt, damit die Abhängigkeiten der einzelnen Variablen betrachtet werden können.
  • Variablen die eine zu hohe Korrelaton vorweisen, werden aus dem Datensatz entfernt da sich diese negativ auf die Modellierung auswirken können.
  • Der folgende Korrelationsplot berücksichtigt alle Variablen.
In [603]:
# Korrelation aller Variablen nach Pearson (using the function from section 2.7)
correlation_matrix_plot(X_train, 24, 12, False, "seismic", "pearson")

# Korrelation aller Variablen nach Spearman (using the function from section 2.7)
correlation_matrix_plot(X_train, 24, 12, False, "seismic", "spearman")
In [604]:
pearson = X_train.corr(method="pearson")
spearman = X_train.corr(method="spearman")

delta = spearman.abs() - pearson.abs()
hm = sns.heatmap(delta, cmap="seismic", center=0)
hm.set_title("Abweichungen zwischen Spearman und Pearson",
             fontsize=24, weight="bold");

Bei der Feature Selection wird die Spearman-Korrelation verwendet. Da diese ohne Betrachtung der Abstände der einzelnen Werte auskommt, ist sie inklusiver als die Pearson-Korrelation.

Um die Feature Selection durchführen zu können, muss der Datensatz auf numerische Daten reduziert werden. Die Vorbereitungen hierzu wurden bereits im Abschnitt Data Preparation getroffen.

In [605]:
# Trainingsdaten
X_train_feature = X_train.select_dtypes(["number", "boolean"])
print(f"Spalten des Datensatzes 'X_train' nur mit numerischen und booleschen Datentypen: {len(X_train_feature.columns)}")

#Testdaten
X_test_feature = X_test.select_dtypes(["number", "boolean"])
print(f"Spalten des Datensatzes 'X_test' nur mit numerischen und booleschen Datentypen: {len(X_test_feature.columns)}")
Spalten des Datensatzes 'X_train' nur mit numerischen und booleschen Datentypen: 64
Spalten des Datensatzes 'X_test' nur mit numerischen und booleschen Datentypen: 65

3.7.1 Feature Selection anhand von Korrelation

Features mit zu hohen Korrelationen untereinander können als redundant betrachtet werden. Nachfolgend werden alle Features mit einer Korrelation über einem Threshold entfernt. Der Threshold wird bestimmt, indem er nach und nach (von 1 aus kommend) verringert wird.

  • Daher werden alle Variablen mit einem threshold von <= - 0,9 und >= 0,9 entfernt.
  • Bei der Pearson-Methode werden 36 features entfernt.
  • Bei der Spearman-Methode werden 35 features entfernt.
In [606]:
# Feature selection auf Basis zu hoher Korrelation
def get_columns_with_high_correlations(data: pd.DataFrame, threshold, corr_method):
    corr = data.corr(method=corr_method)

    # create a triangle of boolean Trues as a mask to keep values from corr matrix
    #       t   t   t   t
    #           t   t   t
    #               t   t
    #                   t
    # all values in  the matrix that overlap with this mask will be kept
    # all values below will be removed to prevent double deletion and deletion of correlations in the diagonal (where corr is always 1)
    mask = np.triu(np.ones(corr.shape), k=1).astype(np.bool)

    # upper side of corr matrix
    upper_triange = corr.where(mask)

    # get all ABSOLUTE correlations that are > threshold
    cols_to_drop = [column for column in upper_triange.columns if any(
        upper_triange[column].abs() > threshold)]
    return cols_to_drop


def drop_columns_with_high_correlation(data: pd.DataFrame, threshold, corr_method):
    cols = get_columns_with_high_correlations(data, threshold, corr_method)
    data = data.drop(cols, axis=1)
    print(f"Removed {len(cols)} features using method: {corr_method} and threshold: {threshold}")
    return data
In [607]:
# Feature selection by removing the highest korrelation (threshold <= -0,9 and >= 0,9 = correlations similar or higher than 0,9 and correlations similar or lower than -0,9 will be romoved)

# RUN THIS ONLY FOR FINDING THRESHOLD
# try to find propper threshold using the function in section 3.7
"""
for thresh in [0.9, 0.8, 0.7]:
    drop_columns_with_high_correlation(X_train_feature, thresh, "pearson")

    drop_columns_with_high_correlation(X_train_feature, thresh, "spearman")
"""
Out[607]:
'\nfor thresh in [0.9, 0.8, 0.7]:\n    drop_columns_with_high_correlation(X_train_feature, thresh, "pearson")\n\n    drop_columns_with_high_correlation(X_train_feature, thresh, "spearman")\n'
In [608]:
# perform feature selection on dataset "X_train_feature" with selected threshold (using the function in section 3.7)
corr_thresh = 0.9
pearson = drop_columns_with_high_correlation(X_train_feature, corr_thresh, "pearson")

spearman = drop_columns_with_high_correlation(X_train_feature, corr_thresh, "spearman")

# select feature set to continue with
X_train_removed_features = spearman
Removed 27 features using method: pearson and threshold: 0.9
Removed 26 features using method: spearman and threshold: 0.9
In [609]:
print(f"Es verbleiben im Datensatz X_train nach spearman: {len(X_train_removed_features.columns)} Features")
Es verbleiben im Datensatz X_train nach spearman: 38 Features

Nach der Bereinigung des Datensatzes sind alle Variablen mit einem threshold von <= - 0,9 und >= 0,9 entfernt worden, wie am nachfolgenden Korrelationsplot ersichtlich ist.

In [610]:
# Korrelation aller Variablen nach entfernung der features durch den threshold nach Spearman (using the function from section 2.7)
correlation_matrix_plot(X_train_removed_features, 24, 12, False, "seismic", "spearman")

3.7.2 Feature Importance durch Logistische Regression

Bei der Ermittlung der Feature Importance werden die Features mithilfe logistischer Regression analysiert. Dabei werden die Koeffizienten der einzelnen Variablen in Abhängigkeit zu der Zielvariable Response betrachtet. Daraus lässt sich der Score ermitteln. Auch hier wird ein Grenzwert benötigt, der zwischen unwichtigen und wichtigen Features unterscheidet. Dieser Threshold kann nach Betrachtung der Feature Importance am besten eingeschätzt werden.

Score Erklärung
>0 Wenn der Score >0 ist dann hat die Variable einen tendenziell positiven Einfluss darauf das eine KFZ-Versicherung abgeschlossen wird.
<0 Wenn der Score <0 ist dann hat die Variable einen tendenziell negativen Einfluss darauf das eine KFZ-Versicherung abgeschlossen wird.
=0 Wenn der Score =0 ist dann hat die Variable weder einen tendenziell positiven, noch negativen Einfluss darauf, dass eine KFZ-Versicherung abgeschlossen wird.
In [611]:
# feature importance by linear regression
feat_imp_thresh = 0.004

# define the model
model = LogisticRegression(max_iter=1000, random_state=42)

# fit the model 
model.fit(X_train_removed_features, y_train)    # y_train is the dataset after undersampling (y_train_undersampling --> section 3.5.4)

# get importance
importance = model.coef_[0]
i = pd.DataFrame(importance)
i["Feature"] = X_train_removed_features.columns
i = i.rename(columns={0: "Score"})


"""
# summarize feature importance --> console output
for i, v in enumerate(importance):
    print('Feature: %0d %s, Score: %.5f' %
          (i, X_train_removed_features.columns[i], v))
"""


# plot feature importance
i = i.sort_values("Score", ascending=False)
b = sns.barplot(x="Feature", y="Score", data=i)
b.tick_params(axis='x', rotation=90)
b.set_title("Scores der Features", fontsize="30", weight="bold")
b.set_xlabel("Feature", fontsize=20, weight="bold")
b.set_ylabel("Score", fontsize=20, weight="bold")
b.axhline(feat_imp_thresh, color="r")
b.axhline(-feat_imp_thresh, color="r");

Nach der Untersuchung der Feature Importance bleiben nur wenige Features für die Modellierung übrig.

Die Test- und Trainingsdaten werden auf die ausgewählten Features reduziert.

In [612]:
# output in console only the relevant features
relevant_features = i.loc[i["Score"].abs() > feat_imp_thresh]
print(relevant_features)

# Trainingsdaten
modelling_data_train = X_train_removed_features[relevant_features["Feature"]]

# Testdaten
modelling_data_test = X_test_feature[relevant_features["Feature"]]

###

# Dataset from the test.csv
real_data_for_modelling = real_dataset[relevant_features["Feature"]]
       Score                                Feature
12  0.042192       mean_Age_by_Policy_Sales_Channel
9   0.040001                mean_Age_by_Vehicle_Age
0   0.021738                                    Age
7   0.006221                mean_Age_by_Region_Code
2   0.004201                         Vehicle_Damage
1  -0.004050                        Driving_License
10 -0.018263           diff_Age_mean_by_Vehicle_Age
13 -0.020454  diff_Age_mean_by_Policy_Sales_Channel

4. Modeling

Im Folgenden werden 3 verschiedene Modelle untersucht:

  1. Random Forest Classification
  2. Neuronales Netz
  3. Gradient Boosting

Um die optimalen Parametereinstellungen zu finden, wird auf jedes Modell Hyperparametertuning angewendet. Das bedeutet, dass aus einer Vorauswahl von möglichen Parameterwerten alle Kombinationen ausprobiert werden. So kann das beste Modell gefunden werden.

Zum einfachen Vergleich werden die Vorhersagen der Modelle in einem Array gespeichert.

In [613]:
predictions = [None, None, None]
In [614]:
# Function to run all classifications through
# trains and evaluates models
# returns classifier, grid tuner, and prediction
def hyper_parameter_tuning(param_grid, clf, X_train, y_train, X_test, y_test, model_name, cv=5, use_multithreading=False):

    grid_tuner = None
    if use_multithreading == True:
        # build Grid Search CV
        grid_tuner = HalvingGridSearchCV(
            estimator=clf, param_grid=param_grid, cv=cv, verbose=2, n_jobs=4)
    else:
        grid_tuner = HalvingGridSearchCV(
            estimator=clf, param_grid=param_grid, cv=cv, verbose=2)

    grid_tuner.fit(X_train, y_train)

    # predict
    y_pred = grid_tuner.predict(X_test)

    print(grid_tuner.best_params_)
    print("\n")
    print(f"Train Accuracy: {grid_tuner.score(X_train, y_train)}")
    print(f"Test Accuracy:  {grid_tuner.score(X_test, y_test)}")
    return clf, grid_tuner, {"Prediction": y_pred, "Name": f"{model_name}"}

4.1 Modell: Random Forest mit Hyperparametertuning

Das Random Forest-Modell ist das einfachste Modell im Vergleich. Es besteht aus einer Anzahl von Entscheidungsbäumen. Grob vereinfacht, stimmen alle Bäume darüber ab, welche entgültige Klassifikation angewendet wird. Über die Parameter n_estimators und max_depth wird die Anzahl und die maximale Tiefe der Bäume bestimmt.

In [615]:
# Flag to select new HPT run or run on best values found previously
use_best_values = True

param_grid = {}
cv = 5
use_multithreading = True

# these values come from previous runs
# set use_best_values to true to quickly generate new output without rerunning HPT
if use_best_values == True:
    param_grid = {
        "n_estimators": [400],
        "max_features": ["auto"],
        "criterion": ["entropy"],
        # "max_depth": [8],
        # "min_samples_leaf": [2],
        "min_samples_split": [8],
        "bootstrap": [True]
    }
    cv = 2
else:
    # Hyperparametertuning
    param_grid = {
        "n_estimators": [100, 200, 400, 800],
        "criterion": ["gini", "entropy"],
        "max_features": ["auto"],
        "max_leaf_nodes": [None, 4, 8, 32],
        "max_depth": [None, 4, 8, 16, 32],
        # "min_samples_leaf": range(2, 5, 1),
        "min_samples_split": [2, 4, 8],
        "bootstrap": [True]
    }
    cv = 5

rf_clf, rf_grid_tuner, rf_prediction = hyper_parameter_tuning(param_grid, RandomForestClassifier(random_state=42),
                                                              modelling_data_train, y_train,
                                                              modelling_data_test, y_test,
                                                              "Random Forest (HPT)", cv, use_multithreading)

predictions[0] = rf_prediction

# best options:
# {'bootstrap': True, 'criterion': 'entropy', 'max_features': 'auto', 'min_samples_split': 8, 'n_estimators': 400}


# Train Accuracy: 0.8426775192856709
# Test Accuracy:  0.715211970074813
n_iterations: 1
n_required_iterations: 1
n_possible_iterations: 1
min_resources_: 65852
max_resources_: 65852
aggressive_elimination: False
factor: 3
----------
iter: 0
n_candidates: 1
n_resources: 65852
Fitting 2 folds for each of 1 candidates, totalling 2 fits
{'bootstrap': True, 'criterion': 'entropy', 'max_features': 'auto', 'min_samples_split': 8, 'n_estimators': 400}


Train Accuracy: 0.8426775192856709
Test Accuracy:  0.715211970074813

4.2 Modell: Neuronales Netz mit Hyperparametertuning

Neuronale Netze sind vom Aufbau des Gehirns inspiriert. Ein neuronales Netz besteht aus Neuronen, die in Schichten angeordnet und immer mit allen Neuronen der Vorgängerschicht verbunden sind. Die Neuronen entscheiden anhand einer Aktivierungsfunktion, ob sie aktiviert werden oder nicht. Dieses Verhalten hat Einfluss darauf, welche Neuronen im nachfolgenden Layer aktiviert werden. Auf diese Art werden Neuronen von Schicht zu Schicht aktiviert, bis sie im Outputlayer ankommen. Dort ist wird je nach dem, welches Neuron aktiviert ist entschieden, welche Vorhersage getroffen werden kann.

Im Zuge des Hyperparametertunings werden verschiedene Tiefen, Aktivierungsfunktionen und Anzahl der Iterationen des neuronalen Netzes ausprobiert.

In [616]:
# Flag to select new HPT run or run on best values found previously
use_best_values = True
use_multithreading = True

if use_best_values == True:
    param_grid = {
        'activation': ['tanh'],
        'early_stopping': [False],
        'hidden_layer_sizes': [(100, 100, 100)],
        'max_iter': [500],
        'solver': ['lbfgs']
    }
    cv = 2
else:
    # Hyperparametertuning
    param_grid = {
        "hidden_layer_sizes": [(100, 100,), (100, 100, 100), (100, 100, 100, 100)],
        "activation": ["identity", "tanh"],
        "solver": ["lbfgs", "sgd"],
        "max_iter": [100, 200, 500],
        "early_stopping": [False],
    }
    cv = 5

nn_clf, nn_grid_tuner, nn_prediction = hyper_parameter_tuning(param_grid, MLPClassifier(random_state=42),
                                                              modelling_data_train, y_train,
                                                              modelling_data_test, y_test,
                                                              "Neuronales Netz (HPT)", cv, use_multithreading)
predictions[1] = nn_prediction

# best options:
# {'activation': 'tanh', 'early_stopping': False, 'hidden_layer_sizes': (100, 100, 100), 'max_iter': 500, 'solver': 'lbfgs'}


# Train Accuracy: 0.7943722286339063
# Test Accuracy:  0.6818130113313208
n_iterations: 1
n_required_iterations: 1
n_possible_iterations: 1
min_resources_: 65852
max_resources_: 65852
aggressive_elimination: False
factor: 3
----------
iter: 0
n_candidates: 1
n_resources: 65852
Fitting 2 folds for each of 1 candidates, totalling 2 fits
{'activation': 'tanh', 'early_stopping': False, 'hidden_layer_sizes': (100, 100, 100), 'max_iter': 500, 'solver': 'lbfgs'}


Train Accuracy: 0.7943722286339063
Test Accuracy:  0.6818130113313208

4.3 Modell: Gradient Boosting mit Hyperparametertuning

Gradient Boosting ist eine besondere Form von Decision Trees. Beim Gradient Boosting werden anders als beim Random Forest keine willkürlichen Bäume gebaut, sondern die einzelnen Bäume lernen vom Fehler der anderen.

Gradient Boosting startet statt mit einem Baum mit einem Blatt. Der Wert wird über den Logarithmus aus der Wahrscheinlichkeit, dass die Zielvariable True ist gebildet. Aus dieser Wahrscheinlichkeit werden Residuen für jede Beobachtung gebildet.

In [617]:
# Flag to select new HPT run or run on best values found previously
use_best_values = True
use_multithreading = True

if use_best_values == True:
    param_grid = {
        "n_estimators": [10],
        "loss": ["exponential"],
        "learning_rate": [0.4],
        "criterion": ["friedman_mse"],
        "min_samples_split": [4],
        # "min_samples_leaf": [1],
        # "max_depth": [5],
        "max_features": ["auto"]
    }
    cv = 2
else:
    # Hyperparametertuning
    param_grid = {
        "n_estimators": [10, 20, 30, 50, 100, 200, 400],
        "loss": ["deviance", "exponential"],
        "learning_rate": np.linspace(0, 1, 6)[1:],  # 0,2 steps
        "criterion": ["friedman_mse"],
        "min_samples_split": [2, 4, 8],
        # "min_samples_leaf": [2,4],
        # "max_depth": [3, 5, 7],
        "max_features": ["auto"],
        "max_leaf_nodes": [8, 16, 32, None]
    }
    cv = 5

gb_clf, gb_grid_tuner, gb_prediction = hyper_parameter_tuning(param_grid, GradientBoostingClassifier(random_state=42),
                                                              modelling_data_train, y_train,
                                                              modelling_data_test, y_test,
                                                              "Gradient Boosting (HPT)", cv, use_multithreading)
predictions[2] = gb_prediction

# best options:
# {'criterion': 'friedman_mse', 'learning_rate': 0.4, 'loss': 'exponential', 'max_features': 'auto', 'min_samples_split': 4, 'n_estimators': 10}


# Train Accuracy: 0.7937040636578996
# Test Accuracy:  0.6855230345189658
n_iterations: 1
n_required_iterations: 1
n_possible_iterations: 1
min_resources_: 65852
max_resources_: 65852
aggressive_elimination: False
factor: 3
----------
iter: 0
n_candidates: 1
n_resources: 65852
Fitting 2 folds for each of 1 candidates, totalling 2 fits
{'criterion': 'friedman_mse', 'learning_rate': 0.4, 'loss': 'exponential', 'max_features': 'auto', 'min_samples_split': 4, 'n_estimators': 10}


Train Accuracy: 0.7937040636578996
Test Accuracy:  0.6855230345189658

5. Evaluation

Bei der Evaluation werden die Gütemaße der Modelle berechnet und verglichen.

Es werden folgende Gütemaße verglichen:

  • TPR: Anteil der korrekten true Vorhersagen unter allen tatsächlichen true Beobachtungen
  • TNR: Anteil der korrekten false Vorhersagen unter allen tatsächlichen false Beobachtungen

  • FPR: Anteil der falschen true Vorhersagen unter allen tatsächlichen false Beobachtungen

  • FNR: Anteil der falschen false Vorhersagen unter allen tatsächlich true Beobachtungen
In [618]:
def get_confusion_matrix(y_test, y_prediction):
    matrix = metrics.confusion_matrix(y_test, y_prediction)

    matrix = np.append(matrix, [np.sum(matrix, axis=0)], axis=0)
    col = np.array([np.sum(matrix, axis=1)])
    matrix = np.concatenate((matrix, col.T), axis=1)

    return matrix


def get_scores(y_test, y_prediction):
    matrix = metrics.confusion_matrix(y_test, y_prediction)

    TN = matrix[0][0]
    FP = matrix[0][1]
    FN = matrix[1][0]
    TP = matrix[1][1]

    # Recall / Sensitivität / True Positive Rate / Trefferquote
    TPR = TP / (TP + FN)

    # Anteil der fälschlich als negativ klassifizierten Beobachtungen
    FNR = 1 - TPR

    # Spezifizität
    TNR = TN / (TN + FP)

    # False Positive Rate
    FPR = 1 - TNR

    # calc scores
    n = TP + TN + FP + FN
    ACC = (TP + TN) / n
    ER = 1 - ACC
    PRECISION = TP / (TP + FP)

    return matrix, TPR, FNR, TNR, FPR, ACC, ER, PRECISION

def get_barplot(title, data, y, ax):
    p = sns.barplot(data=data, x="Name", y=y, ax=ax)
    p.set_title(title, weight="bold")
    p.set_xlabel("")
    p.set_yticks(np.linspace(0, 1, 11))
    p.set_ylabel("")
    p.set_yticks(np.linspace(0,1,11))
    p.set_yticklabels("")
    p.tick_params(axis='x', rotation=60)

def get_roc_curves(models, ax:plt.Axes):
    for model in models:
        prediction = model["Prediction"]
        fpr, tpr, thresh = roc_curve(y_test, prediction, pos_label=1)
        p_l = sns.lineplot(x=fpr,y=tpr, ax=ax)

    # random curve
    random_probs = [0 for i in range(len(y_test))]
    p_fpr, p_tpr, _ = roc_curve(y_test, random_probs, pos_label=1)
    p_r = sns.lineplot(x=p_fpr, y=p_tpr, dashes=True, ax=ax)
    ax.lines[3].set_linestyle("--")
    ax.set_title("ROC-Curves", weight="bold")
    ax.set_xlabel("False Positive Rate")
    ax.set_ylabel("True Positive Rate")
    ax.set_yticks(np.linspace(0,1,11))


def plot_scores(data: pd.DataFrame):
    
    fig, ax = plt.subplots(2, 6, figsize=(30, 20))
    plt.subplots_adjust(wspace=0.1, hspace=0.4)

    d_real = pd.DataFrame(y_test)
    vc = pd.DataFrame(d_real["Response"].value_counts() / len(d_real))

    p_real = sns.barplot(data=vc, x="Response", y="Response", ax=ax[0][0], palette=sns.color_palette('binary_r', 2))
    p_real.set_title("Realität", weight="bold")
    p_real.set_xticklabels(["True", "False"])
    p_real.set_xlabel("")
    p_real.set_ylabel("")
    p_real.set_yticks(np.linspace(0,1,11))


    p_TPR = get_barplot("True Positive Rate\n(big is better)", data, "TPR", ax[0][1])
    p_FNR = get_barplot("False Negative Rate\n(small is better)", data, "FNR", ax[0][2])
    p_TNR = get_barplot("True Negative Rate\n(big is better)", data, "TNR", ax[0][3])
    p_FPR = get_barplot("False Positive Rate\n(small is better)", data, "FPR", ax[0][4])
    p_AUC = get_barplot("Area Under Curve\n(big is better)", data, "AUC", ax[0][5])

    p_ACC = get_barplot("Korrektklassifikationsrate\n(big is better)", data, "ACC", ax[1][1])
    p_ER = get_barplot("Fehlerrate\n(small is better)", data, "ER", ax[1][2])
    p_PRECISION = get_barplot("Precision\n(big is better)", data, "PRECISION", ax[1][3])

    p_roc = get_roc_curves(predictions,ax[1][4])
In [619]:
scores = pd.DataFrame(
    columns=["Name", "TN", "FP", "FN", "TP", "TPR", "FNR", "FPR", "AUC", "ACC", "ER", "PRECISION"])
for model in predictions:
    if model is not None:
        matrix, TPR, FNR, TNR, FPR, ACC, ER, PRECISION = get_scores(y_test, model["Prediction"])
        AUC = metrics.roc_auc_score(y_test, model["Prediction"])
        scores = scores.append({"Name": model["Name"],
                                "TN": matrix[0][0], "FP": matrix[0][1],
                                "FN": matrix[1][0], "TP": matrix[1][1],
                                "TPR": TPR,
                                "FNR": FNR,
                                "TNR": TNR,
                                "FPR": FPR,
                                "AUC": AUC,
                                "ACC":ACC,
                                "ER":ER,
                                "PRECISION":PRECISION
                                }, ignore_index=True)
plot_scores(scores)

5.1 Bestes Modell

Aus den drei berechneten Modellen können wir anhand der Gütemaße das beste heraussuchen. Welches das beste Modell ist hängt davon ab, auf welche Faktoren besonderen Wert gelegt wird. Die Aufgabenstellung des Projektes war es, ein Modell zu entwickeln, um möglichst effektiv Crossselling betreiben zu können. Auf der Basis der zur Verfügung gestellten Daten, versuchen wir vorherzusagen welche Kunden gezielt angesprochen werden sollten, um eine KFZ-Versicherung abzuschließen.

  • Das bedeutet unser Modell sollte eine möglichst hohe Trefferquote haben, denn es lohnt sich nur auf Kunden zuzugehen, die auch Bereit sind eine Versicherung abzuschließen.

  • In zweiter Priorität versuchen wir eine möglichst niedrige False Positive Rate zu erreichen, denn der Versuch einen Kunden zu erreichen, der fläschlicherweise als kaufwillig eingestuft wurde, kostet Zeit und Geld.

In [620]:
def plot_confusion_matrix(y_test, y_prediction, model_name, axis=None):
    conf_matrix = get_confusion_matrix(y_test, y_prediction)

    plot = sns.heatmap(conf_matrix, annot=True, fmt="d", ax=axis)
    plot.set_xticklabels(["False", "True", "Total"])
    plot.set_yticklabels(["False", "True", "Total"])
    plot.set_xlabel("Predicted")
    plot.set_ylabel("Actual")
    plot.set_title(f"Konfusionsmatrix von {model_name}", fontsize=30, weight="bold")
    plot.axis = axis
    return plot


def plot_auc(y_test, y_prediction, model_name):
    fpr, tpr, t = metrics.roc_curve(y_test, y_prediction)
    roc_auc = metrics.auc(fpr, tpr)
    d = metrics.RocCurveDisplay(
        fpr=fpr, tpr=tpr, roc_auc=roc_auc, estimator_name=model_name)
    return d

# returns classifer, grid_tuner, predicction and scores


def select_model(model_name):
    if model_name == "Random Forest (HPT)":
        return rf_clf, rf_grid_tuner, rf_prediction, scores.loc[scores["Name"] == "Random Forest (HPT)"].sum()
    if model_name == "Neuronales Netz (HPT)":
        return nn_clf, nn_grid_tuner, nn_prediction, scores.loc[scores["Name"] == "Neuronales Netz (HPT)"].sum()
    if model_name == "Gradient Boosting (HPT)":
        return gb_clf, gb_grid_tuner, gb_prediction, scores.loc[scores["Name"] == "Gradient Boosting (HPT)"].sum()
    else:
        print(f"KEIN MODELL MIT DEM NAMEN: {model_name}")
        raise ValueError(f"KEIN MODELL MIT DEM NAMEN: {model_name}")


def print_model_scores(clf, tuner, prediction, scores):

    n = scores.TP + scores.TN + scores.FP + scores.FN

    # calc scores
    ACC = (scores.TP + scores.TN) / n
    ER = 1 - ACC
    PRECISION = scores.TP / (scores.TP + scores.FP)
    print("Korrektklassifikationsrate: %.2f" % (ACC))
    print("Fehlerrate:                 %.2f" % (ER))
    print("Recall (TPR):               %.2f" % (scores.TPR))
    print("Precision:                  %.2f" % (PRECISION))
    print("AUC                         %.2f" % (scores.AUC))

    fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(30, 10))

    # confusion matrix
    p_conf = plot_confusion_matrix(
        y_test, prediction["Prediction"], f"{prediction['Name']}", ax1)

    # AUC
    p_auc = plot_auc(y_test, prediction["Prediction"],
                     f"Model: {prediction['Name']}")
    p_auc.plot(ax2)
    ax2.set_title("ROC Curve", fontsize=30, weight="bold")

    p, r, t = metrics.precision_recall_curve(y_test, prediction["Prediction"])
    prc = metrics.PrecisionRecallDisplay(p, r)
    prc.plot(ax3)
    ax3.set_title("Precision-Recall Curve", fontsize=30, weight="bold");
    fig.__setattr__


# select best model by name
# best_clf, best_grid_tuner, best_prediction, best_scores = select_model("Random Forest (HPT)")
# best_clf, best_grid_tuner, best_prediction, best_scores = select_model("Neuronales Netz (HPT)")
best_clf, best_grid_tuner, best_prediction, best_scores = select_model("Gradient Boosting (HPT)")
print_model_scores(best_clf, best_grid_tuner, best_prediction, best_scores)
Korrektklassifikationsrate: 0.69
Fehlerrate:                 0.31
Recall (TPR):               0.94
Precision:                  0.27
AUC                         0.80

6. Anwendung

Das beste Modell wird auf die Realdaten (test.csv) angewendet. Die Realdaten wurden analog zu den Testdaten aus dem Train-Test-Split behandelt. Nur so können die Features erstellt werden, die zur Auswertung benötigt werden.

In [621]:
import csv

# reconstruct best model

# build test grid
# USE THIS FOR QUICK FINISH RUN
# THIS SETUP EQUALS BEST PARAMS FROM GRID BELOW
param_grid = {
    "n_estimators": [50],
    "max_features": ["auto"],
    "max_depth": [8],
    "min_samples_leaf": [2],
    "min_samples_split": [8],
    "bootstrap": [True]
}


# build model
clf = RandomForestClassifier(n_estimators=50, max_features="auto",
                             max_depth=8, min_samples_leaf=2, min_samples_split=8, bootstrap=True)
clf.fit(modelling_data_train, y_train)

prediction = clf.predict(real_data_for_modelling)

out = pd.DataFrame(real_dataset["id"])
out["response"] = prediction
out = out.to_numpy()

with open("sample_submission.csv", "w", newline="") as f:
    writer =  csv.writer(f, delimiter=";")
    writer.writerows(out)
    f.flush()