Details

This analysis explores how AFL match margins vary across teams and seasons, and whether multilevel modelling can account for these differences. Using AFL match data from the 2022 to 2026 seasons retrieved through the fitzroy R package, two statistical models were developed and compared. The analysis examines season effects, team-level random effects, and model fit using the Akaike Information Criterion (AIC). This resource provides step-by-step R code to replicate the data preparation, modelling, comparison and visualisation processes.


1.0 Data Preparation

1.1 Load Required Packages

Load the required R packages that are needed for this analysis.

library(fitzRoy)
library(dplyr)
library(ggplot2)
library(lme4)

1.2 Import AFL Match Data

Create a dataset with AFL match data from the fitzroy R package. Forming a function allows the specific data to be taken straight out of the package, rather than filtering afterwards.

seasons <- 2022:2026

afl_data <- lapply(seasons, function(year) {
  fitzRoy::fetch_results(
    season = year,
    comp = "AFLM",
    source = "afltables")}) |>
  bind_rows()

1.3 Inspect the Data

Before analysing, it is important to understand the structure of the data and identify the variables that will be needed for this analysis.

names(afl_data)
##  [1] "Game"         "Date"         "Round"        "Home.Team"    "Home.Goals"  
##  [6] "Home.Behinds" "Home.Points"  "Away.Team"    "Away.Goals"   "Away.Behinds"
## [11] "Away.Points"  "Venue"        "Margin"       "Season"       "Round.Type"  
## [16] "Round.Number"
head(afl_data)
## # A tibble: 6 × 16
##    Game Date       Round Home.Team Home.Goals Home.Behinds Home.Points Away.Team
##   <dbl> <date>     <chr> <chr>          <int>        <int>       <int> <chr>    
## 1 15984 2022-03-16 R1    Melbourne         14           13          97 Footscray
## 2 15985 2022-03-17 R1    Carlton           14           17         101 Richmond 
## 3 15986 2022-03-18 R1    St Kilda          12           13          85 Collingw…
## 4 15987 2022-03-19 R1    Geelong           20           18         138 Essendon 
## 5 15988 2022-03-19 R1    GWS               13           14          92 Sydney   
## 6 15989 2022-03-19 R1    Brisbane…         11           14          80 Port Ade…
## # ℹ 8 more variables: Away.Goals <int>, Away.Behinds <int>, Away.Points <int>,
## #   Venue <chr>, Margin <int>, Season <dbl>, Round.Type <chr>,
## #   Round.Number <int>
summary(afl_data)
##       Game            Date               Round            Home.Team        
##  Min.   :15984   Min.   :2022-03-16   Length:1073        Length:1073       
##  1st Qu.:16252   1st Qu.:2023-04-30   Class :character   Class :character  
##  Median :16520   Median :2024-06-10   Mode  :character   Mode  :character  
##  Mean   :16520   Mean   :2024-06-15                                        
##  3rd Qu.:16788   3rd Qu.:2025-07-26                                        
##  Max.   :17056   Max.   :2026-09-26                                        
##                                                                            
##    Home.Goals    Home.Behinds    Home.Points      Away.Team        
##  Min.   : 3.0   Min.   : 2.00   Min.   : 31.00   Length:1073       
##  1st Qu.:10.0   1st Qu.: 9.00   1st Qu.: 71.00   Class :character  
##  Median :12.0   Median :11.00   Median : 86.00   Mode  :character  
##  Mean   :12.8   Mean   :11.34   Mean   : 88.15                     
##  3rd Qu.:16.0   3rd Qu.:14.00   3rd Qu.:105.00                     
##  Max.   :31.0   Max.   :25.00   Max.   :205.00                     
##                                                                    
##    Away.Goals     Away.Behinds    Away.Points        Venue          
##  Min.   : 2.00   Min.   : 2.00   Min.   : 23.00   Length:1073       
##  1st Qu.: 9.00   1st Qu.: 8.00   1st Qu.: 65.00   Class :character  
##  Median :12.00   Median :10.00   Median : 80.00   Mode  :character  
##  Mean   :11.88   Mean   :10.47   Mean   : 81.74                     
##  3rd Qu.:14.00   3rd Qu.:13.00   3rd Qu.: 97.00                     
##  Max.   :25.00   Max.   :22.00   Max.   :165.00                     
##                                                                     
##      Margin             Season      Round.Type         Round.Number  
##  Min.   :-128.000   Min.   :2022   Length:1073        Min.   : 1.00  
##  1st Qu.: -19.000   1st Qu.:2023   Class :character   1st Qu.: 7.00  
##  Median :   6.000   Median :2024   Mode  :character   Median :13.00  
##  Mean   :   6.412   Mean   :2024                      Mean   :13.48  
##  3rd Qu.:  31.000   3rd Qu.:2025                      3rd Qu.:20.00  
##  Max.   : 171.000   Max.   :2026                      Max.   :29.00  
##                                                       NA's   :2

