Concepts¶
Indicator matrices. For \(n\) examples and \(m\) classes or labels, the true and predicted labels are matrices \(Y, \hat Y \in \{0,1\}^{n \times m}\) (real-valued for soft labels). For ordinal data with \(m\) classes, \(Y\) has \(m-1\) columns of cumulative thresholds \(\mathbb{I}(y_i > j)\).
Cell matrices. Four matrices are formed entry by entry: true positives \(Y \wedge \hat Y\), false positives, false negatives and true negatives. Summing a cell matrix over some axis gives the counts that a measure uses.
Aggregation operators. A binary measure \(M(tp, fp, fn, tn)\) becomes a measure on the whole problem once the cell matrices are reduced:
Operator |
Reduces |
Name in the API |
|---|---|---|
\(\Sigma_1\) |
all entries |
|
\(\Sigma_m\) |
down each column (per class or label) |
|
\(\Sigma_n\) |
along each row (per example) |
|
\(\Sigma_k\) |
across cross-validation folds |
|
Structural properties. Each measure records whether it is aggregation-decomposable, whether it is skew-invariant, and whether its value at \(tp = 0\) depends on \(tn\). These decide which identities hold: for example, micro-averaging equals denominator-weighted macro-averaging only for decomposable measures. See Measures and their properties.
A worked example¶
Four classes, ten examples. The classifier gets class 0 mostly right and confuses the others a little:
from aggmetrics import IndicatorMatrix
y_true = [0, 0, 0, 0, 1, 1, 2, 2, 3, 3]
y_pred = [0, 0, 0, 1, 1, 2, 2, 2, 3, 0]
im = IndicatorMatrix(y_true, y_pred, task="multiclass")
im.per_class() # one row per class: tp, fp, fn, tn
Class |
tp |
fp |
fn |
tn |
Precision |
Recall |
|---|---|---|---|---|---|---|
0 |
3 |
1 |
1 |
5 |
3/4 = 0.750 |
3/4 = 0.750 |
1 |
1 |
1 |
1 |
7 |
1/2 = 0.500 |
1/2 = 0.500 |
2 |
2 |
1 |
0 |
7 |
2/3 = 0.667 |
2/2 = 1.000 |
3 |
1 |
0 |
1 |
8 |
1/1 = 1.000 |
1/2 = 0.500 |
pooled |
7 |
3 |
3 |
27 |
7/10 = 0.700 |
7/10 = 0.700 |
The three class-wise operators then differ only in how they combine the rows:
micro applies the measure to the pooled counts: precision \(7/10 = 0.7\).
macro averages the per-class values with equal weights: \((0.750 + 0.500 + 0.667 + 1.000)/4 = 0.729\).
weighted averages with weights equal to the number of true examples of each class, \((4, 2, 2, 2)\): \((0.750 \cdot 4 + 0.500 \cdot 2 + 0.667 \cdot 2 + 1.000 \cdot 2)/10 = 0.733\).
im.compute("precision", "micro") # 0.7
im.compute("precision", "macro") # 0.7292
im.compute("precision", "weighted") # 0.7333
im.compute("f1", "macro") # 0.6792
The exemplar operator reduces along rows, so it needs several labels per example to be interesting. Four examples with three labels:
Y = [[1, 0, 1], [0, 1, 0], [1, 1, 0], [0, 0, 1]]
P = [[1, 0, 0], [0, 1, 1], [1, 1, 0], [0, 0, 0]]
im = IndicatorMatrix(Y, P, task="multilabel")
im.parts("f1", "exemplar") # [0.667, 0.667, 1.0, 0.0], one per example
im.compute("f1", "exemplar") # 0.5833, the mean of those
im.compute("f1", "micro") # 0.7273, over the same predictions
The same predictions give 0.73 or 0.58 depending on whether the unit of account is the label decision or the example. Neither is wrong; they answer different questions.
Relation to scikit-learn¶
The average= argument of scikit-learn corresponds exactly to these operators:
|
|
|---|---|
|
|
|
|
|
|
|
|
For \(F_1\) and precision on the example above, precision_score(y_true, y_pred, average=...) and
im.compute("precision", ...) return the same values to the last digit. One difference to
keep in mind: when an entry is undefined, scikit-learn substitutes 0 (zero_division=0),
while aggmetrics leaves it out by default. Pass convention="zero" to match, as explained in
Aggregation: operators, conventions and folds.