Model Evaluation · ML Systems Lab

The Validation Set Is Lying to You: Four Leakage Patterns Nobody Warns You About

Your model hits 0.94 AUC on validation. You deploy. Two weeks later, production AUC is 0.71. The model didn't degrade — your validation set was infected from the start. Leakage is the most reliably career-damaging mistake in applied ML, and it hides in places most practitioners never check.

Leakage means your model has access to information during training that it won't have at prediction time. The validation set is supposed to catch this. It doesn't — because in most pipelines, the validation set is infected by the same leakage that corrupts the training set.

Type 1: Target leakage (the classic)

A feature is computed from or correlated with the target after the fact. Example: a credit default model includes "number of late payment notices sent" as a feature. Late notices are sent after the default is already detected — the feature is a consequence of the label, not a cause. In training data, this feature perfectly predicts the label. In production, it doesn't exist at prediction time.

The diagnostic: plot feature values for positive vs negative labels. A feature with AUC > 0.95 on its own should be investigated immediately. Either it's extremely good (rare) or it leaks from the label.

Type 2: Temporal leakage

Future data is used to compute features for past events. The most common version: you compute a 30-day rolling average user activity as a feature for a purchase event. But you forgot to anchor the window to the event timestamp — it uses data from after the purchase. In batch training pipelines, this is easy to introduce and hard to detect without explicit timestamp audits.

The fix: point-in-time correct joins. For every event at time T, features must be computed using only data available at T - epsilon. This requires either a time-travel-capable feature store or explicit timestamp filtering in every feature computation.

Type 3: Preprocessing leakage

Your scaler, imputer, or encoder is fitted on the full dataset (including validation) before the train/val split. This means the validation set has already "touched" the training distribution through the preprocessing step.

The correct order: 1. Split first (train, val, test) 2. Fit scaler/encoder on train only 3. Transform val and test using train-fitted parameters

Using sklearn Pipeline ensures this order is preserved. Fitting on the full dataset before splitting is the mistake. It's subtle: your scaler.mean_ incorporates validation set statistics, giving your model slight information about validation examples during training.

Type 4: Group leakage

Your dataset has natural groups (users, patients, products) and multiple examples per group. A random 80/20 split puts some examples from user_id=12345 in training and others in validation. Any user-level features (historical engagement, spending patterns, demographics) are now shared between train and validation via the group. Your model learns user-specific patterns that generalize perfectly to the other rows from the same user in validation — but not to new users in production.

The fix: group-aware splits. All rows from a given entity (user, patient, product) must be in the same split. sklearn provides GroupShuffleSplit. If you have time-ordered data, use a chronological split where validation contains only events that happen after all training events.

The meta-lesson:

Leakage usually isn't visible in code. It's visible in unrealistically good validation metrics. If your validation AUC is > 0.95 for a hard problem, or your RMSE is suspiciously low, investigate before celebrating. Strong validation performance is a diagnostic signal, not just a success metric.

The question to ask for every feature: "At prediction time in production, is this value available, and is it computed the same way as at training time?" If the answer is "I'm not sure," audit the feature.

Continue interactively
Read this post inside ML Systems Lab — with Simplify toggle, interview Q&As, inline glossary, and the MLE Path forward pointer.
Open in MSL →