ml-models / Data and evaluation / Classification metrics intro

Classification metrics

A classifier makes two kinds of mistake, and a single "accuracy" number hides which one it is making. This page covers the confusion matrix, precision, recall and F1, the ROC and precision–recall curves and the areas under them, log loss for probabilities, how to choose a threshold, and the difference between micro and macro averaging.

On this page
  1. Problem
  2. Intuition
  3. Mechanics
  4. Counts and ratios
  5. Scores, thresholds and curves
  6. Log loss
  7. Choosing a threshold
  8. Many classes: micro versus macro
  9. Worked example
  10. Ten scored examples
  11. Sweeping the threshold
  12. AUC two ways
  13. Average precision
  14. Accuracy when positives are rare
  15. Micro versus macro on three classes
  16. Implementation
  17. Tradeoffs
  18. Check yourself
  19. Connections
  20. Read first
  21. Read next
  22. References

Problem

Suppose one email in a hundred is a phishing attempt. A filter that labels every email "fine" is right 99% of the time and catches nothing. Accuracy answers "how often was the label right?", which is rarely the question you care about. A hospital wants to know how many sick patients the screen finds and how many healthy ones it frightens; a search engine wants to know how many of the top results are good; a fraud team wants a ranked list it can work down until the budget runs out.

Each of these is a different summary of the same raw material: how the model's predictions line up with the truth, and, for models that output a score, how that alignment changes as you move the cutoff. Choosing the metric is choosing what to optimize, so it deserves the same care as choosing the model.

Intuition

Think of a metal detector at a beach. Turn the sensitivity up and it finds every coin, but it also beeps at bottle caps and you spend your day digging junk. Turn it down and every beep is a real coin, but you walk past coins. Recall is the share of the coins you found; precision is the share of beeps that were coins. Sensitivity is the threshold, and no setting gets both to 100% unless the detector is perfect.

A ROC curve and a precision–recall curve are simply the record of what happens at every sensitivity setting, so you can judge the detector before deciding where to set the dial. The area under a curve compresses that record into one number when you need to compare detectors.

The confusion matrix for a threshold of 0.5 A two by two grid. Rows are the true class, columns the predicted class. True positives 3, false negatives 1, false positives 2, true negatives 4. To the right, the formulas: precision is true positives over predicted positives (the first column); recall is true positives over actual positives (the first row). predicted positive predicted negative actual positive actual negative TP = 3 found FN = 1 missed FP = 2 false alarm TN = 4 correctly ignored precision = TP / (TP + FP) = 0.60 recall = TP / (TP + FN) = 0.75 F1 = 2TP / (2TP + FP + FN) = 0.67 accuracy = (TP + TN) / n = 0.70

Every threshold metric is a ratio of these four counts.

Mechanics

Counts and ratios

For a binary problem, mark one class positive. A prediction is one of four things: a true positive (TP), false positive (FP), false negative (FN) or true negative (TN).

  • Accuracy = (TP + TN) / n. Honest only when the classes are balanced and the two mistakes cost about the same.
  • Precision = TP / (TP + FP): of what the model flagged, how much was right. Falls when you tolerate more false alarms.
  • Recall (sensitivity, true-positive rate) = TP / (TP + FN): of what was out there, how much did the model find.
  • False-positive rate = FP / (FP + TN); specificity = 1 − FPR.
  • F1 is the harmonic mean of precision and recall, 2PR / (P + R) = 2TP / (2TP + FP + FN). The harmonic mean is dragged toward the smaller number, so you cannot hide a poor recall behind a high precision. fbeta_score weights recall β times as much as precision.
  • When a denominator is zero (no predicted positives, so precision is 0/0), scikit-learn's zero_division parameter decides the value; the default "warn" returns 0 and raises a warning.

Scores, thresholds and curves

Most models output a score or probability, and a label requires a threshold. Sweeping the threshold from high to low traces out a path. The ROC curve plots recall (TPR) against the false-positive rate. A random scorer follows the diagonal; a perfect one hugs the top-left corner. The area under it (AUC) has a clean meaning: the probability that a randomly chosen positive gets a higher score than a randomly chosen negative, counting ties as half. It only depends on the ordering of the scores, so it ignores calibration and any threshold.

The precision–recall curve plots precision against recall. Precision depends on the class balance, which ROC's FPR hides: with 1,000 negatives per positive, a 1% false-positive rate is ten false alarms for every real hit, and the ROC curve still looks excellent. When positives are rare and you care about the flagged list, the PR curve is the more informative one. Average precision summarizes it as AP = Σ (Rn − Rn−1) Pn, the precision at each threshold weighted by the recall gained there. scikit-learn's implementation is a step sum, not the trapezoid area, because linear interpolation between PR points is too optimistic.

Log loss

