aggmetrics 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, Ronaldo C. Prati
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ 3. Neither the name of the copyright holder nor the names of its contributors
17
+ may be used to endorse or promote products derived from this software without
18
+ specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
21
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
22
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,242 @@
1
+ Metadata-Version: 2.4
2
+ Name: aggmetrics
3
+ Version: 0.1.0
4
+ Summary: Aggregating classification performance measures for machine learning: indicator matrices and aggregation operators
5
+ Author-email: "Ronaldo C. Prati" <ronaldo.prati@ufabc.edu.br>
6
+ License: BSD-3-Clause
7
+ Project-URL: Homepage, https://github.com/rcprati/aggmetrics
8
+ Project-URL: Repository, https://github.com/rcprati/aggmetrics
9
+ Project-URL: Documentation, https://aggmetrics.readthedocs.io
10
+ Project-URL: Issues, https://github.com/rcprati/aggmetrics/issues
11
+ Keywords: machine learning,classification,evaluation,performance measures,metrics,multilabel,multiclass,class imbalance,confusion matrix
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: License :: OSI Approved :: BSD License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
17
+ Requires-Python: >=3.9
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: numpy>=1.20
21
+ Provides-Extra: sklearn
22
+ Requires-Dist: scikit-learn>=1.0; extra == "sklearn"
23
+ Provides-Extra: frame
24
+ Requires-Dist: pandas>=1.3; extra == "frame"
25
+ Provides-Extra: test
26
+ Requires-Dist: pytest>=7.0; extra == "test"
27
+ Requires-Dist: pytest-cov>=4.0; extra == "test"
28
+ Requires-Dist: scikit-learn>=1.0; extra == "test"
29
+ Requires-Dist: pandas>=1.3; extra == "test"
30
+ Provides-Extra: examples
31
+ Requires-Dist: scikit-learn>=1.0; extra == "examples"
32
+ Requires-Dist: pandas>=1.3; extra == "examples"
33
+ Requires-Dist: matplotlib>=3.5; extra == "examples"
34
+ Provides-Extra: docs
35
+ Requires-Dist: sphinx>=7.0; extra == "docs"
36
+ Requires-Dist: furo; extra == "docs"
37
+ Requires-Dist: myst-nb>=1.0; extra == "docs"
38
+ Requires-Dist: sphinx-copybutton; extra == "docs"
39
+ Provides-Extra: all
40
+ Requires-Dist: scikit-learn>=1.0; extra == "all"
41
+ Requires-Dist: pandas>=1.3; extra == "all"
42
+ Dynamic: license-file
43
+
44
+ # aggmetrics
45
+
46
+ [![tests](https://github.com/rcprati/aggmetrics/actions/workflows/ci.yml/badge.svg)](https://github.com/rcprati/aggmetrics/actions/workflows/ci.yml)
47
+ [![Documentation Status](https://readthedocs.org/projects/aggmetrics/badge/?version=latest)](https://aggmetrics.readthedocs.io/en/latest/)
48
+ [![PyPI](https://img.shields.io/pypi/v/aggmetrics)](https://pypi.org/project/aggmetrics/)
49
+ [![License](https://img.shields.io/badge/license-BSD--3-blue.svg)](LICENSE)
50
+
51
+ **Aggregating classification performance measures for machine learning: indicator matrices and aggregation operators.**
52
+
53
+ Any binary performance measure expressible in terms of the four confusion-matrix
54
+ counts is extended to multiclass, multilabel, ordinal, cost-sensitive and
55
+ soft-label settings by substituting one of a small set of aggregation operators.
56
+ Micro-, macro- and exemplar-averaging (and pooling across cross-validation folds)
57
+ become instances of a single construction rather than separate conventions.
58
+
59
+ The package also reports when a measure is being used outside the range where it
60
+ behaves as commonly assumed — which is the part that is hard to get from a
61
+ formula library.
62
+
63
+ ```python
64
+ import numpy as np
65
+ from aggmetrics import IndicatorMatrix, diagnose
66
+
67
+ y_true = np.array([0, 1, 2, 2, 1, 0])
68
+ y_pred = np.array([0, 2, 2, 1, 1, 1])
69
+ im = IndicatorMatrix(y_true, y_pred, task='multiclass')
70
+
71
+ im.compute('f1', 'macro') # 0.5222
72
+ im.compute('f1', 'micro') # 0.5 -- equals accuracy on single-label data
73
+ im.compute('mcc', 'macro') # macro-MCC: no standard implementation exists
74
+ im.compute('informedness', 'micro')
75
+ im.accuracy() # 0.5
76
+
77
+ im.report() # DataFrame: measures x operators
78
+ ```
79
+
80
+ **Documentation:** <https://aggmetrics.readthedocs.io>
81
+
82
+ ## Installation
83
+
84
+ ```bash
85
+ pip install aggmetrics # core, requires only numpy
86
+ pip install "aggmetrics[all]" # adds scikit-learn and pandas support
87
+ ```
88
+
89
+ ## Why not just use `sklearn.metrics`?
90
+
91
+ `sklearn` exposes four `average` values for a fixed list of measures. The
92
+ correspondence is exact — `'micro'`, `'macro'`, `'weighted'` and `'samples'` are
93
+ Σ₁, Σₘ with uniform weights, Σₘ with prevalence weights and Σₙ respectively — but
94
+ the combinations available are a subset of those the construction generates, and
95
+ nothing in the library tells you when a combination is misleading.
96
+
97
+ Three things this package adds:
98
+
99
+ **Any measure under any operator.** Macro-MCC, exemplar-Jaccard, micro-informedness,
100
+ weighted G-mean. 21 binary measures × 4 operators, without special-casing.
101
+
102
+ **Structural properties as first-class data.** Each measure records whether it is
103
+ *aggregation-decomposable*, whether it is skew-invariant, and whether its value at
104
+ `tp = 0` depends on `tn`. These determine which identities apply.
105
+
106
+ ```python
107
+ from aggmetrics import get_measure, list_measures
108
+
109
+ list_measures(decomposable=True) # micro == denominator-weighted macro
110
+ get_measure('mcc').decomposable # False
111
+ get_measure('recall').bayes_threshold() # 0.0 -- maximised by predicting all positive
112
+ get_measure('mcc').attainable_floor(10) # -0.111, not -1
113
+ ```
114
+
115
+ **The generative claim, as an operation.** Supply a binary formula; the properties
116
+ that decide which identities apply are *derived*, not declared:
117
+
118
+ ```python
119
+ from aggmetrics import register_measure, infer_properties
120
+
121
+ def prevalence_threshold(tp, fp, fn, tn):
122
+ r, s = tp / (tp + fn), tn / (tn + fp)
123
+ return (np.sqrt(r * (1 - s)) + s - 1) / (r + s - 1)
124
+
125
+ m = register_measure('prevalence_threshold', prevalence_threshold)
126
+ m.decomposable # False -- inferred by fitting <a,v>/<b,v> and validating
127
+ m.skew_invariant # True -- a function of recall and specificity
128
+ m.bayes_threshold() # None -- no closed form outside the decomposable class
129
+
130
+ im.compute('prevalence_threshold', 'exemplar') # usable immediately
131
+ ```
132
+
133
+ Inference is checked against the 21 measures whose properties are known
134
+ analytically — `verify_inference()` — and all 21 agree, with recovered coefficients
135
+ accurate to 4.2e-15. The check paid for itself: it found two errors in our own hand
136
+ annotations (error rate and FPR both retain a `tn` dependence at `tp = 0`, so both
137
+ *flatter* a wholly wrong classifier, reaching 2/m and 1/(m-1) respectively).
138
+
139
+ **Diagnostics.** `diagnose()` flags the traps:
140
+
141
+ ```python
142
+ y = np.arange(500) % 10
143
+ yhat = (y + 1) % 10 # every single prediction is wrong
144
+ im = IndicatorMatrix(y, yhat, task='multiclass')
145
+
146
+ im.compute('label_accuracy', 'micro') # 0.800 (!)
147
+ im.compute('mcc', 'micro') # -0.111 (!)
148
+ im.accuracy() # 0.000
149
+
150
+ for f in diagnose(im):
151
+ print(f)
152
+ # [warning] inflated_floor: 'label_accuracy' cannot fall below 0.8000 on this
153
+ # 10-class one-hot problem even if every prediction is wrong ...
154
+ # [note] micro_collapse: micro-precision, micro-recall and micro-F1 are all
155
+ # equal to accuracy ...
156
+ # [warning] degenerate_optimum: 'recall' is maximised at threshold 0.000 ...
157
+ ```
158
+
159
+ Checks currently implemented: pooled value outside the per-class range;
160
+ `tn`-dependent measures on one-hot multiclass data; micro-collapse on single-label
161
+ data; undefined per-class entries and the size of the convention effect; measures
162
+ whose optimal decision rule is not the Bayes rule for 0/1 loss.
163
+
164
+ ## Features
165
+
166
+ - **21 binary measures**, resolvable by name or alias (`iou` → `jaccard`, `dice` → `f1`),
167
+ plus `register_measure` for your own, with properties inferred.
168
+ - **Four operators**: `micro`, `macro`, `weighted`, `exemplar`.
169
+ - **Explicit undefined-value conventions**: `exclude` (default), `zero`
170
+ (`sklearn`'s behaviour), `one`, `nan`. The choice is a parameter, not a default
171
+ buried in the implementation.
172
+ - **Soft ground truth** via t-norms, with `marginal_defect()` showing that only the
173
+ product t-norm preserves the marginals.
174
+ - **Cost matrices**, recovering MAE and MSE as special cases.
175
+ - **Observation masks** for partial labels, semi-supervised evaluation and abstention.
176
+ - **scikit-learn scorers** via `make_scorer`, usable in `cross_val_score` and
177
+ `GridSearchCV`.
178
+ - **AI Agent Skills included** (`.agents/skills/aggmetrics-guide/`), providing machine-readable
179
+ runbooks, axiomatic invariants, and diagnostic protocols for AI coding assistants.
180
+
181
+ ## The test suite verifies the theory
182
+
183
+ Each result of the companion paper is an executable test, so a change that breaks
184
+ a theorem breaks a test:
185
+
186
+ ```bash
187
+ python -m pytest tests/test_theory.py -v
188
+ ```
189
+
190
+ | Test | Result verified |
191
+ |---|---|
192
+ | `test_theorem1_holds_for_decomposable` | micro = denominator-weighted macro, the 14 decomposable measures × 3 samples |
193
+ | `test_theorem1_fails_for_non_decomposable` | the identity has no extension beyond that class |
194
+ | `test_theorem3_product_is_uniquely_marginally_consistent` | only the product t-norm preserves marginals |
195
+ | `test_theorem2_micro_collapse` | micro-P = micro-R = micro-F₁ = accuracy, single-label |
196
+ | `test_proposition9_thresholds` | Bayes threshold `(β+δ)/(α+β+γ+δ)` |
197
+ | `test_proposition5_floors` | attainable floors under one-hot encoding |
198
+ | `test_proposition6_recall_prior_free_specificity_not` | one-vs-rest skew behaviour |
199
+ | `test_corollary6_gap_is_normalised_covariance` | the micro–macro gap identity |
200
+ | `test_theorem4_mae_and_mse_as_cost_matrices` | ordinal losses as cost matrices |
201
+ | `test_inference_reproduces_every_annotation` | inference recovers all 21 analytic annotations |
202
+
203
+ 209 tests, no skips, 100% line and branch coverage.
204
+
205
+ ## Examples
206
+
207
+ Notebooks, executed and checked in continuous integration. Each starts from a problem a
208
+ practitioner recognises, shows what the usual tools report, and then what `aggmetrics` reports.
209
+
210
+ | Notebook | What it shows | |
211
+ |---|---|---|
212
+ | [01 The floor trap](examples/notebooks/01_floor_trap.ipynb) | A classifier wrong on every example scores 0.998 on label accuracy with 1000 classes | [![Open in Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/rcprati/aggmetrics/blob/main/examples/notebooks/01_floor_trap.ipynb) |
213
+ | [02 Micro, macro and imbalance](examples/notebooks/02_micro_macro_imbalance.ipynb) | Micro-F1 equals accuracy; micro-MCC outside the per-class range; the hidden choice for 0/0 | [![Open in Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/rcprati/aggmetrics/blob/main/examples/notebooks/02_micro_macro_imbalance.ipynb) |
214
+ | [03 Multilabel: the ranking depends on the operator](examples/notebooks/03_multilabel_ranking.ipynb) | Three models on *yeast* ranked under 18 measure-operator combinations | [![Open in Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/rcprati/aggmetrics/blob/main/examples/notebooks/03_multilabel_ranking.ipynb) |
215
+
216
+ To run them locally: `pip install "aggmetrics[examples]" jupyter`. A short script version of
217
+ the tour is in `examples/quickstart.py`:
218
+
219
+ ```bash
220
+ python examples/quickstart.py
221
+ ```
222
+
223
+ ## Citing
224
+
225
+ Please cite both the software and the companion paper. GitHub's "Cite this repository" button
226
+ and `CITATION.cff` give the software; the paper is:
227
+
228
+ ```bibtex
229
+ @article{prati2027unified,
230
+ author = {Prati, Ronaldo C.},
231
+ title = {A unified algebraic framework for classification performance evaluation},
232
+ journal = {Information Fusion},
233
+ volume = {138},
234
+ pages = {104767},
235
+ year = {2027},
236
+ doi = {10.1016/j.inffus.2026.104767}
237
+ }
238
+ ```
239
+
240
+ ## License
241
+
242
+ BSD 3-Clause.
@@ -0,0 +1,199 @@
1
+ # aggmetrics
2
+
3
+ [![tests](https://github.com/rcprati/aggmetrics/actions/workflows/ci.yml/badge.svg)](https://github.com/rcprati/aggmetrics/actions/workflows/ci.yml)
4
+ [![Documentation Status](https://readthedocs.org/projects/aggmetrics/badge/?version=latest)](https://aggmetrics.readthedocs.io/en/latest/)
5
+ [![PyPI](https://img.shields.io/pypi/v/aggmetrics)](https://pypi.org/project/aggmetrics/)
6
+ [![License](https://img.shields.io/badge/license-BSD--3-blue.svg)](LICENSE)
7
+
8
+ **Aggregating classification performance measures for machine learning: indicator matrices and aggregation operators.**
9
+
10
+ Any binary performance measure expressible in terms of the four confusion-matrix
11
+ counts is extended to multiclass, multilabel, ordinal, cost-sensitive and
12
+ soft-label settings by substituting one of a small set of aggregation operators.
13
+ Micro-, macro- and exemplar-averaging (and pooling across cross-validation folds)
14
+ become instances of a single construction rather than separate conventions.
15
+
16
+ The package also reports when a measure is being used outside the range where it
17
+ behaves as commonly assumed — which is the part that is hard to get from a
18
+ formula library.
19
+
20
+ ```python
21
+ import numpy as np
22
+ from aggmetrics import IndicatorMatrix, diagnose
23
+
24
+ y_true = np.array([0, 1, 2, 2, 1, 0])
25
+ y_pred = np.array([0, 2, 2, 1, 1, 1])
26
+ im = IndicatorMatrix(y_true, y_pred, task='multiclass')
27
+
28
+ im.compute('f1', 'macro') # 0.5222
29
+ im.compute('f1', 'micro') # 0.5 -- equals accuracy on single-label data
30
+ im.compute('mcc', 'macro') # macro-MCC: no standard implementation exists
31
+ im.compute('informedness', 'micro')
32
+ im.accuracy() # 0.5
33
+
34
+ im.report() # DataFrame: measures x operators
35
+ ```
36
+
37
+ **Documentation:** <https://aggmetrics.readthedocs.io>
38
+
39
+ ## Installation
40
+
41
+ ```bash
42
+ pip install aggmetrics # core, requires only numpy
43
+ pip install "aggmetrics[all]" # adds scikit-learn and pandas support
44
+ ```
45
+
46
+ ## Why not just use `sklearn.metrics`?
47
+
48
+ `sklearn` exposes four `average` values for a fixed list of measures. The
49
+ correspondence is exact — `'micro'`, `'macro'`, `'weighted'` and `'samples'` are
50
+ Σ₁, Σₘ with uniform weights, Σₘ with prevalence weights and Σₙ respectively — but
51
+ the combinations available are a subset of those the construction generates, and
52
+ nothing in the library tells you when a combination is misleading.
53
+
54
+ Three things this package adds:
55
+
56
+ **Any measure under any operator.** Macro-MCC, exemplar-Jaccard, micro-informedness,
57
+ weighted G-mean. 21 binary measures × 4 operators, without special-casing.
58
+
59
+ **Structural properties as first-class data.** Each measure records whether it is
60
+ *aggregation-decomposable*, whether it is skew-invariant, and whether its value at
61
+ `tp = 0` depends on `tn`. These determine which identities apply.
62
+
63
+ ```python
64
+ from aggmetrics import get_measure, list_measures
65
+
66
+ list_measures(decomposable=True) # micro == denominator-weighted macro
67
+ get_measure('mcc').decomposable # False
68
+ get_measure('recall').bayes_threshold() # 0.0 -- maximised by predicting all positive
69
+ get_measure('mcc').attainable_floor(10) # -0.111, not -1
70
+ ```
71
+
72
+ **The generative claim, as an operation.** Supply a binary formula; the properties
73
+ that decide which identities apply are *derived*, not declared:
74
+
75
+ ```python
76
+ from aggmetrics import register_measure, infer_properties
77
+
78
+ def prevalence_threshold(tp, fp, fn, tn):
79
+ r, s = tp / (tp + fn), tn / (tn + fp)
80
+ return (np.sqrt(r * (1 - s)) + s - 1) / (r + s - 1)
81
+
82
+ m = register_measure('prevalence_threshold', prevalence_threshold)
83
+ m.decomposable # False -- inferred by fitting <a,v>/<b,v> and validating
84
+ m.skew_invariant # True -- a function of recall and specificity
85
+ m.bayes_threshold() # None -- no closed form outside the decomposable class
86
+
87
+ im.compute('prevalence_threshold', 'exemplar') # usable immediately
88
+ ```
89
+
90
+ Inference is checked against the 21 measures whose properties are known
91
+ analytically — `verify_inference()` — and all 21 agree, with recovered coefficients
92
+ accurate to 4.2e-15. The check paid for itself: it found two errors in our own hand
93
+ annotations (error rate and FPR both retain a `tn` dependence at `tp = 0`, so both
94
+ *flatter* a wholly wrong classifier, reaching 2/m and 1/(m-1) respectively).
95
+
96
+ **Diagnostics.** `diagnose()` flags the traps:
97
+
98
+ ```python
99
+ y = np.arange(500) % 10
100
+ yhat = (y + 1) % 10 # every single prediction is wrong
101
+ im = IndicatorMatrix(y, yhat, task='multiclass')
102
+
103
+ im.compute('label_accuracy', 'micro') # 0.800 (!)
104
+ im.compute('mcc', 'micro') # -0.111 (!)
105
+ im.accuracy() # 0.000
106
+
107
+ for f in diagnose(im):
108
+ print(f)
109
+ # [warning] inflated_floor: 'label_accuracy' cannot fall below 0.8000 on this
110
+ # 10-class one-hot problem even if every prediction is wrong ...
111
+ # [note] micro_collapse: micro-precision, micro-recall and micro-F1 are all
112
+ # equal to accuracy ...
113
+ # [warning] degenerate_optimum: 'recall' is maximised at threshold 0.000 ...
114
+ ```
115
+
116
+ Checks currently implemented: pooled value outside the per-class range;
117
+ `tn`-dependent measures on one-hot multiclass data; micro-collapse on single-label
118
+ data; undefined per-class entries and the size of the convention effect; measures
119
+ whose optimal decision rule is not the Bayes rule for 0/1 loss.
120
+
121
+ ## Features
122
+
123
+ - **21 binary measures**, resolvable by name or alias (`iou` → `jaccard`, `dice` → `f1`),
124
+ plus `register_measure` for your own, with properties inferred.
125
+ - **Four operators**: `micro`, `macro`, `weighted`, `exemplar`.
126
+ - **Explicit undefined-value conventions**: `exclude` (default), `zero`
127
+ (`sklearn`'s behaviour), `one`, `nan`. The choice is a parameter, not a default
128
+ buried in the implementation.
129
+ - **Soft ground truth** via t-norms, with `marginal_defect()` showing that only the
130
+ product t-norm preserves the marginals.
131
+ - **Cost matrices**, recovering MAE and MSE as special cases.
132
+ - **Observation masks** for partial labels, semi-supervised evaluation and abstention.
133
+ - **scikit-learn scorers** via `make_scorer`, usable in `cross_val_score` and
134
+ `GridSearchCV`.
135
+ - **AI Agent Skills included** (`.agents/skills/aggmetrics-guide/`), providing machine-readable
136
+ runbooks, axiomatic invariants, and diagnostic protocols for AI coding assistants.
137
+
138
+ ## The test suite verifies the theory
139
+
140
+ Each result of the companion paper is an executable test, so a change that breaks
141
+ a theorem breaks a test:
142
+
143
+ ```bash
144
+ python -m pytest tests/test_theory.py -v
145
+ ```
146
+
147
+ | Test | Result verified |
148
+ |---|---|
149
+ | `test_theorem1_holds_for_decomposable` | micro = denominator-weighted macro, the 14 decomposable measures × 3 samples |
150
+ | `test_theorem1_fails_for_non_decomposable` | the identity has no extension beyond that class |
151
+ | `test_theorem3_product_is_uniquely_marginally_consistent` | only the product t-norm preserves marginals |
152
+ | `test_theorem2_micro_collapse` | micro-P = micro-R = micro-F₁ = accuracy, single-label |
153
+ | `test_proposition9_thresholds` | Bayes threshold `(β+δ)/(α+β+γ+δ)` |
154
+ | `test_proposition5_floors` | attainable floors under one-hot encoding |
155
+ | `test_proposition6_recall_prior_free_specificity_not` | one-vs-rest skew behaviour |
156
+ | `test_corollary6_gap_is_normalised_covariance` | the micro–macro gap identity |
157
+ | `test_theorem4_mae_and_mse_as_cost_matrices` | ordinal losses as cost matrices |
158
+ | `test_inference_reproduces_every_annotation` | inference recovers all 21 analytic annotations |
159
+
160
+ 209 tests, no skips, 100% line and branch coverage.
161
+
162
+ ## Examples
163
+
164
+ Notebooks, executed and checked in continuous integration. Each starts from a problem a
165
+ practitioner recognises, shows what the usual tools report, and then what `aggmetrics` reports.
166
+
167
+ | Notebook | What it shows | |
168
+ |---|---|---|
169
+ | [01 The floor trap](examples/notebooks/01_floor_trap.ipynb) | A classifier wrong on every example scores 0.998 on label accuracy with 1000 classes | [![Open in Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/rcprati/aggmetrics/blob/main/examples/notebooks/01_floor_trap.ipynb) |
170
+ | [02 Micro, macro and imbalance](examples/notebooks/02_micro_macro_imbalance.ipynb) | Micro-F1 equals accuracy; micro-MCC outside the per-class range; the hidden choice for 0/0 | [![Open in Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/rcprati/aggmetrics/blob/main/examples/notebooks/02_micro_macro_imbalance.ipynb) |
171
+ | [03 Multilabel: the ranking depends on the operator](examples/notebooks/03_multilabel_ranking.ipynb) | Three models on *yeast* ranked under 18 measure-operator combinations | [![Open in Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/rcprati/aggmetrics/blob/main/examples/notebooks/03_multilabel_ranking.ipynb) |
172
+
173
+ To run them locally: `pip install "aggmetrics[examples]" jupyter`. A short script version of
174
+ the tour is in `examples/quickstart.py`:
175
+
176
+ ```bash
177
+ python examples/quickstart.py
178
+ ```
179
+
180
+ ## Citing
181
+
182
+ Please cite both the software and the companion paper. GitHub's "Cite this repository" button
183
+ and `CITATION.cff` give the software; the paper is:
184
+
185
+ ```bibtex
186
+ @article{prati2027unified,
187
+ author = {Prati, Ronaldo C.},
188
+ title = {A unified algebraic framework for classification performance evaluation},
189
+ journal = {Information Fusion},
190
+ volume = {138},
191
+ pages = {104767},
192
+ year = {2027},
193
+ doi = {10.1016/j.inffus.2026.104767}
194
+ }
195
+ ```
196
+
197
+ ## License
198
+
199
+ BSD 3-Clause.
@@ -0,0 +1,74 @@
1
+ # Copyright (c) 2026, Ronaldo C. Prati
2
+ # SPDX-License-Identifier: BSD-3-Clause
3
+ """
4
+ aggmetrics --- classification evaluation via indicator matrices.
5
+
6
+ Any binary performance measure expressible in terms of the four confusion-matrix
7
+ counts is extended to multiclass, multilabel, ordinal, cost-sensitive and
8
+ soft-label settings by substituting one of three aggregation operators, so that
9
+ micro-, macro- and exemplar-averaging are three instances of one construction
10
+ rather than three separate conventions.
11
+
12
+ Quick start
13
+ -----------
14
+ >>> import numpy as np
15
+ >>> from aggmetrics import IndicatorMatrix
16
+ >>> y_true = np.array([0, 1, 2, 2, 1, 0])
17
+ >>> y_pred = np.array([0, 2, 2, 1, 1, 1])
18
+ >>> im = IndicatorMatrix(y_true, y_pred, task='multiclass')
19
+ >>> round(im.compute('f1', 'macro'), 4)
20
+ 0.5222
21
+ >>> round(im.compute('f1', 'micro'), 4)
22
+ 0.5
23
+ >>> round(im.accuracy(), 4)
24
+ 0.5
25
+
26
+ Micro-F1 equals accuracy on single-label data, so reporting both adds nothing;
27
+ macro-F1 differs because it weights the three classes equally.
28
+
29
+ The package also reports when a measure is being used outside the range where it
30
+ behaves as commonly assumed:
31
+
32
+ >>> from aggmetrics import diagnose
33
+ >>> codes = sorted({f.code for f in diagnose(im)})
34
+ >>> 'micro_collapse' in codes
35
+ True
36
+ """
37
+ from __future__ import annotations
38
+
39
+ __version__ = "0.1.0"
40
+ __author__ = "Ronaldo C. Prati"
41
+ __license__ = "BSD-3-Clause"
42
+
43
+ from .aggregation import (aggregate, cells, fold_aggregate, sigma_1, sigma_k,
44
+ sigma_m, sigma_n)
45
+ from .core import IndicatorMatrix, pareto_dominance
46
+ from .diagnostics import (Finding, MeasureWarning, check_hull, check_ovr_skew_drift,
47
+ check_redundancy, diagnose, micro_macro_gap,
48
+ uniform_error_deviation)
49
+ from .inference import (InferredProperties, infer_properties, register_measure,
50
+ verify_inference)
51
+ from .measures import MEASURES, BinaryMeasure, get_measure, list_measures
52
+ from .sklearn_compat import make_scorer, score, sklearn_average
53
+ from .tnorms import TNORMS, get_tnorm, marginal_defect, soft_cells
54
+
55
+ __all__ = [
56
+ "__version__",
57
+ # core
58
+ "IndicatorMatrix", "pareto_dominance",
59
+ # measures
60
+ "BinaryMeasure", "MEASURES", "get_measure", "list_measures",
61
+ # operators
62
+ "aggregate", "cells", "fold_aggregate", "sigma_1", "sigma_m", "sigma_n", "sigma_k",
63
+ # soft labels
64
+ "TNORMS", "get_tnorm", "marginal_defect", "soft_cells",
65
+ # diagnostics
66
+ "Finding", "MeasureWarning", "check_hull", "check_ovr_skew_drift",
67
+ "check_redundancy", "diagnose", "micro_macro_gap", "uniform_error_deviation",
68
+ # inference
69
+ "register_measure", "infer_properties", "verify_inference",
70
+ "InferredProperties",
71
+ # scikit-learn
72
+ "make_scorer", "score", "sklearn_average",
73
+ ]
74
+