BitBootcampBitBootcamp ← all explainers 🔄 Review ⚙ Settings

Drill 92 — The Retraining That Regressed

Theme: Model Lifecycle & MLOps · Format: 20-min drill · Difficulty: core

The story

A card issuer runs a transaction-fraud model on the authorization hot path — every swipe gets a risk score, and scores above a threshold get declined or stepped up. To keep the model fresh against shifting fraud patterns, the team built a continuous training pipeline: every Sunday night a job pulls the last twelve weeks of transactions, joins in the confirmed-fraud labels from the chargeback system, trains a new gradient-boosted model, and pushes it straight to the scoring service. It has run untouched for eight months, and everyone is proud of how hands-off it is.

One weekend an upstream data team ships a routine schema change: the chargeback table's key column is renamed from txn_id to transaction_id. Nothing in fraud-serving depends on that table directly, so the change sails through their review. But the training pipeline's label join — LEFT JOIN chargebacks ON t.txn_id = c.txn_id — now matches almost nothing. Every transaction comes back with a null label, which the pipeline defaults to 0 (not fraud). The trailing-window fraud prevalence the model trains on drops from its usual 0.8% to about 0.02%.

The retrain does exactly what it was built to do. It fits a model to data that says fraud is essentially nonexistent, so it learns to score almost everything as safe. It writes the model artifact, tags it latest, and the scoring service picks it up on its next poll. There is no offline evaluation against a holdout, no comparison to the model already in production, and no human in the loop. For the next week the service returns a score for every authorization, latency is flat, error rate is zero, and every dashboard is green — while genuine fraud is waved straight through and losses climb quietly in the background.

⏸ Stop here and ask the room: the retrain succeeded, the service is scoring, and every operational metric is green. How would anyone on this team find out that Sunday's model is materially worse than the one it replaced?

Run it — facilitator script

Timing for a 20-minute session: 2 min story · 10 min staged questions · 3 min reveal · 5 min transfer.

The retrain ran clean and the service is scoring — so what's actually broken?

Why did none of the operational monitors catch it?

What should stand between the retrain finishing and it serving live traffic?

The reveal

An analyst chasing a spike in chargebacks pulled the training manifests and found it in minutes: Sunday's run had trained on a label prevalence of 0.02% instead of the usual 0.8%, because the label join matched 1% of rows after the key rename. The model that had been live all week scored an AUC of 0.71 on the holdout against the previous champion's 0.94, and its fraud recall at the production threshold had collapsed. The fix wasn't just repairing the join. They put four gates between the retrain and production — data-quality checks, an offline eval gate comparing challenger to champion, a human sign-off, and an automatic pin to the last-known-good model when any gate fails — so that a bad week of data can never again silently become the live model. Once the gates were in, the corrupted retrain was caught at the data-quality check and the pipeline held on the previous champion; the bad model never shipped.

Why this happens — the mechanics

A model is a function fit to the labels you give it. It has no notion that fraud "should" be 0.8% of transactions; if the training data says fraud is 0.02%, the loss function rewards a model that scores almost everything as safe, and that is exactly what it produces — confidently, with valid-looking scores and no error. The pathology is that the artifact is well-formed garbage: it serves, it's fast, it returns a number for every input, and the number is wrong in a way no HTTP status or latency graph can express. This is the defining shape of ML failures — the system behaves exactly as coded while the meaning is broken, because the meaning lives in the data, not the control flow.

The organizational root is continuous training with continuous deployment and no gate between them. Automating the retrain is good practice; the mistake is treating "the pipeline produced a model" as equivalent to "we have a better model." Those are different claims, and only the first is true by construction. In normal software you would never let a build deploy to production without tests passing; a retrain is a build, and shipping "whatever it produced" is deploying an untested artifact. The failure is also a seam: the upstream team owned the schema, the fraud team owned the model, and the label join sat in the gap between them with no contract — so a change that was safe in one world silently poisoned the other. Competent teams hit this precisely because the automation that makes them fast is the same automation that ships the regression at machine speed.

Diagrams

Problem — how the regression ships:

sequenceDiagram
    autonumber
    participant ETL as Upstream ETL
    participant TR as Weekly retrain
    participant REG as Model registry
    participant SS as Scoring service
    participant FR as Fraud losses
    Note over ETL: label key renamed<br/>txn_id to transaction_id
    ETL--xTR: labels fail to join<br/>(match rate ~1%)
    Note over TR: trains on ~0% fraud<br/>learns nothing is fraud
    TR->>REG: push challenger<br/>(no eval gate)
    REG->>SS: auto-promote to prod<br/>(no approval)
    Note over SS: 200 OK, flat latency<br/>AUC silently 0.71
    SS--xFR: real fraud waved through<br/>for a week
    Note over TR,FR: found by an analyst,<br/>not a monitor

Solution — where the gates go:

flowchart TD
    A["Weekly retrain<br/>produces a challenger"] --> DQ["Data-quality checks<br/>(label rate, row count,<br/>join match-rate)"]
    DQ -.->|"out of band"| PIN["Pin last-good champion<br/>(never ship the retrain)"]
    DQ --> EG["Offline eval gate<br/>(challenger vs champion)"]
    EG -.->|"AUC regresses"| PIN
    EG --> HA["Human approval<br/>on the eval report"]
    HA -.->|"no sign-off"| PIN
    HA --> PROM["Promote to serving"]

What good looks like

The lesson

A retrain is a candidate, not a release: "the pipeline produced a model" is not "we have a better model." Gate every retrain against the model already in production — on real data-quality checks and a business-metric comparison — and default to the last-known-good champion when the gate fails, so a bad week of data can never silently become the live model.

Named concepts: champion/challenger evaluation, offline eval gate, training-data validation, model promotion gates, automated rollback.

Real-world echoes

Transfer

Which of your models auto-deploys whatever last night's pipeline produced, with nothing comparing the new model to the one it replaces? "The pipeline is reliable" is not an answer — name the gate that would stop a worse model from reaching production, and the check that would catch corrupted training data before a model is even built.

Make it a ticket: for your highest-stakes model, add an offline eval gate that blocks promotion unless the challenger matches the champion on the business metric (define the threshold in writing), a pre-training data-quality check on label prevalence and join match-rate, and an automatic pin to the last-known-good model when either fails — then prove it by replaying the corrupted run and confirming nothing ships.

Related