Skip to content

8.1. Classification

A model does not have "an accuracy". It has a whole family of behaviours, one for every threshold you could put on its output, and each metric summarizes that family from a different angle. Choosing the wrong summary is not a rounding error β€” it routinely means shipping a model that is worthless for the job it was built for.

So this page is organized around a single question: what decision are you making, and what does a mistake cost you? Every metric below is an answer to some version of that question, and the point is to know which one is asking yours.

Why accuracy is a trap

Start with a screening test for a disease that affects 1 in 100 people. Here is a model:

def predict(patient):
    return "healthy"          # always

On 10 000 patients it gets 9 900 right. 99% accuracy β€” better than most published models, and it would pass any dashboard that reports a single number. It also misses every single sick patient, so as a screening test it has no value whatsoever.

This is not a contrived example, it is the normal situation. Fraud is rare. Equipment failure is rare. Rare is exactly when detection matters, and accuracy is at its most misleading precisely there: the majority class drowns the number you are reading.

The first question to ask about any accuracy figure

What does the trivial classifier get? If 95% of your data is one class, then 95% accuracy is the score of a constant function. Any metric that a return 0 baseline can beat is not measuring your model.

The confusion matrix: where every metric comes from

All of it β€” every metric in this chapter β€” is four numbers arranged in a square. Predict "sick" for someone who is sick and you have a true positive; predict "sick" for someone healthy and you have a false positive1.

Actually positive Actually negative
Predicted positive TP β€” hit FP β€” false alarm (Type I)
Predicted negative FN β€” miss (Type II) TN β€” correct rejection

The two errors are not interchangeable, and their names in your domain tell you which metric to reach for:

Domain A false positive is… A false negative is… Which hurts more
Cancer screening an unnecessary biopsy: cost, anxiety a tumour sent home undiagnosed FN, by far
Spam filter a real email lost in the spam folder an annoying message in the inbox FP, by far
Fraud detection a legitimate purchase declined a fraudulent charge approved depends on the amounts
Search / retrieval an irrelevant result on page 1 a relevant document never shown usually FP at the top

Precision and recall exist because those two columns need to be read separately:

\[ \text{Precision} = \frac{TP}{TP + FP} \qquad\qquad \text{Recall} = \frac{TP}{TP + FN} \]

The formulas look alike and the difference is the entire subject, so read them as questions instead:

  • Precision β€” of everything I flagged, how much was real? It divides by what the model said. It is the metric of the person acting on the alerts.
  • Recall β€” of everything real, how much did I catch? It divides by what reality contains. It is the metric of the person who will suffer the misses.

A mnemonic that actually survives the exam

Look at the denominator. Precision is over predicted positives β€” your claims. Recall is over actual positives β€” the truth. A model that flags one single case and gets it right has 100% precision and terrible recall. A model that flags everyone has 100% recall and precision equal to the prevalence.

Everything moves when you move the threshold

A classifier does not output a class. It outputs a score, and someone chose a cut-off β€” usually 0.5, usually without thinking. That choice is not part of the model; it is part of the decision, and it is where precision and recall trade against each other.

Below: two score distributions, negatives in blue and positives in orange. Drag the threshold and watch the confusion matrix, every metric, and the position on both curves move together.

Three experiments, in order:

  1. Leave prevalence at 50% and sweep the threshold. Precision and recall move in opposite directions β€” always. Lower the threshold and you catch more positives (recall ↑) while flagging more innocents (precision ↓). There is no setting where both are maximal; there is only the setting that suits your costs.
  2. Set prevalence to 1% and leave the threshold at 0.50. Accuracy reads about 0.505 while "always predict negative" scores 0.990. The simulator says so in red. Every metric that mixes the two classes into one number does this.
  3. Keep the model fixed and change only the prevalence. ROC-AUC does not move β€” 0.921 at 50%, at 10%, at 1%. The PR-AUC collapses: 0.899 β†’ 0.643 β†’ 0.248. The model did not get worse; the task did, and only one of the two curves noticed.

That third experiment is the whole argument about which curve to use, so it is worth stating why it happens. ROC plots recall against the false positive rate, \(FP/(FP+TN)\) β€” and its denominator is the negatives, which is exactly the class that grows when positives become rare. Ten false alarms out of a million negatives is an invisible FPR. Precision divides those same ten false alarms by the handful of true positives, where they are catastrophic. When positives are rare and false alarms are expensive, ROC will flatter your model and PR will tell you the truth23.

