High-speed running (HSR) is one example of a performance variable that can be tracked across repeated AFL matches.
In this tutorial, we use a simulated AFL dataset to demonstrate how longitudinal player data can be analysed using a multilevel model in R.
The example focuses on two playing-role groups:
The purpose is not to analyse real AFL players. Instead, the simulated dataset provides a simple and reproducible example for learning how repeated performance measurements can be analysed.
Learning goal
By the end of this tutorial, you
will understand how a multilevel longitudinal model can account for
repeated observations from the same player.
How does simulated high-speed running performance change across AFL matches, and does the performance trajectory differ between playing roles?
The dataset contains repeated observations for 60 simulated AFL players across 10 matches.
Each row represents one player in one match.
| Variable | Description |
|---|---|
Player_ID |
Unique player identifier |
Match |
Match number |
Match_c |
Match number centred at the first match |
Position_Group |
Midfield or Key Position |
HSR_m_min |
High-speed running distance per minute |
The dataset contains 600 observations:
60 players × 10 matches = 600 observations
set.seed(2026)
players <- tibble(
Player_ID = paste0("P", sprintf("%02d", 1:60)),
Position_Group = rep(
c("Midfield", "Key Position"),
times = c(40, 20)
)
)
afl <- expand_grid(
Player_ID = players$Player_ID,
Match = 1:10
) %>%
left_join(players, by = "Player_ID") %>%
mutate(
Match_c = Match - 1
)
player_effects <- tibble(
Player_ID = players$Player_ID,
random_intercept = rnorm(
60,
mean = 0,
sd = 0.14
),
random_slope = rnorm(
60,
mean = 0,
sd = 0.015
)
)
afl <- afl %>%
left_join(player_effects, by = "Player_ID")
afl <- afl %>%
mutate(
position_effect = if_else(
Position_Group == "Midfield",
0.15,
-0.15
),
time_effect = -0.018 * Match_c,
position_time_effect = if_else(
Position_Group == "Midfield",
-0.010 * Match_c,
0.010 * Match_c
),
HSR_m_min =
1.45 +
position_effect +
time_effect +
position_time_effect +
random_intercept +
random_slope * Match_c +
rnorm(
n(),
mean = 0,
sd = 0.10
)
)
afl <- afl %>%
select(
Player_ID,
Match,
Match_c,
Position_Group,
HSR_m_min
)
Before fitting a statistical model, it is useful to examine how HSR performance varies across matches and playing roles.
afl_summary <- afl %>%
group_by(Match, Position_Group) %>%
summarise(
mean_HSR = mean(HSR_m_min),
se = sd(HSR_m_min) / sqrt(n()),
.groups = "drop"
)
ggplot(
afl_summary,
aes(
x = Match,
y = mean_HSR,
colour = Position_Group,
group = Position_Group
)
) +
geom_ribbon(
aes(
ymin = mean_HSR - 1.96 * se,
ymax = mean_HSR + 1.96 * se,
fill = Position_Group
),
alpha = 0.15,
colour = NA
) +
geom_line(linewidth = 1.2) +
geom_point(size = 2.5) +
scale_colour_manual(
values = c(
"Midfield" = "#2F75B5",
"Key Position" = "#E67E22"
)
) +
scale_fill_manual(
values = c(
"Midfield" = "#2F75B5",
"Key Position" = "#E67E22"
)
) +
labs(
title = "Simulated HSR Performance Across Matches",
x = "Match",
y = "HSR (m/min)",
colour = "Playing role",
fill = "Playing role"
) +
theme_minimal(base_size = 12) +
theme(
plot.title = element_text(
face = "bold",
size = 15
),
legend.position = "top"
)
Figure 1. Mean simulated HSR performance across matches by playing role. Shaded areas represent approximate 95% confidence intervals for the group means.
What can we see?
The plot provides an initial
view of differences between playing roles and changes in HSR performance
across matches. These patterns will be examined more formally using a
multilevel longitudinal model in the following sections.
The data contain repeated measurements from the same players across multiple matches. This means we can examine both differences between players and changes within each player over time.
The individual trajectories below help us understand this longitudinal structure before fitting a multilevel model.
Each line represents one simulated player across 10 matches. Players can start at different performance levels and can also change at different rates across matches.
ggplot(
afl,
aes(
x = Match,
y = HSR_m_min,
group = Player_ID,
colour = Position_Group
)
) +
geom_line(
alpha = 0.35,
linewidth = 0.7
) +
geom_smooth(
aes(group = Position_Group),
method = "lm",
se = TRUE,
linewidth = 1.3
) +
scale_colour_manual(
values = c(
"Midfield" = "#2F75B5",
"Key Position" = "#E67E22"
)
) +
labs(
title = "Individual Player HSR Trajectories",
subtitle = "Each line represents one simulated player",
x = "Match",
y = "HSR (m/min)",
colour = "Playing role"
) +
theme_minimal(base_size = 12) +
theme(
plot.title = element_text(
face = "bold",
size = 15
),
plot.subtitle = element_text(
colour = "#666666"
),
legend.position = "top"
)
Figure 2. Individual simulated player trajectories across 10 matches. Thin lines show individual players, while the coloured lines show the overall trajectory for each playing-role group.
The observations are not independent because multiple measurements come from the same player.
For example, Player P01 has 10 observations:
Player P01 → Match 1, Match 2, Match 3, … Match 10
The observations from the same player are therefore clustered within that player.
Longitudinal data structure
Player
→
Match 1
Match 2
Match 3
…
Match 10
Repeated match observations are nested within each player.
Key idea
Players can differ in their baseline
HSR performance and in how their performance changes across matches. A
multilevel model can account for this player-level variation while
estimating the overall effects of match and playing role.
This provides the basis for building the multilevel models in Page 3.
The exploratory analysis showed that repeated observations are clustered within players. We can now build a multilevel model to account for this structure and examine changes in HSR performance across matches.
The first model estimates the overall average HSR performance while allowing each player to have their own baseline level.
This model separates variation between players from variation within players.
model_1 <- lmer(
HSR_m_min ~ 1 + (1 | Player_ID),
data = afl
)
summary(model_1)
## Linear mixed model fit by REML ['lmerMod']
## Formula: HSR_m_min ~ 1 + (1 | Player_ID)
## Data: afl
##
## REML criterion at convergence: -505.5
##
## Scaled residuals:
## Min 1Q Median 3Q Max
## -3.06399 -0.63696 -0.04616 0.60569 2.86579
##
## Random effects:
## Groups Name Variance Std.Dev.
## Player_ID (Intercept) 0.03762 0.1939
## Residual 0.01842 0.1357
## Number of obs: 600, groups: Player_ID, 60
##
## Fixed effects:
## Estimate Std. Error t value
## (Intercept) 1.38750 0.02564 54.11
Next, match number is added to examine whether HSR performance changes across matches.
A random slope allows each player to have their own rate of change.
model_2 <- lmer(
HSR_m_min ~ Match_c + (Match_c | Player_ID),
data = afl
)
summary(model_2)
## Linear mixed model fit by REML ['lmerMod']
## Formula: HSR_m_min ~ Match_c + (Match_c | Player_ID)
## Data: afl
##
## REML criterion at convergence: -765.8
##
## Scaled residuals:
## Min 1Q Median 3Q Max
## -2.74010 -0.66047 -0.03749 0.59168 2.75618
##
## Random effects:
## Groups Name Variance Std.Dev. Corr
## Player_ID (Intercept) 0.0454335 0.21315
## Match_c 0.0003976 0.01994 -0.39
## Residual 0.0094793 0.09736
## Number of obs: 600, groups: Player_ID, 60
##
## Fixed effects:
## Estimate Std. Error t value
## (Intercept) 1.496474 0.028492 52.522
## Match_c -0.024215 0.002923 -8.286
##
## Correlation of Fixed Effects:
## (Intr)
## Match_c -0.437
Playing role is then added as a fixed effect. This allows the model to estimate the average difference in HSR performance between Midfield and Key Position players.
model_3 <- lmer(
HSR_m_min ~ Match_c + Position_Group + (Match_c | Player_ID),
data = afl
)
summary(model_3)
## Linear mixed model fit by REML ['lmerMod']
## Formula: HSR_m_min ~ Match_c + Position_Group + (Match_c | Player_ID)
## Data: afl
##
## REML criterion at convergence: -796.5
##
## Scaled residuals:
## Min 1Q Median 3Q Max
## -2.74768 -0.65429 -0.01373 0.60967 2.68806
##
## Random effects:
## Groups Name Variance Std.Dev. Corr
## Player_ID (Intercept) 0.0200758 0.14169
## Match_c 0.0003976 0.01994 -0.11
## Residual 0.0094792 0.09736
## Number of obs: 600, groups: Player_ID, 60
##
## Fixed effects:
## Estimate Std. Error t value
## (Intercept) 1.291424 0.033524 38.522
## Match_c -0.024215 0.002923 -8.285
## Position_GroupMidfield 0.307576 0.040658 7.565
##
## Correlation of Fixed Effects:
## (Intr) Mtch_c
## Match_c -0.139
## Pstn_GrpMdf -0.809 0.000
The final model includes an interaction between match and playing role.
This tests whether the change in HSR performance across matches differs between the two playing-role groups.
model_4 <- lmer(
HSR_m_min ~ Match_c * Position_Group + (Match_c | Player_ID),
data = afl
)
summary(model_4)
## Linear mixed model fit by REML ['lmerMod']
## Formula: HSR_m_min ~ Match_c * Position_Group + (Match_c | Player_ID)
## Data: afl
##
## REML criterion at convergence: -798.9
##
## Scaled residuals:
## Min 1Q Median 3Q Max
## -2.73850 -0.65766 -0.02019 0.60995 2.63769
##
## Random effects:
## Groups Name Variance Std.Dev. Corr
## Player_ID (Intercept) 0.0198731 0.14097
## Match_c 0.0003182 0.01784 -0.07
## Residual 0.0094792 0.09736
## Number of obs: 600, groups: Player_ID, 60
##
## Fixed effects:
## Estimate Std. Error t value
## (Intercept) 1.270546 0.034020 37.347
## Match_c -0.011154 0.004653 -2.397
## Position_GroupMidfield 0.338893 0.041666 8.134
## Match_c:Position_GroupMidfield -0.019592 0.005699 -3.438
##
## Correlation of Fixed Effects:
## (Intr) Mtch_c Pst_GM
## Match_c -0.219
## Pstn_GrpMdf -0.816 0.179
## Mtch_c:P_GM 0.179 -0.816 -0.219
The models can be compared using AIC. Lower AIC values indicate a better balance between model fit and model complexity.
AIC(
model_1,
model_2,
model_3,
model_4
)
## df AIC
## model_1 3 -499.4887
## model_2 6 -753.7522
## model_3 7 -782.4968
## model_4 8 -782.9325
The final model estimates the effects of match, playing role, and their interaction while allowing players to differ in both their baseline performance and their rate of change.
The key term is the Match × Position interaction. This term represents whether the performance trajectory across matches differs between Midfield and Key Position players.
fixef(model_4)
## (Intercept) Match_c
## 1.27054557 -0.01115386
## Position_GroupMidfield Match_c:Position_GroupMidfield
## 0.33889284 -0.01959238
The final model can be used to visualise the estimated HSR trajectories for the two playing-role groups.
prediction_data <- expand_grid(
Match = 1:10,
Position_Group = c(
"Midfield",
"Key Position"
)
)
prediction_data <- prediction_data %>%
mutate(
Match_c = Match - 1
)
prediction_data$Predicted_HSR <- predict(
model_4,
newdata = prediction_data,
re.form = NA
)
ggplot(
prediction_data,
aes(
x = Match,
y = Predicted_HSR,
colour = Position_Group,
group = Position_Group
)
) +
geom_line(
linewidth = 1.4
) +
geom_point(
size = 2.5
) +
scale_colour_manual(
values = c(
"Midfield" = "#2F75B5",
"Key Position" = "#E67E22"
)
) +
labs(
title = "Predicted HSR Performance Across Matches",
subtitle = "Predictions from the final multilevel model",
x = "Match",
y = "Predicted HSR (m/min)",
colour = "Playing role"
) +
theme_minimal(base_size = 12) +
theme(
plot.title = element_text(
face = "bold",
size = 15
),
plot.subtitle = element_text(
colour = "#666666"
),
legend.position = "top"
)
Figure 3. Predicted HSR trajectories across matches from the final multilevel model. The model accounts for repeated observations within players and estimates different trajectories for the two playing-role groups.
The analysis followed a simple progression:
The final model estimates a higher HSR trajectory for Midfield players than Key Position players. The two groups also show different rates of change across matches, which is represented by the interaction between Match and Position_Group.