Deep dive
How it works
In: a player's (rating, RD, σ) and the results against opponents' (rating, RD) in one period · Out: new (rating, RD, σ) · State: three floats per player plus the time of the last period · Cost: O(games) per player per period plus a ~10-iteration root find.
Work on the Glicko-2 scale: μ = (r − 1500)/173.7178, φ = RD/173.7178.
- For each opponent j: g(φ_j) = 1/√(1 + 3φ_j²/π²) (discounts uncertain opponents), E_j = 1/(1 + exp(−g(φ_j)(μ − μ_j))).
- Estimated variance from games: v = [Σ g(φ_j)² E_j (1 − E_j)]⁻¹.
- Estimated improvement: Δ = v Σ g(φ_j)(s_j − E_j).
- New volatility σ′: solve f(x) = 0 for x = ln σ′² with the Illinois (regula falsi) method, where f(x) = eˣ(Δ² − φ² − v − eˣ) / (2(φ² + v + eˣ)²) − (x − ln σ²)/τ². Tolerance ε = 10⁻⁶.
- Pre-period deviation φ* = √(φ² + σ′²) (uncertainty grows with time/volatility).
- φ′ = 1/√(1/φ*² + 1/v); μ′ = μ + φ′² Σ g(φ_j)(s_j − E_j).
- Convert back: r′ = 173.7178 μ′ + 1500, RD′ = 173.7178 φ′.
- A player with no games in the period: μ and σ unchanged, φ′ = √(φ² + σ²).
Code
use std::f64::consts::PI;
const SCALE: f64 = 173.7178;
#[derive(Clone, Copy, Debug)]
pub struct Rating { pub r: f64, pub rd: f64, pub vol: f64 }
impl Default for Rating { fn default() -> Self { Rating { r: 1500.0, rd: 350.0, vol: 0.06 } } }
fn g(phi: f64) -> f64 { 1.0 / (1.0 + 3.0 * phi * phi / (PI * PI)).sqrt() }
/// One rating period. `games` = (opponent pre-period rating, score 1 / 0.5 / 0).
pub fn update(p: Rating, games: &[(Rating, f64)], tau: f64) -> Rating {
let mu = (p.r - 1500.0) / SCALE;
let phi = p.rd / SCALE;
if games.is_empty() {
let phi_star = (phi * phi + p.vol * p.vol).sqrt();
return Rating { rd: (phi_star * SCALE).min(350.0), ..p };
}
let (mut v_inv, mut sum) = (0.0, 0.0);
for (o, s) in games {
let (mu_j, phi_j) = ((o.r - 1500.0) / SCALE, o.rd / SCALE);
let gj = g(phi_j);
let e = 1.0 / (1.0 + (-gj * (mu - mu_j)).exp());
v_inv += gj * gj * e * (1.0 - e);
sum += gj * (s - e);
}
let v = 1.0 / v_inv;
let delta = v * sum;
// Volatility: Illinois algorithm on f(x) = 0, x = ln(sigma'^2).
let a = (p.vol * p.vol).ln();
let (d2, p2) = (delta * delta, phi * phi);
let f = |x: f64| {
let ex = x.exp();
ex * (d2 - p2 - v - ex) / (2.0 * (p2 + v + ex).powi(2)) - (x - a) / (tau * tau)
};
let mut big_a = a;
let mut big_b = if d2 > p2 + v {
(d2 - p2 - v).ln()
} else {
let mut k = 1.0;
while f(a - k * tau) < 0.0 { k += 1.0; }
a - k * tau
};
let (mut fa, mut fb) = (f(big_a), f(big_b));
while (big_b - big_a).abs() > 1e-6 {
let c = big_a + (big_a - big_b) * fa / (fb - fa);
let fc = f(c);
if fc * fb <= 0.0 { big_a = big_b; fa = fb; } else { fa /= 2.0; } // Illinois step
big_b = c;
fb = fc;
}
let vol = (big_a / 2.0).exp();
let phi_star = (p2 + vol * vol).sqrt();
let phi_new = 1.0 / (1.0 / (phi_star * phi_star) + 1.0 / v).sqrt();
let mu_new = mu + phi_new * phi_new * sum;
Rating { r: SCALE * mu_new + 1500.0, rd: SCALE * phi_new, vol }
}
/// Win probability of a over b, accounting for both deviations (useful for matchmaking).
pub fn expected(a: Rating, b: Rating) -> f64 {
let phi = ((a.rd * a.rd + b.rd * b.rd).sqrt()) / SCALE;
1.0 / (1.0 + (-g(phi) * (a.r - b.r) / SCALE).exp())
}
#[test]
fn glickman_example() {
let p = Rating { r: 1500.0, rd: 200.0, vol: 0.06 };
let opp = |r, rd| Rating { r, rd, vol: 0.06 };
let games = [(opp(1400.0, 30.0), 1.0), (opp(1550.0, 100.0), 0.0), (opp(1700.0, 300.0), 0.0)];
let n = update(p, &games, 0.5);
assert!((n.r - 1464.06).abs() < 0.01, "{n:?}");
assert!((n.rd - 151.52).abs() < 0.01, "{n:?}");
assert!((n.vol - 0.05999).abs() < 1e-5, "{n:?}");
}
Complexity and constants
- O(games) per player per period plus the volatility root find: Illinois converges in order of 5-10 iterations of a few
expcalls. Rating 1M players per period takes order of a second on one core. - RD dynamics (one game per period, opponent RD 50, measured with the code below): RD 350 → 248 after 1 game, 147 after 5, 109 after 10 (about the Lichess provisional line), 82 after 20, 71 after 30. Inactivity regrows it by √(φ² + σ²) per period.
- τ: smaller (0.3) = volatility barely changes, ratings stable; larger (1.2) = quicker reaction to sudden strength changes, noisier. Glickman suggests testing values on your data by predictive accuracy.
Efficiency
Cost: one player-period is a loop over the player's games with an exp per game plus about 5-10 Illinois iterations; order of 1 s for 1M players per period on one core. Memory is three f64 per player (24 B) plus a timestamp. The bound is the exp calls and reading the game log.
Levers:
- Update inactive players in closed form φ′ = √(φ² + n·σ²) after n empty periods — skips n loop passes — sites that rate per game, not per period.
- Looser root-find tolerance than 10⁻⁶ — fewer iterations (order of) — large batch jobs where rating digits beyond the first decimal do not matter.
- Rate each player independently from pre-period ratings — embarrassingly parallel — millions of players.
- Cache g(φ_j) per opponent per period — saves a sqrt per game — when many players share opponents.
Naive → standard → state of the art:
- Elo with a fixed K · O(1) per game, no uncertainty
- Glicko-2 per rating period · O(games) plus ~10 iterations, converges in order of 10-20 games
- Bayesian team models (TrueSkill, OpenSkill) · cost grows with team size, handles multi-player outcomes
Implementation notes
- Rating period: batch all games of a period using every player's pre-period rating; Lichess-style per-game updates still use the opponent's rating from before the game.
- Real-time sites grow RD with elapsed time, not with periods: compute periods elapsed = Δt / period_length and apply step 8 that many times (or the closed form φ² + n·σ²) before the next game.
- Cap RD at the initial 350 so long-inactive players don't get absurd uncertainty.
- Show "?" or hide ratings while RD is high, and exclude provisional players from leaderboards (Lichess: RD > 110; leaderboards need more).
- Matchmaking: use
expectedwith both RDs, or match on rating while allowing wider windows for high-RD players (they need informative games). - Store σ with enough precision (f64); tiny volatility changes compound over thousands of games.
Pitfalls
- Using post-update opponent ratings within the same period: results depend on processing order.
- Mixing scales: forgetting the 173.7178 conversion (it is 400/ln 10) produces wildly wrong updates.
- Treating Glicko-2 as Elo with a variable K and dropping the volatility step: players whose strength changes fast (juniors) lag behind.
- Rating periods with a single game everywhere: the volatility estimate becomes noisy; the paper assumes moderate game counts per period.
- Pool separation: ratings from different time controls or variants are different populations; keep one rating per pool.
Variants and alternatives
In production
- Chess and game sites: Lichess (Glicko-2; a rating is provisional while the deviation is above 110, rankable below 75 in standard chess), Chess.com (Glicko).
- Programming contests: Toph uses Glicko-2 for contest ranks.
- Sports: an arXiv preprint adapts Glicko-2 to football match forecasting and season simulation.
- AI evaluation: ChessArena ranks LLMs playing each other with Glicko and samples matches to shrink rating deviation.
Implementations
| name | lang | repo | install | verdict | licence | health | as_of | notes |
|---|---|---|---|---|---|---|---|---|
| skillratings | Rust | atomflunder/skillratings | cargo add skillratings | — | — | — | — | Elo, Glicko, Glicko-2, TrueSkill and more |
| glicko_2 | Rust | ReagentX/glicko2 | cargo add glicko_2 | — | — | — | — | Standalone Glicko-2 crate |
| glicko2 | Python | deepy/glicko2 | pip install glicko2 | — | — | — | — | Glicko-2 implementation |
| openskill | Python | vivekjoshy/openskill.py | pip install openskill | — | — | — | — | Weng-Lin ratings for teams and free-for-all |
Sources
- Glickman, Example of the Glicko-2 System, glicko.net. pdf
- Glickman, Dynamic Paired Comparison Models with Stochastic Variances, Journal of Applied Statistics, 2001. paper
- Glickman, Parameter Estimation in Large Dynamic Paired Comparison Experiments, Applied Statistics (JRSS C), 1999. paper
- Herbrich, Minka, Graepel, TrueSkill: A Bayesian Skill Rating System, NIPS, 2006. paper
- Weng, Lin, A Bayesian Approximation Method for Online Ranking, JMLR, 2011. paper
- Glickman, The Glicko system, glicko.net. pdf
- Lichess, FAQ: provisional ratings. page
- Toph, Rating System. page
- Nguyen, Tran, An Adaptive Glicko-2 Rating Framework for Probabilistic Football Forecasting and Season Simulation, arXiv 2607.01722. arXiv
- ChessArena: A Chess Testbed for Evaluating Strategic Reasoning Capabilities of Large Language Models, arXiv 2509.24239. arXiv
- Chess.com Support, How do ratings work on Chess.com?. page