A bug is an assumption you did not know you were making; debug in fixed order and stop at the first step whose number proves an assumption false. Every step outputs one sentence: named broken assumption, number that proves it, the fix, the check that would have failed if you were wrong.
The ladder (run in order)
Do not reach step 6 until the six cheaper explanations are ruled out. Most underperformance is a data, label, metric, or skew problem; the model is the residual.
0 Reproduce · seed everything · baseline -> beats baseline?
1 Data bug or model bug? (a overfit-8 · b label audit · c skew · d leakage)
2 Bias or variance? (val error vs training-SET SIZE)
3 Error analysis by slice (sort by error share)
4 Is the metric wrong?
5 Is the label wrong? (clean TEST first)
6 Improve model: capacity -> features -> data
Step 0 — Reproduce, seed, baseline
- Seed everything, including the split seed, DataLoader workers, augmentation RNG, hash-bucket features.
- Standard error of a proportion:
sqrt(p(1-p)/n). At n=2,000, p=0.90 → 0.67 pts. Unseeded split fabricates “improvements.” - Noise floor: run same config 5 seeds. Example
sd = 0.0059for one run. You gate on a difference, whose sd issd*sqrt(2). - Believe nothing under the earned band:
| Compared | 95% band on difference |
|---|---|
| One seed A vs one seed B | 1.96*sd*sqrt(2) = 0.016 |
| Mean of 5 vs mean of 5 | 1.96*sd*sqrt(2/5) = 0.007 |
- Gotcha:
2*sdlooks like a 5% filter but always lets 15.7% of noise through (onlysqrt(2)-sigma on the difference). - Three baselines, cheapest first: majority class / global mean; simple heuristic already deployed; regularized linear or GBDT on raw features. Without a floor you cannot tell “good model” from “easy problem” from “degenerate metric” (0.940 accuracy loses to a constant at 0.942).
Step 1 — Data bug or model bug?
Assumption: the data the model sees is the data I think it sees.
- 1a Overfit 8 samples (shuffle/augmentation/dropout/weight-decay off). Must reach ~0 loss. Failing eliminates capacity, data volume, and generalization by construction → look for label misalignment, wrong loss axis, frozen params, missing
zero_grad,lr=0. Loss pinned atln(C)= uniform guessing (ln(10)=2.30). - 1b Label audit: relabel 100 rows blind. Agreement 0.97+ = fine; 0.88 = 12% wrong, go to step 5. Use 100 not 10 (SE ≈ 3 pts).
- 1c Train/serve skew: recompute same feature, same entity, same timestamp through both paths and diff. Read the % rows differing column. One feature wildly off against a clean background = skew. Causes: different code (
<vs<=), different data availability, different time semantics. - 1d Leakage: number is implausible (0.987 where experts get 0.75).
- Diagnostic 1: single-feature AUC scan; any raw feature above ~0.95 alone is a leak. Use
max(auc, 1-auc)for direction. - Diagnostic 2: ablate the suspect, confirm metric lands in the plausible range (not merely drops). Ablate an ordinary feature as control.
- Patterns: target-derived, lookahead in time, group leakage (split by entity), preprocessing fit before split.
- Group-leakage arithmetic: 8,000 users × 5 rows, random row split → 67% of users straddle it, 99.8% of test rows have their user in train.
- Diagnostic 1: single-feature AUC scan; any raw feature above ~0.95 alone is a leak. Use
Step 2 — Bias or variance?
Assumption: more data would help. Plot val error vs training-set size (not vs epoch), 5–6 sizes, several seeds.
| Curve shape | Diagnosis | What will NOT help |
|---|---|---|
| Both converged, HIGH error | Bias (too simple) | More data, more regularization |
| Large gap, val still falling in n | Variance | More capacity |
| Both converged, LOW error | Done with this class | Anything but new features/metric |
| Large gap, val FLAT in n | Noise ceiling / distribution mismatch → step 5 | More data of same kind |
| Val below train | Accounting (dropout/aug) or leakage | Everything until resolved |
- Price the data: excess error over floor falls ×
0.71 = 1/sqrt(2)per doubling = power law. Fiterr(n) = e_inf + a/sqrt(n); invertn = (a/(e - e_inf))^2. Example: 3 more points of error costs 4×, 5 more costs 36× the data.e_inf= irreducible under this model → send you to features, not labeling.
Step 3 — Error analysis by slice
Assumption: the average describes every user. Overall metric is traffic-weighted, so a slice must be small to hide its failure — and small slices are the growth markets.
- Sort by error share (
n*(1-acc)), not accuracy. Worked case:mobile·es= 4.0% traffic, 25.1% of all errors, accuracy 0.510 while overall reads 0.922. Fixing it moves overall 0.922 → 0.939. - Find the cause by slicing the features, not just the metric (e.g.
page_textnon-null 22% vs 97%; imputer fills nulls with 0 → confident negative). Fix = missingness indicator + slice fallback, not architecture. - Automate: fit a shallow tree on the error indicator; report worst-slice metric as a standing number.
Step 4 — Is the metric wrong?
Assumption: the metric goes up exactly when the product gets better. Five breaks:
- Imbalance: ROC-AUC x-axis is FPR (÷ negatives), so 100:1 imbalance is invisible. FPR 0.05 → precision 0.138. Report PR-AUC / precision-at-recall too.
- Wrong aggregation unit: per-row 0.970 over 20-row sessions =
0.97^20 = 0.544session success. Report at the unit the user experiences. - Inherited threshold: 0.5 is a default. With
C_FN = 20*C_FP, optimump* = C_FP/(C_FP+C_FN) = 1/21 = 0.048. - Calibration (score multiplied by money/bid, not just thresholded): AUC is invariant to any monotone transform; calibration is not. Reliability table →
ECE = Σ (n_b/n)|pred_b − obs_b|. Fix: isotonic or Platt on held-out split, refit every deploy. - Proxy drift (Goodhart): offline metric up, business flat → burden of proof is on the offline metric.
Step 5 — Is the label wrong?
Assumption: the test label is the truth. Label error rate eta caps every score.
- Ceiling (binary): a perfect model scores
1 - eta. Inverta = (observed - eta)/(1 - 2*eta). Observed 0.85 ateta=0.12→ true accuracy 0.961. Observed above1-eta= your noise estimate is wrong / test leaks. C classes: ceiling1-etaholds; the “true accuracy” inversion is binary-only. - Estimate eta: 200 test rows, 3 blind annotators, majority = reference. Report Cohen’s kappa.
- Human ceiling from kappa: undo chance correction
p_o = kappa(1-p_e) + p_e(balance-dependent), then(1-eta)^2 + eta^2 = p_o→ ceiling1-eta. kappa 0.62 balanced-binary → ceiling 0.89, not 0.81. - Find mislabels cheaply: rank by per-row loss / out-of-fold confident-learning; review top 100 by hand (often ~60% are test-set problems, not model errors).
- Clean TEST first: test noise caps the score and reverses rankings; cheaper by the size ratio (200,000 train vs 2,000 test = 100×). Train noise is second (partially self-correcting).
Step 6 — Capacity → features → data
Reached only when nothing was broken. Order set by information gained per hour.
- Capacity first because it is also the diagnostic: double width/depth, drop regularization, watch training error. If train error won’t fall even handicap-off → representation problem, not model size.
- Features second: the only lever that adds information; attacks the
e_infasymptote data cannot reach. Cost = point-in-time offline + online impl + parity test. - Data last: slowest, and step 2 already priced it.
- Legitimate reorder: a starved slice jumps the queue (5,000 targeted rows beat 5,000 rows of the dominant slice).
Gotchas
- Verifying a leakage fix: the offline number must get worse and the offline↔online gap must close. Verify on the gap, never offline alone.
- A threshold tuned for model A means a different operating point on model B; ship the threshold in the model artifact.
- One class always predicted = degenerate optimum under imbalance; fix with class weights / resampling / focal loss — after checking the labels.