DATA 624: HOMEWORK 5

Author

Pascal Hermann Kouogang Tafo

Loading the fpp3 library

library(fpp3)
Warning: package 'fpp3' was built under R version 4.5.3
── Attaching packages ──────────────────────────────────────────── fpp3 1.0.3 ──
✔ tibble      3.3.1     ✔ tsibble     1.2.0
✔ dplyr       1.2.1     ✔ tsibbledata 0.4.1
✔ tidyr       1.3.2     ✔ ggtime      1.0.0
✔ lubridate   1.9.5     ✔ feasts      0.5.0
✔ ggplot2     4.0.3     ✔ fable       0.5.0
Warning: package 'tibble' was built under R version 4.5.2
Warning: package 'dplyr' was built under R version 4.5.3
Warning: package 'tidyr' was built under R version 4.5.2
Warning: package 'lubridate' was built under R version 4.5.2
Warning: package 'ggplot2' was built under R version 4.5.3
Warning: package 'tsibble' was built under R version 4.5.3
Warning: package 'tsibbledata' was built under R version 4.5.3
Warning: package 'ggtime' was built under R version 4.5.3
Warning: package 'feasts' was built under R version 4.5.3
Warning: package 'fabletools' was built under R version 4.5.3
Warning: package 'fable' was built under R version 4.5.3
── Conflicts ───────────────────────────────────────────────── fpp3_conflicts ──
✖ lubridate::date()    masks base::date()
✖ dplyr::filter()      masks stats::filter()
✖ tsibble::intersect() masks base::intersect()
✖ tsibble::interval()  masks lubridate::interval()
✖ dplyr::lag()         masks stats::lag()
✖ tsibble::setdiff()   masks base::setdiff()
✖ tsibble::union()     masks base::union()

EXERCISE 8.1

Consider the number of pigs slaughtered in Victoria, available in the aus_livestock dataset.

  1. Use the ETS() function to estimate the equivalent model for simple exponential smoothing. Find the optimal values of α and ℓ0, and generate forecasts for the next four months.
# Let's visualize the aus_livestock dataset 

head(aus_livestock)
# A tsibble: 6 x 4 [1M]
# Key:       Animal, State [1]
     Month Animal                     State                        Count
     <mth> <fct>                      <fct>                        <dbl>
1 1976 Jul Bulls, bullocks and steers Australian Capital Territory  2300
2 1976 Aug Bulls, bullocks and steers Australian Capital Territory  2100
3 1976 Sep Bulls, bullocks and steers Australian Capital Territory  2100
4 1976 Oct Bulls, bullocks and steers Australian Capital Territory  1900
5 1976 Nov Bulls, bullocks and steers Australian Capital Territory  2100
6 1976 Dec Bulls, bullocks and steers Australian Capital Territory  1800
# Let's get the number of pigs slaughtered in Victoria

Victoria_pigs <- aus_livestock |>
  filter(Animal=="Pigs", State=="Victoria")

head(Victoria_pigs)
# A tsibble: 6 x 4 [1M]
# Key:       Animal, State [1]
     Month Animal State     Count
     <mth> <fct>  <fct>     <dbl>
1 1972 Jul Pigs   Victoria  94200
2 1972 Aug Pigs   Victoria 105700
3 1972 Sep Pigs   Victoria  96500
4 1972 Oct Pigs   Victoria 117100
5 1972 Nov Pigs   Victoria 104600
6 1972 Dec Pigs   Victoria 100500
# Let's plot the series to see pattern of the number of pigs slaughtered

Victoria_pigs |> autoplot(Count) +
  labs(title="Number of Pigs slaughtered in Victoria")

# Let's Use the ETS() function to estimate the equivalent model for simple exponential smoothing and Find the optimal values of α and ℓ0

Victoria_pigs_fit <- Victoria_pigs |>
  model(SES = ETS(Count ~ error("A") + trend("N") + season("N")))

