-
Notifications
You must be signed in to change notification settings - Fork 0
0016 metrics package placement
Status: accepted · Date: 2026-08-06
Issue #61 asks for
scikit-learn-parity evaluation metrics: confusion matrix, accuracy, precision,
recall, F1, F-beta, classification_report and ROC-AUC. The migration
inventory had been naming the gap without filling it — its scikit-learn row read
"check the definitions (macro/micro averaging, handling of absent classes)",
which tells a reader there is a trap and leaves them in it.
Four choices had to be made before any of it could be written, and each one is hard to reverse once the package is published.
DataNet.Text is described, in its own README, as the library for text:
distances, vectorization, stemming. A confusion matrix is not textual. It takes
int labels and double weights and would work identically for a model that
never saw a string.
The alternative — DataNet.Text.Metrics — costs nothing today and everything
later. Moving a type out of a published package is a breaking change for every
consumer that referenced it, and the pressure to move it would only grow as
regression metrics, calibration and clustering scores arrive, none of which have
any business in a text library. The split is cheapest at the moment the package
has no users, which is now.
The cost is a fourth package to version, tag and publish. That cost was already
paid structurally: 0012 made every package
version independently, and DataNet.Metrics creates no inter-package edge — no
DataNet package depends on it and it depends on no DataNet package, so it is
freer to release than DataNet.Text is.
scikit-learn exposes confusion_matrix as a metric in its own right, so parity
alone settles most of it. The stronger reason is cost: precision, recall, F1 and
the report are all read off the same O(samples) pass. A caller who wants four
numbers and can only reach
Precision.Score(yTrue, yPred, …)
pays that pass four times over.
Making the matrix public gives them the choice, and it is the choice the
benchmarks measure:
ClassificationReport.Compute(cm)
over a million samples costs what building the matrix costs, because everything
after it is O(classes).
The type is therefore part of the supported surface: label order, the Labels
view, TotalWeight, the indexer and ToArray(). What stays internal is the
storage — a flat double[] with a stride — so the layout can change without
breaking anyone.
Weighted counts force the matrix to hold double, not int. A confusion matrix
whose cells are double reads oddly — the number of samples in a cell is a
whole number — and every caller who prints one now formats a float.
It was taken anyway. sample_weight runs through the whole of scikit-learn's
metrics API, and a library that omits it is not at parity for any caller doing
class rebalancing or importance weighting. Retrofitting it later would change
the public type of every cell, every support figure and every PerClass array
— a breaking change across the entire package, to add a parameter that was
always going to be needed.
ToArray() returns double[,] for the same reason, and the sample prints the
cells with F0 so the common unweighted case still reads as counts.
scikit-learn spells the unreduced case average=None, one more value of the
same parameter. Transcribing that into an enum gives Averaging.None, which is
wrong in C# on two counts: None is the conventional name for a zero flags
value, and — decisively — the return type is not the same. Every other averaging
mode yields one double; None yields one per class.
An enum member that silently changes the return type of its method cannot exist,
so it becomes a method:
Precision.PerClass(cm)
returns double[] in label order. The enum keeps only the four members that
genuinely reduce to a scalar — Binary, Micro, Macro, Weighted — and the
equivalence table records the rename against average=None.
Averaging.Binary keeps scikit-learn's default and its strictness: it is not an
average at all but a single class scored against the rest, and it throws on a
target with more than two classes rather than guess which class was meant.
- A fourth package in the pack loops, the release workflow, the nuspec
dependency check and the sample.
samples/DataNet.SamplegainsLot5Metrics.cs, which is not optional:PackagingGatefails the build when an exported type is unreachable from the sample, so the surface is checked rather than assumed. -
UndefinedMetricExceptionis the one metrics type the gate cannot see. Its whole public surface is constructors and a consumer catches rather than constructs, which leaves a type reference and no member reference — the same shape as the enum carve-out the gate already documents. It is excluded with that reason rather than exercised artificially. - Callers wanting several metrics are steered towards computing the matrix once.
The overloads taking
yTrue/yPredremain, because the one-metric case should not have to know what a confusion matrix is. - The
doublecells are visible in the public API forever. The alternative was to break every consumer the first time someone passed a weight.
- 0001-target-framework
- 0002-unicode-comparison-unit
- 0003-provenance-and-licensing
- 0004-levenshtein-myers-backlog
- 0005-hamming-jellyfish-divergence
- 0006-ratcliff-autojunk
- 0007-metaphone-scope
- 0008-italian-enza-nltk-divergence
- 0009-sample-consumes-a-local-feed
- 0010-stop-word-list-provenance
- 0011-persistence-format
- 0012-per-package-versioning
- 0013-sentencepiece-parity-scope
- 0014-precompiled-normalizer
- 0015-sonar-rules-in-the-build
- 0016-metrics-package-placement
- 0017-bpe-parity-scope
- 0018-multiclass-roc-auc-parallelism-is-opt-in
- 0019-the-net-analysers-run-in-the-build-too
- 0020-normalize-is-a-projection-not-a-parameter
- 0021-multioutput-is-a-method-not-an-enum
- 0022-added-token-matching-flags
- 0023-byte-level-decode-substitutes
- 0024-weighted-median-averages-within-scikit-learns-epsilon
- 0025-quickselect-replaces-a-full-sort-for-the-median
- 0026-r2-and-explainedvariance-split-their-undefined-cases-differently
- 0027-r2-and-explainedvariance-vectorize-only-a-single-output
- 0028-log1p-is-kahans-identity-not-math-log-1-plus-x
- 0029-balanced-accuracy-adjusted-is-left-to-ieee-754-at-the-edge
- 0030-cohen-kappa-keeps-scikit-learns-expected-matrix-orientation
- 0031-nosamplecorrect-mirrors-numpys-float64-upcast
- 0032-fbeta-substitutes-tp-predicted-and-support-algebraically
- 0033-compensated-sum-is-neumaiers-variant
- 0034-dropout-is-refused-for-want-of-a-user
- 0035-a-null-pre-split-is-removed-with-invert-not-isolated
- 0036-a-member-may-ship-without-an-oracle-if-it-says-so
- 0037-the-guards-run-before-the-commit
- 0038-the-gate-confronts-an-exception-tag-with-the-page-that-documents-it
- 0039-mutual-information-returns-zero-on-an-empty-input
- 0040-a-curve-is-a-sealed-class-per-curve
- 0041-one-sample-file-per-public-class
- 0042-phonetic-encoders-refuse-a-null-word
- 0043-the-equality-table-is-sized-to-the-pattern
- 0044-compression-belongs-to-the-caller
- 0045-a-console-call-carries-its-reason-on-the-line
- 0046-check-adr-immutable-runs-in-ci-only
- 0047-one-gate-per-kernel-not-one-per-alphabet
- 0048-the-gate-depends-on-the-kernel-and-the-alphabet
- 0049-two-gates-per-kernel-tested-where-the-width-is-known
- 0050-the-sentencepiece-bpe-lineage-stays-a-bpe-model
- benchmark_latest
- decisions
- equivalence
- matplotlib
- migration
- nightly_run
- numpy
- pandas
- performance
- pytorch
- seaborn
- sklearn
- statsmodels