Skip to content

fix(f1): review follow-ups from #382#383

Merged
Mec-iS merged 2 commits into
developmentfrom
fix-multiclass-f1-review-followups
Jul 21, 2026
Merged

fix(f1): review follow-ups from #382#383
Mec-iS merged 2 commits into
developmentfrom
fix-multiclass-f1-review-followups

Conversation

@Mec-iS

@Mec-iS Mec-iS commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Follow-up to #382 (no new issue; addresses reviewer feedback there).

Checklist

  • My branch is up-to-date with development branch.
  • Everything works and tested on latest stable Rust.
  • Coverage and Linting have been applied

Current behaviour

F1::get_score correctly computes the macro F-measure as the mean of
per-class F-beta scores (landed in #382), but the implementation had a
few rough edges flagged in code review:

  • A dead if classes == 0 { 0.0 } branch in the multiclass path that
    was actually not dead — it is reachable via empty input, and
    removing it without a guard would return NaN (0.0 / 0.0).
  • Two full O(n) passes over y_true/y_pred (one to build
    classes_set, another to build tp_map/support/predicted).
  • A terse variable name (sup).
  • Undocumented assumptions: the to_f64_bits() HashMap key convention
    (-0.0 vs +0.0), the positive-class bit-representation assumption
    baked into the binary path, and the classes == 1 edge case.
  • No test for the single-class input.

New expected behaviour

Same macro F1 behaviour as #382, with the review feedback applied:

  • Early n == 0 return makes classes == 0 genuinely unreachable, so
    the dead branch is removed safely (empty input still returns 0.0).
  • The two O(n) passes are merged into a single pass.
  • sup -> support_count.
  • Comments document the bit-pattern key convention, the positive-class
    assumption, and the classes == 1 behaviour.
  • New f1_single_class test covers the single-class edge case
    (perfect -> 1.0; tp=2, pred=2, support=3 -> p=1.0, r=2/3 -> 0.8).

Change logs

  • Applied review feedback from Fix multiclass macro F1 to average per-class F-scores #382: removed unreachable classes == 0
    branch (after adding an n == 0 guard so it is truly unreachable),
    merged the two passes over the input into one, renamed sup to
    support_count, documented key/positive-class conventions, and added a
    f1_single_class edge-case test. No behaviour change.

Verification

  • cargo fmt --all -- --check — clean
  • cargo clippy --all-features -- -Drust-2018-idioms -Dwarnings — clean
  • cargo test --all-features — 432 lib tests + 67 doctests pass

Mec-iS added 2 commits July 21, 2026 10:34
Apply review feedback on the multiclass macro F1 fix from PR #382:

- Add an early `n == 0` return so the `classes == 0` branch in the
  multiclass path is genuinely unreachable, then drop it. Without the
  guard, removing that branch would return NaN (0.0/0.0) on empty input.
- Merge the two O(n) passes over y_true/y_pred (classes_set build and
  tp/support/predicted build) into a single pass.
- Rename `sup` -> `support_count` for clarity.
- Document the `to_f64_bits()` key convention (-0.0 vs +0.0), the
  positive-class bit-representation assumption baked into the binary
  path, and the `classes == 1` behaviour in the multiclass path.
- Add `f1_single_class` test covering the single-class edge case.

No behaviour change beyond #382; all existing tests pass unchanged.
Fixes `clippy::for_kv_map` lint in fastpair.rs:149 by iterating
`distances.values()` instead of `distances.iter()` with an unused
key binding.
@Mec-iS
Mec-iS merged commit ce68041 into development Jul 21, 2026
12 checks passed
@Mec-iS
Mec-iS deleted the fix-multiclass-f1-review-followups branch July 21, 2026 09:38
Mec-iS added a commit that referenced this pull request Jul 21, 2026
…Recall/F1 (#384)

* refactor(metrics): share per-class confusion counts across Precision/Recall/F1

Addresses the architectural review feedback from #382: the multiclass
F1 path re-implemented per-class precision/recall bookkeeping using raw
HashMaps, duplicating logic that already lived (in slightly different
form) inside Precision and Recall.

Introduce a pub(crate) `ConfusionCounts` helper (new
src/metrics/confusion.rs) that computes the per-class tp / predicted /
support / class set in a single pass. Precision and Recall each expose a
crate-private `per_class_scores_from_counts` method deriving their
per-class scores from those shared counts, and their `get_score`
implementations are unified onto the same single-pass counts (the
binary and multiclass paths now share one code path that either picks
the positive class or macro-averages). F1's multiclass path builds the
counts once and consumes the per-class precision/recall scores from
Precision and Recall, eliminating the duplicated bookkeeping while
keeping a single source of truth.

Binary behaviour is preserved exactly (verified against all existing
tests, including the {0,1} positive-class convention): for {0,1}
labels the per-class positive-class precision/recall match the previous
direct-computation formulas term-for-term.

Also: add an `n == 0` early return to Precision/Recall (matching F1
post-#383) so the multiclass path can assume `classes >= 1`, and drop
the now-dead `classes == 0` / `support.is_empty()` branches.

README: bump the install snippet from ^0.4.3 to ^0.5.2 (current
Cargo.toml version) and note the metrics refactoring in the roadmap.

No behaviour change; all 432 lib tests + 67 doctests + 40 metrics tests
pass under `cargo test --all-features`. `cargo fmt --check` and
`cargo clippy --all-features -Dwarnings` are clean.

* chore: bump to v0.5.3

- Cargo.toml: 0.5.2 -> 0.5.3
- README install snippet: ^0.5.2 -> ^0.5.3
- CHANGELOG: add [0.5.3] entry covering the metrics refactoring

* refactor(metrics): address #384 review feedback

- Rename ConfusionCounts::from -> new to avoid shadowing the
  std::convert::From trait convention (review blocker).
- Add direct unit tests for ConfusionCounts (binary basic, multiclass,
  spurious predicted label, empty input, perfect predictions) covering
  tp/predicted/support/classes_set accessors — the helper was previously
  only tested indirectly through Precision/Recall/F1.
- Add precision_binary_spurious_predicted_label test documenting the
  binary edge case where y_pred contains a label not in y_true: the new
  path uses predicted(positive) as the denominator (matching sklearn),
  so a spurious predicted label does not inflate it. The pre-refactor
  binary path would have counted it as a false positive (2/3 instead of
  1.0).
- Document that per_class_scores_from_counts iterates only over
  classes_set (y_true labels), so labels appearing only in y_pred are
  silently ignored — matching sklearn's behaviour.
- Add order-independence comments for the HashMap::values().sum() calls
  in the Precision/Recall multiclass paths.
- Clarify the binary Precision comment to note that the denominator is
  predicted(positive), not tp + fp_count, so spurious predicted labels
  do not affect the score.

All 438 lib tests + 67 doctests + 46 metrics tests pass; fmt and clippy
--all-features -Dwarnings clean.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant