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¶

In [ ]:
# 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¶

In [2]:
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
Out[2]:
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¶

In [3]:
df.shape
Out[3]:
(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.

In [4]:
# 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¶

In [5]:
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
Out[5]:
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¶

In [6]:
df.describe()
Out[6]:
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¶

In [7]:
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()
No description has been provided for this image

Von den 918 Patienten des Datensatzes leiden 508 (55.3%) an der Koronaren Herzkrankheit.

3.3 Korrelation der Features¶

In [8]:
# 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
No description has been provided for this image

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.

In [9]:
# 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:

In [10]:
pd.concat([X_train, y_train], axis=1).head()
Out[10]:
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:

In [11]:
pd.concat([X_test, y_test], axis=1).head()
Out[11]:
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.

In [12]:
# 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()
No description has been provided for this image

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:

In [13]:
# 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()
No description has been provided for this image

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)¶

In [14]:
# 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)¶

In [15]:
# SVM-Modell
svm = SVC(kernel="rbf")
svm.fit(X_train_scaled, y_train)
y_pred_svm = svm.predict(X_test_scaled)

Logistische Regression¶

In [16]:
# Logistisches Regressionsmodell
log_reg = LogisticRegression()
log_reg.fit(X_train_scaled, y_train)
y_pred_log = log_reg.predict(X_test_scaled)

Entscheidungsbaum¶

In [17]:
# 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¶

In [18]:
# 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¶

In [19]:
# 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

In [20]:
# 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()
No description has been provided for this image

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.

In [21]:
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_
Out[21]:
{'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:

In [22]:
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:

In [23]:
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:

In [24]:
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_
Out[24]:
{'C': 0.85, 'gamma': 0.014}

Wir testen die verfeinerten Parameter 𝐶 = 0.85 und 𝛾 = 0.014:

In [25]:
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.

In [26]:
# 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()
No description has been provided for this image

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:

In [27]:
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:

In [28]:
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:

In [29]:
# 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:

In [30]:
# 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)

In [31]:
khk_corr_plot # Wir verwenden den gespeicherten Plot aus Abschnitt 3.3
Out[31]:
No description has been provided for this image

Und die Korrelation der Werte untereinander:

In [32]:
# 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
No description has been provided for this image

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:

In [33]:
# 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()
No description has been provided for this image

Wir machen weiter mit dem RandomForestClassifier:

In [34]:
# 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()
No description has been provided for this image

Und schließen mit dem GradientBoostingClassifier ab:

In [35]:
# 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()
No description has been provided for this image

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:

In [36]:
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:

In [37]:
# 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:

In [38]:
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 ...

In [39]:
# 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 ...

In [40]:
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:

In [41]:
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:

In [42]:
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:

In [43]:
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:

In [44]:
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):

In [45]:
svm_vergleich(50)
No description has been provided for this image

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

8. Quellen¶

Die Dokumentationen der verwendeten Bibliotheken.

  • pandas
  • numpy
  • matplotlib
  • seaborn
  • sklearn