S092 · Evaluation Metrics — P/R/F1/ROC/PR/AUC
Accuracy is a lie 90% of the time. Learn why precision/recall/F1, ROC-AUC, and PR-AUC exist, when each one matters, and how to pick the metric your business actually cares about — before your model ships and gets your team paged at 3am.
🎯 Pick the right evaluation metric for a real business problem, defend the choice with a confusion matrix, and read ROC/PR curves like a pro.
Why this session exists
Every ML failure story starts the same way: "The model had 99% accuracy in dev." Then it shipped, and it turned out 99% of transactions were legit — so a model that just always says "not fraud" also scores 99%. The wrong metric doesn't just mislead you; it actively hides the fact that your model is useless. This session is the metric literacy that separates people who ship ML in production from people who ship ML in slides.
- Draw a confusion matrix from a set of predictions and read TP/FP/FN/TN off it without hesitation.
- Choose precision, recall, F1, ROC-AUC, or PR-AUC given a business problem — and defend the pick.
- Explain why ROC-AUC is a trap on heavily imbalanced data and when PR-AUC replaces it.
- Tune a classification threshold to hit a business-defined precision or recall target.
- Compute all of the above in scikit-learn and interpret the resulting curves.
Prerequisites
- S088 · Supervised vs Unsupervised — you know classification vs regression.
- S090 · Logistic Regression — you know your classifier outputs probabilities, not just labels.
- Basic pandas + numpy from S030–S034.
(a) Intuition · 5 min
You're the operator. Two knobs: sensitivity (how easily it beeps) and selectivity (how specific to metal it is). Crank sensitivity high and you catch every weapon — but also every belt buckle and every foil gum wrapper. Crank selectivity high and you never annoy grandma — but you might miss a real knife.
You can't have both maxed. There's a knob. The knob is a threshold. The whole game is: what does it cost to miss a knife vs stop a grandma?
Every classifier outputs a probability between 0 and 1. You pick a threshold (default 0.5) above which you call it "positive." Move the threshold up → fewer positives, higher precision, lower recall. Move it down → more positives, higher recall, lower precision.
ROC and PR curves are what happens if you sweep the threshold from 0 to 1 and plot every point. AUC is the area under that curve — a single number that says "how good is the model across all possible thresholds."
The two questions every metric answers
- Precision — of everything I flagged as positive, what fraction was actually positive? (‘When I say fraud, how often am I right?’)
- Recall — of everything that was actually positive, what fraction did I catch? (‘Of all the real fraud, how much did I flag?’)
- F1 — the harmonic mean of the two. Punishes any model that scores well on only one. Use when you need one number and both matter roughly equally.
- ROC-AUC — probability that a random positive is ranked above a random negative. Great on balanced data, misleading on rare events.
- PR-AUC — the same idea but on the precision-recall curve. Use this on imbalanced data.
A quick history so you know why we have five metrics
- 1904Signal detection theory · RayleighPhysicists study how humans detect faint signals against noise — the origin of TP/FP/FN/TN.
- 1941WWII radar operators · ROC born‘Receiver Operating Characteristic’ literally means how well an operator distinguishes real planes from noise. Curves plotted by hand.
- 1955Cranfield IR experiments · precision & recallCleverdon at Cranfield introduces precision and recall to measure information-retrieval systems (early search).
- 1979F-measure · van RijsbergenCombines precision and recall into a single number. Later called F1 when β=1.
- 2006Davis & Goadrich · PR > ROC on imbalanced dataICML paper proves ROC hides poor performance on rare-class problems. PR-AUC becomes the norm for fraud/medical models.
(b) Visual walkthrough · 15 min
The confusion matrix — the single most important diagram in classification
Every metric on the planet is a ratio built from these four cells. Learn the picture — the formulas follow for free.
How ROC and PR curves are built
Model outputs a probability p for each example. You do not yet call anything positive.
Highest confidence first. This is the ‘ranking’ view of the model.
At each threshold, everything above is positive; below is negative. Compute a confusion matrix.
ROC plots FPR on x, TPR on y. PR plots recall on x, precision on y. One point per threshold.
Random model = 0.5 on ROC, base rate on PR. Perfect model = 1.0 on both.
When to use which curve
Balanced data, ranking matters
- x = False Positive Rate (FP / N), y = True Positive Rate (TP / P)
- Robust to class balance shifts (invariant if positives/negatives change)
- 0.5 = random; 1.0 = perfect
- Use for: churn (~30% positive), spam (~50%), diabetes screening (~10%)
- Trap: on 1-in-1000 problems it inflates because FPR stays tiny even for terrible models.
Imbalanced data, precision matters
- x = Recall (TP / P), y = Precision (TP / (TP+FP))
- Sensitive to class balance — that's the point
- Baseline = base rate of positives (0.001 for 1-in-1000 fraud)
- Use for: fraud, ad click, disease detection, ML anomaly, security alerts
- Trap: harder to interpret across datasets because baseline shifts.
One number, threshold already chosen
- Harmonic mean of precision and recall
- Requires you to commit to a threshold first
- Fβ variant: β>1 weights recall more, β<1 weights precision
- Use for: model comparison in a report, once the operating point is fixed
- Trap: says nothing about ranking quality.
The threshold–metric trade-off
"AUC-ROC is the standard way to judge a classifier. A model with 0.95 AUC is excellent, and if I need one number to compare models, that's the one."
ROC-AUC is computed from true positive rate and false positive rate, and FPR has the negative count in its denominator. When negatives vastly outnumber positives, thousands of false positives barely move FPR — so AUC stays beautiful while the alerts your team actually receives are overwhelmingly wrong. For rare-event problems, precision-recall AUC is the honest curve, because precision has predicted positives in its denominator and therefore feels every false alarm.
The myth is sticky because ROC-AUC has a genuinely elegant property nothing else offers: it is invariant to class balance, so it measures pure ranking quality and is comparable across datasets. That invariance is exactly what makes it excellent for research comparison — and exactly what makes it misleading for an operational decision, because the thing you deliberately factored out (the base rate) is the thing that determines whether your on-call engineer drowns. The metric is not wrong; it answers a different question than the one being asked.
Same predictions, two curves, radically different verdicts on a 1% positive rate:
from sklearn.metrics import roc_auc_score, average_precision_score
from sklearn.metrics import precision_score, recall_score
p = model.predict_proba(Xte)[:, 1]
print('ROC-AUC ', roc_auc_score(yte, p)) # looks great
print('PR-AUC ', average_precision_score(yte, p)) # the honest one
print('baseline PR-AUC =', yte.mean()) # random guessing
yhat = p >= 0.5
print(precision_score(yte, yhat), recall_score(yte, yhat))
# PR-AUC's floor is the positive rate, not 0.5.
# 0.30 PR-AUC at a 1% base rate is a 30x lift, not a failure.Why can you not maximise precision and recall at the same time? This is stated everywhere as a "tradeoff", but it is not an empirical tendency — it is forced by the structure of a thresholded score.
- 1A classifier produces a score, and a decision requires a threshold t. Predicting positive means score ≥ t, so the set of predicted positives is completely determined by t.forced by · any binary decision from a continuous score is a cut point, and nothing else
- 2Lowering t can only add items to the predicted-positive set; it can never remove one. The sets are therefore nested as t decreases.forced by · the condition score ≥ t is monotone in t — this is a set-inclusion fact, not a statistical one
- 3Recall is caught positives ÷ all positives. Its denominator is fixed, and its numerator can only grow as the set grows. So recall is monotonically non-decreasing as you lower the threshold. No exceptions, no dataset dependence.forced by · the denominator is a property of the data, untouched by your threshold
- 4Precision is caught positives ÷ all predicted positives. Both parts grow, so its direction depends on the quality of each newly admitted item. Items are admitted in descending score order, and any model better than random has a positive rate that declines as you go down the ranking.forced by · ranking quality is precisely the statement that higher-scored items are more likely positive
- 5Therefore each item admitted is, on average, less likely to be a true positive than everything already in the set — so it dilutes the ratio. Precision trends downward as recall rises, and the two are pinned to a single curve traced by one parameter.forced by · adding elements of below-average quality to a set must lower its average
Therefore the tradeoff is not a limitation of your model — it is a consequence of both metrics being functions of one threshold on one ranking. A single model does not have a precision or a recall; it has a curve, and picking a point on it is a business decision.
And note what this predicts: the only way to improve both simultaneously is to change the ranking itself — better features, better model — which lifts the entire curve. It also predicts that comparing two models by their F1 at threshold 0.5 is nearly meaningless: you are comparing two arbitrary points on two curves, and the model with the worse point may have the strictly better curve. Compare curves (PR-AUC), then choose the point.
There are exactly four numbers: TP, FP, FN, TN. Every metric you will ever argue about is a ratio built from them, and each one differs only in which cell it puts in the denominator — which is the same as choosing which mistake you have decided to care about.
Precision divides by the predicted-positive column: it asks "when I raise an alarm, how often am I right?" — the cost of a false positive. Recall divides by the actual-positive row: "of the real cases, how many did I catch?" — the cost of a false negative. Accuracy divides by everything, which is why it silently ignores the class you care about when that class is rare.
- Never report accuracy on imbalanced data. At a 1% positive rate, predicting all-negative scores 99% and is worthless. Always compare against the majority-class baseline.
- Pick the metric from the asymmetry of real costs: a missed cancer screening and a false fraud block have wildly different price tags, and the metric should reflect that, not convention.
- F1 is the harmonic mean of precision and recall, so it weights them equally — an assumption you almost never actually hold. Use Fβ when you can state the ratio of costs.
- Threshold-free metrics (ROC-AUC, PR-AUC) evaluate the ranking. Threshold metrics evaluate a decision. Report both, because they answer different questions and improving one may not move the other.
Fire this the moment you see: accuracy quoted for a rare event · a single F1 at threshold 0.5 used to pick between models · ROC-AUC on a heavily imbalanced problem with no PR curve alongside it · a metric chosen before anyone stated the cost of each error type · a model evaluated on a rebalanced test set (the test set must keep the real base rate).
You are building a fraud detector where positives are rare. Which metric drives model selection and threshold choice?
Use PR-AUC to select the model and precision@k to set the threshold, where k is your team's actual daily review capacity. That pairing separates the two decisions correctly: the curve is a property of the model, the point on it is a property of your operations. Reporting ROC-AUC alongside is fine as a stable tracking number, provided nobody mistakes it for an operational one.
The framing that resolves most metric arguments: stop asking "which metric is best" and start asking "what does a false positive cost, and what does a false negative cost". Once those two numbers exist — even roughly — the metric choice and the threshold both fall out, and the debate ends. When they genuinely cannot be stated, precision at fixed capacity is the honest fallback, because capacity is a constraint you can always measure.
(c) Hands-on · 25 min
Build and score a classifier on a real imbalanced dataset (breast cancer + a synthetic 1-in-100 fraud set), print every metric, and draw both curves. Save as metrics_lab.py inside your s092/ folder and uv run metrics_lab.py.
"""metrics_lab.py — classification metrics from first principles + sklearn.
Runs two experiments:
1. Breast cancer (roughly balanced) — ROC-AUC is fine.
2. Synthetic fraud (1% positives) — ROC-AUC lies; PR-AUC tells the truth.
"""
from __future__ import annotations
import numpy as np
from sklearn.datasets import load_breast_cancer, make_classification
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import (
confusion_matrix,
precision_score,
recall_score,
f1_score,
accuracy_score,
roc_auc_score,
average_precision_score,
precision_recall_curve,
roc_curve,
classification_report,
)
RNG = 42
def report(name: str, y_true, y_pred, y_prob) -> None:
"""Print a compact metrics report — everything a code review needs."""
tn, fp, fn, tp = confusion_matrix(y_true, y_pred).ravel()
print(f"\n=== {name} ===")
print(f" n = {len(y_true):>5} positives = {int(y_true.sum()):>4}"
f" base rate = {y_true.mean():.3f}")
print(f" TP={tp:>4} FP={fp:>4} FN={fn:>4} TN={tn:>4}")
print(f" accuracy = {accuracy_score(y_true, y_pred):.3f}")
print(f" precision = {precision_score(y_true, y_pred, zero_division=0):.3f}")
print(f" recall = {recall_score(y_true, y_pred, zero_division=0):.3f}")
print(f" f1 = {f1_score(y_true, y_pred, zero_division=0):.3f}")
print(f" ROC-AUC = {roc_auc_score(y_true, y_prob):.3f}")
print(f" PR-AUC = {average_precision_score(y_true, y_prob):.3f}")
def tune_for_recall(y_true, y_prob, target_recall: float = 0.95) -> float:
"""Return the threshold that hits at least target_recall with best precision."""
prec, rec, thr = precision_recall_curve(y_true, y_prob)
# last elements of prec/rec correspond to threshold=+inf, no threshold there
mask = rec[:-1] >= target_recall
if not mask.any():
return 0.0
# among all thresholds that meet recall, pick the one with highest precision
idx = np.argmax(prec[:-1][mask])
return float(thr[mask][idx])
def experiment_balanced() -> None:
X, y = load_breast_cancer(return_X_y=True)
Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.3, random_state=RNG, stratify=y)
clf = LogisticRegression(max_iter=5000).fit(Xtr, ytr)
prob = clf.predict_proba(Xte)[:, 1]
pred = (prob >= 0.5).astype(int)
report("Breast cancer — threshold 0.5", yte, pred, prob)
def experiment_imbalanced() -> None:
X, y = make_classification(
n_samples=10_000, n_features=20, weights=[0.99, 0.01],
random_state=RNG, n_informative=5,
)
Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.3, random_state=RNG, stratify=y)
clf = LogisticRegression(max_iter=5000, class_weight=None).fit(Xtr, ytr)
prob = clf.predict_proba(Xte)[:, 1]
# A: default threshold — will look great on accuracy, awful on recall
pred_default = (prob >= 0.5).astype(int)
report("Synthetic fraud — threshold 0.5 (naive)", yte, pred_default, prob)
# B: threshold tuned to reach 95% recall — see precision collapse
thr = tune_for_recall(yte, prob, target_recall=0.95)
pred_recall = (prob >= thr).astype(int)
print(f"\n → threshold moved to {thr:.3f} to hit recall ≥ 0.95")
report(f"Synthetic fraud — threshold {thr:.3f} (recall-tuned)", yte, pred_recall, prob)
# Full sklearn text report for the recall-tuned model
print("\n" + classification_report(yte, pred_recall, digits=3))
if __name__ == "__main__":
experiment_balanced()
experiment_imbalanced()Anatomy of the script
Anatomy of the script
Edit experiment_imbalanced and change weights=[0.99, 0.01] to weights=[0.999, 0.001] (a 1-in-1000 problem — closer to real credit card fraud).
Re-run and report both ROC-AUC and PR-AUC. Then answer: which of the two numbers would you put in your model's ship / no-ship review, and why?
(d) Production reality · 15 min
Early competitors submitted models with ROC-AUC of 0.98 and celebrated. The dataset has 0.17% positives. A model that always predicts "not fraud" scores accuracy of 99.83% and ROC-AUC of 0.5. Some submissions were only marginally better than the trivial baseline while looking spectacular.
Kaggle eventually switched the leaderboard metric to PR-AUC (Average Precision). Suddenly a lot of "great" models fell from 0.98 to 0.65 — the honest number.
Early Gmail spam classifiers optimised for F1. Problem: a false positive (legit email in spam folder) is catastrophically worse than a false negative (spam in inbox — annoying but harmless). F1 weights them equally.
Users would lose interview replies, wedding invitations, medical results. Gmail's Net Promoter Score dropped every time the model got "better" by F1.
Switched to a precision-at-target-recall operating point: "hold recall at ~95%, maximise precision at that recall." Equivalently, tune the threshold so precision is at least, say, 99.9% — a false positive rate less than 1 in 1,000.
This is the pattern behind the tune_for_recall function in the hands-on script.
Two changes: (1) evaluate on data with the deployment class balance, not the training balance; (2) calibrate probabilities (Platt scaling or isotonic regression) and set a threshold so specificity is at least 0.95 in the deployment population.
Where this shows up in the rest of the plan
(e) Recall + stretch · 10 min
Explain-out-loud test
If you can't teach these three to a friend without notes, redo the session:
- What is a confusion matrix, and what are TP/FP/FN/TN? (draw it)
- When does accuracy lie? (one concrete example, with numbers)
- When do you use PR-AUC instead of ROC-AUC? (and why)
What comes next
Hub: The 6-Month Learning Plan
Part of a 130-session evergreen learning series. Session structure: intuition → visual → hands-on → production war stories → recall. Duration: 90 minutes.