report(Victoria_pigs_fit)
Series: Count 
Model: ETS(A,N,N) 
  Smoothing parameters:
    alpha = 0.3221247 

  Initial states:
     l[0]
 100646.6

  sigma^2:  87480760

     AIC     AICc      BIC 
13737.10 13737.14 13750.07 
## Let's generate the forecast for the next four months

pigs_forecats <- Victoria_pigs_fit  |>
  forecast(h=4)

pigs_forecats
# A fable: 4 x 6 [1M]
# Key:     Animal, State, .model [1]
  Animal State    .model    Month
  <fct>  <fct>    <chr>     <mth>
1 Pigs   Victoria SES    2019 Jan
2 Pigs   Victoria SES    2019 Feb
3 Pigs   Victoria SES    2019 Mar
4 Pigs   Victoria SES    2019 Apr
# ℹ 2 more variables: Count <dist>, .mean <dbl>

Results

The optimal values of alpha (α) is 0.3221247 and the initial state ℓ0 is 100646.6.

  1. Compute a 95% prediction interval for the first forecast using y_hat±1.96s where
    s is the standard deviation of the residuals. Compare your interval with the interval produced by R.

** First step:** Let’s manually compute a 95% prediction interval for the first forecast

# Let's calculate s is the standard deviation of the residuals.

res <- residuals(Victoria_pigs_fit, type = "response")$.resid
s <- sd(res, na.rm = TRUE) 
s
[1] 9344.666
# Let's get the first forecast value

first_forecast <- pigs_forecats |> 
  as_tibble() |>
  slice(1) |> pull(.mean)
first_forecast
[1] 95186.56
# Let's manually calculate the lower and upper value of the  95% prediction interval for the first forecast

Int_lower_value <- first_forecast - 1.96*s
Int_upper_value <- first_forecast + 1.96*s

Int_lower_value
[1] 76871.01
Int_upper_value
[1] 113502.1
# Let's compute the R's first forecast interval

first_forecast <- pigs_forecats |> 
  hilo(level = 95) |>
  head(1)

R_first_forecast_Interval <- first_forecast[7]
R_first_forecast_Interval
# A tibble: 1 × 1
                   `95%`
                  <hilo>
1 [76854.79, 113518.3]95

Interpretation

The manually computed interval for the first forecast is equivalent to [76871.01 ,113502.1] whereas the R’s 95% prediction interval is [76854.79, 113518.3].The lower value differ by only 16.22 and the upper value by 16.20. It was predictable that the interval would not be exactly the same because he manual formula is a simple approximation, while R’s interval accounts for the ETS model structure and it uses the exact critical value for a standard normal distribution to calculate the 1-step forecast variance rather than the sample standard deviation s of the in-sample residual series. In conclusion,both interval are close to each other.

EXERCISE 8.5

Data set global_economy contains the annual Exports from many countries. Let’s select one country to analyse the following. I will choose cameroon for my analysis.

a) Plot the Exports series and discuss the main features of the data.

# Dataset visualization

head(global_economy)
# A tsibble: 6 x 9 [1Y]
# Key:       Country [1]
  Country     Code   Year         GDP Growth   CPI Imports Exports Population
  <fct>       <fct> <dbl>       <dbl>  <dbl> <dbl>   <dbl>   <dbl>      <dbl>
1 Afghanistan AFG    1960  537777811.     NA    NA    7.02    4.13    8996351
2 Afghanistan AFG    1961  548888896.     NA    NA    8.10    4.45    9166764
3 Afghanistan AFG    1962  546666678.     NA    NA    9.35    4.88    9345868
4 Afghanistan AFG    1963  751111191.     NA    NA   16.9     9.17    9533954
5 Afghanistan AFG    1964  800000044.     NA    NA   18.1     8.89    9731361
6 Afghanistan AFG    1965 1006666638.     NA    NA   21.4    11.3     9938414
# Let's filter data for Cameroon

