ailiteracynepal 🇳🇵
Text size

Chapter 04 · Section II · 24 min read

Precision, recall, and the confusion matrix

The vocabulary every honest ML report uses. Precision and recall tell different stories about classification quality. The confusion matrix shows where the mistakes live. Together they replace 'accuracy' as the right way to evaluate a classifier.

Accuracy is one number. Precision and recall are two numbers — and the difference between them is where every interesting classification problem lives. Whether you should optimise for one or the other depends on what kind of mistake costs more. This section gives you the vocabulary to ask that question precisely, the confusion matrix to see your model’s mistakes laid out clearly, and the F1 score that gives you one summary number when you really need just one.

The confusion matrix — the foundation

Every other metric in this section is computed from the confusion matrix, so we start there. For binary classification, the matrix is 2×2:

                    Predicted
                  No        Yes
            ┌─────────┬─────────┐
   No       │   TN    │   FP    │   ← Actual: No (the model said Yes wrongly)
            ├─────────┼─────────┤
   Yes      │   FN    │   TP    │   ← Actual: Yes (the model said No wrongly)
            └─────────┴─────────┘

Four cells, four important names:

  • TP (True Positive) — model said yes, truth said yes. ✓
  • TN (True Negative) — model said no, truth said no. ✓
  • FP (False Positive) — model said yes, truth said no. ✗ (“false alarm”)
  • FN (False Negative) — model said no, truth said yes. ✗ (“missed it”)

Accuracy is (TP + TN) / (TP + TN + FP + FN). The 95%-accurate-but-useless model from the last section has high TN, low TP, low FP, and high FN. The accuracy averages over them, hiding the fact that recall is terrible.

In scikit-learn:

from sklearn.metrics import confusion_matrix
print(confusion_matrix(y_test, predictions))
# [[955  10]
#  [ 11 139]]

The output is [[TN, FP], [FN, TP]]. With confusion_matrix(..., labels=[1, 0]) you can flip the order if you want positives on top — useful when “positive” is the rare class you care about.

Precision and recall, formally

Two ratios computed from the confusion matrix, each with a specific meaning:

precision = TP / (TP + FP)    →  of the cases the model said "yes", what fraction were actually yes?
recall    = TP / (TP + FN)    →  of the cases that actually were "yes", what fraction did the model find?

Both range from 0 to 1. Both equal 1 in the perfect case.

In plain language:

  • Precision answers “when the model raises an alarm, how often is it right?” Higher precision = fewer false alarms.
  • Recall answers “how many of the real positives does the model actually catch?” Higher recall = fewer misses.

Examples to make it stick:

  • A spam classifier with high precision rarely flags ham as spam — but might miss some real spam. The user trusts every flag (precision), but some spam still gets through (low recall).
  • A loan-default classifier with high recall catches almost every actual defaulter — but also flags many non-defaulters as risky. The lender catches every bad loan (recall), but reviews many false alarms (low precision).

You almost never optimise for both simultaneously. You trade off.

The trade-off, made visible

For any probabilistic classifier (logistic regression, random forest, etc.), the prediction is “yes” when the predicted probability exceeds a threshold (default 0.5). Raising the threshold makes the model more conservative (higher precision, lower recall). Lowering the threshold makes it more aggressive (lower precision, higher recall).

import numpy as np
from sklearn.metrics import precision_score, recall_score

probabilities = model.predict_proba(X_test)[:, 1]   # probability of positive class

for threshold in [0.3, 0.5, 0.7]:
    preds = (probabilities >= threshold).astype(int)
    p = precision_score(y_test, preds)
    r = recall_score(y_test, preds)
    print(f"threshold={threshold}: precision={p:.3f}  recall={r:.3f}")

Output (illustrative):

threshold=0.3: precision=0.62  recall=0.88
threshold=0.5: precision=0.71  recall=0.62
threshold=0.7: precision=0.84  recall=0.41

Same model. Three threshold choices. Three different precision/recall trade-offs. The “right” threshold depends on which mistake costs more — which is a business question, not a technical one.

