Kreuzvalidierung
Kurz gesagt: Die Kreuzvalidierung teilt die Daten in mehrere Folds und trainiert und prüft das Modell viele Male; sie liefert einen mittleren Wert mit seiner Streuung statt einer einzigen Glückszahl.
Problem
„Der Baum oder die Gerade?“
Der Shop will wissen, wie viele Bestellungen jeder Kunde in den nächsten 4 Monaten aufgeben wird. Damit entscheidet er, wem er eine Rabattkarte schickt und wie viel Ware er bestellt. Die Merkmale kommen aus den Bestellungen vor dem 1. Juli 2026, wie in der Lektion über Klassifikation: Zahl der Bestellungen, Tage seit der letzten, wie lange er schon kauft und die durchschnittliche Bestellung. Die Zielgröße ist die Zahl der Bestellungen vom 1. Juli bis 31. Oktober.
Du sagst „lineare Regression“, und dein Kollege sagt „ein Baum mit 3 Ebenen“. Du lässt beide auf 6 verschiedenen Aufteilungen in Training und Test laufen (random_state von 0 bis 5) und misst den MAE in Bestellungen pro Kunde:
Es sind 1.279 Kunden, und sie geben in den 4 Monaten im Schnitt 2,70 Bestellungen auf. Bei den Aufteilungen 0, 1, 2 und 4 liegt der Baum weniger daneben, bei 3 und 5 die Gerade. Hättest du nur die Aufteilung 2 laufen lassen, hätte dein Kollege mit 1,25 gegen 1,31 gewonnen. Bei der Aufteilung 5 gewinnst du. Wer hat recht?
Die Idee
Eine Prüfung ist eine Stichprobe
Die Testdaten sind 256 Kunden, zufällig ausgewählt. Wie der Mittelwert aus 30 Bestellungen in der Lektion über Stichproben rauscht auch der Fehler auf 256 Kunden: Eine andere Stichprobe gibt eine andere Zahl. Der Fehler der Geraden springt von 1,31 auf 1,43 allein dadurch, welche Kunden im Test gelandet sind.
Der Abstand zwischen den beiden Modellen beträgt ein paar Hundertstel. Das Rauschen der Aufteilung liegt bei etwa zehn Hundertsteln. Mit einer Prüfung kannst du nicht sagen, welches besser ist, genauso wenig wie du aus 30 Bestellungen die durchschnittliche Bestellung bestimmen kannst.
Die Idee
Kreuzvalidierung
Die Lösung ist, viele Male zu prüfen, mit jedem Kunden genau einmal im Test. Du teilst die Kunden in 5 Folds (Teilmengen). Du trainierst mit 4 und misst am fünften, dann wechselst du, welcher der Test ist. Du bekommst 5 Fehler für jedes Modell. Das heißt Kreuzvalidierung.
KFold teilt in Folds, und cross_val_score trainiert und misst das Modell an jedem. Mit shuffle=True werden die Kunden vor dem Teilen gemischt:
Viermal lernst du an 1.023 Kunden und prüfst an 256, einmal an 1.024 und 255. scikit-learn sucht immer die größte Zahl, deshalb kommt der Fehler mit einem Minus (neg_mean_absolute_error), und das Minus davor dreht ihn zurück.
Die Baseline (der Mittelwert, DummyRegressor) liegt um 2,70 ± 0,09 daneben. Die Gerade liegt um 1,34 ± 0,06 daneben und der Baum um 1,38 ± 0,09. Die Zahl nach dem ± ist die Streuung der fünf Fehler (std). Beide Modelle schlagen die Baseline klar, und der Abstand zwischen ihnen (0,04) ist kleiner als die Streuung. Der ehrliche Schluss lautet „die Gerade ist etwas besser, aber nicht eindeutig“. Wenn zwei Modelle so nah beieinander liegen, nimmst du das einfachere.
Genauer, für Neugierige
Beide Modelle werden auf denselben Folds verglichen: Deshalb ist kf ein einziges Objekt mit einem random_state. cv=5 teilt ebenfalls in 5, aber ohne Mischen: Der erste Fold sind die ersten 256 Kunden der Reihe nach. Ist die Tabelle sortiert (nach Nummer, nach dem Datum der ersten Bestellung), unterscheiden sich die Folds voneinander und der Wert wird verzerrt.
Probier es aus
Ist der Wald besser?
Dein Kollege schlägt einen Random Forest vor (RandomForestRegressor mit 50 Bäumen, max_depth=5). Prüfe ihn mit Kreuzvalidierung auf denselben Folds kf. Lege in forest_mae und line_mae die fünf Fehler (MAE, positiv) des Waldes und der Geraden ab, in gap den Unterschied im mittleren Fehler (Wald minus Gerade) und in spread die Streuung der Fehler des Waldes.
Lösung
from sklearn.ensemble import RandomForestRegressor
import numpy as np
import pandas as pd
from sklearn.model_selection import KFold, cross_val_score, train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.tree import DecisionTreeRegressor
from sklearn.dummy import DummyRegressor
from sklearn.metrics import mean_absolute_error
orders = pd.read_csv('year_orders.csv')
items = pd.read_csv('year_items.csv')
orders['ordered_at'] = pd.to_datetime(orders['ordered_at'])
items['revenue'] = items['quantity'] * items['unit_price']
orders['value'] = orders['id'].map(items.groupby('order_id')['revenue'].sum())
cutoff = pd.Timestamp('2026-07-01')
before = orders[orders['ordered_at'] < cutoff]
after = orders[orders['ordered_at'] >= cutoff]
customers = before.groupby('customer_id').agg(
orders=('id', 'size'),
last=('ordered_at', 'max'),
first=('ordered_at', 'min'),
avg_value=('value', 'mean'),
)
customers['days_since'] = (cutoff - customers['last']).dt.days
customers['tenure'] = (cutoff - customers['first']).dt.days
customers['next'] = after.groupby('customer_id').size().reindex(customers.index, fill_value=0)
X = customers[['orders', 'days_since', 'tenure', 'avg_value']]
y = customers['next']
kf = KFold(n_splits=5, shuffle=True, random_state=42)
forest = RandomForestRegressor(n_estimators=50, max_depth=5, random_state=0)
forest_mae = -cross_val_score(forest, X, y, cv=kf, scoring='neg_mean_absolute_error')
line_mae = -cross_val_score(LinearRegression(), X, y, cv=kf, scoring='neg_mean_absolute_error')
gap = forest_mae.mean() - line_mae.mean()
spread = forest_mae.std()
print(forest_mae.round(2), line_mae.round(2))
print(round(gap, 2), round(spread, 2))Der Wald liegt im Schnitt um 1,36 Bestellungen daneben, die Gerade um 1,34: gap ist 0,02 zugunsten der Geraden. Die Streuung des Waldes ist 0,10, viel mehr als der Abstand. Der Wald ist nicht besser, und er ist langsamer und schwerer zu erklären. Ohne das Minus vor cross_val_score kommen die Fehler negativ heraus, und mit cv=5 statt kf werden die beiden Modelle auf anderen Folds gemessen als im Beispiel.
Die Idee
Überanpassung
Warum hat der Baum 3 Ebenen und nicht 20? Je tiefer er ist, desto feiner teilt er die Kunden. Hier ist der Fehler auf den Trainingskunden und auf den Testkunden für Tiefen von 1 bis 12:
Auf den Trainingskunden fällt der Fehler jedes Mal: von 1,87 auf 0,41. Auf den Testkunden fällt er bis zur Tiefe 3 (1,50) und wächst dann auf 1,91. Ein Baum ohne Begrenzung erreicht 23 Ebenen und liegt bei den Trainingskunden um 0,001 daneben, fast null, und bei den Testkunden um 2,00.
Der tiefe Baum hat nicht mehr über die Kunden gelernt. Er hat jeden Trainingskunden samt seinem Rauschen auswendig gelernt: Ein Kunde hat zufällig mehr bestellt, ein anderer weniger. Das heißt Überanpassung. Das Zeichen ist ein großer Abstand zwischen dem Trainingsfehler und dem Testfehler. Deshalb sagt der Fehler auf den Daten, aus denen das Modell gelernt hat, nichts über neue Kunden.
Probier es aus
Wähle die Tiefe
Wähle die Tiefe des Baums mit Kreuzvalidierung. Lege in cv_mae (eine Series mit der Tiefe von 1 bis 10 als Index) den mittleren MAE aus cross_val_score mit den Folds kf ab und in best_depth die Tiefe mit dem kleinsten Fehler.
Lösung
import numpy as np
import pandas as pd
from sklearn.model_selection import KFold, cross_val_score, train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.tree import DecisionTreeRegressor
from sklearn.dummy import DummyRegressor
from sklearn.metrics import mean_absolute_error
orders = pd.read_csv('year_orders.csv')
items = pd.read_csv('year_items.csv')
orders['ordered_at'] = pd.to_datetime(orders['ordered_at'])
items['revenue'] = items['quantity'] * items['unit_price']
orders['value'] = orders['id'].map(items.groupby('order_id')['revenue'].sum())
cutoff = pd.Timestamp('2026-07-01')
before = orders[orders['ordered_at'] < cutoff]
after = orders[orders['ordered_at'] >= cutoff]
customers = before.groupby('customer_id').agg(
orders=('id', 'size'),
last=('ordered_at', 'max'),
first=('ordered_at', 'min'),
avg_value=('value', 'mean'),
)
customers['days_since'] = (cutoff - customers['last']).dt.days
customers['tenure'] = (cutoff - customers['first']).dt.days
customers['next'] = after.groupby('customer_id').size().reindex(customers.index, fill_value=0)
X = customers[['orders', 'days_since', 'tenure', 'avg_value']]
y = customers['next']
kf = KFold(n_splits=5, shuffle=True, random_state=42)
cv_mae = pd.Series(dtype=float)
for depth in range(1, 11):
tree = DecisionTreeRegressor(max_depth=depth, random_state=0)
cv_mae[depth] = -cross_val_score(tree, X, y, cv=kf, scoring='neg_mean_absolute_error').mean()
best_depth = cv_mae.idxmin()
print(cv_mae.round(2))
print(best_depth)Die Kreuzvalidierung gibt bei Tiefe 1 den Wert 1,89, bei 3 den kleinsten, 1,38, und dann wächst der Fehler bis 1,70 bei 10. best_depth ist 3. Misst du den Fehler an denselben Kunden, aus denen der Baum gelernt hat, fällt er nur, und das „Beste“ ist der tiefste Baum: genau der überangepasste.
Die Idee
Wenn die Reihenfolge die Zeit ist
Kunden kann man mischen: Ein Kunde weiß nichts von einem anderen. Tage nicht. Wenn du die Bestellungen für morgen vorhersagst, hat das Modell nur die Vergangenheit. Gemischte Folds würden es auch an Tagen nach den Testtagen trainieren, und das ist das Datenleck aus der vorigen Lektion.
TimeSeriesSplit teilt nach der Zeit: Es lernt am Anfang und prüft an den folgenden Tagen, dann lernt es an mehr und prüft an noch späteren. Ein Beispiel: die Bestellungen eines Tages aus dem Wochentag, den Bestellungen vor 7 Tagen und dem Mittelwert der vorherigen 7 Tage:
Die ersten 7 Tage haben kein „vor 7 Tagen“ und fallen weg, es bleiben 358. Jeder Test-Fold sind 59 Tage nach dem Trainingsteil, und der Trainingsteil wächst: erst 2 Monate, am Ende 10. Die Gerade liegt im Schnitt um 4,51 ± 0,18 Bestellungen pro Tag daneben.
Probier es aus
Schlägt es die Baseline?
Vergleiche die Gerade mit der Baseline (DummyRegressor, dem Mittelwert der Trainingstage) mit TimeSeriesSplit und 5 Folds. Lege in line_mae und base_mae die fünf Fehler (MAE, positiv) von jedem ab.
Lösung
import numpy as np
import pandas as pd
from sklearn.model_selection import TimeSeriesSplit, cross_val_score
from sklearn.linear_model import LinearRegression
from sklearn.dummy import DummyRegressor
orders = pd.read_csv('year_orders.csv')
orders['ordered_at'] = pd.to_datetime(orders['ordered_at'])
daily = orders.set_index('ordered_at').resample('D').size().rename('orders').to_frame()
daily['weekday'] = daily.index.dayofweek
daily['last_week'] = daily['orders'].shift(7)
daily['week_mean'] = daily['orders'].shift(1).rolling(7).mean()
daily = daily.dropna()
X = pd.get_dummies(daily['weekday'], prefix='day', dtype=int).join(daily[['last_week', 'week_mean']])
y = daily['orders']
ts = TimeSeriesSplit(n_splits=5)
line_mae = -cross_val_score(LinearRegression(), X, y, cv=ts, scoring='neg_mean_absolute_error')
base_mae = -cross_val_score(DummyRegressor(), X, y, cv=ts, scoring='neg_mean_absolute_error')
print(line_mae.round(2), base_mae.round(2))
print(f'{line_mae.mean():.2f} ± {line_mae.std():.2f}', f'{base_mae.mean():.2f} ± {base_mae.std():.2f}')Die Gerade liegt um 4,51 ± 0,18 Bestellungen pro Tag daneben, die Baseline um 5,69 ± 1,92. Der erste Fold der Baseline ist 9,29: Sie lernt an November und Dezember, dem Höhepunkt des Jahres, und sagt Januar und Februar voraus, die schwächsten Monate. Die Streuung zeigt, dass der Mittelwert der Vergangenheit eine schlechte Vorhersage ist, wenn die Saison wechselt. Mit gemischten Folds (KFold) sind die Zahlen anders: Das Modell hat an Tagen nach den Testtagen gelernt.
Die Idee
Der Test wird einmal berührt
Probierst du 10 Modelle an den Testdaten aus und nimmst das beste, war der Test an der Auswahl beteiligt. Eines von zehn hat Glück mit genau diesen Kunden, und sein Fehler sieht besser aus, als er ist.
Die Reihenfolge ist deshalb:
- Du legst am Anfang einen Testteil beiseite und schaust nicht hin.
- Du wählst das Modell und seine Einstellungen (Tiefe, Merkmale) mit Kreuzvalidierung nur auf den Trainingsdaten.
- Du trainierst das gewählte Modell mit dem ganzen Trainingsteil und misst es einmal am Test. Das ist die Zahl, die du dem Chef nennst.
Bei den 1.023 Trainingskunden liegt die Baseline um 2,74 ± 0,17 daneben, die Gerade um 1,32 ± 0,06 und der Baum um 1,36 ± 0,07. Die Gerade hat den kleineren Fehler und ist einfacher: Du wählst sie. Die 256 Testkunden sind noch unberührt.
Genauer, für Neugierige
Änderst du nach dem Test das Modell und misst noch einmal am Test, wird der Test Teil der Auswahl. Dann brauchst du einen neuen Testteil: neue Daten, zum Beispiel den nächsten Monat. Deshalb ist für ein Modell der Bestellungen die ehrlichste Prüfung die Zeit nach dem Training, wie bei TimeSeriesSplit.
Die Idee
Wann man kein Modell braucht
Ein Modell kostet Zeit: Daten, Training, darauf achten, dass es nicht schlechter wird. Manchmal lohnt es sich nicht:
- Wenige Daten. Bei ein paar Dutzend Kunden springt der Fehler der Kreuzvalidierung so stark, dass du nicht sagen kannst, ob das Modell den Mittelwert schlägt.
- Eine Regel ohne Modell leistet dasselbe. „Der Kunde wird im selben Tempo kaufen“ ist eine Division. Wenn das Modell sie nicht klar schlägt, ist die Regel besser: Alle verstehen sie, und nichts kann kaputtgehen.
- Die Kosten eines Fehlers. Ist ein Fehler teuer (du lehnst einen Kredit ab, sperrst eine Karte), muss das Modell sehr sicher sein, oder ein Mensch muss prüfen.
- Niemand handelt nach der Vorhersage. Folgt aus der Zahl keine Entscheidung (wer einen Gutschein bekommt, wie viel Ware), ist das Modell nur eine Übung.
Die Gerade hat einen Koeffizienten von 0,469 für die bisherige Zahl der Bestellungen, und die anderen Merkmale spielen kaum mit. Es ist also fast „die Hälfte der bisherigen Bestellungen“: 8 Monate davor, 4 Monate danach. Prüf es in der Übung.
Übung
Das Urteil
Die Kreuzvalidierung auf den Trainingsdaten hat die Gerade gewählt. Trainiere sie mit X_train und prüfe sie einmal am Test: Lege in model das trainierte Modell ab, in test_mae seinen Fehler (MAE) auf den Testkunden und in base_mae den Fehler der Baseline im Test.
Lösung
import numpy as np
import pandas as pd
from sklearn.model_selection import KFold, cross_val_score, train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.tree import DecisionTreeRegressor
from sklearn.dummy import DummyRegressor
from sklearn.metrics import mean_absolute_error
orders = pd.read_csv('year_orders.csv')
items = pd.read_csv('year_items.csv')
orders['ordered_at'] = pd.to_datetime(orders['ordered_at'])
items['revenue'] = items['quantity'] * items['unit_price']
orders['value'] = orders['id'].map(items.groupby('order_id')['revenue'].sum())
cutoff = pd.Timestamp('2026-07-01')
before = orders[orders['ordered_at'] < cutoff]
after = orders[orders['ordered_at'] >= cutoff]
customers = before.groupby('customer_id').agg(
orders=('id', 'size'),
last=('ordered_at', 'max'),
first=('ordered_at', 'min'),
avg_value=('value', 'mean'),
)
customers['days_since'] = (cutoff - customers['last']).dt.days
customers['tenure'] = (cutoff - customers['first']).dt.days
customers['next'] = after.groupby('customer_id').size().reindex(customers.index, fill_value=0)
X = customers[['orders', 'days_since', 'tenure', 'avg_value']]
y = customers['next']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
model = LinearRegression().fit(X_train, y_train)
test_mae = mean_absolute_error(y_test, model.predict(X_test))
base = DummyRegressor().fit(X_train, y_train)
base_mae = mean_absolute_error(y_test, base.predict(X_test))
print(round(test_mae, 2), round(base_mae, 2))Die Gerade liegt bei den 256 Testkunden um 1,38 Bestellungen daneben, die Baseline um 2,58. Das ist die Zahl für den Chef: „Die Vorhersage für 4 Monate liegt im Schnitt um 1,4 Bestellungen daneben.“ In der Kreuzvalidierung gab die Gerade 1,32 ± 0,06: Der Test ist etwas schlechter, aber im Rauschen. Trainierst du mit allen Kunden, hat das Modell auch die Testkunden gesehen, und 1,38 ist nicht mehr ehrlich. Der Baum deines Kollegen liegt im Test mehr daneben, aber du wählst nicht wegen des Tests gegen ihn: Die Auswahl war vorher getroffen.
Schlägt die Gerade eine Regel ohne Modell? Die Regel lautet „der Kunde wird im selben Tempo kaufen“: für 4 Monate die Hälfte seiner Bestellungen aus den 8 Monaten davor. Lege in rule die Vorhersage der Regel für jeden Testkunden ab und in rule_mae ihren Fehler im Test.
Lösung
import numpy as np
import pandas as pd
from sklearn.model_selection import KFold, cross_val_score, train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.tree import DecisionTreeRegressor
from sklearn.dummy import DummyRegressor
from sklearn.metrics import mean_absolute_error
orders = pd.read_csv('year_orders.csv')
items = pd.read_csv('year_items.csv')
orders['ordered_at'] = pd.to_datetime(orders['ordered_at'])
items['revenue'] = items['quantity'] * items['unit_price']
orders['value'] = orders['id'].map(items.groupby('order_id')['revenue'].sum())
cutoff = pd.Timestamp('2026-07-01')
before = orders[orders['ordered_at'] < cutoff]
after = orders[orders['ordered_at'] >= cutoff]
customers = before.groupby('customer_id').agg(
orders=('id', 'size'),
last=('ordered_at', 'max'),
first=('ordered_at', 'min'),
avg_value=('value', 'mean'),
)
customers['days_since'] = (cutoff - customers['last']).dt.days
customers['tenure'] = (cutoff - customers['first']).dt.days
customers['next'] = after.groupby('customer_id').size().reindex(customers.index, fill_value=0)
X = customers[['orders', 'days_since', 'tenure', 'avg_value']]
y = customers['next']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
model = LinearRegression().fit(X_train, y_train)
model_mae = mean_absolute_error(y_test, model.predict(X_test))
rule = X_test['orders'] / 2
rule_mae = mean_absolute_error(y_test, rule)
print(round(model_mae, 2), round(rule_mae, 2))
print(dict(zip(X.columns, model.coef_.round(3).tolist())))Die Regel liegt um 1,41 daneben, die Gerade um 1,38: ein Abstand von 0,03 Bestellungen, unter dem Rauschen einer einzelnen Prüfung. Der Koeffizient der Geraden für die Zahl der Bestellungen ist 0,469, fast die Hälfte, also geben die beiden fast dasselbe. Nimmst du alle bisherigen Bestellungen ohne Teilen durch 2, sagst du 8 Monate statt 4 voraus. Hier ist die Regel die vernünftige Wahl: eine Division, die alle verstehen, gegen ein Modell, das trainiert und überwacht werden muss.
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Modelle: aus Beispielen lernen · Lineare Regression · Klassifikation · Datenleck
- Aufgaben zum Thema: Modelle mit scikit-learn