Classification Metrics
Precision prices a false alarm, recall prices a miss, and one threshold moves both. Every number you report is a claim about which mistake you'd rather make.
Why Does This Exist?
A support inbox takes 40,000 tickets a week. About 300 are account takeovers, a stranger inside with the recovery address changed and money moving. Those need a security engineer within the hour.
Your model flags 1,100 tickets and catches 210 of the 300.
Is that good? One number cannot say, because two questions hide inside and they disagree. Of the 1,100 you flagged, 210 were real, so 0.19. Of the 300 real ones, you caught 210, so 0.70.
Those are precision and recall. They stay separate because they price different mistakes. A false alarm costs eleven minutes of engineer time on a password reset dressed as an intrusion, plus the odd frozen account that belonged to someone innocent. A miss gives the attacker more days inside. That is why you will never ship one without the other and its threshold.
Mechanically both are one count over two totals, everything you flagged and everything that really was a takeover. That split is the diagram. The four counts themselves live in the confusion matrix, which assumes someone checked those tickets by hand to make ground truth.
This page turns those counts into precision, recall, F1 and F-beta. The precision-recall curve sweeps every threshold at once. Threshold selection is where you commit to one cut from costs. ROC and AUC give the ranking view that cannot see calibration, and model evaluation puts the metric beside the split, the threshold and the slices. The precision versus recall comparison settles which side to favour when you must pick.
Think of It Like This
The mesh size on a fishing net
You are after mackerel, and your net has one setting that matters, how wide the holes are.
Pull it tight and almost nothing escapes. You land every mackerel in the water, plus seaweed, jellyfish, bottles and undersized fish you throw back. Huge haul, mostly junk.
Open the mesh and the haul comes back clean, nearly all keepers. But most of the mackerel swam through, and you will never know how many.
One hole size does both jobs, so you cannot fix one side without moving the other. And each number alone is trivially faked. Strain the whole sea and you caught 100 percent of the mackerel. Refuse to fish and everything in your empty net is a mackerel.
Where it stops: the net cannot price your mistakes. It does not know a missed mackerel costs dinner while a false haul costs an afternoon sorting junk. Fishermen set mesh from prices and quotas, and you set thresholds the same way, from costs.
How It Actually Works
Start from the four counts in the confusion matrix. Write for catches that were real, meaning flagged and truly a takeover. Write for false alarms, meaning flagged but clean. Write for misses, meaning real but passed. Write for clean tickets correctly passed. Our week gives , , and . You can check the totals yourself, flagged is and really positive is .
Precision and recall share one cell
Precision asks, of what you flagged, how much was real. Recall asks, of what was real, how much you flagged. Same numerator, different denominator:
is precision and is recall. The diagram draws this twice. On the left the brighter column is everything flagged, 1,100 tickets, and 210 over that column is 0.19. On the right the brighter row is everything really positive, 300 tickets, and 210 over that row is 0.70. The accented cell is in both, because it is the same 210 catches.
Two baselines show why neither stands alone. Flag all 40,000 tickets and recall is exactly 1.0 while precision falls to . Flag nothing and recall is 0 while precision has no denominator at all. Both are one line of code away, which is why a report without its threshold tells you nothing.
Accuracy hides the same week in plain sight:
That reads 0.98 while 90 takeovers walk out. The F1-score definition names the fix, you must price the two errors separately.
Why F1 leans low and what beta changes
F1 is the harmonic mean of and :
The second form uses counts directly, over , so over . Harmonic sits near the smaller input and the larger cannot rescue it. Take precision 0.99 with recall 0.01, a model that flags three tickets a week and sleeps through nearly every intrusion. Ordinary averaging gives 0.50, a respectable score for something close to worthless. F1 gives .
But F1 hides a choice, that both errors matter equally. For a takeover queue that is false. F-beta makes the weight a parameter:
Here says how many times more recall counts than precision, and the weighting goes as . At you are back to F1. At recall carries four times the weight:
Choosing is a costing exercise, not a modelling one. Engineer minutes on one side, what your incident write ups say a takeover costs on the other. Report beside raw precision and recall, never instead of them.
What averaging hides with more than two classes
Same inbox, bigger job, four queues instead of yes or no. Precision and recall go per class, so a headline needs an average, and three conventions disagree.
- Macro gives every class one vote. Three queues scoring F1 0.90, 0.60 and 0.30 average to 0.60, so a queue with 3 percent of traffic counts as much as one with 86 percent.
- Micro pools all counts first, then divides once. Since each ticket is one prediction and one truth, pooled precision and recall come out identical, so micro F1 equals accuracy and follows the biggest class.
- Weighted averages by frequency. With shares 0.86, 0.11 and 0.03 the same three scores give , about 0.85. Same predictions, macro 0.60 and weighted 0.85.
Use macro when the rare queue is the reason you built the thing. Use micro or weighted when bulk throughput is the product. Either way, publish per class numbers first. An average that moves from 0.60 to 0.85 by choice of weights cannot be the only number on the card.
Code
The point is the check, same 210 catches recomputed from counts, no library needed.
def precision_recall_f1(tp: int, fp: int, fn: int) -> tuple[float, float, float]: precision: float = tp / (tp + fp) recall: float = tp / (tp + fn) f1: float = 2 * precision * recall / (precision + recall) return precision, recall, f1
tp: int = 210fp: int = 890fn: int = 90p: floatr: floatf1: floatp, r, f1 = precision_recall_f1(tp, fp, fn)print(f"{p:.3f} {r:.3f} {f1:.3f}") # -> 0.191 0.700 0.300print(f"{2 * tp}/{2 * tp + fp + fn} = {f1:.2f}") # -> 420/1400 = 0.30The printout matches the hand computation, 210 over 1,100 is 0.19, 210 over 300 is 0.70, and 420 over 1,400 is 0.30.
Watch Out For
Shipping F1 as though it were the neutral option
The symptom is a model card with one number on it and no threshold beside it. Someone asks why the takeover queue keeps missing intrusions, and the answer is a default accepted months ago, with precision and recall equally weighted. Nobody chose it. It was the shape of the function call.
F1 is a position, not a baseline. Price both mistakes, set from that ratio, and freeze the threshold on validation data. Then report that threshold beside raw precision and recall. One number cannot tell a reader which way the model fails, and threshold selection is where that choice belongs.
Quoting accuracy as evidence the rare class works
The symptom is a dashboard reading 0.98 accuracy across 40,000 tickets and the review moves on. Then takeovers turn up in the billing queue, because 90 misses hide inside 38,810 correct passes. Nothing was misreported. Accuracy divides by everything, so a class holding under 1 percent of rows contributes under 1 percent of the evidence.
Report per class precision and recall, always, and put macro beside micro so the gap shows. Then check the rare class is measurable at all. With 300 takeovers split over five folds you get 60 a fold, so six tickets move recall ten points. Stratified sampling keeps those folds comparable.
The Quick Version
- Precision is 210 catches over 1,100 flags, about 0.19. Recall is the same 210 over 300 real takeovers, exactly 0.70.
- One threshold moves precision and recall in opposite directions, and either alone can be maxed by flagging everything or nothing.
- F1 is the harmonic mean, so it sits near the weaker half. Precision 0.99 with recall 0.01 scores 0.02, not 0.50.
- F-beta makes the equal weight explicit, with recall weighted by beta squared. Beta 2 lifts this week from F1 0.30 to F2 0.46.
- Macro gives each class one vote while micro pools counts and equals accuracy, so the same predictions can read 0.60 or 0.85 by choice of average.
- Accuracy 0.98 on 40,000 tickets still misses 90 of 300 takeovers, which is why rare problems report precision, recall and their threshold together.