Failure Modes/Chapter 20 of 35

Data leakage & dataset mixing

4 min readEdit on GitHub

The classical ML failure mode, amplified. Agents refactor data pipelines five times in an afternoon and lose the split each time.

The taxonomy

TypeMechanismDetection
Train/test contaminationTest rows present in trainHash + intersect IDs
Lookahead leakageFuture information used to predict pastTime-aware split, walk-forward
Group leakageSame entity in train and test (different rows)GroupKFold; explicit group check
Target leakageFeature is a downstream consequence of the labelDomain knowledge + feature-importance audit
Preprocessing leakageScaler/encoder fit on full data before splitPipelines, fit-on-train-only assertion
Eval-set tuningTest set seen during HP search → no clean testThree-way split: train / val / test
Cross-split mixingTwo datasets merged with overlapping rowsProvenance column + dedup audit

Every one of these has shipped in production at least once. They are catchable.

Detection recipes

Hash-based intersection check

The simplest, cheapest check. Run it after every data refactor.

import hashlib, pandas as pd

def row_hash(row) -> str:
    return hashlib.sha256(
        "|".join(str(row[c]) for c in sorted(row.index)).encode()
    ).hexdigest()

train_hashes = set(train.apply(row_hash, axis=1))
test_hashes = set(test.apply(row_hash, axis=1))
overlap = train_hashes & test_hashes
assert not overlap, f"{len(overlap)} rows leak from train into test"

Wire this into your data-loading code path. Crash on overlap. Never warn. A warning the agent ignores is no protection.

Group leakage check

If your data has a natural unit (user, document, patient), assert no overlap:

train_groups = set(train["group_id"])
test_groups = set(test["group_id"])
assert not (train_groups & test_groups), "group leakage"

Time leakage check

assert train["timestamp"].max() < test["timestamp"].min(), \
 "time leakage: train extends past test start"

Preprocessing leakage check

The cleanest defense: use sklearn Pipeline (or equivalent) so encoders and scalers fit only on training data. If you must compute statistics outside a pipeline, assert provenance:

def fit_encoder(df: pd.DataFrame, split: str) -> Encoder:
 assert split == "train", f"encoder must fit on train, got {split}"
...

How the agent introduces leakage

Common patterns when refactoring an existing pipeline:

  • "Cleaning up" by computing aggregate features over the whole dataset before splitting.
  • Adding a "convenient" df = pd.concat([train, test]) to compute encodings, then forgetting to undo it.
  • Replacing GroupKFold with KFold to "simplify."
  • Loading a "cleaner" dataset version that includes deduped rows that were in the original test set.

Code review catches these only if you know what to look for. Make the leakage checks part of the test suite, not an inspection task.

Dataset mixing

A specific form of leakage: combining datasets that share rows or share the generation process of the test set.

Example: you fine-tune on a "high-quality" web crawl that happens to include the eval benchmark's source pages. Your eval number jumps. You announce state-of-the-art. A reviewer notices the contamination. You retract.

This has happened to public papers and shipped models more than once. The defense:

  1. Provenance column on every dataset. Every row knows which source it came from.
  2. Eval contamination check. For text, n-gram or substring overlap between training corpus and eval set. For images, perceptual hash. Run it; report the contamination rate; subtract it from your training corpus before fine-tuning.
  3. Cite the contamination check in your model card. "We removed N% of training documents that overlapped with eval."

Without this, you do not know whether your model is actually capable or whether you accidentally trained on the test.

When leakage is "fine"

Almost never. The exception: deliberately leaking for diagnostic purposes (e.g., upper-bound experiments where you train on test to see what's achievable). In those cases:

  • Mark the run loudly: runs/2026-05-04-LEAKED-upper-bound/.
  • Never compare to non-leaked runs as if they were equivalent.
  • Delete the artifacts after analysis to prevent accidental promotion.

Encode it in your CLAUDE.md

Add explicit hard rules:

## Data integrity (hard rules)

- All splits enforce GroupKFold by `<your_group_col>`.
- All preprocessing fits on training fold only; assertions enforce this.
- The leakage check in tests/data/test_no_leakage.py runs in CI; never skip.
- Fine-tuning corpora go through scripts/contamination_check.py against the eval set.
- A `provenance` column is required on every row of every assembled dataset.

The agent reads this on every task. The leakage check runs automatically. You catch problems before they become headline numbers.