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

Step 1: The question

Implement the evaluation audit and predict its findings and metrics for E1 to E5. Then diagnose the launch model's failure: separate the causes that broke the model in service from the causes that only inflated the offline estimate, and for each give the evidence, a confirming check, a minimal fix, and a regression check. Correct the dashboard's accuracy claim with the majority-class baseline, precision, recall, and the share flagged, each with its denominator, and name the leaks in the team's notes that the audit cannot detect, with a check for each.

The churn model that failed after launch

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.

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;

The fixture experiments

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

Constraints

  • Keep the audit's rule order, finding strings, and null handling exactly as stated
  • Use only the scenario figures, fixtures, and notes; label any other figure as an assumption
  • The audit reads only the manifest, the registry entries, the selection record, and the report: treat anything else as outside its view

Deliverables

  • An audit that reproduces the findings and metrics for E1 to E5, with the rule behind each finding
  • Offline and production accuracy, majority-class baseline, precision, recall, and share flagged, each as a fraction with its denominator
  • A diagnosis table: cause, evidence, effect (breaks the model in service, inflates the offline estimate, or both), and a confirming check
  • A minimal fix and a regression check for each cause, and what to rerun before trusting a relaunch estimate
  • The leaks in the notes the audit cannot detect, why, and a check for each, plus the other kinds it cannot see

Step 2: Hints

Optional. Each hint gives a little more away.

Step 3: Work through it

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.

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???

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?

Kept in this browser. Saving your score below keeps a copy with it.

0 / 5000

Step 4: Reference answer

Step 5: Score yourself

Score each dimension 0, 1, or 2, then save the score to your history. Readiness thresholds are editorial estimates.

Grades your written answer against this rubric with your own AI provider key from Settings. It is a second opinion: the score you save is still yours.

Audit implementation

What earns a 2: Reproduces the fixture findings and metrics with rules applied in the stated order, and explains each boundary: offset 0, the label-window day, a zero denominator.

Metric honesty

What earns a 2: Writes every metric as a fraction with a named denominator, compares accuracy with the majority-class baseline on the same population, and reports undefined ratios as undefined.

Leakage diagnosis

What earns a 2: Ties each cause to evidence in the notes or the audit output, names a check that would confirm it, and says whether it affects the model in service, the offline estimate, or both.

Fixes and regression checks

What earns a 2: Proposes the smallest fix per cause that keeps the evaluation honest, and a regression check that would fail if the cause returned.

Audit limits

What earns a 2: Explains why each blind spot is invisible to declared metadata and names a concrete data check that would expose it.

0 of 10 · Scored 0 of 5 dimensions

Ready at 8 of 10, with no 0 in Audit implementation or Leakage diagnosis or Metric honesty

Kept in this browser until you sign up, then it moves to your account.

Complete this level when your audit passes the linked problem's tests and you can explain, for any finding, whether it affects the model in service, the offline estimate, or both, and which check would confirm it.

Your saved scores

Practice saves and mock interview answers on this question, newest first.

No saved scores yet. Score every dimension, then save.

Step 6: Follow-up questions

An interviewer may ask these next.

In machine learning, what does leakage mean, and how is it different from data leakage in security?

Why should a fitted preprocessing step see only the training split?

With a forward-looking label, why is a time-ordered split not enough on its own?

What is the majority-class baseline, and when does it matter most?

When is precision undefined, and when is recall undefined?

Step 7: Mark complete