If the model outputs probabilities, log loss (cross-entropy) scores them directly: −(1/n) Σ [y log p + (1 − y) log(1 − p)]. It is the quantity logistic regression and neural classifiers minimize (see loss functions). A confident wrong answer is punished without bound: predicting 0.01 for something that happens costs 4.6 nats, while 0.99 for something that happens and 0.01 for something that does not together cost only 0.01 nats on average. scikit-learn clips probabilities to [ε, 1 − ε] with ε the machine epsilon of the dtype, so a stated certainty never produces an infinity.

Choosing a threshold

The threshold is a decision, separate from the model. Set it from costs: if a missed fraud costs 100 times a false alarm, flag far more cases than the 0.5 default would. Or set it from capacity: review the top 50 a day, which is a precision-at-k decision. Or set it from a requirement: the highest threshold that still gives 95% recall. Choose it on validation data, never on the test set (see data leakage), and re-check it when class balance shifts.

Many classes: micro versus macro

With more than two classes, compute each class's metric one-vs-rest and then combine. Macro averaging takes the unweighted mean of the per-class scores, so a rare class counts as much as a common one. Micro pools all the TP, FP and FN first and computes one ratio, so common classes dominate (for single-label problems micro-F1 equals accuracy). Weighted averaging weights the per-class scores by support. Report macro when every class matters, micro when each example matters equally.

ROC curve and precision–recall curve for ten scored examples Left, a ROC curve of false-positive rate against true-positive rate rising in steps from the origin to the top right, above the diagonal, with an area of 0.875. Right, a precision–recall curve of precision against recall that starts at precision 1.0 at recall 0.25 and falls to 0.4 at recall 1.0. false-positive rate ROC, AUC = 0.875 recall precision–recall, AP = 0.854

The same ten examples. Each dot is one threshold; the vertical axes are true-positive rate on the left and precision on the right.

ComponentShape/ParamsMeaningSource
confusion_matrix y_true, y_pred: (n,); returns (n_classes, n_classes) Rows are true classes, columns predicted classes; the counts behind every threshold metric. _classification.py
accuracy_score normalize=True The fraction of exact label matches. _classification.py
precision_score / recall_score average="binary", pos_label=1, zero_division="warn" TP / (TP + FP) and TP / (TP + FN); average selects micro, macro, weighted or samples for many classes. precision, recall
f1_score average="binary", zero_division="warn" 2TP / (2TP + FP + FN), the harmonic mean of precision and recall. _classification.py
roc_curve y_true, y_score: (n,); drop_intermediate=True FPR, TPR and the thresholds at which they change; the corner points that add no shape are dropped by default. _ranking.py
roc_auc_score binary needs scores; max_fpr; multi_class for many classes Builds roc_curve and applies the trapezoid rule via auc. roc_auc_score, auc
precision_recall_curve y_true, y_score: (n,); drop_intermediate=False Precision and recall per threshold; a final point (precision 1, recall 0) is appended with no threshold. _ranking.py
average_precision_score average="macro", pos_label=1 Σ (Rn − Rn−1) Pn, not interpolated. _ranking.py
log_loss y_true: (n,); y_proba: (n,) or (n, n_classes) Mean negative log-likelihood; probabilities clipped to [ε, 1 − ε]. log_loss, _log_loss

Worked example

Ten scored examples

Four of ten examples are positive. The model's scores in descending order are 0.95, 0.85, 0.8, 0.65, 0.55, 0.45, 0.4, 0.3, 0.2, 0.1 and their true labels, in the same order, are 1, 1, 0, 1, 0, 1, 0, 0, 0, 0. Reading the scores as probabilities, log loss is 0.505.

Sweeping the threshold

Call an example positive when its score is at least the threshold:

ThresholdTPFPFNTNPrecisionRecallF1
0.910361.000.250.40
0.721250.670.500.57
0.532140.600.750.67
0.3543030.571.000.73
0.1545010.441.000.62

Lowering the threshold never reduces recall and usually lowers precision. F1 peaks at 0.35 here (0.73), which finds all four positives at the price of three false alarms.

AUC two ways

Walking the ten thresholds gives the ROC points in the left plot; the trapezoid area is 0.875. Counting directly: there are 4 positives and 6 negatives, so 24 positive–negative pairs; the positive outscores the negative in 21 of them, and 21/24 = 0.875. The two routes agree because AUC is that pair fraction.

Average precision

At each distinct threshold the precision–recall pair is (0.25, 0.5, 0.5, 0.75, 0.75, 1.0, 1.0, 1.0, 1.0, 1.0) for recall and (1.0, 1.0, 0.67, 0.75, 0.6, 0.67, 0.57, 0.5, 0.44, 0.4) for precision. Weighting each precision by the recall gained gives AP = 0.854: recall rises by 0.25 at four of the thresholds where a positive appears, and the precision at those moments is 1.00, 1.00, 0.75 and 0.67 (0.25 × 3.42 = 0.854). Notice AP (0.854) is not the ROC area (0.875): the two curves reward different things.

