Condados
← All posts The Computer Vision Metrics Handbook · Chapter 1 of 6 View all →

Classification Metrics, From the Ground Up: Precision, Recall, F1, ROC and AUC

The confusion matrix and everything that falls out of it — precision, recall, F1, specificity, the precision/recall trade-off, PR and ROC curves, and AUC — built on one running toy example (a backyard cat-cam), with the equations, the intuition, and scikit-learn.

Luis Condados · · Updated June 6, 2026
Classification Metrics, From the Ground Up: Precision, Recall, F1, ROC and AUC

Almost every evaluation number you’ll ever report — for detectors, trackers, segmenters, even generative models — is built from four counts on a binary decision. Get the confusion matrix and the precision/recall trade-off right once, and the rest of the field stops being a zoo of acronyms. We’ll build all of it on one tiny running example — a backyard cat camera — so every formula comes with a number you can check by hand.

The running example: Whiskers, the backyard cat-cam

Meet Whiskers, a wildlife camera pointed at a backyard. Its one job: snap a photo whenever a cat walks by. Last night it scored 10 photos, each with a confidence that there’s a cat in frame. By morning you check what was really in each shot. Here’s the night’s log — we’ll compute every metric in this article from exactly this table:

PhotoWhat was really thereCat? (truth)Whiskers’ score
#1cat under the floodlight✅ yes0.95
#2cat on the fence✅ yes0.88
#3cat by the bins✅ yes0.72
#4cat at the door✅ yes0.60
#5cat in deep shadow✅ yes0.45
#6cat half-out of frame✅ yes0.20
#7a raccoon❌ no0.55
#8empty bush❌ no0.40
#9empty lawn❌ no0.15
#10a swaying shadow❌ no0.30

So 6 photos truly have a cat, 4 don’t. To turn scores into yes/no decisions we need a threshold — start with the usual 0.5: flag a photo as “cat” when the score is ≥ 0.5. That flags photos #1, #2, #3, #4 (real cats) and #7 (the raccoon — a 0.55 false alarm). Photos #5 and #6 are real cats Whiskers slept through. Hold this picture; every metric below is just a different question about it.

The confusion matrix

Intuition: sort every photo into one of four bins — did we flag it, and was it really a cat? Everything else is arithmetic on those four counts.

For a binary classifier, every prediction lands in one of four cells, comparing the predicted label against the truth:

Actual positiveActual negative
Pred. positiveTrue Positive (TP)False Positive (FP)
Pred. negativeFalse Negative (FN)True Negative (TN)

A false positive is a false alarm; a false negative is a miss. Which one hurts more is a domain question — a spam filter and a cancer screen weight them very differently — and that choice is what every metric below is really about.

What was really therecatno catflagged: catflagged: no catTP4real cats, caughtFP1false alarm (raccoon)FN2cats missedTN3correctly ignored
Whiskers’ night, sorted. The green diagonal is “got it right”; the red off-diagonal is the two ways to be wrong — a false alarm (FP) and a miss (FN).

Accuracy, and why it lies

Intuition: of all 10 photos, what fraction did Whiskers get right?

The obvious metric is the fraction of correct predictions:

Accuracy=TP+TNTP+TN+FP+FN.\text{Accuracy} = \frac{TP + TN}{TP + TN + FP + FN}.

The trap is class imbalance. Suppose the backyard were quieter and only 1 of 100 photos had a cat. A lazy camera that never flags anything scores 99% accuracy while catching zero cats — its TN count alone carries the score. Accuracy hides the failure because the easy negatives dominate the sum. You need metrics that ignore the easy negatives — which is exactly precision and recall.

Precision and recall

These two split “was the model right?” into two different questions.

Intuition — precision: when Whiskers cries “cat!”, how often is it right? It punishes false alarms.

Precision=TPTP+FP.\text{Precision} = \frac{TP}{TP + FP}.

Intuition — recall (sensitivity, true-positive rate): of all the cats that actually walked by, how many did we catch? It punishes misses.

Recall=TPTP+FN.\text{Recall} = \frac{TP}{TP + FN}.

