Glicko-Boost Rating System

See How to choose a rating system for a decision guide across every system, or Glicko for the system Glicko-Boost extends.

Overview

Glicko-Boost is Mark Glickman’s extension of Glicko, written for the Deloitte/FIDE Chess Rating Challenge and described in Glicko-Boost. It adds four things to Glicko: an advantage for playing white, a second pass that re-rates every player against the opponents’ first-pass ratings, an RD “boost” for players whose results were exceptional, and a rating-dependent model for how RD grows between periods.

It is the first system in Elote whose published update is defined over a rating period rather than over one game. The two-pass structure cannot be expressed as a stream of pairwise updates – a player’s rating change depends on how their opponents did in the same period – so GlickoBoostCompetitor overrides apply_rating_period() and does the whole population at once. The pairwise API is unchanged: beat/lost_to/tied run the same algorithm over a one-game period.

How It Works

Every step is built on Glicko updating with a white-advantage term. For a player with rating \(r\) and rating deviation \(RD\) who played \(J\) games with scores \(s_j\) against opponents \((r_j, RD_j)\):

\[E(\eta, w_j, r, r_j, RD_j) = \frac{1}{1 + 10^{-g(RD_j)(r + w_j\eta - r_j)/400}}\]
\[g(RD) = \frac{1}{\sqrt{1 + 3q^2 RD^2 / \pi^2}}, \qquad q = \frac{\ln 10}{400}\]
\[d^2 = \left[q^2 \sum_{j=1}^{J} g(RD_j)^2 E (1 - E)\right]^{-1}\]
\[RD' = \sqrt{\left(\frac{1}{RD^2} + \frac{1}{d^2}\right)^{-1}}, \qquad r' = r + (RD')^2 q \sum_{j=1}^{J} g(RD_j)(s_j - E)\]

Here \(w_j\) is \(+1\) when the player had white in game \(j\), \(-1\) when black, and \(\eta\) is the rating advantage of playing white. With \(\eta = 0\) the term disappears and this is ordinary Glicko updating.

The period update

Given every player’s pre-period rating and RD, one period is five steps:

  1. Update every player from the pre-period ratings and RDs.

  2. Update every player again from the pre-period ratings and RDs, but against the opponents’ step 1 ratings and RDs. A player who lost to opponents who all had a strong month is now judged against those better ratings.

  3. Compute each player’s performance z-score and boost their pre-period RD if it is large (below).

  4. Repeat step 1 with the boosted RDs.

  5. Repeat step 2 on the step 4 results. These are the period’s final ratings and RDs.

If no RD was boosted in step 3, steps 4 and 5 reproduce steps 1 and 2 exactly, and Elote skips them.

The RD boost

The boost catches an improving player whose RD is too small for their rating to move fast enough. The z-score measures how far the period’s results ran ahead of what the player’s pre-period rating predicted, against the step 2 population:

\[Z = \frac{\sum_j g(RD_j)(s_j - E)}{\sqrt{\sum_j g(RD_j)^2 E (1 - E)}}\]
\[\begin{split}RD^{\dagger} = \begin{cases} RD & Z \le k \\ \min\left(RD_{unr},\ (1 + (Z - k)B_1) RD + B_2\right) & Z > k \end{cases}\end{split}\]

\(k = 1.96\) is the point beyond which a z-score would occur by chance 2.5% of the time, so on an ordinary month nobody is boosted and steps 4 and 5 never run.

RD increase over time

Instead of Glicko’s constant \(c\), the increase depends on both the RD and the rating, so that higher-rated players – whose strength is more stable – gain slightly less uncertainty per idle period:

\[RD_{new} = \min\left(RD_{unr},\ \sqrt{RD^2 + \exp\left(\alpha_0 + \alpha_1 RD + \alpha_2 RD \tfrac{r}{1000} + \alpha_3 \tfrac{r}{1000} + \alpha_4 \left(\tfrac{r}{1000}\right)^2\right)}\right)\]

Elote applies this when a competitor next takes part, for the rating periods it sat out, the way GlickoCompetitor handles inactivity. Results that share a period end therefore never inflate each other’s RD.

Prediction

expected_score uses the paper’s own approximation, which combines the two rating deviations rather than using only the opponent’s:

\[E_A = \frac{1}{1 + 10^{-g\left(\sqrt{RD_A^2 + RD_B^2}\right)(r_A + \eta - r_B)/400}}\]

With the default \(\eta = 0\) this is exactly complementary: a.expected_score(b) + b.expected_score(a) == 1.

The colour convention

Elote’s result API carries no colour argument, and Glicko-Boost does not get one added for it. Instead argument order is the colour: in a rating-period row (a, b, outcome, scores), and in a.beat(b), a is white and b is black.

Callers who have no colour information should leave _eta at its default of 0.0, which is why that – rather than the paper’s 30.0 – is Elote’s default. The update then depends only on rating differences and is invariant to the order in which each row is written.

Argument order is preserved through matchup() (and therefore tournament()) and rating_period(): a loss is dispatched as the caller’s lost_to, which reverses internally, so a row written white-first stays that way. The same holds for train_arena_with_dataset() and process_history(), which dispatch a loss in caller order too.

Key Parameters

All of these are class-level, so they are settable through configure_class and reachable from walk_forward() and tune() through competitor_params.

Parameter

Elote default

Meaning (Glickman’s optimized value)

eta

0.0

Rating advantage for playing white (30.0)

b1

0.20139

RD boost multiplicative factor (0.20139)

b2

17.5

RD boost additive factor (17.5)

k

1.96

z-score threshold above which the RD is boosted (1.96)

alpha0 .. alpha4

5.83733, -1.75374e-04, -7.080124e-05, 0.001733792, 0.00026706

RD-increase-over-time coefficients (same values)

rd_unrated

250.0

RD cap, \(RD_{unr}\) (250.0)

rating_period_days

30.0

Days in one rating period, used to turn elapsed time into RD increases

initial_rating (default 1500) and initial_rd (default 250) are constructor arguments, so they travel through base_competitor_kwargs rather than competitor_params. Glickman’s default rating for an unrated player is 1946.25, which is specific to the FIDE population the system was fitted on; Elote keeps its house default of 1500.

How It Differs From Glicko and Glicko-2

Glicko

Glicko-2

Glicko-Boost

Update granularity

One game (Elote replays a period pairwise)

One game

The whole period, twice over

Colour

Not modelled

Not modelled

\(\eta\) inside \(E()\), from argument order

Reacting to a surprising month

Nothing beyond the ordinary update

A per-player volatility \(\sigma\) updated every period

A one-off RD boost when \(Z > k\), then a re-rate

RD growth while idle

\(\sqrt{RD^2 + c^2 t}\)

Driven by volatility

\(\exp\) of a rating-and-RD-dependent polynomial

Expected score

\(g(RD_B)\) only

Transformed scale

\(g\!\left(\sqrt{RD_A^2 + RD_B^2}\right)\), so it is complementary

Glicko-2’s volatility is a persistent per-player parameter that is re-estimated every period; Glicko-Boost’s boost is a one-shot widening of the prior for the period being rated, discarded afterwards. Glicko-Boost is therefore cheaper and has no iterative solve, at the cost of not tracking volatility over time.

Implementation in Elote

from datetime import datetime
from elote import GlickoBoostCompetitor

# The paper's white advantage; leave it at 0.0 if your data has no colour.
GlickoBoostCompetitor.configure_class(eta=30.0)

alice = GlickoBoostCompetitor(initial_rating=2300, initial_rd=140)
bob = GlickoBoostCompetitor(initial_rating=2295, initial_rd=80)
cara = GlickoBoostCompetitor(initial_rating=2280, initial_rd=150)

# One rating period, applied to the whole population at once. The first
# competitor of each row had white.
GlickoBoostCompetitor.apply_rating_period(
    [
        (alice, bob, 0.0, None),
        (cara, alice, 1.0, None),
        (bob, cara, 0.5, None),
    ],
    period_end=datetime(2026, 1, 31),
)

print(alice.rating, alice.rd)
print(alice.expected_score(bob))

Through an arena, the same period is one call:

from elote import LambdaArena, GlickoBoostCompetitor

arena = LambdaArena(lambda a, b: True, base_competitor=GlickoBoostCompetitor)
arena.rating_period(
    [("alice", "bob", 0.0, None), ("cara", "alice", 1.0, None)],
    period_end=datetime(2026, 1, 31),
)
print(arena.leaderboard())

beat, lost_to and tied are supported and run the same algorithm over a one-game period, so a single result is never a different formula from a batch. Because of the two-pass structure a lone pairwise call is not identical to a Glicko update, and applying a period one row at a time does not give the period’s ratings – group results into periods when you have them.

Advantages

  • Period-native: rates a whole month the way the published system does, with no within-period ordering effect

  • Opponent-aware: the second pass judges a player against how their opponents actually did, not just their pre-period ratings

  • Catches improvers: the RD boost widens the prior for a player whose results were far ahead of their rating, so the re-rate can move them further

  • Colour: models the advantage of moving first, at no cost to the uniform API

  • Flexible RD growth: uncertainty grows at a rate that depends on the player’s rating and current RD

Limitations

  • Needs periods: the value is in the batch; a stream of one-game periods loses the second pass’s information

  • Many parameters: twelve system constants, fitted by Glickman on FIDE data and not necessarily right for another domain

  • No volatility tracking: unlike Glicko-2, nothing persists between periods about how erratic a player is

  • Colour is positional: rows have to be written white-first for \(\eta\) to mean anything

  • Cost: four Glicko passes over the population per period instead of one

Numeric reference

tests/test_GlickoBoostCompetitor_known_values.py checks this implementation against the worked example in Section 4 of Glickman’s paper. Two of that table’s columns are not reproducible from the paper’s own formulas – the module’s docstring records the measured deviations and the sweep that establishes it – so they are pinned as regression values while the rest of the table is asserted as the published oracle.