2.1.13 · D3Data Preprocessing & Feature Engineering

Worked examples — Data leakage identification and prevention

3,978 words18 min readBack to topic

This page is the exhaustive drill. The parent topic note told you the three kinds of leakage and why they break a model. Here we hunt down every distinct shape that leakage can take — every "cell" — and work one example per cell until you have seen the whole landscape and can never be surprised again.

Before we start, two plain-words reminders so no symbol arrives unearned:


The scenario matrix

Every leakage bug is one of these cells. The examples below are labelled with the cell they cover, and together they touch all of them.

# Cell class The trap in one line Example
A Target leakage — direct A feature is literally computed from . Ex 1
B Target leakage — downstream Feature is caused by an event that causes. Ex 2
C Temporal — future window A rolling/aggregate feature peeks past time . Ex 3 (figure)
D Preprocessing contamination Scaler / imputer fit on train + test together. Ex 4
E Split contamination — duplicates/groups Same entity lands in both train and test. Ex 5
F CV contamination Leakage happening inside cross-validation folds. Ex 6
G Degenerate / zero case Feature is constant, or split leaves a fold empty — silent failure. Ex 7
H Limiting case Leak is tiny () or total ( perfect proxy) — what score do you expect? Ex 8
I Real-world word problem A messy business scenario mixing several cells. Ex 9
J Exam twist Looks leaky but is clean; or looks clean but leaks. Ex 10
K Selection / tuning contamination Feature selection or hyperparameter search run on the full dataset. Ex 11

Prerequisite links you may want open while reading: Train-testvalidation split strategies, Cross-validation techniques, Feature scaling and normalization, Time series forecasting fundamentals, Causal inference basics.


Cell A — Target leakage, direct

Steps.

  1. Ask the one rule: at prediction time (loan approval), does loss_amount_usd exist? Why this step? Legality is decided by timing, not by intuition. A loan that has not defaulted yet has no loss recorded.
  2. Notice the deterministic link: loss_amount_usd . So the feature is (recall = "a rule from one number to another") with . Why this step? This is the direct form: the feature is a function of the label itself, not just correlated with it.
  3. The model learns the trivial rule . With a perfect proxy present it copies it and scores near-perfectly on training data. Why this step? Any learner minimises empirical risk by copying a feature that already equals the answer; the ceiling is , and any real dataset with a whisker of noise lands just below it — say .
  4. In production loss_amount_usd is always (loan just approved), so the model always predicts . Accuracy collapses to the base rate of the majority class.

Verify: Suppose of loans default. Always-predict-repaid accuracy . So a training model that scored (essentially the ceiling minus noise) becomes a dummy — impressive-looking but learning nothing. The gap is the leakage tax made visible.


Cell B — Target leakage, downstream (caused by an event causes)

Steps.

  1. Draw the chain: . So the feature is downstream of . Why this step? Causal inference basics tells us: features on the effect side of the target are unavailable at the instant we must predict, because the effect hasn't happened yet.
  2. Timing test: at swipe time (prediction time ), has the customer called about this transaction? No — the transaction is happening now. Why this step? Same one rule. Downstream leakage is sneaky because the feature isn't literally , it's a consequence, so it survives a naive "is this the label?" check.
  3. Fix: only use features whose value is fixed by time amount, merchant_category, hour, historical_fraud_rate_of_merchant. Why this step? Tie it back to the one rule through causality: the causal arrow runs , so the call can only take its value after is decided. "After " means "after prediction time," and anything that only exists after prediction time cannot be computed at prediction time — hence it is illegal. Cause-side and contemporaneous features carry no such arrow from , so they stay legal.

Verify (sanity): Ask "could a fraudster's model compute this feature before the swipe?" num_customer_calls_this_week — no, the call is triggered by the fraud. Rejected. hour_of_day — yes, known instantly. Accepted. The test is decidable and consistent.


Cell C — Temporal leakage, future window

Figure — Data leakage identification and prevention

Figure, in words (so you can follow without seeing it): the horizontal axis is day relative to prediction time, labelled ; the vertical axis is the sales value. Each day is a dot. Blue dots ( to ) are known past. The red dot at is today — the value we are trying to predict — and a red dashed vertical line marks the "now" boundary through it. Orange dots ( to ) are the future, which does not exist yet. A grey double-headed arrow across the top spans and is labelled "centred window (peeks future!)" — notice it crosses the red line into orange territory. A separate green double-headed arrow near the bottom spans , labelled "trailing legal window" — it stays entirely left of the red line.