Accuracy when positives are rare

A hundred patients, five of them sick, and a model that says "healthy" every time: TP 0, FP 0, FN 5, TN 95. Accuracy is 0.95; precision is 0/0, reported as 0, and recall is 0.00. A 95% accurate model that finds nobody is exactly what the other metrics are for.

Micro versus macro on three classes

Per-class (TP, FP, FN) counts are cat = 8, 2, 2; dog = 1, 1, 4; bird = 0, 0, 3. Per-class F1 values are cat 0.80, dog 0.29, bird 0.00. The macro average is 0.36, pulled down by the two rare classes. Pooling the counts first (TP 9, FP 3, FN 9) gives micro F1 = 18 / 30 = 0.60, dominated by the common class. Neither is wrong; they answer different questions.

Implementation

from sklearn.metrics import (
    confusion_matrix, precision_score, recall_score, f1_score,
    roc_auc_score, roc_curve, average_precision_score,
    precision_recall_curve, log_loss,
)

y_true  = [1, 1, 0, 1, 0, 1, 0, 0, 0, 0]
y_score = [0.95, 0.85, 0.80, 0.65, 0.55, 0.45, 0.40, 0.30, 0.20, 0.10]
y_pred  = [int(s >= 0.5) for s in y_score]

confusion_matrix(y_true, y_pred)          # [[tn, fp], [fn, tp]]
precision_score(y_true, y_pred)           # tp / (tp + fp)
recall_score(y_true, y_pred)              # tp / (tp + fn)
f1_score(y_true, y_pred)                  # 2tp / (2tp + fp + fn)

roc_auc_score(y_true, y_score)            # needs scores, not labels
average_precision_score(y_true, y_score)  # step sum, not the trapezoid
fpr, tpr, thresholds = roc_curve(y_true, y_score)
precision, recall, thresholds = precision_recall_curve(y_true, y_score)

log_loss(y_true, y_score)                 # needs probabilities
f1_score(y_multi, y_pred_multi, average="macro")   # or "micro", "weighted"

Note the row and column order of confusion_matrix: rows are true classes and columns are predicted classes, sorted by label, so for labels 0 and 1 the layout is [[TN, FP], [FN, TP]].

Tradeoffs

  • ROC versus PR. ROC is insensitive to class balance, which is both a virtue (comparable across datasets) and a trap (it can look excellent when the flagged list is mostly false alarms). Use PR or precision-at-k when positives are rare and you act on the top of the list.
  • Threshold-free versus threshold-based. AUC and AP rank models without a cutoff but say nothing about performance at the cutoff you will deploy. Report a threshold metric at the operating point too.
  • F1 treats the two errors alike. If a miss is much worse than a false alarm, use F-beta with β > 1 or, better, a cost-weighted score; F1 also ignores true negatives entirely.
  • Log loss versus accuracy. Log loss rewards well-calibrated probabilities and punishes overconfidence, while accuracy and AUC ignore how sure the model was. They can disagree about which of two models is better (see loss functions).
  • Variance. With few positives every metric is noisy. A recall measured on 20 positives has a standard error near 0.1; report intervals or cross-validated spreads, not a bare number.
  • Averaging choices. Macro, micro and weighted answer different questions and can differ by a factor of two, as above. State which one you report.
  • Metric versus loss. Most of these metrics are not differentiable, so models are trained on a surrogate (log loss) and judged on another; the gap between them is a real risk to monitor.

Check yourself

A model has precision 1.0 and recall 0.25. What is its F1?

2 × 1.0 × 0.25 / (1.0 + 0.25) = 0.4. The harmonic mean is closer to the smaller number than the arithmetic mean (0.625) would be.

Why can a model with a very high AUC still have poor precision at the threshold you deploy?

AUC measures ranking over all thresholds and is blind to class balance. With rare positives, even a small false-positive rate produces many more false alarms than hits at your operating point.

You scale all of a model's scores by 0.5 without changing their order. What changes?

AUC and average precision do not change, since they depend only on ordering. A fixed threshold of 0.5 now gives different counts, and log loss changes.

Connections

References

  • Fawcett. An Introduction to ROC Analysis. Pattern Recognition Letters, 2006.
  • Davis and Goadrich. The Relationship Between Precision-Recall and ROC Curves. ICML, 2006.
  • Saito and Rehmsmeier. The Precision-Recall Plot Is More Informative than the ROC Plot When Evaluating Binary Classifiers on Imbalanced Datasets. PLOS ONE, 2015.
  • scikit-learn 1.9.1: f1_score, roc_auc_score, average_precision_score, log_loss.