Per-Class Precision

Per-class precision computes precision separately for each class, treating that class as positive and everything else as negative:

\(\text{Precision}_c = \dfrac{TP_c}{TP_c + FP_c}\)

where \(TP_c\) counts the samples correctly predicted as \(c\) and \(FP_c\) the samples wrongly predicted as \(c\). It answers, for each class in turn: when the model says \(c\), how often is it right?

The result is a list rather than a number, and that is the point. Every single-figure summary — macro, micro, weighted — either averages this list or replaces it, and each of them can hide the class you care about.

Reading the table

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

y_true = ["A", "A", "B", "B", "C", "C"]
y_pred = ["A", "B", "B", "C", "C", "A"]
labels = ["A", "B", "C"]
print("six samples", precision_score(y_true, y_pred, average=None, labels=labels))

rng = np.random.default_rng(6)
support = {"common": 4000, "medium": 800, "rare": 120}
truth, pred = [], []
accuracy_by_class = {"common": 0.95, "medium": 0.70, "rare": 0.35}
for name, n in support.items():
    truth += [name] * n
    others = [k for k in support if k != name]
    for _ in range(n):
        pred.append(name if rng.uniform() < accuracy_by_class[name] else rng.choice(others))

names = list(support)
p = precision_score(truth, pred, average=None, labels=names, zero_division=0)
r = recall_score(truth, pred, average=None, labels=names, zero_division=0)
predicted = confusion_matrix(truth, pred, labels=names).sum(axis=0)
for name, pi, ri, n_pred in zip(names, p, r, predicted):
    print(f"{name:7} precision {pi:.4f}  recall {ri:.4f}  support {support[name]:5d}  predicted {n_pred:5d}")
# six samples [0.5 0.5 0.5]
# common  precision 0.9625  recall 0.9553  support  4000  predicted  3970
# medium  precision 0.8074  recall 0.6863  support   800  predicted   680
# rare    precision 0.1815  recall 0.4083  support   120  predicted   270

The first line is a small case worth working through by hand. Six samples, true labels A, A, B, B, C, C, predicted A, B, B, C, C, A. Each class is predicted exactly twice and is right once, so every per-class precision is 0.5. Nothing distinguishes the classes, which is what makes it a poor illustration of why the table is useful.

The imbalanced case underneath shows the real behavior. The rare class has a precision of 0.1815 and a recall of 0.4083 — the model finds 41 percent of the rare cases, and when it announces one it is wrong 82 percent of the time. Neither number alone conveys that, and a single averaged figure conveys neither.

The last column explains it. The rare class has 120 real members and receives 270 predictions. Precision equals recall multiplied by support divided by predicted count, so it cannot exceed support divided by predicted count. Here that upper bound is 120/270, or about 0.444, even if every rare case were detected; the actual recall of 0.4083 brings precision down to 0.1815. A small excess of predictions does not by itself imply low precision: 99 correct predictions among 101 predictions for a class with 100 members gives about 0.9802. Reading recall, support, and predicted count together shows how missed cases and false positives contribute.

Where the averages come from

SummaryBuilt from this table?
Macro precisionYes — the unweighted mean of the column
Weighted precisionYes — the mean weighted by support
Micro precisionYes, but with a different weight — pooling \(TP\) and \(FP\) across classes is the mean of the per-class values weighted by predicted count, where the weighted average above uses true support

On the model above, macro precision is 0.6504 and micro precision is 0.8982. Both describe the same predictions. Which one belongs in a report depends on whether a rare class matters as much as a common one, and the per-class table is what lets a reader check the claim either way.

Two details that bite

A class the model never predicts has \(TP_c + FP_c = 0\) and an undefined precision. scikit-learn’s zero_division decides what appears in the list — 0, 1, or nan — and the choice changes the macro average dramatically, which is covered under macro precision. Set it explicitly rather than accepting a default.

And the order of the list is the order of labels, not the order the classes appear in the data. Pass labels explicitly and label the output; a per-class table whose rows are attached to the wrong classes is worse than no table.

Read the per-class table with recall beside it and the support and predicted counts beside both. That is four columns and it answers most of the questions a single metric provokes.

References


Discover more from Insightful Data Lab

Subscribe to get the latest posts sent to your email.

Similar Posts

Questions, corrections, or additional insights?

This site uses Akismet to reduce spam. Learn how your comment data is processed.