Cameroon_economy <- global_economy |>
  filter(Country=="Cameroon") |>
  select(Year, Exports) |>
  drop_na()
Cameroon_economy
# A tsibble: 53 x 2 [1Y]
    Year Exports
   <dbl>   <dbl>
 1  1965    23.8
 2  1966    21.0
 3  1967    20.6
 4  1968    22.1
 5  1969    23.4
 6  1970    26.2
 7  1971    23.0
 8  1972    20.4
 9  1973    20.7
10  1974    25.4
# ℹ 43 more rows
# Let's plot the Exports series and discuss the main features of the data.

Cameroon_economy |> autoplot(Exports) +
  labs(title="Cameroon Exports as % of GDP from 1960 - 2017")

Interpretation

Cameroon exports (% of GDP) fluctuate between 15% and 33% with no persistent trend. The series does not show a seasonal cycle which can be expected for annual data. Moreover, Cameroon exports jump to around 33% in 1982 to 1985, then we observe a huge drop around 16 to 17% in 1987-1988 with a dip less than 15% in 1993 stands out as well. Since the mid-1990s the series shows a type of cyclical fluctuations in a narrower band around 20% to 29% and has been drifting down since 2012 and ending around 18% in 2017.

b) Use an ETS(A,N,N) model to forecast the series, and plot the forecasts.

#  Fit ETS model

fit <- Cameroon_economy |>
  model(ETS_ANN = ETS(Exports ~ error("A") + trend("N") + season("N")))

# Model summary

report(fit)
Series: Exports 
Model: ETS(A,N,N) 
  Smoothing parameters:
    alpha = 0.713174 

  Initial states:
     l[0]
 22.97788

  sigma^2:  13.9796

     AIC     AICc      BIC 
354.1795 354.6693 360.0904 
# Let's Forecast for the next 5 years and plot the forecasts

forecast_annual <- fit |>
  forecast(h=5)


forecast_annual |>
  autoplot(Cameroon_economy) +
  labs(title="Cameroon exports: ETS(A,N,N) forecast")

Results

The values of alpha (α) is 0.713174 and the initial state ℓ0 is 22.97788 for the ETS ANN model. We know that a high value alpha (α) means the forecast depends in majority on the latest observations. The forecast exhibit a flat line around 19% for the next 5 years (2018-2022).

**c) Compute the RMSE values for the training data.

# Let's find the RMES in the accuracy measures

RMSE_ETS_ANN <- accuracy(fit) |>
  select(RMSE)
RMSE_ETS_ANN
# A tibble: 1 × 1
   RMSE
  <dbl>
1  3.67

The RMSE values for the training data of the ETS(A,N,N) model is 3.667707.

  1. Compare the results to those from an ETS(A,A,N) model. (Remember that the trended model is using one more parameter than the simpler model.)
# Let's fit the ETS models AAN

fit_AAN <- Cameroon_economy |>
  model(ETS_ANN = ETS(Exports ~ error("A") + trend("A") + season("N")))
report(fit_AAN)
Series: Exports 
Model: ETS(A,A,N) 
  Smoothing parameters:
    alpha = 0.7034786 
    beta  = 0.0001000191 

  Initial states:
     l[0]      b[0]
 22.20102 -0.062012

  sigma^2:  14.5576

     AIC     AICc      BIC 
358.2066 359.4832 368.0580 
# Let's fit both ETS models for a better comparison

fit_both_ETS <- Cameroon_economy |>
  model(ETS_ANN = ETS(Exports ~ error("A") + trend("N") + season("N")),
        ETS_AAN = ETS(Exports ~ error("A") + trend("A") + season("N"))
        )

# Model summary

report(fit_both_ETS)
Warning in report.mdl_df(fit_both_ETS): Model reporting is only supported for
individual models, so a glance will be shown. To see the report for a specific
model, use `select()` and `filter()` to identify a single model.
# A tibble: 2 × 9
  .model  sigma2 log_lik   AIC  AICc   BIC   MSE  AMSE   MAE
  <chr>    <dbl>   <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
