Skip to content

Add metrics_rating prediction-quality metrics (PlayerRatings metrics()) - #304

Merged
seonghobae merged 5 commits into
seonghobae-elomfrom
seonghobae-metrics
Jul 31, 2026
Merged

Add metrics_rating prediction-quality metrics (PlayerRatings metrics())#304
seonghobae merged 5 commits into
seonghobae-elomfrom
seonghobae-metrics

Conversation

@seonghobae

@seonghobae seonghobae commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Implements CRAN PlayerRatings 1.1-0 metrics() (R/ratings.R lines 936-957, READ; no journal paper exists — the CRAN source is the normative reference) as metrics_rating: per-predictor-column binomial deviance / RMSE / MAE for binary-outcome forecasts, times 100, optionally scaled by the 0.5-constant-predictor baseline.

Verified R semantics preserved

  • bdev uses the capped predictor column; mse/mae use the raw uncapped column (R:949/951 reference pred[,i], not predc) — the key quirk, pinned by an anchor whose mutants were EXECUTED-killed.
  • Elementwise NaN removal per metric; scaled baselines use the act-only row set (different from the numerator row set when the column has NaNs) — pinned by a dedicated baseline-row-set anchor.
  • bdev baseline implemented as the exact constant ln 2 (algebraic identity, documented; sub-ulp divergence from R's numeric summation for non-0/1 act).

Reduced scope (documented in the source): which/sort/digits/drop presentation args skipped (full unrounded np x 3 matrix returned); na.rm=FALSE skipped; Inf / empty row sets / out-of-domain caps / scale=TRUE with all-0.5 act are rejected where R recycles or yields NaN/Inf.

Testing

  • Spec-verify: APPROVED-WITH-CHANGES (9 findings, all 5 mandatory changes adopted; reviewer independently recomputed the anchors and all matched).
  • Six exact-Fraction-oracle anchors + error contract + MC-500 #[ignore] (scaled/unscaled column-constant ratios, bdev ratio exactly ln 2; bitwise column-permutation invariance).
  • 5 mutation kills EXECUTED: cap-on-mse/mae, bdev-uncapped, stride transpose, missing sqrt, baseline pair-removal. Unobservable algebraic rearrangements documented, not claimed.
  • cargo: 819 pass. pytest: 903 pass (2 pre-existing Windows-symlink-privilege environment failures, unrelated).

Stacked on #303 (elom).

Adversarial impl-review outcome

Five review rounds (independent adversarial reviewer agents), all findings fixed:

  • Round 1 (FINDINGS, fixed 35caf74): wrapper silently cast numeric strings (lists / object arrays / cap tuples) and accepted bool arrays; 0-D scalars promoted to 1-D before the shape check.
  • Round 2 (FINDINGS, fixed 7449f50): object-dtype arrays of np.bool_ bypassed the bool rejection (np.bool_ is not a bool subclass).
  • Round 3 (FINDINGS, fixed 89b8810): object-dtype None silently cast to NaN; now rejected (missing must be explicit np.nan).
  • Round 4 (FINDINGS, fixed 9f80291): np.ma.MaskedArray masks silently dropped by np.asarray; masked arrays now rejected.
  • Round 5: CLEAN — all fixes verified, final probe sweep (Decimal/Fraction accepted; datetime/timedelta/structured/masked rejected) passed.

Rust core reviewed CLEAN in Round 1 (no core findings in any round). All regression tests included in TestMetrics.

Rust core reimplementation of CRAN PlayerRatings 1.1-0 metrics()
(R/ratings.R 936-957, READ; no journal paper exists -- CRAN source is
the normative reference): per predictor column, binomial deviance on
the cap-clamped predictions and RMSE/MAE on the RAW uncapped
predictions (the R source quirk at lines 949/951), each times 100 and
optionally divided by the 0.5-constant-predictor baseline. Numerator
NaN removal is elementwise per pair; scaled baselines use the act-only
row set (a different set when the predictor column has NaNs). The bdev
baseline is implemented as the exact constant ln 2 (algebraic identity
documented in the source, sub-ulp divergence from R's summation for
non-0/1 act).

Reduced scope (documented): which/sort/digits/drop presentation
arguments are not implemented (full unrounded np x 3 matrix returned);
na.rm=FALSE not implemented; Inf, empty per-column row sets, out-of-
domain caps, and scale=TRUE with an all-0.5 act baseline are rejected
where R would recycle or emit NaN/Inf.

Tests: six exact-Fraction-oracle anchors (unscaled, cap quirk, scaled,
NaN row sets, baseline-row-set killer, two-column stride pins with
both columns pinned), error contract, MC-500 #[ignore] (scaled-vs-
unscaled column-constant ratios with bdev ratio exactly ln 2;
bitwise column-permutation invariance). Five mutation kills EXECUTED:
cap-on-mse/mae, bdev-uncapped, stride transpose, missing sqrt,
baseline pair-removal. Algebraic rearrangements (sqrt(a)/sqrt(b) vs
sqrt(a/b)) are documented as unobservable.

PyO3 binding metrics_rating (plain name) + Python wrapper with
complex/object-dtype/shape validation before casts; TestMetrics;
CHANGELOG.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 961fce97-7209-4a02-abe4-140a75279937

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch seonghobae-metrics

Comment @coderabbitai help to get the list of available commands.

seonghobae and others added 4 commits July 27, 2026 06:43
Impl-review findings: numeric strings (plain, object-dtype, and cap
tuples) were silently parsed as floats, bool arrays were accepted as
0/1, and 0-D scalars were promoted to 1-D by ascontiguousarray before
the shape check, bypassing the documented contract. Validation now
checks dtype kind and ndim before any cast; regression tests added.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Round-2 review finding: np.bool_ is not a bool subclass, so object
arrays of np.bool_ bypassed the bool rejection and cast to 0/1 floats.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Round-3 adversarial review found that object-dtype arrays containing
None were silently cast to NaN by astype(float64), bypassing the
explicit-missing contract (missing values must be passed as np.nan).
None (and any element that is str/bytes/bool/np.bool_) is now rejected
with ValueError before the cast, with regression tests for both act
and pred paths.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Round-4 adversarial review found that np.ma.MaskedArray inputs lost
their mask through np.asarray, so masked missing values were silently
counted as observed. Masked arrays are now rejected with ValueError
before conversion; missing values must be encoded as explicit np.nan.
Regression tests cover both act and pred paths.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@seonghobae
seonghobae merged commit 0937644 into seonghobae-elom Jul 31, 2026
6 checks passed
@seonghobae
seonghobae deleted the seonghobae-metrics branch July 31, 2026 12:38
@seonghobae

Copy link
Copy Markdown
Contributor Author

Note: this PR was squash-merged into a non-main stacked base, so its commits did not land on main via this PR. The feature set reached main via #374 (stack tip integration) after #290.

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