ML & Data Foundations · 2 to 3 hours
Diagnose leakage and misleading metrics
Implement an audit that flags leaky features, preprocessing, time splits, selection, and mismatched reports, predict its findings on five fixture experiments, and diagnose why a churn model that looked excellent offline failed after launch.
Hands-on debugging
3 h
Overview
In machine learning, leakage means a model is trained or evaluated with information it will not have when it makes real predictions. That is a different problem from data leakage in Security & Privacy, where data reaches someone who should not see it. You implement an audit over an experiment manifest, predict its findings on fixtures, and use it with the team's notes to diagnose a churn model whose offline results did not survive launch.
Study time: 2 to 3 hours
Interview format: Live coding against fixtures, then a diagnosis table for a model that failed after launch
By the end you can
- Explain what leakage means for a model and how it differs from data leakage in security
- Check that every feature is computable at the prediction cutoff and that fitted preprocessing sees only training rows
- Place evaluation splits after the last training label is final, and keep model and threshold choices on validation
- Report accuracy against the majority-class baseline, with precision, recall, and their denominators, on the split's own population
- Separate the causes of an inflated offline estimate from the causes of a production failure, with a confirming check, a fix, and a regression check for each
- Name what an audit of declared metadata cannot detect
Prerequisites
Helpful background: Basic JavaScript or Python, and the confusion matrix (true and false positives and negatives). The reference code is in TypeScript and Python; the practice problem runs in JavaScript or Python.
This topic covers data splits, leakage, misleading metrics, and evaluation over time for any model. Evaluation & Observability covers evaluation specific to LLM applications.
What leakage means for a model
In machine learning, leakage means a model is trained or evaluated with information that will not be available when it makes real predictions. Kaufman and colleagues call it information about the target that should not be legitimately available; scikit-learn's pitfalls guide describes it as information that would not be available at prediction time being used to build the model. It is not the security meaning, where data reaches someone who should not see it (Security & Privacy covers that).
Leakage enters in two places:
- Features: a column computed after the prediction time, or from the outcome itself.
- Procedure: fitting, splitting, selecting, and reporting so that evaluation rows shape the model or the reported number.
It causes two separate harms: an inflated offline estimate, so a team ships on a number that will not hold, and a model that relies on a signal the scoring job cannot reproduce (training-serving skew, in the terms of Google's Rules of ML). Leakage usually makes offline results look better than production will be, so an unusually strong result is the first thing to check.
The churn model that failed after launch
Worked example
Suppose a meal-kit subscription service scores every active subscriber each week with a model that predicts whether they will cancel in the next 30 days. The model was trained on six months of history, looked excellent offline, and failed in its first month of service. All figures are synthetic.
| Item | Value (synthetic) |
|---|---|
| Prediction | At the end of each cutoff day, using data up to that day |
| Label | 1 if the subscriber cancels within 30 days after the cutoff, otherwise 0 |
| History | A sample of 20,000 subscriber-week rows with cutoffs on days 1 to 181 |
| Launch split | 14,000 train, 3,000 validation, 3,000 test; 270 test rows have label 1 |
| Launch report (test split) | tp 228, fp 24, tn 2,636, fn 12 |
| First month of service | 10,000 scored rows whose 30-day labels have matured: tp 60, fp 240, tn 8,860, fn 840 |
| Dashboard headline | "Accuracy 98.8% offline, 89.2% in production" |
A flag is a prediction of 1. The first month ran in shadow: flags were logged and no one was contacted, so outreach did not change any outcome.
Features must exist at prediction time
Every feature must be computable from data that exists at the row's cutoff. Describe each feature by the last day its data window covers, relative to the cutoff: 0 is the cutoff day, negative is earlier, and positive reads the future.
- Point-in-time joins. Build each training row from tables as they were at its cutoff (an as-of join). Joining to today's table reads later updates; Rules of ML #31 warns that a joined table may change between training and serving.
- Label-derived features. Anything created by the outcome, or by the process that leads to it (an exit survey, a refund, a cancellation reason), is a copy of the label whatever its timestamp says. The lineage in a feature registry is how you find them.
- Backfills. A value computed later for historical rows can cover a wider window than the scoring job will see. Log the values the scoring job actually used and compare them with training values for the same rows; Rules of ML #29 recommends logging served features, at least for a sample, to check that training and serving agree.
Leakage through fitting, time, and selection
Fitting. Imputers, scalers, encoders, and feature selection learn statistics from the rows they are fitted on. Fit them on the training split only and apply them to the others; scikit-learn's pitfalls guide says never to include test data in fit or fit_transform. A target encoder is the sharpest case: it stores a label rate per category, so fitting it on evaluation rows copies their labels into a feature. Under cross-validation, a pipeline refits the step inside each training fold.
Time. With a forward-looking label, a random split lets training labels describe the same weeks the model is evaluated on. Split by time and leave a gap. For example (not a fixture value), with a 30-day label window and a last training cutoff on day 200, the last training label is final at the end of day 230, so the first evaluation cutoff can be day 231. scikit-learn's TimeSeriesSplit has a gap parameter that excludes samples from the end of each training set before the test set. It counts samples, not days: with one row per day, a gap of at least the label window plays this role.
Selection. Every choice made by looking at a split (model, hyperparameters, features, threshold) turns it into a validation set. Choose on validation and run the test split once. scikit-learn says test data should never be used to make choices about the model, and Rules of ML #33 says to test on data gathered after the training data.
Baselines, denominators, and populations
Write each metric as a fraction with a named denominator.
| Metric | Numerator / denominator | From the counts |
|---|---|---|
| Accuracy | correct rows / all rows | (tp + tn) / (tp + fp + tn + fn) |
| Precision | true positives / flagged rows | tp / (tp + fp) |
| Recall | true positives / actual positives | tp / (tp + fn) |
| Share flagged | flagged rows / all rows | (tp + fp) / (tp + fp + tn + fn) |
Report the share flagged at the deployed threshold: outreach capacity is spent on flagged rows.
Majority-class baseline. Always predicting the more common actual class scores max(tp + fn, tn + fp) / total. On imbalanced data, accuracy near this baseline shows nothing. For example (not a fixture value), on 1,000 rows with 50 positives, a model that flags no one scores 950 / 1,000 = 95%, exactly the baseline, with precision 0 / 0 (undefined) and recall 0 / 50 = 0.
Undefined ratios. Precision is undefined when nothing is flagged, and recall when the population has no actual positives; the crash course calls the result NaN. scikit-learn's precision_score returns 0 with an UndefinedMetricWarning by default and lets zero_division set the value, so the choice must be deliberate. This question's audit contract deliberately reports null.
Population. A metric describes the rows it was computed on. Filtering rows (missing features, survivors, only contacted customers) changes the population and usually the base rate, so check that a report's row and positive counts match its split.
The evaluation audit contract
Timing. A row is scored at the end of its cutoff day. Its label is 1 when the subscriber cancels on any of the next labelWindowDays days, so the label becomes final at the end of day cutoffDay + labelWindowDays. A model can serve only after its last training label is final, so an honest evaluation split starts on a later day.
Function. Implement auditEvaluation(experiment) in TypeScript or JavaScript, or audit_evaluation(experiment) in Python. It returns { findings, metrics }. Inputs are well-formed (the practice problem lists every guarantee): each split has at least one row and the report total is at least 1, so validate nothing and throw nothing.
Python uses snake_case for these keys: label_window_days, cutoff_day, window_end_offset_days, derived_from_label, fit_splits, threshold_split, model_split, and baseline_accuracy. Every other key and every finding string is the same. The practice problem has the full mapping.
Rules, in output order. Run every rule; one finding never stops the others.
- For each feature, in list order:
label-derived:<name>whenderivedFromLabelis true; otherwiseafter-cutoff:<name>whenwindowEndOffsetDaysis greater than 0. At most one finding per feature. - For each preprocessing step, in list order:
fit-beyond-train:<name>when any entry offitSplitsis not"train". An empty list is clean, and so is["train", "train"]. - Let L be the largest
cutoffDayamong train rows pluslabelWindowDays. Forvalidation, thentest(always this order, whatever the row order):time-overlap:<split>when the smallestcutoffDayamong that split's rows is less than or equal to L. Each evaluation split is compared with train only. selected-on-test, once, whenthresholdSplitormodelSplit(or both) is"test".population-mismatch, once, whentp + fp + tn + fndiffers from the number of rows whosesplitequalsreport.split, or whentp + fndiffers from that split's number of label-1 rows.recall-undefinedwhentp + fnis 0; thenprecision-undefinedwhentp + fpis 0.accuracy-not-above-baselinewhentp + tnis less than or equal tomax(tp + fn, tn + fp). Compare these integer counts; that is the same as comparing the two ratios, which share a denominator.
Metrics, from the report's counts with total = tp + fp + tn + fn. They are computed even when the population is flagged, and then describe the reported rows, not the split.
| JavaScript key | Python key | Value |
|---|---|---|
accuracy | accuracy | (tp + tn) / total |
baselineAccuracy | baseline_accuracy | max(tp + fn, tn + fp) / total |
precision | precision | tp / (tp + fp), or null (Python None) when tp + fp is 0 |
recall | recall | tp / (tp + fn), or null (Python None) when tp + fn is 0 |
Do not round inside the function. A precision or recall of 0 is 0, never null.
type Split = "train" | "validation" | "test";
type EvalSplit = "validation" | "test";
type Experiment = {
labelWindowDays: number;
rows: { id: string; split: Split; cutoffDay: number; label: 0 | 1 }[];
features: {
name: string;
windowEndOffsetDays: number; // last day of the window relative to the cutoff (0 = cutoff day)
derivedFromLabel: boolean;
}[];
preprocessing: {
name: string;
fitSplits: Split[]; // splits the step was fitted on; [] fits nothing
}[];
selection: { thresholdSplit: EvalSplit; modelSplit: EvalSplit };
report: { split: EvalSplit; tp: number; fp: number; tn: number; fn: number };
};
type AuditResult = {
findings: string[];
metrics: { accuracy: number; baselineAccuracy: number; precision: number | null; recall: number | null };
};
declare function auditEvaluation(experiment: Experiment): AuditResult;Code exercise
Evaluation audit
Implement auditEvaluation (Python: audit_evaluation) for the contract above. Emit findings in the stated rule order, compute the four metrics from the report's counts, and return null (None) for a ratio whose denominator is 0.
Expected
The linked practice problem calls auditEvaluation(experiment) in JavaScript and audit_evaluation(experiment) in Python and compares the whole result. JavaScript uses the camelCase keys and Python the snake_case keys listed in the contract; finding strings are identical in both languages, and feature and step names are copied into findings exactly, case included.
The time rule follows the contract's timing. A row is scored at the end of its cutoff day. Its label is 1 when the subscriber cancels on any of the next labelWindowDays days, so the label becomes final at the end of day cutoffDay + labelWindowDays. A model can serve only after its last training label is final, so an honest evaluation split starts on a later day.
Metrics are not rounded inside the function: the tests round floats to four decimal places before comparison, and a ratio with a zero denominator must be null (Python None), never 0. Leave the input unchanged (no in-place sort of rows, no added keys) and return a new findings list (a list in Python) and a new metrics object. The tests cover the five fixture experiments in one comparison; feature and preprocessing rules in list order, including label-derived before after-cutoff, offsets of 0 and below, and an empty fit list; the time rule on the day the last training label becomes final and one day later, with a 0-day window and rows in any order; metric arithmetic with null for a zero denominator and accuracy exactly at the baseline; selection and population findings in order; and clean and flagged experiments whose input must come back unchanged and whose metrics are not rounded.
Limitations
The audit trusts declared metadata and reads only row IDs, splits, cutoff days, labels, registry entries, the selection record, and the reported counts, so a fault that is not recorded in those fields passes. It models one train, validation, and test layout, not cross-validation folds or a backtest over several windows. A final refit on train plus validation before one test run is a legitimate protocol that this contract would flag, so record such a refit separately.
The fixture experiments
Worked example
E1 mirrors the launch configuration on a 22-row sample so you can trace it by hand; E2 to E5 are later experiments by the same team.
Fixture rows. Every experiment uses labelWindowDays 30. Each cell lists a split's cutoff days in manifest order, then its label-1 rows in parentheses. Row IDs are the split prefix plus the position in the list (tr1, tr2, and so on; va1 onward; te1 onward). In rows, each experiment lists its train rows, then validation, then test, in the order shown.
| Experiment | Train | Validation | Test |
|---|---|---|---|
| E1 | 64, 12, 97, 35, 118, 50, 7, 83 (tr2, tr6) | 41, 109, 22, 76 (va3) | 58, 90, 15, 102, 69, 31, 115, 44, 9, 80 (te2, te5, te9) |
| E2 | 95, 20, 120, 61, 8, 104 (tr3) | 158, 150, 165 (va2) | 196, 181, 207, 189, 214, 185, 200, 192 (te1, te6) |
| E3 | 30, 5, 44, 17 (tr2) | 80, 75 (va1) | 110, 96, 103, 120, 99 (none) |
| E4 | 140, 15, 98, 60, 33, 121, 77, 150 (tr4, tr7) | 190, 181, 199 (va3) | 236, 214, 251, 228, 219, 244, 222, 240, 231, 247 (te2, te5, te9) |
| E5 | 70, 12, 55, 31, 90 (tr3) | 130, 121, 144, 126, 137, 150 (va2) | 175, 160, 168, 181 (te4) |
Fixture configuration. A feature is written as its name (windowEndOffsetDays, derivedFromLabel) and a step as its name (fitSplits), where (none) is an empty list.
| Experiment | Features | Preprocessing | Selection: threshold, model | Report: split; tp, fp, tn, fn |
|---|---|---|---|---|
| E1 | orders_last_8w (0, false); tickets_30d (14, false); retention_offer_sent (30, true); days_since_signup (0, false) | median_imputer (train); region_target_encoder (train, validation, test); standard_scaler (train) | test, validation | test; 2, 0, 7, 0 |
| E2 | orders_last_8w (0, false); skips_last_4w (0, false); tickets_30d_asof (0, false); payment_failures_prev_cycle (-7, false) | median_imputer (train); standard_scaler (train) | validation, validation | test; 1, 1, 5, 1 |
| E3 | orders_last_8w (0, false); cancel_reason_code (0, true); days_since_signup (0, false) | log_transform (none); standard_scaler (train) | validation, test | test; 0, 0, 5, 0 |
| E4 | orders_last_8w (0, false); skips_last_4w (0, false); tickets_30d_asof (0, false); payment_failures_prev_cycle (-7, false) | median_imputer (train); region_target_encoder (train); standard_scaler (train) | validation, validation | test; 2, 0, 7, 1 |
| E5 | orders_last_8w (0, false); payment_failures_prev_cycle (-7, false) | median_imputer (train, validation); log_transform (none) | test, validation | validation; 1, 2, 2, 1 |
Predict the audit's findings
Trace the seven rules by hand for each experiment, and write every metric as a fraction, before you run any code.
Predict the outcome
Predict the audit's findings
Using only the contract and the fixture tables, predict each experiment's findings in order and its four metrics as fractions. Write your answers before you reveal them.
| Experiment | Story | Findings, in order | Metrics | Deciding arithmetic |
|---|---|---|---|---|
| E1 | Launch configuration, 22-row sample | ? | ? | ? |
| E2 | First time-based rebuild | ? | ? | ? |
| E3 | Rebuild scored on a holiday-week test window | ? | ? | ? |
| E4 | Second time-based rebuild | ? | ? | ? |
| E5 | Validation report from the analytics dashboard | ? | ? | ? |
What an audit of declared metadata cannot see
An audit like this one checks declared fields only: registry metadata, split assignments, cutoffs, labels, and reported counts. Wrong or missing metadata passes, and several kinds of leakage need checks on the data itself.
| Blind spot | What goes wrong | A check that exposes it |
|---|---|---|
| One entity in several splits | A customer, account, device, or document family has rows in train and in evaluation | Split by group and check that no group crosses splits (the Model Adaptation Medium's split validator does this) |
| Duplicates with new IDs | Copies look like new rows and can land in different splits | Deduplicate on a natural key before splitting |
| Proxies | A feature that is legal by its metadata encodes the outcome through a business process | Trace how each high-importance feature is produced, and retrain without it |
| Label-definition changes | The label means something different in training and in production | Version the label definition with the data |
| Outcome-dependent collection | Which rows exist or have labels depends on the outcome (rows only for customers still active at extraction, outcomes only for contacted customers) | Compare row counts and base rates with the full source population |
Fix the cause and keep it fixed
Fix each cause at its source, then add a check that fails if the cause returns.
- Compute point-in-time features in one code path that both training and scoring call, and compare logged served values with training values for the same rows.
- Fit preprocessing inside the training pipeline, on train rows only.
- Split by time with a gap of at least the label window.
- Choose the model, features, and threshold on validation, and run the test split once.
- Report on the whole split, with the baseline, precision, recall, the share flagged, and their denominators.
Turn every bug you find into an audit rule or a fixture, and run the audit on every experiment manifest in CI. Expect the honest number after a fix to be lower: that is the fix working, not a regression.
Diagnose the launch failure
The audit sees only the manifest, the feature registry, the selection record, and the report. The team's notes below add what the manifest does not record; use both, with the figures in the churn model scenario.
Diagnose
Diagnose the launch failure
The launch report and the first month of service are in the churn model scenario, and E1 mirrors the launch configuration. The team's notes on how the launch model was built (synthetic):
- Rows were assigned to train, validation, and test at random, so all three splits cover days 1 to 181.
- For every historical row (all three splits),
tickets_30dwas backfilled two weeks after each cutoff and counts support tickets in the 30 days up to the backfill date. The scoring job computes it at the cutoff. retention_offer_sentis joined from the offers table with no date filter. The retention team sends an offer when a subscriber opens the cancellation page.region_target_encoderreplaces each postal region with its churn rate, computed once over all 20,000 rows before the split.- The decision threshold was the best F1 among 12 candidates on the test split, and the report used the same split.
- The evaluation script dropped the 100 test rows with a missing
tickets_30dbefore computing the report. Most belong to subscribers in their first month; 30 of them have label 1. - Feature summary:
retention_offer_sentis 1 on 11% of historical rows and on 0.2% of rows at scoring time. Meantickets_30dis 1.4 on historical rows and 0.6 at scoring time. These two features have the highest importance in the trained model. - About 1,500 subscribers share a delivery address with another subscription on the service, for example a family box and a lunch box. The manifest has no household field, and the random split placed many such pairs in different splits.
- A rerun of the February extraction appended 1,200 rows a second time with new row IDs before the split.
- Recompute offline and production accuracy, the majority-class baseline, precision, recall, and the share of rows flagged, each as a fraction with its denominator. Say what the dashboard headline hides.
- Build a diagnosis table with one row per cause: the evidence, its effect (breaks the model in service, inflates the offline estimate, or both), a confirming check, a minimal fix, and a regression check.
- Which notes would the audit flag, and as which of E1's findings? Which notes can it not see, why, and what check would catch each?
- What must the team rerun before it can trust an estimate for a relaunch?
Completion checklist
- I can reproduce the audit's findings and metrics for E1 to E5 and name the rule behind each.
- I can compute the day the last training label becomes final and decide whether an evaluation split overlaps it.
- I can compare accuracy with the majority-class baseline and report precision and recall with their denominators, or as undefined.
- I can say whether a leakage finding inflates the offline estimate, breaks the production model, or both.
- I can name leaks the audit cannot see and a check for each.
Sources
- scikit-learn: Common pitfalls and recommended practices Defines data leakage as using information that would not be available at prediction time, says never to include test data in fit or fit_transform and that test data should never be used to make choices about the model, and shows pipelines that keep preprocessing inside cross-validation folds.
- scikit-learn: TimeSeriesSplit Time-ordered cross-validation that avoids training on future data; gap is the number of samples excluded from the end of each training set before the test set.
- scikit-learn: precision_score Precision is tp / (tp + fp); when tp + fp is 0 it returns 0 and raises UndefinedMetricWarning by default, and zero_division sets the value instead.
- Google Machine Learning Crash Course: Accuracy, recall, precision, and related metrics Defines accuracy, precision, and recall; shows that a model that always predicts negative scores 99% accuracy when 1% of examples are positive; explains the NaN that appears when TP and FP are both 0.
- Google: Rules of Machine Learning Describes training-serving skew and its causes; Rule #29 (log the features used at serving to check consistency with training), Rule #31 (a joined table may change between training and serving), and Rule #33 (test on data gathered after the training data).
- Kaufman, Rosset, Perlich, and Stitelman: Leakage in data mining: formulation, detection, and avoidance (ACM TKDD, 2012) Formulates leakage as information about the target that should not be legitimately available to the model, and proposes avoiding it through learn-predict separation.
The meal-kit subscription service, its churn model, manifests, fixtures, figures, team notes, and rubric are original synthetic teaching material about a fictional application. Study times and readiness thresholds are editorial estimates. Library details are as of November 2026.
Related on CodeSnatch
Lessons, problems, and articles that build the skills this question uses.
System Design
Point-in-time joins and training-serving skew in a full ML platform, the ideas behind the audit's feature rules.
AI Engineering
Builds the group and duplicate checks across splits that an audit of declared metadata cannot run.
Practice problem
Practice the as-of lookup by timestamp that a point-in-time join runs to build each training row.
System Design
Covers backfills, lineage, and data quality tests, useful for tracing how each feature value was produced.
Behavioral Interview
Frames the story of catching your own analytical error and walking a shared result back, which ML loops grade.
Practice the interview question
Answer it in your own words before you look at the reference answer.