What AUC-ROC actually measures

It has an exact interpretation, and it is not "accuracy across thresholds":

\[ \text{AUC} = P\big(\,s(x^+) > s(x^-)\,\big) \]

the probability that a randomly chosen positive gets a higher score than a randomly chosen negative. AUC 0.92 means: pick one sick and one healthy patient at random, and the model ranks them correctly 92% of the time. It measures ranking4, and it never looks at your threshold or at whether the probabilities are calibrated.

That is also its blind spot. A model can rank perfectly (AUC = 1.0) and still output 0.51 for every positive and 0.49 for every negative β€” useless probabilities, perfect ranking.

F1, and when the harmonic mean is the wrong average

\[ F_1 = 2\cdot\frac{P \cdot R}{P + R} \]

The harmonic mean is deliberately unforgiving: it sits close to the smaller of the two. With \(P = 1.0\) and \(R = 0.01\) the arithmetic mean is a respectable 0.505, while \(F_1 = 0.0198\). That is the point β€” F1 refuses to let a model hide a catastrophic recall behind excellent precision.

But F1 encodes a specific opinion β€” that precision and recall matter equally β€” and that opinion is usually wrong. \(F_\beta\) lets you state yours:

\[ F_\beta = (1+\beta^2)\cdot\frac{P \cdot R}{\beta^2 P + R} \]

\(\beta\) is how many times more you care about recall than precision. \(F_2\) (recall twice as important) is standard in medical screening; \(F_{0.5}\) (precision twice as important) in recommendation and search. If you can name the ratio of costs, you should not be using \(F_1\).

F1 ignores the true negatives entirely

Look at the formula: TN appears nowhere. That is a feature when negatives are an uninteresting ocean (retrieval: documents you did not return), and a bug when they are not. For a balanced problem where being right about negatives is part of the job, MCC (Matthews correlation) uses all four cells and is much harder to fool:

\[ \text{MCC} = \frac{TP \cdot TN - FP \cdot FN}{\sqrt{(TP+FP)(TP+FN)(TN+FP)(TN+FN)}} \]

It runs from βˆ’1 to 1, and it is only high when the model does well on both classes5. Set the simulator to 1% prevalence and compare F1 and MCC as you move the threshold.

Which metric, and why

Your situation Use Because
Balanced classes, errors cost the same Accuracy It is what you mean, and nothing is hiding
Missing a positive is expensive (disease, defect, fraud) Recall, \(F_2\) The cost is in the FN column; optimize the column
A false alarm is expensive (spam, moderation, alerts) Precision, \(F_{0.5}\) The cost is in the FP column
Rare positives, comparing models PR-AUC, average precision ROC hides false alarms behind a huge TN count
Comparing rankers, roughly balanced ROC-AUC Threshold-free and prevalence-invariant
You need the probability, not the label Log loss, Brier, calibration curve Ranking metrics do not check whether 0.8 means 80%
Imbalanced multi-class Macro-F1, balanced accuracy Gives the rare classes a vote
Reporting one number to a stakeholder Confusion matrix Give them four; one number always hides something

Multi-class: micro, macro, weighted

Beyond two classes, "the F1" no longer exists β€” you have to say how the per-class scores get averaged, and the choice changes the answer completely. Take a 1 000-sample problem with classes A (900), B (90) and C (10):

Class Precision Recall F1 Support
A 0.963 0.978 0.970 900
B 0.755 0.611 0.675 90
C 0.232 0.300 0.261 10

Those are the numbers in the matrix below. Move the recall of any class and watch which of the three averages notices:

Averaging Value What it does When to use
Micro 0.938 Pools all TP/FP/FN, then computes once β€” equals accuracy here You care about overall correctness, every sample equal
Macro 0.636 Plain mean of the per-class F1s β€” the rare class counts as much as A You care about every class, especially rare ones
Weighted 0.937 Mean weighted by support β€” A dominates Rarely what you want; it reproduces the imbalance

Read those three numbers again. Micro says 0.938, weighted says 0.937 β€” and the model gets class C right 3 times out of 10. Macro is the only one that reports the failure, and the gap between macro and weighted is a direct measure of how much worse you do on the rare classes. If you report one number for an imbalanced multi-class problem, report macro.

