The aim of this project was to find out at what age AFL players generally perform best. Once again the Wheel-o player ratings were used to determine performance. The data was used from Wheel-o ratings and was 2012-2026. Peak age will be separated by and compared between positions.
This analysis uses AFL player statistics from 2012 to 2026.
The files should be saved in the same folder as this R Markdown file.
The files are:
afl-player-stats-2012.csvafl-player-stats-2013.csvafl-player-stats-2014.csvafl-player-stats-2015.csvafl-player-stats-2016.csvafl-player-stats-2017.csvafl-player-stats-2018.csvafl-player-stats-2019.csvafl-player-stats-2020.csvafl-player-stats-2021.csvafl-player-stats-2022.csvafl-player-stats-2023.csvafl-player-stats-2024.csvafl-player-stats-2025.csvafl-player-stats-2026.csvData source: https://www.wheeloratings.com/afl_stats.html?comp=afl&season=2026
The tidyverse package is used.
# load library
library(tidyverse)
Each season is stored in a separate CSV file.
# import data
afl12 <- read.csv("afl-player-stats-2012.csv")
afl13 <- read.csv("afl-player-stats-2013.csv")
afl14 <- read.csv("afl-player-stats-2014.csv")
afl15 <- read.csv("afl-player-stats-2015.csv")
afl16 <- read.csv("afl-player-stats-2016.csv")
afl17 <- read.csv("afl-player-stats-2017.csv")
afl18 <- read.csv("afl-player-stats-2018.csv")
afl19 <- read.csv("afl-player-stats-2019.csv")
afl20 <- read.csv("afl-player-stats-2020.csv")
afl21 <- read.csv("afl-player-stats-2021.csv")
afl22 <- read.csv("afl-player-stats-2022.csv")
afl23 <- read.csv("afl-player-stats-2023.csv")
afl24 <- read.csv("afl-player-stats-2024.csv")
afl25 <- read.csv("afl-player-stats-2025.csv")
afl26 <- read.csv("afl-player-stats-2026.csv")
A season variable is added before combining the data.
# add season
afl12$Season <- 2012
afl13$Season <- 2013
afl14$Season <- 2014
afl15$Season <- 2015
afl16$Season <- 2016
afl17$Season <- 2017
afl18$Season <- 2018
afl19$Season <- 2019
afl20$Season <- 2020
afl21$Season <- 2021
afl22$Season <- 2022
afl23$Season <- 2023
afl24$Season <- 2024
afl25$Season <- 2025
afl26$Season <- 2026
The seasons are then combined into one dataset.
# combine data
afl <- bind_rows(
afl12,
afl13,
afl14,
afl15,
afl16,
afl17,
afl18,
afl19,
afl20,
afl21,
afl22,
afl23,
afl24,
afl25,
afl26
)
Some basic checks are done before the analysis.
# inspect data
head(afl)
## Player Team Age Age_Decimal Matches RatingPoints_Avg
## 1 Aaron Black North Melbourne 22 22.08 3 7.233
## 2 Aaron Cornelius Brisbane 22 22.59 8 7.312
## 3 Aaron Davey Melbourne 29 29.55 8 11.300
## 4 Aaron Edwards North Melbourne 28 28.83 9 6.711
## 5 Aaron Hall Gold Coast 22 22.14 8 8.787
## 6 Aaron Joseph Carlton 23 23.49 13 5.338
## Supercoach_Avg DreamTeamPoints_Avg CoachesVotes_Total CoachesVotes_Avg
## 1 49.667 39.000 0 0.000
## 2 47.875 53.000 5 0.625
## 3 62.625 53.875 0 0.000
## 4 68.667 62.333 3 0.333
## 5 60.250 62.250 0 0.000
## 6 50.077 48.692 1 0.077
## CoachesVotes_MatchesPolled BnFVotes_Total TimeOnGround Kicks Handballs
## 1 0 0 72.667 3.333 4.333
## 2 1 0 79.750 6.250 3.500
## 3 0 0 79.250 6.500 4.125
## 4 1 0 85.444 6.889 3.889
## 5 0 1 72.500 8.125 4.500
## 6 1 0 76.846 7.538 4.769
## Disposals DisposalEfficiency KickingEfficiency HandballEfficiency
## 1 7.667 69.565 80.000 61.538
## 2 9.750 61.538 64.000 57.143
## 3 10.625 75.294 75.000 75.758
## 4 10.778 60.825 59.677 62.857
## 5 12.625 69.307 61.538 83.333
## 6 12.308 73.125 67.347 82.258
## KickPercentage Inside50s Rebound50s Clangers DisposalsPerClanger Turnovers
## 1 43.478 0.667 0.000 0.667 11.500 1.000
## 2 64.103 1.750 0.000 2.250 4.333 2.750
## 3 61.176 1.875 0.750 1.750 6.071 1.875
## 4 63.918 1.222 0.000 1.889 5.706 2.667
## 5 64.356 2.500 0.500 1.250 10.100 2.625
## 6 61.250 1.231 1.462 1.846 6.667 2.000
## DisposalsPerTurnover MetresGained MetresGainedPerDisposal
## 1 7.667 103.000 13.435
## 2 3.545 149.125 15.295
## 3 5.667 155.250 14.612
## 4 4.042 165.222 15.330
## 5 4.810 252.125 19.970
## 6 6.154 150.385 12.219
## ContestedPossessions UncontestedPossessions TotalPossessions
## 1 4.667 3.000 7.667
## 2 4.125 5.125 9.250
## 3 6.375 4.625 11.000
## 4 5.000 5.667 10.667
## 5 3.000 9.625 12.625
## 6 4.615 7.308 11.923
## ContestedPossessionRate Intercepts GroundBallGets GroundBallGetsForward50
## 1 60.870 0.333 4.000 2.333
## 2 44.595 0.500 2.375 1.250
## 3 57.955 1.875 5.125 1.750
## 4 46.875 0.556 3.111 1.667
## 5 23.762 1.500 2.250 0.625
## 6 38.710 4.231 4.000 0.077
## CentreBounceAttendances CentreClearances StoppageClearances TotalClearances
## 1 0.000 0.000 0.000 0.000
## 2 0.000 0.000 0.000 0.000
## 3 0.250 0.250 0.625 0.875
## 4 0.000 0.111 0.333 0.444
## 5 0.125 0.000 0.750 0.750
## 6 1.385 0.077 0.231 0.308
## Marks ContestedMarks MarksInside50 InterceptMarks MarksOnLead FreesFor
## 1 2.667 0.667 1.000 0.000 1.333 0.000
## 2 4.125 1.250 2.125 0.125 1.000 0.500
## 3 1.625 0.000 0.250 0.000 0.250 0.875
## 4 5.000 1.000 2.667 0.000 2.333 0.889
## 5 3.000 0.125 0.750 0.250 0.625 0.250
## 6 3.308 0.154 0.077 0.538 0.000 0.462
## FreesAgainst FreesDiff RuckContests RuckContestPercentage Hitouts
## 1 0.667 -0.667 0.00 0.000 0.00
## 2 0.750 -0.250 0.75 0.755 0.25
## 3 1.250 -0.375 0.00 0.000 0.00
## 4 0.667 0.222 0.00 0.000 0.00
## 5 0.500 -0.250 0.00 0.000 0.00
## 6 1.077 -0.615 0.00 0.000 0.00
## HitoutsWinPercentage HitoutsToAdvantage HitoutsToAdvantagePercentage Tackles
## 1 NA 0 NA 2.000
## 2 33.333 0 0 1.500
## 3 NA 0 NA 4.625
## 4 NA 0 NA 2.000
## 5 NA 0 NA 3.625
## 6 NA 0 NA 2.231
## TacklesInside50 ContestDefensiveOneOnOnes ContestDefensiveLossPercentage
## 1 1.333 0.000 NA
## 2 0.875 0.125 0
## 3 1.500 0.000 NA
## 4 0.667 0.111 100
## 5 0.750 0.000 NA
## 6 0.077 0.769 20
## ContestOffensiveOneOnOnes ContestOffensiveWinPercentage Goals_Total Goals_Avg
## 1 1.000 33.333 3 1.000
## 2 1.875 26.667 13 1.625
## 3 0.500 25.000 7 0.875
## 4 1.778 31.250 17 1.889
## 5 0.125 0.000 8 1.000
## 6 0.000 NA 1 0.077
## Behinds ShotsAtGoal GoalAssists ScoreboardImpact GoalAccuracy
## 1 0.333 1.667 0.667 10.333 60.000
## 2 0.625 2.375 0.375 12.625 68.421
## 3 0.375 1.375 0.375 7.875 63.636
## 4 0.667 3.111 0.778 16.667 60.714
## 5 0.625 2.000 0.375 8.875 50.000
## 6 0.000 0.077 0.077 0.923 100.000
## ScoreInvolvements ScoreInvolvementPercentage ScoreLaunches KickIns
## 1 3.333 13.699 0.333 0.000
## 2 3.875 19.018 0.250 0.000
## 3 4.125 19.760 0.875 0.125
## 4 5.444 19.291 0.333 0.000
## 5 3.750 19.481 0.625 0.000
## 6 2.308 9.585 0.615 0.077
## KickInsPlayOnPercentage Bounces OnePercenters PressureActs
## 1 NA 0.000 0.333 9.667
## 2 NA 0.000 0.750 10.250
## 3 0 0.125 0.500 17.625
## 4 NA 0.222 1.000 10.667
## 5 NA 0.125 0.625 15.875
## 6 0 0.154 2.154 7.769
## PressureActsDefensiveHalf Spoils Season Position FirstPossessions
## 1 1.667 0.333 2012 <NA> NA
## 2 2.625 0.375 2012 <NA> NA
## 3 4.500 0.125 2012 <NA> NA
## 4 2.111 0.556 2012 <NA> NA
## 5 4.875 0.500 2012 <NA> NA
## 6 5.308 1.615 2012 <NA> NA
## FirstPossessionToClearance HitoutToFirstPossession HitoutToClearance
## 1 NA NA NA
## 2 NA NA NA
## 3 NA NA NA
## 4 NA NA NA
## 5 NA NA NA
## 6 NA NA NA
## DisposalRetentionPercentage KicksIntoF50 KickIntoF50RetentionPercentage
## 1 NA NA NA
## 2 NA NA NA
## 3 NA NA NA
## 4 NA NA NA
## 5 NA NA NA
## 6 NA NA NA
## AssistedMetresGained NetMetresGained KickMetresGained RetainedMetresGained
## 1 NA NA NA NA
## 2 NA NA NA NA
## 3 NA NA NA NA
## 4 NA NA NA NA
## 5 NA NA NA NA
## 6 NA NA NA NA
## KickMetresGainedPerKick HandballMetresGained HandballMetresGainedPerHandball
## 1 NA NA NA
## 2 NA NA NA
## 3 NA NA NA
## 4 NA NA NA
## 5 NA NA NA
## 6 NA NA NA
## Equity_PreClearance Equity_PostClearance Equity_Possession Equity_BallUse
## 1 NA NA NA NA
## 2 NA NA NA NA
## 3 NA NA NA NA
## 4 NA NA NA NA
## 5 NA NA NA NA
## 6 NA NA NA NA
## xThreatPerKick ThreatRating xRetainPerKick RetentionRating
## 1 NA NA NA NA
## 2 NA NA NA NA
## 3 NA NA NA NA
## 4 NA NA NA NA
## 5 NA NA NA NA
## 6 NA NA NA NA
## GroundBallGetsDefensive50 HardBallGets LooseBallGets
## 1 NA NA NA
## 2 NA NA NA
## 3 NA NA NA
## 4 NA NA NA
## 5 NA NA NA
## 6 NA NA NA
## PostClearanceContestedPossessions PostClearanceGroundBallGets
## 1 NA NA
## 2 NA NA
## 3 NA NA
## 4 NA NA
## 5 NA NA
## 6 NA NA
## GathersFromHitout CrumbingPossessions HandballReceives AverageFieldPosition
## 1 NA NA NA NA
## 2 NA NA NA NA
## 3 NA NA NA NA
## 4 NA NA NA NA
## 5 NA NA NA NA
## 6 NA NA NA NA
## Defensive50Possessions DefensiveMidfieldPossessions
## 1 NA NA
## 2 NA NA
## 3 NA NA
## 4 NA NA
## 5 NA NA
## 6 NA NA
## AttackingMidfieldPossessions Forward50Possessions
## 1 NA NA
## 2 NA NA
## 3 NA NA
## 4 NA NA
## 5 NA NA
## 6 NA NA
## CentreBounceAttendancePercentage CentreClearanceRate RuckHardBallGets
## 1 NA NA NA
## 2 NA NA NA
## 3 NA NA NA
## 4 NA NA NA
## 5 NA NA NA
## 6 NA NA NA
## ScoreAssists xScoreShots xScorePerShot xScoreRatingPerShot xScoreShots_Set
## 1 NA NA NA NA NA
## 2 NA NA NA NA NA
## 3 NA NA NA NA NA
## 4 NA NA NA NA NA
## 5 NA NA NA NA NA
## 6 NA NA NA NA NA
## xScorePerShot_Set xScoreRatingPerShot_Set xScoreShots_General
## 1 NA NA NA
## 2 NA NA NA
## 3 NA NA NA
## 4 NA NA NA
## 5 NA NA NA
## 6 NA NA NA
## xScorePerShot_General xScoreRatingPerShot_General KickInPercentage
## 1 NA NA NA
## 2 NA NA NA
## 3 NA NA NA
## 4 NA NA NA
## 5 NA NA NA
## 6 NA NA NA
dim(afl)
## [1] 9976 127
names(afl)
## [1] "Player" "Team"
## [3] "Age" "Age_Decimal"
## [5] "Matches" "RatingPoints_Avg"
## [7] "Supercoach_Avg" "DreamTeamPoints_Avg"
## [9] "CoachesVotes_Total" "CoachesVotes_Avg"
## [11] "CoachesVotes_MatchesPolled" "BnFVotes_Total"
## [13] "TimeOnGround" "Kicks"
## [15] "Handballs" "Disposals"
## [17] "DisposalEfficiency" "KickingEfficiency"
## [19] "HandballEfficiency" "KickPercentage"
## [21] "Inside50s" "Rebound50s"
## [23] "Clangers" "DisposalsPerClanger"
## [25] "Turnovers" "DisposalsPerTurnover"
## [27] "MetresGained" "MetresGainedPerDisposal"
## [29] "ContestedPossessions" "UncontestedPossessions"
## [31] "TotalPossessions" "ContestedPossessionRate"
## [33] "Intercepts" "GroundBallGets"
## [35] "GroundBallGetsForward50" "CentreBounceAttendances"
## [37] "CentreClearances" "StoppageClearances"
## [39] "TotalClearances" "Marks"
## [41] "ContestedMarks" "MarksInside50"
## [43] "InterceptMarks" "MarksOnLead"
## [45] "FreesFor" "FreesAgainst"
## [47] "FreesDiff" "RuckContests"
## [49] "RuckContestPercentage" "Hitouts"
## [51] "HitoutsWinPercentage" "HitoutsToAdvantage"
## [53] "HitoutsToAdvantagePercentage" "Tackles"
## [55] "TacklesInside50" "ContestDefensiveOneOnOnes"
## [57] "ContestDefensiveLossPercentage" "ContestOffensiveOneOnOnes"
## [59] "ContestOffensiveWinPercentage" "Goals_Total"
## [61] "Goals_Avg" "Behinds"
## [63] "ShotsAtGoal" "GoalAssists"
## [65] "ScoreboardImpact" "GoalAccuracy"
## [67] "ScoreInvolvements" "ScoreInvolvementPercentage"
## [69] "ScoreLaunches" "KickIns"
## [71] "KickInsPlayOnPercentage" "Bounces"
## [73] "OnePercenters" "PressureActs"
## [75] "PressureActsDefensiveHalf" "Spoils"
## [77] "Season" "Position"
## [79] "FirstPossessions" "FirstPossessionToClearance"
## [81] "HitoutToFirstPossession" "HitoutToClearance"
## [83] "DisposalRetentionPercentage" "KicksIntoF50"
## [85] "KickIntoF50RetentionPercentage" "AssistedMetresGained"
## [87] "NetMetresGained" "KickMetresGained"
## [89] "RetainedMetresGained" "KickMetresGainedPerKick"
## [91] "HandballMetresGained" "HandballMetresGainedPerHandball"
## [93] "Equity_PreClearance" "Equity_PostClearance"
## [95] "Equity_Possession" "Equity_BallUse"
## [97] "xThreatPerKick" "ThreatRating"
## [99] "xRetainPerKick" "RetentionRating"
## [101] "GroundBallGetsDefensive50" "HardBallGets"
## [103] "LooseBallGets" "PostClearanceContestedPossessions"
## [105] "PostClearanceGroundBallGets" "GathersFromHitout"
## [107] "CrumbingPossessions" "HandballReceives"
## [109] "AverageFieldPosition" "Defensive50Possessions"
## [111] "DefensiveMidfieldPossessions" "AttackingMidfieldPossessions"
## [113] "Forward50Possessions" "CentreBounceAttendancePercentage"
## [115] "CentreClearanceRate" "RuckHardBallGets"
## [117] "ScoreAssists" "xScoreShots"
## [119] "xScorePerShot" "xScoreRatingPerShot"
## [121] "xScoreShots_Set" "xScorePerShot_Set"
## [123] "xScoreRatingPerShot_Set" "xScoreShots_General"
## [125] "xScorePerShot_General" "xScoreRatingPerShot_General"
## [127] "KickInPercentage"
Only players who played at least 10 matches are used.
Players with a missing age or Player Rating are also removed.
# create analysis dataset
peak_data <- afl[
afl$Matches >= 10 &
!is.na(afl$Age) &
!is.na(afl$RatingPoints_Avg),
]
# inspect analysis data
dim(peak_data)
## [1] 6702 127
summary(peak_data$Age)
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 19.00 22.00 25.00 25.43 28.00 39.00
summary(peak_data$RatingPoints_Avg)
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 1.350 7.139 8.915 9.208 10.938 22.213
Using a minimum of 10 matches helps stop a player with only a few games from having too much effect on the results.
The number of observations at each age is first calculated.
# count observations by age
age_counts <- aggregate(
RatingPoints_Avg ~ Age,
data = peak_data,
FUN = length
)
# rename count column
names(age_counts)[2] <- "N"
# display age counts
age_counts
## Age N
## 1 19 231
## 2 20 383
## 3 21 506
## 4 22 608
## 5 23 649
## 6 24 621
## 7 25 610
## 8 26 586
## 9 27 539
## 10 28 477
## 11 29 404
## 12 30 353
## 13 31 259
## 14 32 191
## 15 33 139
## 16 34 70
## 17 35 43
## 18 36 19
## 19 37 8
## 20 38 4
## 21 39 2
The average Player Rating is then calculated for each age.
# calculate average rating by age
age_ratings <- aggregate(
RatingPoints_Avg ~ Age,
data = peak_data,
FUN = mean
)
# rename average rating column
names(age_ratings)[2] <- "AverageRating"
The average ratings and number of observations are then combined.
# combine age results
age_summary <- merge(
age_ratings,
age_counts,
by = "Age"
)
# only keep ages with at least 20 observations
age_summary <- age_summary[
age_summary$N >= 20,
]
# sort by age
age_summary <- age_summary[
order(age_summary$Age),
]
# display age summary
age_summary
## Age AverageRating N
## 1 19 6.748597 231
## 2 20 7.573329 383
## 3 21 8.121115 506
## 4 22 8.722090 608
## 5 23 8.958313 649
## 6 24 9.513810 621
## 7 25 9.651289 610
## 8 26 9.661070 586
## 9 27 9.793699 539
## 10 28 9.790113 477
## 11 29 9.973765 404
## 12 30 9.868402 353
## 13 31 9.877286 259
## 14 32 9.402639 191
## 15 33 9.472705 139
## 16 34 9.622429 70
## 17 35 9.914023 43
A minimum of 20 observations is used so very small age groups do not have too much effect.
The age with the highest average Player Rating is found.
# find age with highest average rating
best_age <- age_summary[
which.max(age_summary$AverageRating),
]
# display highest rated age
best_age
## Age AverageRating N
## 11 29 9.973765 404
The average Player Rating at each age is shown on a graph.
# create average rating by age plot
age_plot <- ggplot(
age_summary,
aes(
x = Age,
y = AverageRating
)
) +
geom_line() +
geom_point() +
labs(
title = "Average AFL Player Rating by Age",
x = "Age",
y = "Average Player Rating"
) +
theme_minimal()
# display age plot
age_plot
A quadratic regression is used to model the relationship between age and Player Rating.
The squared age term allows the model to increase and then decrease instead of only fitting a straight line.
# fit quadratic regression
age_model <- lm(
RatingPoints_Avg ~
Age +
I(Age^2),
data = peak_data
)
# display model results
summary(age_model)
##
## Call:
## lm(formula = RatingPoints_Avg ~ Age + I(Age^2), data = peak_data)
##
## Residuals:
## Min 1Q Median 3Q Max
## -8.1516 -1.9590 -0.2988 1.6926 12.2189
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) -12.858468 1.329039 -9.675 <2e-16 ***
## Age 1.557667 0.102657 15.173 <2e-16 ***
## I(Age^2) -0.026531 0.001953 -13.587 <2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 2.739 on 6699 degrees of freedom
## Multiple R-squared: 0.07616, Adjusted R-squared: 0.07588
## F-statistic: 276.1 on 2 and 6699 DF, p-value: < 2.2e-16
The coefficients from the quadratic model are used to estimate the age where the curve reaches its highest point.
# save model coefficients
age_coefs <- coef(age_model)
# calculate predicted peak age
predicted_peak_age <- -
age_coefs["Age"] /
(2 * age_coefs["I(Age^2)"])
# display predicted peak age
predicted_peak_age
## Age
## 29.35612
A range of ages is then created so the regression curve can be plotted.
# create age prediction values
age_range <- data.frame(
Age = seq(
min(age_summary$Age),
max(age_summary$Age),
by = 0.1
)
)
# predict rating across ages
age_range$PredictedRating <- predict(
age_model,
newdata = age_range
)
# find predicted maximum rating
predicted_peak_rating <- max(
age_range$PredictedRating
)
# display predicted peak rating
predicted_peak_rating
## [1] 10.00501
The regression curve is plotted with the observed average ratings.
# create quadratic regression plot
regression_plot <- ggplot(
age_summary,
aes(
x = Age,
y = AverageRating
)
) +
geom_point() +
geom_line(
data = age_range,
aes(
x = Age,
y = PredictedRating
)
) +
labs(
title = "AFL Player Rating and Age",
subtitle = "Quadratic regression curve",
x = "Age",
y = "Average Player Rating"
) +
theme_minimal()
# display regression plot
regression_plot
Peak age is also compared between AFL positions.
Players without a recorded position are removed.
# create position dataset
position_data <- peak_data[
!is.na(peak_data$Position) &
peak_data$Position != "",
]
The number of observations for each age and position is calculated.
# calculate number of observations by age and position
position_counts <- aggregate(
RatingPoints_Avg ~ Age + Position,
data = position_data,
FUN = length
)
# rename count column
names(position_counts)[3] <- "N"
The average Player Rating is then calculated for every age and position.
# calculate average rating by age and position
position_ratings <- aggregate(
RatingPoints_Avg ~ Age + Position,
data = position_data,
FUN = mean
)
# rename rating column
names(position_ratings)[3] <- "AverageRating"
The results are combined.
Only age and position groups with at least 10 observations are kept.
# combine position results
position_summary <- merge(
position_ratings,
position_counts,
by = c("Age", "Position")
)
# only keep groups with at least 10 observations
position_summary <- position_summary[
position_summary$N >= 10,
]
# sort position results
position_summary <- position_summary[
order(
position_summary$Position,
position_summary$Age
),
]
# display position results
position_summary
## Age Position AverageRating N
## 1 19 Gen. Defender 6.934435 23
## 7 20 Gen. Defender 6.728825 40
## 14 21 Gen. Defender 7.586371 70
## 21 22 Gen. Defender 7.753122 74
## 28 23 Gen. Defender 8.064024 82
## 35 24 Gen. Defender 8.166943 70
## 42 25 Gen. Defender 9.081400 70
## 49 26 Gen. Defender 8.628753 81
## 56 27 Gen. Defender 8.478716 74
## 63 28 Gen. Defender 8.861134 67
## 70 29 Gen. Defender 8.856492 61
## 77 30 Gen. Defender 8.540000 50
## 84 31 Gen. Defender 9.027816 38
## 91 32 Gen. Defender 7.882750 28
## 98 33 Gen. Defender 8.218789 19
## 2 19 Gen. Forward 6.497703 37
## 8 20 Gen. Forward 6.684408 49
## 15 21 Gen. Forward 7.190161 62
## 22 22 Gen. Forward 7.175136 66
## 29 23 Gen. Forward 7.622439 82
## 36 24 Gen. Forward 7.938653 75
## 43 25 Gen. Forward 8.490438 64
## 50 26 Gen. Forward 8.182283 60
## 57 27 Gen. Forward 8.337880 50
## 64 28 Gen. Forward 8.410821 39
## 71 29 Gen. Forward 8.862447 38
## 78 30 Gen. Forward 8.184806 36
## 85 31 Gen. Forward 8.241350 20
## 92 32 Gen. Forward 8.577235 17
## 99 33 Gen. Forward 8.982167 12
## 9 20 Key Defender 7.066700 10
## 16 21 Key Defender 7.092923 13
## 23 22 Key Defender 8.032654 26
## 30 23 Key Defender 7.843511 47
## 37 24 Key Defender 8.708386 44
## 44 25 Key Defender 8.747896 48
## 51 26 Key Defender 8.228463 41
## 58 27 Key Defender 8.930347 49
## 65 28 Key Defender 8.276085 47
## 72 29 Key Defender 8.871667 45
## 79 30 Key Defender 8.524515 33
## 86 31 Key Defender 7.952520 25
## 93 32 Key Defender 8.046722 18
## 4 19 Key Forward 5.032000 13
## 10 20 Key Forward 6.107536 28
## 17 21 Key Forward 6.908722 36
## 24 22 Key Forward 7.150600 35
## 31 23 Key Forward 8.010250 36
## 38 24 Key Forward 7.729607 28
## 45 25 Key Forward 7.834135 37
## 52 26 Key Forward 8.383946 37
## 59 27 Key Forward 8.747226 31
## 66 28 Key Forward 7.952516 31
## 73 29 Key Forward 9.006458 24
## 80 30 Key Forward 8.351765 17
## 87 31 Key Forward 9.766643 14
## 94 32 Key Forward 9.048824 17
## 101 33 Key Forward 8.097000 16
## 108 34 Key Forward 8.939667 12
## 5 19 Mid-Forward 6.468562 16
## 11 20 Mid-Forward 8.090826 23
## 18 21 Mid-Forward 7.988423 26
## 25 22 Mid-Forward 7.907333 27
## 32 23 Mid-Forward 7.819750 28
## 39 24 Mid-Forward 9.930792 24
## 46 25 Mid-Forward 9.574850 20
## 53 26 Mid-Forward 9.664571 14
## 60 27 Mid-Forward 10.433833 18
## 67 28 Mid-Forward 10.400462 13
## 74 29 Mid-Forward 9.971600 10
## 81 30 Mid-Forward 10.593800 10
## 6 19 Midfielder 7.092250 36
## 12 20 Midfielder 8.192887 62
## 19 21 Midfielder 8.712352 91
## 26 22 Midfielder 9.564765 102
## 33 23 Midfielder 10.662200 95
## 40 24 Midfielder 10.570161 93
## 47 25 Midfielder 10.740144 97
## 54 26 Midfielder 10.852263 95
## 61 27 Midfielder 10.719330 88
## 68 28 Midfielder 11.069112 80
## 75 29 Midfielder 11.709629 70
## 82 30 Midfielder 11.123159 63
## 89 31 Midfielder 11.223157 51
## 96 32 Midfielder 10.203313 32
## 103 33 Midfielder 10.358417 24
## 110 34 Midfielder 10.918000 11
## 27 22 Ruck 8.034154 13
## 34 23 Ruck 9.558412 17
## 41 24 Ruck 10.926500 22
## 48 25 Ruck 11.386375 24
## 55 26 Ruck 11.432286 21
## 62 27 Ruck 11.040955 22
## 69 28 Ruck 11.575263 19
## 76 29 Ruck 11.643214 14
## 83 30 Ruck 12.529167 18
## 90 31 Ruck 11.368211 19
## 97 32 Ruck 11.702214 14
## 104 33 Ruck 10.937000 11
The age with the highest average Player Rating is found for each position.
# create empty peak position table
position_peaks <- data.frame()
# find peak age for each position
for (position in sort(
unique(position_summary$Position)
)) {
# select position
current_position <- position_summary[
position_summary$Position == position,
]
# find highest rated age
peak_row <- current_position[
which.max(
current_position$AverageRating
),
]
# add result
position_peaks <- rbind(
position_peaks,
data.frame(
Position = position,
PeakAge = peak_row$Age,
PeakRating = peak_row$AverageRating,
N = peak_row$N
)
)
}
# round ratings
position_peaks$PeakRating <- round(
position_peaks$PeakRating,
2
)
# display peak age by position
position_peaks
## Position PeakAge PeakRating N
## 1 Gen. Defender 25 9.08 70
## 2 Gen. Forward 33 8.98 12
## 3 Key Defender 27 8.93 49
## 4 Key Forward 31 9.77 14
## 5 Mid-Forward 30 10.59 10
## 6 Midfielder 29 11.71 70
## 7 Ruck 30 12.53 18
The relationship between age and Player Rating is shown separately for each position.
# create position plot
position_plot <- ggplot(
position_summary,
aes(
x = Age,
y = AverageRating
)
) +
geom_line() +
geom_point() +
facet_wrap(
~ Position
) +
labs(
title = "Average AFL Player Rating by Age and Position",
x = "Age",
y = "Average Player Rating"
) +
theme_minimal()
# display position plot
position_plot
A small table is created with the main overall values.
# create final results
final_results <- data.frame(
Result = c(
"Highest Observed Average Age",
"Quadratic Model Peak Age",
"Quadratic Model Peak Rating"
),
Value = c(
best_age$Age,
round(predicted_peak_age, 2),
round(predicted_peak_rating, 2)
)
)
# display final results
knitr::kable(
final_results,
caption = "Overall Peak Age Results"
)
| Result | Value |
|---|---|
| Highest Observed Average Age | 29.00 |
| Quadratic Model Peak Age | 29.36 |
| Quadratic Model Peak Rating | 10.01 |
# display position peak table
knitr::kable(
position_peaks,
caption = "Observed Peak Age by AFL Position"
)
| Position | PeakAge | PeakRating | N |
|---|---|---|---|
| Gen. Defender | 25 | 9.08 | 70 |
| Gen. Forward | 33 | 8.98 | 12 |
| Key Defender | 27 | 8.93 | 49 |
| Key Forward | 31 | 9.77 | 14 |
| Mid-Forward | 30 | 10.59 | 10 |
| Midfielder | 29 | 11.71 | 70 |
| Ruck | 30 | 12.53 | 18 |
The main tables can also be saved as CSV files.
# save age results
write.csv(
age_summary,
"afl_rating_by_age.csv",
row.names = FALSE
)
# save position results
write.csv(
position_peaks,
"afl_peak_age_by_position.csv",
row.names = FALSE
)