Details

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.


1.0 Data Preparation

1.1 Load Required Packages

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

library(EUfootball)
library(dplyr)
library(fpp3)

1.2 Inspect the Data

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"

1.3 Check for Missing Values

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

1.4 Filter and Prepare a Dataset

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)

1.5 Match Results into Monthly Totals

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

2.0 Data Analysis

2.1 Time Plot

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()

2.2 Seasonal Plot

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()

2.3 Seasonal Subseries Plot

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()

2.4 Autocorrelation Function (ACF)

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()

2.5 Partial Autocorrelation Function (PACF)

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()

3.0 Training and Testing Data

3.1 Split the Data

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"))

3.2 Fit the Forecasting Models

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))

3.3 Generate Forecasts

The forecast() function generates predictions for the months included in the testing dataset.

forecasts <- model_fits |>
  forecast(new_data = testing_data)

3.4 Visualise Forecasts

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()

3.5 Evaluate Forecast Accuracy

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.