1 ETS_ANN   14.0   -174.  354.  355.  360.  13.5  20.9  2.77
2 ETS_AAN   14.6   -174.  358.  359.  368.  13.5  20.8  2.79
# Let's compare the training accuracy measure

accurary_measure <- accuracy(fit_both_ETS) 
accurary_measure
# A tibble: 2 × 10
  .model  .type          ME  RMSE   MAE   MPE  MAPE  MASE RMSSE   ACF1
  <chr>   <chr>       <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>  <dbl>
1 ETS_ANN Training -0.103    3.67  2.77 -2.18  12.5 0.998 0.963 0.0410
2 ETS_AAN Training  0.00421  3.67  2.79 -1.72  12.5 1.01  0.964 0.0468

Results

The ETS(A,A,N) model uses two extra parameters (β and the initial slope), and that hurts it on the information criteria (AIC, AICc and BIC). Indeed, the ETS(A,N,N) model informations criteria values are lower than their values in the ETS(A,A,N) model because we know lower is better on all three, so the simple model wins.

The values of alpha (α) and beta are respectively 0.7034786 and 0.0001000191 with both initial states ℓ0 and b(0) or slope are respectively 22.20102 and -0.062012 for the ETS AAN model. In the ETS(A,A,N) model has beta close to zero, the trend is nearly constant and their RMSE improves to 3.668642.

e) Compare the forecasts from both methods. Which do you think is best?

# Let's Forecast both models for the next 5 years and plot the forecasts

forecast_both_models <- fit_both_ETS |>
  forecast(h=5)

forecast_both_models |> 
  autoplot (Cameroon_economy) +
  labs(title= "Cameroon Exports: ETS(A,N,N) vs ETS(A,A,N)")

INterpretation

ANN exhibit a flat line at around 19%, whereas AAN produces a declining linear slope which follows the most recent observations. The forecast of the next five year for both models are within about 0.5% points of each other which is minimal. In conclusion, the ETS(A,N,N) model is better because it has fewer parameters and better AIC/AICc/BIC.

f) Calculate a 95% prediction interval for the first forecast for each model, using the RMSE values and assuming normal errors. Compare your intervals with those produced using R.

# Let's pull out the first forecast for each model and training RMSEs

RMSE_both <- accuracy(fit_both_ETS) |>
  select(.model,RMSE)

forecast_1 <- forecast_both_models |>
  filter(Year==2018)


# Let's manually calculate 95% prediction intervals

prediction_intervals <- forecast_1 %>%
  left_join(RMSE_both, by = ".model") %>%
  mutate(
    Lower95 = .mean - 1.96 * RMSE,
    Upper95 = .mean + 1.96 * RMSE
  )

prediction_intervals
# A fable: 2 x 7 [?]
# Key:     .model [2]
  .model  Year   Exports
  <chr>  <dbl>    <dist>
1 ETS_A…  2018 N(19, 15)
2 ETS_A…  2018 N(19, 14)
# ℹ 4 more variables: .mean <dbl>, RMSE <dbl>, Lower95 <dbl>, Upper95 <dbl>
# Let's compute the R's first forecast interval

forecast_1 |>
  hilo(level=95) |>
  select(.model, Year, .mean, '95%')
# A tsibble: 2 x 4 [1Y]
# Key:       .model [2]
  .model   Year .mean                  `95%`
  <chr>   <dbl> <dbl>                 <hilo>
1 ETS_ANN  2018  19.1 [11.76011, 26.41645]95
2 ETS_AAN  2018  19.0 [11.55286, 26.50913]95

Interpretation

