In diesem Projekt wurde das Ziel verfolgt, die Vorhersagengenauigkeit eines Klassifikationsmodells im Hinblick auf die Sensitivity unter den Bedingungen eines Class Imbalance Problems zu erhöhen. . Die Sensitivity ist hierbei von besonderem Interesse, da sie die Fähigkeit misst, die positive Klasse korrekt zu identifizieren.
Um dieses Ziel zu erreichen, wurden verschiedene Ansätze verfolgt:
Unterschiedliche Modelle: Es wurden verschiedene Klassifikationsalgorithmen (Random Forest, GLM, Naive Bayes, K-Nearest Neighbor, Conditional Inference Trees) getestet, um ihre Leistung hinsichtlich der Sensitivity zu vergleichen.
Sampling-Methoden: Um das Ungleichgewicht der Klassen auszugleichen, kamen Methoden Random Oversampling, Random Undersampling, SMOTE, Nearmiss, Tomek, SMOTETomek und ADASYN zum Einsatz.
Die Ergebnisse wurden analysiert, um die optimale Kombination aus Modell und Sampling-Methode identifizieren, die eine deutliche Steigerung der Sensitivity ermöglichen.
Ladung der benötigten Pakete:
library(ggplot2)
library(caret)
library(ggmosaic)
library(ROSE)
library(knitr)
library(tidyr)
library(themis)
library(recipes)
library(ggfortify)
library(corrplot)
library(patchwork)
library(dplyr)
library(kableExtra)
library(reshape2)
Das für dieses Projekt genutzte Dataset Binary stammt von der Plattform Kaggle. Binary ist ein simuliertes Dataset, das Informationen zu den Zulassungen von Studierenden an eine Bildungseinrichtung enthält. Es umfasst 400 Fälle und 4 verschiedene Variablen, die für den Zulassungsprozess relevant sind, und dient als Muster-Dataset für Bildungs- und Analysezwecke.
Ladung der Daten:
data <- read.csv("binary.csv", sep = ",")
head(data) %>%
kable(caption = "Binary") %>%
kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE,
position = "left")
| admit | gre | gpa | rank |
|---|---|---|---|
| 0 | 380 | 3.61 | 3 |
| 1 | 660 | 3.67 | 3 |
| 1 | 800 | 4.00 | 1 |
| 1 | 640 | 3.19 | 4 |
| 0 | 520 | 2.93 | 4 |
| 1 | 760 | 3.00 | 2 |
dim(data)
## [1] 400 4
summary(data)
## admit gre gpa rank
## Min. :0.0000 Min. :220.0 Min. :2.260 Min. :1.000
## 1st Qu.:0.0000 1st Qu.:520.0 1st Qu.:3.130 1st Qu.:2.000
## Median :0.0000 Median :580.0 Median :3.395 Median :2.000
## Mean :0.3175 Mean :587.7 Mean :3.390 Mean :2.485
## 3rd Qu.:1.0000 3rd Qu.:660.0 3rd Qu.:3.670 3rd Qu.:3.000
## Max. :1.0000 Max. :800.0 Max. :4.000 Max. :4.000
Variablen:
Ermittlung der fehlenden Werte:
sapply(data, function(x) sum(is.na(x)))
## admit gre gpa rank
## 0 0 0 0
Ermittlung der Klassen der Variablen:
sapply(data, FUN = class)
## admit gre gpa rank
## "integer" "integer" "numeric" "integer"
Faktorisierung der Zielvariable:
data$admit <- as.factor(data$admit)
ggplot(data, aes(x = admit, fill = admit)) + geom_bar(width = 0.3, alpha = 0.8) +
geom_text(stat = 'count', aes(label = scales::percent(after_stat(count)/sum(after_stat(count)))), vjust = -0.5, size = 3) +
theme_minimal() +
scale_fill_manual(values = c("skyblue", "#04B4AE")) +
geom_hline(yintercept = 0, size = 1, colour="#333333") +
labs(y = "Häufigkeit") +
theme(legend.position = "none") +
ggtitle("Verteilung der Zielvariable")
Der Anteil der aufgenommenen Personen beträgt 32 %, während der Anteil der nicht aufgenommenen Personen mit 68 % deutlich höher ist.
ggplot(data, aes(x = rank, fill = after_stat(count))) + geom_bar(width = 0.3, alpha = 0.8) +
labs(x = "Rank", y = "Häufigkeit") +
geom_text(stat = 'count', aes(label = after_stat(count)), vjust = -0.5, size = 3)+
theme_minimal() +
geom_hline(yintercept = 0, size = 1, colour="#333333") +
theme(legend.position = "none") +
ggtitle("Verteilung des Schulrangs")
Korrelation von gpa und gre:
cor(data$gpa, data$gre)
## [1] 0.3842659
Eine Korrelation von 0,38 zwischen den Variablen gpa (Schulnotendurchsnitt) und gre (Testergebnisse) weist auf eine positive, aber moderate Korrelation hin. Diese Korrelation deutet darauf hin, dass es einen gewissen Zusammenhang zwischen den Schulnoten und den Testergebnissen gibt. Studierende mit besseren Noten tendieren also dazu, auch höhere Testergebnisse zu erzielen. Dennoch gibt es wahrscheinlich auch weitere Faktoren, die die Testergebnisse beeinflussen können, da die Korrelation nicht sehr stark ist.
to.plot <- data[, c('gre','gpa')]
to.plot.r <- melt(to.plot)
ggplot(to.plot.r, aes(x = factor(variable), y = value, fill = variable, color = variable)) +
geom_violin(trim=T, alpha = 0.5) +
ggtitle("Verteilung gre und gpa") +
geom_boxplot(width=0.2, outlier.color = "red", outlier.shape = 16) +
facet_wrap(.~variable, scales = 'free') +
scale_fill_manual(values = c("lightblue", "lightgreen")) +
scale_color_manual(values = c("navy", "darkgreen")) +
labs(x = "", y = "Wert") +
theme_minimal() +
theme(legend.position = "none")
Die wenigen Außreiser werden in diesem Projekt nicht entfernt.
data_prop <- data %>%
group_by(rank, admit) %>%
summarise(count = n()) %>%
mutate(prozent = count / sum(count) * 100)
ggplot(data_prop, aes(x = rank, y = prozent, fill = as.factor(admit))) +
geom_bar(stat="identity", position = position_dodge(width = 0.6), width = 0.8) +
labs(x = "Rank", y = "Prozent") +
scale_fill_manual(
values = c("lightblue", "#04B4AE"),
labels = c("Nicht aufgenommen", "Aufgenommen"),
name = "Admit"
) +
geom_text(aes(label = paste0(round(prozent), "%")),
position = position_dodge(width = 0.6),
vjust = -0.4,
size = 2.5) +
theme_minimal() +
ggtitle("Schulrang und Aufnahme")
Die Abbildung zeigt, dass der Rang der besuchten Schule eine wichtige Rolle bei der Aufnahmequote spielt. Ein höherer Rang der Schule korreliert mit einer höheren Wahrscheinlichkeit der Aufnahme in die Bildungseinrichtung.
ggplot(data, aes(x = gpa, y = gre, shape = factor(rank), color = factor(admit))) +
geom_point() +
geom_hline(yintercept = 587.7, linetype = "dashed", colour = "navy") +
geom_vline(xintercept = 3.390, linetype = "dashed", colour = "navy") +
labs(color = "Admit", shape = "Rank", x = "gpa", y = "gre") +
scale_color_manual(values = c("skyblue", "darkred")) +
theme_minimal()
Die Abbildung zeigt die Zusammenhänge zwischen den Variablen gre, gpa, admit und rank. Die dunkelblauen gestrichelten Linien markieren die Durchschnittswerte von gpa (Schulnoten) und gre (Testergebnisse). Die Abbildung deutet darauf hin, dass die Aufnahmeentscheidungen nicht ausschließlich auf akademischen Leistungen basieren. Die Schulqualität (rank) scheint ebenfalls eine Rolle zu spielen, ist jedoch kein garantierter Faktor für eine Aufnahme. So lässt sich erkennen, dass in einigen Fällen selbst ein hoher Schulrang (bessere Schulen) in Kombination mit überdurchschnittlichen Leistungen nicht zu einer Aufnahme führte. Ebenso gab es einige wenige Fälle, in denen Personen mit unterdurchschnittlichen Schulnoten und Testergebnissen aus Schulen mit niedrigem Rang dennoch aufgenommen wurden.
Um ein Vorhersagemodell zu erstellen, müssen die Daten zunächst in ein Trainings- und ein Testset aufgeteilt werden. In diesem Projekt wurde ein Verhältnis von 70:30 zwischen den beiden Sets festgelegt.
set.seed(404)
sapply(data, FUN = class)
## admit gre gpa rank
## "factor" "integer" "numeric" "integer"
train.index <- createDataPartition(data$admit,
p=0.7,
list=FALSE)
head(train.index)
## Resample1
## [1,] 1
## [2,] 2
## [3,] 3
## [4,] 5
## [5,] 11
## [6,] 12
training <- data[train.index,]
testing<- data[-train.index,]
nrow(training); nrow(testing)
## [1] 281
## [1] 119
Im nächsten Schritt werden die Modelle Naive Bayes, KNN, GLM, Ctree und Random Forest trainiert und getestet.
set.seed(404)
model.nb <- train(admit ~ .,
data = training,
method = 'naive_bayes')
model.knn <- train(admit ~ .,
data = training,
method = 'knn')
model.glm <- train(admit ~ .,
data = training,
method = 'glm')
model.ctree <- train(admit ~ .,
data = training,
method = 'ctree')
model.rf <- train(admit ~ .,
data = training,
method = 'rf')
## note: only 2 unique complexity parameters in default grid. Truncating the grid to 2 .
model_list <- list("glm" = model.glm, "tree" = model.ctree, "knn" = model.knn,
"nb" = model.nb, "rf" = model.rf)
results_model <- data.frame()
for (model_name in names(model_list)) {
predictions <- predict(model_list[[model_name]], newdata = testing)
conf_matrix <- confusionMatrix(predictions, testing$admit, positive = '1')
kappa <- conf_matrix$overall["Kappa"]
accuracy <- conf_matrix$overall["Accuracy"]
sensitivity <- conf_matrix$byClass["Sensitivity"]
specificity <- conf_matrix$byClass["Specificity"]
f1 <- conf_matrix$byClass["F1"]
results_model <- rbind(results_model, data.frame(Model = model_name, Accuracy = accuracy, Kappa = kappa, Sensitivity = sensitivity,
Specificity = specificity, F1 = f1))
}
Das Ziel des Projekts besteht darin, die Vorhersagegenauigkeit für die kleinere positive Klasse zu verbessern. Daher wurden die Modellergebnisse nach der Sensitivity sortiert.
rownames(results_model) <- NULL
sorted_results <- results_model %>%
arrange(desc(Sensitivity))
kable(sorted_results, caption = "Model Performance Metrics")
| Model | Accuracy | Kappa | Sensitivity | Specificity | F1 |
|---|---|---|---|---|---|
| rf | 0.7394958 | 0.3306115 | 0.3947368 | 0.9012346 | 0.4918033 |
| nb | 0.7394958 | 0.2977346 | 0.3157895 | 0.9382716 | 0.4363636 |
| glm | 0.7394958 | 0.2485231 | 0.2105263 | 0.9876543 | 0.3404255 |
| knn | 0.6890756 | 0.1334383 | 0.1842105 | 0.9259259 | 0.2745098 |
| tree | 0.6806723 | 0.0000000 | 0.0000000 | 1.0000000 | NA |
df_long <- pivot_longer(results_model,
cols = c(Accuracy, Kappa, Sensitivity, Specificity, F1),
names_to = "Wert",
values_to = "Zahl")
ggplot(df_long, aes(x = Wert, y = Zahl, color = Model, group = Model)) + geom_point(size = 2) + geom_line() +
scale_color_manual(
values = c("blue","red", "#04B4AE", "#D358F7", "lightblue"))+
theme_minimal() + labs(x = "Kennzahl", y = "Wert")
Die besten Ergebnisse hinsichtlich der Sensitivity, aber auch in Bezug auf Kappa, F1 und Accuracy, erzielten die Modelle Random Forest, Naive Bayes und GLM. Diese Modelle wurden daher für die weitere Analyse ausgewählt.
Im nächsten Schritt werden die wichigsten Variablen der ausgewählten Modelle Random Forest, Naive Bayes und GLM ermittelt.
create_importance_plot <- function(model, title, colors, importance_col) {
coefs <- varImp(model)
df_imp <- data.frame(coefs$importance)
df_imp$Vars <- row.names(df_imp)
colnames(df_imp)[1] <- importance_col
df_imp <- df_imp[order(df_imp[[importance_col]], decreasing = TRUE), ]
df_imp$Vars <- factor(df_imp$Vars, levels = df_imp$Vars)
ggplot(df_imp, aes(y = Vars, x = .data[[importance_col]], fill = Vars)) +
geom_vline(xintercept = 0, size = 0.8, colour = "#333333") +
geom_bar(stat = "identity", width = 0.03) +
geom_point(size = 3, aes(color = Vars)) +
scale_fill_manual(values = colors) +
scale_color_manual(values = colors) +
labs(y = "", x = "Importance") +
ggtitle(title) +
theme_minimal() +
theme(legend.position = "none")
}
colors <- c("gre" = "#FF5733", "gpa" = "#58d68d", "rank" = "#3357FF")
plot_rf <- create_importance_plot(model.rf, "Variable Importance in Random Forest", colors, "Importance")
plot_nb <- create_importance_plot(model.nb, "Variable Importance in Naive Bayes", colors, "Importance")
plot_glm <- create_importance_plot(model.glm, "Variable Importance in GLM", colors, "Importance")
(plot_rf + plot_nb) / (plot_glm + plot_spacer())
Die Abbildung zeigt, dass in den verschiedenen Modellen unterschiedliche Variablen als wichtig erachtet werden. Im Random Forest sind die Variablen gre und insbesondere gpa von großer Bedeutung, während die Variable rank keine Rolle spielt. Im Naive Bayes hingegen ist rank die wichtigste Variable, während gre nur eine geringe und gpa keine Bedeutung hat. Im GLM ist ebenfalls rank die wichtigste Variable, während gpa eine geringe und gre keine Bedeutung aufweist.
Im nächsten Schritt werden die Konfusionsmatrizen der ausgewählten Modelle als Balkendiagramme dargestellt. Diese dienen später als Grundlage für den Vergleich mit den Modellen, die auf den gesampelten Daten basieren.
plot_confusion_matrices <- function(models, testing_data, target_variable) {
plots <- list()
for (model_name in names(models)) {
model <- models[[model_name]]
predictions <- predict(model, newdata = testing_data)
conf_matrix <- confusionMatrix(predictions, testing_data[[target_variable]], positive = '1')
conf_df <- as.data.frame(as.table(conf_matrix$table))
colnames(conf_df) <- c("Prediction", "Reference", "Freq")
conf_prop <- conf_df %>%
group_by(Reference) %>%
mutate(Percentage = Freq / sum(Freq) * 100)
plot <- ggplot(conf_prop, aes(x = Reference, y = Freq, fill = Prediction)) +
geom_bar(stat = "identity", position = "stack", alpha = 0.7) +
scale_fill_manual(values = c("#04B486", "#5882FA")) +
geom_text(aes(label = ifelse(Percentage > 2, paste0(round(Percentage, 1), "%"), "")),
position = position_stack(vjust = 0.5),
color = "white", size = 3) +
labs(title = model_name,
x = "Reference",
y = "Häufigkeit",
fill = "Prediction") +
theme_minimal()
plots[[model_name]] <- plot
}
return(plots)
}
models <- list(
RF = model.rf,
NB = model.nb,
GLM = model.glm
)
confusion_plots <- plot_confusion_matrices(models, testing, "admit")
conf_rf <- confusion_plots$RF
conf_nb <- confusion_plots$NB
conf_glm <- confusion_plots$GLM
conf_rf
conf_nb
conf_glm
Die Sensitivity in Random Forest liegt bei 39,5%, in Naive Bayes bei 31,6% und in GLM bei 21,1%.
Im nächsten Schritt wird das Trainingsdatenset mit verschiedenen Sampling-Methoden bearbeitet, um die Performance der Modelle zu verbessern. Die folgenden Sampling-Methoden wurden ausgewählt:
Random Undersampling: Bei dieser Methode werden zufällig Fälle der Mehrheitsklasse entfernt, um das Ungleichgewicht im Datensatz zu reduzieren
Random Oversampling: Hierbei werden zufällig Fälle der Minderheitsklasse dupliziert, um das Ungleichgewicht zu verringern und mehr Beispiele der Minderheitsklasse bereitzustellen
SMOTE: SMOTE bewirkt eine Erhöhung der Beobachtungen der Minderheitsklassen durch die Generierung neuer synthetischer Daten, welche auf den ursprünglichen Daten des Datensatzes basieren
Tomek Links: Die Methode identifiziert und entfernt Beobachtungen aus den Mehrheitsklassen, die nahe zu jenen aus der Minderheitsklasse liegen
SMOTETomek: Eine Kombination aus SMOTE und Tomek Links, die sowohl synthetische Instanzen erzeugt als auch Rauschen durch Entfernen von Tomek Links reduziert
Nearmiss: NearMiss ist ein Undersampling-Verfahren, das Instanzen der Mehrheitsklasse entfernt, die zu den nächsten Nachbarn der Minderheitsklasse gehören, um die Trennung zwischen den Klassen zu optimieren
Adasyn: ADASYN ist eine Erweiterung von SMOTE, die sich auf die schwieriger zu klassifizierenden Instanzen der Minderheitsklasse konzentriert und mehr synthetische Beispiele für diese erzeugt
Zwischen den oben aufgelisteten Methoden gibt es eine Unterteilung in verschiedene Kategorien: Die Methoden des Oversampling (Random Oversampling, SMOTE, ADASYN), die Methoden des Undersampling (Random Undersampling, NearMiss, Tomek Links) sowie die Hybrid-Sampling-Methode (SMOTETomek).
table(training$admit)
##
## 0 1
## 192 89
Random Undersampling:
#set.seed(404)
under <- ovun.sample(admit~., data = training, method = "under", N = 178)$data
table(under$admit)
##
## 0 1
## 89 89
Random Oversampling:
set.seed(404)
over <- ovun.sample(admit~., data = training, method = "over", N = 384)$data
table(over$admit)
##
## 0 1
## 192 192
SMOTE:
set.seed(404)
recipe <- recipe(admit ~ ., data = training) %>%
step_smote(admit, over_ratio = 1)
prepared_recipe <- prep(recipe, training = training)
smote <- juice(prepared_recipe)
table(smote$admit)
##
## 0 1
## 192 192
Tomek Links:
set.seed(404)
recipe <- recipe(admit ~ ., data = training) %>%
step_tomek(admit)
prepared_recipe <- prep(recipe, training = training)
tomek<- juice(prepared_recipe)
table(tomek$admit)
##
## 0 1
## 158 55
SMOTETomek:
set.seed(404)
recipe <- recipe(admit ~ ., data = training) %>%
step_smote(admit, over_ratio = 1) %>% # Zuerst SMOTE
step_tomek(admit)
prepared_recipe <- prep(recipe, training = training)
stomek<- juice(prepared_recipe)
table(stomek$admit)
##
## 0 1
## 159 159
Nearmiss:
set.seed(404)
recipe <- recipe(admit ~ ., data = training) %>%
step_nearmiss(admit, under_ratio = 1, neighbors = 3)
prepared_recipe <- prep(recipe, training = training)
nearmiss <- juice(prepared_recipe)
table(nearmiss$admit)
##
## 0 1
## 89 89
Adasyn:
set.seed(404)
recipe <- recipe(admit ~ ., data = training) %>%
step_adasyn(admit, over_ratio = 1, neighbors = 3)
prepared_recipe <- prep(recipe, training = training)
adasyn <- juice(prepared_recipe)
table(adasyn$admit)
##
## 0 1
## 192 192
Nach dem Sampling der Trainingsdaten werden diese mit den ausgewählten Modellen trainiert und anschließend getestet.
set.seed(404)
model.over <- train(admit ~ .,
data = over,
method = 'rf')
## note: only 2 unique complexity parameters in default grid. Truncating the grid to 2 .
model.under <- train(admit ~ .,
data = under,
method = 'rf')
## note: only 2 unique complexity parameters in default grid. Truncating the grid to 2 .
model.smote <- train(admit ~ .,
data = smote,
method = 'rf')
## note: only 2 unique complexity parameters in default grid. Truncating the grid to 2 .
model.tomek <- train(admit ~ .,
data = tomek,
method = 'rf')
## note: only 2 unique complexity parameters in default grid. Truncating the grid to 2 .
model.stomek <- train(admit ~ .,
data = stomek,
method = 'rf')
## note: only 2 unique complexity parameters in default grid. Truncating the grid to 2 .
model.nearmiss <- train(admit ~ .,
data = stomek,
method = 'rf')
## note: only 2 unique complexity parameters in default grid. Truncating the grid to 2 .
model.adasyn <- train(admit ~ .,
data = adasyn,
method = 'rf')
## note: only 2 unique complexity parameters in default grid. Truncating the grid to 2 .
set.seed(404)
model_list <- list("over" = model.over, "under" = model.under, "smote" = model.smote, "tomek" = model.tomek, "stomek" = model.stomek, "nearmiss" = model.nearmiss, "adasyn" = model.adasyn, "origin_rf" = model.rf)
results_model <- data.frame()
for (model_name in names(model_list)) {
predictions <- predict(model_list[[model_name]], newdata = testing)
conf_matrix <- confusionMatrix(predictions, testing$admit, positive = '1')
kappa <- conf_matrix$overall["Kappa"]
accuracy <- conf_matrix$overall["Accuracy"]
sensitivity <- conf_matrix$byClass["Sensitivity"]
specificity <- conf_matrix$byClass["Specificity"]
f1 <- conf_matrix$byClass["F1"]
results_model <- rbind(results_model, data.frame(Model = model_name, Accuracy = accuracy, Kappa = kappa, Sensitivity = sensitivity,
Specificity = specificity, F1 = f1))
}
Die Ergebnisse werden nach Sensitivity sortiert und ausgegeben.
rownames(results_model) <- NULL
sorted_results <- results_model %>%
arrange(desc(Sensitivity))
kable(sorted_results, caption = "Model Performance Metrics")
| Model | Accuracy | Kappa | Sensitivity | Specificity | F1 |
|---|---|---|---|---|---|
| under | 0.6050420 | 0.1562830 | 0.5263158 | 0.6419753 | 0.4597701 |
| adasyn | 0.6806723 | 0.2443182 | 0.4473684 | 0.7901235 | 0.4722222 |
| smote | 0.6806723 | 0.2332994 | 0.4210526 | 0.8024691 | 0.4571429 |
| stomek | 0.6470588 | 0.1647727 | 0.3947368 | 0.7654321 | 0.4166667 |
| nearmiss | 0.6554622 | 0.1787578 | 0.3947368 | 0.7777778 | 0.4225352 |
| origin_rf | 0.7310924 | 0.3143680 | 0.3947368 | 0.8888889 | 0.4838710 |
| over | 0.6554622 | 0.1542728 | 0.3421053 | 0.8024691 | 0.3880597 |
| tomek | 0.6722689 | 0.1578661 | 0.2894737 | 0.8518519 | 0.3606557 |
df_long <- pivot_longer(results_model,
cols = c(Accuracy, Kappa, Sensitivity, Specificity, F1),
names_to = "Wert",
values_to = "Zahl")
df_long <- df_long %>%
mutate(Linientyp = ifelse(Model %in% "origin_rf", "solid", "dashed"))
ggplot(df_long, aes(x = Wert, y = Zahl, color = Model, group = Model, linetype = Linientyp)) + geom_point(size = 2) + geom_line() +
scale_color_manual(
values = c("blue","red", "black", "#04B4AE", "#D358F7", "lightblue", "lightgreen", "#d35400"))+
theme_minimal() + labs(x = "Kennzahl", y = "Wert") +
guides(linetype = "none") +
ggtitle("Sampling und Random Forest")
Die Abbildung zeigt die Ergebnisse der Random Forest-Modellierung unter Anwendung verschiedener Sampling-Methoden. Die schwarze gestrichelte Linie repräsentiert die Ergebnisse des Random Forest-Modells mit dem ursprünglichen, nicht gesampelten Datensatz.
Der höchste Sensitivity-Wert wird mit der Methode Random Undersampling erreicht und beträgt 52,6 %. Allerdings ist der Kappa-Wert im Vergleich zum Basismodell deutlich gesunken und liegt nur noch bei 15,6 %, während er im Basismodell 31,4 % beträgt.
Dies verdeutlicht, dass die Steigerung der Sensitivität in diesem Fall mit einem deutlichen Verlust an Modellqualität (gemessen am Kappa-Wert) einhergeht.
set.seed(404)
predictions <- predict(model.under, newdata = testing)
under_matrix <- confusionMatrix(predictions, testing$admit, positive = '1')
under_df <- as.data.frame(as.table(under_matrix$table))
colnames(under_df) <- c("Prediction", "Reference", "Freq")
conf_prop <- under_df %>%
group_by(Reference) %>%
mutate(Percentage = Freq / sum(Freq) * 100)
under_plot <- ggplot(conf_prop, aes(x = Reference, y = Freq, fill = Prediction)) +
geom_bar(stat = "identity", position = "stack", alpha=0.7) +
scale_fill_manual(values = c("#04B486", "#5882FA")) +
geom_text(aes(label = paste0(round(Percentage, 1), "%")),
position = position_stack(vjust = 0.5),
color = "white", size = 3) +
labs(title = "RF mit Undersampling",
x = "Reference",
y = "Häufigkeit",
fill = "Prediction") +
theme_minimal()
(conf_rf + under_plot) +
plot_layout(guides = "collect")
Der maximale Zugewinn der Sensitivity nach Sampling beträgt beim Random Forest 13,2 %. Der Verlust der Specificity beträgt 24,7%.
set.seed(404)
filtered_under <- results_model %>%
filter(Model %in% c("origin_rf", "under"))
rownames(results_model) <- NULL
kable(filtered_under, caption = "Model Performance Metrics")
| Model | Accuracy | Kappa | Sensitivity | Specificity | F1 |
|---|---|---|---|---|---|
| under | 0.6050420 | 0.156283 | 0.5263158 | 0.6419753 | 0.4597701 |
| origin_rf | 0.7310924 | 0.314368 | 0.3947368 | 0.8888889 | 0.4838710 |
set.seed(404)
model.over <- train(admit ~ .,
data = over,
method = 'nb')
model.under <- train(admit ~ .,
data = under,
method = 'nb')
model.smote <- train(admit ~ .,
data = smote,
method = 'nb')
model.tomek <- train(admit ~ .,
data = tomek,
method = 'nb')
model.stomek <- train(admit ~ .,
data = stomek,
method = 'nb')
model.nearmiss <- train(admit ~ .,
data = stomek,
method = 'nb')
model.adasyn <- train(admit ~ .,
data = adasyn,
method = 'nb')
set.seed(404)
model_list <- list("over" = model.over, "under" = model.under, "smote" = model.smote, "tomek" = model.tomek, "stomek" = model.stomek, "nearmiss" = model.nearmiss, "adasyn" = model.adasyn, "origin_nb" = model.nb)
results_model <- data.frame()
for (model_name in names(model_list)) {
predictions <- predict(model_list[[model_name]], newdata = testing)
conf_matrix <- confusionMatrix(predictions, testing$admit, positive = '1')
kappa <- conf_matrix$overall["Kappa"]
accuracy <- conf_matrix$overall["Accuracy"]
sensitivity <- conf_matrix$byClass["Sensitivity"]
specificity <- conf_matrix$byClass["Specificity"]
f1 <- conf_matrix$byClass["F1"]
results_model <- rbind(results_model, data.frame(Model = model_name, Accuracy = accuracy, Kappa = kappa, Sensitivity = sensitivity,
Specificity = specificity, F1 = f1))
}
rownames(results_model) <- NULL
sorted_results <- results_model %>%
arrange(desc(Sensitivity))
kable(sorted_results, caption = "Model Performance Metrics")
| Model | Accuracy | Kappa | Sensitivity | Specificity | F1 |
|---|---|---|---|---|---|
| smote | 0.6470588 | 0.2697253 | 0.6578947 | 0.6419753 | 0.5434783 |
| adasyn | 0.6386555 | 0.2570060 | 0.6578947 | 0.6296296 | 0.5376344 |
| stomek | 0.6470588 | 0.2604321 | 0.6315789 | 0.6543210 | 0.5333333 |
| nearmiss | 0.6470588 | 0.2604321 | 0.6315789 | 0.6543210 | 0.5333333 |
| over | 0.6806723 | 0.2949797 | 0.5789474 | 0.7283951 | 0.5365854 |
| under | 0.6974790 | 0.3230088 | 0.5789474 | 0.7530864 | 0.5500000 |
| origin_nb | 0.7394958 | 0.2977346 | 0.3157895 | 0.9382716 | 0.4363636 |
| tomek | 0.7478992 | 0.2790792 | 0.2368421 | 0.9876543 | 0.3750000 |
df_long <- pivot_longer(results_model,
cols = c(Accuracy, Kappa, Sensitivity, Specificity, F1),
names_to = "Wert",
values_to = "Zahl")
df_long <- df_long %>%
mutate(Linientyp = ifelse(Model %in% "origin_nb", "solid", "dashed"))
ggplot(df_long, aes(x = Wert, y = Zahl, color = Model, group = Model, linetype = Linientyp)) + geom_point(size = 2) + geom_line() +
scale_color_manual(
values = c("blue","red", "black", "#04B4AE", "#D358F7", "lightblue", "lightgreen", "#d35400"))+
guides(linetype = "none") +
theme_minimal() + labs(x = "Kennzahl", y = "Wert")
Die Abbildung zeigt die Ergebnisse der Naive-Bayes-Modellierung unter Anwendung verschiedener Sampling-Methoden. Die schwarze gestrichelte Linie repräsentiert die Ergebnisse des Naive-Bayes-Modells mit dem ursprünglichen, nicht gesampelten Datensatz.
Der höchste Sensitivity-Wert wird mit den Methoden SMOTE und ADASYN erreicht und beträgt 65,8 %. Als bestes Modell wird SMOTE ausgewählt, da der Kappa-Wert mit SMOTE-Sampling höher ist als mit ADASYN-Sampling. Der Kappa-Wert des besten Modells beträgt 26,97 %. Damit ist der Kappa-Wert im Vergleich zum Basismodell um 2,8 % gesunken.
set.seed(404)
predictions <- predict(model.smote, newdata = testing)
smote_matrix <- confusionMatrix(predictions, testing$admit, positive = '1')
smote_df <- as.data.frame(as.table(smote_matrix$table))
colnames(smote_df) <- c("Prediction", "Reference", "Freq")
conf_prop <- smote_df %>%
group_by(Reference) %>%
mutate(Percentage = Freq / sum(Freq) * 100)
smote_plot <- ggplot(conf_prop, aes(x = Reference, y = Freq, fill = Prediction)) +
geom_bar(stat = "identity", position = "stack", alpha=0.7) +
scale_fill_manual(values = c("#04B486", "#5882FA")) +
geom_text(aes(label = paste0(round(Percentage, 1), "%")),
position = position_stack(vjust = 0.5),
color = "white", size = 3) +
labs(title = "NB mit SMOTE",
x = "Reference",
y = "Häufigkeit",
fill = "Prediction") +
theme_minimal()
(conf_nb + smote_plot) +
plot_layout(guides = "collect")
Der maximale Zugewinn der Sensitivity nach Sampling mit SMOTE beträgt beim Naive-Bayes-Modell 34,2 % im Vergleich zum Basismodell. Der Verlust der Specificity beträgt hingegen 29,6 %.
set.seed(404)
filtered_smote <- results_model %>%
filter(Model %in% c("smote", "origin_nb"))
kable(filtered_smote, caption = "Model Performance Metrics")
| Model | Accuracy | Kappa | Sensitivity | Specificity | F1 |
|---|---|---|---|---|---|
| smote | 0.6470588 | 0.2697253 | 0.6578947 | 0.6419753 | 0.5434783 |
| origin_nb | 0.7394958 | 0.2977346 | 0.3157895 | 0.9382716 | 0.4363636 |
set.seed(404)
model.over <- train(admit ~ .,
data = over,
method = 'glm')
model.under <- train(admit ~ .,
data = under,
method = 'glm')
model.smote <- train(admit ~ .,
data = smote,
method = 'glm')
model.tomek <- train(admit ~ .,
data = tomek,
method = 'glm')
model.stomek <- train(admit ~ .,
data = stomek,
method = 'glm')
model.nearmiss <- train(admit ~ .,
data = stomek,
method = 'glm')
model.adasyn <- train(admit ~ .,
data = adasyn,
method = 'glm')
set.seed(404)
model_list <- list("over" = model.over, "under" = model.under, "smote" = model.smote, "tomek" = model.tomek, "stomek" = model.stomek, "nearmiss" = model.nearmiss, "adasyn" = model.adasyn, "origin_glm" = model.glm)
results_model <- data.frame()
for (model_name in names(model_list)) {
predictions <- predict(model_list[[model_name]], newdata = testing)
conf_matrix <- confusionMatrix(predictions, testing$admit, positive = '1')
kappa <- conf_matrix$overall["Kappa"]
accuracy <- conf_matrix$overall["Accuracy"]
sensitivity <- conf_matrix$byClass["Sensitivity"]
specificity <- conf_matrix$byClass["Specificity"]
f1 <- conf_matrix$byClass["F1"]
results_model <- rbind(results_model, data.frame(Model = model_name, Accuracy = accuracy, Kappa = kappa, Sensitivity = sensitivity,
Specificity = specificity, F1 = f1))
}
rownames(results_model) <- NULL
sorted_results <- results_model %>%
arrange(desc(Sensitivity))
kable(sorted_results, caption = "Model Performance Metrics")
| Model | Accuracy | Kappa | Sensitivity | Specificity | F1 |
|---|---|---|---|---|---|
| adasyn | 0.6302521 | 0.2252146 | 0.6052632 | 0.6419753 | 0.5111111 |
| over | 0.6050420 | 0.1776209 | 0.5789474 | 0.6172840 | 0.4835165 |
| smote | 0.6386555 | 0.2280887 | 0.5789474 | 0.6666667 | 0.5057471 |
| stomek | 0.6134454 | 0.1899970 | 0.5789474 | 0.6296296 | 0.4888889 |
| nearmiss | 0.6134454 | 0.1899970 | 0.5789474 | 0.6296296 | 0.4888889 |
| under | 0.6638655 | 0.2578734 | 0.5526316 | 0.7160494 | 0.5121951 |
| tomek | 0.7478992 | 0.2790792 | 0.2368421 | 0.9876543 | 0.3750000 |
| origin_glm | 0.7394958 | 0.2485231 | 0.2105263 | 0.9876543 | 0.3404255 |
df_long <- pivot_longer(results_model,
cols = c(Accuracy, Kappa, Sensitivity, Specificity, F1),
names_to = "Wert",
values_to = "Zahl")
df_long <- df_long %>%
mutate(Linientyp = ifelse(Model %in% "origin_glm", "solid", "dashed"))
ggplot(df_long, aes(x = Wert, y = Zahl, color = Model, group = Model, linetype = Linientyp)) + geom_point(size = 2) + geom_line() +
scale_color_manual(
values = c("blue","red", "black", "#04B4AE", "#D358F7", "lightblue", "lightgreen", "#d35400"))+
guides(linetype = "none") +
theme_minimal() + labs(x = "Kennzahl", y = "Wert")
Die Abbildung zeigt die Ergebnisse der GLM-Modellierung
unter Anwendung verschiedener Sampling-Methoden. Die schwarze
gestrichelte Linie repräsentiert die Ergebnisse des
GLM-Modells mit dem ursprünglichen, nicht gesampelten
Datensatz.
Der höchste Sensitivity-Wert wird mit den Methoden ADASYN erreicht und beträgt 60,5 %. Allerdings ist der Kappa-Wert im Vergleich zum Basismodell leicht gesunken und liegt bei 22,5,6 %, während er im Basismodell 24,8 % beträgt.
set.seed(404)
predictions <- predict(model.adasyn, newdata = testing)
adasyn_matrix <- confusionMatrix(predictions, testing$admit, positive = '1')
adasyn_df <- as.data.frame(as.table(adasyn_matrix$table))
colnames(adasyn_df) <- c("Prediction", "Reference", "Freq")
conf_prop <- adasyn_df %>%
group_by(Reference) %>%
mutate(Percentage = Freq / sum(Freq) * 100)
adasyn_plot <- ggplot(conf_prop, aes(x = Reference, y = Freq, fill = Prediction)) +
geom_bar(stat = "identity", position = "stack", alpha=0.7) +
scale_fill_manual(values = c("#04B486", "#5882FA")) +
geom_text(aes(label = paste0(round(Percentage, 1), "%")),
position = position_stack(vjust = 0.5),
color = "white", size = 3) +
labs(title = "GLM mit ADASYN",
x = "Reference",
y = "Häufigkeit",
fill = "Prediction") +
theme_minimal()
(conf_glm + adasyn_plot) +
plot_layout(guides = "collect")
Der maximale Zugewinn der Sensitivity nach Sampling mit ADASYN beträgt beim GLM-Modell 39,4 % im Vergleich zum Basismodell. Der Verlust der Specificity beträgt hingegen 34,5 %.
set.seed(404)
filtered_adasyn <- results_model %>%
filter(Model %in% c("adasyn", "origin_glm"))
kable(filtered_adasyn, caption = "Model Performance Metrics")
| Model | Accuracy | Kappa | Sensitivity | Specificity | F1 |
|---|---|---|---|---|---|
| adasyn | 0.6302521 | 0.2252146 | 0.6052632 | 0.6419753 | 0.5111111 |
| origin_glm | 0.7394958 | 0.2485231 | 0.2105263 | 0.9876543 | 0.3404255 |
Zuletzt werden die Ergebnisse der Modellirung und der Sampling miteinander verglichen.
final_df <- rbind(filtered_under[1, ], filtered_smote[1, ], filtered_adasyn[1, ])
final_df <- final_df %>%
mutate(Model = case_when(
Model == "under" ~ "under_rf",
Model == "smote" ~ "smote_nb",
Model == "adasyn" ~ "adasyn_glm",
TRUE ~ Model # Fallback, falls es andere Werte gibt
))
sorted_final_df <- final_df %>%
arrange(desc(Sensitivity))
rownames(sorted_final_df) <- NULL
kable(sorted_final_df, caption = "Models Performance Metrics")
| Model | Accuracy | Kappa | Sensitivity | Specificity | F1 |
|---|---|---|---|---|---|
| smote_nb | 0.6470588 | 0.2697253 | 0.6578947 | 0.6419753 | 0.5434783 |
| adasyn_glm | 0.6302521 | 0.2252146 | 0.6052632 | 0.6419753 | 0.5111111 |
| under_rf | 0.6050420 | 0.1562830 | 0.5263158 | 0.6419753 | 0.4597701 |
df_long <- pivot_longer(final_df,
cols = c(Accuracy, Kappa, Sensitivity, Specificity, F1),
names_to = "Wert",
values_to = "Zahl")
ggplot(df_long, aes(x = Wert, y = Zahl, color = Model, group = Model)) + geom_point(size = 2) + geom_line() +
scale_color_manual(
values = c("#04B4AE", "#D358F7", "lightgreen"))+
theme_minimal() + labs(x = "Kennzahl", y = "Wert")
Die Abbildung zeigt die Ergebnisse der Modellierung nach Anwendung
verschiedener Sampling-Methoden. Die besten Ergebnisse wurden für den
Random Forest mit Random
Undersampling, für Naive Bayes mit
SMOTE und für GLM mit
ADASYN erzielt.
Den höchsten Sensitivity-Wert zeigt das
Naive-Bayes-Modell mit SMOTE. Dieses
Modell weist zudem die höchsten Werte für Accuracy,
Kappa und F1 auf. Die
Specificity-Werte sind bei allen drei Modellen
identisch.
df_final <- rbind(sorted_final_df[1,], filtered_under[filtered_under == "origin_rf",], filtered_smote[filtered_smote == "origin_nb",], filtered_adasyn[filtered_adasyn == "origin_glm",])
sorted_df_final <- df_final %>%
arrange(desc(Sensitivity))
rownames(sorted_df_final) <- NULL
kable(sorted_df_final, caption = "Models Performance Metrics")
| Model | Accuracy | Kappa | Sensitivity | Specificity | F1 |
|---|---|---|---|---|---|
| smote_nb | 0.6470588 | 0.2697253 | 0.6578947 | 0.6419753 | 0.5434783 |
| origin_rf | 0.7310924 | 0.3143680 | 0.3947368 | 0.8888889 | 0.4838710 |
| origin_nb | 0.7394958 | 0.2977346 | 0.3157895 | 0.9382716 | 0.4363636 |
| origin_glm | 0.7394958 | 0.2485231 | 0.2105263 | 0.9876543 | 0.3404255 |
df_long <- pivot_longer(df_final,
cols = c(Accuracy, Kappa, Sensitivity, Specificity, F1),
names_to = "Wert",
values_to = "Zahl")
df_long <- df_long %>%
mutate(Linientyp = ifelse(Model %in% "smote_nb", "dashed", "solid"))
ggplot(df_long, aes(x = Wert, y = Zahl, color = Model, group = Model, linetype = Linientyp)) + geom_point(size = 2) + geom_line() +
scale_color_manual(
values = c("#04B4AE", "#D358F7", "blue", "red"))+
guides(linetype = "none") +
theme_minimal() + labs(x = "Kennzahl", y = "Wert")
Abschließend wird das beste Modell, Naive Bayes mit SMOTE-Sampling, mit allen drei Basismodellen ohne Sampling verglichen. Die schwarze gestrichelte Linien repräsentieren die Basismodelle ohne Sampling. Wie erwartet wird durch das Sampling der Sensitivity-Wert deutlich verbessert. Auch der F1-Wert des besten Modells mit Sampling ist höher als der F1-Wert der Basismodelle ohne Sampling. Schlechtere Ergebnisse erzielt das beste Modell mit Sampling hingegen bei den Werten Accuracy und insbesondere bei Specificity. Der Kappa-Wert des besten Modells mit Sampling ist um 4,45 % niedriger als beim Basismodell Random Forest, um 3,1 % niedriger als beim Basismodell Naive Bayes, aber um 2,1 % höher als beim Basismodell GLM.