Skip to content

Machine Learning Foundations

Evaluating classifiers: precision, recall, and ROC

Measuring a classifier past raw accuracy

A model can be 99% accurate and completely useless. The confusion matrix, the precision-recall trade-off, and the threshold knob are how you find out which.

Your spam filter reports 97% accuracy. Sounds great - until you notice that 97% of the mail was ham anyway, so a filter that flags nothing would score the same and catch zero spam. Accuracy hides this failure completely. To see what a classifier actually does you need four raw counts - the confusion matrix - and two numbers built from them, precision and recall, that pull in opposite directions as you turn one knob: the decision threshold. Sweep that knob across every setting and you trace the ROC curve, a single picture of how good the model really is.

The one idea

A classifier from Lesson 18 doesn't output "spam" or "ham" - it outputs a probability. You pick a cut-off; scores above it become "spam." Every choice of cut-off makes a different set of mistakes. Precision asks "of what I flagged, how much was right?" Recall asks "of what I should have caught, how much did I get?" You cannot maximise both at once - tightening one loosens the other. Judging a classifier means seeing that whole trade-off, not one lucky point on it.

The confusion matrix

Fix a threshold and every prediction lands in one of four bins, tallied in a 2×2 table. Rows are the truth, columns are the prediction:

  • TP (true positive) - real spam, flagged spam. ✔
  • FP (false positive) - real ham, flagged spam. A good email in the junk folder.
  • FN (false negative) - real spam, flagged ham. Spam slips into the inbox.
  • TN (true negative) - real ham, flagged ham. ✔

The diagonal (TP, TN) is what you got right; the off-diagonal (FP, FN) is every mistake, split by kind. From these four counts, three definitions:

Precision, recall, accuracy

Precision reads down the "flagged spam" column - of everything you flagged, what fraction was truly spam. Recall reads across the "real spam" row - of all the spam that existed, what fraction you caught (also called the true positive rate). When you need one number, the F1 score is their harmonic mean, , which stays low unless both are high.

Why accuracy lies on rare classes

Accuracy divides correct predictions by all predictions - so it is dominated by whichever class is biggest. Imagine a fraud detector where only of transactions are fraud. A model that outputs "legit" for every transaction achieves accuracy while catching zero fraud: its recall is . The same trap hits medical screening, spam, anomaly detection, and safety filters - anywhere the class you care about is rare, accuracy will happily hide total failure behind a big number.

On imbalanced data, always look past accuracy

Recall exposes the failure accuracy conceals: with , recall is no matter how high accuracy climbs. This is exactly why we report precision and recall (or the full ROC curve) instead of a single accuracy score. In the demo below, drag the threshold all the way right so nothing is flagged - accuracy stays a respectable ~63% while recall collapses to 0%. That gap is the lie, made visible.

The threshold knob and the ROC curve

The model's probability is fixed; the threshold is yours to set. Slide it toward and you flag only your most confident predictions - precision rises, recall falls. Slide it toward and you flag almost everything - recall rises, precision falls. Plot two rates as you sweep the threshold through every value - the true positive rate (recall) against the false positive rate - and you get the ROC curve. Its area, the AUC, summarises the whole model in one number.

ROC axes and AUC

Each threshold is one point ; sweeping the threshold traces the curve from - flag nothing - to - flag everything. The AUC is the area beneath it. A perfect model reaches the top-left corner, ; random guessing sits on the diagonal, . Beautifully, the AUC equals the probability that a randomly chosen positive is scored above a randomly chosen negative - a threshold-free measure of how well the model ranks.

Below are 24 emails a model has already scored - 9 real spam (top lane), 15 real ham (bottom lane) - placed on the probability axis. Drag the blue threshold line (or the slider). Watch the confusion matrix, the precision/recall bars, and the moving dot on the ROC curve all update from that single motion. Dots turn green when classified correctly, red when they become an error.

Threshold → confusion → ROC, in one motion. drag the strip or use the slider.
actualSPAM +actualHAM −← predict HAMpredict SPAM →0.000.250.500.751.00model's predicted probability of spamt = 0.50
correct (TP / TN)error (FP / FN)threshold - drag itshaded = predicted spam
pred.
spam
pred.
ham
real
spam
7TP
2FN
real
ham
4FP
11TN
Precision - of flagged, % real spam64%
Recall - of real spam, % caught78%
Accuracy - % of all cases correct75%
False Positive Rate = FP / all hamTrue Positive Rate (recall)011AUC = 0.82

