# *****************************************************
# ----------------- Home Loan Approval ----------------
# Beschreibung: Datenimport und Preprocessing
# *****************************************************
# 1. Vorarbeiten --------------------------------------
# Workspace aufräumen
rm(list = ls())
setwd("C:/Users/daves/OneDrive/Desktop/Portfolio - DS/Assignment 1 - ML/portfolio-dave-assignment-ml")
# Package für Imputationen
if(!require("Hmisc")) install.packages("Hmisc")
# Lade Software aus R Packages
library(tidyverse)
library(tidymodels)
library(Hmisc)
# Wichtige Anmerkung zu Beginn - Variablenerklärung:
# Die Variable Credit_History zeigt gemäss Kaggle-Informationen,
# ob die Kredithistorie den Richtlinien entspricht (1) oder nicht (0).
MDS Project Machine Learning
Assignment Machine Learning
1. Import & Preprocess
# *****************************************************
# 2. Datenimport --------------------------------------
# Importiere den CSV-Datensatz loan_sanction
<- read_csv("daten/loan_sanction.csv") home_loan
# Datensatz inspizieren - Was für Spalten bzw. Datentypen haben wir in den Daten?
glimpse(home_loan)
Rows: 614
Columns: 13
$ Loan_ID <chr> "LP001002", "LP001003", "LP001005", "LP001006", "LP0…
$ Gender <chr> "Male", "Male", "Male", "Male", "Male", "Male", "Mal…
$ Married <chr> "No", "Yes", "Yes", "Yes", "No", "Yes", "Yes", "Yes"…
$ Dependents <chr> "0", "1", "0", "0", "0", "2", "0", "3+", "2", "1", "…
$ Education <chr> "Graduate", "Graduate", "Graduate", "Not Graduate", …
$ Self_Employed <chr> "No", "No", "Yes", "No", "No", "Yes", "No", "No", "N…
$ ApplicantIncome <dbl> 5849, 4583, 3000, 2583, 6000, 5417, 2333, 3036, 4006…
$ CoapplicantIncome <dbl> 0, 1508, 0, 2358, 0, 4196, 1516, 2504, 1526, 10968, …
$ LoanAmount <dbl> NA, 128, 66, 120, 141, 267, 95, 158, 168, 349, 70, 1…
$ Loan_Amount_Term <dbl> 360, 360, 360, 360, 360, 360, 360, 360, 360, 360, 36…
$ Credit_History <dbl> 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, NA, …
$ Property_Area <chr> "Urban", "Rural", "Urban", "Urban", "Urban", "Urban"…
$ Loan_Status <chr> "Y", "N", "Y", "Y", "Y", "Y", "Y", "N", "Y", "N", "Y…
# Es gibt 13 Spalten (Variablen) und 614 Zeilen (Beobachtungen) im home_loan-Datensatz
# Die Datentypen lauten wie folgt:
#chr (8): Loan_ID, Gender, Married, Dependents, Education, Self_Employed, Property_Area, Loan_Status
#dbl (5): ApplicantIncome, CoapplicantIncome, LoanAmount, Loan_Amount_Term, Credit_History
# Zählen von fehlenden Werten (Missing Values) pro Spalte
sapply(home_loan, function(x) sum(is.na(x)))
Loan_ID Gender Married Dependents
0 13 3 15
Education Self_Employed ApplicantIncome CoapplicantIncome
0 32 0 0
LoanAmount Loan_Amount_Term Credit_History Property_Area
22 14 50 0
Loan_Status
0
# Wir sehen in folgenden Spalten NA-Werte:
# Gender - 13
# Married - 3
# Dependents - 15
# Self_Employed - 32
# LoanAmount - 22
# Loan_Amount_Term - 14
# Credit_History - 50
# *****************************************************
# 3. Preprocessing ------------------------------------
# Faktortransformationen durchführen
$Gender <- as.factor(home_loan$Gender)
home_loan$Married <- as.factor(home_loan$Married)
home_loan$Dependents <- as.factor(home_loan$Dependents)
home_loan$Education <- as.factor(home_loan$Education)
home_loan$Self_Employed <- as.factor(home_loan$Self_Employed)
home_loan$Credit_History <- as.factor(home_loan$Credit_History)
home_loan$Property_Area <- as.factor(home_loan$Property_Area)
home_loan$Loan_Status <- as.factor(home_loan$Loan_Status) home_loan
# Struktur nach Transformation prüfen
glimpse(home_loan)
Rows: 614
Columns: 13
$ Loan_ID <chr> "LP001002", "LP001003", "LP001005", "LP001006", "LP0…
$ Gender <fct> Male, Male, Male, Male, Male, Male, Male, Male, Male…
$ Married <fct> No, Yes, Yes, Yes, No, Yes, Yes, Yes, Yes, Yes, Yes,…
$ Dependents <fct> 0, 1, 0, 0, 0, 2, 0, 3+, 2, 1, 2, 2, 2, 0, 2, 0, 1, …
$ Education <fct> Graduate, Graduate, Graduate, Not Graduate, Graduate…
$ Self_Employed <fct> No, No, Yes, No, No, Yes, No, No, No, No, No, NA, No…
$ ApplicantIncome <dbl> 5849, 4583, 3000, 2583, 6000, 5417, 2333, 3036, 4006…
$ CoapplicantIncome <dbl> 0, 1508, 0, 2358, 0, 4196, 1516, 2504, 1526, 10968, …
$ LoanAmount <dbl> NA, 128, 66, 120, 141, 267, 95, 158, 168, 349, 70, 1…
$ Loan_Amount_Term <dbl> 360, 360, 360, 360, 360, 360, 360, 360, 360, 360, 36…
$ Credit_History <fct> 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, NA, …
$ Property_Area <fct> Urban, Rural, Urban, Urban, Urban, Urban, Urban, Sem…
$ Loan_Status <fct> Y, N, Y, Y, Y, Y, Y, N, Y, N, Y, Y, Y, N, Y, Y, Y, N…
# Behebung der Fehlwerte:
# Im nächsten Schritt definieren wir pro Spalte, wie wir die Fehlwerte eliminieren möchten.
# Grundsätzlich versuchen wir das Löschen von Zeilen zu meiden, da wir einen eher kleinen Datensatz haben.
# Gender - 13 / Married - 3 / Dependents - 15
# -> Modus-Imputation: Die Anzahl Fehlwerte sind hier eher gering: Wir ergänzen über den Modus.
summary(home_loan$Gender) # Wird "Mann" ergänzen -> 489 zu 112
Female Male NA's
112 489 13
summary(home_loan$Married) # Wird "Yes" ergänzen -> 398 zu 213
No Yes NA's
213 398 3
summary(home_loan$Dependents) # Wird "0" ergänzen -> kommt am häufigsten vor
0 1 2 3+ NA's
345 102 101 51 15
# Self_Employed - 32
# -> Modus-Imputation: "No" ist stark überlegen, aus diesem Grund macht die Modusimputation Sinn.
summary(home_loan$Self_Employed) # Wird "No" ergänzen -> 500 zu 82
No Yes NA's
500 82 32
# Credit_History - 50
# -> Modus-Imputation: Wird "1" ergänzen. Wir gehen davon aus, dass die Kredithistorie der meisten Leute die Richtlinien
# erfüllt. Dies zeigt sich auch in den Daten (475 zu 89). Würde man 50x 0 zuweisen,
# würde sich der Wert der nicht passenden Historien um 56% erhöhen.
# Aber Achtung, dies könnte ein heikler Schritt sein. Die Historie ist, wie wir später explorativ
# erkennen werden und auch hier über table() sehen, ein wesentlicher Einflussfaktor für den Kredit-
# entscheid und es müssten die Gründe abgeklärt werden, wieso es hier so viele Fehlwerte gibt.
summary(home_loan$Credit_History)
0 1 NA's
89 475 50
table(home_loan$Credit_History, home_loan$Loan_Status)
N Y
0 82 7
1 97 378
# N & Y stehen für abgelehnt/angenommen und 0 & 1 steht für Credit-History widerspricht den Richtlinien / entspricht den Richtlinien
# Wir sehen hier also zum Beispiel, dass 82 von 89 Anträgen abgelehnt wurden, welche nicht den Richtlinien entsprachen.
# Modusimputation
$Gender <- impute(home_loan$Gender, fun=mode)
home_loan$Married <- impute(home_loan$Married, fun=mode)
home_loan$Dependents <- impute(home_loan$Dependents, fun=mode)
home_loan$Self_Employed <- impute(home_loan$Self_Employed, fun=mode)
home_loan$Credit_History <- impute(home_loan$Credit_History, fun=mode) home_loan
# Loan_Amount_Term - 14
# -> Median-Imputation: Wird "360" ergänzen, dies macht aus unserer Sicht Sinn, da dieser Wert deutlich am häufigsten
# vorkommt (512 von 613).
table(home_loan$Loan_Amount_Term)
12 36 60 84 120 180 240 300 360 480
1 2 2 4 3 44 4 13 512 15
# Medianimputation
$Loan_Amount_Term <- impute(home_loan$Loan_Amount_Term, fun=median) home_loan
# LoanAmount - 22
# Da wir diese Variable als zentral erachten, führen wir zuerst ein paar Analysen durch, um die Entscheidung
# zur Imputation zu treffen:
# Summary und Boxplot
summary(home_loan$LoanAmount)
Min. 1st Qu. Median Mean 3rd Qu. Max. NA's
9.0 100.0 128.0 146.4 168.0 700.0 22
boxplot(home_loan$LoanAmount)
# Wir erkennen einen grösseren Mittelwert als Median und gegen oben einige Ausreisser.
# Punktediagramm - JointIncome vs. Loan Amount
# Feature Engineering: Hinzufügen des gemeinsamen Einkommens
<- home_loan %>%
home_loan mutate(JointIncome = ApplicantIncome + CoapplicantIncome)
# Punktediagramm ausgeben
%>%
home_loan ggplot(aes(JointIncome, LoanAmount)) +
geom_point() +
scale_x_log10() +
geom_smooth(method = "lm")
# Aus der Grafik erkennen wir tendenziell eine positive Korrelation zwischen der Antragssumme und des
# JointIncome (zumindest im Einkommensbereich bis 10'000). Um die fehlenden NA-Werte möglichst gut zu
# imputieren, verwenden wir die lineare Regression zur Schätzung des Imputationswertes
# Implementierung eines linearen Regressionsmodelles für die Schätzung der NA-Werte
<- lm(LoanAmount ~ log10(JointIncome), data = home_loan)
modell
# Im oben eingeführten Modell, haben wir als Y-Wert die LoanAmount-Werte und als X-Werte die logarithmierten JointIncome-Werte aus dem Dataframe "home_loan" festgelegt und anschliessend ein lineares
# Regressionsmodell geschätzt. Als nächstes erstellen wir eine neue Spalte "PredictedLoanAmount", wo wir die Vorhersagewerte (unter Anwendung der JointIncome-Spalte aus dem Datensatz home_loan) speichern.
# Danach wird mit dem Code "mutate + ifelse" die LoanAmount Spalte mit unserer Vorhersage überschrieben, falls dort ein NA-Wert vorliegt, sonst wird der beobachtete Wert belassen.
# Abschliessend wird die "PredictedLoanAmount"-Spalte wieder entfernt, da sie nicht mehr benötigt wird.
$PredictedLoanAmount <- predict(modell, newdata = home_loan)
home_loan<- home_loan %>%
home_loan mutate(LoanAmount = ifelse(is.na(LoanAmount), PredictedLoanAmount, LoanAmount))
<- home_loan %>%
home_loan select(-PredictedLoanAmount)
# Das Modell wird ebenfalls nicht mehr benötigt und kann entfernt werden
rm(modell)
# Überprüfung ob nun alle NA-Werte erfolgreich entfernt wurden
sapply(home_loan, function(x) sum(is.na(x)))
Loan_ID Gender Married Dependents
0 0 0 0
Education Self_Employed ApplicantIncome CoapplicantIncome
0 0 0 0
LoanAmount Loan_Amount_Term Credit_History Property_Area
0 0 0 0
Loan_Status JointIncome
0 0
# Anmerkung:
# Wir wollten Werte z.B. Gender mit der K-nearest neighbors Methode einfügen, jedoch gab uns der R-Code die Information aus,
# dass unser Datensatz für die Berechnung der K-nächsten Nachbarn bei unserer vorliegenden Komplexität (Variablenanzahl) nicht möglich sei.
# Sind Duplikate enthalten? / Achtung: ID-Variable muss ausgeschlossen werden
<- duplicated(home_loan %>% select(-Loan_ID))
dpl_home_loan_idx
sum(dpl_home_loan_idx)
[1] 0
# Unser Datensatz weist keine Duplikate auf
# Wir entfernen das Objekt dpl_train_idx wieder
rm(dpl_home_loan_idx)
# *****************************************************
# 4. Zwischenresultate speichern ----------------------
# Wir speichern home_loan in dem Ordner 'zwischenresultate'
# write_rds(home_loan, "zwischenresultate/home_loan.rds")
# Das Ziel ist das Projekt modular aufzubauen, damit wir R nicht jedes Mal komplett laufen lassen müssen.
2. Train-Test-Split & Exploration
# *****************************************************
# ----------------- Home Loan Approval ----------------
# Beschreibung: Explorative Analyse
# *****************************************************
# 1. Vorarbeiten --------------------------------------
# Workspace aufräumen
rm(list = ls())
# Package zur Darstellung von Plots nebeneinander und übereinander
if(!require("gridExtra")) install.packages("gridExtra")
# Lade Software aus R Packages
library(tidyverse)
library(tidymodels)
library(ggplot2)
library(dplyr)
library(gridExtra)
# Lade Zwischenresultate
<- read_rds("zwischenresultate/home_loan.rds") home_loan
# *****************************************************
# 2. Train-Test Split ---------------------------------
# Der Train-Test-Split wurde in unserem Beispieldatensatz von Kaggle
# zwar durchgeführt, jedoch fehlte die Zielvariable (Loan_Status). Aus
# diesem Grund muss hier nochmals ein Split gemacht werden.
# Wir entscheiden uns für einen 75 (Train) - 25 (Test) Split. Leider
# liegen nicht so viele Daten vor, so ergeben sich aber trotzdem
# 450 Daten zum trainieren und 150 zum Testen.
# Seed, um den Train-Test Split reproduzierbar zu machen
set.seed(123)
# Stratifizierter Split (75% Training, 25% Test)
<- initial_split(home_loan, prop = 3/4, strata = Loan_Status)
train_test_split
<- training(train_test_split)
train <- testing(train_test_split)
test
# Verhältnis Y (Yes) zu N (No) der Variable Loan_Status prüfen
# -> War der stratifizierte Split effektiv?
sum(train$Loan_Status=="Y")/nrow(train)
[1] 0.6869565
sum(test$Loan_Status=="Y")/nrow(test)
[1] 0.6883117
# Verhältnis des Splits prüfen
nrow(train) / (nrow(train)+nrow(test))
[1] 0.7491857
# Dem Trainingsdatensatz wurden korrekt 75% der Daten zugewiesen
# Wir speichern den test-Datensatz in die "zwischenresultate",
# so können wir ihn von dort laden, sobald wir ihn benötigen.
write_rds(test, "zwischenresultate/test.rds")
# Wir entfernen nun den Datensatz test wieder (ohne ihn zu betrachten)
rm(test)
# Wir entfernen ebenfalls den home_loan-Data-Frame, da wir diesen nicht mehr benötigen
rm(home_loan)
# Das Splitting-Objekt wird auch nicht mehr benötigen
rm(train_test_split)
# *****************************************************
# 3. Explorative Analyse ------------------------------
# UNIVARIATE ANALYSE
# Sammlung von Kreisdiagrammen (4 univariate Variablenauswertungen, um einen Überblick zu erhalten)
# Geschlechterverteilung, Verteilung Zivilstand, Verteilung Ausbildung, Anstellungsverhältnis
# Erste Grafik
# Berechne die Prozentsätze
<- train %>%
train_percent1 group_by(Gender) %>%
summarise(Count = n()) %>%
mutate(Percent = Count / sum(Count) * 100)
# Kreisdiagramm erstellen
<- ggplot(train_percent1, aes(x = "", y = Percent, fill = Gender)) +
grafik1 geom_bar(stat = "identity") +
theme_minimal() +
labs(title = "Geschlechterverteilung", x = NULL, y = NULL) +
coord_polar("y", start = 0) + # Wandle das Balkendiagramm in ein Kreisdiagramm um
theme(plot.title = element_text(hjust = 0.5)) +
geom_text(aes(label = paste(round(Percent, 1), "%")), position = position_stack(vjust = 0.5), color = "white") +
theme(plot.title = element_text(hjust = 0.5),
legend.position = "bottom") +
scale_fill_discrete(name = "Geschlecht", breaks = c("Female", "Male"),
labels=c("Weiblich","Männlich"))
# Zweite Grafik
# Berechne die Prozentsätze
<- train %>%
train_percent2 group_by(Married) %>%
summarise(Count = n()) %>%
mutate(Percent = Count / sum(Count) * 100)
# Kreisdiagramm erstellen
<- ggplot(train_percent2, aes(x = "", y = Percent, fill = Married)) +
grafik2 geom_bar(stat = "identity") +
theme_minimal() +
labs(title = "Verteilung Zivilstand", x = NULL, y = NULL) +
coord_polar("y", start = 0) + # Wandle das Balkendiagramm in ein Kreisdiagramm um
theme(plot.title = element_text(hjust = 0.5)) +
geom_text(aes(label = paste(round(Percent, 1), "%")), position = position_stack(vjust = 0.5), color = "white") +
theme(plot.title = element_text(hjust = 0.5),
legend.position = "bottom") +
scale_fill_discrete(name = "Zivilstand", breaks = c("No", "Yes"),
labels=c("Ledig","Verheiratet"))
# Dritte Grafik
# Berechne die Prozentsätze
<- train %>%
train_percent3 group_by(Education) %>%
summarise(Count = n()) %>%
mutate(Percent = Count / sum(Count) * 100)
# Kreisdiagramm erstellen
<- ggplot(train_percent3, aes(x = "", y = Percent, fill = Education)) +
grafik3 geom_bar(stat = "identity") +
theme_minimal() +
labs(title = "Verteilung Ausbildung", x = NULL, y = NULL) +
coord_polar("y", start = 0) + # Wandle das Balkendiagramm in ein Kreisdiagramm um
theme(plot.title = element_text(hjust = 0.5)) +
geom_text(aes(label = paste(round(Percent, 1), "%")), position = position_stack(vjust = 0.5), color = "white") +
theme(plot.title = element_text(hjust = 0.5),
legend.position = "bottom") +
scale_fill_discrete(name = "Ausbildungsgrad", breaks = c("Graduate", "Not Graduate"),
labels=c("Mit Hochschulabschluss","Ohne Hochschulabschluss"))
# Vierte Grafik
# Berechne die Prozentsätze
<- train %>%
train_percent4 group_by(Self_Employed) %>%
summarise(Count = n()) %>%
mutate(Percent = Count / sum(Count) * 100)
# Kreisdiagramm erstellen
<- ggplot(train_percent4, aes(x = "", y = Percent, fill = Self_Employed)) +
grafik4 geom_bar(stat = "identity") +
theme_minimal() +
labs(title = "Anstellungsverhältnis", x = NULL, y = NULL) +
coord_polar("y", start = 0) + # Wandle das Balkendiagramm in ein Kreisdiagramm um
theme(plot.title = element_text(hjust = 0.5)) +
geom_text(aes(label = paste(round(Percent, 1), "%")), position = position_stack(vjust = 0.5), color = "white") +
theme(plot.title = element_text(hjust = 0.5),
legend.position = "bottom") +
scale_fill_discrete(name = "Anstellungsverhältnis", breaks = c("No", "Yes"),
labels=c("Angestellt","Selbstständig"))
# Darstellung von Grafiken 1 bis 4 in einem Plot
grid.arrange(grafik1, grafik2, grafik3, grafik4, ncol = 2, nrow = 2)
# Löschen der für die Grafiken erstellten Data Frames und Grafik-Objekten
rm(grafik1, grafik2, grafik3, grafik4)
rm(train_percent1, train_percent2, train_percent3, train_percent4)
# Balkendiagramm zu den Dependents (also abhängigen Personen im gleichen Haushalt z.B. Kinder)
ggplot() +
geom_bar(data=train, aes(x=Dependents, fill=Dependents))+
theme_minimal() +
labs(title = "Anzahl abhängiger Personen im Haushalt (z.B. Kinder)",
x = "",
y = "Anzahl") +
theme(plot.title=element_text(hjust=0.5)) +
geom_text(data=train, aes(x=Dependents, label = ..count..), stat = "count",
vjust=2,
color="white",
size=3) +
theme(axis.title.y = element_text(angle = 0,
vjust = 0.5)) +
theme(legend.position = "none") +
scale_x_discrete(labels = c("0" = "0", "1" = "1", "2" = "2", "3+" = "3 und mehr")) #x-Achsenbeschriftungen umbenennen v.a. 3+ zu 3 und mehr
# 2er-Diagramm (Histogramme)
# Diagramm 1: Histogramm zur angefragten Kredithöhe
<- ggplot()+
grafik6 geom_histogram(data = train, aes(x = LoanAmount), fill = "steelblue", color = "black", bins = 20) +
theme_minimal() +
scale_x_log10() +
labs(title = "Angefragte Kredithöhe",
x = "Kredithöhe - Achse log(10) skaliert",
y = "Anzahl") +
theme(plot.title=element_text(hjust=0.5)) +
theme(axis.title.y = element_text(angle = 0,
vjust = 0.5)) +
theme(plot.margin = margin(t = 20, r = 20, b = 20, l = 10, unit = "pt"))
# Diagramm 2: Histogramm mit dem gemeinsamen Einkommen (Beachte das JointIncome wurde von uns erstellt und zeigt die Summe der Variablen ApplicantIncome und CoapplicantIncome)
<- ggplot()+
grafik7 geom_histogram(data = train, aes(x = JointIncome), fill = "steelblue", color = "black", bins = 20) +
theme_minimal() +
scale_x_log10() +
labs(title = "Haushaltseinkommen",
x = "Einkommen - Achse log(10) skaliert",
y = "Anzahl") +
theme(plot.title=element_text(hjust=0.5)) +
theme(axis.title.y = element_text(angle = 0,
vjust = 0.5)) +
theme(plot.margin = margin(t = 20, r = 20, b = 20, l = 10, unit = "pt"))
# Darstellung von Grafik6 und Grafik7 in einem Plot
grid.arrange(grafik6, grafik7, ncol = 2)
# Löschen der für die gemeinsame Darstellung erstellten Grafik-Objekten
rm(grafik6, grafik7)
# BIVARIATE ANALYSE
# Spannend wird es nun mit den bivariaten Analysen. Wir knüpfen bei der letzten Grafik an und verbinden
# die angefragte Kredithöhe mit dem Haushaltseinkommen (JointIncome) -> Die Grafik wird einem bekannt
# vorkommen. Es ist diejenige, welche wir im 01_import_preprocess bereits für die Imputation mittels
# linearer Regression erstellt haben. -> Hier wird sie noch etwas (optisch) verfeinert.
%>%
train ggplot(aes(JointIncome, LoanAmount)) +
geom_point() +
theme_minimal() +
scale_x_log10() +
geom_smooth(method = "lm", se=FALSE) +
labs(title = "Gegenüberstellung Gesamteinkommen vs. Kredithöhe",
x = "Einkommen - Achse log(10) skaliert",
y = "Kredithöhe") +
theme(plot.title=element_text(hjust=0.5)) +
theme(axis.title.y = element_text(angle = 0,
vjust = 0.5)) +
theme(plot.margin = margin(t = 20, r = 20, b = 20, l = 10, unit = "pt"))
# Erweiterung von der vorherigen Grafik mit der Färbung nach Kreditstatus
# Multivariate Grafik x-Achse: JointIncome / y-Achse: LoanAmount und Färbung nach Kreditstatus
ggplot(train, aes(x = JointIncome, y = LoanAmount, color = Loan_Status)) +
geom_point(alpha = 0.6) + # alpha für Transparenz, damit überlappende Punkte sichtbar sind
theme_minimal() +
labs(title = "Gegenüberstellung Gesamteinkommen vs. Kredithöhe", x = "Einkommen - Achse log(10) skaliert", y = "Kredithöhe", color = "Kreditstatus") +
theme(plot.title=element_text(hjust=0.5)) +
scale_x_log10() +
geom_smooth(method = "lm", se = FALSE) + # Linien für lineare Modelle
scale_color_manual(name = "Kreditstatus",
values = c("N" = "Red", "Y" = "Steelblue"),
labels = c("N" = "Abgelehnt", "Y" = "Angenommen")) +
theme(plot.margin = margin(t = 20, r = 20, b = 20, l = 10, unit = "pt"))
# Wir sehen im unteren Bereich bis ca. 100'000 einen leichten linearen Zusammenhang, danach gibt es mehrere Ausreisser nach oben.
# Diese Ausreisser ziehen auch v.a. die blaue Kurve sicherlich etwas nach oben
# Im Unteren Bereich, bei geringen Einkommen, können wir gut erkennen, dass dort hohe Kreditanträge (zumindest in Relation zu den Einkommen)
# oft abgelehnt wurden, wohingegen die Anträge mit tiefen Kredithöhen grundsätzlich angenommen wurden.
# Gruppiertes Balkendiagramm (relativ)
# Gegenüberstellung Kredithistorie vs. Kreditentscheid
# Eine wesentliche Erkenntnis haben wir bei folgender Grafik erhalten.
# Gemäss Beschrieb auf Kaggle beschreibt die Credit_History, ob diese
# die Richtlinien der Bank erfüllen. -> Das Ergebnis aus folgender bivariaten
# Analyse zeigt, dass diese Variable einen zentralen Einfluss auf die Annahme /
# Ablehnung eines Antrages hat.
<- train %>%
train_grafik10 count(Credit_History, Loan_Status) %>%
group_by(Credit_History) %>%
mutate(Prop = n / sum(n)) %>%
ungroup()
ggplot()+
geom_bar(data=train, aes(x = Credit_History, fill = Loan_Status),
position = "fill") +
theme_minimal() +
labs(title = "Gegenüberstellung Kredithistorie vs. Kreditentscheid",
x = "", y = "Relativer Anteil") +
theme(plot.title=element_text(hjust=0.5)) +
geom_text(data = train_grafik10,
aes(x = Credit_History, y = Prop, label = scales::percent(Prop), group = Loan_Status),
position = position_fill(vjust = 0.5),
color = "white", size = 3.5) +
scale_x_discrete(breaks = c("0", "1"),
labels=c("Weicht von den Richtlinien ab", "Erfüllt die Richtlinien")) +
scale_y_continuous(labels = percent_format(accuracy = 1)) +
scale_fill_discrete(name = "Kreditstatus", breaks=c("N","Y"),
labels=c("Abgelehnt", "Angenommen")) +
theme(plot.margin = margin(t = 20, r = 20, b = 20, l = 20, unit = "pt"))
# Löschen des für die Grafik erstellten dataFrames aus dem Environment
rm(train_grafik10)
# Boxplot: Gegenüberstellung Gesamteinkommen vs. Wohnort (ländlich, agglomeration, städtisch)
ggplot()+
geom_boxplot(data=train, aes(x = Property_Area, y = JointIncome, fill = Property_Area)) + #NEU x-Achse unterscheidet nun nach Feiertag und gefüllt wird auch nach diesen
labs(title = "Gegenüberstellung Gesamteinkommen vs. Wohnort (ländlich, agglomeration, städtisch)",
x = "",
y = "Gesamteinkommen - Achse log(10) skaliert") +
scale_y_log10() +
theme(plot.title = element_text(hjust = 0.5),
axis.text.x = element_text(angle = 60, vjust = 1, hjust = 1)) +
theme(legend.position = "none") +
scale_x_discrete(breaks = c("Rural", "Semiurban", "Urban"),
labels=c("Ländlich", "Agglomeration", "Städtisch")) +
theme(plot.margin = margin(t = 20, r = 20, b = 20, l = 10, unit = "pt"))
# Die Grafiken (Boxen) sehen sehr ähnlich aus, es gibt keine markanten Unterschiede
# in den Gesamteinkommen. In den Agglomeraten und städtischen Regionen gibt es aber
# gegen oben ein paar extremere Ausreisser. Die Top-Verdiener leben somit tendenziell
# eher in der Agglomeration oder der Stadt.
# Gruppiertes Balkendiagramm (relativ)
# Gegenüberstellung Anstellungsverhältnis vs. Kreditentscheid
<- train %>%
train_grafik12 count(Self_Employed, Loan_Status) %>%
group_by(Self_Employed) %>%
mutate(Prop = n / sum(n)) %>%
ungroup()
ggplot()+
geom_bar(data=train, aes(x = Self_Employed, fill = Loan_Status),
position = "fill") +
theme_minimal() +
labs(title = "Gegenüberstellung Anstellungsverhältnis vs. Kreditentscheid",
x = "", y = "Relativer Anteil") +
theme(plot.title=element_text(hjust=0.5)) +
geom_text(data = train_grafik12,
aes(x = Self_Employed, y = Prop, label = scales::percent(Prop), group = Loan_Status),
position = position_fill(vjust = 0.5),
color = "white", size = 3.5) +
scale_x_discrete(breaks = c("No", "Yes"),
labels=c("Angestellt", "Selbständig")) +
scale_y_continuous(labels = percent_format(accuracy = 1)) +
scale_fill_discrete(name = "Kreditstatus", breaks=c("N","Y"),
labels=c("Abgelehnt", "Angenommen")) +
theme(plot.margin = margin(t = 20, r = 20, b = 20, l = 20, unit = "pt"))
# Wir erkennen hier einen Unterschied. Die Ablehnungsrate ist bei den selbständigen etwas höher als bei den Angestellten.
#Löschen des für die Grafik erstellten dataFrames aus dem Environment entfernen
rm(train_grafik12)
# *****************************************************
# 4. Zwischenresultate speichern ----------------------
# Wir speichern 'train' in dem Ordner 'zwischenresultate'
# write_rds(train, "zwischenresultate/train.rds")
# Den Datensatz 'test' haben wir bereits oben abgespeichert und entfernt.
3. Feature Engineering & Cleaning
# *****************************************************
# ----------------- Home Loan Approval ----------------
# Beschreibung: Feature Engineering & Data Cleaning
# *****************************************************
# 1. Vorarbeiten --------------------------------------
# Workspace aufräumen
rm(list = ls())
# Lade Software aus R Packages
library(tidyverse)
library(tidymodels)
library(corrplot)
# Lade Zwischenresultate
<- read_rds("zwischenresultate/train.rds") train1
# *****************************************************
# 2. Feature Engineering-------------------------------
# Die Variable Joint Income wurde bereits im R-Skript 01_import_preprocess erstellt,
# da dies aus unserer Sicht für diverse vorherige Schritte bereits Sinn ergeben hat.
# Im Folgen fügen wir 4 neue Variablen hinzu:
# MonthlyPayment -> Zeigt die monatliche Rückzahlungshöhe (Zins + Amortisation)
# PaymentIncomeRatio -> Verhältniss der Rückzahlungshöhe zum gemeinsamen Einkommen
# BooleanDependents -> Klassifizierung in Dependents "Ja" oder "Nein"
<- train1 %>%
train1 mutate(MonthlyPayment = LoanAmount*1000/Loan_Amount_Term,
PaymentIncomeRatio = MonthlyPayment/JointIncome,
BooleanDependents = as.factor(ifelse(Dependents == 0, "No", "Yes")))
ggplot()+
geom_boxplot(data=train1, aes(x = Loan_Status, y = MonthlyPayment, fill = Loan_Status)) + #NEU x-Achse unterscheidet nun nach Feiertag und gefüllt wird auch nach diesen
labs(title = "Gegenüberstellung Monaltiche Zahlungen vs. Kreditentscheid",
x = "",
y = "Monatliche Zahlungen - Achse log(10) skaliert") +
scale_y_log10() +
theme(plot.title = element_text(hjust = 0.5)) +
theme(legend.position = "none") +
scale_x_discrete(breaks = c("N", "Y"),
labels=c("Abgelehnt", "Angenommen")) +
theme(plot.margin = margin(t = 20, r = 20, b = 20, l = 10, unit = "pt"))
# Bei Betrachtung der Grafik zwischen dem monatlichen Zahlbetrag und dem Antragsentscheid kann man keine
# eindeutigen Unterschiede erkennen. Was wir sehen können ist, dass es bei den abgelehnten Anträgen ein
# paar mehr Ausreisser nach unten gibt. Bei den angenommenen Anträgen ein paar mehr nach oben.
# Wichtig wird eher der Vergleich zum Einkommen. Wenn man ein höheres Einkommen hat, kann man
# sich auch eine höhere monaltiche Zahlung leisten, weshalb nicht per se gesagt werden kann, dass
# ein höherer monatlicher Zahlbetrag eher zu einer Ablehnung führt.
ggplot(train1, aes(x = JointIncome, y = MonthlyPayment, color = Loan_Status)) +
geom_point(alpha = 0.6) + # alpha für Transparenz, damit überlappende Punkte sichtbar sind
labs(x = "Einkommen - Achse: log(10) transformiert", y = "Monatliche Zahlungen", color = "Kreditstatus") +
theme_minimal() +
scale_x_log10() +
scale_y_log10() +
geom_smooth(method = "lm", se = FALSE) + # Linien für lineare Modelle
scale_color_manual(name = "Kreditstatus",
values = c("N" = "Red", "Y" = "Steelblue"),
labels = c("N" = "Abgelehnt", "Y" = "Angenommen")) +
theme(plot.margin = margin(t = 20, r = 20, b = 20, l = 10, unit = "pt"))
# Auf der linken Seite sehen wir zuerst ein paar rote Punkte, dort wird das Einkommen zu tief sein
# für eine Annahme des Antrages.
# Die Ergebnisse sind aber eher durchmischt.
# Beachten muss man sicher, dass wir hier nur zwei Variablen miteinander anschauen, für eine
# Annahme/Ablehnung des Antrages werden noch weitere einen wesentlichen Einfluss haben
# Beachten muss man auch, dass wohl noch ein paar fehlerhafte Eingaben in den Daten sind z.B.
# eine monatliche Zahlrate von 9'250 vs. ein Einkommen von ca. 7'500.- (welcher sogar angenommen wurde)
ggplot()+
geom_boxplot(data=train1, aes(x = Loan_Status, y = PaymentIncomeRatio, fill = Loan_Status)) + #NEU x-Achse unterscheidet nun nach Feiertag und gefüllt wird auch nach diesen
labs(title = "Gegenüberstellung Einkommen-Rückzahlungs-Ratio vs. Kreditentscheid",
x = "",
y = "Einkommen-Rückzahlungs-Ratio log(10) transformiert") +
scale_y_log10() +
theme(plot.title = element_text(hjust = 0.5)) +
theme(legend.position = "none") +
scale_x_discrete(breaks = c("N", "Y"),
labels=c("Abgelehnt", "Angenommen")) +
theme(plot.margin = margin(t = 20, r = 20, b = 20, l = 10, unit = "pt"))
# Wir sehen auch bei der Einkommen-Rückzahluns-Ratio eher ein ähnliches Bild.
# Ein paar Ausreisser (v.a. oben, aber auch unten) fallen sehr stark auf.
# z.B. ein Ratio von ca. 1.25 wie oben bereits zahlenmässig erwähnt (9'250 Rückzahlungsbetrag)
# bei ca. 7'500 Einkommen, deutet auf eine fehlerhafte Eingabe hin.
# Aber auch hier lässt sich nicht eindeutig sagen, dass es einen erkennbaren Unterschied gibt
# zwischen den angenommenen und abgelehnten Anträgen anhand der Einkommen-Rückzahlungs-Ratio
<- train1 %>%
train1_grafik4 count(BooleanDependents, Loan_Status) %>%
group_by(BooleanDependents) %>%
mutate(Prop = n / sum(n)) %>%
ungroup()
ggplot() +
geom_bar(data=train1, aes(x = BooleanDependents, fill = Loan_Status),
position = "fill") +
theme_minimal() +
labs(title = "Gegenüberstellung abhängiger Personen im Haushalt (z.B. Kinder) vs. Kreditentscheid",
x = "", y = "") +
theme(plot.title=element_text(hjust=0.5)) +
geom_text(data = train1_grafik4,
aes(x = BooleanDependents, y = Prop, label = scales::percent(Prop), group = Loan_Status),
position = position_fill(vjust = 0.5),
color = "white", size = 3.5) +
scale_x_discrete(breaks = c("Yes", "No"),
labels=c("Vorhanden", "Nicht vorhanden")) +
scale_y_continuous(labels = percent_format(accuracy = 1)) +
scale_fill_discrete(name = "Kreditstatus", breaks=c("N","Y"),
labels=c("Abgelehnt", "Angenommen")) +
coord_flip() +
theme(plot.margin = margin(t = 20, r = 20, b = 20, l = 20, unit = "pt"))
# Auch hier erkennen wir praktisch keine Unterschiede.
# Eine wesenltiche Erkenntnis nach dem Feature Engineering und der explorativen Analyse ist,
# dass vor allem die Variable Credit_History (ob die Historie die Richtlinien der Bank erfüllt oder nicht)
# visuell einen erheblichen Zusammenhang zur Annahme oder Ablehnung des Antrages beiträgt.
#Löschen des für die Grafik erstellten Data Frames
rm(train1_grafik4)
# Second Feature Engineering (Zum austesten)
<- train1 %>% mutate(JointIncome_log = log10(JointIncome)) train1
# *****************************************************
# 3. Cleaning Up --------------------------------------
# Über str() verschaffen wir uns nochmals einen Überblick über die Variablen
str(train1)
tibble [460 × 18] (S3: tbl_df/tbl/data.frame)
$ Loan_ID : chr [1:460] "LP001003" "LP001029" "LP001036" "LP001038" ...
$ Gender : Factor w/ 2 levels "Female","Male": 2 2 1 2 2 2 2 2 2 2 ...
..- attr(*, "imputed")= int [1:13] 24 127 172 189 315 335 461 468 478 508 ...
..- attr(*, "names")= chr [1:460] "2" "14" "18" "19" ...
$ Married : Factor w/ 2 levels "No","Yes": 2 1 1 2 2 2 2 2 1 1 ...
..- attr(*, "imputed")= int [1:3] 105 229 436
..- attr(*, "names")= chr [1:460] "2" "14" "18" "19" ...
$ Dependents : Factor w/ 4 levels "0","1","2","3+": 2 1 1 1 1 1 3 2 1 2 ...
..- attr(*, "imputed")= int [1:15] 103 105 121 227 229 294 302 333 336 347 ...
..- attr(*, "names")= chr [1:460] "2" "14" "18" "19" ...
$ Education : Factor w/ 2 levels "Graduate","Not Graduate": 1 1 1 2 2 2 2 1 1 1 ...
$ Self_Employed : Factor w/ 2 levels "No","Yes": 1 1 1 1 1 1 1 1 1 2 ...
..- attr(*, "imputed")= int [1:32] 12 20 25 30 31 96 108 112 115 159 ...
..- attr(*, "names")= chr [1:460] "2" "14" "18" "19" ...
$ ApplicantIncome : num [1:460] 4583 1853 3510 4887 7660 ...
$ CoapplicantIncome : num [1:460] 1508 2840 0 0 0 ...
$ LoanAmount : num [1:460] 128 114 76 133 104 116 112 201 74 106 ...
$ Loan_Amount_Term : 'impute' Named num [1:460] 360 360 360 360 360 360 360 360 360 360 ...
..- attr(*, "names")= chr [1:460] "2" "14" "18" "19" ...
..- attr(*, "imputed")= int [1:9] 11 29 86 98 99 164 165 228 242
$ Credit_History : Factor w/ 2 levels "0","1": 2 2 1 2 1 1 1 2 2 2 ...
..- attr(*, "imputed")= int [1:50] 17 25 31 43 80 84 87 96 118 126 ...
..- attr(*, "names")= chr [1:460] "2" "14" "18" "19" ...
$ Property_Area : Factor w/ 3 levels "Rural","Semiurban",..: 1 1 3 1 3 2 1 3 3 1 ...
$ Loan_Status : Factor w/ 2 levels "N","Y": 1 1 1 1 1 1 1 1 1 1 ...
$ JointIncome : num [1:460] 6091 4693 3510 4887 7660 ...
$ MonthlyPayment : 'impute' Named num [1:460] 356 317 211 369 289 ...
..- attr(*, "imputed")= int [1:9] 11 29 86 98 99 164 165 228 242
..- attr(*, "names")= chr [1:460] "2" "14" "18" "19" ...
$ PaymentIncomeRatio: 'impute' Named num [1:460] 0.0584 0.0675 0.0601 0.0756 0.0377 ...
..- attr(*, "imputed")= int [1:9] 11 29 86 98 99 164 165 228 242
..- attr(*, "names")= chr [1:460] "2" "14" "18" "19" ...
$ BooleanDependents : Factor w/ 2 levels "No","Yes": 2 1 1 1 1 1 2 2 1 2 ...
$ JointIncome_log : num [1:460] 3.78 3.67 3.55 3.69 3.88 ...
# Keine Notwendigkeit für ein clean up sehen wir bei:
# "Loan_ID" -> Informativ (wird dann bei Recipe ausgeschlossen)
# "Gender" -> Gut, 2 Levels: Männlich, weiblich
# "Married" -> Gut, 2 Levels: Yes, No
# "Dependents" -> Gut, 4 Levels: 0, 1, 2, 3+
# 3+ für drei oder mehr belassen wir, da wir es als Faktor klassifiziert haben
# "Education" -> Gut, 2 Levels: Graduate, Not Graduate
# "Self_Employed" -> Gut, 2 Levels Yes, No
# "Credit_History" -> Gut, 2 Levels 0 (erfüllt die Richtlinien NICHT), 1 (erfüllt die Richtlinien)
# "Property_Area" -> Gut, 3 Levels Rural, Semiurban, Urban
# "Loan_Status" -> Informativ (wird dann bei Recipe ausgeschlossen)
# BooleanDependents -> Gut, 2 Levels Yes, No
#Wir schauen uns nun die anderen Variablen an:
summary(train1$ApplicantIncome)
Min. 1st Qu. Median Mean 3rd Qu. Max.
150 2891 3812 5182 5746 51763
summary(train1$CoapplicantIncome)
Min. 1st Qu. Median Mean 3rd Qu. Max.
0 0 1036 1592 2209 41667
summary(train1$JointIncome)
Min. 1st Qu. Median Mean 3rd Qu. Max.
1880 4164 5394 6774 7495 51763
# Wir sehen eine grosse Range, was mit fehlerhaften Eingaben zusammenhangen könnte
# Kurze Boxplot-Betrachtung
ggplot()+
geom_boxplot(data=train1, aes(x = JointIncome))
# Es gibt extrem grosse Ausreisser nach oben
# Wir treffen die Entscheidung Werte auszuschliessen, welche dem 10-fachen des IQA entsprechen
# Normalerweise gilt ein Wert als Ausreisser ab 1.5
# Damit soll der Effekt begrenzt werden, dass extrem hohe Werte das Ergebnis beeinflussen z.B.
# weil das jährliche statt monatliche Gehalt eingegeben wurde.
<- IQR(train1$JointIncome)
iqr
%>%
train1 filter(JointIncome > 10*iqr) %>%
select(JointIncome, PaymentIncomeRatio, LoanAmount, Loan_Amount_Term, MonthlyPayment, Loan_Status)
# A tibble: 6 × 6
JointIncome PaymentIncomeRatio LoanAmount Loan_Amount_Term MonthlyPayment
<dbl> <dbl> <dbl> <dbl> <dbl>
1 33846 0.0213 260 360 722.
2 35673 0.00701 90 360 250
3 42083 0.0462 350 180 1944.
4 39999 0.0833 600 180 3333.
5 51763 0.0451 700 300 2333.
6 37719 0.0112 152 360 422.
# ℹ 1 more variable: Loan_Status <fct>
summary(train1$LoanAmount)
Min. 1st Qu. Median Mean 3rd Qu. Max.
9.0 101.8 128.0 143.8 163.2 700.0
%>%
train1 select(LoanAmount, Loan_Amount_Term, JointIncome, Loan_Status, MonthlyPayment, PaymentIncomeRatio) %>%
arrange(LoanAmount)
# A tibble: 460 × 6
LoanAmount Loan_Amount_Term JointIncome Loan_Status MonthlyPayment
<dbl> <dbl> <dbl> <fct> <dbl>
1 9 360 2378 N 25
2 17 120 2385 Y 142.
3 25 120 3459 Y 208.
4 26 360 6500 Y 72.2
5 28.5 360 2000 N 79.2
6 40 360 13262 Y 111.
7 40 180 4106 Y 222.
8 42 180 3716 Y 233.
9 44 360 5649 Y 122.
10 45 360 3934 Y 125
# ℹ 450 more rows
# ℹ 1 more variable: PaymentIncomeRatio <dbl>
# Die Range geht von 9'000 bis 700'000
# Wir erachten v.a. die extrem tiefen LoanAmounts, als etwas merkwürdig.
# Wir werden später diese nochmals betrachten beim MonthlyPayment, denn
# dort wird nicht nur die absolute Summe betrachtet, sondern in Abhängigkeit
# von der Laufzeit.
summary(train1$Loan_Amount_Term)
9 values imputed to 360
Min. 1st Qu. Median Mean 3rd Qu. Max.
12.0 360.0 360.0 342.5 360.0 480.0
table(train1$Loan_Amount_Term)
12 36 60 84 120 180 240 300 360 480
1 2 2 3 2 32 3 10 392 13
# Die Range geht von 12 Monaten bis 480
# Davon ist ein Eintrag bei 12 Monaten und 2 bei 36 Monaten (3 Beobachtungen von Total 614)
# Wenn wir dazu die Rückzahlungsbeträge anschauen, scheinen diese extrem hoch. Ebenfalls
# erachten wir so kurze Hypothekarkredite als unwahrscheinlich, v.a. im Vergleich zu allen
# anderen Hypothekarlaufzeiten.
# Aus diesem Grund entschliessen wir uns alle Werte mit einer Laufzeit unter 60 Monaten (5 Jahre)
# auszuschliessen. In der Praxis könnte man natürlich bei der Bank anfragen, was die Mindestlaufzeit ist.
# Die besprochenen Zeilen anschauen:
%>%
train1 filter(Loan_Amount_Term <60) %>%
select(Loan_Amount_Term, LoanAmount, MonthlyPayment, JointIncome, PaymentIncomeRatio, Loan_Status)
# A tibble: 3 × 6
Loan_Amount_Term LoanAmount MonthlyPayment JointIncome PaymentIncomeRatio
<dbl> <dbl> <dbl> <dbl> <dbl>
1 36 155 4306. 3481 1.24
2 36 80 2222. 3358 0.662
3 12 111 9250 7482 1.24
# ℹ 1 more variable: Loan_Status <fct>
summary(train1$MonthlyPayment)
Imputed Values:
37 113 368 422 424 45 46 166
277.7778 422.2222 344.4444 222.2222 305.5556 266.6667 244.4444 505.5556
198
333.3333
Min. 1st Qu. Median Mean 3rd Qu. Max.
25.0 288.9 362.5 477.9 500.0 9250.0
%>%
train1 select(MonthlyPayment, LoanAmount, Loan_Amount_Term, JointIncome, Loan_Status, PaymentIncomeRatio) %>%
arrange(MonthlyPayment)
# A tibble: 460 × 6
MonthlyPayment LoanAmount Loan_Amount_Term JointIncome Loan_Status
<dbl> <dbl> <dbl> <dbl> <fct>
1 25 9 360 2378 N
2 72.2 26 360 6500 Y
3 79.2 28.5 360 2000 N
4 111. 40 360 13262 Y
5 122. 44 360 5649 Y
6 125 45 360 3934 Y
7 128. 46 360 2378 N
8 131. 47 360 3013 Y
9 131. 63 480 2237 N
10 133. 48 360 4885 Y
# ℹ 450 more rows
# ℹ 1 more variable: PaymentIncomeRatio <dbl>
# Wir definieren nun, dass eine angenommene Hypotheksumme von 20'000 über 30 Jahre das Minimum darstellt
# Dies würde ein monatlicher Zahlbetrag von 56.- ergeben. Wir beschliessen hiermit die Werte auszuschliessen,
# welche diesen Betrag monatlich unterschreiten.
# Die besprochenen Zeilen anschauen:
%>%
train1 filter(MonthlyPayment < 56) %>%
select(MonthlyPayment, Loan_Amount_Term, LoanAmount, JointIncome, PaymentIncomeRatio, Loan_Status)
# A tibble: 1 × 6
MonthlyPayment Loan_Amount_Term LoanAmount JointIncome PaymentIncomeRatio
<dbl> <dbl> <dbl> <dbl> <dbl>
1 25 360 9 2378 0.0105
# ℹ 1 more variable: Loan_Status <fct>
summary(train1$PaymentIncomeRatio)
Imputed Values:
37 113 368 422 424 45 46
0.08796003 0.05493393 0.06722179 0.08169935 0.03426663 0.05679801 0.07168459
166 198
0.07355675 0.07802747
Min. 1st Qu. Median Mean 3rd Qu. Max.
0.007008 0.056381 0.069371 0.078921 0.081404 1.236873
# aufsteigend:
%>%
train1 select(PaymentIncomeRatio, LoanAmount, Loan_Amount_Term, MonthlyPayment, JointIncome, Loan_Status) %>%
arrange(PaymentIncomeRatio)
# A tibble: 460 × 6
PaymentIncomeRatio LoanAmount Loan_Amount_Term MonthlyPayment JointIncome
<dbl> <dbl> <dbl> <dbl> <dbl>
1 0.00701 90 360 250 35673
2 0.00838 40 360 111. 13262
3 0.00969 55 360 153. 15759
4 0.0105 9 360 25 2378
5 0.0111 26 360 72.2 6500
6 0.0112 152 360 422. 37719
7 0.0127 103 360 286. 22500
8 0.0131 70 360 194. 14866
9 0.0179 96 360 267. 14880
10 0.0183 110 360 306. 16692
# ℹ 450 more rows
# ℹ 1 more variable: Loan_Status <fct>
# erachten wir als plausibel (konservativ betrachtet)
# absteigend:
%>%
train1 select(PaymentIncomeRatio, LoanAmount, Loan_Amount_Term, MonthlyPayment, JointIncome, Loan_Status) %>%
arrange(desc(PaymentIncomeRatio))
# A tibble: 460 × 6
PaymentIncomeRatio LoanAmount Loan_Amount_Term MonthlyPayment JointIncome
<dbl> <dbl> <dbl> <dbl> <dbl>
1 1.24 155 36 4306. 3481
2 1.24 111 12 9250 7482
3 0.662 80 36 2222. 3358
4 0.355 108 84 1286. 3620
5 0.298 144 84 1714. 5746
6 0.298 155 60 2583. 8666
7 0.293 125 60 2083. 7100
8 0.281 172 84 2048. 7283
9 0.192 135 360 375 1950
10 0.173 140 180 778. 4493
# ℹ 450 more rows
# ℹ 1 more variable: Loan_Status <fct>
# Wir erkennen hier teils sehr hohe oder sogar komplett unrealistische Werte (>1)
# Wir entschliessen uns alle Werte auszuschliessen, welche über 50% ausmachen
# Gemäss Internetrecherche sollten die Zahlungen nicht mehr als ca. 30% ausmachen
# Interessant ist hier ebenfalls, dass wir wieder die Werte 12 und 36 sehen.
# Wir könnten uns durchaus vorstellen, dass diese 120 oder 360 hätten sein sollen.
# Die besprochenen Zeilen anschauen:
%>%
train1 filter(PaymentIncomeRatio > 0.5) %>%
select(PaymentIncomeRatio, LoanAmount, Loan_Amount_Term, MonthlyPayment, JointIncome, Loan_Status)
# A tibble: 3 × 6
PaymentIncomeRatio LoanAmount Loan_Amount_Term MonthlyPayment JointIncome
<dbl> <dbl> <dbl> <dbl> <dbl>
1 1.24 155 36 4306. 3481
2 0.662 80 36 2222. 3358
3 1.24 111 12 9250 7482
# ℹ 1 more variable: Loan_Status <fct>
# Wir schliessen nun die oben erwähnten Zeilen aus (dataCleaning):
# Ausschliessen
<- train1 %>%
train1 filter(JointIncome < 10*iqr,
>= 60,
Loan_Amount_Term >= 56,
MonthlyPayment <= 0.5)
PaymentIncomeRatio
# In diesem Beispiel entfernen wir 10 von 460 Werten, was wir mit einem konservativen Ansatz gleichstellen.
# Wir können nun noch den iqr entfernen
rm(iqr)
# Korrelationsmatrix
%>%
train1 select(-Loan_ID, -JointIncome, -MonthlyPayment, -PaymentIncomeRatio, -BooleanDependents) %>%
mutate(Gender = as.integer(Gender),
Married = as.integer(Married),
Dependents = as.integer(Dependents),
Education = as.integer(Education),
Self_Employed = as.integer(Self_Employed),
ApplicantIncome = as.integer(ApplicantIncome),
CoapplicantIncome = as.integer(CoapplicantIncome),
LoanAmount = as.integer(LoanAmount),
Loan_Amount_Term = as.integer(Loan_Amount_Term),
Credit_History = as.integer(Credit_History),
Property_Area = as.integer(Property_Area),
Loan_Status = as.integer(Loan_Status),
JointIncome_log = as.integer(JointIncome_log)) %>%
select(Loan_Status, everything()) %>%
cor(use = "complete.obs", method = "spearman") %>%
corrplot(type = "lower", diag = FALSE, tl.col = "black", tl.cex = 0.7)
# Wir entfernen im ersten Schritt die nicht relevanten Variablen: Loan_ID: Keine Aussagekraft, JointIncome -> Ist eine Zusammensetzung
# aus ApplicantIncome und Coapplicantincome (welche wir separat hineinnehmen um allfällige Korrelationen dort zu erkennen).
# MonthlyPayment und PaymentIncomeRatio entfernen wir ebenfalls, da sie einfach in anderer Form Aussagen zu bereits bestehenden Variabeln machen.
# BolleanDependents fliesst über die Ursprungsvariable Dependents detaillierter ein.
# Wenn wir die erste Spalte betrachten, also wie Loan_Status korreliert ist mit anderen Variablen, sehen wir, dass sie sehr stark Korreliert
# mit der Credit_History (was wir auch bei der explorativen Analyse erkannt haben), sowie ebenfalls leicht korreliert mit der Education und dem Zivilstand.
# *****************************************************
# 4. Zwischenresultate speichern ----------------------
# Wir speichern 'train1' in dem Ordner 'Zwischenresultate'.
# write_rds(train1, "zwischenresultate/train1.rds")
4. Modelling
# *****************************************************
# ----------------- Home Loan Approval ----------------
# Beschreibung: Modelling
# *****************************************************
# 1. Vorarbeiten --------------------------------------
# Workspace aufräumen
rm(list = ls())
# Lade Software aus R Packages
library(tidyverse)
library(tidymodels)
library(vetiver) # Um das finale Modell zu exportieren.
# Lade Zwischenresultate
<- read_rds("zwischenresultate/train1.rds")
train2 <- read_rds("zwischenresultate/test.rds") test2
# *****************************************************
# 2. test2 analog zu train2 aufbauen ------------------
# Feature Engineering auf Testdatensatz anwenden
<- test2 %>%
test2 mutate(MonthlyPayment = LoanAmount*1000/Loan_Amount_Term,
PaymentIncomeRatio = MonthlyPayment/JointIncome,
BooleanDependents = as.factor(ifelse(Dependents == 0, "No", "Yes")),
JointIncome_log = log10(JointIncome))
# Data Cleaning auf Testdatensatz anwenden
<- IQR(test2$JointIncome)
iqr
<- test2 %>%
test2 filter(JointIncome < 10*iqr,
>= 60,
Loan_Amount_Term >= 56,
MonthlyPayment <= 0.5)
PaymentIncomeRatio
# Es werden hier 4 von 154 Werte entfernt, was ebenfalls als konservativ zu betrachten ist.
rm(iqr)
# *****************************************************
# 3. Recipe erstellen ---------------------------------
#Loan_Amount_Term, MonthlyPayment, PaymentIncomeRatio -> Umwandlung in Double (ist später notwendig)
$Loan_Amount_Term <- as.double(train2$Loan_Amount_Term)
train2$MonthlyPayment <- as.double(train2$MonthlyPayment)
train2$PaymentIncomeRatio <- as.double(train2$PaymentIncomeRatio)
train2$JointIncome_log <- as.double(train2$JointIncome_log)
train2
$Loan_Amount_Term <- as.double(test2$Loan_Amount_Term)
test2$MonthlyPayment <- as.double(test2$MonthlyPayment)
test2$PaymentIncomeRatio <- as.double(test2$PaymentIncomeRatio)
test2$JointIncome_log <- as.double(test2$JointIncome_log)
test2
# Als erstes schauen wir uns nochmal die Datentypen der Spalten genau an.
glimpse(train2)
Rows: 450
Columns: 18
$ Loan_ID <chr> "LP001003", "LP001029", "LP001036", "LP001038", "LP…
$ Gender <fct> Male, Male, Female, Male, Male, Male, Male, Male, M…
$ Married <fct> Yes, No, No, Yes, Yes, Yes, Yes, Yes, No, No, Yes, …
$ Dependents <fct> 1, 0, 0, 0, 0, 0, 2, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, …
$ Education <fct> Graduate, Graduate, Graduate, Not Graduate, Not Gra…
$ Self_Employed <fct> No, No, No, No, No, No, No, No, No, Yes, No, No, No…
$ ApplicantIncome <dbl> 4583, 1853, 3510, 4887, 7660, 2600, 3365, 4166, 316…
$ CoapplicantIncome <dbl> 1508, 2840, 0, 0, 0, 1911, 1917, 3369, 0, 0, 1330, …
$ LoanAmount <dbl> 128.0000, 114.0000, 76.0000, 133.0000, 104.0000, 11…
$ Loan_Amount_Term <dbl> 360, 360, 360, 360, 360, 360, 360, 360, 360, 360, 3…
$ Credit_History <fct> 1, 1, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, …
$ Property_Area <fct> Rural, Rural, Urban, Rural, Urban, Semiurban, Rural…
$ Loan_Status <fct> N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, N, …
$ JointIncome <dbl> 6091, 4693, 3510, 4887, 7660, 4511, 5282, 7535, 316…
$ MonthlyPayment <dbl> 355.5556, 316.6667, 211.1111, 369.4444, 288.8889, 3…
$ PaymentIncomeRatio <dbl> 0.05837392, 0.06747638, 0.06014562, 0.07559739, 0.0…
$ BooleanDependents <fct> Yes, No, No, No, No, No, Yes, Yes, No, Yes, No, No,…
$ JointIncome_log <dbl> 3.784689, 3.671451, 3.545307, 3.689042, 3.884229, 3…
# Recipe aufsetzen
<-
status_rec recipe(Loan_Status ~ ., data = train2) %>%
update_role(Loan_ID, new_role = "ID") %>% # Weist der Loan_ID den Typ "ID"-zu
step_dummy(all_nominal_predictors(), one_hot = TRUE) %>% # Führt ein One-Hot-Encoding durch
step_zv(all_predictors()) %>% # Entfernt 'zero-variance' Variablen.
step_normalize(all_predictors()) # Standardisiert alle numerischen Variablen
# Zusammenfassung des 'recipe'
summary(status_rec) %>%
print(n = 18)
# A tibble: 18 × 4
variable type role source
<chr> <list> <chr> <chr>
1 Loan_ID <chr [3]> ID original
2 Gender <chr [3]> predictor original
3 Married <chr [3]> predictor original
4 Dependents <chr [3]> predictor original
5 Education <chr [3]> predictor original
6 Self_Employed <chr [3]> predictor original
7 ApplicantIncome <chr [2]> predictor original
8 CoapplicantIncome <chr [2]> predictor original
9 LoanAmount <chr [2]> predictor original
10 Loan_Amount_Term <chr [2]> predictor original
11 Credit_History <chr [3]> predictor original
12 Property_Area <chr [3]> predictor original
13 JointIncome <chr [2]> predictor original
14 MonthlyPayment <chr [2]> predictor original
15 PaymentIncomeRatio <chr [2]> predictor original
16 BooleanDependents <chr [3]> predictor original
17 JointIncome_log <chr [2]> predictor original
18 Loan_Status <chr [3]> outcome original
# *****************************************************
# 4. Cross-Validation-Split ---------------------------
# Seed für Reproduzierbarkeit
set.seed(123)
# 5-Fold CV
<- vfold_cv(train2, v = 5, repeats = 1, strata = Loan_Status)
folds # v = Anzahl Folds
# repeats = Wie oft die Cross Validation wiederholt werden soll
# strata = y : Macht einen stratifizierten Split nach Loan_Status, dass der Anteil Yes/No in jedem Fold gleich stark vorhanden ist.
# *****************************************************
# 5. Logistische Regression ---------------------------
# Logistische Regression initieren
<-
lr_mod logistic_reg(penalty = tune(), mixture = tune()) %>%
set_engine("glmnet")
# logistisches Regressionsmodell initieren, getunt wird penalty und mixture
# set_engine("glmnet") -> Welches Package wir für die Modellberechnung verwenden -> glmnet berechnet regularisierte Modelle!
# Workflow
<-
lr_workflow workflow() %>%
add_model(lr_mod) %>%
add_recipe(status_rec)
# Aufsetzen des Workflows -> workflow()
# Das Modell hinzufügen add_model(lr_mod)
# recipe dazugeben
# Tuning Grid
<- expand.grid(penalty = 10^seq(-5, 3, by = 1), mixture = c(0, 1))
lr_grid #mit expand.grid alle Kombinationen durchtesten (Lambda von 10^-5 bis 10^3) + jeweils 0 oder 1 für mixture
# Wie sieht Grid aus?
print(lr_grid)
penalty mixture
1 1e-05 0
2 1e-04 0
3 1e-03 0
4 1e-02 0
5 1e-01 0
6 1e+00 0
7 1e+01 0
8 1e+02 0
9 1e+03 0
10 1e-05 1
11 1e-04 1
12 1e-03 1
13 1e-02 1
14 1e-01 1
15 1e+00 1
16 1e+01 1
17 1e+02 1
18 1e+03 1
# Tuning / Model Fitting
<-
lr_res %>%
lr_workflow tune_grid(
resamples = folds,
grid = lr_grid,
control = control_grid(save_pred = TRUE),
metrics = metric_set(roc_auc))
# tune_grid() -> Durchführen des Hyperparametertunings
# resamples = folds -> Die vordefinierten Folds werden genutzt, um dann jedes Parameter-Set zu evaluieren
# Übergabe des hierfür definierten grid = lr_grid
# control = control_grid(save_pred = TRUE) -> speichert die Vorhersagen für die verschiedenen Hyperparameterwerte -> benötgen wir für die spätere Berechnung der Modellgüte
# metrics = metric_set(roc_auc) -> definiert das Modellgütemass
# Wir sortieren die Hyperparameter Spezifikationen nach ROC AUC
%>%
lr_res show_best("roc_auc", n = 18) %>%
arrange(desc(mean))
# A tibble: 18 × 8
penalty mixture .metric .estimator mean n std_err .config
<dbl> <dbl> <chr> <chr> <dbl> <int> <dbl> <chr>
1 0.1 0 roc_auc binary 0.752 5 0.0285 Preprocessor1_Mode…
2 0.01 1 roc_auc binary 0.751 5 0.0269 Preprocessor1_Mode…
3 0.00001 0 roc_auc binary 0.751 5 0.0278 Preprocessor1_Mode…
4 0.0001 0 roc_auc binary 0.751 5 0.0278 Preprocessor1_Mode…
5 0.001 0 roc_auc binary 0.751 5 0.0278 Preprocessor1_Mode…
6 0.01 0 roc_auc binary 0.751 5 0.0278 Preprocessor1_Mode…
7 1 0 roc_auc binary 0.746 5 0.0258 Preprocessor1_Mode…
8 0.001 1 roc_auc binary 0.746 5 0.0250 Preprocessor1_Mode…
9 0.00001 1 roc_auc binary 0.746 5 0.0245 Preprocessor1_Mode…
10 0.0001 1 roc_auc binary 0.746 5 0.0245 Preprocessor1_Mode…
11 100 0 roc_auc binary 0.744 5 0.0223 Preprocessor1_Mode…
12 10 0 roc_auc binary 0.744 5 0.0228 Preprocessor1_Mode…
13 0.1 1 roc_auc binary 0.700 5 0.0135 Preprocessor1_Mode…
14 1000 0 roc_auc binary 0.5 5 0 Preprocessor1_Mode…
15 1 1 roc_auc binary 0.5 5 0 Preprocessor1_Mode…
16 10 1 roc_auc binary 0.5 5 0 Preprocessor1_Mode…
17 100 1 roc_auc binary 0.5 5 0 Preprocessor1_Mode…
18 1000 1 roc_auc binary 0.5 5 0 Preprocessor1_Mode…
# Plot der Resultate
%>%
lr_res collect_metrics() %>%
filter(.metric == "roc_auc") %>%
ggplot(aes(x = penalty, y = mean, color = factor(mixture), linetype = factor(mixture))) +
geom_point() +
geom_line() +
labs(x = "Penalty", y = "AUC ROC") +
scale_x_log10() +
theme_bw()
# Wir wählen das besten Modell
<-
lr_best %>%
lr_res show_best("roc_auc", n = 18) %>%
arrange(desc(mean)) %>%
slice(1)
# Resultate vorbereiten für Konfusionsmatrix
<- 0.5
thres
<-
lr_konfusionsmatrix %>%
lr_res collect_predictions(parameters = lr_best) %>%
mutate(pred = ifelse(.pred_Y >= thres, "Y", "N"))
# Konfusionsmatrix
table(lr_konfusionsmatrix$pred, lr_konfusionsmatrix$Loan_Status, dnn = c("pred", "true"))
true
pred N Y
N 58 4
Y 80 308
# Wir sehen eine hohe Anzahl an False Positive (FP) fehlern, was bedeutet, dass das lineare
# Regressionsmodell einen Antrag mit Annehmen vorhersagt, obwohl er in der Realität abgelehnt
# wurde. Dies ist eher ein schlechtes Zeichen. (Wir schauen dann bei Naive Bayes nochmals)
# Resultate vorbereiten für ROC Kurve
<-
lr_auc %>%
lr_res collect_predictions(parameters = lr_best) %>%
roc_curve(Loan_Status, .pred_Y, event_level = "second") %>%
mutate(model = "Logistic Regression")
# collect_predictions(parameters = lr_best) -> Extrahiert die Vorhersagen des Modells für die besten Hyperparameter-Einstellungen (lr_best), die aus dem vorherigen Hyperparametertuning ermittelt wurden.
# roc_curve -> Berechnet die ROC-Kurve für die vorhergesagten Werte und die tatsächlichen Werte (y), wobei .pred_Y die Spalte mit den vorhergesagten Wahrscheinlichkeiten für die Yes-Klasse ist. Die Option event_level = "second" bedeutet, dass Yes als zweites Level definiert ist.
# mutate(model = "Logistic Regression"): Fügt der ROC-Kurve eine Spalte hinzu, um das Modell anzugeben, das für die Kurve verwendet wurde, in diesem Fall "Logistic Regression".
# ROC Kurve
autoplot(lr_auc)
# Last Fit
# Last Fit Spezifikation (optimale Hyperparameter Werte)
<-
last_lr_mod logistic_reg(penalty = 0.1, mixture = 0) %>%
set_engine("glmnet")
# Workflow anpassen (optimales Modell)
<-
last_lr_workflow %>%
lr_workflow update_model(last_lr_mod)
# Das Modell im lr_workflow wird upgedatet durch das last_lr_mod
# Last Fit (auf ganzem Trainingsdatensatz)
<-
last_lr_fit %>%
last_lr_workflow fit(data = train2)
# Extrahieren der Parameter (Übersicht)
%>%
last_lr_fit extract_fit_parsnip() %>%
tidy() %>%
print(n = 28)
# A tibble: 28 × 3
term estimate penalty
<chr> <dbl> <dbl>
1 (Intercept) 0.889 0.1
2 ApplicantIncome -0.0526 0.1
3 CoapplicantIncome -0.0130 0.1
4 LoanAmount -0.132 0.1
5 Loan_Amount_Term -0.125 0.1
6 JointIncome -0.0573 0.1
7 MonthlyPayment -0.0224 0.1
8 PaymentIncomeRatio -0.0956 0.1
9 JointIncome_log 0.0623 0.1
10 Gender_Female 0.00892 0.1
11 Gender_Male -0.00892 0.1
12 Married_No -0.0919 0.1
13 Married_Yes 0.0919 0.1
14 Dependents_X0 0.00856 0.1
15 Dependents_X1 -0.0803 0.1
16 Dependents_X2 0.0573 0.1
17 Dependents_X3. 0.0190 0.1
18 Education_Graduate 0.0414 0.1
19 Education_Not.Graduate -0.0414 0.1
20 Self_Employed_No 0.0274 0.1
21 Self_Employed_Yes -0.0274 0.1
22 Credit_History_X0 -0.466 0.1
23 Credit_History_X1 0.466 0.1
24 Property_Area_Rural -0.0892 0.1
25 Property_Area_Semiurban 0.158 0.1
26 Property_Area_Urban -0.0729 0.1
27 BooleanDependents_No 0.00861 0.1
28 BooleanDependents_Yes -0.00859 0.1
# extract_fit_parsnip(): Extrahiert die Parameter des trainierten Modells in einem für das Parsnip-Paket geeigneten Format.
# tidy() %>%: Verwendet die tidy()-Funktion, um die extrahierten Modellparameter in eine saubere, tabellarische Form zu bringen.
# Variable importance
# Barplot der Parameter
%>%
last_lr_fit extract_fit_parsnip() %>%
tidy() %>%
filter(abs(estimate) > 0.05 & term != "(Intercept)") %>%
mutate(pos = estimate >= 0) %>%
arrange(desc(estimate)) %>%
ggplot(aes(x = reorder(term, estimate, sort), y = estimate, fill = pos)) +
geom_bar(stat = "identity") +
coord_flip() +
labs(x = NULL, y = "Stärke des Koeffizienten") +
guides(fill = "none") +
theme_bw()
# Wir sehen, dass die Credit-History (yes / no) extrem wichtig ist
# Andere Variablen z.B. LoanAmount und Loan Amount Term wie auch Wohngegend sind ebefalls wichtig
# Danach folgend noch einige andere Variablen, mit ähnlichem Einfluss
# *****************************************************
# 6. Naive Bayes --------------------------------------
# Wir laden zwei zusätzliche R-Packages
library(discrim)
library(naivebayes)
# Modell Spezifikation
<-
nb_mod naive_Bayes(Laplace = 1.0) %>%
set_engine("naivebayes")
# naive_Bayes(Laplace = 1.0) -> Inititierung Modell + Count-Initierung bei 1, damit das Problem behoben wird, dass wenn ein Spam ein Wort nicht hat, dass dann die W'keit gesamt auf 0 gesetzt wird -> wurde etwas weiter oben mal genau beschrieben.
# Package das verwendet wird -> naivebayes
.2 <- train2 %>% select(-Loan_ID)
train2.2 <- test2 %>% select(-Loan_ID)
test2
# Recipe für Naive Bayes
<-
nb_recipe recipe(Loan_Status ~ ., data = train2.2) %>%
step_mutate_at(all_predictors(), fn = factor)
# Workflow für Naive Bayes
<-
nb_workflow workflow() %>%
add_model(nb_mod) %>%
add_recipe(nb_recipe)
# Model Fitting Naive Bayes
<-
nb_fit %>%
nb_workflow fit(data = train2.2)
# Schauen wir uns das Fit Objekt an
%>%
nb_fit extract_fit_parsnip()
parsnip model object
================================= Naive Bayes ==================================
Call:
naive_bayes.default(x = maybe_data_frame(x), y = y, laplace = ~1,
usekernel = TRUE)
--------------------------------------------------------------------------------
Laplace smoothing: 1
--------------------------------------------------------------------------------
A priori probabilities:
N Y
0.3066667 0.6933333
--------------------------------------------------------------------------------
Tables:
--------------------------------------------------------------------------------
:: Gender (Bernoulli)
--------------------------------------------------------------------------------
Gender N Y
Female 0.1857143 0.1783439
Male 0.8142857 0.8216561
--------------------------------------------------------------------------------
:: Married (Bernoulli)
--------------------------------------------------------------------------------
Married N Y
No 0.4000000 0.3280255
Yes 0.6000000 0.6719745
--------------------------------------------------------------------------------
:: Dependents (Categorical)
--------------------------------------------------------------------------------
Dependents N Y
0 0.58450704 0.58860759
1 0.21126761 0.15506329
2 0.13380282 0.17405063
3+ 0.07042254 0.08227848
--------------------------------------------------------------------------------
:: Education (Bernoulli)
--------------------------------------------------------------------------------
Education N Y
Graduate 0.7357143 0.7961783
Not Graduate 0.2642857 0.2038217
--------------------------------------------------------------------------------
:: Self_Employed (Bernoulli)
--------------------------------------------------------------------------------
Self_Employed N Y
No 0.8285714 0.8726115
Yes 0.1714286 0.1273885
--------------------------------------------------------------------------------
# ... and 11 more tables
--------------------------------------------------------------------------------
# Wir rechnen die Vorhersagen auf Training- und Testset
.2_pred <- predict(nb_fit, train2.2, type = "prob")
train2.2_pred <- predict(nb_fit, test2.2, type = "prob")
test2
# Resultate vorbereiten für Konfusionsmatrix
<- 0.5
thres
<-
nb_konfusionsmatrix .2_pred %>%
train2bind_cols(train2.2) %>%
mutate(pred = ifelse(.pred_Y >= thres, "Y", "N"))
# Konfusionsmatrix
table(nb_konfusionsmatrix$pred, nb_konfusionsmatrix$Loan_Status, dnn = c("pred", "true"))
true
pred N Y
N 134 2
Y 4 310
# Wir sehen praktisch nur True Negative und True Positive Predications. Dies ist im
# Vergleich zum logistischen Regressionsmodell viel besser. Dies sehen wir ebenfalls mit
# der nächsten Grafik zur ROC-Kurve. * Beachte die spätere Anmerkung hierzu
# Resultate vorbereiten für ROC Kurve
<-
nb_auc .2_pred %>%
train2bind_cols(train2.2["Loan_Status"]) %>%
roc_curve(Loan_Status, .pred_Y, event_level = "second") %>%
mutate(model = "Naive Bayes")
# ROC Kurve
autoplot(nb_auc)
# Vergleich der ROC Kurven
bind_rows(nb_auc, lr_auc) %>%
ggplot(aes(x = 1 - specificity, y = sensitivity, col = model)) +
geom_path(linewidth = 0.7, alpha = 0.8) +
geom_abline(lty = 3) +
coord_equal() +
scale_color_viridis_d(option = "plasma", end = .6) +
theme_bw()
# Beachte: Die Naive-Bayes Kurve sieht hier viel besser aus und liefert viel bessere Ergebnisse.
# Dies werden wir später sehen, täuscht jedoch, da Naive Bays das Training auf allen Werten durchführt
# Die logistische Regression hingegen macht das Hyperparamtertuning mit 5 Cross Fold Validation Sets.
# Bedeutet jedes Mal wird mit 1/5 der Daten, welche NICHT für das Training verwendet wurden, evaluiert.
# Dies werden wir nun bei der Testset-Performance sehen.
# *****************************************************
# 7. Testset-Performance ------------------------------
# Testset-Performance Last Fit (LR und NB)
<- augment(last_lr_fit, test2)
test_lr_aug <- augment(nb_fit, test2.2)
test_nb_aug
# ROC AUC für Logistische Regression
%>%
test_lr_aug roc_auc(truth = Loan_Status, .pred_Y, event_level = "second")
# A tibble: 1 × 3
.metric .estimator .estimate
<chr> <chr> <dbl>
1 roc_auc binary 0.810
# Die Fläche unter der ROC-Kurve ist 0.810
# Vergleich ROC AUC - Logistische Regression auf Train-Datensatz
%>% augment(train2) %>% roc_auc(truth = Loan_Status, .pred_Y, event_level = "second") last_lr_fit
# A tibble: 1 × 3
.metric .estimator .estimate
<chr> <chr> <dbl>
1 roc_auc binary 0.803
# Wir sehen auf den Train-Daten war die ROC-AUC 0.803
# ROC AUC für Naive Bayes
%>%
test_nb_aug roc_auc(truth = Loan_Status, .pred_Y, event_level = "second")
# A tibble: 1 × 3
.metric .estimator .estimate
<chr> <chr> <dbl>
1 roc_auc binary 0.771
# Die Fläche unter der ROC-Kurve ist 0.771
# Vergleich der ROC Kurven auf dem Testdatensatz
%>% roc_curve(Loan_Status, .pred_Y, event_level = "second") %>% mutate(model = "logistische Regression") %>%
test_lr_aug bind_rows(test_nb_aug %>% roc_curve(Loan_Status, .pred_Y, event_level = "second") %>% mutate(model = "Naive Bayes")) %>%
ggplot(aes(x = 1 - specificity, y = sensitivity, col = model)) +
geom_path(linewidth = 0.7, alpha = 0.8) +
geom_abline(lty = 3) +
coord_equal() +
scale_color_viridis_d(option = "plasma", end = .6) +
theme_bw()
# Wir sehen nun, dass die logistische Regression besser ist. Der Unterschied zum Vergleich der ROC-Kurven von oben (Train-Set)
# ist signifikant. Die Gründe haben wir oben bereits erwähnt.
# *********************************************
# 8. Baseline-Vergleich -----------------------
# Das Verhältnis zwischen YES und NO haben wir herausgefunden beträgt 0.68 (siehe 02_split_explore)
%>%
test2 mutate(.pred_Y = 0.68) %>%
roc_auc(truth = Loan_Status, .pred_Y, event_level = "second")
# A tibble: 1 × 3
.metric .estimator .estimate
<chr> <chr> <dbl>
1 roc_auc binary 0.5
%>%
train2 mutate(.pred_Y = 0.68) %>%
roc_auc(truth = Loan_Status, .pred_Y, event_level = "second")
# A tibble: 1 × 3
.metric .estimator .estimate
<chr> <chr> <dbl>
1 roc_auc binary 0.5
# Verbesserung unseres Modelles zur Baseline
0.810-0.5)/0.5 (
[1] 0.62
# 0.810: ROC-AUC aus unserem logistischen Regressionsmodell
# 0.5: ROC-AUC aus der Baseline oben
# Die führt zu einer Verbesserung von 62%
# *****************************************************
# 9. Modell speichern ---------------------------------
# Mit dem R-Package vetiver können wir unser finales Modell
# in einer Form speichern, so dass es parat ist für das Deployment.
<- vetiver_model(last_lr_fit, "last_lr_fit")
final_model
# Was für ein Objekt ist es?
final_model
── last_lr_fit ─ <bundled_workflow> model for deployment
A glmnet classification modeling workflow using 16 features
# Wir speichern das Objekt anschliessend als RDS File.
# Das braucht VIEL Speicherplatz!
# write_rds(final_model, "zwischenresultate/final_model.rds")