Klassifikation von KHK-Risiken anhand medizinischer Daten¶
Dieses Notebook beschäftigt sich mit der Anwendung verschiedener Verfahren des maschinellen Lernens auf einen klinischen Datensatz zur Klassifikation des Risikos für koronare Herzkrankheiten (KHK).
Name: Niklas Werthmann
Matrik. Nr.: xxxxxxxxxxxxxxxx
Kurs: xxxxxxxxxxxx
1. Import notwendiger Bibliotheken und Laden der Daten¶
# Bibliotheken importieren
import pandas as pd
import numpy as np
%matplotlib inline
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.model_selection import train_test_split, StratifiedKFold
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
from sklearn.neighbors import KNeighborsClassifier
from sklearn.svm import SVC
from sklearn.linear_model import LogisticRegression
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.neural_network import MLPClassifier
from sklearn.metrics import accuracy_score
from sklearn.model_selection import GridSearchCV
from sklearn.inspection import permutation_importance
from sklearn.preprocessing import PolynomialFeatures
from sklearn.manifold import TSNE
# Laden des Datensatzes
df = pd.read_csv("KHK_Klassifikation.csv")
2. Übersicht & Korrektur des Datensatzes¶
2.1 Datentypen, Spaltennamen & Inhalt¶
df.info()
df.head()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 918 entries, 0 to 917 Data columns (total 10 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 Alter 918 non-null int64 1 Geschlecht 918 non-null object 2 Blutdruck 918 non-null int64 3 Chol 918 non-null int64 4 Blutzucker 918 non-null int64 5 EKG 918 non-null object 6 HFmax 918 non-null int64 7 AP 918 non-null object 8 RZ 918 non-null float64 9 KHK 918 non-null int64 dtypes: float64(1), int64(6), object(3) memory usage: 71.8+ KB
Alter | Geschlecht | Blutdruck | Chol | Blutzucker | EKG | HFmax | AP | RZ | KHK | |
---|---|---|---|---|---|---|---|---|---|---|
0 | 40 | M | 140 | 289 | 0 | Normal | 172 | N | 0.0 | 0 |
1 | 49 | F | 160 | 180 | 0 | Normal | 156 | N | 1.0 | 1 |
2 | 37 | M | 130 | 283 | 0 | ST | 98 | N | 0.0 | 0 |
3 | 48 | F | 138 | 214 | 0 | Normal | 108 | Y | 1.5 | 1 |
4 | 54 | M | 150 | 195 | 0 | Normal | 122 | N | 0.0 | 0 |
2.2 Dimensionen & Anzahl der Datenpunkte¶
df.shape
(918, 10)
Es liegen 918 Datenpunkte vor und 10 Dimensionen
2.3 Korrektur¶
In Abschnitt 2.1 lässt sich erkennen, dass einige Datentypen nicht korrekt sind. Geschlecht, EKG, AP und KHK müssen in die im Kontext korrekten Datentypen umgewandelt werden.
# Konvertiere die Variable 'Geschlecht' in eine kategorische Variable
df['Geschlecht'] = df['Geschlecht'].astype('category')
# Konvertiere die Variable 'EKG' in eine kategorische Variable
df['EKG'] = df['EKG'].astype('category')
# Konvertiere die Variable 'AP' in eine kategorische Variable (liegt bereits als kat. Variable vor, jedoch ist der Datentyp 'object')
df['AP'] = df['AP'].astype('category')
# Konvertiere die Variable 'KHK' in eine boolsche Variable
df['KHK'] = df['KHK'].astype('bool')
2.3.1 Übersicht über den korrigierten Datensatz¶
df.info()
df.head()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 918 entries, 0 to 917 Data columns (total 10 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 Alter 918 non-null int64 1 Geschlecht 918 non-null category 2 Blutdruck 918 non-null int64 3 Chol 918 non-null int64 4 Blutzucker 918 non-null int64 5 EKG 918 non-null category 6 HFmax 918 non-null int64 7 AP 918 non-null category 8 RZ 918 non-null float64 9 KHK 918 non-null bool dtypes: bool(1), category(3), float64(1), int64(5) memory usage: 47.1 KB
Alter | Geschlecht | Blutdruck | Chol | Blutzucker | EKG | HFmax | AP | RZ | KHK | |
---|---|---|---|---|---|---|---|---|---|---|
0 | 40 | M | 140 | 289 | 0 | Normal | 172 | N | 0.0 | False |
1 | 49 | F | 160 | 180 | 0 | Normal | 156 | N | 1.0 | True |
2 | 37 | M | 130 | 283 | 0 | ST | 98 | N | 0.0 | False |
3 | 48 | F | 138 | 214 | 0 | Normal | 108 | Y | 1.5 | True |
4 | 54 | M | 150 | 195 | 0 | Normal | 122 | N | 0.0 | False |
Jetzt liegt der Datensatz in der korrekten Form vor und wir können mit der Analyse beginnen, welche Variablen KHK beeinflussen.
3. Explorative Datenanalyse¶
Wir analysieren die Struktur und Verteilung der Daten, untersuchen die Zielvariable und suchen nach Korrelationen zwischen den Features.
3.1 Statistische Übersicht¶
df.describe()
Alter | Blutdruck | Chol | Blutzucker | HFmax | RZ | |
---|---|---|---|---|---|---|
count | 918.000000 | 918.000000 | 918.000000 | 918.000000 | 918.000000 | 918.000000 |
mean | 53.510893 | 132.396514 | 198.799564 | 0.233115 | 136.809368 | 0.887364 |
std | 9.432617 | 18.514154 | 109.384145 | 0.423046 | 25.460334 | 1.066570 |
min | 28.000000 | 0.000000 | 0.000000 | 0.000000 | 60.000000 | -2.600000 |
25% | 47.000000 | 120.000000 | 173.250000 | 0.000000 | 120.000000 | 0.000000 |
50% | 54.000000 | 130.000000 | 223.000000 | 0.000000 | 138.000000 | 0.600000 |
75% | 60.000000 | 140.000000 | 267.000000 | 0.000000 | 156.000000 | 1.500000 |
max | 77.000000 | 200.000000 | 603.000000 | 1.000000 | 202.000000 | 6.200000 |
Hier lässt sich u.a erkennen, dass das Durchschnittsalter der Patienten ca. 53 1/2 Jahre beträgt, wobei der jüngste Patient 28 und der älteste 77 Jahre alt ist.
3.2 Verteilung der Zielvariable KHK¶
ax = sns.countplot(data=df, x='KHK')
plt.ylabel("Anzahl")
plt.title("Verteilung der Zielvariable KHK")
# Zahlen und Prozentsätze über den Balken anzeigen
for p in ax.patches:
height = p.get_height()
ax.annotate(f'{int(height)}\n({height/df["KHK"].count()*100:.1f}%)',
(p.get_x() + p.get_width() / 2., height),
ha='center', va='baseline')
plt.show()
Von den 918 Patienten des Datensatzes leiden 508 (55.3%) an der Koronaren Herzkrankheit.
3.3 Korrelation der Features¶
# Umwandlung kategorialer Variablen in numerische Werte
df_encoded = pd.get_dummies(df, drop_first=True)
# Korrelation der Zielvariable KHK mit den anderen Features
khk_corr = df_encoded.corr()["KHK"].drop("KHK")
# Visualisierung der Korrelationen
fig, ax = plt.subplots(figsize=(8, 6))
sns.barplot(x=khk_corr.values, y=khk_corr.index, hue=khk_corr.index, palette="coolwarm", dodge=False, ax=ax)
ax.set_title("Korrelation der Features mit KHK")
ax.set_xlabel("Korrelationskoeffizient")
# Zahlen über den Balken anzeigen
for i in ax.containers:
ax.bar_label(i, fmt='%.2f')
# Plot speichern - wird in Abschnitt 6.1 benötigt
khk_corr_plot = fig
Der Plot zeigt die Korrelation zwischen verschiedenen Features (Variablen) und der Zielgröße KHK.
Wichtige Anmerkung: Korrelation bedeutet nicht Kausalität! - dies wird indirekt in Abschnitt 6 überprüft
4. Datenvorbereitung¶
Wir trennen die Daten in Features (X) und Zielvariable (y) und führen eine Skalierung durch.
# Features und Zielvariable definieren
X = df_encoded.drop(columns=["KHK"])
y = df_encoded["KHK"]
# Aufteilen in Trainings- und Testdaten
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=1, stratify=y)
# Datensatz standardisieren
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
Damit sieht der Trainingsdatensatz so aus:
pd.concat([X_train, y_train], axis=1).head()
Alter | Blutdruck | Chol | Blutzucker | HFmax | RZ | Geschlecht_M | EKG_Normal | EKG_ST | AP_Y | KHK | |
---|---|---|---|---|---|---|---|---|---|---|---|
209 | 54 | 125 | 216 | 0 | 140 | 0.0 | True | True | False | False | True |
656 | 62 | 124 | 209 | 0 | 163 | 0.0 | False | True | False | False | False |
451 | 64 | 144 | 0 | 0 | 122 | 1.0 | True | False | True | True | True |
294 | 32 | 95 | 0 | 1 | 127 | 0.7 | True | True | False | False | True |
545 | 48 | 132 | 272 | 0 | 139 | 0.2 | True | False | True | False | False |
... und der Testdatensatz wie folgt:
pd.concat([X_test, y_test], axis=1).head()
Alter | Blutdruck | Chol | Blutzucker | HFmax | RZ | Geschlecht_M | EKG_Normal | EKG_ST | AP_Y | KHK | |
---|---|---|---|---|---|---|---|---|---|---|---|
417 | 44 | 130 | 209 | 0 | 127 | 0.0 | True | False | True | False | False |
395 | 38 | 135 | 0 | 1 | 150 | 0.0 | True | True | False | False | True |
901 | 58 | 170 | 225 | 1 | 146 | 2.8 | False | False | False | True | True |
624 | 63 | 150 | 407 | 0 | 154 | 4.0 | False | False | False | False | True |
414 | 54 | 130 | 0 | 1 | 110 | 3.0 | True | True | False | True | True |
4.1 PCA: Dimensionsreduktion und Visualisierung¶
Wir führen eine PCA durch, um die transformierten Trainingsdaten im zweidimensionalen Raum zu betrachten.
# PCA durchführen
pca = PCA(n_components=2)
X_pca = pca.fit_transform(X_train_scaled)
# Visualisierung
plt.figure(figsize=(8, 6))
sns.scatterplot(x=X_pca[:, 0], y=X_pca[:, 1], hue=y_train)
plt.title("PCA-Visualisierung der Trainingsdaten")
plt.show()
In der dargestellten PCA-Visualisierung sind die Datenpunkte in zwei Gruppen (blau und orange) unterteilt. Diese Farben repräsentieren die zwei Klassen: "KHK nicht vorhanden" (blau) und "KHK vorhanden" (orange). Die orangefarbene Gruppe (KHK vorhanden) ist hauptsächlich auf der rechten Seite des Diagramms konzentriert, während die blaue Gruppe (KHK nicht vorhanden) auf der linken Seite liegt. Es lässt sich eine gewisse Trennung der beiden Klassen erkennen, jedoch gibt es auch Bereiche, in denen sich die Klassen überlappen.
Konsequenz
Die Visualisierung deutet auf eine potenziell gute Separierbarkeit hin, da die beiden Gruppen hauptsächlich in unterschiedlichen Bereichen konzentriert sind. Allerdings ist die Überlappung ein Hinweis darauf, dass die Klassen nicht vollständig trennbar sind, was auch die Wahl des Modells beeinflussen könnte.
Für eine fundierte Aussage prüfen wir die Separierbarkeit zusätzlich:
Wir beginnen mit der Visualisierungsmethode t-SNE, die nichtlineare Beziehungen abbildet:
# t-SNE mit 2 Komponenten
tsne = TSNE(n_components=2, random_state=1, perplexity=30, max_iter=1000)
X_tsne = tsne.fit_transform(X_train_scaled)
# Visualisierung
plt.figure(figsize=(8, 6))
sns.scatterplot(x=X_tsne[:, 0], y=X_tsne[:, 1], hue=y_train, alpha=0.8)
plt.title("t-SNE-Visualisierung der Trainingsdaten")
plt.legend(title="KHK", loc="best")
plt.show()
Diese Visualisierung zeigt, dass sich die Situation im Vergleich zur PCA-Analyse leicht verbessert hat. Es lassen einige Gruppierungen erkennen und die Datenpunkte der Klassen KHK vorhanden und KHK nicht vorhanden sind nur teilweise miteinander vermischt. Eine klare Trennung der beiden Gruppen ist nicht erkennbar.
Da die visuelle Analyse keine nützlichen Ergebnisse liefert, wenden wir uns nun verschiedenen Klassifikationsmodellen zu. Diese Modelle sollen uns dabei helfen, die Gruppen KHK vorhanden und KHK nicht vorhanden algorithmisch abzugrenzen und die zugrunde liegenden Muster in den Daten zu identifizieren.
5. Klassifikationsverfahren¶
Wir wenden sechs verschiedene Klassifikationsverfahren an, darunter kNN, SVM & Random Forest. Abschließend folgt eine Bewertung der Genauigkeit dieser Modelle.
k-Nearest Neighbors (kNN)¶
# kNN-Modell
knn = KNeighborsClassifier(n_neighbors=4)
knn.fit(X_train_scaled, y_train)
y_pred_knn = knn.predict(X_test_scaled)
Support Vector Machines (SVM)¶
# SVM-Modell
svm = SVC(kernel="rbf")
svm.fit(X_train_scaled, y_train)
y_pred_svm = svm.predict(X_test_scaled)
Logistische Regression¶
# Logistisches Regressionsmodell
log_reg = LogisticRegression()
log_reg.fit(X_train_scaled, y_train)
y_pred_log = log_reg.predict(X_test_scaled)
Entscheidungsbaum¶
# Entscheidungsbaum-Modell
tree = DecisionTreeClassifier(max_depth=4, ccp_alpha=0.01, random_state=1)
tree.fit(X_train_scaled, y_train)
y_pred_tree = tree.predict(X_test_scaled)
Neuronales Netz¶
# Neuronales Netz-Modell
mlp = MLPClassifier(hidden_layer_sizes=(50, 50), max_iter=2000)
mlp.fit(X_train_scaled, y_train)
y_pred_mlp = mlp.predict(X_test_scaled)
Random Forest¶
# Random Forest-Modell
forest = RandomForestClassifier(n_estimators=100, random_state=1)
forest.fit(X_train_scaled, y_train)
y_pred_forest = forest.predict(X_test_scaled)
5.1 Vergleich der Modelle¶
Wir vergleichen die Genauigkeit der verschiedenen Modelle
# Vergleich der Modelle
model_results = {
"kNN": accuracy_score(y_test, y_pred_knn),
"SVM": accuracy_score(y_test, y_pred_svm),
"Logistische Regression": accuracy_score(y_test, y_pred_log),
"Entscheidungsbaum": accuracy_score(y_test, y_pred_tree),
"Neuronales Netz": accuracy_score(y_test, y_pred_mlp),
"Random Forest": accuracy_score(y_test, y_pred_forest),
}
# Darstellung der Ergebnisse
results_df = pd.DataFrame(list(model_results.items()), columns=["Modell", "Genauigkeit"])
results_df = results_df.sort_values(by="Genauigkeit", ascending=False)
# Visualisierung der Genauigkeit
plt.figure(figsize=(8, 6))
ax = sns.barplot(x="Genauigkeit", y="Modell", data=results_df)
plt.title("Modellvergleich: Genauigkeit")
# Exakte Werte anzeigen
for i in ax.containers:
ax.bar_label(i, fmt='%.4f')
plt.show()
Die eingesetzten Klassifikationsverfahren erzielten eine Präzision von knapp unter 80 % und zeigen, dass die Modelle in der Lage sind, die Klassen KHK vorhanden und KHK nicht vorhanden in einem Großteil der Fälle korrekt zu unterscheiden. Da fast alle Modelle ähnliche Ergebnisse liefern und die Testdaten mit einem random_state erstellt wurden, lässt sich hier kein "bestes" Modell finden. Im folgenden Abschnitt versuchen wir die bisherige Genauigkeit eines ausgewählten Modells erhöhen.
6. Optimierung der Modelle¶
Wir verwenden das aus der Vorlesung bekannte Modell Support Vector Machine (SVM). Zu Beginn verwenden wir eine sog. GridSearch zur Optimierung der Parameter.
Wir übergeben der GridSearch zu Beginn eine breite Auswahl von Parametern, um zunächst eine grobe Eingrenzung der optimalen Werte zu erzielen. Diese Eingrenzung dient als Grundlage, um in einem zweiten Schritt eine detailliertere Feinjustierung vorzunehmen.
svm_class = SVC(kernel='rbf', random_state=1)
param_grid = {'C': [1, 5, 10, 50, 100, 200, 300],
'gamma': [0.0001, 0.0005, 0.001, 0.005, 0.01, 0.1, 1, 1.5]}
grid = GridSearchCV(svm_class, param_grid, cv= StratifiedKFold(n_splits=10))
grid.fit(X_train_scaled, y_train)
grid.best_params_
{'C': 1, 'gamma': 0.01}
Um diese Parameter zu testen, definieren wir eine Funktion mit zwei Eingabevariablen, die das SVM Modell trainiert und die Genauigkeit zurückgibt:
def SVM(C, gamma):
# SVM-Modell mit den angegebenen Parametern
svm = SVC(C=C, gamma=gamma, kernel="rbf", random_state=1)
svm.fit(X_train_scaled, y_train)
y_pred_svm = svm.predict(X_test_scaled)
# Genauigkeit berechnen & ausgeben
print(accuracy_score(y_test, y_pred_svm))
Nun testen wir die SVM mit den Parametern:
SVM(1, 0.01)
0.8043478260869565
Die Genauigkeit der SVM wurde mit den Parametern 𝐶 = 1 und 𝛾 = 0.01 auf 0.8043478260869565 erhöht. Dies entspricht einer Verbesserung um ca. 1,63%!
Nun wollen wir diese Parameter weiter verfeinern, um unsere SVM weiter zu optimieren:
param_grid = {'C': [0.6, 0.7, 0.85, 1, 1.5, 2, 2.5],
'gamma': [0.004, 0.006, 0.008, 0.01, 0.012, 0.014, 0.016, 0.018]}
grid = GridSearchCV(svm_class, param_grid, cv= StratifiedKFold(n_splits=10))
grid.fit(X_train_scaled, y_train)
grid.best_params_
{'C': 0.85, 'gamma': 0.014}
Wir testen die verfeinerten Parameter 𝐶 = 0.85 und 𝛾 = 0.014:
SVM(0.85, 0.014)
0.8043478260869565
Dies hat zu keiner Verbesserung geführt ... wahrscheinlich, da die zuvor getesteten Parameter bereits nahe am Optimum lagen und die Modellleistung in diesem Bereich stabil ist.
Daher visualisieren wir nun die Ausgabe der letzen GridSearch als Heatmap, um die Ergebnisse der GridSearch visuell zu analysieren und mögliche Muster oder Trends in den Parametereinstellungen zu identifizieren.
# Visualize the results of the grid search
results = pd.DataFrame(grid.cv_results_)
scores = results.pivot(index="param_C", columns="param_gamma", values="mean_test_score")
plt.figure(figsize=(8, 6))
sns.heatmap(scores, annot=True, fmt=".3f", cmap="viridis")
plt.title("Grid Search Scores")
plt.xlabel("Gamma")
plt.ylabel("C")
plt.show()
Hier lässt sich erkennen, dass es insgesamt drei Kombinationen von 𝐶 und 𝛾 gibt, die den höchsten Score (0.830) haben. Wir haben bisher aber nur die Kombination 𝐶 = 0.85 und 𝛾 = 0.014 getestet, welche zu keiner Verbesserung führte.
Im Folgenden testen wir nun die anderen beiden Kombinationen 𝐶 = 1, 𝛾 = 0.012 und 𝐶 = 0.7, 𝛾 = 0.016:
SVM(1, 0.012)
SVM(0.7, 0.016)
0.8043478260869565
0.8152173913043478
Die Kombination 𝐶 = 1 & 𝛾 = 0.012 führt ebenfalls zu keiner Verbesserung, die Kombination 𝐶 = 0.7 & 𝛾 = 0.016 hingegen schon! Mit den Parametern 𝐶 = 0.7 & 𝛾 = 0.016 wird die Genauigkeit der SVM auf 0.8152173913043478 angehoben. Dies entspricht einer Verbesserung von ca. 1,09%!
Ingsgesamt haben wir die Genauigkeit der SVM durch die zwei Iterationen der GridSearch um ca. 2,72% (0.0272173913) verbessert!
Da die GridSearch hier an ihre Grenzen zu stoßen scheint, wird die Optimierung der SVM im Folgenden durch gezieltes Feature Engineering forgesetzt:
6.1 Automatic Feature Engineering¶
Wir betrachten zwei unterschiedliche Möglichkeiten von automatischen Feature Engineering Methoden und testen diese auf unserem optimierten SVM Modell. Dafür implementieren wir zunächst eine Testfunktion des optimierten SVM Modells:
def SVM_Optimized(X_train_scaled, X_test_scaled):
# SVM-Modell mit den angegebenen Parametern
svm = SVC(C=0.7, gamma=0.016, kernel="rbf", random_state=1)
svm.fit(X_train_scaled, y_train)
y_pred_svm = svm.predict(X_test_scaled)
# Genauigkeit berechnen & ausgeben
print(accuracy_score(y_test, y_pred_svm))
Nun implementieren wir einen Test mit Interaktionstermini und testen die Genauigkeit mit der Testfunktion:
# Interaktionstermini erzeugen
poly = PolynomialFeatures(degree=2, interaction_only=True, include_bias=False)
X_train_poly = poly.fit_transform(X_train_scaled)
X_test_poly = poly.transform(X_test_scaled)
SVM_Optimized(X_train_poly, X_test_poly)
0.7717391304347826
Die Genauigkeit des Modells wurde verschlechtert ... Als nächsten testen wir eine PCA:
# PCA anwenden
pca = PCA(n_components=10) # 10 Hauptkomponenten
X_train_pca = pca.fit_transform(X_train_scaled)
X_test_pca = pca.transform(X_test_scaled)
SVM_Optimized(X_train_pca, X_test_pca)
0.8152173913043478
Die Genauigkeit des Modells bleibt identisch. Da die automatischen Optimierungsalgorithmen uns nicht zu helfen scheinen, wenden wir uns einer manuellen Optimierung durch die Analyse von Korrelationen und den sog. Feature Importance Werten zu.
6.2 Manual Feature Engineering¶
Zuerst betrachten wir die Korrelation einzelner Werte mit der Zielvariablen (Dieser Plot wurde bereits in Abschnitt 3.3 der explorativen Datenanalyse kurz betrachtet, hier jetzt im Kontext des Feature Engineering)
khk_corr_plot # Wir verwenden den gespeicherten Plot aus Abschnitt 3.3
Und die Korrelation der Werte untereinander:
# Convert the scaled training set back to a DataFrame for easier correlation calculation
X_train_scaled_df = pd.DataFrame(X_train_scaled, columns=X.columns)
# Calculate the correlation matrix
correlation_matrix_scaled = X_train_scaled_df.corr()
# Summe der absoluten Korrelationswerte jeder Variablen berechnen
correlation_sums = correlation_matrix_scaled.abs().sum().sort_values()
print(correlation_sums)
# Display the correlation matrix
plt.figure(figsize=(10, 8))
sns.heatmap(correlation_matrix_scaled, annot=True, fmt=".2f")
plt.title("Correlation Matrix of Scaled Training Set")
plt.show()
Geschlecht_M 2.009811 Blutdruck 2.125782 Blutzucker 2.164855 Chol 2.187446 RZ 2.370236 EKG_Normal 2.411709 EKG_ST 2.599268 AP_Y 2.617020 HFmax 2.883610 Alter 2.910357 dtype: float64
Bevor wird aus diesem Plot konkrete Maßnahmen ableiten, betrachten wir zunächst weitere Methoden die Feature Importance Werte bestimmen, um einen umfassenden Überblick zu erhalten.
Wir beginnen mit einer Permutation:
# Berechnung der Permutationsbedeutung
result = permutation_importance(svm, X_test_scaled, y_test, n_repeats=10, random_state=1)
# Visualisierung der Permutationsbedeutung
sorted_idx = result.importances_mean.argsort()
plt.figure(figsize=(10, 6))
plt.barh(range(len(sorted_idx)), result.importances_mean[sorted_idx], xerr=result.importances_std[sorted_idx])
plt.yticks(range(len(sorted_idx)), X.columns[sorted_idx])
plt.xlabel("Permutationsbedeutung")
plt.title("Feature Importance basierend auf Permutationsbedeutung")
plt.show()
Wir machen weiter mit dem RandomForestClassifier:
# Random Forest Modell trainieren
rf = RandomForestClassifier(n_estimators=1000, random_state=1)
rf.fit(X_train_scaled, y_train)
# Berechnung der Feature Importance
importances = rf.feature_importances_
std = np.std([tree.feature_importances_ for tree in rf.estimators_], axis=0)
indices = np.argsort(importances)
# Visualisierung der Feature Importance
plt.figure(figsize=(10, 6))
plt.barh(range(len(indices)), importances[indices], xerr=std[indices])
plt.yticks(range(len(indices)), X.columns[indices])
plt.xlabel("Feature Importance")
plt.title("Feature Importance basierend auf Random Forest")
plt.show()
Und schließen mit dem GradientBoostingClassifier ab:
# Gradient Boosting Modell trainieren
gb = GradientBoostingClassifier(n_estimators=100, random_state=1)
gb.fit(X_train_scaled, y_train)
# Berechnung der Feature Importance
importances_gb = gb.feature_importances_
indices_gb = np.argsort(importances_gb)
# Visualisierung der Feature Importance
plt.figure(figsize=(10, 6))
plt.barh(range(len(indices_gb)), importances_gb[indices_gb])
plt.yticks(range(len(indices_gb)), X.columns[indices_gb])
plt.xlabel("Feature Importance")
plt.title("Feature Importance basierend auf Gradient Boosting")
plt.show()
Jetzt liegen uns ingesamt fünf verschiedene Datensätze vor, die uns jeweils die Features und ihre Beziehungen näherbringen.
Wir betrachten Schrittweise jede Variable und gewichten sie anhand der gesammelten Metriken:
Alter:
- Korrelation (Features): hohe Korrelation mit HFmax
- Korrelation (KHK): 0.282039
- Summierte Korrelation: 2.91 - höchster Wert!
- Importance Metriken: niedrig
- Gewichtung: 1.5
- -> da hohe Korrelation und höchster summierte Korrelation, trotz niedriger Importance, wird dieses Feature höher gewichtet
Blutdruck:
- Korrelation (Features): minimal
- Korrelation (KHK): 0.107589
- Summierte Korrelation: 2.13 (sehr niedrig)
- Importance Metriken: minimal
- Gewichtung: 0.0
- -> kaum Korrelationen und keine Importance, geringe Korrelation zu KHK
Chol:
- Korrelation (Features): hohe Korrelation mit Blutzucker
- Korrelation (KHK): -0.232741
- Summierte Korrelation: 2.19 (sehr niedrig)
- Importance Metriken: sehr hoch
- Gewichtung: 1.0
- -> Hohe Importance trotz geringer Korrelationen (Ausnahme Blutzucker), durchschnittliche Korrelation zu KHK
Blutzucker:
- Korrelation (Features): hohe Korrelation mit Chol
- Korrelation (KHK): 0.267291
- Summierte Korrelation: 2.16 (sehr niedrig)
- Importance Metriken: niedrig
- Gewichtung: 0.5
- -> Hohe Korrelation mit Chol, aber sehr niedrige summierte Korrelation und niedrige Importance
HFmax:
- Korrelation (Features): sehr hohe Korrelation mit RZ und AP_Y
- Korrelation (KHK): -0.400421
- Summierte Korrelation: 2.88 (sehr hoch)
- Importance Metriken: sehr hoch
- Gewichtung: 0.5
- -> Niedrigere Gewichtung, da alle Metriken extrem hoch ausschlagen und die Korrelation mit KHK sehr hoch ist
RZ:
- Korrelation (Features): sehr hohe Korrelation mit AP_Y und hohe mit Alter
- Korrelation (KHK): 0.403951
- Summierte Korrelation: 2.37 (mittel)
- Importance Metriken: hoch
- Gewichtung: 1.5
- -> Korreliert stark mit KHK, hohe Metriken
Geschlecht_M:
- Korrelation (Features): mittlere Korrelation mit RZ und Chol
- Korrelation (KHK): 0.305445
- Summierte Korrelation: 2.01 (kleinster Wert)
- Importance Metriken: minimal
- Gewichtung: 2.0
- -> wird sehr hoch bewertet, da das Feature stark mit KHK korreliert, trotz kleiner summierter Korrelation und minimaler Importance
EKG_Normal:
- Korrelation (Features): mittlere Korrelation mit Alter und extrem hohe mit EKG_ST
- Korrelation (KHK): -0.091580
- Summierte Korrelation: 2.41 (mittel)
- Importance Metriken: fast unbedeutend
- Gewichtung: 0.0
- -> Da fast unbedeutend, wird dieser Feature eliminiert
EKG_ST:
- Korrelation (Features): extrem hohe Korrelation mit EKG_ST
- Korrelation (KHK): 0.102527
- Summierte Korrelation: 2.60 (mittel)
- Importance Metriken: fast unbedeutend
- Gewichtung: 0.0
- -> Da fast unbedeutend, wird dieser Feature eliminiert
AP_Y:
- Korrelation (Features): sehr hohe Korrelation mit RZ und HFmax
- Korrelation (KHK): 0.494282
- Summierte Korrelation: 2.62 (hoch)
- Importance Metriken: hoch
- Gewichtung: 1.5
- -> Korreliert stark mit KHK, hohe Metriken
Jetzt wollen wir mit den neu gewichteten Variablen unsere SVM testen. Zu Beginn implementieren wird eine Testfunktion, welche den Traingsdatensatz mit den übergeben Paramatern manipuliert (gewichtet) und testet:
def evaluate_feature_weights(weights):
X_train_weighted = X_train_scaled.copy()
X_test_weighted = X_test_scaled.copy()
for feature, weight in weights.items():
feature_index = X.columns.get_loc(feature)
X_train_weighted[:, feature_index] *= weight
X_test_weighted[:, feature_index] *= weight
svm = SVC(C=0.7, gamma=0.016, kernel="rbf", random_state=1)
svm.fit(X_train_weighted, y_train)
y_pred_svm = svm.predict(X_test_weighted)
print(accuracy_score(y_test, y_pred_svm))
Jetzt erstellen wir den Eingabeparameter aus den zuvor ermittelten weights:
# Manuelle Gewichtung der Features
feature_weights = {
'Alter': 1.5,
'Blutdruck': 0.0,
'Chol': 1.0,
'Blutzucker': 0.5,
'HFmax': 0.5,
'RZ': 1.5,
'Geschlecht_M': 2.0,
'EKG_Normal': 0.0,
'EKG_ST': 0.0,
'AP_Y': 1.5
}
Und testen unser Modell mit dem gewichteten Datensatz:
evaluate_feature_weights(feature_weights)
0.8206521739130435
Dies entspricht einer weiteren Verbesserung um ca. 0.54%!
Durch fortlaufendes Testing optimieren wir die weights weiter. Folgende weights ...
# Manuelle Gewichtung der Features
feature_weights = {
'Alter': 1.4,
'Blutdruck': 0.0,
'Chol': 0.9,
'Blutzucker': 0.4,
'HFmax': 0.4,
'RZ': 1.4,
'Geschlecht_M': 1.9,
'EKG_Normal': 0,
'EKG_ST': 2,
'AP_Y': 1.3
}
... führen zu einer weiteren Verbesserung der SVM ...
accuracy = evaluate_feature_weights(feature_weights)
0.8315217391304348
... auf ca. 83, 15% Genauigkeit. Dies entspricht einer weiteren Verbesserung von ca. 1,09%!
Insgesamt konnten wir die Modellleistung des SVM von 0.7780 auf 0.8315 optimieren. Dies entspricht einer Gesamtverbesserung von ca. 5,35%!
7. Auswertung und Fazit¶
In diesem Abschnitt testen und vergleichen wir die optimierte SVM mit und ohne Feature Weights sowie die nicht optimierte SVM auf vielen unterschiedlich gesplitteten Datensätzen (andere random states beim splitten). Abschließend ziehen wir ein Fazit.
7.1 Auswertung¶
Zuerst implementieren wir eine Testfunktion für die optimierte SVM mit Feature weights, die Traingsdaten als Parameter akzeptiert und die Genaugikeit für Vorhersagen auf diesen Daten ausgibt:
def test_optimized_svm_with_feature_weights(X_train_scaled, X_test_scaled, y_train, y_test):
feature_weights = {
'Alter': 1.4,
'Blutdruck': 0.0,
'Chol': 0.9,
'Blutzucker': 0.4,
'HFmax': 0.4,
'RZ': 1.4,
'Geschlecht_M': 1.9,
'EKG_Normal': 0,
'EKG_ST': 2,
'AP_Y': 1.3
}
X_train_weighted = X_train_scaled.copy()
X_test_weighted = X_test_scaled.copy()
for feature, weight in feature_weights.items():
feature_index = X.columns.get_loc(feature)
X_train_weighted[:, feature_index] *= weight
X_test_weighted[:, feature_index] *= weight
svm = SVC(C=0.7, gamma=0.016, kernel="rbf", random_state=1)
svm.fit(X_train_weighted, y_train)
y_pred_svm = svm.predict(X_test_weighted)
return accuracy_score(y_test, y_pred_svm)
Die Implementierung der Testfunktion für die optimierte SVM ohne Feature weights erfolgt analog:
def test_optimized_svm(X_train_scaled, X_test_scaled, y_train, y_test):
svm = SVC(C=0.7, gamma=0.016, kernel="rbf", random_state=1)
svm.fit(X_train_scaled, y_train)
y_pred_svm = svm.predict(X_test_scaled)
return accuracy_score(y_test, y_pred_svm)
Abschließend fehlt nur noch die unoptimierte SVM:
def test_svm(X_train_scaled, X_test_scaled, y_train, y_test):
svm = SVC(kernel="rbf", random_state=1)
svm.fit(X_train_scaled, y_train)
y_pred_svm = svm.predict(X_test_scaled)
return accuracy_score(y_test, y_pred_svm)
Wir implementieren die Testfunktion, mit der wir diese Modelle auf unterschiedlichen Traingsdatensätzen anwenden und die jeweilige minimale, maximale und durchschnittliche Genauigkeit berechnen. Die Ausgabe der Ergebnisse erfolgt durch einen Plot:
def svm_vergleich(random_states_from_0_to):
# Listen zur Speicherung der Genauigkeiten
accuracies_svm = []
accuracies_optimized_svm = []
accuracies_optimized_svm_with_weights = []
# Schleife über die verschiedenen Random States
for random_state in range(random_states_from_0_to):
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=random_state, stratify=y)
# Daten standardisieren
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
# Unoptimierte SVM testen
accuracy_svm = test_svm(X_train_scaled, X_test_scaled, y_train, y_test)
accuracies_svm.append(accuracy_svm)
# Optimierte SVM testen
accuracy_optimized_svm = test_optimized_svm(X_train_scaled, X_test_scaled, y_train, y_test)
accuracies_optimized_svm.append(accuracy_optimized_svm)
# Optimierte SVM mit Feature-Gewichtungen testen
accuracy_optimized_svm_with_weights = test_optimized_svm_with_feature_weights(X_train_scaled, X_test_scaled, y_train, y_test)
accuracies_optimized_svm_with_weights.append(accuracy_optimized_svm_with_weights)
# Min-, Mittel- und Max-Genauigkeiten berechnen
min_accuracies = [np.min(accuracies_svm), np.min(accuracies_optimized_svm), np.min(accuracies_optimized_svm_with_weights)]
mean_accuracies = [np.mean(accuracies_svm), np.mean(accuracies_optimized_svm), np.mean(accuracies_optimized_svm_with_weights)]
max_accuracies = [np.max(accuracies_svm), np.max(accuracies_optimized_svm), np.max(accuracies_optimized_svm_with_weights)]
# Daten als Plot anzeigen
plt.figure(figsize=(10, 6))
x_labels = ['Unoptimierte SVM', 'Optimierte SVM', 'Optimierte SVM mit Gewichtungen']
accuracies_df = pd.DataFrame({
'Modell': x_labels,
'Minimale Genauigkeit': min_accuracies,
'Mittlere Genauigkeit': mean_accuracies,
'Maximale Genauigkeit': max_accuracies
})
accuracies_df = accuracies_df.melt(id_vars='Modell', var_name='Metrik', value_name='Genauigkeit')
ax = sns.barplot(x='Modell', y='Genauigkeit', hue='Metrik', data=accuracies_df)
# Exakte Werte im Plot anzeigen
for container in ax.containers:
ax.bar_label(container, fmt='%.3f')
plt.xlabel('Modell')
plt.ylabel('Genauigkeit')
plt.title('Vergleich der SVM-Modelle')
plt.legend(title='Metrik')
plt.show()
Wir führen die Funktion aus und testen die SVMs auf 50 unterschiedlichen Datensätzen (random state 0 bis 49):
svm_vergleich(50)
Unser optimiertes Modell mit Gewichtungen schlägt die anderen beiden in allen drei Kategorien! Zusätzlich schlägt das optimierte Modell ohne Gewichtungen das Standardmodell ebenfalls in allen Kategorien.
7.2 Fazit¶
Dieses Notebook analysierte einen klinischen Datensatz zur Klassifikation des Risikos für koronare Herzkrankheiten (KHK). Durch explorative Datenanalyse, Datenkorrekturen, Skalierung und die Anwendung verschiedener Klassifikationsverfahren wurden wesentliche Erkenntnisse gewonnen. Aufbauend darauf wurde die Support Vector Machine (SVM) als vielversprechendes Verfahren ausgewählt und weiter optimiert.
Dabei wurden die Parameter C und Gamma mithilfe einer GridSearch angepasst, während sowohl automatische als auch manuelle Methoden des Feature-Engineerings eingesetzt wurden. Insbesondere wurden Korrelationen, Importance-Metriken und Beziehungen zwischen den Features untersucht, um die Modellentwicklung gezielt zu verbessern.
Das finale Modell, eine SVM mit optimierten Parametern und Feature-Gewichtungen, wurde auf weiteren Datensätzen getestet und mit den anderen Modellen verglichen. Dieser abschließende Vergleich zeigt die Robustheit der Optimierungen und belegt, dass die Verbesserungen nicht spezifisch für den ursprünglichen Datensatz waren, sondern allgemein anwendbar sind – ein Hinweis darauf, dass Overfitting erfolgreich vermieden wurde.
Optimierte SVM-Parameter:
Parameter | Wert |
---|---|
C | 0.7 |
Gamma | 0.016 |
Kernel | rbf |
Random State | 1 |
Feature-Gewichtungen:
Feature | Gewichtung |
---|---|
Alter | 1.4 |
Blutdruck | 0.0 |
Chol | 0.9 |
Blutzucker | 0.4 |
HFmax | 0.4 |
RZ | 1.4 |
Geschlecht_M | 1.9 |
EKG_Normal | 0 |
EKG_ST | 2 |
AP_Y | 1.3 |