library(elo)
elo.run()
functionIt is useful to allow Elos to update as matches occur. We refer to this as “running” Elo scores.
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).
$wins.A <- tournament$points.Home > tournament$points.Visitor
tournamentelo.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.
# on the fly
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.
You can also adjust the home and visitor teams with different k’s (but note that this no longer conserves total Elo score!):
<- 20*log(abs(tournament$points.Home - tournament$points.Visitor) + 1)
k1 elo.run(score(points.Home, points.Visitor) ~ team.Home + team.Visitor + k(k1, k1/2), 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.
$elo.Visitor <- 1500
tournamentelo.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.
Why would you want to do this? One instance might be when a person plays against a computer whose Elo score is known (or estimated).
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 (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 =
). Note here again that total Elo score might not be conserved.
$elo.Visitor <- 1500
tournamentelo.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()
tells elo.run()
when to update Elos. It also determines matches to group together in as.matrix()
.
<- elo.run(score(points.Home, points.Visitor) ~ team.Home + team.Visitor +
er group(week),
data = tournament, k = 20)
as.matrix(er)
## Athletic Armadillos Blundering Baboons Cunning Cats Defense-less Dogs
## [1,] 1510.000 1490.000 1490.000 1510.000
## [2,] 1499.425 1500.575 1500.575 1499.425
## [3,] 1489.425 1490.575 1510.575 1509.425
## [4,] 1499.458 1480.542 1520.542 1499.458
## [5,] 1489.458 1470.542 1520.542 1489.458
## [6,] 1490.033 1461.971 1509.681 1480.033
## [7,] 1501.153 1453.313 1499.945 1470.647
## [8,] 1509.785 1444.682 1509.104 1461.489
## [9,] 1519.765 1455.165 1499.124 1451.005
## [10,] 1527.812 1446.423 1507.865 1442.959
## [11,] 1537.372 1458.979 1508.123 1434.819
## [12,] 1547.007 1450.239 1518.364 1437.130
## [13,] 1556.067 1461.837 1528.173 1429.335
## [14,] 1564.318 1453.079 1518.019 1421.394
## Elegant Emus Fabulous Frogs Gallivanting Gorillas Helpless Hyenas
## [1,] 1490.000 1510.000 1510.000 1490.000
## [2,] 1500.575 1499.425 1499.425 1500.575
## [3,] 1490.575 1489.425 1509.425 1510.575
## [4,] 1480.542 1499.458 1499.458 1520.542
## [5,] 1490.542 1509.458 1509.458 1520.542
## [6,] 1501.403 1518.883 1508.883 1529.113
## [7,] 1510.789 1528.618 1517.541 1517.993
## [8,] 1501.302 1538.106 1527.554 1507.980
## [9,] 1502.056 1527.241 1526.800 1518.844
## [10,] 1512.539 1537.228 1516.813 1508.362
## [11,] 1502.978 1524.672 1516.555 1516.501
## [12,] 1511.718 1515.038 1514.245 1506.260
## [13,] 1501.910 1522.832 1505.185 1494.661
## [14,] 1509.851 1532.986 1513.944 1486.411
This can be useful in situations when using the Elo framework for games which aren’t explicitly head-to-head (e.g., golf, swimming). For those situations, the person who won can be considered as having beaten (head-to-head) every other person. The person who came in second “beat” everyone but the first. However, we wouldn’t want to update Elos after every “head-to-head”; rather, they should all be considered together in updating Elo.
An example might help clarify. Suppose participants 1-3 go head-to-head in a game, with participant 2 coming in first, participant 1 coming in second, and participant 3 coming in last. Then we might have a dataset like
<- data.frame(
d team1 = c("Part 2", "Part 2", "Part 1"),
team2 = c("Part 1", "Part 3", "Part 3"),
won = 1
) d
## team1 team2 won
## 1 Part 2 Part 1 1
## 2 Part 2 Part 3 1
## 3 Part 1 Part 3 1
We would want to consider all three of these matches at the same time, so we add a grouping variable and run elo.run()
:
$group <- 1
dfinal.elos(elo.run(won ~ team1 + team2 + group(group), data = d, k = 20))
## Part 1 Part 2 Part 3
## 1500 1520 1480
elo.run.multiteam()
The situation described immediately above (multiple teams instead of pairwise head-to-head) has a shortcut implemented: elo.run.multiteam()
. The helper function multiteam()
takes vectors of first-place teams (first column), second place teams (second column), etc., and does the heavy lifting of data manipulation for you. Note that this runs elo.run()
in the background, but is less flexible than elo.run()
because (1) there cannot be ties; (2) it does not accept adjustments; and (3) k-values are constant for each “game” (sets of head-to-head matchups).
<- data.frame(
d2 first = "Part 2",
second = "Part 1",
third = "Part 3"
)final.elos(elo.run.multiteam(~ multiteam(first, second, third), k = 20, data = d2))
## Part 1 Part 2 Part 3
## 1500 1520 1480
A larger example shows the utility of this function:
data("tournament.multiteam")
str(tournament.multiteam)
## Classes 'tbl_df', 'tbl' and 'data.frame': 28 obs. of 6 variables:
## $ week : num 1 1 2 2 3 3 4 4 5 5 ...
## $ half : chr "First Half of Season" "First Half of Season" "First Half of Season" "First Half of Season" ...
## $ Place_1: chr "Defense-less Dogs" "Athletic Armadillos" "Helpless Hyenas" "Elegant Emus" ...
## $ Place_2: chr "Fabulous Frogs" "Cunning Cats" "Cunning Cats" "Blundering Baboons" ...
## $ Place_3: chr "Blundering Baboons" "Gallivanting Gorillas" "Gallivanting Gorillas" "Athletic Armadillos" ...
## $ Place_4: chr "Helpless Hyenas" "Elegant Emus" "Defense-less Dogs" "Fabulous Frogs" ...
<- elo.run.multiteam(~ multiteam(Place_1, Place_2, Place_3, Place_4),
erm data = tournament.multiteam, k = 20)
final.elos(erm)
## Athletic Armadillos Blundering Baboons Cunning Cats
## 1672.333 1439.738 1559.373
## Defense-less Dogs Elegant Emus Fabulous Frogs
## 1336.748 1449.914 1524.204
## Gallivanting Gorillas Helpless Hyenas
## 1517.780 1499.910
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.
<- elo.run(score(points.Home, points.Visitor) ~ team.Home + team.Visitor,
e data = tournament, k = 20)
summary(e)
##
## An object of class 'summary.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
rank.teams(e)
## Athletic Armadillos Blundering Baboons Cunning Cats
## 1 7 3
## Defense-less Dogs Elegant Emus Fabulous Frogs
## 8 5 2
## Gallivanting Gorillas Helpless Hyenas
## 4 6
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 8 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.A: num -10 10 10 -10 10.6 ...
## $ update.B: 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.
<- elo.run(score(points.Home, points.Visitor) ~ adjust(team.Home, 10) + team.Visitor,
results data = tournament, k = 20)
<- data.frame(
newdat team.Home = "Athletic Armadillos",
team.Visitor = "Blundering Baboons"
)predict(results, newdata = newdat)
## [1] 0.6676045
We now get to elo.run()
when custom probability calculations and Elo updates are needed. Note that these use cases are coded in R instead of C++ and may run as much as 50x slower than the default.
For instance, suppose you want to change the adjustment based on team A’s current Elo:
<- function(wins.A, elo.A, elo.B, k, adjust.A, adjust.B, ...)
custom_update
{*(wins.A - elo.prob(elo.A, elo.B, adjust.B = adjust.B,
kadjust.A = ifelse(elo.A > 1500, adjust.A / 2, adjust.A)))
}<- function(elo.A, elo.B, adjust.A, adjust.B)
custom_prob
{1/(1 + 10^(((elo.B + adjust.B) - (elo.A + ifelse(elo.A > 1500, adjust.A / 2, adjust.A)))/400.0))
}<- elo.run(score(points.Home, points.Visitor) ~ adjust(team.Home, 10) + team.Visitor,
er2 data = tournament, k = 20, prob.fun = custom_prob, update.fun = custom_update)
## Using R instead of C++
final.elos(er2)
## Athletic Armadillos Blundering Baboons Cunning Cats
## 1564.660 1452.278 1518.165
## Defense-less Dogs Elegant Emus Fabulous Frogs
## 1420.876 1510.015 1533.209
## Gallivanting Gorillas Helpless Hyenas
## 1514.233 1486.564
Compare this to the results from the default:
<- elo.run(score(points.Home, points.Visitor) ~ adjust(team.Home, 10) + team.Visitor,
er3 data = tournament, k = 20)
final.elos(er3)
## Athletic Armadillos Blundering Baboons Cunning Cats
## 1563.967 1452.822 1517.847
## Defense-less Dogs Elegant Emus Fabulous Frogs
## 1421.358 1509.906 1533.127
## Gallivanting Gorillas Helpless Hyenas
## 1514.205 1486.768
This example is a bit contrived, as it’d be easier just to use adjust()
(actually, this is tested for in the tests), but the point remains.
Why would you want this? Consider fivethirtyeight’s NFL Elo model, which uses a custom Elo update.
Elo is great, but is it the best ranking/rating system? The third vignette discusses alternatives implemented in the elo
package.