Skip to content

feat(scaling): Glicko-2 rating system (Glickman 2022 + PlayerRatings glicko2) - #301

Closed
seonghobae wants to merge 5 commits into
seonghobae-elofrom
seonghobae-glicko2
Closed

feat(scaling): Glicko-2 rating system (Glickman 2022 + PlayerRatings glicko2)#301
seonghobae wants to merge 5 commits into
seonghobae-elofrom
seonghobae-glicko2

Conversation

@seonghobae

@seonghobae seonghobae commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Glicko-2 rating system (iteration 60)

Implements glicko2_rating — Glicko-2 with per-player rating volatility — as a Rust core + thin PyO3/Python wrapper.

Sources (citation governance)

  • READ: Glickman, M. E. (2022). Example of the Glicko-2 system (technical note, glicko.net). Steps 1-8 incl. the revised (2012-02-22) Illinois volatility algorithm; worked example reproduced by the crate to 1e-7.
  • READ: CRAN PlayerRatings 1.1-0 glicko2() (R/ratings.R lines ~412-586) and glicko2_c C kernel — batch-period semantics, participant-only lag * sigma^2 inflation (source comment: "nlag*(cvols^2) in Glicko-2, (nlag+1)*(cval^2) in Glicko"), q * rdmax volatility ceiling, tau>0 gate, gamma signs, tallies.
  • NOT READ (as-cited): Glickman (2001), J. Applied Statistics 28(6) — cited by both READ sources as the origin.
  • DERIVED (documented in-code): Glickman's Step-5 f(x) equals -1/2 the derivative of PlayerRatings' penalized negative log-likelihood, so the Illinois root coincides with R's optimize() optimum.
  • Documented deviation: the note applies Step 6 (phi' = sqrt(phi^2 + sigma^2)) to idle players every period; PlayerRatings — and this port — defer idle growth via lag * sigma^2 at next participation.

Verification

  • Spec-verify (adversarial): APPROVED-WITH-CHANGES, all 5 mandatory changes adopted (tolerance policy 1e-7 + byte-for-byte Illinois endpoint-A mandate; true return-after-idle fixture; sigma-clamp-active fixture; R-vs-note idle deviation documented; n_players dropped from the Rust signature).
  • Executed float64 oracle (independent of the crate): Glickman worked-example anchor, two-period inflation/lag/idle, rdmax clamp, volatility-ceiling clamp, gamma, unsorted-period == sorted, fractional-score + tau-0 freeze, return-after-idle.
  • 9 executed mutation kills (each verified to FAIL the pinned test, baseline re-verified OK): own-g swap, lag+1 inflation, variance-clamp drop, volatility-clamp drop, skipped volatility update, stale-sigma Step-6 inflation, Illinois endpoint-B return, gamma sign flip, rating-before-deviation order.
  • Every assert reads crate outputs; 500-rep Monte Carlo structural invariants (#[ignore], executed: pass).
  • Full suites: cargo 794 pass; pytest 305 pass.

Stacked on #298 (seonghobae-glicko).

Adversarial impl-review outcome

  • Round 1 (post-02cfa6b): FINDINGS ? 3 Low. (1) Python wrapper leaked
    TypeError/OverflowError instead of the documented ValueError for
    int(n_players) with inf, object-dtype gamma, and float(tau/rdmax)
    with None. (2) Error-contract tests claimed exhaustive coverage but
    omitted non-finite init/rdmax, negative score, and 18 Python-side cases.
    (3) PyO3 binding truncated u64 player ids with as usize on 32-bit
    targets. Core algorithm, Illinois volatility loop, ordering, lag
    semantics, and all paper anchors confirmed clean.
  • Fixes (ab8f7ec): narrow try/except -> ValueError re-raise in the
    wrapper (red-green mutation kill EXECUTED); usize::try_from with
    PyValueError in the binding; Rust g2_error_contract + Python
    TestGlicko2 extended incl. the ln(10)/400*rdmax volatility-ceiling
    boundary (at-ceiling Ok, just-above Err).
  • Round 2 (post-ab8f7ec): CLEAN.

seonghobae and others added 3 commits July 27, 2026 03:27
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>
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>
…glicko2)

Rust core glicko2_rating with batch-per-period updates on the Glicko-2
scale: participant-only pre-period variance inflation
phi^2 <- min(phi^2 + lag * sigma^2, (q rdmax)^2) (Glicko-2 lag, not
Glicko-1 lag+1; R source comment pinned), per-player volatility via
Glickman's Step-5 Illinois iteration (eps 1e-6, endpoint A; DERIVED:
f(x) = -1/2 d/dx of PlayerRatings' penalized nllh, so the Illinois root
matches R's optimum), tau == 0 volatility freeze, volatility ceiling
q * rdmax, per-game white advantage gamma, W/D/L and lag bookkeeping.
Documented R-vs-note deviation: idle players get no per-period Step-6
growth; lag * sigma^2 applies at next participation.

Anchored to an executed float64 oracle: Glickman worked-example anchor
(r'=1464.05, RD'=151.52, sigma'=0.059996) with heterogeneous init,
two-period inflation/lag/idle pins, rdmax + volatility-ceiling clamps,
gamma, unsorted-period, fractional-score + tau-0, return-after-idle.
Nine executed mutation kills (own-g swap, lag off-by-one, variance-clamp
drop, volatility-clamp drop, skipped volatility update, stale-sigma
inflation, Illinois endpoint swap, gamma sign, rating-before-deviation).

PyO3 binding glicko2_rating; Python wrapper fast_mlsirm.glicko2_rating
returning Glicko2Result, inheriting the Elo/Glicko period-label fidelity
contract. cargo 794 pass; pytest 305 pass.

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: cb987db2-4b12-4119-8ef9-44a17e085500

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

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

- Python wrapper: wrap int(n_players), gamma asarray, and float(tau)/
  float(rdmax) in narrow try/except -> ValueError so callers see the
  documented exception type instead of leaked TypeError/OverflowError
  (red-green mutation kill EXECUTED: reverting the n_players guard
  fails the error-contract test).
- PyO3 binding: usize::try_from for white/black player ids instead of
  'as usize' truncation on 32-bit targets; PyValueError on overflow.
- Error-contract coverage: Rust g2_error_contract adds non-finite init
  arrays, non-finite/negative rdmax, negative score, and at/above the
  ln(10)/400*rdmax volatility ceiling boundary; Python TestGlicko2 adds
  18 cases (out-of-range/negative index, score bounds, gamma shape/
  non-finite/complex/object, non-finite tau/rdmax, inf/None n_players,
  None tau/rdmax, init NaN/length mismatch, 10000-player cap).

cargo g2_: 11 pass; pytest TestGlicko2: 5 pass.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Base automatically changed from seonghobae-glicko to seonghobae-elo July 31, 2026 12:37
* feat(scaling): Stephenson rating system (PlayerRatings steph())

Rust core stephenson_rating extending Glicko with per-game neighborhood
variance (ngames*hval^2), per-game bonus bval/100 on both sides,
participants-only lambda drift toward opponents, and (lag+1)*cval^2
per-period deviation-variance inflation clamped at rdmax^2. Normative
source: CRAN PlayerRatings 1.1-0 R driver (ratings.R 591-737) + C kernel
(ratings.c stephenson_c 157-202), both READ and line-cited; no journal
paper exists (Kaggle-2010 provenance noted as NOT independently
verifiable). PyO3 binding + NumPy wrapper with PlayerRatings defaults.

Tests anchored to an EXECUTED faithful oracle port (S1 heterogeneous
init, S2 two-period draw/lag, S3 full knobs, S4 rdmax clamp +
prior-run continuation, S5 bval symmetry, lambda=0 contrast; 1e-12
pins), 500-rep MC invariants (#[ignore]), and five EXECUTED mutation
kills: bval drop, lambda sign flip, per-game hval scaling drop,
(lag+1)->lag, opponent-g->own-g.

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

* fix(stephenson): impl-review round-1 fixes (1 High, 2 Medium)

- Enforce the 2..=10000 n_players cap in the Python wrapper BEFORE any
  length-n allocation (High: huge n_players previously attempted the
  allocation instead of raising ValueError).
- Preserve integer fidelity for white/black player-id columns: integer
  dtypes cast directly to u64; float/object inputs are rejected at or
  above the dtype's exact-integer bound before the uint64 cast, matching
  the existing period-label contract (Medium).
- Guard u64 counter overflow in the Rust core: init_games/init_lag
  values that could overflow across the run's increments now return Err
  instead of panicking (debug) or wrapping (release) (Medium).

Regression tests: Rust st_error_contract overflow cases; Python
fidelity + pre-allocation cap cases.

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

---------

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

Copy link
Copy Markdown
Contributor Author

Superseded by #374 which lands the remaining #290#328 stack tip (seonghobae-ncohen feature set) onto main after #290 squash-merge made intermediate retargets CONFLICTING. Content preserved in #374 merge.

@seonghobae seonghobae closed this Jul 31, 2026
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