The whole ROC curve is fixed by the model; the blue dot is your threshold. Sweep it and the dot rides the staircase. Shaded area = AUC.

At t = 0.50 the filter flags 11 of 24 as spam - precision 64% · recall 78% · accuracy 75%.

The aha: one model, a whole line of operating points

The ROC curve doesn't move as you drag - it belongs to the model. What you are choosing is a single operating point on it, and the right point depends on the cost of each mistake. For a spam filter, a false positive (a real email lost to the junk folder) is far worse than a false negative, so you favour precision - push the threshold high. For cancer screening, a false negative (a missed tumour) is catastrophic, so you favour recall - push it low. Same model, same AUC, different threshold. The AUC tells you how good the model can ever be; the threshold is where you decide to live on it.
import numpy as np

# fixed model outputs: P(spam) for 24 emails, and the true labels (1 = spam)
scores = np.array([.97,.90,.83,.77,.71,.64,.56,.47,.34,           # 9 real spam
                   .04,.08,.11,.15,.19,.23,.27,.32,.37,.43,.49,.57,.66,.78,.88])  # 15 real ham
labels = np.array([1]*9 + [0]*15)

def confusion(t):
    pred = scores >= t                        # decide: flag as spam?
    tp = np.sum(pred & (labels == 1))
    fp = np.sum(pred & (labels == 0))
    fn = np.sum(~pred & (labels == 1))
    tn = np.sum(~pred & (labels == 0))
    return tp, fp, fn, tn

tp, fp, fn, tn = confusion(0.5)
precision = tp / (tp + fp)                     # of flagged, how many were spam  -> 0.64
recall    = tp / (tp + fn)                     # of real spam, how much we caught -> 0.78

# sweep every threshold to trace the ROC curve, then integrate for the AUC
ts  = np.sort(np.unique(scores))[::-1]
tpr = [confusion(t)[0] / (labels == 1).sum() for t in ts]
fpr = [confusion(t)[1] / (labels == 0).sum() for t in ts]
auc = np.trapz(tpr, fpr)                     # area under the ROC -> 0.82
The four counts, precision/recall, and the ROC sweep in NumPy

Check yourself

On data that is 99% negative, a model that always predicts "negative" gets:

Raising the decision threshold flags fewer cases, which typically:

From the confusion matrix, define precision and recall - and say what the ROC curve and its AUC measure.

Retrieve the four counts and the curve they build. Try to state it, then check.

Lock it in

  • Accuracy hides failure on imbalanced data: a detector that always predicts the majority class scores while catching none of the rare class (recall ).
  • The confusion matrix's four counts build (of what you flagged, how much was right) and (of what existed, how much you caught); F1 is their harmonic mean, low unless both are high.
  • Precision and recall trade off through one knob, the threshold: raise it and precision rises while recall falls; lower it and the reverse.
  • Sweeping the threshold traces the ROC curve; its AUC ( perfect, chance) equals the probability a random positive outranks a random negative - a threshold-free measure of ranking.
  • Choose the operating point by the cost of each mistake: a spam filter favours precision, cancer screening favours recall. Same model, same AUC, different threshold.

Primary source

Josh Starmer's StatQuest playlist has the clearest visual walkthroughs anywhere of the confusion matrix and of ROC & AUC - watch those two episodes back to back. For the definitions, the metric formulas, and a hands-on threshold widget, work through the classification section of Google's Machine Learning Crash Course.

Ask your teacher

This is exactly how you would judge a real spam filter - or an LLM safety classifier like the ones behind Lesson 58 that decide whether a prompt or response is harmful. Both live on the same knife-edge: set the threshold too low and you block harmless requests (false positives, angry users); too high and harmful ones slip through (false negatives). Accuracy would hide that failure, because unsafe prompts are rare - precision and recall expose it. Ask me about the precision-recall curve (better than ROC when positives are very rare), how to pick a threshold from a cost matrix, why AUC can look great while a chosen operating point is poor, or how F1 and its weighted cousins fold both numbers into one.