The manual calculation uses the standard normal multiplier 1.96 whereas R computes intervals using the exact quantile (\(z_{0.975} \approx 1.959964\)). For first forecast the variance is just the standard deviation to second power (σ²), so the RMSE-based intervals match the model-based ones to the second decimal. In conclusion, for the first forecast, the manual calculation and those produced using R result will typically match but they would only differ for later horizons, where the model’s intervals widen.

EXERCISE 8.6

Forecast the Chinese GDP from the global_economy data set using an ETS model. Experiment with the various options in the ETS() function to see how much the forecasts change with damped trend, or with a Box-Cox transformation. Try to develop an intuition of what each is doing to the forecasts.

[Hint: use a relatively large value of h when forecasting, so you can clearly see the differences between the various options when plotting the forecasts.]

# Let's filter the data of interest first

China <- global_economy |>
  filter(Country=="China")

head(China)
# A tsibble: 6 x 9 [1Y]
# Key:       Country [1]
  Country Code   Year          GDP Growth   CPI Imports Exports Population
  <fct>   <fct> <dbl>        <dbl>  <dbl> <dbl>   <dbl>   <dbl>      <dbl>
1 China   CHN    1960 59716467625.  NA       NA    4.43    4.31  667070000
2 China   CHN    1961 50056868958. -27.3     NA    3.49    3.87  660330000
3 China   CHN    1962 47209359006.  -5.58    NA    2.91    4.05  665770000
4 China   CHN    1963 50706799903.  10.3     NA    2.86    4.01  682335000
5 China   CHN    1964 59708343489.  18.2     NA    2.86    3.77  698355000
6 China   CHN    1965 70436266147.  17.0     NA    3.19    3.64  715185000
# Let's plot historical China GDP

China |>
  autoplot(GDP) +
  labs(
    title = "China's GDP",
    x = "Year",
    y = "GDP (US dollars)"
  )

# Let's determine the optimal Box-Cox transformation parameter using the Guerrero method

lambda <- China |>
  features(GDP, features = guerrero) |>
  pull(lambda_guerrero)
lambda
[1] -0.03446284
# Let's fit a variety of ETS model specifications

fit_ETS <- China |>
  model(
    `Auto (ETS)` = ETS(GDP),
    `Holt's Linear` = ETS(GDP ~ error("A") + trend("A") + season("N")),
    `Damped Linear` = ETS(GDP ~ error("A") + trend("Ad") + season("N")),
    `Box-Cox Holt` = ETS(box_cox(GDP, lambda) ~ error("A") + trend("A") + season("N")),
    `log` = ETS(log(GDP) ~ error("A") + trend("A") + season("N")),
    `Damped Box-Cox` = ETS(box_cox(GDP, lambda) ~ error("A") + trend("Ad") + season("N"))
  )


# Let's generate forecasts with a large value of h=25 and plot the comparison

forecast_25h <- fit_ETS |>
  forecast(h=25)

forecast_25h |> autoplot(China, level=NULL) +
  labs(title ="China GDPForecasts: ETS Model Variations for h=25",
       y ="GDP in USD",
       x = "Year")

Interpretation

The additive trend is extrapolating the estimated trend without damping. Also,damping trend reduces gradually the trend’s contribution, whereas damped Box-Cox combines variance stabilization with a slowing trend.

Intuition to take away

  • The trend component sets the shape of the forecast including flat, straight, damped or curved.
  • Damping controls how quickly the projected trend fades, whereas Box-Cox controls the scale on which the trend and errors are modeled.

EXERCISE 8.7

Find an ETS model for the Gas data from aus_production and forecast the next few years.

head(aus_production)
# A tsibble: 6 x 7 [1Q]
  Quarter  Beer Tobacco Bricks Cement Electricity   Gas
    <qtr> <dbl>   <dbl>  <dbl>  <dbl>       <dbl> <dbl>
