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 istDie 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
BeobachtungenTNR: 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
Beobachtungendef 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()