learn-model-evaluation-with-phoebe / Practitioner session 5 of 10
Learn Model Evaluation with Phoebe · Practitioner track · Session 5 of 10

The cost matrix

This is the session the classic half funnels into. Four sessions of metrics, and not one of them knew what a mistake costs - tonight you attach dollars to the error cells and watch something uncomfortable happen: the threshold that maximizes F1 and the threshold that minimizes cost are different places, and the gap on Lumen's slice is $704. You will press a button that makes the metric better and the business worse, learn why that trap has a name (Goodhart's law), and leave with the one-page cost matrix that turns any metric conversation into a money conversation.

🟠 Practitioner track DS · analysts · ML + AI engineers Python · live evaluator on key pages 45 min
0-3 · Welcome 3-18 · Costs into cells + Elkan's rule 18-42 · Build-along: the $704 button 42-45 · Q&A
Part 1 · covers Elkan 2001, sklearn cost-sensitive learning example + threshold-tuning guide

Put prices on the error cells 8 min live

The cost matrix is the confusion matrix with money in it. At Lumen: a false positive is ~$2 of wasted outreach; a false negative is a ~$74 order that never happens. Once those two numbers exist, "how good is the model?" stops being a metrics debate and becomes arithmetic: expected cost = FP x $2 + FN x $74, and the best threshold is whichever one minimizes it. This is not a niche trick - it is a 2001 result (Elkan, "The Foundations of Cost-Sensitive Learning") that sklearn's own documentation now builds its threshold-tuning guide around.

FALSE POSITIVE wasted outreach price: $2 FALSE NEGATIVE lost order price: $74 EXPECTED COST FP x $2 + FN x $74 computed at every threshold THE DIAL pick the threshold that minimizes it Elkan 2001: with calibrated probabilities the optimal threshold follows from the cost RATIO - here $2:$74, nowhere near 0.5.
🔍 Click to zoom - two prices in, one threshold out
LiveWhere the prices come from (and how rough is fine)5 min▶

Teams stall here: "we don't know the exact cost of a false negative." You do not need exact - you need the RATIO roughly right, because the optimal threshold moves with the ratio, not the absolute figures. Getting prices:

  • FP price - usually operational: cost of the outreach, the analyst-minutes to triage an alert, the goodwill hit of a wrong takedown. Lumen: ~$2 a nudge.
  • FN price - usually opportunity: the average order (~$74 from the Lumen canon), the average fraud loss, the downstream cost of the missed defect.
  • Disagree about the number? Run the evaluator at BOTH candidate prices - if the chosen threshold barely moves, the disagreement doesn't matter; if it moves a lot, you found a genuinely important business question, and it belongs to the business owner, not the notebook.

sklearn's canonical example does exactly this with the German Credit dataset - a cost matrix where misclassifying bad credit as good is 5x more costly, shipped WITH the dataset since the 1990s. The field has always known; the defaults just made everyone forget.

Real world

The quarter spent polishing F1. A churn team pushed F1 from 0.61 to 0.68 over a quarter - reprocessing pipelines, new features, a bigger model. Finance saw nothing. Post-mortem: their retention offer cost $8, a churned customer cost $310 - the cost-optimal threshold sat far to the recall side of their F1-optimal one the whole time. Moving one number on day one would have delivered more than the quarter of modeling. You will reproduce their mistake with one button in Demo 1, safely.

Self-studyThe sklearn mechanics, exactly4 min read▶

Two tools, both in sklearn >= 1.5, both from the threshold-tuning user guide:

from sklearn.model_selection import TunedThresholdClassifierCV
from sklearn.metrics import make_scorer, confusion_matrix

def lumen_gain(y, y_pred):                 # negative cost = gain
    tn, fp, fn, tp = confusion_matrix(y, y_pred).ravel()
    return -(fp * 2 + fn * 74)

tuned = TunedThresholdClassifierCV(
    model, scoring=make_scorer(lumen_gain)
).fit(X_train, y_train)                    # NEVER tune on training data alone -
print(tuned.best_threshold_)               # it cross-validates internally

And for a threshold someone hands you (a policy decision, a regulator): FixedThresholdClassifier. The guide's two warnings, verbatim in spirit: default 0.5 cutoffs are "most certainly not ideal for most use cases," and never use the same data to train the classifier AND tune the threshold.

Part 2 · covers Ng "satisficing vs optimizing", Rules of ML #39

When the metric becomes the target 6 min live

