This analysis investigates how accurately English Premier League goal total can be forecast using historical match results. Using data from the EUfootball R package covering the 2010/2011 to 2017/2018 seasons. Utilising time-series techniques, four forecasting models were created and compared. This resource provides step by step R code to replicate the preparation, analysis and evaluation processes.
Load the required R packages that are needed for this analysis.
library(EUfootball)
library(dplyr)
library(fpp3)
Before analysing, it is important to understand the structure of the data and identify the variables. The Matches dataset contains historical football match results with dates, teams, goals scored, and more.
soccer_data <- EUfootball::Matches
names(soccer_data)
## [1] "Home" "Guest" "Goals90Home"
## [4] "Goals90Guest" "Goals45Home" "Goals45Guest"
## [7] "date" "matchday" "day"
## [10] "SeasonFrom" "SeasonTo" "eloHome"
## [13] "eloGuest" "MVHome" "MVGuest"
## [16] "oddsHome" "oddsDraw" "oddsGuest"
## [19] "FormGoals3Home" "FormGoals3Guest" "PromotedHome"
## [22] "PromotedGuest" "TitleholderHome" "TitleholderGuest"
## [25] "CupTitleholderHome" "CupTitleholderGuest" "League"
## [28] "MVHome.T" "MVGuest.T" "pHome"
## [31] "pDraw" "pGuest"
Missing values can affect any calculations and predictions on future results, especially for variables that are needed. For this analysis, we will need League, Date, SeasonFrom, SeasonTo, Goals90Home, & Goals90Guest.
colSums(is.na(soccer_data))
## Home Guest Goals90Home Goals90Guest
## 0 0 4 4
## Goals45Home Goals45Guest date matchday
## 14 14 0 0
## day SeasonFrom SeasonTo eloHome
## 0 0 0 0
## eloGuest MVHome MVGuest oddsHome
## 0 1118 1118 114
## oddsDraw oddsGuest FormGoals3Home FormGoals3Guest
## 114 118 0 0
## PromotedHome PromotedGuest TitleholderHome TitleholderGuest
## 0 0 0 0
## CupTitleholderHome CupTitleholderGuest League MVHome.T
## 0 0 0 1118
## MVGuest.T pHome pDraw pGuest
## 1118 118 118 118
The dataset needs to be filtered to reduce the quantity of workign data and ensure remaining statistics meet the requirements needed for this analysis.
soccer_data <- soccer_data |>
filter(League == "BPL",
SeasonFrom >= 2010,
SeasonTo <= 2018) |>
mutate(
date = as.Date(date),
total_goals = Goals90Home + Goals90Guest)
The match-level data is grouped by month, and the total goals scored across all matches in each month are calculated.
monthly_goals <- soccer_data |>
mutate(month = yearmonth(date)) |>
group_by(month) |>
summarise(total_goals = sum(total_goals), matches = n(), .groups = "drop") |>
as_tsibble(index = month) |> #time series tibble
fill_gaps(.full = TRUE, total_goals = 0, matches = 0)
monthly_goals
## # A tsibble: 94 x 3 [1M]
## month total_goals matches
## <mth> <dbl> <dbl>
## 1 2010 Aug 86 30
## 2 2010 Sep 81 30
## 3 2010 Oct 89 39
## 4 2010 Nov 157 51
## 5 2010 Dec 96 40
## 6 2011 Jan 120 44
## 7 2011 Feb 128 42
## 8 2011 Mar 64 22
## 9 2011 Apr 134 47
## 10 2011 May 108 35
## # ℹ 84 more rows
This plot helps to display the monthly Premier League goal totals across the selected period, additionally making it easier to identify any abnormalities and fluctuations in scoring.
monthly_goals |>
autoplot(total_goals) +
labs(
title = "Monthly Total Goals in the English Premier League",
x = "Month",
y = "Total Goals") +
theme_minimal()
This plot examines the seasonal goals over the duration of the calender year, with comparison to each season. Keep in mind the Premier League off-season is June and July.
monthly_goals |>
gg_season(total_goals) +
labs(
title = "Seasonal Plot of Monthly Premier League Goals",
x = "Month",
y = "Total Goals",
colour = "Year") +
theme_minimal()
Whilst still displaying over a calender year, the total goals scored each season is categorised into their respective months. Easier to determine if particular months had consistent goal results over the seasons.
monthly_goals |>
gg_subseries(total_goals) +
labs(
title = "Monthly Goal Totals by Calendar Month",
x = "Month",
y = "Total Goals") +
theme_minimal()
The ACF measures the relationship observations at different time lags, which can help to determine whether past monthly goal totals are associated with later results.
monthly_goals |>
feasts::ACF(total_goals) |>
autoplot() +
labs(
title = "Autocorrelation of Monthly Goal Totals",
x = "Lag",
y = "ACF") +
theme_minimal()
The PACF measures the relationship between observations at a particular lag after accounting for the relationships at shorter lags.
pacf_results <- monthly_goals |>
feasts::PACF(total_goals)
autoplot(pacf_results) +
labs(
title = "Partial Autocorrelation of Monthly Goal Totals",
x = "Lag",
y = "PACF") +
theme_minimal()
The training data is used to create the forecasting models, while the testing data is used to evaluate how accurately the models predict future totals. In this analysis, the training period includes all of the months before August 2017 (back to 2010), while the test period covers August 2017 to May 2018.
training_data <- monthly_goals |>
filter(month < yearmonth("2017-08"))
testing_data <- monthly_goals |>
filter(
month >= yearmonth("2017-08"),
month <= yearmonth("2018-05"))
Four forecasting models are fitted using the training data; naive, drift, exponential smoothing (ETS), and auto regressive integrated moving average (ARIMA).
model_fits <- training_data |>
model(
Naive = NAIVE(total_goals),
Drift = RW(total_goals ~ drift()),
ETS = ETS(total_goals),
ARIMA = ARIMA(total_goals))
The forecast() function generates predictions for the months included in the testing dataset.
forecasts <- model_fits |>
forecast(new_data = testing_data)
A plot helps identify how closely each model follows the observed data and whether the forecasts capture the overall behaviour of the series. Any dfferences between predicted and actual values indicates forecast errors.
autoplot(forecasts, monthly_goals) +
labs(
title = "Forecasts of Monthly Premier League Goal Totals",
subtitle = "Models fitted to the training period",
x = "Month",
y = "Total Goals") +
theme_minimal()
A lower value for both measures indicates a more desirable outcome and accuracy.
accuracy_results <- accuracy(
forecasts,
testing_data) |>
select(.model, RMSE, MAE) |>
arrange(RMSE)
accuracy_results
## # A tibble: 4 × 3
## .model RMSE MAE
## <chr> <dbl> <dbl>
## 1 ETS 16.1 11.4
## 2 ARIMA 22.8 16.7
## 3 Naive 107. 102.
## 4 Drift 112. 107.