What is an Elo Rating Model

An Elo Model is a model that creates a team rating number and then updates it weekly based upon their results. The formula for an Elo Model is:

Rating(New) = Rating(Old) + K(actual result – probability of winning)

Rating(Old): Elo Rating before match

Rating(New): New rating after match completed and formula applied

K: Factor which determines how quickly ratings move up and down after each game

Probability of winning = 1/(1 + 10 ^ (Opposition Rating – Own Rating /400))

Actual Result: 1 for a win, 0.5 for a draw and 0 for a loss

Creating an Elo Rating Model in R

The following steps are required to create an Elo Model

  1. Get results data frame using fitzRoy package

  2. Create Score column (1 for home team win, 0.5 for a draw, 0 for a home team loss)

  3. Filter for the games that you want to use for your elo rating model

  4. Create a function to find best values for k and initial elo score

  5. Create grid for k numbers and initial elo numbers you want to try

  6. Apply function created to find best k and initial elo numbers

  7. Apply these numbers to create elo model using the elo package.

From there we can create a data frame with each team’s final elo rating from the end of the season.

Then we can join our elo ratings to our initial results data frame to see the elo ratings for each team for the end of each round.

Below is the code to complete all of this

#### packages required
library(fitzRoy)
library(tidyverse)
## Warning: package 'ggplot2' was built under R version 4.2.3
## Warning: package 'tibble' was built under R version 4.2.3
## Warning: package 'tidyr' was built under R version 4.2.3
## Warning: package 'purrr' was built under R version 4.2.3
## Warning: package 'dplyr' was built under R version 4.2.3
## Warning: package 'stringr' was built under R version 4.2.3
## Warning: package 'lubridate' was built under R version 4.2.3
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ dplyr     1.1.3     ✔ readr     2.1.4
## ✔ forcats   1.0.0     ✔ stringr   1.5.0
## ✔ ggplot2   3.4.3     ✔ tibble    3.2.1
## ✔ lubridate 1.9.2     ✔ tidyr     1.3.0
## ✔ purrr     1.0.2     
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag()    masks stats::lag()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(elo)
## Warning: package 'elo' was built under R version 4.2.3
#### load in results data
elo <- fitzRoy::fetch_results_afltables(season = 2023)

#### Create score column
elo <- elo %>%
  mutate(Score = ifelse(Home.Points > Away.Points, 1,
                        ifelse(Home.Points < Away.Points, 0,
                               0.5)))

#### filter to only inclue regular season games
elo <- elo %>% 
  filter(Date < '2023-09-07')

#### Create function to get best parameters for k and initial elo number
elo_score <- function(initial_elos, k, data){
  
  # obtain elo ratings
  elo <- elo::elo.run(formula = Score ~ Home.Team + Away.Team,
                      initial_elos = initial_elos,
                      k = k,
                      data = data) %>%
    as.data.frame()
  
  data <- data %>% 
    mutate(p.A = elo$p.A) %>% 
    mutate(pred = ifelse(p.A > .5, 1, 0))
  
  cm <- caret::confusionMatrix(data = factor(data$pred, levels = c(0,0.5,1)),
                               reference = factor(data$Score, levels = c(0, 0.5,1)))
  
  return(cm$overall["Accuracy"])
  
}

# Create a grid
params <- expand.grid(init = seq(1000, 3000, by = 50),
                      kfac = seq(10, 50, by = 5))

# Apply function
params$accuracy <- mapply(elo_score, params$init, params$kfac, MoreArgs = list(data = elo))

# What was the best combination?
accuracy <- subset(params, accuracy == max(params$accuracy))

#### Create elo model using best parameters
eloratings <- elo::elo.run(formula = Score ~ Home.Team + Away.Team,
                           data = elo,
                           initial.elos = 2000,
                           k = 10,
                           history = T)

#### Create data frame with each team's final elo rating
FinalElos <- final.elos(eloratings) %>% 
  as.data.frame() %>% 
  rownames_to_column()

FinalElos <- FinalElos %>% 
  rename('Team' = 'rowname',
         'EloScore' = '.')

#### turn model to a data frame
eloratings <- eloratings %>% 
  as.data.frame()

#### Join results and elo ratings
elo <- left_join(elo, eloratings, by = c('Home.Team' = 'team.A', 'Away.Team' = 'team.B'))
## Warning in left_join(elo, eloratings, by = c(Home.Team = "team.A", Away.Team = "team.B")): Detected an unexpected many-to-many relationship between `x` and `y`.
## ℹ Row 39 of `x` matches multiple rows in `y`.
## ℹ Row 39 of `y` matches multiple rows in `x`.
## ℹ If a many-to-many relationship is expected, set `relationship =
##   "many-to-many"` to silence this warning.
#### only keep columns of interest
elo <- elo %>% 
  select('Season', 'Round', 'Home.Team', 'Home.Points', 'Away.Team', 'Away.Points', 'Score', 'elo.A', 'elo.B') %>% 
  rename(
    Season = Season,
    Round = Round,
    Home.Team = Home.Team,
    Home.Points = Home.Points,
    Away.Team = Away.Team,
    Away.Points = Away.Points,
    Score = Score,
    HomeEndofRoundElo = elo.A,
    AwayEndofRoundElo = elo.B
  )

Conclusion

We have created an Elo Rating Model for a season of AFL. You could create an Elo Rating Model for any sport where you have results from that sport in a sequential order. Once created you could do many things, such as visualise a team’s elo rating across a season, or even compare two teams