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
| Summary | Built from this table? |
|---|---|
| Macro precision | Yes — the unweighted mean of the column |
| Weighted precision | Yes — the mean weighted by support |
| Micro precision | Yes, 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.