1 1956 Q1   284    5225    189    465        3923     5
2 1956 Q2   213    5178    204    532        4436     6
3 1956 Q3   227    5297    208    561        4806     7
4 1956 Q4   308    5681    197    570        4418     6
5 1957 Q1   262    5577    187    529        4339     5
6 1957 Q2   228    5651    214    604        4811     7
## Let's filter the data of interest and plot to visualize the gas proudction trend 

gas_data <- aus_production |>
  select(Quarter, Gas) |>
  filter(!is.na(Gas))

gas_data |> autoplot(Gas)+
  labs(title="Quarterly Australian Gas production")

We observe an upward trend in the gas production and a seasonal fluctuations which become larger as the production increases.

  1. Let’s find an ETS model and forecast the next few years.
# fit ETS models 

fit_gas <- gas_data |>
  model(
    auto = ETS(Gas),
    `Multiplicative trend` = ETS(Gas ~ error("M") + trend("A") + season("M")),
    `Multiplicative Damped trend` = ETS(Gas ~ error("M") + trend("Ad") + season("M"))
  )

c) Let’s forecast the next 4 years meaning (16 quarters)

forescast_gas <- fit_gas |>
  forecast(h = 16)

# Plot the forecasts

forescast_gas |>
  autoplot(aus_production, level = NULL) +
  labs(
    title = "Australian Gas Production Forecasts",
    subtitle = "ETS models: ordinary versus damped trend",
    x = "Year",
    y = "Gas production"
  )

Result

Multiplicative seasonality is necessary because it assumes that seasonal effects are proportional to the underlying level of the series. In fact, Additive seasonality assumes a fixed-size swing, so it would be too big early on and too small later while A multiplicative form scales the pattern with the level.

  1. Experiment with making the trend damped. Does it improve the forecasts?

For that, we need to compare the models In-sample accuracy and information criteria.

# Compare in-sample accuracy

fit_gas |>
  accuracy() |>
  select(.model, RMSE, MAE, MAPE)
# A tibble: 3 × 4
  .model                       RMSE   MAE  MAPE
  <chr>                       <dbl> <dbl> <dbl>
1 auto                         4.60  3.02  4.08
2 Multiplicative trend         4.60  3.02  4.08
3 Multiplicative Damped trend  4.59  3.03  4.10
# Compare information criteria

fit_gas |>
  glance() |>
  select(.model, AIC, AICc, BIC)
# A tibble: 3 × 4
  .model                        AIC  AICc   BIC
  <chr>                       <dbl> <dbl> <dbl>
1 auto                        1681. 1682. 1711.
2 Multiplicative trend        1681. 1682. 1711.
3 Multiplicative Damped trend 1684. 1685. 1718.

Interpretation

The AICc values for both models are very close. Often, the non dampedmodel has a slightly lower AICc because the data exhibits strong, persistent growth that does not yet show definitive signs of saturation over the sample period. Moreover, the non-damped often performs slightly better on a short period forecast whereas the damping is usually safer because isn’t supported by the likelihood, but it is a reasonable safeguard against the trend overshooting.

Exercise 8.8

set.seed(12345678)
myseries <- aus_retail |>
  filter(`Series ID` == sample(aus_retail$`Series ID`, 1))

head(myseries)
# A tsibble: 6 x 5 [1M]
# Key:       State, Industry [1]
  State              Industry                      `Series ID`    Month Turnover
  <chr>              <chr>                         <chr>          <mth>    <dbl>
1 Northern Territory Clothing, footwear and perso… A3349767W   1988 Apr      2.3
2 Northern Territory Clothing, footwear and perso… A3349767W   1988 May      2.9
3 Northern Territory Clothing, footwear and perso… A3349767W   1988 Jun      2.6
4 Northern Territory Clothing, footwear and perso… A3349767W   1988 Jul      2.8
5 Northern Territory Clothing, footwear and perso… A3349767W   1988 Aug      2.9
6 Northern Territory Clothing, footwear and perso… A3349767W   1988 Sep      3  
  1. Why is multiplicative seasonality necessary for this series?
