Skip to content
VibeFormer
Intermediate28 min

Pipelines and Data Leakage

The many ways leakage sneaks in — scaling before splitting, target encoding, temporal leaks — and how pipelines prevent it.

Assumes you know

Pipelines and Data Leakage

Intuition first

Leakage is when information that will not be available at prediction time sneaks into training. The model uses it, scores brilliantly in testing, and then fails in production — because the thing it was relying on is gone.

It is the most damaging error in applied machine learning, and the reason is psychological rather than technical: every other bug makes your numbers worse, so you go looking for it. Leakage makes your numbers better. Nobody investigates a model that just hit 0.97.

The defence is structural rather than vigilant. If your preprocessing is wrapped so that it physically cannot see validation data, you do not have to remember to be careful.

The three kinds

1. Preprocessing leakage

Any transformation that learns from data must learn from training data only. Standardisation learns a mean and standard deviation. Imputation learns a fill value. PCA learns components. Target encoding learns per-category averages.

Fit any of them on the full dataset and every fold's "held-out" rows have already influenced the transformation.

python
# WRONG — the scaler has seen the test set
X_scaled = StandardScaler().fit_transform(X)
X_train, X_test = train_test_split(X_scaled)

# RIGHT — split first, fit only on train
X_train, X_test = train_test_split(X)
scaler = StandardScaler().fit(X_train)
X_train = scaler.transform(X_train)
X_test = scaler.transform(X_test)      # transform, never fit

The optimism from this alone is usually small — a point or two — which is why it survives code review. It becomes severe with target encoding or feature selection.

2. Target leakage

A feature contains information derived from the outcome, or recorded only after it.

Real examples:

  • Predicting hospital readmission using discharge_medication_count, recorded at discharge — after the stay the model is meant to predict from.
  • Predicting churn using days_since_last_login, which is mechanically small for active users and large for churned ones.
  • Predicting loan default using total_payments_made, which is low precisely because the loan defaulted.
  • Predicting fraud using chargeback_flag, which is a consequence of confirmed fraud.

3. Group and temporal leakage

The same entity appearing on both sides of a split, or training data that postdates validation data. Covered in the splits lesson; the mechanism is that the model recognises the entity or the era rather than the pattern.

Pipelines make it structural

A pipeline bundles preprocessing with the estimator into one object with a single fit. When cross-validation refits that object per fold, every transformation is refitted on that fold's training portion automatically.

python
import numpy as np
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.impute import SimpleImputer
from sklearn.feature_selection import SelectKBest, f_classif
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score, StratifiedKFold

numeric = ["age", "income", "tenure"]
categorical = ["region", "plan"]

# Every step here LEARNS something, so every step must sit inside the pipeline.
preprocess = ColumnTransformer([
    ("num", Pipeline([
        ("impute", SimpleImputer(strategy="median")),   # learns medians
        ("scale", StandardScaler()),                    # learns mean and sd
    ]), numeric),
    ("cat", Pipeline([
        ("impute", SimpleImputer(strategy="most_frequent")),
        ("encode", OneHotEncoder(handle_unknown="ignore")),  # learns categories
    ]), categorical),
])

model = Pipeline([
    ("prep", preprocess),
    ("select", SelectKBest(f_classif, k=10)),   # learns which features — leaks badly outside
    ("clf", LogisticRegression(max_iter=1000)),
])

cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=0)
scores = cross_val_score(model, df[numeric + categorical], y, cv=cv)
print(f"{scores.mean():.4f} ± {scores.std(ddof=1) / np.sqrt(len(scores)):.4f}")

SelectKBest is the important line. Feature selection performed once on the whole dataset is one of the largest sources of optimism there is, because it searches thousands of candidates using the labels. Inside a pipeline it is redone per fold.

Solved problem 1 · How large is selection leakage?

A dataset has n=100n = 100 rows and 10,000 pure noise features, independent of a balanced binary label. A team selects the 10 features most correlated with the label using all 100 rows, then cross-validates a classifier on those 10.

What accuracy should they expect, and what is the honest accuracy?

Step 1 — establish the truth

The features are noise by construction. No feature carries information about the label, so the true accuracy of any model is 0.50.5.

Step 2 — why some features look informative anyway

With n=100n = 100, the sample correlation between a random feature and the label has standard deviation approximately

1n1=1990.1005\frac{1}{\sqrt{n - 1}} = \frac{1}{\sqrt{99}} \approx 0.1005

Across 10,000 independent features, the extremes of that distribution will be large. The expected maximum of mm standard normals is roughly 2lnm\sqrt{2\ln m}, so in correlation units:

