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

The threshold dial

Last session you learned that a classifier does not have one precision and one recall - it has a curve of them, and someone picks a point. Tonight you own the mechanism: precision and recall as formulas you can recite, the dial that trades them, F1 and why it is a harmonic mean and not a nicer-sounding average, and F-beta for when the costs lean one way. Then you drill on Lumen's real scores until you can predict every tile before you touch the slider. That fluency is the price of admission to b5, where the dial gets priced in dollars.

🟢 Practitioner track DS · analysts · ML + AI engineers Python · live evaluator on key pages 45 min
0-3 · Welcome 3-18 · P/R mechanics + F-beta 18-42 · Build-along: fluency drills 42-45 · Q&A
Part 0

Where the dial sits in the arc

b1 gave you the four cells and the accuracy paradox. This session is the middle of the classic trilogy: the threshold is the single control that trades the two error cells against each other, and precision/recall/F1/F-beta are the vocabulary for describing where you set it. b3 lifts the same idea into curves (every threshold at once); b5 replaces the vocabulary with prices. Same 2,000-session Lumen slice, same seed 42, same evaluator - tonight you learn to drive it blind.

Live - presented in session Self-study - read after class ▶ Live evaluator - real math on real scores Official sources covered
★ What you walk out with today Precision and recall as formulas you own (not look up), the raising-the-threshold mechanism said in one breath - fewer, surer flags - the reason F1 is harmonic and what that punishes, F-beta with the beta direction memorized, and the precision_recall_curve gotcha that bites everyone once.
Part 1 · covers sklearn precision/recall/F-scores + balanced_accuracy_score, Google MLCC "Accuracy, recall, precision"

Two formulas, one dial 8 min live

Both metrics share a numerator - the true positives - and differ only in what they divide by. Precision = TP / (TP + FP): of everything we flagged, how much was real? Its denominator is the model's own flag pile. Recall = TP / (TP + FN): of everything real, how much did we flag? Its denominator is fixed by the world - the 64 actual converters in Lumen's slice do not care where you put the cut. That asymmetry is the whole mechanism: raising the threshold makes fewer, surer flags. The flag pile shrinks and cleans up (precision rises), but real buyers slip under the cut (recall falls). Lower it and the trade runs backwards.

Where every session's score lands, and where the cut slices it drag the cut CONVERTERS 64 real buyers FALSE NEGATIVES missed buyers, no nudge TRUE POSITIVES caught - most scores pile here raise the cut: FN grows raise the cut: TP shrinks NON-CONVERTERS 1,936 sessions TRUE NEGATIVES left alone - the huge pile FALSE POSITIVES wasted outreach, ~$2 each raise the cut: TN grows raise the cut: FP shrinks threshold = the cut score 0.0 score 1.0 Slide right: both flag cells (TP, FP) shrink, both no-flag cells grow. Fewer, surer flags - precision up, recall down.
🔍 Click to zoom - the dial mechanism: two score strips, one cut line, four cells trading
Two averages of the same two numbers - only one tells the truth the model's two metrics arithmetic mean harmonic mean, which is F1 Precision 1.00 - both of its flags were correct Recall 0.02 - it flags two sessions all year Arithmetic mean 0.51 flattery - it says roughly half good Harmonic mean (F1) 0.039 - about as useless as the model is Bar length is the score on a 0 to 1 scale. Harmonic means are dragged toward the worse number. On Lumen's slice at the 0.50 cut: precision 21.5%, recall 63.6%, F1 0.321 - pulled toward the weak side.
🔍 Click to zoom - the arithmetic mean flatters a useless model; the harmonic mean does not
LiveF1: why harmonic, not arithmetic5 min▶

You often need precision and recall folded into one number - to sort candidate models, to put in a dashboard cell. F1 is the harmonic mean: F1 = 2PR / (P + R). The choice of mean is the entire point:

  • Take a degenerate model: precision 1.0, recall 0.02 (it flags two sessions all year, both correct). The arithmetic mean says 0.51 - "roughly half good." Flattery.
  • The harmonic mean says ~0.039 - roughly as useless as the model actually is.

Harmonic means punish lopsidedness: the score is dragged toward the WORSE of the two numbers, and a zero on either side gives F1 = 0. You cannot buy a good F1 by maxing one metric and abandoning the other - which is exactly the gaming an arithmetic mean would invite. On Lumen's slice at the default 0.50 cut: precision 21.5%, recall 63.6%, F1 0.321 - pulled down toward the weak precision, as it should be.

One F1 = a silent assumption F1 weights precision and recall equally, which quietly assumes a false positive and a false negative hurt the same amount. At Lumen they cost $2 and $74. Keep quoting F1 - it is the standard shared vocabulary - but hear the assumption every time you do. b5 removes it.
LiveF-beta: leaning the average when costs lean5 min▶