# let's plot it first

myseries |>
  autoplot(Turnover) +
  labs(
    title = "Turnover: NSW Cafes, Restaurants, and Catering Services",
    y = "Turnover ($M AUD)",
    x = "Month"
  )

We observe that the annual range (peak minus trough) was about less than 2 in the late 1980s, and around 10 in 2018, while the mean rose from about 3 to about 14. In that regard, Multiplicative Seasonality is necessary because the seasonal swing grows with the level meaning they are proportional to the trend level rather than constant in absolute dollars.

  1. Apply Holt-Winters’ multiplicative method to the data. Experiment with making the trend damped.
# Fit the models

fit_Holt_Winters <- myseries |>
  model(
    `Holt-Winters (M,A,M)`  = ETS(Turnover ~ error("M") + trend("A") + season("M")),
    `Damped HW (M,Ad,M)`    = ETS(Turnover ~ error("M") + trend("Ad") + season("M"))
  )

# Inspect estimated parameters and AICc
report(fit_Holt_Winters)
Warning in report.mdl_df(fit_Holt_Winters): Model reporting is only supported
for individual models, so a glance will be shown. To see the report for a
specific model, use `select()` and `filter()` to identify a single model.
# A tibble: 2 × 11
  State     Industry .model  sigma2 log_lik   AIC  AICc   BIC   MSE  AMSE    MAE
  <chr>     <chr>    <chr>    <dbl>   <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>  <dbl>
1 Northern… Clothin… Holt-… 0.00447   -870. 1775. 1776. 1841. 0.376 0.473 0.0511
2 Northern… Clothin… Dampe… 0.00457   -872. 1781. 1783. 1851. 0.379 0.479 0.0505
# let's compare forecasts visually for the next 5 years

fit_Holt_Winters |>
  forecast(h = "5 years") |>
  autoplot(myseries, level = NULL) +
  labs(
    title = "Holt-Winters Forecast Comparison",
    x = "Month",
    y = "Turnover"
  )

  1. Compare the RMSE of the one-step forecasts from the two methods. Which do you prefer?
# One-step-ahead training accuracy

train_accuracy <- fit_Holt_Winters |>
  accuracy() |>
  select(.model, RMSE, MAE, MAPE)

train_accuracy
# A tibble: 2 × 4
  .model                RMSE   MAE  MAPE
  <chr>                <dbl> <dbl> <dbl>
1 Holt-Winters (M,A,M) 0.613 0.450  5.15
2 Damped HW (M,Ad,M)   0.616 0.444  5.06

The preferred method will be the one with the lower RMSE, provided its residuals are also reasonably well behaved. Therefore the the standard Holt-Winters is good.

  1. Check that the residuals from the best method look like white noise.

After identifying the model with the lower RMSE, inspect its residuals using Residual diagnostics for ordinary Holt-Winters and Ljung-Box test for residual autocorrelation.

best_fit <- fit_Holt_Winters |> select(`Holt-Winters (M,A,M)`)

# Visual diagnostic plots

best_fit |>
   gg_tsresiduals()

# Ljung-Box test for white noise
# For monthly seasonal data, lag = 24 is recommended (dof = number of estimated states/parameters)

best_fit |>
  augment() |>
  features(.innov, ljung_box, lag = 24, dof = 16)
# A tibble: 1 × 3
  .model               lb_stat lb_pvalue
  <chr>                  <dbl>     <dbl>
1 Holt-Winters (M,A,M)    34.5 0.0000333

Interpretation

  • Residuals are not centered or spread evenly over time, consistent with errors that scale with the level.
  • Most autocorrelation falls within the dashed line bound except for 3 lags.
  • The histogram
  1. Now find the test set RMSE, while training the model to the end of 2010.
# Let's create the training and test splits

train_data <- myseries |>
  filter(Month <= yearmonth("2010 Dec"))

test_data <- myseries |>
  filter(Month > yearmonth("2010 Dec"))