Goodhart's law: when a measure becomes a target, it ceases to be a good measure. In evaluation it has a precise mechanism - every summary metric bakes in a cost assumption, and optimizing the metric optimizes that hidden assumption, not your business. F1 assumes false positives and false negatives cost the same. At Lumen they differ by 37x. So maximizing F1 quietly maximizes the wrong objective - politely, reproducibly, with great-looking dashboards.

LiveNg's escape hatch: satisficing vs optimizing4 min▶

Andrew Ng's Structuring ML Projects doctrine, still the cleanest fix:

  • Pick ONE optimizing metric - the number you genuinely try to maximize. Make it the money one when you can (expected cost/gain), a proxy only when you must.
  • Make everything else a satisficing metric - a constraint with a floor or ceiling: recall >= 0.9, latency <= 200ms, cost per query <= $0.01. You don't optimize constraints; you respect them.
  • His companion rule - the single-number evaluation metric - is about ITERATION SPEED, not truth: one number to rank experiments fast, with the cost matrix deciding what that number is.

Anthropic's own success-criteria guidance says the same thing for LLM systems: "most use cases need multidimensional evaluation" - their worked example is an F1 floor AND a toxicity ceiling AND a latency ceiling. One optimizing number, several guardrails. That shape repeats in b8.

Self-studyThe two-hop proxy chain3 min read▶