Probabilities, not just labels

Ranking and thresholding both ignore a question that matters in production: when the model says 0.8, does the event happen 80% of the time? A model can rank perfectly and still be systematically overconfident β€” and if a human or a downstream system consumes those numbers as probabilities, the error is silent and expensive.

\[ \text{LogLoss} = -\frac{1}{N}\sum_{i=1}^{N}\big[y_i \log \hat{p}_i + (1-y_i)\log(1-\hat{p}_i)\big] \]

Punishes confident mistakes without mercy: predicting 0.01 for something that happens costs \(-\log(0.01) = 4.6\), while predicting 0.4 costs \(0.92\). One confident error can dominate the whole average β€” which is why it is the standard training loss but a jumpy reporting metric.

\[ \text{Brier} = \frac{1}{N}\sum_{i=1}^{N}(\hat{p}_i - y_i)^2 \]

The mean squared error of the probabilities6. Bounded in \([0,1]\), far less sensitive to a single confident mistake than log loss, and it decomposes cleanly into calibration + refinement. Good for reporting.

Bucket the predictions by predicted probability and plot, for each bucket, the observed frequency. A calibrated model lies on the diagonal. Deep networks are famously overconfident7, and the standard fix is temperature scaling: divide the logits by a single scalar \(T\) fitted on validation data. It changes no ranking β€” AUC is untouched β€” and can cut calibration error by an order of magnitude.

Metrics and losses are not the same thing

The page you are reading is about metrics: what you report and what you select models by. A loss is what gradient descent minimizes, and it has an extra requirement β€” it must be differentiable. Accuracy, F1 and AUC are all flat or discontinuous almost everywhere, so none of them can be trained on directly.

That is why you train with cross-entropy and report F1. When they disagree β€” validation loss rising while F1 still improves, say β€” it usually means confidence is degrading faster than ranking, which is a calibration problem, not a ranking one.

The threshold is a business decision, and it has a formula

"Use 0.5" is a default inherited from argmax, not a decision. If you can say what a miss costs relative to a false alarm, the optimal cut-off follows exactly. Predicting positive on a case whose true probability is \(p\) has expected cost \(C_{FP}(1-p)\); predicting negative costs \(C_{FN}\,p\). Flagging is worth it when the first is smaller8, which happens when

\[ p \;>\; p^\star = \frac{C_{FP}}{C_{FP} + C_{FN}} \]

Read it: if a miss costs 9Γ— a false alarm, then \(p^\star = 1/10\) β€” flag anything above 10%, not 50%. The 0.5 default is only correct when the two errors cost the same, which is almost never why it was chosen.

Note what the simulator does not need: a better model. Same weights, same scores, same everything β€” only the cut-off moves, and the expected cost changes by tens of percent. Threshold selection is usually the cheapest improvement available to a deployed model, and it is the one most often left at its default.

How to pick a threshold honestly

  1. Ask what each error costs. Money, hours, risk β€” any consistent unit works.
  2. Compute \(p^\star = C_{FP}/(C_{FP}+C_{FN})\), or sweep the threshold on a validation split and minimize the cost directly if you cannot price things exactly.
  3. Check the model is calibrated first. \(p^\star\) is a statement about probabilities; on uncalibrated scores it is arbitrary.
  4. Report the confusion matrix at the chosen threshold, not just the AUC. AUC describes the whole family of models you did not ship.

In code

Every metric above has one call in scikit-learn9, and the order of the calls is the argument of this page: rank first, then choose a threshold from costs, then report the matrix.

from sklearn.metrics import (classification_report, confusion_matrix,
                             roc_auc_score, average_precision_score,
                             precision_recall_curve, brier_score_loss)

probs = model.predict_proba(X_val)[:, 1]

# threshold-free: the two summaries, and they answer different questions
print("ROC-AUC:", roc_auc_score(y_val, probs))          # ranking quality
print("PR-AUC :", average_precision_score(y_val, probs)) # ranking quality where positives are rare

# pick the threshold from costs, on validation data β€” never on test
prec, rec, thr = precision_recall_curve(y_val, probs)
C_FP, C_FN = 1.0, 10.0
p_star = C_FP / (C_FP + C_FN)                            # = 0.0909...

