The ‘elo’ Package

Ethan Heinzen

2019-01-20

The elo Package

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

library(elo)

Naming Schema

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

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

The elo.run() function

With two variable Elos

To calculate a series of Elo updates, use elo.run(). This function has a formula = and data = interface. We first load the dataset tournament.

data(tournament)
str(tournament)
## 'data.frame':    56 obs. of  6 variables:
##  $ team.Home     : chr  "Blundering Baboons" "Defense-less Dogs" "Fabulous Frogs" "Helpless Hyenas" ...
##  $ team.Visitor  : chr  "Athletic Armadillos" "Cunning Cats" "Elegant Emus" "Gallivanting Gorillas" ...
##  $ points.Home   : num  14 21 15 13 22 18 20 23 25 23 ...
##  $ points.Visitor: num  22 18 11 15 13 20 22 10 16 18 ...
##  $ week          : num  1 1 1 1 2 2 2 2 3 3 ...
##  $ half          : chr  "First Half of Season" "First Half of Season" "First Half of Season" "First Half of Season" ...

formula = should be in the format of wins.A ~ team.A + team.B. The score() function will help to calculate winners on the fly (1 = win, 0.5 = tie, 0 = loss).

tournament$wins.A <- tournament$points.Home > tournament$points.Visitor
elo.run(wins.A ~ team.Home + team.Visitor, data = tournament, k = 20)
## 
## An object of class 'elo.run', containing information on 8 teams and 56 matches.
elo.run(score(points.Home, points.Visitor) ~ team.Home + team.Visitor, data = tournament, k = 20)
## 
## An object of class 'elo.run', containing information on 8 teams and 56 matches.

For more complicated Elo updates, you can include the special function k() in the formula = argument. Here we’re taking the log of the win margin as part of our update.

elo.run(score(points.Home, points.Visitor) ~ team.Home + team.Visitor +
        k(20*log(abs(points.Home - points.Visitor) + 1)), data = tournament)
## 
## An object of class 'elo.run', containing information on 8 teams and 56 matches.

It’s also possible to adjust one team’s Elo for a variety of factors (e.g., home-field advantage). The adjust() special function will take as its second argument a vector or a constant.

elo.run(score(points.Home, points.Visitor) ~ adjust(team.Home, 10) + team.Visitor,
        data = tournament, k = 20)
## 
## An object of class 'elo.run', containing information on 8 teams and 56 matches.

With a fixed-Elo opponent

elo.run() also recognizes if the second column is numeric, and interprets that as a fixed-Elo opponent.

tournament$elo.Visitor <- 1500
elo.run(score(points.Home, points.Visitor) ~ team.Home + elo.Visitor,
        data = tournament, k = 20)
## 
## An object of class 'elo.run', containing information on 8 teams and 56 matches.

Regress Elos back to the mean

The special function regress() can be used to regress Elos back to a fixed value after certain matches. Giving a logical vector identifies these matches after which to regress back to the mean. Giving any other kind of vector regresses after the appropriate groupings (see, e.g., duplicated(..., fromLast = TRUE)). The other three arguments determine what Elo to regress to (to =, which could be a different value for different teams), by how much to regress toward that value (by =), and whether to regress teams which aren’t actively playing (regress.unused =).

tournament$elo.Visitor <- 1500
elo.run(score(points.Home, points.Visitor) ~ team.Home + elo.Visitor +
        regress(half, 1500, 0.2),
        data = tournament, k = 20)
## 
## An object of class 'elo.run.regressed', containing information on 8 teams and 56 matches, with 2 regressions.

Group matches

The special function group() doesn’t affect elo.run(), but determines matches to group together in as.matrix() (below).

Helper functions

There are several helper functions that are useful to use when interacting with objects of class "elo.run".

summary.elo.run() reports some summary statistics.

e <- elo.run(score(points.Home, points.Visitor) ~ team.Home + team.Visitor,
             data = tournament, k = 20)
summary(e)
## 
## An object of class 'elo.run', containing information on 8 teams and 56 matches.
## 
## Mean Square Error: 0.2195
## AUC: 0.6304
## Favored Teams vs. Actual Wins: 
##        Actual
## Favored  0 0.5  1
##   TRUE   6   1 16
##   (tie)  2   1  9
##   FALSE  8   3 10

as.matrix.elo.run() creates a matrix of running Elos.

head(as.matrix(e))
##      Athletic Armadillos Blundering Baboons Cunning Cats Defense-less Dogs
## [1,]            1510.000           1490.000     1500.000          1500.000
## [2,]            1510.000           1490.000     1490.000          1510.000
## [3,]            1510.000           1490.000     1490.000          1510.000
## [4,]            1510.000           1490.000     1490.000          1510.000
## [5,]            1499.425           1490.000     1500.575          1510.000
## [6,]            1499.425           1500.575     1500.575          1499.425
##      Elegant Emus Fabulous Frogs Gallivanting Gorillas Helpless Hyenas
## [1,]         1500           1500                  1500            1500
## [2,]         1500           1500                  1500            1500
## [3,]         1490           1510                  1500            1500
## [4,]         1490           1510                  1510            1490
## [5,]         1490           1510                  1510            1490
## [6,]         1490           1510                  1510            1490

as.data.frame.elo.run() gives the long version (perfect, for, e.g., ggplot2).

str(as.data.frame(e))
## 'data.frame':    56 obs. of  7 variables:
##  $ team.A: Factor w/ 8 levels "Athletic Armadillos",..: 2 4 6 8 3 4 7 8 4 3 ...
##  $ team.B: Factor w/ 8 levels "Athletic Armadillos",..: 1 3 5 7 1 2 5 6 1 2 ...
##  $ p.A   : num  0.5 0.5 0.5 0.5 0.471 ...
##  $ wins.A: num  0 1 1 0 1 0 0 1 1 1 ...
##  $ update: num  -10 10 10 -10 10.6 ...
##  $ elo.A : num  1490 1510 1510 1490 1501 ...
##  $ elo.B : num  1510 1490 1490 1510 1499 ...

Finally, final.elos() will extract the final Elos per team.

final.elos(e)
##   Athletic Armadillos    Blundering Baboons          Cunning Cats 
##              1564.318              1453.079              1518.019 
##     Defense-less Dogs          Elegant Emus        Fabulous Frogs 
##              1421.394              1509.851              1532.986 
## Gallivanting Gorillas       Helpless Hyenas 
##              1513.944              1486.411

Making Predictions

It is also possible to use the Elos calculated by elo.run() to make predictions on future match-ups.

results <- elo.run(score(points.Home, points.Visitor) ~ adjust(team.Home, 10) + team.Visitor,
                   data = tournament, k = 20)
newdat <- data.frame(
  team.Home = "Athletic Armadillos",
  team.Visitor = "Blundering Baboons"
)
predict(results, newdata = newdat)
## [1] 0.6676045

Basic Functions Revisited - Formula Interface

All three of the “basic” functions accept formulas as input, just like elo.run().

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:

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