Zum Inhalt
wannacode

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:

PythonDaten: die Tabellen des Shops

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:

PythonDaten: die Tabellen des Shops

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:

PythonDaten: die Tabellen des Shops

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 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]
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
  1. orders sind alle Bestellungen des Jahres, bis zum 31. Oktober 2026. Die Punkte sind die Bestellungen von vier Kunden, nach Monaten.
  2. cutoff ist der 1. Juli. Links von der Wand liegt before: was du weißt. Rechts liegt after: was du vorhersagst.
  3. lost ist 1, wenn der Kunde rechts keine einzige Bestellung hat. Das Label entsteht aus den Punkten nach dem 1. Juli.
  4. 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.
  5. Die Aufteilung: 959 Kunden zum Trainieren und 320 zum Testen, von insgesamt 1.279.
  6. Mit der Zukunft in den Merkmalen beträgt die Genauigkeit 99,7%. Die Baseline „alle kommen wieder“ liegt bei 72,5%.
  7. Ehrlich: Die Merkmale enden am 1. Juli. Die Punkte rechts werden nicht gezählt, und days_since wird ab der Wand gemessen.
  8. 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

1. Am 1. Juli entscheidest du, wer einen Gutschein bekommt. Welches Merkmal ist für das Modell ehrlich?

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.

PythonDaten: die Tabellen des Shops

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:

PythonDaten: die Tabellen des Shops

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) und pipe.predict skalieren die Testdaten mit den gelernten Mittelwerten und fragen das Modell.
PythonDaten: die Tabellen des Shops

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.

1. Was macht pipe.score(X_test, y_test), wenn pipe = make_pipeline(StandardScaler(), LogisticRegression()) mit den Trainingsdaten trainiert wurde?
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:

PythonDaten: die Tabellen des Shops

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.

PythonDaten: die Tabellen des Shops

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.

PythonDaten: die Tabellen des Shops

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.

PythonDaten: die Tabellen des Shops

Test

Teste dich

1. Ein Modell für „kommt der Kunde wieder“ erreicht 99 % Genauigkeit, die Baseline liegt bei 72,5 %. Was tust du zuerst?
2. Du füllst die fehlenden Werte einer Spalte mit ihrem Mittelwert. Aus welchen Zeilen berechnest du den Mittelwert?
3. Nach drop_duplicates fällt die Genauigkeit des Baums von 89,4 % auf 58,4 %. Welche Zahl stimmt?
4. Du sagst die Bestellungen pro Tag für die nächsten drei Monate voraus. Wie teilst du die Daten?
5. Warum steckst du den Scaler und das Modell in eine Pipeline, statt die Tabelle am Anfang einmal zu skalieren?

Erstellt von Ivo Gergov im Rahmen der Kampagne von Evo Tech für zugängliche Programmierausbildung ·