Data Leakage Hunting
Every other defect makes something fail. This one makes everything look
better — which means the normal feedback loop of engineering runs in reverse.
A leak is discovered by celebration, adopted into the pipeline, published in
the README, and found six months later by production.
So the alarm has to be inverted, and it is the first rule of this skill:
An unexpectedly good result is a bug report until proven otherwise.
The invariant being violated is always the same one, and it is worth writing on
the wall:
No information derived from the evaluation data may influence anything used
to produce the prediction. Established at the split (T0), it must still hold
at fit time, at feature-construction time, at tuning time, and at the moment
the number goes in the report (Tn).
That is invariant-hunting's shape exactly: a property established in one place
that has to survive a journey. The difference is that here the violation
rewards you, so nobody goes looking.
Composes with the library:
- invariant-hunting — the T0→Tn frame; this is its ML instance
- model-evaluation-discipline — the number the leak corrupts, and the
baselines that make a leak visible
- training-run-provenance — the split must be an artifact, not a seed
- discriminating-proof — the falsification experiments in Part 3
- honest-degradation — a leaked metric is the archetype of a plausible,
confident, wrong answer
- daubert-defensible-writing — how to report that every prior number is void
Part 1 — Write the split contract before you model
Most leaks are decided before a single model is trained, by three questions
nobody wrote down. Answer them in the repo, not in your head:
- What is the unit of generalization? The thing the model must work on
next, that it has never seen: a new user, a new patient, a new document, a
new day, a new hospital. If the unit is a user and you split on rows, the
evaluation is measuring memorization of users it already knows.
- What is the time boundary? If the data has a time dimension and the
model will be used to predict forward, then every evaluation row must be
strictly after every training row. A random split on time-ordered data lets
the model interpolate a future it will never have.
- What defines a group? Same entity, same session, same source document,
near-duplicate image, augmented copies of one original, retried request. All
copies of one group belong on one side of the split.
Then store the split as data — the actual ids or indices, hashed and
versioned — not as the seed that generated it. A seed is not a split: change a
library version and the same seed produces a different partition, and every
comparison you made becomes meaningless (training-run-provenance).
Part 2 — The eight families
Name the family before testing. Each has a distinct habitat and a distinct
falsifier.
- Preprocessing fitted before the split. A scaler, imputer, encoder,
vocabulary, PCA, feature selector, or normalization statistic computed over
the full dataset. The single most common leak in existence, and it is one
line of code order. The fix is structural: the entire transformation chain
lives inside a pipeline object that is fitted on the training fold only —
and inside each fold in cross-validation, not once around it.
- Temporal leakage. A random split on time-series data; a feature whose
aggregation window extends past the prediction timestamp; a join that
attaches a value known only later; a feature store queried "as of now"
instead of "as of the event". Anything computed with
groupby().mean() over
the whole table is a candidate.
- Group leakage. The same entity in both splits — patient, user, device,
session, article, or several images from one photo series. The score
measures recall of the entity, not generalization to a new one.
- Target leakage in a feature. A column that is a consequence, proxy, or
encoding of the label:
account_closed_date for churn, n_treatments for
diagnosis, a field only populated after the outcome, or an id that
correlates with the class because of the order in which data was collected.
The tell is a single feature with implausible importance.
- Duplicates and near-duplicates across splits. Web-scraped corpora,
augmented copies, retried records, boilerplate. Count them; do not assume.
- Tuning on the test set. The slowest leak and the hardest to see: each
experiment that peeks transfers a few bits of the test set into your choices.
Threshold selection, early stopping, feature selection, architecture search,
and "I tried it and it was worse" are all peeks. After a hundred experiments
the test set is a training signal.
- Label-process leakage. The annotator saw a model prediction; labels were
derived from a source that is also a feature; two labellers' agreement was
used to drop the hard examples. The test set is now easier than reality.
- Pretraining contamination. The benchmark exists in the pretrained model's
corpus. Standard for any public dataset and any foundation model — and it
makes a public benchmark an upper bound, not a measurement.
Part 3 — Falsify, do not eyeball
Suspicion is cheap. These experiments are the discriminating kind — each one
predicts a specific result, and the wrong result is the finding
(discriminating-proof).
| Experiment |
If the leak is absent |
If it is present |
| Shuffle the labels, retrain |
score falls to the baseline |
above chance ⇒ leakage or a pipeline bug, with no exceptions |
| Re-split by group instead of by row |
score changes little |
large drop ⇒ group leakage |
| Re-split by time, train past → test future |
score changes little |
large drop ⇒ temporal leakage |
| Drop the dominant feature |
score degrades gracefully |
collapse ⇒ that feature carries the label |
| Hash / near-dup match across splits |
near-zero overlap |
any overlap is a defect, quantified |
| Train on 10% of the data |
clearly worse |
as good ⇒ the task is being solved by something other than learning |
| Evaluate on a freshly collected period |
within the interval |
drop ⇒ the offline set no longer represents reality |
The label shuffle deserves its own line, because it is the negative control this
whole field is missing: a model trained on randomized labels must not beat the
baseline. If it does, stop everything — no result from that pipeline means
anything until it is explained.
Run these before you report a good number, not after someone doubts it.
Part 4 — When you find one, the numbers are void
This is where the reporting discipline matters, because the temptation is to
fix the leak, rerun, and quietly replace the number.
- Every metric produced by the leaking pipeline is void, not adjusted. They
do not get corrected downward; they get withdrawn. Any decision made on them
(this architecture beat that one, this feature helped) is unsupported and has
to be re-run.
- Say it explicitly wherever the old number lives: README, dashboard,
paper, ticket, slide.
claim-provenance-discipline — a number that traveled
keeps traveling until someone stops it by name.
- Fix it structurally. "We removed the ID column" is a point fix for one
instance. The class fix is a pipeline that cannot express the leak: the
transform chain fitted inside the fold, splits materialized as versioned
artifacts, a feature store with point-in-time correctness, and a CI check that
fails on cross-split id overlap.
- Add the falsifier as a permanent test. The label-shuffle control and the
cross-split-overlap check belong in CI. They cost minutes and they are the
only things that catch the next leak the day it is introduced.
Deliverable
## Leakage review — <dataset> / <pipeline> @ <version>
Unit of generalization: <entity> Time boundary: <yes/no + cutoff>
Group key: <field> Split stored as: <artifact hash, not a seed>
### Families checked
family | checked how | result | evidence
### Falsification runs
shuffled-labels: <score vs baseline> 10%-data: <score>
group-split: <delta> time-split: <delta> cross-split duplicates: <n>
### Findings
<id> — family — the leaking path — which reported numbers are now void
### Structural fixes + CI controls added
### Not checked
Anti-patterns
- Celebrating the jump instead of investigating it.
- Fitting the scaler, encoder, or vocabulary before the split — or once around
a cross-validation loop instead of inside each fold.
- Storing the split seed rather than the split.
- Random splits on data with a time dimension or a repeating entity.
- Treating cross-validation as protection against leakage; it multiplies
preprocessing leaks rather than preventing them.
- Reporting the improved number after fixing the leak, and leaving the old one
in the README as if it had merely been superseded.
- Removing one leaking column and declaring the class closed.
- Never running the shuffled-label control, so the pipeline has never once been
proven capable of producing a bad score.
- Reusing a public benchmark with a foundation model and reporting the result
as a measurement rather than as an upper bound.
1---2name: data-leakage-hunting3description: Data Leakage Hunting4---56# Data Leakage Hunting78Every other defect makes something fail. This one makes everything look9better — which means the normal feedback loop of engineering runs in reverse.10A leak is discovered by celebration, adopted into the pipeline, published in11the README, and found six months later by production.1213So the alarm has to be inverted, and it is the first rule of this skill:1415> **An unexpectedly good result is a bug report until proven otherwise.**1617The invariant being violated is always the same one, and it is worth writing on18the wall:1920> **No information derived from the evaluation data may influence anything used21> to produce the prediction.** Established at the split (T0), it must still hold22> at fit time, at feature-construction time, at tuning time, and at the moment23> the number goes in the report (Tn).2425That is `invariant-hunting`'s shape exactly: a property established in one place26that has to survive a journey. The difference is that here the violation27*rewards* you, so nobody goes looking.2829Composes with the library:3031- **invariant-hunting** — the T0→Tn frame; this is its ML instance32- **model-evaluation-discipline** — the number the leak corrupts, and the33 baselines that make a leak visible34- **training-run-provenance** — the split must be an artifact, not a seed35- **discriminating-proof** — the falsification experiments in Part 336- **honest-degradation** — a leaked metric is the archetype of a plausible,37 confident, wrong answer38- **daubert-defensible-writing** — how to report that every prior number is void3940---4142## Part 1 — Write the split contract before you model4344Most leaks are decided before a single model is trained, by three questions45nobody wrote down. Answer them in the repo, not in your head:46471. **What is the unit of generalization?** The thing the model must work on48 *next*, that it has never seen: a new user, a new patient, a new document, a49 new day, a new hospital. If the unit is a user and you split on rows, the50 evaluation is measuring memorization of users it already knows.512. **What is the time boundary?** If the data has a time dimension and the52 model will be used to predict forward, then every evaluation row must be53 strictly after every training row. A random split on time-ordered data lets54 the model interpolate a future it will never have.553. **What defines a group?** Same entity, same session, same source document,56 near-duplicate image, augmented copies of one original, retried request. All57 copies of one group belong on one side of the split.5859Then store the split **as data** — the actual ids or indices, hashed and60versioned — not as the seed that generated it. A seed is not a split: change a61library version and the same seed produces a different partition, and every62comparison you made becomes meaningless (`training-run-provenance`).6364## Part 2 — The eight families6566Name the family before testing. Each has a distinct habitat and a distinct67falsifier.68691. **Preprocessing fitted before the split.** A scaler, imputer, encoder,70 vocabulary, PCA, feature selector, or normalization statistic computed over71 the full dataset. The single most common leak in existence, and it is one72 line of code *order*. The fix is structural: the entire transformation chain73 lives inside a pipeline object that is fitted on the training fold only —74 and inside each fold in cross-validation, not once around it.752. **Temporal leakage.** A random split on time-series data; a feature whose76 aggregation window extends past the prediction timestamp; a join that77 attaches a value known only later; a feature store queried "as of now"78 instead of "as of the event". Anything computed with `groupby().mean()` over79 the whole table is a candidate.803. **Group leakage.** The same entity in both splits — patient, user, device,81 session, article, or several images from one photo series. The score82 measures recall of the entity, not generalization to a new one.834. **Target leakage in a feature.** A column that is a consequence, proxy, or84 encoding of the label: `account_closed_date` for churn, `n_treatments` for85 diagnosis, a field only populated after the outcome, or an id that86 correlates with the class because of the order in which data was collected.87 The tell is a single feature with implausible importance.885. **Duplicates and near-duplicates across splits.** Web-scraped corpora,89 augmented copies, retried records, boilerplate. Count them; do not assume.906. **Tuning on the test set.** The slowest leak and the hardest to see: each91 experiment that peeks transfers a few bits of the test set into your choices.92 Threshold selection, early stopping, feature selection, architecture search,93 and "I tried it and it was worse" are all peeks. After a hundred experiments94 the test set is a training signal.957. **Label-process leakage.** The annotator saw a model prediction; labels were96 derived from a source that is also a feature; two labellers' agreement was97 used to drop the hard examples. The test set is now easier than reality.988. **Pretraining contamination.** The benchmark exists in the pretrained model's99 corpus. Standard for any public dataset and any foundation model — and it100 makes a public benchmark an upper bound, not a measurement.101102## Part 3 — Falsify, do not eyeball103104Suspicion is cheap. These experiments are the discriminating kind — each one105predicts a specific result, and the wrong result is the finding106(`discriminating-proof`).107108| Experiment | If the leak is absent | If it is present |109|---|---|---|110| **Shuffle the labels**, retrain | score falls to the baseline | above chance ⇒ leakage or a pipeline bug, with no exceptions |111| **Re-split by group** instead of by row | score changes little | large drop ⇒ group leakage |112| **Re-split by time**, train past → test future | score changes little | large drop ⇒ temporal leakage |113| **Drop the dominant feature** | score degrades gracefully | collapse ⇒ that feature carries the label |114| **Hash / near-dup match across splits** | near-zero overlap | any overlap is a defect, quantified |115| **Train on 10% of the data** | clearly worse | as good ⇒ the task is being solved by something other than learning |116| **Evaluate on a freshly collected period** | within the interval | drop ⇒ the offline set no longer represents reality |117118The label shuffle deserves its own line, because it is the negative control this119whole field is missing: **a model trained on randomized labels must not beat the120baseline.** If it does, stop everything — no result from that pipeline means121anything until it is explained.122123Run these before you report a good number, not after someone doubts it.124125## Part 4 — When you find one, the numbers are void126127This is where the reporting discipline matters, because the temptation is to128fix the leak, rerun, and quietly replace the number.129130- **Every metric produced by the leaking pipeline is void, not adjusted.** They131 do not get corrected downward; they get withdrawn. Any decision made on them132 (this architecture beat that one, this feature helped) is unsupported and has133 to be re-run.134- **Say it explicitly** wherever the old number lives: README, dashboard,135 paper, ticket, slide. `claim-provenance-discipline` — a number that traveled136 keeps traveling until someone stops it by name.137- **Fix it structurally.** "We removed the ID column" is a point fix for one138 instance. The class fix is a pipeline that *cannot* express the leak: the139 transform chain fitted inside the fold, splits materialized as versioned140 artifacts, a feature store with point-in-time correctness, and a CI check that141 fails on cross-split id overlap.142- **Add the falsifier as a permanent test.** The label-shuffle control and the143 cross-split-overlap check belong in CI. They cost minutes and they are the144 only things that catch the next leak the day it is introduced.145146---147148## Deliverable149150```151## Leakage review — <dataset> / <pipeline> @ <version>152Unit of generalization: <entity> Time boundary: <yes/no + cutoff>153Group key: <field> Split stored as: <artifact hash, not a seed>154155### Families checked156family | checked how | result | evidence157158### Falsification runs159shuffled-labels: <score vs baseline> 10%-data: <score>160group-split: <delta> time-split: <delta> cross-split duplicates: <n>161162### Findings163<id> — family — the leaking path — which reported numbers are now void164165### Structural fixes + CI controls added166### Not checked167```168169## Anti-patterns170171- Celebrating the jump instead of investigating it.172- Fitting the scaler, encoder, or vocabulary before the split — or once around173 a cross-validation loop instead of inside each fold.174- Storing the split seed rather than the split.175- Random splits on data with a time dimension or a repeating entity.176- Treating cross-validation as protection against leakage; it multiplies177 preprocessing leaks rather than preventing them.178- Reporting the improved number after fixing the leak, and leaving the old one179 in the README as if it had merely been superseded.180- Removing one leaking column and declaring the class closed.181- Never running the shuffled-label control, so the pipeline has never once been182 proven capable of producing a bad score.183- Reusing a public benchmark with a foundation model and reporting the result184 as a measurement rather than as an upper bound.