Data leakage, groups and time series CV

scikit-learn · Advanced · 12 min

Leakage is the most common reason a model that scored well in development fails in production.

Data leakage means information that would not be available at prediction time reaches training or validation. Scores look great; production does not. Common sources:

  • preprocessing fitted on all data before splitting (fixed by pipelines),
  • features computed from the future or from the target, such as "account closed date" when predicting churn,
  • duplicate or related rows in both train and validation.

The last one is about the splitter. Random K-fold assumes rows are independent. When they are not, pick a splitter that mirrors how the model will be used:

  • Groups: several rows per patient, customer or device. GroupKFold(n_splits) with groups= passed to cross_val_score keeps each group in one fold, so you measure performance on unseen customers. StratifiedGroupKFold also balances classes.
  • Time: TimeSeriesSplit(n_splits, gap=...) always validates on data after the training window. gap leaves a buffer between train and test when features use lagged windows.

A quick leakage check: if one feature alone gives near-perfect predictions, or if the CV score is far above what domain experts consider plausible, investigate before celebrating. Also compare random-CV and group/time-CV scores; a large drop means random CV was overly optimistic.

Example

import numpy as np
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import GroupKFold, KFold, TimeSeriesSplit, cross_val_score

rng = np.random.default_rng(0)
groups = np.repeat(np.arange(50), 10)          # 50 patients, 10 rows each
patient_effect = rng.normal(size=(50, 2))[groups]  # rows of a patient look alike
X = patient_effect + rng.normal(0, 0.1, (500, 2))
y = (rng.normal(size=50)[groups] > 0).astype(int)  # label is per patient, unrelated to X

rf = RandomForestClassifier(random_state=0)
print(cross_val_score(rf, X, y, cv=KFold(5, shuffle=True, random_state=0)).mean().round(2))  # 0.86 (leak!)
print(cross_val_score(rf, X, y, cv=GroupKFold(5), groups=groups).mean().round(2))           # 0.58

for tr, te in TimeSeriesSplit(n_splits=3, gap=1).split(np.arange(10)):
    print(tr, te)
# [0 1 2] [4 5]
# [0 1 2 3 4] [6 7]
# [0 1 2 3 4 5 6] [8 9]   (validation always after training, gap of 1)

Remember

  • Pick the splitter that mirrors deployment: groups for entities, time order for forecasts
  • Pass groups= to cross-validation functions when using group splitters
  • Implausibly high scores are a leakage signal

Watch out for

  • Shuffling time-ordered data in K-fold, which trains on the future.

Check yourself

  1. You predict outcomes for new hospital patients; each patient has many visits in the data. Which CV is appropriate?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: scikit-learn.org/stable/common_pitfalls.html