1.4 Check for Missing Values

Missing values can affect any calculations and predictions on future results, especially for variables that are needed.

colSums(is.na(afl_data))
##         Game         Date        Round    Home.Team   Home.Goals Home.Behinds 
##            0            0            0            0            0            0 
##  Home.Points    Away.Team   Away.Goals Away.Behinds  Away.Points        Venue 
##            0            0            0            0            0            0 
##       Margin       Season   Round.Type Round.Number 
##            0            0            0            2

2.0 Visualise Match Margin Distribution

Creating a histogram assists in displaying the distribution of AFL match margins before fitting the models. This also provides a summary of the margin range and frequency across the distribution, in turn, providing context for evaulation.

ggplot(afl_data, aes(x = Margin)) +
  geom_histogram(bins = 30, colour = "black", fill = "steelblue") +
  labs(
    title = "Distribution of AFL Match Margins",
    subtitle = "AFL matches from 2022 to 2026",
    x = "Home-team margin (points)",
    y = "Number of matches") +
  theme_minimal()

3.0 Standard Linear Regression Model

A standard linear regression model is fitted using match margin as the outcome variable and season as the explanatory variable.

Model1 <- lm(
  Margin ~ Season,
  data = afl_data)

4.0 Multilevel Models

Multilevel models are fitted using the lmer() function from the lme4 package, providing the creation of separate random intercepts for home and away teams. This is useful when considering that teams can play each other multiple times.

Model2 <- lmer(
  Margin ~ Season +
    (1 | Home.Team) +
    (1 | Away.Team),
  data = afl_data,
  REML = TRUE)

Model3 <- lmer(
  Margin ~ Season +
    (1 + Season | Home.Team) +
    (1 | Away.Team),
  data = afl_data,
  REML = TRUE)

5.0 Model Comparison

The standard linear regression and multilevel model is compared using the Akaike Information Criterion (AIC), with a lower score reflecting a better balance between fit and complexity. This comparison assists in determining whether random effects provide sufficient improvement to warrant additional model complexity.

Model1_ML <- lm(
  Margin ~ Season,
  data = afl_data)

Model2_ML <- lmer(
  Margin ~ Season +
    (1 | Home.Team) +
    (1 | Away.Team),
  data = afl_data,
  REML = FALSE)

Model3_ML <- lmer(
  Margin ~ Season +
    (1 + Season | Home.Team) +
    (1 | Away.Team),
  data = afl_data,
  REML = FALSE)

AIC(Model1_ML, Model2_ML, Model3_ML)
##           df      AIC
## Model1_ML  3 10980.30
## Model2_ML  5 10694.87
## Model3_ML  7 10705.43

6.0 Visualisation

The random effects estimated by the multilevel model are extracted and displayed using point plots. These visualisations show how the model adjusts the estimated match margin for individual teams after accounting for the other model terms.

home_effects <- ranef(Model2)$Home.Team |>
  tibble::rownames_to_column("Team") |>
  rename(Home_Effect = `(Intercept)`) |>
  arrange(Home_Effect)

away_effects <- ranef(Model2)$Away.Team |>
  tibble::rownames_to_column("Team") |>
  rename(Away_Effect = `(Intercept)`) |>
  arrange(Away_Effect)

# plot home team random effects
ggplot(home_effects, aes(x = reorder(Team, Home_Effect), y = Home_Effect)) +
  geom_point() +
  coord_flip() +
  labs(
    title = "Estimated Home-Team Effects",
    x = "Team",
    y = "Estimated contribution to home margin (points)") +
  theme_minimal()

# plot away team random effects
ggplot(away_effects, aes(x = reorder(Team, Away_Effect), y = Away_Effect)) +
  geom_point() +
  coord_flip() +
  labs(
    title = "Estimated Away-Team Effects",
    x = "Team",
    y = "Estimated contribution to home margin (points)") +
  theme_minimal()