When to optimise for what

A short rules-of-thumb table:

ApplicationCare more aboutWhy
Disease screening (cheap follow-up test)RecallMissing a real case is much worse than a false alarm
Fraud flagging (manual review queue)PrecisionFalse alarms waste analyst time
Spam filterBalanceFalse positives lose real email; false negatives are annoying
Loan default flagging (high-trust lender)RecallA missed default loses real money
Loan default flagging (high-volume retailer)PrecisionToo many false flags slow down approval
Resume screeningPrecision (and audit per-group)False positives waste interviews; false negatives reject good candidates
Search engine rankingPrecision at the topUsers rarely look past the first few results

Notice that almost every line is context-dependent. There is no universal “you should care about precision” or “you should care about recall.” Defining which one matters more — and saying so explicitly — is part of framing the problem.

F1: one number when you need just one

When you need to report a single quality number — say, for a hyperparameter search that compares dozens of models — use the F1 score, the harmonic mean of precision and recall:

F1 = 2 * (precision * recall) / (precision + recall)

In scikit-learn:

from sklearn.metrics import f1_score
f1 = f1_score(y_test, predictions)

F1 ranges 0 to 1 like precision and recall, and rewards models that have both high precision AND high recall — it punishes a model that gets one right but tanks the other. It’s the right single number for imbalanced classification when you don’t yet know what threshold you’ll deploy at.

For multi-class problems, scikit-learn computes F1 per class and averages, with two flavors:

  • average="macro" — unweighted average across classes. Treats all classes equally.
  • average="weighted" — weighted by support (number of samples per class). Reflects overall performance.

For imbalanced multi-class, macro-F1 is usually more informative.

classification_report — the daily-driver summary

scikit-learn’s classification_report computes precision, recall, F1, and support per class in one call:

from sklearn.metrics import classification_report
print(classification_report(y_test, predictions, target_names=["ham", "spam"]))
              precision    recall  f1-score   support
         ham       0.99      0.99      0.99       965
        spam       0.94      0.93      0.93       150

    accuracy                           0.98      1115
   macro avg       0.97      0.96      0.96      1115
weighted avg       0.98      0.98      0.98      1115

Read this. The ham class is easy (99% precision and recall). The spam class is harder (94% precision, 93% recall). Both are well above baseline. F1 of 0.93 on spam is a strong result for a balanced precision/recall trade-off.

The classification report is the right thing to read first on every classification project. It tells the whole story in eight lines.

ROC curves, AUC, and PR curves (briefly)

Two more visualisations worth knowing, both about the precision/recall/threshold story:

  • ROC curve — plots true positive rate (= recall) against false positive rate for all thresholds. The area under the curve (AUC) is a single number summarising the model’s discriminative ability across all thresholds. AUC of 0.5 = random guessing. AUC of 1.0 = perfect. AUC of 0.8-0.9 is strong.

  • Precision-Recall curve — plots precision against recall for all thresholds. More informative than ROC when classes are heavily imbalanced. The area under it (AP, average precision) is the corresponding single-number summary.

from sklearn.metrics import roc_auc_score, average_precision_score
probs = model.predict_proba(X_test)[:, 1]
print(f"ROC AUC: {roc_auc_score(y_test, probs):.3f}")
print(f"PR AUC:  {average_precision_score(y_test, probs):.3f}")

Use AUC when you need a single threshold-independent quality number. For imbalanced problems, prefer PR-AUC.

Check your understanding

Quick check

A clinic deploys a TB screening classifier where every flagged patient gets a free cheap follow-up test. The clinic asks you to optimise the model. Which metric should you focus on, and why?

Quick check

A classification report shows precision=0.40, recall=0.95 for the positive class. What is the model doing?

What comes next

You can evaluate a model honestly. The last section of Chapter 4 covers the failure mode that hides from precision/recall too if you don’t know where to look: overfitting. The phenomenon where a model learns the training data too well — including its noise — and fails on new data. We’ll meet learning curves, train/test gap diagnostics, and the small disciplines that keep your models honest.