0.1005×2ln10,000=0.1005×2×9.21=0.1005×4.290.430.1005 \times \sqrt{2 \ln 10{,}000} = 0.1005 \times \sqrt{2 \times 9.21} = 0.1005 \times 4.29 \approx 0.43

So the best noise feature shows a sample correlation around 0.430.43 — which looks like a genuinely strong predictor.

Step 3 — what the leaked pipeline reports

The 10 selected features were chosen because they correlate with these particular 100 labels. Cross-validating afterwards does not undo that: every fold's validation rows helped choose the features, so the correlation is present in every fold.

Reported accuracy is typically 0.75 to 0.90 on pure noise.

Step 4 — the correct procedure

Put selection inside the cross-validation loop. Each fold then selects features using only its training rows, and the validation rows are genuinely unseen. The selected features differ wildly from fold to fold — itself a diagnostic — and accuracy lands at about 0.500.50.

Answer

Leaked: 0.75–0.90 on data containing no signal whatsoever. Honest: 0.50\approx 0.50.

Selection leakage can manufacture 40 points of accuracy out of nothing. This is not a contrived example — it is the standard failure mode of high-dimensional biological and financial datasets.

A leakage checklist

Run through this before trusting any result.

  1. Is any feature recorded at or after the moment of the outcome? Check timestamps, not intuition.
  2. Would this column exist at prediction time in production? Trace where it is populated.
  3. Is any preprocessing fitted outside the cross-validation loop? Scalers, imputers, encoders, selectors, dimensionality reduction.
  4. Are there duplicate or near-duplicate rows across splits?
  5. Do rows share an entity — user, patient, device, document? If so, group the split.
  6. Is the data temporal? If so, split by time.
  7. Was the test set used more than once? If yes, it is a validation set now.
  8. Is the result surprisingly good? Treat that as evidence of a bug until proven otherwise.

When the pipeline is not enough

Pipelines protect against preprocessing leakage. They cannot detect target leakage, because they have no way to know that days_since_last_login is downstream of churn. That requires understanding how the data was produced.

Two practices that help:

  • Build a data dictionary recording, for each column, when it is populated relative to the prediction moment.
  • Simulate the production query. Reconstruct the feature set as it would have existed at a historical timestamp, and train on that. Tedious, and it catches what nothing else does.

Exercise 1

A churn model reaches AUC 0.99 using these features: plan_type, monthly_spend, support_tickets, days_since_last_login, cancellation_reason_code. Identify the leaks.

Show solution

cancellation_reason_code — certain leakage. It only exists for customers who have already cancelled. It is not a predictor of churn, it is a record of churn. A model given this feature learns "if this field is populated, the customer churned", which is tautological and unavailable for any current customer.

days_since_last_login — almost certainly leakage, depending on computation. If computed as of a snapshot date before the churn window, it is legitimate and predictive. If computed as of today, then for churned customers it counts from the day they left — mechanically large — and for active customers it is small. That encodes the outcome.

support_tickets — needs checking. Legitimate if counted before the snapshot; leaking if it includes the cancellation contact itself.

plan_type, monthly_spend — fine, assuming values as of the snapshot rather than final values. Note that monthly_spend for a churned customer might be recorded as 0 after cancellation, which would leak too.

AUC 0.99 on churn is not achievable. Real churn models land around 0.75–0.85, because churn depends on unobservable things like a competitor's advertisement. The 0.99 is the tell.

Fix: rebuild the feature set at a fixed snapshot date, include only values populated strictly before it, and define the label from the window after it.

Exercise 2

Explain why SimpleImputer fitted on the full dataset leaks, and quantify roughly how much it matters compared with leaked feature selection.

Show solution

SimpleImputer(strategy="median") computes a median across all rows it is fitted on. If fitted on the full dataset, that median incorporates validation rows, so imputed values in the training set carry a trace of validation data, and the imputed values in validation were computed partly from validation itself.

The magnitude is small. A median is a highly aggregated statistic — one number summarising thousands of rows — so a single validation row shifts it negligibly. Typical optimism is a fraction of a percentage point, and with large nn it is unmeasurable.

Contrast with feature selection, which can manufacture 40 points, as the solved problem shows. The difference is how much the procedure depends on individual labels. Imputation uses only feature values, aggregated. Selection uses the labels, and searches thousands of candidates to find whichever best matches this specific sample.

The practical conclusion is not that imputation leakage is acceptable — put it in the pipeline, it costs nothing — but that when triaging a suspiciously good result, look at label-dependent steps first: selection, target encoding, resampling, threshold tuning.


Next: Feature Engineering, where the features themselves are constructed — carefully, and inside the pipeline.