Introduction to Elo Rankings

Elo is a system of ratings/rankings (named after its creator, Arpad Elo) for pairwise matchups. In short, pairs of “teams” (“A” and “B”) begin a match with rankings \(R_A\) and \(R_B\). The result (“score”) of the game is coded as 0/0.5/1 for loss/tie/win, respectively. The prior expectation of this result can be expressed as \[P_A = \frac{1}{1 + 10^{(R_B - R_A) / 400}}\] \[P_B = \frac{1}{1 + 10^{(R_A - R_B) / 400}} = 1 - P_A\] where \[P_i\] is the prior probability that team \(i\) wins the match.

After each match, ratings are updated as follows: \[R^{new}_A = R_A + K(S_A - P_A)\] \[R^{new}_B = R_B + K(S_B - P_B) = R_B + K(1 - S_A - (1 - P_A)) = R_B - K(S_A - P_A)\] where \(S_i\) is the score of team \(i\) (0/0.5/1) and \(K\) is an update weight (commonly called the “k-factor”).

Therefore, we see that the system as a whole (all teams) retains (“conserves”) its total sum of Elo ratings; for every rating point team A gains/loses, team B loses/gains the same amount.

The elo Package

The elo package includes functions to address all kinds of Elo calculations.

Naming Schema

Most functions begin with the prefix “elo.”, for easy autocompletion.

  • Vectors or scalars of Elo scores are denoted elo.A or elo.B.

  • Vectors or scalars of wins by team A are denoted by wins.A.

  • Vectors or scalars of win probabilities are denoted by p.A.

  • Vectors of team names are denoted team.A or team.B.

Basic Functions

To calculate the probability team.A beats team.B, use elo.prob():

elo.A <- c(1500, 1500)
elo.B <- c(1500, 1600)
elo.prob(elo.A, elo.B)
## [1] 0.500000 0.359935

To calculate the score update after the two teams play, use elo.update():

wins.A <- c(1, 0)
elo.update(wins.A, elo.A, elo.B, k = 20)
## [1] 10.0000 -7.1987

To calculate the new Elo scores after the update, use elo.calc():

elo.calc(wins.A, elo.A, elo.B, k = 20)
##      elo.A    elo.B
## 1 1510.000 1490.000
## 2 1492.801 1607.199

It may be helpful to calculate wins.A from raw scores:

points.A <- c(4, 1)
points.B <- c(3, 3)
elo.calc(score(points.A, points.B), elo.A, elo.B, k = 20)
##      elo.A    elo.B
## 1 1510.000 1490.000
## 2 1492.801 1607.199

Formula Interface

All of the “basic” functions accept formulas as input:

dat <- data.frame(elo.A = c(1500, 1500), elo.B = c(1500, 1600),
                  wins.A = c(1, 0), k = 20)
form <- wins.A ~ elo.A + elo.B + k(k)
elo.prob(form, data = dat)
## [1] 0.500000 0.359935
elo.update(form, data = dat)
## [1] 10.0000 -7.1987
elo.calc(form, data = dat)
##      elo.A    elo.B
## 1 1510.000 1490.000
## 2 1492.801 1607.199

Note that for elo.prob(), formula = can be more succinct:

elo.prob(~ elo.A + elo.B, data = dat)
## [1] 0.500000 0.359935

We can even adjust the Elos, for, e.g., home-field advantage.

elo.calc(wins.A ~ adjust(elo.A, 10) + elo.B + k(k), data = dat)
##      elo.A    elo.B
## 1 1509.712 1490.288
## 2 1492.534 1607.466

Final Thoughts

All of these functions assume that Elo scores are constant. The next vignette explores calculating “running” Elos.