Skip to content

feat(scaling): Glicko rating system with deviation inflation - #298

Merged
seonghobae merged 2 commits into
seonghobae-elofrom
seonghobae-glicko
Jul 31, 2026
Merged

feat(scaling): Glicko rating system with deviation inflation#298
seonghobae merged 2 commits into
seonghobae-elofrom
seonghobae-glicko

Conversation

@seonghobae

@seonghobae seonghobae commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Iteration 59 of the autonomous paper-implementation loop. Stacked on #297 (Elo).

Summary

Implements the Glicko rating system (Glickman) as a Rust core mlsirm_core::scaling::glicko_rating with a thin PyO3 binding and Python wrapper fast_mlsirm.glicko_rating.

Citation governance

  • READ: Glickman, M. E. (n.d.). The Glicko system [Technical note]. http://www.glicko.net/glicko/glicko.pdf — Step 1b/Step 2 formulas; the worked example (r=1500, RD=200 vs three opponents → r'=1464, RD'=151.4) is reproduced to full float64 precision by gk_paper_anchor_ga.
  • READ: Stephenson, A., & Sonas, J. (2020). PlayerRatings 1.1-0 [R package]. R/ratings.R glicko() + src/ratings.c glicko_c — batch-per-period reference (cval inflation, rdmax clamp, gamma, W/D/L + lag bookkeeping).
  • NOT READ (as-cited): Glickman, M. E. (1999). Parameter estimation in large dynamic paired comparison experiments. Applied Statistics, 48(3), 377–394.

Model

Per period (ascending label, batch semantics): participant-only variance inflation v = min(v + (lag+1)c², rdmax²); g = 1/sqrt(1 + 3(q/π)²v) post-inflation; per-game accumulation with opponent g; variance update first, then rating with the new variance. Documented non-identity: no rating-sum conservation (asymmetric opponent-g weighting; pinned ≠ 6600 by test).

Verification

  • Executed float64 oracle (fixtures GA–GF); spec-verify verdict APPROVED-WITH-CHANGES, all 6 mandatory changes adopted (fractional-score fixture, expanded mutation plan, inherited elo fidelity contract, n≥2, Result<_,String>, no-status divergence documented).
  • 7 executed mutation kills: MU1 opponent-g swap (heterogeneous-RD anchor; uniform-RD fixtures are blind), MU2 inflation off-by-one, MU3 clamp drop, MU4 stale-variance update, MU5 missing q², MU7 gamma sign (exact pins), MU9 all-player inflation (full dev-vector pin; idle player must not inflate).
  • Every assert reads crate-returned values; gk_mc_500 (#[ignore]) executed once, pass.
  • Full suites: cargo 785 pass; pytest 300 pass.
  • Python wrapper inherits the Elo period-label fidelity contract (integer-dtype lossless u64 path; dtype-derived float bound — float32 2^24, float64 2^53), regression-tested.

Adversarial implementation review (2 rounds)

Round 1 ? FINDINGS (files/glicko_impl_review.md): confirmed all core semantics clean vs PlayerRatings R/C reference (participant-only pre-period inflation with rdmax clamp, post-inflation g for all players, opponent-g accumulation, variance-before-rating update order, gamma signs, exact-only tallies, lag order; GB deviation-vector pin distinguishes MU9, GD pins kill gamma sign swap). One Medium defect: the float period-fidelity bound used 2.0 ** np.finfo(dtype).nmant ? nmant excludes the implicit leading bit, so exactly representable labels were rejected one power of two early (float32 at 2^23, float64 at 2^52) in both the elo and glicko wrappers.

Fix (bb2a441): bound is now 2.0 ** (np.finfo(dtype).nmant + 1) (float64 2^53, float32 2^24, float16 2^11) with the >= comparison kept (2^53 itself stays rejected: 2^53 + 1 aliases onto it). Boundary tests added for both elo and glicko pin acceptance of 2^24 - 1 (float32) and 2^53 - 1 (float64) via crate game tallies; these asserts FAIL under the old 2**nmant code, killing that mutant.

Round 2 ? CLEAN (files/glicko_impl_review_round2.md): fix verified correct for all three float dtypes, >= semantics confirmed, boundary tests confirmed mutation-killing, TestGlicko + TestElo 11/11 pass, no new defects in the fix diff.

Implements the Glicko rating system as a Rust core
(mlsirm_core::scaling::glicko_rating) with a thin PyO3 binding and Python
wrapper. Sources READ: Glickman's 'The Glicko system' technical note
(worked example reproduced to full float64 precision) and CRAN
PlayerRatings 1.1-0 glicko()/glicko_c. Glickman (1999), the derivation
paper, was NOT read and is cited as the origin per both READ sources.

- Batch-per-period Step 2 updates with opponent-g weighting and the
  new-variance rating step; participant-only Step 1b inflation
  RD = min(sqrt(RD^2 + (lag+1) c^2), rdmax).
- Per-player init_rating/init_dev arrays (heterogeneous RDs); results
  cover ALL 0..n players (documented no-status divergence from R).
- Documented non-identity: no rating-sum conservation (pinned by test).
- Tests anchored to an executed float64 oracle: Glickman worked-example
  anchor, two-period inflation/lag/idle-player full-vector pins,
  rdmax clamp, gamma exact pins, unsorted periods, fractional score,
  error contract, MC-500 (#[ignore]).
- Seven executed mutation kills: opponent-g swap, inflation off-by-one,
  clamp drop, stale-variance update, missing q^2, gamma sign,
  all-player inflation.
- Python wrapper inherits the Elo period-label fidelity contract
  (integer-dtype lossless u64 path; dtype-derived float bound).

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: efaf8670-9060-4719-b69c-6ef3e70a467d

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

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

np.finfo(dtype).nmant excludes the implicit leading bit, so the
exact-integer ceiling of a float dtype is 2**(nmant + 1), not 2**nmant.
The elo/glicko wrappers were rejecting exactly representable period
labels one power of two early (float32 at 2**23, float64 at 2**52).
Bound is now 2**(nmant + 1) with the >= comparison kept (2**53 itself
is ambiguous because 2**53 + 1 rounds onto it). Boundary tests pin
acceptance of 2**24 - 1 (float32) and 2**53 - 1 (float64) via crate
game tallies, killing a 2**nmant mutant.

Found by adversarial implementation review of PR #298.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@seonghobae
seonghobae merged commit 474889c into seonghobae-elo Jul 31, 2026
6 checks passed
@seonghobae
seonghobae deleted the seonghobae-glicko branch July 31, 2026 12:37
@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