ml-models / Data and evaluation / Data leakage core

Data leakage

Leakage is any route by which information the model will not have at prediction time reaches the model during training or evaluation. The result is a score that looks excellent and a deployed model that does not. This page covers the three families (target leakage, train–test contamination and temporal leakage), shows each with numbers, and ends with the structural fix (do every learned step inside a Pipeline) and a checklist.

On this page
  1. Problem
  2. Intuition
  3. Mechanics
  4. Target leakage
  5. Train–test contamination
  6. Temporal leakage
  7. The structural fix: one pipeline
  8. Worked example
  9. Pure noise that "predicts" the label
  10. Duplicates across the split
  11. A feature from after the outcome
  12. Scaling on all rows
  13. Implementation
  14. A leakage checklist
  15. Tradeoffs
  16. Check yourself
  17. Connections
  18. Read first
  19. Read next
  20. References

Problem

A model is only useful if it works on rows it has not seen, at a moment when the answer is not yet known. We estimate that by holding rows back and scoring on them. Leakage breaks the estimate: something about the held-back rows, or about the future, or about the answer itself, got into the model. The score then measures how well the model uses a shortcut, not how well it predicts.

The danger is that leakage rarely fails loudly. Nothing crashes; the validation number is simply too good. The worse a leak is, the more the result looks like a breakthrough. A very high score on a hard problem is the first thing to distrust.

Intuition

Imagine a student sitting an exam. If they saw the questions beforehand, the mark tells you nothing about what they know. There are three ways that can happen. They may have been handed a column that is secretly the answer key (target leakage). They may have seen the exam paper itself while studying, because the paper and the practice sheets overlapped (train–test contamination). Or they may have read tomorrow's newspaper to predict tomorrow's news (temporal leakage).

The test for all three is the same question: at the moment this prediction would be made in real use, would I have this piece of information? If not, it cannot be in the training recipe, the features or the evaluation, whether it arrived through a column, a row or a statistic.

Three routes by which information leaks into a model Three rows. Target leakage: a feature column that is computed from or after the label flows into the model. Train–test contamination: a statistic or a duplicated row crosses from the test set into training. Temporal leakage: data from after the prediction time flows backward into a model trained to predict that time. feature built from the label model target leakage test rows (statistics, copies) model contamination rows from after the cutoff model temporal leakage

Different doors, one problem: the model sees something it would not have in production.

Mechanics

Target leakage

A feature is available in your table but is created by, or only exists because of, the outcome. Predicting loan default with a column "number of late-payment notices sent" is the classic case: notices are only sent after someone has missed payments. Others: "discharge medication" in a model for diagnosing the condition; "refund issued" when predicting churn; a column that is a transformation of the label (a rescaled copy, a category that the label defines). Tell-tale signs are a single feature that alone predicts nearly perfectly, and importance that is wildly concentrated.

Train–test contamination

Information flows from evaluation rows into training without a feature naming the label:

  • Preprocessing fit on everything. Computing a mean, a scale, a vocabulary, an imputation value, a PCA basis or a target encoding on all rows before the split lets the test rows influence what the model is given.
  • Selection fit on everything. Choosing the "best" features, hyperparameters or model by looking at the test score makes the test score a training objective.
  • Duplicates and near-duplicates. The same record, image, document or user appears on both sides. The model gets credit for remembering it.
  • Groups split apart. Several rows from one patient, device or session are spread over both sides, so the model can recognize the entity, not the pattern. This is why splitters such as GroupKFold exist.

Temporal leakage

When rows are ordered in time, a random split lets the model train on the future and be scored on the past. Even features can leak time: "total purchases this year" computed at year end contains purchases that had not happened on the prediction date. The fix is to split by time (train strictly before validation strictly before test) and to build every feature using only data stamped before the prediction time, including the aggregates. TimeSeriesSplit produces such splits.

The structural fix: one pipeline