Google's Rules of ML, Rule #39, generalizes the trap: launch metrics are themselves proxies. The full chain is offline eval score -> online metric -> long-term business goal, and each hop can break independently:

  • Offline-to-online breaks via drift, serving skew (Rule #37 - b9's whole story), or a metric detached from user pain.
  • Online-to-business breaks when engagement rises while satisfaction falls - the classic clickbait failure.

The defense is not a cleverer metric - it is knowing which hop you are standing on when you read a number, and never celebrating hop-one progress as hop-three victory. Tattoo for the capstone: my eval score is a proxy for a proxy.

Demo 1 of 2

The $704 button ★ 14 min · everyone runs it

The evaluator below has two buttons. Optimize F1 sweeps every threshold and jumps to the best F1 the model can produce - the number most teams would put on the slide. Optimize $ sweeps the same thresholds and minimizes expected cost with Lumen's prices. Press both. Read what each one says about the other.

Press Optimize F1: threshold 0.61, F1 0.706, precision 100% - a gorgeous slide. Now read the cost tile: $1,850, and the verdict: $704 worse than the cost-optimal point. The metric went up. The money went down.

Press Optimize $: threshold 0.22, cost $1,146 - and look at the metrics card: F1 0.161, precision 8.8%, recall 100%. The BEST business operating point has the WORST-looking F1 on the card. If your review process kills anything with ugly metrics, it kills the right answer.

Understand WHY: a missed converter costs 37x a wasted nudge, so the money wants recall - flag broadly, tolerate cheap false alarms. F1 weighs both errors equally and refuses that trade. Neither number is lying; they optimize different worlds.

Now change the world: set FP cost to $40 (imagine each flag burns analyst time) and press Optimize $ again. Watch the optimal threshold march toward the F1 point. The cost matrix IS the decision.

Say the lesson out loud: F1-optimal is cost-optimal only when the errors cost the same - which is a business claim nobody at Lumen would sign.

Two objectives sweep the same dial and stop in different places The threshold runs 0 to 1. Same model, same scores, same 2,000 sessions - only the objective changed. cost-optimal 0.22 F1-optimal 0.61 0 0.5 default 1.0 Optimize $ - threshold 0.22 expected cost $1,146 F1 0.161 - the worst card on the table precision 8.8%, recall 100% Optimize F1 - threshold 0.61 expected cost $1,850 F1 0.706 - the gorgeous slide precision 100% $704 apart on the same slice The money wants recall here: a missed converter costs 37x a wasted nudge, so it flags broadly. F1-optimal is cost-optimal only when the two errors cost the same, which nobody at Lumen would sign.
🔍 Click to zoom - two optimizers, one dial, $704 between their answers
The honest rail Every dollar figure is computed live: 2,000 embedded Lumen sessions, fixed model scores, your prices in the boxes. The $704 gap is not scripted - change the prices and it grows, shrinks, or flips. That is the point.
Demo 2 of 2

Write Lumen's eval line the grown-up way ★ 8 min · workshop

Turn what you just saw into the one-line eval spec this course will keep reusing - one optimizing metric, satisficing constraints, prices declared. Draft it together, then stress-test it against the evaluator.

The line: "Minimize expected error cost at FP=$2 / FN=$74 on the held-out slice; subject to precision >= 8% (ops can absorb the alert volume) and no segment's recall below 80%." Every clause is a decision someone can veto - that is a feature.

Stress-test clause 1: at the cost-optimal threshold, is precision above the 8% floor? (Barely - 8.8%. If ops pushes the floor to 15%, find the new best threshold BY HAND with the slider and read what it costs: constraints have prices too.)

Note what is NOT in the line: accuracy (base-rate theater), F1 (an equal-costs assumption Lumen rejects). They can appear on a dashboard; they cannot be the target.

Real world

This line generalizes beyond classifiers. Swap the middle and it is an LLM spec: "maximize task success on the golden set; subject to faithfulness >= 0.9, TTFT <= 800ms, cost <= $0.02/query." Same architecture - one optimizing number, priced constraints. b8 fills in the latency and cost pieces; b10's capstone writes both versions in full.

Anatomy of a grown-up eval line - one optimizer, the rest are floors the one optimizing number satisficing constraints - floors and ceilings kept off the line The classifier spec Minimize expected error cost at FP=$2 / FN=$74 on the held-out slice precision >= 8% - ops absorbs the volume no segment's recall below 80% The same shape for an LLM system Maximize task success on the golden set faithfulness >= 0.9 TTFT <= 800ms cost <= $0.02 per query Kept off the line: accuracy (base-rate theater) and F1 (an equal-costs assumption Lumen rejects) One number is maximized; every other requirement is a floor or a ceiling someone can veto. Same architecture for classifiers and LLM systems - only the constraint names change.
🔍 Click to zoom - one optimizing number, the rest declared as constraints with prices
Homework

Try it yourself - this week ◐ 30-40 min total

Source material

Official sources covered

Certificates and graded assessments stay with the official providers (linked each session). This page covers:

✓sklearn threshold-tuning guide + cost-sensitive learning example (Elkan 2001, TunedThresholdClassifierCV)Parts 1-2 + both demos · the German Credit walkthrough linked as homework
✓Ng, Structuring ML Projects W1 - single-number metric + satisficing vs optimizingPart 2 + Demo 2's eval line
◐Google Rules of ML - Rule #39 (launch metrics as proxies)The two-hop chain · Rule #37's skew ladder lands in b9
◐Anthropic define-success - multidimensional criteria incl. latency + pricePreviewed in Demo 2 · b8 makes latency/cost first-class
◐Saito & Rehmsmeier 2015 - why the imbalanced curves behave this wayTaught in b3; the money consequence lands here
Check yourself

Three questions before you go 🎯 ◐ 90 seconds

1 · On Lumen's slice, the F1-optimal threshold costs $704 more than the cost-optimal one. What is the root cause?

Every summary metric embeds a cost assumption. F1's is equal costs - optimize it and you optimize that assumption. The $2-vs-$74 asymmetry wants a recall-heavy threshold F1 refuses to choose.

2 · The cost-optimal operating point shows precision 8.8% and F1 0.161. What should a review committee do?

When the miss is 37x the false alarm, the money WANTS low precision and total recall. A committee that requires pretty metrics forces the expensive threshold. (C is usually impossible - the two optima coincide only at equal costs.)

3 · In Ng's vocabulary, "latency under 200ms" and "expected cost minimized" are, respectively:

One optimizing metric for iteration speed; everything else becomes a constraint with a threshold. Anthropic's multidimensional success criteria are the same architecture for LLM systems.

Practitioner session 5 cheat sheet · pin this

Cost matrixThe confusion matrix with prices. Lumen: FP $2, FN $74. Expected cost = FP x $2 + FN x $74, minimized over thresholds.
Elkan's ruleWith calibrated probabilities the optimal threshold follows from the cost RATIO - almost never 0.5. sklearn: TunedThresholdClassifierCV.
The $704 gapF1-opt (t=0.61, $1,850) vs cost-opt (t=0.22, $1,146). Same model, same data - different hidden objective.
Goodhart's mechanismEvery summary metric bakes in a cost assumption; optimizing the metric optimizes the assumption. F1's assumption: equal costs.
Ng's architectureONE optimizing metric (make it the money) + satisficing constraints (recall floor, latency ceiling). Constraints have prices too.
Ratio over precisionRough prices are fine - the threshold moves with the ratio. If stakeholders' candidate prices pick the same threshold, stop arguing.
Two-hop chainEval score -> online metric -> business goal. Each hop breaks independently. Never celebrate hop 1 as hop 3.
The eval line"Minimize [priced cost] subject to [floors/ceilings]" - every clause vetoable by its owner. Same shape for LLM systems (b8, b10).