# 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.

• 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

# 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.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