Most contamination comes from the order of operations. The safe order is: split first; then, for each training set, fit every learned step (scaler, imputer, selector, encoder, model) on those rows only; finally transform the held-out rows with the fitted steps. A Pipeline enforces that order: its fit clones each transformer, calls fit_transform on the training data one step at a time, then fits the final estimator, and its predict only calls transform on new rows. Cross-validating the whole pipeline refits all of it inside each fold, so no fold's validation rows ever influence a fitted step.

Wrong order versus right order Top row, the wrong order: all rows flow into one fit step, and only afterwards are they split into train and test, so the test rows influenced the fit. Bottom row, the right order: the rows are split first, the fit step sees only the train rows, and the fitted result is applied to the test rows. wrong all rows fit scaler / select split train test right all rows split train fit on train only test transform only

Splitting first costs nothing; fitting before splitting can silently inflate every number that follows.

ComponentShape/ParamsMeaningSource
Pipeline steps: list of (name, estimator); memory, verbose Chains transformers and a final estimator so that fitted state is learned only from the rows passed to fit. pipeline.py
Pipeline._fit X, y; loops over every step but the last Clones each transformer, calls fit_transform on the running training data and stores the fitted copy. pipeline.py
Pipeline.fit X: training rows, y: labels Runs _fit for the transformers, then fits the final estimator on the transformed training data. pipeline.py
Pipeline.predict X: new rows Calls transform (never fit) on each transformer, then the final estimator's predict. pipeline.py
train_test_split test_size, shuffle=True, stratify, random_state A random split; wrong for grouped or time-ordered data, which need the splitters below. _split.py
GroupKFold n_splits=5, groups passed to split Keeps every group entirely in the test fold or entirely out of it, so an entity cannot straddle the split. _split.py
TimeSeriesSplit n_splits=5, gap, max_train_size Each fold trains on earlier rows and tests on later ones, never the reverse. _split.py

Worked example

Pure noise that "predicts" the label

Take 40 rows, a random 0/1 label, and 500 features that are also pure coin flips, so no feature has any real relationship to the label. Split the rows into the first 20 for training and the last 20 for test. Leaky: pick the feature that agrees with the label most often over all 40 rows, then report its accuracy on the 20 test rows. In one run (seed 3) that feature, number 88, scores 75.0% on test. Honest: pick the best feature using the 20 training rows only. Its training accuracy is 90.0%, the best of 500 coin flips by luck, and on the test rows it scores 25.0%.

One run is noisy, so repeat with 100 different seeds: the leaky procedure averages 72.9% on test, the honest one 47.9%, which is what no signal means (a coin). The honest training accuracy averages 82.7%, so selection also inflated the training score; only the held-out rows told the truth. The leaky number is entirely an artefact of letting the test labels vote on the selection.

Duplicates across the split

The training set has 8 records and the test set has 10, of which 4 also appear in training. A "model" that only memorizes training records (and guesses the training majority label, 0, for anything unseen) scores 70.0% on the full test set: 4 copies right by memory plus 3 of the 6 fresh records right by the majority guess. Drop the 4 duplicates from the test set and it scores 50.0%, because the memorizer knows nothing that transfers. The 20 points in between are pure leakage, and the gap grows with the share of duplicates.

A feature from after the outcome

Ten loans, four of them defaulted. The number of late notices is 3, 0, 0, 2, 0, 4, 0, 0, 1, 0 for the loans in order; the loan defaulted whenever the count is above zero. The best threshold rule (> 0.5) is right on 100.0% of loans. The debt ratio, which is known at application time, has a best single threshold rule (> 0.365) that is right on 60.0%. A model trained with the notices column would look nearly perfect and be useless at the moment it matters, because at application time the notices do not exist yet.

Scaling on all rows

The preprocessing page shows the same test age standardized to different values depending on whether the mean and standard deviation came from the four training rows or all five. That difference is information from the test row flowing into the number the model receives; with a statistic as small as a mean it is subtle, with a selection step or a target encoding it can be large, as the first example shows.

Implementation

