Machine Learning: Training und Testing in Python
Daten in Trainings- und Testmengen aufteilen mit scikit-learn. Behandelt test_size, random_state, stratify und Auswertungsmetriken.
Die Train/Test-Aufteilung ist der grundlegendste Schritt beim Aufbau eines Machine-Learning-Modells. Die Idee ist einfach: Halte einen Teil deiner Daten während des Trainings vor dem Modell verborgen und miss dann, wie gut das Modell auf diesem verborgenen Anteil abschneidet. Ohne diese Trennung gibt es keine ehrliche Möglichkeit zu wissen, ob dein Modell ein Muster wirklich gelernt hat oder lediglich die Trainingsbeispiele auswendig gelernt hat.
Dieses Kapitel behandelt, wie train_test_split aus scikit-learn funktioniert, was jeder Parameter bewirkt und wie das resultierende Modell ausgewertet wird – sowohl für Regressions- als auch für Klassifikationsprobleme.
Warum Trainings- und Testdaten trennen?
Ein Modell, das auf denselben Daten trainiert und ausgewertet wird, wirkt weit genauer als es wirklich ist. Dies nennt man Data Leakage oder Overfitting: Das Modell hat die Trainingsbeispiele auswendig gelernt, anstatt ein verallgemeinerbares Muster zu erkennen.
Stell dir einen Schüler vor, der 100 Übungsaufgaben lernt und dann einen Test mit genau diesen 100 Aufgaben schreibt. Er erreicht vielleicht 100 % – aber dieser Wert sagt nichts darüber aus, ob er den Stoff wirklich versteht.
Die Train/Test-Aufteilung verhindert dies durch:
- Training des Modells auf einem Teil der Daten, damit es Muster erlernen kann.
- Testen des Modells auf einem separaten, ungesehenen Teil, um die reale Leistung zu messen.
Eine typische Aufteilung ist 80 % Training / 20 % Test, wobei das richtige Verhältnis davon abhängt, wie viele Daten vorhanden sind.
Die Funktion train_test_split
scikit-learn stellt train_test_split in seinem Modul model_selection bereit. Sie mischt den Datensatz zufällig durch und teilt ihn in zwei Teile auf:
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)Die vier Rückgabewerte liegen immer in dieser Reihenfolge vor: Trainingsmerkmale, Testmerkmale, Trainingsbezeichnungen, Testbezeichnungen.
Wichtige Parameter
| Parameter | Typ | Beschreibung |
|---|---|---|
test_size | float oder int | Anteil (0–1) oder absolute Anzahl der Testsamples. Standard ist 0.25. |
train_size | float oder int | Komplement zu test_size. Normalerweise setzt man eines der beiden, nicht beide. |
random_state | int | Seed für den Zufallszahlengenerator. Verwende eine beliebige ganze Zahl, um die Aufteilung reproduzierbar zu machen. |
stratify | array-like | Übergib hier y, um die Klassenanteile in beiden Splits beizubehalten. Unverzichtbar bei unausgewogenen Datensätzen. |
shuffle | bool | Ob vor der Aufteilung gemischt werden soll. Standard ist True. Auf False setzen für Zeitreihendaten. |
Auswirkung von test_size
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.datasets import load_iris
X, y = load_iris(return_X_y=True) # 150 samples
for ts in [0.1, 0.2, 0.3]:
X_tr, X_te, _, _ = train_test_split(X, y, test_size=ts, random_state=42)
print(f"test_size={ts}: train={len(X_tr)}, test={len(X_te)}")Ausgabe:
test_size=0.1: train=135, test=15
test_size=0.2: train=120, test=30
test_size=0.3: train=105, test=45random_state und Reproduzierbarkeit
Ohne random_state ist die Aufteilung bei jedem Skriptlauf anders. Setze sie auf eine beliebige ganze Zahl, um reproduzierbare Ergebnisse zu erhalten:
import numpy as np
from sklearn.model_selection import train_test_split
X = np.arange(10).reshape(-1, 1)
y = np.arange(10)
_, X_te1, _, _ = train_test_split(X, y, test_size=0.3, random_state=42)
_, X_te2, _, _ = train_test_split(X, y, test_size=0.3, random_state=42)
print("Same random_state → same split:", list(X_te1.ravel()) == list(X_te2.ravel()))
# TrueDie Wahl der ganzen Zahl (42, 0, 1 usw.) spielt keine Rolle – wichtig ist nur, dass stets derselbe Wert verwendet wird.
Regressionsbeispiel: Hauspreise vorhersagen
Das folgende Beispiel erzeugt einen synthetischen Datensatz aus Hausgrößen und -preisen, trainiert ein lineares Regressionsmodell und wertet es auf dem zurückgehaltenen Testset aus.
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error, r2_score
# Generate synthetic data: house size (sq ft) → price
np.random.seed(42)
n = 200
sqft = np.random.randint(500, 3500, n).astype(float)
price = 150 * sqft + np.random.randn(n) * 20000
X = sqft.reshape(-1, 1)
y = price
# Split
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
print("Training samples:", len(X_train)) # 160
print("Testing samples:", len(X_test)) # 40
# Train
model = LinearRegression()
model.fit(X_train, y_train)
# Evaluate on the test set
y_pred = model.predict(X_test)
mse = mean_squared_error(y_test, y_pred)
r2 = r2_score(y_test, y_pred)
print(f"Mean Squared Error: {mse:,.0f}")
print(f"R² Score: {r2:.4f}")
print(f"Coefficient: {model.coef_[0]:.2f}")
print(f"Intercept: {model.intercept_:.2f}")Ausgabe:
Training samples: 160
Testing samples: 40
Mean Squared Error: 489,271,263
R² Score: 0.9651
Coefficient: 147.55
Intercept: 5237.02Interpretation der Metriken:
- MSE (Mean Squared Error) ist die durchschnittliche quadratische Abweichung zwischen Vorhersagen und tatsächlichen Werten. Kleiner ist besser, aber die Skala hängt von der Zielvariable ab (hier: Dollar).
- R² liegt zwischen 0 und 1. Ein Wert von 0,965 bedeutet, dass das Modell etwa 96,5 % der Varianz in den Hauspreisen erklärt – eine starke Anpassung für diesen einfachen Datensatz.
Mehr zur linearen Regression findest du unter Lineare Regression in Python.
Klassifikationsbeispiel: Iris-Blumenarten
Bei einer Klassifikationsaufgabe sind Genauigkeit und ein Klassifikationsbericht aussagekräftiger als MSE.
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.datasets import load_iris
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, classification_report
X, y = load_iris(return_X_y=True)
# stratify=y ensures each class is proportionally represented in both splits
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
print("Train class distribution:", np.bincount(y_train)) # [40 40 40]
print("Test class distribution: ", np.bincount(y_test)) # [10 10 10]
model = LogisticRegression(max_iter=200)
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
print("\nAccuracy:", round(accuracy_score(y_test, y_pred), 4))
print()
print(classification_report(y_test, y_pred,
target_names=["setosa", "versicolor", "virginica"]))Ausgabe:
Train class distribution: [40 40 40]
Test class distribution: [10 10 10]
Accuracy: 0.9667
precision recall f1-score support
setosa 1.00 1.00 1.00 10
versicolor 1.00 0.90 0.95 10
virginica 0.91 1.00 0.95 10
accuracy 0.97 30
macro avg 0.97 0.97 0.97 30
weighted avg 0.97 0.97 0.97 30Warum stratify=y wichtig ist: Ohne diese Option könnte eine zufällige Aufteilung eines unausgewogenen Datensatzes dazu führen, dass die meisten Samples einer seltenen Klasse ins Training wandern und keine mehr im Testset verbleiben. stratify=y stellt sicher, dass jede Klasse in beiden Splits in denselben Anteilen vertreten ist.
Mehr zur Klassifikation findest du unter Logistische Regression in Python und Konfusionsmatrix in Python.
Häufige Fallstricke
Vorverarbeitung vor oder nach der Aufteilung?
Teile die Daten stets vor dem Anpassen von Vorverarbeitungsschritten auf (Skalierung, Kodierung, Imputation). Wenn du alle Daten skalierst und erst dann aufteilst, hat das Testset den Skalierer beeinflusst – eine Form von Data Leakage.
Die richtige Reihenfolge:
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train) # fit ONLY on training data
X_test_scaled = scaler.transform(X_test) # transform test with same parametersSiehe Feature Scaling in Python für eine ausführliche Anleitung.
Mischen und Zeitreihendaten
train_test_split mischt die Daten standardmäßig. Bei Zeitreihendaten ist das falsch – du würdest auf zukünftigen Daten trainieren, um die Vergangenheit vorherzusagen. Setze shuffle=False und stelle sicher, dass die Daten vor der Aufteilung chronologisch sortiert sind:
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, shuffle=False
)Wenn eine einzelne Aufteilung nicht ausreicht
Eine einzelne 80/20-Aufteilung liefert nur eine Schätzung der Modellleistung, die davon abhängt, welche Samples zufällig ins Testset gelangt sind. Kreuzvalidierung wiederholt die Aufteilung mehrfach und mittelt die Ergebnisse, was eine wesentlich stabilere Schätzung ergibt – besonders bei kleinen Datensätzen.
Auswahl einer Auswertungsmetrik
Die richtige Metrik hängt von der Art des Problems ab:
| Problem | Gängige Metriken |
|---|---|
| Regression | MSE, RMSE, MAE, R² |
| Binäre Klassifikation | Accuracy, Precision, Recall, F1, AUC-ROC |
| Mehrklassen-Klassifikation | Accuracy, Makro-/gewichtetes F1 |
Für die binäre Klassifikation siehe AUC-ROC-Kurve in Python und Konfusionsmatrix in Python. Für die Hyperparameter-Optimierung nach einer funktionierenden Aufteilung siehe Grid Search in Python.
Zusammenfassung
- Teile die Daten vor jeder Vorverarbeitung auf, um Data Leakage zu vermeiden.
- Verwende
test_size=0.2als vernünftigen Standardwert; passe ihn je nach Datensatzgröße an. - Setze
random_stateauf eine beliebige ganze Zahl, um reproduzierbare Aufteilungen zu erhalten. - Verwende
stratify=ybei Klassifikationsaufgaben, besonders bei unausgewogenen Daten. - Setze
shuffle=Falsefür Zeitreihendaten. - Eine einzelne Train/Test-Aufteilung ist ein schneller Ausgangspunkt; nutze Kreuzvalidierung für zuverlässigere Leistungsschätzungen.