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)
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) # on the fly elo.run(score(points.Home, points.Visitor) ~ team.Home + team.Visitor, data = tournament, k = 20)
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)
You can also adjust the home and visitor teams with different k's (but note that this no longer conserves total Elo score!):
k1 <- 20*log(abs(tournament$points.Home - tournament$points.Visitor) + 1) elo.run(score(points.Home, points.Visitor) ~ team.Home + team.Visitor + k(k1, k1/2), data = tournament)
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)
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)
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.
tournament$elo.Visitor <- 1500 elo.run(score(points.Home, points.Visitor) ~ team.Home + elo.Visitor + regress(half, 1500, 0.2), data = tournament, k = 20)
The special function group()
tells elo.run()
when to update Elos. It also determines matches
to group together in as.matrix()
.
er <- elo.run(score(points.Home, points.Visitor) ~ team.Home + team.Visitor + group(week), data = tournament, k = 20) as.matrix(er)
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
d <- data.frame( team1 = c("Part 2", "Part 2", "Part 1"), team2 = c("Part 1", "Part 3", "Part 3"), won = 1 ) d
We would want to consider all three of these matches at the same time, so we add a grouping variable and run elo.run()
:
d$group <- 1 final.elos(elo.run(won ~ team1 + team2 + group(group), data = d, k = 20))
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).
d2 <- data.frame( first = "Part 2", second = "Part 1", third = "Part 3" ) final.elos(elo.run.multiteam(~ multiteam(first, second, third), k = 20, data = d2))
A larger example shows the utility of this function:
data("tournament.multiteam") str(tournament.multiteam) erm <- elo.run.multiteam(~ multiteam(Place_1, Place_2, Place_3, Place_4), data = tournament.multiteam, k = 20) final.elos(erm)
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) rank.teams(e)
as.matrix.elo.run()
creates a matrix of running Elos.
head(as.matrix(e))
as.data.frame.elo.run()
gives the long version (perfect, for, e.g., ggplot2
).
str(as.data.frame(e))
Finally, final.elos()
will extract the final Elos per team.
final.elos(e)
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)
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:
custom_update <- function(wins.A, elo.A, elo.B, k, adjust.A, adjust.B, ...) { k*(wins.A - elo.prob(elo.A, elo.B, adjust.B = adjust.B, adjust.A = ifelse(elo.A > 1500, adjust.A / 2, adjust.A))) } custom_prob <- function(elo.A, elo.B, adjust.A, adjust.B) { 1/(1 + 10^(((elo.B + adjust.B) - (elo.A + ifelse(elo.A > 1500, adjust.A / 2, adjust.A)))/400.0)) } er2 <- elo.run(score(points.Home, points.Visitor) ~ adjust(team.Home, 10) + team.Visitor, data = tournament, k = 20, prob.fun = custom_prob, update.fun = custom_update) final.elos(er2)
Compare this to the results from the default:
er3 <- elo.run(score(points.Home, points.Visitor) ~ adjust(team.Home, 10) + team.Visitor, data = tournament, k = 20) final.elos(er3)
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.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.