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.
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 |
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
warnings.filterwarnings("ignore")
Der Datensatz wurde von der NextGen Insurance bereitgestellt.
Der Datensatz "train_dataset" wird zur Analyse eingelesen:
Der Datensatz "real_dataset" wird zur Analyse eingelesen:
# 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
)
Zur Betrachtung der Variablen aus dem Datensatz werden die ersten zwanzig Einträge angezeigt:
train_dataset.head(20)
real_dataset.head(20)
Um eventuelle Korrekturen vorzunehmen betrachten wir die Datentypen der im Datensatz enthaltenen Variablen.
train_dataset.info()
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.
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.
real_dataset = real_dataset.rename(columns={"Vehicle__Damage": "Vehicle_Damage", "Annual__Premium": "Annual_Premium"})
real_dataset.info()
int64 unterstützt keine nullable Values (NaN),
deshalb wird der Pandas-Datentyp Int64 verwendet.
train_dataset["Age"].unique()
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.
# 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.
# 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())
Die Ausgabe weist darauf hin, dass diese Variablen richtig einglesen werden konnten und es keine (inhaltlich) falschen Ausprägungen gibt.
True, False und Missing
Values(NaN).
# 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.
train_dataset["Gender"].unique()
Die Ausgabe weist darauf hin, dass diese Variablen richtig einglesen werden konnten und es keine (inhaltlich) falschen Ausprägungen gibt.
Male, Female und Missing Values(NaN).
# 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.
train_dataset["Region_Code"].unique()
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.
# 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.
train_dataset["Vehicle_Age"].unique()
Die Ausgabe weist darauf hin, dass diese Variablen richtig einglesen werden konnten und es keine (inhaltlich) falschen Ausprägungen gibt.
> 2 Years, 1-2 Year,
< 1 Year und Missing Values(NaN).
# 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.
train_dataset["Policy_Sales_Channel"].unique()
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.
# 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.
train_dataset["Vintage"].unique()
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.
# 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.
train_dataset.drop("Unnamed: 0", axis="columns", inplace=True)
Diese Spalte beinhaltet keine Informationen und wird aus dem "train_dataset" Datensatz entfernt.
train_dataset.info()
Folgende statistische Kennzahlen werden verwenden:
train_dataset.describe(include="all").transpose()
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 |
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.
train_dataset.isna().sum()
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.
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)}")
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.
# 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
);
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.
# 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)
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")
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.
Driving_License und Vintage, sowie
Previously_Insured und Vintage auf,
mit einer Korrelation von 0,0024.
Vehicle_Damage und Previously_Insured.
Vehicle_Damage und Response. Wenn ich
in der Vergangenheit einen Schadensfall hatte, bin ich eher dazu
geneigt eine Versicherung abzuschließen.
Vintage liege nahe an 0.
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!
Beobachtungen:
Driving_License und
Previously_Insured beinhalten die gleichen Daten.
Die Variable Gender beschreibt das Geschlecht der
Kunden. Diese ist eine kategoriale Variable mit den zwei
Ausprägungen Male und Female.
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:
Die Variable Age beschreibt das Alter der Kunden.
Erwartungen:
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:
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:
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"]}')
Alle Datensätze bei denen das Alter über 100 Jahren liegt, sind nicht realitätsnah und werden genauer betrachtet:
train_dataset.loc[(train_dataset.Age >= 100)]
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.
Die Variable
Driving_License beschreibt ob ein Kunde einen
Führerschein besitzt.
Erwartungen:
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:
Die Variable Region_Code beschreibt den Wohnort der
Kunden.
Erwartungen:
len(train_dataset["Region_Code"].unique())
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.
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:
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:
Die Variable
Previously_Insured beschreibt ob ein Kunde eine
KFZ-Versicherung besitzt.
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:
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:
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:
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:
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:
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')
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:
Die Variable
Vehicle_Damage beschreibt, ob es an einem Fahrzeug
schonmal einen Schadensfall gab.
Erwartung:
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:
Die Variable
Annual_Premium beschreibt die Höhe des jährlichen
Versicherungsbeitrag der Krankenversicherung des Kunden.
Erwartungen:
Annual_Premium ist abhängig vom Alter des
Versicherten.
train_dataset["Annual_Premium"].describe()
Annual_Premium liegt bei rund 30.500 Rs.
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');
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")
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:
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:
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)
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')
Beobachtungen:
Die Variable
Policy_Sales_Channel beschreibt den Verkaufskanal, über
den die bestehende Krankenversicherung abgeschlossen wurde.
Erwartungen:
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 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.
# 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");
combined
print(f"Anzahl der Vertriebskanäle ohne positive Response: {len(no_positive_responses)}")
Beobachtungen:
Die Variable Vintage beschreibt die Dauer des
Versicherungsverhältnisses im letzten Jahr.
Erwartung:
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:
Vintage. Es scheinen keine Verkaufsaktionen
stattgefunden zu haben, oder sie sind ohne Erfolg geblieben.
# 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:
Response besonders gut ist
Die Variable Response beschreibt das Interesse der
Kunden an einer KFZ-Versicherung. Es ist die Zielvariable, die
mithilfe eines Modells vorhergesagt werdern soll.
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:
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:
Die Erkenntnisse, die im Kapitel Data Understanding gewonnen wurden, werden nachfolgend angewandt, um invalide Daten zu entfernen und die Datenqualität zu erhöhen.
print(f"Es sind {len(train_dataset.loc[train_dataset['Age']> 100])} Datensätze von dieser Änderung betroffen.")
train_dataset.loc[train_dataset["Age"] > 100, "Age"] = np.NaN
train_dataset.loc[train_dataset["Age"] < 18, "Age"] = np.NaN
train_dataset["Annual_Premium"].describe()
Negative Werte für
Annual_Premium sind nicht valide. Es würde bedeuten,
dass die Versicherungsgesellschaft den Kunden bezahlt.
print(f"Es sind {len(train_dataset.loc[train_dataset['Annual_Premium']< 0])} Datensätze von dieser Änderung betroffen.")
# remove negative values
train_dataset.loc[train_dataset["Annual_Premium"] < 0, "Annual_Premium"] = np.NaN
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.
# 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
train_dataset.isna().sum()
Für den Split unterteilen wir die Daten aus der "train.csv" in Trainingsdaten und Testdaten. Hier wird ein 70/30-Split genutzt.
#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)
Imputationsstrategie:
Die Imputation erfolgt anhand der nachfolgenden Prozedur. Es werden
verschiedene Imputationsstrategien (mean,
median, hot_code_locf und
most_frequent) ausprobiert.
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
#Trainingsdaten
X_train["Age"] = impute_data(X_train, "Age", "regression")
#Testdaten
X_test["Age"] = impute_data(X_test, "Age", "regression")
#Trainingsdaten
X_train["Annual_Premium"] = impute_data(X_train, "Annual_Premium", "median")
#Testdaten
X_test["Annual_Premium"] = impute_data(X_test, "Annual_Premium", "median")
#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())
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()}')
Die Imputation war erfolgreich und alle Missing Values in den Trainingsdaten und Testdaten wurden ersetzt.
Zunächst muss die Zielvariable
Response wieder zu den Trainingsdaten und Testdaten
hinzugefügt werden, da wir beim Sampling die Zielvariable
betrachten.
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.
# 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")
# 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))
plot_prop_of_split(X_train, X_test, "Response", "(vor Sampling)")
print_class_len_and_ratio(X_train, "Response")
print("-"*50)
print_class_len_and_ratio(X_test, "Response")
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.
Response mit den Ausprägungen True und False.
# 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")
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.
Response mit den Ausprägungen True und False.
# 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 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.
# 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.
# 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")
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.
# 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")
Age in Oktile (q=8) um die
Intervalle festzustellen.
Age_bin für den X_train und
X_test Datensatz sind:
# 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()}")
# 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)]);
Beobachtung:
Age relativ grob klassifiziert.
# 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);
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));
# 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
# 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)
# 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)
# 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)
Konvertieren von kategorialen Variablen in Dummy/Indikator-Variablen:
# 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()]
pd.set_option('display.max_columns', 100)
X_train
# 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")
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.
# 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)}")
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.
threshold von <= - 0,9 und >= 0,9 entfernt.
# 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
# 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")
"""
# 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
print(f"Es verbleiben im Datensatz X_train nach spearman: {len(X_train_removed_features.columns)} 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.
# 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")
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. |
# 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.
# 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"]]
Im Folgenden werden 3 verschiedene Modelle untersucht:
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.
predictions = [None, None, None]
# 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}"}
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.
# 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
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.
# 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
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.
# 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
Bei der Evaluation werden die Gütemaße der Modelle berechnet und verglichen.
Es werden folgende Gütemaße verglichen:
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
falschen false Vorhersagen unter allen
tatsächlich true Beobachtungen
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])
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)
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.
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)
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.
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()