Deep dive
How it works
In: two ratings and a result (1 / 0.5 / 0) · Out: two updated ratings · State: one rating per competitor (plus a game count for K) · Cost: O(1) per result.
- Model: performance difference is logistic (most systems) or normal (Elo's original, FIDE's table) with scale chosen so 400 points ≈ 10:1 odds.
- Expected score from the difference D = R_A − R_B: E = 1/(1 + 10^(−D/400)). FIDE caps |D| at 400 for the calculation.
- Rating period: sum K(S − E) over all games in the period using pre-period ratings (FIDE: monthly), or update after every game (online).
- K-factor is the step size: larger K converges faster but is noisier. Typical: 32-40 for new players, 10-20 for established ones.
- Initial rating: provisional rating from the first n games (performance rating), then switch to K updates.
P(win) for A
1.0 | ______
| ___---
0.5 |-------------__--‾‾-------------- E = 1 / (1 + 10^(-D/400))
| ___---
0.0 |___---‾__________________________ D
-800 -400 0 400 800
Code
/// Logistic Elo expected score for A against B.
pub fn expected(ra: f64, rb: f64) -> f64 { 1.0 / (1.0 + 10f64.powf((rb - ra) / 400.0)) }
/// FIDE-style K factor (rating regulations, simplified).
pub fn fide_k(rating: f64, games_played: u32, age: u32, ever_reached_2400: bool) -> f64 {
if games_played < 30 || (age < 18 && rating < 2300.0) { 40.0 }
else if ever_reached_2400 { 10.0 }
else if rating < 2400.0 { 20.0 }
else { 10.0 }
}
/// One rating period: all games use the pre-period rating; difference capped at 400.
pub fn period_update(r: f64, k: f64, games: &[(f64, f64)]) -> f64 {
// FIDE: if K x games > 700, use the largest K with K x n <= 700.
let k = if k * games.len() as f64 > 700.0 { (700.0 / games.len() as f64).floor() } else { k };
let delta: f64 = games.iter()
.map(|&(opp, score)| {
let d = (r - opp).clamp(-400.0, 400.0);
score - 1.0 / (1.0 + 10f64.powf(-d / 400.0))
})
.sum();
r + k * delta
}
/// Per-game update for both players (online style). Returns new (ra, rb).
pub fn update(ra: f64, rb: f64, score_a: f64, ka: f64, kb: f64) -> (f64, f64) {
let ea = expected(ra, rb);
(ra + ka * (score_a - ea), rb + kb * ((1.0 - score_a) - (1.0 - ea)))
}
/// Performance rating of a result set (logistic inverse); used for provisional ratings.
pub fn performance(games: &[(f64, f64)]) -> f64 {
let n = games.len() as f64;
let avg_opp = games.iter().map(|g| g.0).sum::<f64>() / n;
let p = (games.iter().map(|g| g.1).sum::<f64>() / n).clamp(0.01, 0.99); // avoid ±infinity
avg_opp + 400.0 * (p / (1.0 - p)).log10()
}
#[test]
fn examples() {
assert!((expected(1700.0, 1500.0) - 0.7597).abs() < 1e-4);
let (a, b) = update(1500.0, 1700.0, 1.0, 20.0, 20.0); // upset win
assert!((a - 1515.19).abs() < 0.01 && (b - 1684.81).abs() < 0.01);
// Capped difference: a 2800 beating a 2000 is scored as if the gap were 400.
let r = period_update(2800.0, 10.0, &[(2000.0, 1.0)]);
assert!((r - 2800.91).abs() < 0.01);
assert!((performance(&[(2000.0, 0.5), (2200.0, 0.5)]) - 2100.0).abs() < 1e-9);
}
Complexity and constants
- O(1) per game; a rating period is a sum over the player's games.
- Logistic vs normal: the logistic curve has fatter tails, i.e. gives underdogs more chances; FIDE still uses a normal-based lookup table (σ of a single performance = 200, so σ of a difference ≈ 283).
- Convergence: with K = 20 a player 200 points underrated, scoring as expected for their true strength, gains about 20 × (0.76 − 0.5) ≈ 5 points per game against equals: dozens of games to converge. K = 40 halves that.
- Rating noise at equilibrium: a player of constant strength random-walks around it with standard deviation ≈ √(87·K) points (no draws): about 30 at K = 10, 42 at K = 20, 59 at K = 40. Draws reduce it.
Efficiency
Cost: one update is two powf/exp calls and a few multiplies: order of tens of ns on a modern core, compute-bound but negligible; storage is one f64 (8 bytes) per competitor plus a counter. Real systems are bound by reading the game stream, not by the arithmetic.
Levers:
- Fit Bradley-Terry in batch by maximum likelihood — order-independent, gives confidence intervals (LMSYS 2023 leaderboard post) — when the comparison set is fixed or replayed, as for an AI-model leaderboard.
- Rating periods — sum K(S − E) over a period's games with pre-period ratings (FIDE Rating Regulations) — when updates arrive in bursts and order must not matter.
- Table lookup of expected score — FIDE publishes a difference-to-score table (FIDE Rating Regulations) — when hand computation or exact reproducibility matters.
- Parallel batches — ratings of disjoint pairs update independently (order of) — when replaying millions of historic games.
Naive → standard → state of the art:
- Recompute all ratings from scratch after every game · O(games) per result
- Online Elo update · O(1) per result, 8 bytes per competitor
- Batch Bradley-Terry fit with bootstrap intervals (Chatbot Arena,
arena-rank) · iterative optimisation over all comparisons, run offline
Implementation notes
- Store ratings as floats (or tenths), round only for display; FIDE rounds per-game changes, accumulated rounding shifts long-run averages.
- Use the pre-period rating for every game in a period, otherwise results depend on game order.
- Draws matter: S = 0.5. In chess at master level, a large share of games are draws, and AI-model votes can end in ties; a model that predicts draw probability separately (e.g. Davidson extension of Bradley-Terry) helps forecasting but is not Elo.
- Inflation/deflation: new players entering above their strength and leaving after losing donates points; control the entry rating and K for new players. FIDE's 2024 changes (floor 1400, one-time adjustment for players under 2000) were a response to deflation at the bottom.
- For matchmaking, pair by rating difference but remember Elo has no uncertainty; give new players a provisional flag and higher K.
Pitfalls
- Using one K for everyone online: newcomers take forever to reach their level and distort everyone they play on the way.
- Comparing ratings across pools (FIDE vs USCF vs Lichess): each pool has its own offset and spread.
- Allowing rating manipulation: losing on purpose to lower your rating for easier matches or prize sections; detect with performance anomalies.
- Expected-score formula with natural log scale by mistake (
exp(-D/400)): changes the scale by a factor ln 10. - Uncapped difference with tiny K at high ratings: a single loss to a much weaker player can wipe out a month of gains (FIDE's 400-point cap limits it).
Variants and alternatives
In production
- Chess: FIDE ratings (Elo with K 10/20/40); Lichess uses Glicko-2; USCF uses a logistic variant with variable K.
- Esports and online matchmaking: Microsoft's TrueSkill, built for Xbox Live, extends the idea to teams with uncertainty.
- Sports: FIFA's men's World Ranking has been Elo-based since 2018 ("SUM"); World Football Elo Ratings (eloratings.net); FiveThirtyEight sports forecasts (unlinked, not re-verified).
- AI-model leaderboards: LMSYS Chatbot Arena ranks models from human pairwise votes, first with online Elo, now Bradley-Terry.
Implementations
| name | lang | repo | install | verdict | licence | health | as_of | notes |
|---|---|---|---|---|---|---|---|---|
| skillratings | Rust | atomflunder/skillratings | cargo add skillratings | — | — | — | — | Elo, Glicko-2, TrueSkill and more; most used Rust option |
| arena-rank | Python | lmarena/arena-rank | pip install arena-rank | — | — | — | — | Bradley-Terry with confidence intervals; the LMArena leaderboard method |
| openskill | Python | vivekjoshy/openskill.py | pip install openskill | — | — | — | — | Multiplayer and team ratings (Weng-Lin), TrueSkill alternative |
Sources
- Elo, The Rating of Chessplayers, Past and Present, Arco, 1978.
- FIDE, FIDE Rating Regulations (Handbook B.02), fide.com. handbook
- Bradley, Terry, Rank Analysis of Incomplete Block Designs: I. The Method of Paired Comparisons, Biometrika, 1952. paper
- Glickman, A Comprehensive Guide to Chess Ratings, American Chess Journal, 1995.
- Glickman, Example of the Glicko-2 system. pdf
- Herbrich, Minka, Graepel, TrueSkill: A Bayesian Skill Rating System, NIPS 2006. paper · project
- Chiang et al., Chatbot Arena: An Open Platform for Evaluating LLMs by Human Preference, 2024. arXiv
- Chiang et al., Chatbot Arena: New models & Elo system update, LMSYS blog, 2023. post
- FIFA, Men's World Ranking procedure. page
- Lichess, Rating systems. page