pred = (probs >= p_star).astype(int)
print(confusion_matrix(y_val, pred))
print(classification_report(y_val, pred, digits=3))      # per-class P/R/F1 + macro & weighted

# is 0.8 really 80%?
print("Brier:", brier_score_loss(y_val, probs))

Two mistakes this code is arranged to avoid

Tuning the threshold on the test set. The cut-off is a parameter like any other; choose it on validation, report it on test, or your test number is optimistic.

classification_report without average. Its bottom rows give macro and weighted. Read the macro row for imbalanced problems, and read the per-class rows before either.

Key takeaways

  1. Accuracy answers "how often am I right", which is the wrong question whenever the classes are imbalanced or the two errors cost different amounts. Always compare it against the always-predict-the-majority baseline.
  2. Every metric here is four numbers in a square. When in doubt, look at the confusion matrix; it is the only report that cannot hide anything.
  3. Precision divides by what you claimed, recall by what is true. Which one matters follows from which error hurts.
  4. ROC-AUC measures ranking and is invariant to prevalence β€” which is a virtue when comparing models and a trap when positives are rare. PR-AUC is the honest one there.
  5. F1 assumes precision and recall matter equally. If you can name the ratio, use \(F_\beta\); if negatives matter too, use MCC.
  6. In multi-class, the averaging is the metric. Macro is the one that notices rare classes.
  7. Ranking is not calibration. If a human or a system consumes the probability, measure it β€” log loss, Brier, a calibration curve β€” and fix it with temperature scaling.
  8. The threshold is not part of the model. It comes from \(p^\star = C_{FP}/(C_{FP}+C_{FN})\), and moving it is usually the cheapest win available.

Additional Resources

  1. ROC and AUC, Clearly Explained! β€” Starmer, J., StatQuest. Builds the ROC curve threshold by threshold, which is exactly what the first simulator animates. Watch it if the curve still feels like a formula rather than a picture:

  2. Metrics and scoring: quantifying the quality of predictions β€” scikit-learn User Guide. The reference for what each function actually computes, including the averaging rules that decide what f1_score means in the multi-class case.

  3. Classification: Accuracy, recall, precision β€” Google Machine Learning Crash Course. A second pass over the same trade-offs with different worked examples, and a good source of practice questions.

References

The works cited through the text, in order of appearance:


  1. Confusion matrix β€” Wikipedia. A useful map of the two dozen names the four cells and their ratios go by (sensitivity, specificity, PPV, NPV, TPR, FPR…). β†©

  2. Davis, J., & Goadrich, M. (2006). The Relationship Between Precision-Recall and ROC Curves β€” ICML. Proves that a curve dominating in ROC space dominates in PR space too, and that the converse fails β€” which is why PR separates models that ROC ranks as equals. β†©

  3. Saito, T., & Rehmsmeier, M. (2015). The Precision-Recall Plot Is More Informative than the ROC Plot When Evaluating Binary Classifiers on Imbalanced Datasets β€” PLOS ONE 10(3). The measured version of the third experiment above. β†©

  4. Fawcett, T. (2006). An introduction to ROC analysis β€” Pattern Recognition Letters 27(8), 861–874. The standard reference, including the ranking interpretation of AUC and the convex-hull view of choosing operating points. β†©

  5. Chicco, D., & Jurman, G. (2020). The advantages of the Matthews correlation coefficient (MCC) over F1 score and accuracy in binary classification evaluation β€” BMC Genomics 21(6). Worked cases where F1 and accuracy look fine and MCC does not. β†©

  6. Brier, G. W. (1950). Verification of forecasts expressed in terms of probability β€” Monthly Weather Review 78(1), 1–3. From weather forecasting, where being asked what "70% chance of rain" means came up early. β†©

  7. Guo, C., Pleiss, G., Sun, Y., & Weinberger, K. Q. (2017). On Calibration of Modern Neural Networks β€” ICML. Documents that modern networks are far more overconfident than the shallow ones that preceded them, and introduces temperature scaling. β†©

  8. Elkan, C. (2001). The Foundations of Cost-Sensitive Learning β€” IJCAI. Derives the optimal threshold from the cost matrix, and how to rescale probabilities when the training prevalence differs from the deployment one. β†©

  9. Metrics and scoring β€” scikit-learn User Guide. The exact definition of every function used above, including the micro/macro/weighted averaging rules. β†©