Datenleck
Kurz gesagt: Ein Datenleck liegt vor, wenn ein Modell beim Training Informationen sieht, die es bei der echten Vorhersage nicht haben wird; dann sieht der Testwert besser aus, als das Modell im echten Einsatz sein wird.
Problem
99,7 % Genauigkeit
Ein Kollege hat das Modell aus der Lektion über Klassifikation gebaut: welche Kunden nach dem 1. Juli nicht mehr bestellen. Nur nimmt er die Kundentabelle direkt aus dem System, so wie sie heute, am 1. November, ist: Zahl der Bestellungen, Tage seit der letzten Bestellung und durchschnittliche Bestellung. Und er zeigt dir:
Es sind 1.279 Kunden. Die Regel „alle kommen wieder“ liegt bei 72,5 % der Testkunden richtig. Das Modell liegt bei 99,7 % richtig: Von 320 Kunden in den Testdaten verfehlt es einen. In der Lektion über Klassifikation hatte das Modell mit denselben drei Merkmalen 72,2 %, unter der Regel. Was hat sich geändert? Die Tabelle.
Die Idee
Die Antwort steckt in den Merkmalen
Sieh nach, wie viele Tage seit der letzten Bestellung die zurückkehrenden Kunden (lost ist 0) und die verlorenen (lost ist 1) haben:
Jeder zurückkehrende Kunde hat von 0 bis 122 Tage, und jeder verlorene von 123 bis 363. Eine einzige Zahl trennt sie ohne Fehler. Warum? days_since wird bis heute berechnet. Wer nach dem 1. Juli bestellt hat, hat eine letzte Bestellung nach dem 1. Juli, ist also weniger als 123 Tage vom 1. November entfernt. Das Merkmal enthält das Label. Dasselbe gilt für orders: Es zählt auch die Bestellungen nach dem 1. Juli.
Das nennt man Datenleck (Leakage): Informationen, die das Modell bei der Vorhersage nicht haben wird, gelangen ins Training. Am 1. Juli weißt du nicht, wer im August bestellt. Die Regel ist eine: Jedes Merkmal wird nur aus dem berechnet, was im Moment der Vorhersage bekannt ist.
Genauer, für Neugierige
Ein Datenleck versteckt sich in Spalten, die das System nach dem Ereignis aktualisiert: „Bestellungen gesamt“, „letzter Kauf“, „Status“, „Artikel zurückgegeben“. Frag bei jeder Spalte, wann ihr Wert geschrieben wird. Ist es nach dem Tag der Vorhersage, taugt die Spalte nicht als Merkmal, so nützlich sie auch aussieht. Die stärksten Merkmale eines Modells sind auch die, die du als Erstes prüfst.
Die Idee
Die ehrliche Zahl
Dieselben Merkmale zweimal. Einmal aus allen Bestellungen bis heute, einmal nur aus den Bestellungen vor dem 1. Juli und bis zum 1. Juli:
Mit der Zukunft in den Merkmalen beträgt die Genauigkeit 99,7 %. Ehrlich sind es 72,2 %, und die Baseline ist 72,5 %. Mit diesen drei Merkmalen gewinnt das Modell an Genauigkeit nichts, genau wie in der Lektion über Klassifikation. Unangenehm, aber wahr. Besser, du weißt es jetzt, als nachdem du die Gutscheine verteilt hast.
features(rows, now) ist eine Funktion für beide Tabellen: Der einzige Unterschied ist, welche Bestellungen du ihr gibst und bis zu welchem Tag sie zählt. .loc[honest.index] nimmt dieselben 1.279 Kunden in derselben Reihenfolge, damit sind die beiden Aufteilungen identisch: 959 Kunden für das Training und 320 für den Test.
Sieh es dir an
Wie die Zukunft ins Merkmal gelangt
Derselbe Code, langsam. Die Punkte sind die Bestellungen von vier Kunden pro Monat, und die Wand ist der 1. Juli. Das Label kommt von den Punkten rechts davon. Das Merkmal des Kollegen zählt sie mit.
import pandas as pdfrom sklearn.model_selection import train_test_splitfrom sklearn.linear_model import LogisticRegression
orders = pd.read_csv('year_orders.csv', parse_dates=['ordered_at'])items = pd.read_csv('year_items.csv')items['value'] = items['quantity'] * items['unit_price']orders['basket'] = orders['id'].map(items.groupby('order_id')['value'].sum())
cutoff = pd.Timestamp('2026-07-01')before = orders[orders['ordered_at'] < cutoff]after = orders[orders['ordered_at'] >= cutoff]
def features(rows, now): people = rows.groupby('customer_id').agg( orders=('id', 'size'), last=('ordered_at', 'max'), basket=('basket', 'mean')) people['days_since'] = (now - people['last']).dt.days return people[['orders', 'days_since', 'basket']]
honest = features(before, cutoff)leaky = features(orders, pd.Timestamp('2026-11-01')).loc[honest.index]y = (~honest.index.isin(after['customer_id'])).astype(int)
for name, X in [('mit der Zukunft', leaky), ('ehrlich', honest)]: X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.25, random_state=42, stratify=y) model = LogisticRegression(max_iter=1000).fit(X_train, y_train) print(name, round(model.score(X_test, y_test), 3))print('Kunden:', len(honest), 'Training:', len(X_train), 'Test:', len(X_test))print('Baseline:', round(1 - y_test.mean(), 3))Schritt für Schritt, in Worten
- orders sind alle Bestellungen des Jahres, bis zum 31. Oktober 2026. Die Punkte sind die Bestellungen von vier Kunden, nach Monaten.
- cutoff ist der 1. Juli. Links von der Wand liegt before: was du weißt. Rechts liegt after: was du vorhersagst.
- lost ist 1, wenn der Kunde rechts keine einzige Bestellung hat. Das Label entsteht aus den Punkten nach dem 1. Juli.
- Die Tabelle des Kollegen zählt bis heute. orders zählt auch die Punkte rechts mit, und days_since wird ab heute gemessen: dieselben Punkte, aus denen das Label entsteht.
- Die Aufteilung: 959 Kunden zum Trainieren und 320 zum Testen, von insgesamt 1.279.
- Mit der Zukunft in den Merkmalen beträgt die Genauigkeit 99,7%. Die Baseline „alle kommen wieder“ liegt bei 72,5%.
- Ehrlich: Die Merkmale enden am 1. Juli. Die Punkte rechts werden nicht gezählt, und days_since wird ab der Wand gemessen.
- Die ehrliche Genauigkeit beträgt 72,2%, unter der Baseline. 99,7% war ein Datenleck, kein Modell.
Die Kunden in der Szene sind eine Skizze: Die Zahlen in den Bildunterschriften sind echt, von der Zahl der Kunden bis zu den beiden Genauigkeiten.
Probier es aus
Noch ein Merkmal
Der Kollege will noch ein Merkmal: recent, die Zahl der Bestellungen des Kunden in den letzten 30 Tagen. Berechne es ehrlich, Stand 1. Juli: Lege in month die Bestellungen der 30 Tage vor cutoff ab, in people['recent'] ihre Zahl für jeden Kunden (0 für die ohne Bestellung) und in score die Genauigkeit des Modells mit den vier Merkmalen.
Lösung
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
orders = pd.read_csv('year_orders.csv', parse_dates=['ordered_at'])
items = pd.read_csv('year_items.csv')
items['value'] = items['quantity'] * items['unit_price']
orders['basket'] = orders['id'].map(items.groupby('order_id')['value'].sum())
cutoff = pd.Timestamp('2026-07-01')
before = orders[orders['ordered_at'] < cutoff]
after = orders[orders['ordered_at'] >= cutoff]
people = before.groupby('customer_id').agg(
orders=('id', 'size'), last=('ordered_at', 'max'), basket=('basket', 'mean'))
people['days_since'] = (cutoff - people['last']).dt.days
people['lost'] = (~people.index.isin(after['customer_id'])).astype(int)
month = before[before['ordered_at'] >= cutoff - pd.Timedelta(days=30)]
people['recent'] = month.groupby('customer_id').size()
people['recent'] = people['recent'].fillna(0)
X = people[['orders', 'days_since', 'basket', 'recent']]
y = people['lost']
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.25, random_state=42, stratify=y)
model = LogisticRegression(max_iter=1000).fit(X_train, y_train)
score = model.score(X_test, y_test)
print(round(people['recent'].mean(), 3), round(score, 3))Im Schnitt hat ein Kunde 0,699 Bestellungen im Juni, und die Genauigkeit bleibt bei 72,2 %: recent hilft nicht. Zählst du die letzten 30 Tage bis heute, also den Oktober, wird die Genauigkeit 75,6 % und sieht aus wie eine Verbesserung gegenüber der Baseline. Das ist sie nicht: Die Bestellungen im Oktober sind die Zukunft, die du vorhersagst. Ein Datenleck ergibt nicht immer 99 %. Manchmal ergibt es gerade so viel, dass du es glaubst.
Die Idee
Vorbereitung vor der Aufteilung
Die logistische Regression arbeitet besser, wenn die Merkmale ähnlich skaliert sind: days_since geht bis 240, und orders geht selten über 10. Der StandardScaler zieht von jedem Merkmal seinen Mittelwert ab und teilt durch die Standardabweichung. So wird jedes Merkmal um 0 zentriert, mit einer Streuung von 1. Der Scaler lernt die Mittelwerte mit fit aus den Daten, die du ihm gibst. Der Kollege gibt ihm alles:
Der Scaler hat von allen 1.279 Kunden gelernt: Die Mittelwerte sind 5,88 Bestellungen, 67,42 Tage und 10,69 €. Die Trainingsdaten allein haben 5,96, 67,84 und 10,68. Der Unterschied kommt von den 320 Testkunden: Sie haben die Vorbereitung beeinflusst, bevor das Modell sie gesehen hat. Die Testdaten sind nicht mehr ganz ungesehen.
Hier ändert sich die Genauigkeit nicht: Sie ist wieder 72,2 %, weil die Mittelwerte nah beieinander liegen. Nicht immer ist es so. Ein Datenleck wächst bei wenigen Daten und bei einer Vorbereitung, die auf das Label schaut, zum Beispiel wenn du die Merkmale danach auswählst, wie sie mit lost in allen Zeilen zusammenhängen. Das Auffüllen fehlender Werte mit dem Mittelwert (fillna) ist derselbe Fall: Der Mittelwert wird nur aus den Trainingsdaten berechnet.
Die Idee
Pipeline: Die Vorbereitung lernt nur aus den Trainingsdaten
Eine Pipeline verbindet die Vorbereitung und das Modell zu einem Objekt. make_pipeline(StandardScaler(), LogisticRegression()) baut eine Kette aus zwei Schritten:
pipe.fit(X_train, y_train)trainiert den Scaler und dann das Modell, nur mit den Trainingsdaten;pipe.score(X_test, y_test)undpipe.predictskalieren die Testdaten mit den gelernten Mittelwerten und fragen das Modell.
Die Mittelwerte des Scalers in pipe sind 5,96, 67,84 und 10,68: genau die der Trainingsdaten. Die Genauigkeit ist 72,2 %. Jetzt kann die Reihenfolge nicht mehr schiefgehen: Die Vorbereitung ist Teil des Modells. Eine Pipeline hilft auch in der nächsten Lektion: In der Kreuzvalidierung lernt jede Aufteilung den Scaler neu.
Genauer, für Neugierige
pipe[0] ist der erste Schritt, und pipe[-1] ist das Modell. Eine Kette kann mehr Schritte haben: SimpleImputer füllt die fehlenden Werte auf, OneHotEncoder macht aus Kategorien 0/1-Spalten, und ColumnTransformer wendet verschiedene Vorbereitungen auf verschiedene Spalten an. Alle lernen mit einem einzigen fit, nur aus den Trainingsdaten.
Die Idee
Ein Kunde in Training und Test zugleich
Die Tabelle wurde aus zwei Exporten zusammengeklebt, und jeder Kunde steht zweimal darin. Im Code macht das pd.concat: Es stellt die beiden Tabellen untereinander. Es sieht harmlos aus: dieselben Daten, nur mehr davon. Du probierst einen Entscheidungsbaum (DecisionTreeClassifier), der ohne Tiefenbegrenzung wächst:
Es sind 2.558 Zeilen, und 1.279 davon sind Wiederholungen. Mit den Duplikaten liegt der Baum bei 89,4 % der Testdaten richtig, über der Baseline. Ohne sie bei 58,4 %. Bei einer zufälligen Aufteilung hat die Mehrheit der Testzeilen einen Zwilling in den Trainingsdaten, und ein Baum ohne Begrenzung merkt sich jede Zeile. Er sagt nicht voraus, er erinnert sich. Ein neuer Kunde hat keinen Zwilling: 58,4 % ist die echte Zahl, unter der Baseline, weil der Baum überangepasst ist. Davon handelt die nächste Lektion.
Die Regel: Du entfernst die Duplikate (drop_duplicates) vor der Aufteilung. Hier macht reset_index() aus customer_id eine Spalte, damit die Wiederholungen desselben Kunden entfernt werden und nicht zwei verschiedene Kunden mit denselben Zahlen.
Genauer, für Neugierige
Manchmal hat ein Kunde mehrere echte Zeilen, zum Beispiel eine pro Bestellung. Es gibt keine Duplikate, aber wenn seine Zeilen sowohl in den Trainings- als auch in den Testdaten liegen, lernt das Modell wieder den Kunden, nicht die Regel. Dann teilst du nach Kunde: GroupShuffleSplit(n_splits=1, test_size=0.25, random_state=42).split(X, y, groups=rows['customer_id']) legt alle Zeilen eines Kunden auf dieselbe Seite.
Die Idee
Zufällige Tage aus einer Zeitreihe
In der Lektion über Regression hast du nach der Zeit geteilt. Hier siehst du, warum eine zufällige Aufteilung lügt. Die Vorhersage gilt den Bestellungen pro Tag, aus der Nummer des Tages und dem Wochentag. Das Modell ist ein Random Forest (RandomForestRegressor): viele Bäume, jeder mit etwas anderen Zeilen trainiert, und die Vorhersage ist ihr Durchschnitt.
Mit zufälligen Tagen liegt der Wald im Schnitt um 4,88 Bestellungen pro Tag daneben, und der Mittelwert (DummyRegressor) um 5,76: Der Wald sieht besser aus. Bis zum 31. Juli trainiert und an August bis Oktober ausprobiert, liegt der Wald um 7,96 daneben und der Mittelwert um 5,44. Der Wald ist schlechter als der Mittelwert.
Bei einer zufälligen Aufteilung hat der Wald, um den 15. September richtig zu treffen, den 14. und den 16. September gesehen. Im echten Leben hast du am 31. Juli keinen einzigen Tag des Septembers. Außerdem setzen Bäume einen Trend nicht fort: Ein Tag nach dem letzten bekannten ist für sie wie die letzten bekannten Tage. Und August bis Oktober hat noch eine Überraschung: Der September ist schwächer. Warum, finden wir im Projekt heraus. Ein bis Juli trainiertes Modell kann das nicht wissen, und du am 31. Juli auch nicht. Deshalb ist 7,96 die ehrliche Zahl.
Übung
Repariere das Modell des Kollegen
Repariere den Code des Kollegen. Die Merkmale stammen nur aus den Bestellungen vor cutoff, und days_since wird bis cutoff gezählt. Der Scaler und die logistische Regression stecken in pipe, nur mit den Trainingsdaten trainiert. Lege in score die Genauigkeit von pipe auf den Testdaten ab.
Lösung
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
orders = pd.read_csv('year_orders.csv', parse_dates=['ordered_at'])
items = pd.read_csv('year_items.csv')
items['value'] = items['quantity'] * items['unit_price']
orders['basket'] = orders['id'].map(items.groupby('order_id')['value'].sum())
cutoff = pd.Timestamp('2026-07-01')
before = orders[orders['ordered_at'] < cutoff]
after = orders[orders['ordered_at'] >= cutoff]
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import make_pipeline
people = before.groupby('customer_id').agg(
orders=('id', 'size'), last=('ordered_at', 'max'), basket=('basket', 'mean'))
people['days_since'] = (cutoff - people['last']).dt.days
people['lost'] = (~people.index.isin(after['customer_id'])).astype(int)
X = people[['orders', 'days_since', 'basket']]
y = people['lost']
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.25, random_state=42, stratify=y)
pipe = make_pipeline(StandardScaler(), LogisticRegression())
pipe.fit(X_train, y_train)
score = pipe.score(X_test, y_test)
print(round(score, 3))Die ehrliche Genauigkeit ist 72,2 % gegen 99,7 % mit der Zukunft. Es gibt zwei Korrekturen. Erstens hören die Merkmale am 1. Juli auf: before statt orders und cutoff statt today. Zweitens lernt der Scaler aus den 959 Kunden der Trainingsdaten, nicht aus allen 1.279: pipe.fit(X_train, y_train), nicht pipe.fit(X, y).
Der Kollege hat auch ein Modell für den Umsatz pro Tag, ohne die stornierten Bestellungen. Probiere es ehrlich aus. Trainiere den Random Forest (n_estimators=50, random_state=42) und einen DummyRegressor mit den Tagen bis zum 31. Juli (late ist die Maske der Tage ab dem 1. August). Lege dann in mae den Fehler (MAE) des Waldes an August bis Oktober ab und in base_mae den Fehler des Mittelwerts.
Lösung
import pandas as pd
from sklearn.ensemble import RandomForestRegressor
from sklearn.dummy import DummyRegressor
from sklearn.metrics import mean_absolute_error
orders = pd.read_csv('year_orders.csv', parse_dates=['ordered_at'])
items = pd.read_csv('year_items.csv')
items['value'] = items['quantity'] * items['unit_price']
orders['basket'] = orders['id'].map(items.groupby('order_id')['value'].sum())
orders = orders[orders['status'] != 'cancelled']
daily = orders.set_index('ordered_at').resample('D')['basket'].sum().rename('revenue').to_frame()
daily['day'] = range(len(daily))
daily['weekday'] = daily.index.dayofweek
X = daily[['day', 'weekday']]
y = daily['revenue']
late = daily.index >= '2026-08-01'
forest = RandomForestRegressor(n_estimators=50, random_state=42).fit(X[~late], y[~late])
base = DummyRegressor().fit(X[~late], y[~late])
mae = mean_absolute_error(y[late], forest.predict(X[late]))
base_mae = mean_absolute_error(y[late], base.predict(X[late]))
print(round(mae, 2), round(base_mae, 2))Der Wald liegt im Schnitt um 128,81 € pro Tag daneben und der Mittelwert der Tage bis Juli um 65,24 €: Der Wald liegt etwa doppelt so weit daneben wie der Mittelwert. Bei einer zufälligen Aufteilung ergibt derselbe Wald 65,12 € und sieht gut aus. Trainierst du ihn mit allen Tagen, sinkt der Fehler auf 23,96 €, weil der Wald die Testdaten gesehen hat. Der ehrliche Schluss: Mit der Nummer des Tages und dem Wochentag taugt der Wald nicht für eine Vorhersage nach vorn.
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Die Daten beschreiben · Modelle: aus Beispielen lernen · Lineare Regression · Klassifikation
- Geht weiter in: Kreuzvalidierung
- Aufgaben zum Thema: Modelle mit scikit-learn