Skip to content

Add predict_rating game-outcome prediction (PlayerRatings predict.rating) - #306

Merged
seonghobae merged 2 commits into
seonghobae-fidefrom
seonghobae-predict
Jul 31, 2026
Merged

Add predict_rating game-outcome prediction (PlayerRatings predict.rating)#306
seonghobae merged 2 commits into
seonghobae-fidefrom
seonghobae-predict

Conversation

@seonghobae

@seonghobae seonghobae commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements predict_rating / predict_rating_multi — game-outcome prediction from fitted rating states, a Rust reimplementation of CRAN PlayerRatings 1.1-0 predict.rating (R/ratings.R lines 1056–1133, READ and normative; no journal paper exists for this dispatch — CRAN package provenance only). Stacked on #305 (fide). This completes coverage of the PlayerRatings package (elo, glicko, glicko2, steph, elom, fide, metrics, predict).

Model contract

Three branches, matching the R dispatch:

  • Elo logistic (no deviations): pred = 1 / (1 + 10^((brat − wrat − gamma)/400)), per-game or scalar gamma.
  • Glicko-family deviation shrink: exponent multiplied by vec = 1/sqrt(1 + qip3·(wdev² + bdev²)) with qip3 = 3(ln10/400/π)² — the joint two-player shrink from the R source (both players' squared deviations inside ONE sqrt).
  • EloM multi-player: pred = (rating − rowmean_{na.rm})/40 per row of a nr×np player matrix; optional placing returns min-tie ranks of −pred with NaN kept.

R semantics preserved: strict games < tng unrated cutoff (stored value → NA), trat replaces ALL missing extracted values (unmatched −1, low-games, stored-NA alike), pred >= thresh → 1/0 with NaN propagation.

REDUCED-SCOPE vs R: index-based players (−1 = unmatched; caller does name matching), no data-frame plumbing.

Verification evidence

  • Spec-verify (adversarial, before implementation): APPROVED-WITH-CHANGES; all 4 mandatory changes adopted (trat-replaces-stored-NA semantics, oracle header wording, NaN-allowed/Inf-rejected error contract, P9 stored-NA fixtures + scalar-gamma broadcast test).
  • Exact oracle EXECUTED (predict_oracle.py, fixtures P1–P9): pins include P2 = 0.7898441797581306, P3 = 0.776912664201114, P9 = 0.7844611342833985, qip3 = 1.007239860196398e-05.
  • Mutation testing — 7/7 EXECUTED kills (source restored byte-identical):
    Mutant Mutation Killed by
    MU1 gamma sign flip pr_anchor_elo_gamma_sign
    MU2 qip3 factor 3→2 pr_deviation_shrink
    MU3 two-player < tng<= pr_tng_boundary
    MU3b multi < tng<= pr_elom_rowmean
    MU4 divisor 400→200 pr_anchor_elo_gamma_sign
    MU5 placing rank >>= pr_placing_min_ties
    MU6 thresh >=> pr_thresh_ge_and_nan
  • Every assert reads crate/wrapper outputs (no test-local identities).
  • Suites: cargo test -p mlsirm-core --lib → 833 passed, 0 failed; pytest tests/test_paper_features.py → 328 passed (incl. new TestPredict ×6). pr_mc_500_invariants MC-500 #[ignore] test passes.

Adversarial impl-review outcome

  • Round 1: FINDINGS (2 MAJOR) — (1) Python wrappers lost games/tng integer fidelity above 2^53 via a float round-trip, shifting the strict games < tng cutoff; (2) predict_rating_multi computed nr * np unchecked, so a wrapping product could pass the length check and panic across the PyO3 boundary. Both fixed in 4cbba18: lossless _predict_games_u64/_predict_tng_u64 validators (int inputs exact to u64; float inputs bounded by the source dtype's exact-integer limit) and checked_mul with a checked error, plus Rust + pytest regressions for both.
  • Round 2: CLEAN — round-1 repros re-run (NaN / ValueError, no panic), new edge probes (u64-max games, float32 fidelity bound, np scalar/0-d tng, Fraction/Decimal, bool/masked rejection) passed, pr_ suite 8 pass, TestPredict 7 pass, oracle pins unchanged.

…ting)

Rust core predict_rating_two/predict_rating_multi in mlsirm-core scaling
(Elo logistic, deviation-shrunk Glicko-family with qip3 = 3(ln10/400/pi)^2,
EloM rowmean branch with optional min-tie placing), PyO3 bindings, Python
wrappers, exact-oracle anchor tests P1-P9, 7-mutant EXECUTED kill map,
MC-500 invariants, TestPredict pytest coverage, CHANGELOG entry.

Normative source: CRAN PlayerRatings 1.1-0 R/ratings.R lines 1056-1133
(READ). REDUCED-SCOPE: index-based players, per-game/scalar gamma.

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: 3a016896-5c78-4341-bd02-271cda70ad37

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-predict

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

Impl-review round-1 findings: (1) games/tng went through a float
round-trip in the Python wrappers, losing exact integer counts at or
above 2^53 and silently shifting the strict games < tng cutoff — new
_predict_games_u64/_predict_tng_u64 keep integer inputs lossless and
reject float inputs at/above the source dtype's exact-integer bound;
(2) predict_rating_multi computed nr*np unchecked, so a wrapping
product could pass the length check and panic on indexing — now
checked_mul with a checked error. Regression tests at both the Rust
error contract and pytest levels.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@seonghobae
seonghobae merged commit d6f3790 into seonghobae-fide Jul 31, 2026
6 checks passed
@seonghobae
seonghobae deleted the seonghobae-predict 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