The picture makes the two denominators obvious — precision divides by the flagged set, recall by the actual cats:

actually cats (6)flagged as cat (5)2missed (FN)4caught (TP)1false alarm (FP)recall = 4 ⁄ 6 ≈ 0.67precision = 4 ⁄ 5 = 0.80
Same 4 true positives, two different denominators: recall divides by the green set (all real cats), precision by the blue set (everything flagged).

They pull in opposite directions. Flag everything → recall 1.0, precision in the gutter. Flag only your single most-confident shot → precision 1.0, recall near zero. The art is the balance, and it’s set by the threshold (next section).

Two more counts come from the negative column, used in ROC analysis:

Specificity (TNR)=TNTN+FP,FPR=FPFP+TN=1Specificity.\text{Specificity (TNR)} = \frac{TN}{TN + FP}, \qquad \text{FPR} = \frac{FP}{FP + TN} = 1 - \text{Specificity}.

For Whiskers: specificity =3/(3+1)=0.75= 3/(3+1) = 0.75 (it correctly ignored 3 of the 4 non-cats), so FPR =0.25= 0.25.

F1 and Fβ: one number from two

Intuition: report a single score that stays low unless precision and recall are both decent — so you can’t hide a terrible recall behind a great precision.

That’s the harmonic mean, which (unlike the plain average) is dragged toward the smaller of the two:

F1=2PrecisionRecallPrecision+Recall.F_1 = 2 \cdot \frac{\text{Precision} \cdot \text{Recall}}{\text{Precision} + \text{Recall}}.

When the two errors aren’t equally costly, weights recall β times as much as precision (β=2\beta = 2 favours recall, β=0.5\beta = 0.5 favours precision):

Fβ=(1+β2)PrecisionRecallβ2Precision+Recall.F_\beta = (1 + \beta^2)\,\frac{\text{Precision} \cdot \text{Recall}}{\beta^2 \cdot \text{Precision} + \text{Recall}}.

For Whiskers, if missing cats is the real sin we’d track F2 ≈ 0.69 (pulled toward the weaker recall); if false 3 a.m. alerts are worse, F0.5 ≈ 0.77 (pulled toward the stronger precision).

The threshold, and the curves that free you from it

Every number above came from one choice: threshold 0.5. Slide that threshold and the whole confusion matrix changes. Here’s Whiskers’ night laid out by score — cats above the line, non-cats below, the threshold as a movable wall (flag everything to its right):

0.00.51.0threshold 0.5 → flag to the rightcat #5, missedraccoon, false alarmcat (6)no cat (4)
The two dots straddling the wall tell the story: a real cat at 0.45 falls just left (missed), the raccoon at 0.55 just right (false alarm). Move the wall and you trade one kind of error for the other.

A single F1 only describes one wall position. To judge the model itself, sweep the threshold:

Precision–Recall curve and Average Precision

Plot precision against recall across all thresholds and you get the PR curve. The summary number is Average Precision (AP) — the area under it, a sum over the points where recall steps up:

AP=n(RnRn1)Pn.\text{AP} = \sum_{n} (R_n - R_{n-1})\, P_n.

ROC curve and AUC

Plot true-positive rate (recall) against false-positive rate across thresholds; the area under it is AUC (AUROC). It has a beautifully concrete meaning:

AUC is the probability that the model scores a random cat higher than a random non-cat.

So 0.5 is random guessing and 1.0 is perfect ranking. AUC is threshold-free and great for ranking quality — but on very imbalanced data it looks optimistically high (FPR has a huge TN denominator), so prefer PR-AUC / AP there.

Now you try

Reading is one thing; moving the numbers is another. The playground below starts on Whiskers’ exact night. Drag the threshold and watch the confusion matrix recompute while the live dot traces the PR and ROC curves — find the wall position that catches the shadow-cat without letting the raccoon in. Then switch to Edit the counts and push TP/FP/FN/TN around to see how each metric reacts (try making precision 1.0 and watch what happens to recall).

