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.
Load the required R packages that are needed for this analysis.
library(fitzRoy)
library(dplyr)
library(ggplot2)
library(lme4)
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()
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
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
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()
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)
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)
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
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()