When you KNOW one error hurts more, F-beta tilts the harmonic mean. Beta scales how much recall counts relative to precision:

  • beta = 2 - recall matters more (a miss is the expensive error: fraud, disease, Lumen's $74 lost orders).
  • beta = 0.5 - precision matters more (a false alarm is the expensive error: spam filters, auto-takedowns).
  • beta = 1 is plain F1.
from sklearn.metrics import fbeta_score, balanced_accuracy_score

fbeta_score(y_test, y_pred, beta=2)    # recall-weighted: misses hurt more
fbeta_score(y_test, y_pred, beta=0.5)  # precision-weighted: false alarms hurt more
balanced_accuracy_score(y_test, y_pred)  # macro-average recall - the honest
# accuracy replacement on imbalanced data (b1's paradox, patched)
betaLeansReach for it whenLumen reading
2Recalla miss is the expensive error - fraud, disease, churnthe right lean: FN ~$74 vs FP ~$2
1Neither (plain F1)costs unknown, or genuinely equalthe silent equal-costs assumption
0.5Precisiona false alarm is the expensive error - spam, takedownsthe wrong lean for Lumen

An honest flag before you fall in love with it: F-beta is a soft version of what b5 does properly. Beta encodes "recall matters more-ish"; a cost matrix encodes "a miss costs $74 and a false alarm costs $2," and then the optimal threshold is a calculation, not a vibe. Use F-beta when you know the direction of the asymmetry but not its price. Once you have prices, graduate.

Self-studybalanced accuracy, one more look2 min read▶

balanced_accuracy_score is the macro-average of per-class recall: recall on the converters averaged with recall on the non-converters. The lazy "always predict no" model from b1 scores 96.8% on plain accuracy but exactly 0.5 on balanced accuracy - the same as coin-flipping. That single property is why sklearn's docs recommend it to avoid "inflated performance estimates on imbalanced datasets." It is the drop-in replacement when a stakeholder insists on "an accuracy number."

Part 2 · covers Ng, Advanced Learning Algorithms W3 + sklearn precision_recall_curve

Pick the point on purpose 7 min live

Andrew Ng's Advanced Learning Algorithms (week 3) turns the mechanism into doctrine, in two moves. Move one: plot the tradeoff curve and pick the operating point deliberately. The threshold is not a training artifact to inherit at 0.5 - it is a decision, and the curve is the menu of every decision available. Move two: iterate against a single-number metric. When you are comparing model candidates daily, two numbers (P and R) stall every review in argument; one number (F1, F-beta, or later b5's cost) makes "did this change help?" answerable in seconds. Pick the point deliberately, then compress it to one number so iteration stays fast - those two habits carry the rest of this course.

precision_recall_curve returns three arrays, and one of them is shorter 0 1 2 ... n-2 n-1 prec ... 1.00 rec ... 0.00 thresholds ... none The appended (precision 1.00, recall 0.00) point carries no threshold. Zip against thresholds, never against prec or rec - the shorter array sets the correct length. Off by one and every cut you report is shifted one row from the pair you think it produces.
🔍 Click to zoom - thresholds is one cell short, so zip against it
Liveprecision_recall_curve, and the gotcha in its return6 min▶

sklearn hands you the whole menu in one call - every precision/recall pair the model can reach, plus the thresholds that reach them:

from sklearn.metrics import precision_recall_curve

scores = model.predict_proba(X_test)[:, 1]
prec, rec, thresholds = precision_recall_curve(y_test, scores)

# GOTCHA: thresholds is ONE SHORTER than prec and rec.
# The final (precision=1, recall=0) point is appended with no
# threshold attached - so zip against thresholds, not the metrics:
for p, r, t in zip(prec, rec, thresholds):
    print(f"cut at {t:.2f} -> precision {p:.2f}, recall {r:.2f}")

# picking a point: e.g. the highest-recall cut with precision >= 0.30
import numpy as np
ok = np.where(prec[:-1] >= 0.30)[0]
best_t = thresholds[ok[0]]

The length mismatch is not trivia - off-by-one it and every threshold you report is shifted one row from the precision/recall pair you think it produces. You would ship a cut believing it clears a precision bar it does not clear. Zip against thresholds and the alignment is guaranteed.

Real world

The reviewed-and-still-wrong threshold memo. A retention team picked "the threshold where precision crosses 40%" from a notebook that zipped precision against the full arrays. The off-by-one shifted their chosen cut one step loose; live precision came in under the bar they had promised legal for a customer-contact program. The fix was one character in a zip. The habit: after picking a threshold from the curve, recompute P and R at that cut directly with precision_score / recall_score and confirm they match the row you selected.

Self-studySatisficing vs optimizing - the doctrine's second half2 min read▶

Ng's fuller version, from Structuring ML Projects: when several things matter, make ONE the optimizing metric and turn the rest into constraints - "maximize F2, subject to latency under 300ms." You will meet this again in b8, where latency and cost per query become first-class satisficing metrics for LLM systems, straight from Anthropic's own success-criteria list.

Demo · fluency drills

Drive the dial blind ★ 20 min · everyone runs it