Steps.

  1. Look at where the grey (centred) arrow sits relative to the red "now" line: it crosses to the right, into the orange dots. Why this step? Temporal leakage is inherently geometric — it's about which side of the "now" line a data point sits on. Crossing the line = using the future.
  2. A centred window reaches into (orange). Those values do not exist yet at time . Why this step? The average literally contains future terms.
  3. Split the sum to expose the leak: Why this step? Making the illegal terms explicit shows exactly what to delete.
  4. Fix: use a trailing window that touches only the past (the green arrow). To keep the arithmetic honest below, we take the trailing window over the same left-shoulder days we can actually see, (i.e. history up to and including today). See Time series forecasting fundamentals.

Verify (numbers): Take sales for days . Centred 5-day mean . The legal trailing mean over exactly the three past-and-present days is . Both are 3-vs-5-day means computed on the same number series, so the comparison is apples-to-apples: they differ (), and the difference is purely the future's influence. That non-zero gap is the leak.


Cell D — Preprocessing contamination

Let = the total number of rows the scaler is fitted on, = training rows, = test rows, so .

Steps.

  1. The scaler computes and , then outputs . Why this step? and are learned parameters. Learning them from test rows lets test information touch training features.
  2. Fit-on-all mean mixes both groups: Why this step? It shows depends on test rows — the contamination in one line.
  3. Fix: scaler.fit(X_train) only, then scaler.transform(X_test). Even cleaner: put the scaler inside a pipeline so it can never accidentally see test data.

Verify (numbers): Train sqft (), test (), equal sizes. Fit-on-all mean . A legal train point scaled with the contaminated mean uses instead of — the numerator instead of the honest . Wrong by a factor of .


Cell E — Split contamination via duplicates / groups

Steps.

  1. Rows from one patient are near-duplicates (same physiology). Random split can put visit 1 in train and visit 2 in test. Why this step? The independence assumption behind Train-testvalidation split strategies is between entities, not rows.
  2. The model effectively memorises the patient during training and is graded on them in test — an unfair peek. Why this step? Memorising an entity then scoring on that same entity inflates the test score, exactly the "study with the exam answers" failure the parent note warns about.
  3. Fix: grouped split — every row of a patient goes entirely to one side (GroupShuffleSplit). Why this step? Forcing whole entities to one side restores true independence between train and test, so the test score measures generalisation to new patients.

Verify (count): With patients visits rows: under a row-level split, the chance that a given patient has all three of its rows land on the same side is , so the chance it is split across both sides is . Nearly half of all patients leak. A grouped split makes this probability exactly .


Cell F — Contamination inside cross-validation

Steps.

  1. In -fold CV, each fold takes turns being the validation slice. If scaling happened before the loop, every validation fold already influenced . Why this step? CV simulates deployment times; any pre-loop fitting quietly contaminates all simulations.
  2. Correct order: inside each fold, fit preprocessing on the training folds only, transform the held-out fold. A pipeline passed to cross_val_score does this automatically. Why this step? Re-fitting per fold means the held-out slice never touches the statistics used to transform it — each fold becomes an honest miniature of production.
  3. Consequence of getting it wrong: CV scores are optimistic, so your model looks better than it is. Why this step? Naming the concrete symptom (inflated CV score) tells you what to watch for — a CV score you cannot reproduce on truly fresh data is the fingerprint of this bug.

Verify (magnitude): With , each validation fold is of data. Pre-loop fitting means is computed from instead of the honest — every fold's held-out leaks into its own . Fraction of contaminated statistic per fold .


Cell G — Degenerate / zero case

Steps.

  1. (a) Constant feature. , so scaling divides by zero. Phenomenon: zero-variance no-op — the feature carries no information () and the transform is undefined. Not leakage: no external data enters training. Why this step? Separating the two phenomena is the whole point of this cell — a divide-by-zero is a numeric crash, categorically different from information flowing across the now-line.
  2. (b) Singleton class. With positive example, a stratified split cannot place it in both train and validation. Phenomenon: empty-fold instability — one side ends up with positives, making recall undefined. Not leakage: again, no future/test information reaches training; the metric is simply undefined. See Model evaluation metrics. Why this step? Distinguishing "undefined metric" from "inflated metric" matters: leakage inflates scores, degeneracy makes them undefined or crashing — opposite symptoms.
  3. Fix: drop zero-variance columns; for tiny classes, resample or report the metric's instability honestly rather than trusting one undefined number. Why this step? Removing information-free columns kills the divide-by-zero, and flagging singleton-class instability stops you from believing a metric.

Verify (numbers): Constant column : variance (transform undefined). Recall with true positives available: denominator undefined. Both are degenerate, neither is leakage.


Cell H — Limiting cases