# Let's fit both models on training data first

fit_train <- train_data |>
  model(
    `Holt-Winters (M,A,M)` = ETS(Turnover ~ error("M") + trend("A") + season("M")),
    `Damped HW (M,Ad,M)`   = ETS(Turnover ~ error("M") + trend("Ad") + season("M"))
  )

# Let's Forecast over the test period (from 2011 Jan to end of series)

test_length <- nrow(test_data)
fc_test <- fit_train |> forecast(h = test_length)

# Let's Evaluate out-of-sample test set accuracy

fc_test |>
  accuracy(myseries) |>
  select(.model, RMSE, MAE, MAPE)
# A tibble: 2 × 4
  .model                RMSE   MAE  MAPE
  <chr>                <dbl> <dbl> <dbl>
1 Damped HW (M,Ad,M)    1.15 0.878  6.45
2 Holt-Winters (M,A,M)  1.78 1.60  12.6 

Results

The test set RMSE evaluates how well the models generalize to unseen future observations over multiple forecast horizons. The model with the smallest test RMSE forecasts this particular series best over the five-year test period.

Exercise 8.9

For the same retail data, try an STL decomposition applied to the Box-Cox transformed series, followed by ETS on the seasonally adjusted data. How does that compare with your best previous forecasts on the test set?

# Let's get the same series

set.seed(12345678)
myseries <- aus_retail |>
  filter(`Series ID` == sample(aus_retail$`Series ID`, 1))

# Let's train/test split (training to end of 2010)

train_data <- myseries |> filter(Month <= yearmonth("2010 Dec"))
test_data  <- myseries |> filter(Month > yearmonth("2010 Dec"))

# Let's Fit models: Previous benchmarks + Decomposition Model

fit_models <- train_data |>
  model(
    `Holt-Winters (M,A,M)` = ETS(Turnover ~ error("M") + trend("A") + season("M")),
    `Damped HW (M,Ad,M)`   = ETS(Turnover ~ error("M") + trend("Ad") + season("M")),
    
   # STL decomposition on Box-Cox transformed data:
   
    `STL + ETS (Box-Cox)`  = decomposition_model(
      STL(box_cox(Turnover, guerrero(Turnover)) ~ season(window = "periodic")),
      ETS(season_adjust ~ season("N")))
  )

#Let's Generate forecasts for the test horizon
fc_test <- fit_models |>
  forecast(new_data = test_data)

# Let's Evaluate out-of-sample accuracy on the test set

fc_accuracy <- fc_test |>
  accuracy(myseries) |>
  select(.model, RMSE, MAE, MAPE, MASE)

fc_accuracy
# A tibble: 3 × 5
  .model                RMSE   MAE  MAPE  MASE
  <chr>                <dbl> <dbl> <dbl> <dbl>
1 Damped HW (M,Ad,M)   1.15  0.878  6.45 0.960
2 Holt-Winters (M,A,M) 1.78  1.60  12.6  1.75 
3 STL + ETS (Box-Cox)  0.887 0.713  5.43 0.779
# Visual comparison

fc_test |>
  autoplot(myseries |> filter(year(Month) >= 2006), level = NULL) +
  labs(
    title = "Turnover Forecast Comparison: Post-2010 Test Period",
    y = "Turnover ($M AUD)",
    x = "Month"
  )

Interpretation

The Holt-Winter has the highest test RMSE, followed by Damped HW and finally STL + ETS (Box-Cox), and its flexible seasonal decomposition and Box–Cox transformation have helped. The Guerrero Box-Cox transformation linearizes the exponential drift and stabilizes the variation when seasonal swings become larger as turnover increases, allowing the non-seasonal ETS model on season_adjust to project a more disciplined, realistic trajectory.

In conclusion, STL + ETS is a useful alternative because it estimates seasonality separately and can accommodate evolving seasonal patterns when you use a non-periodic STL window.