Things worth confirming for yourself: at threshold 0 everything is flagged (recall 1.0, precision in the gutter); push it to 1.0 and you flag nothing (recall 0); and no slider position beats the raccoon — it always costs you either a false alarm or a missed cat. That stubbornness is the 0.83 AUC.

In scikit-learn

The same Whiskers numbers, computed for real — the printed values match every hand calculation above:

import numpy as np
from sklearn.metrics import (
    confusion_matrix, precision_score, recall_score, f1_score,
    roc_auc_score, average_precision_score, classification_report,
)

# Whiskers' night: photos #1..#10, 1 = a cat is really there.
y_true = np.array([1, 1, 1, 1, 1, 1, 0, 0, 0, 0])
y_prob = np.array([.95, .88, .72, .60, .45, .20, .55, .40, .15, .30])  # scores
y_pred = (y_prob >= 0.5).astype(int)                                   # threshold

tn, fp, fn, tp = confusion_matrix(y_true, y_pred).ravel()
print(f"TP={tp} FP={fp} FN={fn} TN={tn}")     # TP=4 FP=1 FN=2 TN=3
print("precision", precision_score(y_true, y_pred))   # 0.80
print("recall   ", recall_score(y_true, y_pred))      # 0.667
print("f1       ", f1_score(y_true, y_pred))           # 0.727

# Threshold-free, computed from the scores (not the 0/1 predictions):
print("ROC-AUC", roc_auc_score(y_true, y_prob))            # 0.833
print("PR-AUC ", average_precision_score(y_true, y_prob))  # 0.917

print(classification_report(y_true, y_pred, digits=3))

The key habit: pass scores to roc_auc_score / average_precision_score and hard labels to precision/recall/F1. Mixing them up is the most common metrics bug there is.

Caveats & common pitfalls

  • Every score sits on top of the labels. If you’d mislabeled the raccoon as a cat, Whiskers’ “precision” would rise to 1.0 while the camera got no better — noisy ground truth caps and distorts every number. Audit the labels before chasing the last point.
  • One threshold = one operating point. A lone precision/recall/F1 describes a single wall position; always report the threshold you used (or the curve).
  • ROC-AUC flatters imbalanced data. Its FPR has a huge true-negative denominator, so it stays high even when the model is useless on the minority class — prefer PR-AUC / AP there [1].
  • Discrimination ≠ calibration. AUC and AP measure ranking; they say nothing about whether a “0.8” really means 80%. If you act on probabilities, check calibration separately [3].

Takeaways

  • Everything starts at the confusion matrix. TP/FP/FN/TN, and which error costs more — Whiskers was 4/1/2/3.
  • Accuracy is unreliable under imbalance (0.70 here, but 99% for a camera that never fires on a quiet night) — reach for precision/recall.
  • Precision (0.80) vs recall (0.67) is a trade-off set by the threshold; F1/Fβ combine them at one operating point.
  • To judge the model across thresholds, use curves: PR-AUC / AP (≈0.92 here) under imbalance, ROC-AUC (≈0.83) for general ranking quality.
  • Feed scores to AUC/AP and labels to P/R/F1.

Next in the series: how these scale to many classes and to multi-label problems → Multiclass & multilabel classification metrics.

Further reading

References

[1] Davis, J., & Goadrich, M. (2006). The Relationship Between Precision-Recall and ROC Curves. Proceedings of the 23rd International Conference on Machine Learning (ICML). DOI:10.1145/1143844.1143874.

[2] Fawcett, T. (2006). An Introduction to ROC Analysis. Pattern Recognition Letters, 27(8), 861–874. DOI:10.1016/j.patrec.2005.10.010.

[3] Hastie, T., Tibshirani, R., & Friedman, J. (2009). The Elements of Statistical Learning (2nd ed.), ch. 2 & 7. Springer. Free PDF.

[4] Bishop, C. M. (2006). Pattern Recognition and Machine Learning, §1.5 & §4.2. Springer.

[5] Pedregosa, F., et al. (2011). Scikit-learn: Machine Learning in Python. JMLR, 12, 2825–2830. Docs: Classification metrics.