Steps.

  1. (total leak): the noise vanishes, so is a perfect encoding of . Training accuracy ; the model is just reading the answer. Why this step? This is the upper limit — the worst, most flattering case, and it names the ceiling every leak approaches.
  2. (leak drowned in noise): the signal is swamped, so is effectively pure noise, carries no information about , and accuracy base rate (predict the majority class). Why this step? The lower limit shows leakage severity is continuous, not binary — real bugs live between these extremes.
  3. Interpolation: severity rises smoothly as the encoding dominates the noise , matching the parent note's " is unbounded" claim — bigger signal-to-noise ratio accuracy nearer the ceiling. Why this step? Tying severity to the same symbols and makes the middle predictable: you can estimate how bad a partial leak is from how cleanly the feature tracks .

Verify (numbers): Base rate positives. Perfect proxy () acc . Pure noise () acc . So the observed accuracy must lie in ; anything above that vanishes in production is exactly the leak signature.


Cell I — Real-world word problem (mixes several cells)

Steps.

  1. cancellation_ticket_opened — churning causes the ticket. Cell B, downstream target leak. Drop it. Why this step? The causal arrow means the ticket only exists after , i.e. after prediction time (start of month) — so it is unavailable and illegal.
  2. avg_monthly_spend averaged over the whole year includes the churn month itself. Cell C, future window. Recompute using only months . Why this step? Any average reaching into or past the prediction month contains values that do not exist yet at the month's start.
  3. Whole-data MinMax scaling. Cell D, preprocessing contamination. Fit the scaler on train only, inside a pipeline. Why this step? MinMax's min and max are fitted statistics; fitting them on test rows leaks test information into training features exactly as in Cell D.
  4. Same customer across months → use a grouped, time-ordered split (Cell E): earlier months train, later months test, grouped by customer. Why this step? Grouping by customer stops one person straddling both sides, and ordering by time stops future months training a model scored on the past.

Verify (severity estimate): Base churn rate , so an always-predict-stay dummy scores . The direct-ish ticket feature alone can push training accuracy to ; after removing it and the future-average, an honest model must still beat to be worth anything, and its production accuracy should sit within a few points of its CV accuracy. That closing gap is the proof the fixes worked.


Cell J — Exam twist (looks leaky but isn't / vice versa)

Steps.

  1. (a) Clean. age_at_signup is fixed at signup, fully known at prediction time . Correlation with is fine — leakage is about timing/causality, not about being predictive. Why this step? The classic exam trap: students reject useful features just because they're correlated. Correlation is the goal, not the crime.
  2. (b) Leaky. The median used for imputation is a fitted statistic; computing it over train + test is Cell D contamination in disguise. Why this step? Imputation is preprocessing, so it obeys the same fit-on-train-only rule as scaling. See Production model deployment — in production you impute with the stored training median.

Verify (numbers): Train credit_score , median . Full-data (with test ) median of . Imputing a missing training value with instead of the honest leaks points of test information. Bug confirmed.


Cell K — Selection / tuning contamination

Steps.

  1. Feature selection on all data. Ranking features by correlation with uses the labels of the test rows to decide which columns survive. Why this step? The selection decision is a fitted quantity — it depends on test . That decision then shapes the training features, so test information flows backward exactly like a contaminated in Cell D.
  2. Hyperparameter search on all data. Picking the setting with the best full-dataset score lets the model config be tuned to fit the test rows. Why this step? The chosen hyperparameters are also fitted quantities; optimising them against data that includes the test set is a subtler train-test contamination — the "study with the exam answers" failure applied to model configuration.
  3. Fix: split first. Do feature selection and hyperparameter search only inside training folds — nest them inside CV so the held-out fold never influences which features or settings are chosen. Why this step? Nesting restores the one rule at the level of decisions, not just feature values: nothing derived from the held-out data may touch what the model is or which columns it reads.

Verify (magnitude): With a single split, selecting features on all data means the test rows contribute a fraction of the correlations that decide the feature set — a non-zero, illegal influence. Nested CV drives that contribution to exactly .


Recall Self-test

Which cell: avg_of_5_days_centred_on_today ::: C — temporal future window. Which cell: scaler fit on train+test ::: D — preprocessing contamination. Which cell: same user's rows split randomly ::: E — group/duplicate split contamination. Which cell: loss_amount when predicting default ::: A — direct target leakage. Which cell: feature caused by an event the label triggers ::: B — downstream target leakage. Which cell: top-10 features chosen by correlation before splitting ::: K — selection/tuning contamination. Is a highly correlated but time-legal feature leaky? ::: No — correlation is allowed; only bad timing/causality leaks. Constant column and singleton-class split — are these leakage? ::: No — they are degenerate failures (undefined/crashing), not future peeking. How does perfect-proxy accuracy compare to base rate? ::: Perfect proxy ; base rate is the floor when the leak is pure noise. What does mean? ::: A rule that manufactures a feature value out of the answer — the hallmark of a target leak.