Why Create An AFL ELO Rating System?

The AFL is a popular sporting league which many people take interest in viewing. As is the case with most sporting events, many people like to make tips or predictions on outcomes, either for fun or for betting purposes. A simple ELO model such as the one that will be provided in this document can make basic predictions about match outcomes.

Load Libraries

library(tidyverse)
library(elo)
library(fitzRoy)

The data we will use to create our ELO model will come from AFL Tables through the fitzRoy package.

Load Data

results <- fitzRoy::fetch_results_afltables(2024)

Now we will set some parameters that will help to determine each teams rating for each game. A home ground advantage parameter will be set to give the home team a slight advantage, and a k value parameter will be set to determine the size of each games effect on a teams ELO rating. There are many more parameters you could attempt to use, but these are just some with the K value being the only required one.

Set Parameters

# Set parameters
HGA <- 15 # home ground advantage
k_val <- 25 

We will need to create a Score variable which determines the winner and loser of each match, as ELO only accepts arguments on a 0 to 1 scale. It is also possible to scale margin from 0 to 1 to get more informative results, however in this example we wil just look at wins and losses.

results$Score <- ifelse(results$Margin > 0, 1,
                         ifelse(results$Margin < 0, 0, 0.5))

Next, we will create our ELO rating system based simply on wins and losses of each team, along with whether or not they have home ground advantage. The initial ELO rating for each side will be set to 1500.

Creating the ELO

elo.data <- elo.run(Score ~
      adjust(Home.Team, HGA) +
      Away.Team +
      group(Round.Number),
      initial.elos = 1500,
      k = k_val,
      data = results
  ) |>
  as.data.frame()

With our ELO model created, we now need to clean the data set to graph each teams ELO ratings over time.

# adding a match id to ensure that games stay in correct order

elo_df <- as.data.frame(elo.data) |>
    select(team.A, team.B, elo.A, elo.B) |>
    mutate(MatchID = row_number())

# grouping by home and away teams

# home team df
  
  elo_df_home <- elo_df |>
    rename(Team = team.A,
           Elo = elo.A) |>
    group_by(Team) |>
    select(Team, Elo, MatchID) 
  
# away team df
  
  elo_df_away <- elo_df |>
    rename(Team = team.B,
           Elo = elo.B) |>
    group_by(Team) |>
    select(Team, Elo, MatchID) 
  
  # bind dfs together
  
  elo_df_full <- bind_rows(elo_df_home, elo_df_away)
  
  # group by team and mutate a week variable - matchID is no longer required after this step
  
  elo_df_full <- elo_df_full |>
    group_by(Team) |>
    mutate(Week = row_number()) |>
    select(-MatchID)

We are now ready to plot our graph.

ELO Ratings Graph Over Time

elo_df_full |>
    ggplot(aes(Week, Elo, col = Team)) +
    geom_line()

If you would like to look at certain matchups more clearly, this can be done using the filter function.

Looking At Individual Matchups Example

elo_df_full |>
  filter(Team %in% c("Essendon", "Collingwood")) |>
    ggplot(aes(Week, Elo, col = Team)) +
    geom_line()