Data leakage & dataset mixing
The classical ML failure mode, amplified. Agents refactor data pipelines five times in an afternoon and lose the split each time.
The taxonomy
| Type | Mechanism | Detection |
|---|---|---|
| Train/test contamination | Test rows present in train | Hash + intersect IDs |
| Lookahead leakage | Future information used to predict past | Time-aware split, walk-forward |
| Group leakage | Same entity in train and test (different rows) | GroupKFold; explicit group check |
| Target leakage | Feature is a downstream consequence of the label | Domain knowledge + feature-importance audit |
| Preprocessing leakage | Scaler/encoder fit on full data before split | Pipelines, fit-on-train-only assertion |
| Eval-set tuning | Test set seen during HP search → no clean test | Three-way split: train / val / test |
| Cross-split mixing | Two datasets merged with overlapping rows | Provenance 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
GroupKFoldwithKFoldto "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:
- Provenance column on every dataset. Every row knows which source it came from.
- 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.
- 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.