Same evaluator as b1 - Lumen's 2,000 scored sessions, seed 42, every metric recomputed live on each drag. Last session you explored it; tonight you drill it. The bar is prediction, not observation: you say what the tiles will do BEFORE the slider moves, in four-cell vocabulary, and the evaluator grades you.

Drill 1 - call all five tiles. Before touching anything, write down which direction each of the five tiles (accuracy, precision, recall, F1, error cost) moves when the threshold rises from 0.50, and say each reason in cell language ("FP shrinks faster than TP, so..."). Then drag up and score yourself out of 5. Anything under 5/5: reread the strip diagram and repeat until the calls are boring.

Drill 2 - find the crossover by feel. Somewhere on the dial, precision and recall are equal. Hunt it by feel - overshoot, bracket it, close in. Notice how steep both metrics are around the crossing: small threshold moves, large metric moves. This is why "we use threshold 0.5" deserves the same scrutiny as any other shipped constant.

Drill 3 - the cost preview. Confirm the FN cost box reads 74 (Lumen's lost order). Read the error-cost tile at your crossover point, then drag left until recall hits 100% and read it again - at that floor the cost is $1,146, the cheapest point on tonight's dial even though precision has collapsed to single digits. Sit with that disagreement between the pretty-metrics point and the cheap point. Do NOT press the two buttons yet - they are b5's opening scene, and the punchline costs $704.

How to know you are fluent Fluency test, borrowed from language learning: explain to a rubber duck why recall can only fall as the threshold rises (its denominator is frozen at 64 real buyers) while precision can wobble non-monotonically on the way up (both its numerator and denominator are shrinking). If that sentence feels obvious, b2 is done its job.
Homework

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

Source material

Official sources covered

This session teaches the precision/recall core of the sklearn metrics guide, the matching Google crash-course lesson, and Ng's tradeoff doctrine - drilled on the live evaluator rather than on slides. Certificates and graded assessments stay with the official providers. This page covers:

✓sklearn metrics guide - precision_recall_fscore_support, f1_score, fbeta_score, precision_recall_curve, balanced_accuracy_scoreFormulas, the beta direction, and the thresholds-length gotcha · curves continue in b3
✓Google ML Crash Course - "Accuracy, recall, precision"The precision/recall tension, drilled live · ROC/AUC lesson lands in b3
✓Ng, Advanced Learning Algorithms W3 - the P/R tradeoff + picking the operating pointBoth doctrine moves taught · satisficing vs optimizing returns in b8
◐sklearn threshold-tuning guide - the cost-driven ending of this storyPreviewed in Drill 3 · TunedThresholdClassifierCV and Elkan in b5
Check yourself

Three questions before you go 🎯 ◐ 90 seconds

1 · Why is F1 a harmonic mean instead of a plain average of precision and recall?

The harmonic mean is dragged toward the worse number, so you cannot score well by maxing one metric and abandoning the other. An arithmetic mean would call a two-flags-a-year model "half good" - the exact gaming F1 exists to prevent.

2 · Your team decides a missed converter hurts far more than a wasted outreach email. Which F-beta leans the right way?

beta above 1 favors recall, beta below 1 favors precision - beta = 2 for expensive misses, beta = 0.5 for expensive false alarms. And remember the honest flag: once you can PRICE the asymmetry ($2 vs $74), b5's cost matrix beats any beta.

3 · precision_recall_curve returns prec, rec, and thresholds. What is the trap?

sklearn appends the (precision=1, recall=0) endpoint with no threshold attached. Zip against the full metric arrays and every reported threshold is off by one - you ship a cut that does not clear the precision bar you promised. Zip against thresholds, then re-verify the chosen point directly.

Practitioner session 2 cheat sheet · pin this

PrecisionTP / (TP + FP). "When we flag, are we right?" Denominator = the model's own flag pile, so it responds to the cut.
RecallTP / (TP + FN). "How many did we catch?" Denominator fixed by the world (Lumen: 64 real buyers) - it can only fall as the cut rises.
The mechanismRaise the threshold: fewer, surer flags. Precision up, recall down. Lower it: the trade runs backwards. One dial, four cells.
F1 = harmonic2PR/(P+R). Dragged toward the worse number; P=1.0/R=0.02 scores ~0.039, not the arithmetic 0.51. Punishes lopsidedness by design.
F-betabeta=2 leans recall (expensive misses), beta=0.5 leans precision (expensive false alarms). A soft version of b5's priced cost matrix - graduate when you have prices.
Balanced accuracyMacro-average recall. The "always no" model scores 0.5, not 96.8%. The drop-in when someone demands "an accuracy number" on imbalanced data.
The curve gotchaprecision_recall_curve's thresholds array is one shorter than prec/rec. Zip against thresholds; re-verify the chosen cut with direct metric calls.
Ng's doctrinePlot the curve, pick the operating point deliberately; then iterate on ONE number so model comparisons take seconds, not meetings.