elo
PackageThe elo
package includes functions to address all kinds of Elo calculations.
library(elo)
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
.
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
elo.run()
functionTo 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.
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.
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 two arguments determine what Elo to regress to (to =
), and by how much to regress toward that value (by =
).
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.
The special function group()
doesn’t affect elo.run()
, but determines matches to group together in as.matrix()
(below).
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 16 5 35
## FALSE 0 0 0
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
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
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