from sklearn.feature_selection import SelectKBest, f_classif
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import GroupKFold, TimeSeriesSplit, cross_val_score
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler

# wrong: the selector and scaler have already seen every row
X_sel = SelectKBest(f_classif, k=10).fit_transform(X, y)
cross_val_score(LogisticRegression(), X_sel, y, cv=5)

# right: every learned step is refit inside each training fold
model = Pipeline([
    ("scale", StandardScaler()),
    ("select", SelectKBest(f_classif, k=10)),
    ("clf", LogisticRegression()),
])
cross_val_score(model, X, y, cv=5)

# grouped rows: an entity never straddles the split
cross_val_score(model, X, y, cv=GroupKFold(n_splits=5), groups=patient_id)

# time-ordered rows: train on the past, validate on the future
cross_val_score(model, X, y, cv=TimeSeriesSplit(n_splits=5))

A leakage checklist

  • For every feature, can you say when it is known? If it is known only after the outcome, remove it.
  • Is the split done before any statistic, selection, imputation, encoding or resampling is fit? Is the whole recipe inside a Pipeline?
  • Are there duplicate or near-duplicate records, or several rows per entity, spread across the splits? Split by entity.
  • Is the data ordered in time? Split by time and build aggregates with a cutoff.
  • Was the test set used once, at the end? Tuning on it makes it a validation set.
  • Is the score suspiciously good, or is one feature carrying nearly all the importance? Treat that as a bug until shown otherwise.
  • Does the deployed score on fresh data match the offline estimate? A large gap is the final symptom.

Tradeoffs

  • Pipelines versus convenience. A pipeline refits every step per fold, which costs time for expensive preprocessing (a large PCA, a heavy encoder). The cost buys an honest estimate; caching with memory reduces it.
  • Unsupervised preprocessing on all data. Fitting a scaler or vocabulary on unlabeled test inputs is sometimes done on purpose (transductive learning), but then the reported number is for that setting, not for new data. Say so, or avoid it.
  • Gap between train and validation. With time series, features such as rolling windows reach back; leave a gap equal to the longest lookback so windows do not straddle the boundary.
  • Near-duplicates are hard to define. Exact-match checks catch the easy cases; images, paraphrased text and the same user with different ids need hashing, embeddings or domain keys. Over-aggressive deduplication discards real data.
  • Leakage in the loop. Repeated peeking at one test set (many experiments, one benchmark) leaks slowly through your own decisions. Keep a final untouched set, and expect public leaderboards to be optimistic.
  • Pretraining contamination. A large language model may have seen a benchmark's test items in its training corpus. The same logic applies, and the evidence is usually indirect.

Check yourself

You impute missing values with the column median over the whole dataset and then cross-validate. Is that leakage?

Yes, a mild form: each validation fold's values contributed to the median used to fill the training fold. Put the imputer in a pipeline so it is refit inside each training fold.

A model's validation accuracy is 99.9% on a problem humans find hard. What do you check first?

Which feature carries the signal, and whether it is available at prediction time; then duplicates across the split; then whether any learned step was fit before the split.

Why does the honest procedure's training accuracy look so good on pure noise?

Choosing the best of 500 coin-flip features on 20 rows guarantees some feature agrees by chance. The selection overfits the training rows, and only rows that did not take part in the selection reveal it.

Connections

Read first

Read next

No page builds on this one yet.

References

  • Kaufman, Rosset, Perlich and Stitelman. Leakage in Data Mining: Formulation, Detection, and Avoidance. ACM Transactions on Knowledge Discovery from Data, 2012.
  • Hastie, Tibshirani and Friedman. The Elements of Statistical Learning, 2nd ed., section 7.10.2 ("The wrong and right way to do cross-validation"). Springer, 2009.
  • Kapoor and Narayanan. Leakage and the Reproducibility Crisis in Machine-Learning-Based Science. Patterns, 2023.
  • scikit-learn 1.9.1: Pipeline, GroupKFold, TimeSeriesSplit, train_test_split.