AI Engineering
/
ML & Data Foundations
/

Diagnose leakage and misleading metrics

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.

Medium
Hands-on

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.

ItemValue (synthetic)
PredictionAt the end of each cutoff day, using data up to that day
Label1 if the subscriber cancels within 30 days after the cutoff, otherwise 0
HistoryA sample of 20,000 subscriber-week rows with cutoffs on days 1 to 181
Launch split14,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 service10,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.

MetricNumerator / denominatorFrom the counts
Accuracycorrect rows / all rows(tp + tn) / (tp + fp + tn + fn)
Precisiontrue positives / flagged rowstp / (tp + fp)
Recalltrue positives / actual positivestp / (tp + fn)
Share flaggedflagged 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.

  1. For each feature, in list order: label-derived:<name> when derivedFromLabel is true; otherwise after-cutoff:<name> when windowEndOffsetDays is greater than 0. At most one finding per feature.
  2. For each preprocessing step, in list order: fit-beyond-train:<name> when any entry of fitSplits is not "train". An empty list is clean, and so is ["train", "train"].
  3. Let L be the largest cutoffDay among train rows plus labelWindowDays. For validation, then test (always this order, whatever the row order): time-overlap:<split> when the smallest cutoffDay among that split's rows is less than or equal to L. Each evaluation split is compared with train only.
  4. selected-on-test, once, when thresholdSplit or modelSplit (or both) is "test".
  5. population-mismatch, once, when tp + fp + tn + fn differs from the number of rows whose split equals report.split, or when tp + fn differs from that split's number of label-1 rows.
  6. recall-undefined when tp + fn is 0; then precision-undefined when tp + fp is 0.
  7. accuracy-not-above-baseline when tp + tn is less than or equal to max(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 keyPython keyValue
accuracyaccuracy(tp + tn) / total
baselineAccuracybaseline_accuracymax(tp + fn, tn + fp) / total
precisionprecisiontp / (tp + fp), or null (Python None) when tp + fp is 0
recallrecalltp / (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.

Open coding workspace

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.

ExperimentTrainValidationTest
E164, 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)
E295, 20, 120, 61, 8, 104 (tr3)158, 150, 165 (va2)196, 181, 207, 189, 214, 185, 200, 192 (te1, te6)
E330, 5, 44, 17 (tr2)80, 75 (va1)110, 96, 103, 120, 99 (none)
E4140, 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)
E570, 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.

ExperimentFeaturesPreprocessingSelection: threshold, modelReport: split; tp, fp, tn, fn
E1orders_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, validationtest; 2, 0, 7, 0
E2orders_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, validationtest; 1, 1, 5, 1
E3orders_last_8w (0, false); cancel_reason_code (0, true); days_since_signup (0, false)log_transform (none); standard_scaler (train)validation, testtest; 0, 0, 5, 0
E4orders_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, validationtest; 2, 0, 7, 1
E5orders_last_8w (0, false); payment_failures_prev_cycle (-7, false)median_imputer (train, validation); log_transform (none)test, validationvalidation; 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.

ExperimentStoryFindings, in orderMetricsDeciding arithmetic
E1Launch configuration, 22-row sample???
E2First time-based rebuild???
E3Rebuild scored on a holiday-week test window???
E4Second time-based rebuild???
E5Validation 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 spotWhat goes wrongA check that exposes it
One entity in several splitsA customer, account, device, or document family has rows in train and in evaluationSplit by group and check that no group crosses splits (the Model Adaptation Medium's split validator does this)
Duplicates with new IDsCopies look like new rows and can land in different splitsDeduplicate on a natural key before splitting
ProxiesA feature that is legal by its metadata encodes the outcome through a business processTrace how each high-importance feature is produced, and retrain without it
Label-definition changesThe label means something different in training and in productionVersion the label definition with the data
Outcome-dependent collectionWhich 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):

  1. Rows were assigned to train, validation, and test at random, so all three splits cover days 1 to 181.
  2. For every historical row (all three splits), tickets_30d was 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.
  3. retention_offer_sent is joined from the offers table with no date filter. The retention team sends an offer when a subscriber opens the cancellation page.
  4. region_target_encoder replaces each postal region with its churn rate, computed once over all 20,000 rows before the split.
  5. The decision threshold was the best F1 among 12 candidates on the test split, and the report used the same split.
  6. The evaluation script dropped the 100 test rows with a missing tickets_30d before computing the report. Most belong to subscribers in their first month; 30 of them have label 1.
  7. Feature summary: retention_offer_sent is 1 on 11% of historical rows and on 0.2% of rows at scoring time. Mean tickets_30d is 1.4 on historical rows and 0.6 at scoring time. These two features have the highest importance in the trained model.
  8. 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.
  9. A rerun of the February extraction appended 1,200 rows a second time with new row IDs before the split.
  1. 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.
  2. 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.
  3. 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?
  4. 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

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.

Practice the interview question

Answer it in your own words before you look at the reference answer.