DATA 624 Homework 5

Author

Jonnathan Zuna

8.1

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

# Loading required packages
library(fpp3)
pigs <- aus_livestock |>
  filter(Animal == "Pigs", State == "Victoria")

pigs |> autoplot(Count)

The series is monthly with no clear seasonal pattern and a slowly changing level, so simple exponential smoothing is a reasonable starting point.

a. Use the ETS() function to estimate the equivalent model for simple exponential smoothing. Find the optimal values of alpha and l0, and generate forecasts for the next four months.

fit <- pigs |>
  model(ETS(Count ~ error("A") + trend("N") + season("N")))

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

Alpha is 0.3221, so each new month gets ~ 32% of the weight and the older history shares the other 68%, which means the level reacts at a moderate speed. The starting level l0 is 100,646.6 pigs and sigma squared is 87,480,760.

fc <- fit |> forecast(h = 4)

fc |> autoplot(pigs |> filter(year(Month) >= 2010))

Simple exponential smoothing has no trend or season, so all four forecasts sit at the same flat value of ~ 90000 pigs.

b. Compute a 95% prediction interval for the first forecast using y hat plus or minus 1.96s where s is the standard deviation of the residuals. Compare your interval with the interval produced by R.

s <- augment(fit) |> pull(.resid) |> sd(na.rm = TRUE)
yhat <- fc$.mean[1]

c(lower = yhat - 1.96 * s, upper = yhat + 1.96 * s)
    lower     upper 
 76871.01 113502.10 
fc |> hilo(level = 95) |> pull(`95%`) |> head(1)
<hilo[1]>
[1] [76854.79, 113518.3]95

My interval is (76,871, 113,502) and R gives (76,855, 113,518). R is wider by ~16 on each side, under 0.1% of the interval, because it uses the model’s own sigma (the square root of 87,480,760, ~ 9350) instead of the sample standard deviation of the residuals.

8.5

Data set global_economy contains the annual Exports from many countries. Select one country to analyse.

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

aus <- global_economy |> filter(Country == "Australia")

aus |> autoplot(Exports)

Australian exports are yearly so there is no seasonality. The series moves up slowly over time with short ups and downs around that movement.

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

fit_ann <- aus |>
  model(ANN = ETS(Exports ~ error("A") + trend("N") + season("N")))

fit_ann |>
  forecast(h = 5) |>
  autoplot(aus)

The ANN forecast is a flat line at the last smoothed level.

c. Compute the RMSE values for the training data.

accuracy(fit_ann) |> select(.model, RMSE)
# A tibble: 1 × 2
  .model  RMSE
  <chr>  <dbl>
1 ANN     1.15

The training RMSE for ANN is 1.1468.

d. 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.) Discuss the merits of the two forecasting methods for this data set.

fit <- aus |>
  model(
    ANN = ETS(Exports ~ error("A") + trend("N") + season("N")),
    AAN = ETS(Exports ~ error("A") + trend("A") + season("N"))
  )

accuracy(fit) |> select(.model, RMSE)
# A tibble: 2 × 2
  .model  RMSE
  <chr>  <dbl>
1 ANN     1.15
2 AAN     1.12
glance(fit) |> select(.model, AICc)
# A tibble: 2 × 2
  .model  AICc
  <chr>  <dbl>
1 ANN     258.
2 AAN     259.

AAN fits the training data slightly better, with an RMSE of 1.1167 vs 1.1468 for ANN, a drop of 0.0301. But its AICc is 259.47 vs 257.84 for ANN, so the extra trend parameters cost more than they gain. ANN gives the better balance of fit and simplicity for this series.

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

fit |>
  forecast(h = 5) |>
  autoplot(aus, level = NULL)

The ANN line stays flat while the AAN line keeps the recent upward slope going, and the first forecast is 20.84 for AAN vs 20.61 for ANN. Because ANN has the lower AICc (257.84 versus 259.47), I think ANN is the best choice.

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.

fc <- fit |> forecast(h = 5)
rmse <- accuracy(fit) |> select(.model, RMSE)

fc |>
  as_tibble() |>
  group_by(.model) |>
  slice(1) |>
  ungroup() |>
  select(.model, .mean) |>
  left_join(rmse, by = ".model") |>
  mutate(lower = .mean - 1.96 * RMSE, upper = .mean + 1.96 * RMSE)
# A tibble: 2 × 5
  .model .mean  RMSE lower upper
  <chr>  <dbl> <dbl> <dbl> <dbl>
1 AAN     20.8  1.12  18.6  23.0
2 ANN     20.6  1.15  18.4  22.9
fc |>
  hilo(level = 95) |>
  as_tibble() |>
  group_by(.model) |>
  slice(1) |>
  ungroup() |>
  select(.model, `95%`)
# A tibble: 2 × 2
  .model                  `95%`
  <chr>                  <hilo>
1 AAN    [18.57028, 23.10700]95
2 ANN    [18.31970, 22.89462]95

The manual intervals are a little narrower than the ones from R. For ANN mine is (18.36, 22.85) and R gives (18.32, 22.89). For AAN mine is (18.65, 23.03) and R gives (18.57, 23.11). RMSE divides the squared errors by n, while Rs sigma divides by n minus the number of estimated parameters, which is 2 for ANN (alpha and l0) and 4 for AAN (alpha, beta, l0 and b0). That is why the gap is larger for AAN.

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.

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

china |> autoplot(GDP)

China’s GDP grows faster and faster over time, so the growth looks exponential rather than linear. That is a good reason to try a Box Cox transformation.

lambda <- china |>
  features(GDP, features = guerrero) |>
  pull(lambda_guerrero)

lambda
[1] -0.03446284

The Guerrero lambda is ~ -0.034, very close to 0, so the Box Cox transformation is basically a log. On the log scale exponential growth becomes a straight line, which is the kind of trend ETS handles well.

fit <- china |>
  model(
    ETS = ETS(GDP),
    Damped = ETS(GDP ~ trend("Ad")),
    BoxCox = ETS(box_cox(GDP, lambda)),
    BoxCoxDamped = ETS(box_cox(GDP, lambda) ~ trend("Ad"))
  )

glance(fit) |> select(.model, AICc)
# A tibble: 4 × 2
  .model        AICc
  <chr>        <dbl>
1 ETS          3103.
2 Damped       3107.
3 BoxCox       -137.
4 BoxCoxDamped -133.

AICc cannot be compared between the two groups, because the Box Cox models are measured on the transformed scale (~ -137 and -133) and the others on the original GDP scale (~ 3,103 and 3,107). Within each group the undamped model wins: ETS 3,103.2 vs Damped 3,106.7 (a gap of 3.5), and BoxCox -137.4 vs BoxCoxDamped -133.2 (a gap of 4.1). So damping does not improve the fit for this series.

fit |>
  forecast(h = 30) |>
  autoplot(china, level = NULL)

The damped trend models flatten out and end up below the undamped ones at long horizons. The Box Cox models grow in the transformed scale, so after back transforming they keep climbing faster and their intervals become uneven.

8.7

Find an ETS model for the Gas data from aus_production and forecast the next few years. Why is multiplicative seasonality necessary here? Experiment with making the trend damped. Does it improve the forecasts?

aus_production |> autoplot(Gas)

The seasonal swings get larger as the level of gas production rises. A constant size seasonal pattern cannot capture that, so multiplicative seasonality is needed.

fit <- aus_production |>
  model(
    Auto = ETS(Gas),
    HW = ETS(Gas ~ error("M") + trend("A") + season("M")),
    HW_damped = ETS(Gas ~ error("M") + trend("Ad") + season("M"))
  )

report(fit |> select(Auto))
Series: Gas 
Model: ETS(M,A,M) 
  Smoothing parameters:
    alpha = 0.6528545 
    beta  = 0.1441675 
    gamma = 0.09784922 

  Initial states:
     l[0]       b[0]      s[0]    s[-1]    s[-2]     s[-3]
 5.945592 0.07062881 0.9309236 1.177883 1.074851 0.8163427

  sigma^2:  0.0032

     AIC     AICc      BIC 
1680.929 1681.794 1711.389 

The automatic search chose ETS(M,A,M), which is multiplicative error, additive trend and multiplicative seasonality, matching what the plot shows. Alpha is 0.653, beta is 0.144 and gamma is 0.098, so the level reacts quickly while the trend and the seasonal pattern change slowly.

glance(fit) |> select(.model, AICc)
# A tibble: 3 × 2
  .model     AICc
  <chr>     <dbl>
1 Auto      1682.
2 HW        1682.
3 HW_damped 1685.
accuracy(fit) |> 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 HW         4.60  3.02  4.08
3 HW_damped  4.59  3.03  4.10

Auto and HW give identical results because the automatic search picked the same ETS(M,A,M). The damped version has a higher AICc (1685.09 vs 1681.79) nearly identical RMSE (4.5918 vs 4.5951), and a slightly higher MAE (3.031 vs 3.022) and MAPE (4.10 vs 4.08).

fit |>
  forecast(h = "5 years") |>
  autoplot(aus_production |> filter(year(Quarter) >= 2000), level = NULL)

Damping does not improve the forecasts here. Its RMSE is lower by only 0.0033 (4.5951 - 4.5918), while its MAE, MAPE and AICc all get worse. The damped forecast grows more slowly, but the extra phi parameter is not worth it, so I keep the undamped ETS(M,A,M).

8.8

Recall your retail time series data

set.seed(12345678)

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

myseries |> autoplot(Turnover)

a. Why is multiplicative seasonality necessary for this series?

The seasonal swings grow together with the level of turnover, so the seasonal effect is a percentage of the level and not a fixed amount. This is the case where multiplicative seasonality fits. The fitted starting seasonal factors range from ~ 0.76 to 1.37 of the level, which shows the seasonal effect is a percentage of the level and not a fixed amount.

b. Apply Holt Winters multiplicative method to the data. Experiment with making the trend damped.

fit <- myseries |>
  model(
    HW = ETS(Turnover ~ error("M") + trend("A") + season("M")),
    HW_damped = ETS(Turnover ~ error("M") + trend("Ad") + season("M"))
  )

report(fit |> select(HW))
Series: Turnover 
Model: ETS(M,A,M) 
  Smoothing parameters:
    alpha = 0.5262368 
    beta  = 0.0001026197 
    gamma = 0.0001037781 

  Initial states:
     l[0]       b[0]      s[0]     s[-1]    s[-2]    s[-3]     s[-4]    s[-5]
 2.347713 0.04437301 0.8398709 0.7617432 0.833525 1.373239 0.9998723 1.033418
    s[-6]    s[-7]    s[-8]    s[-9]    s[-10]    s[-11]
 1.032927 1.119514 1.140226 1.019343 0.9810424 0.8652791

  sigma^2:  0.0045

     AIC     AICc      BIC 
1774.530 1776.274 1841.014 
report(fit |> select(HW_damped))
Series: Turnover 
Model: ETS(M,Ad,M) 
  Smoothing parameters:
    alpha = 0.5566535 
    beta  = 0.0002378027 
    gamma = 0.1112567 
    phi   = 0.9794538 

  Initial states:
     l[0]     b[0]      s[0]    s[-1]     s[-2]    s[-3]    s[-4]    s[-5]
 2.527185 0.109247 0.8093026 0.746469 0.8360684 1.354604 1.003636 1.045657
    s[-6]    s[-7]    s[-8]    s[-9]    s[-10]    s[-11]
 1.034894 1.093245 1.160544 1.054001 0.9937055 0.8678735

  sigma^2:  0.0046

     AIC     AICc      BIC 
1780.720 1782.674 1851.114 

In the HW model alpha is 0.526 while beta (0.0001) and gamma (0.0001) are almost 0, so the trend and the seasonal pattern stay nearly fixed and only the level updates. In the damped model phi is 0.979, so the trend shrinks by ~2% at each step.

fit |>
  forecast(h = "3 years") |>
  autoplot(myseries |> filter(year(Month) >= 2010), level = NULL)

c. Compare the RMSE of the one step forecasts from the two methods. Which do you prefer?

accuracy(fit) |> select(.model, RMSE, MAE, MAPE)
# A tibble: 2 × 4
  .model     RMSE   MAE  MAPE
  <chr>     <dbl> <dbl> <dbl>
1 HW        0.613 0.450  5.15
2 HW_damped 0.616 0.444  5.06
glance(fit) |> select(.model, AICc)
# A tibble: 2 × 2
  .model     AICc
  <chr>     <dbl>
1 HW        1776.
2 HW_damped 1783.

The one step RMSE is almost the same, 0.6130 for HW and 0.6155 for HW_damped, a gap of 0.0025. HW also has the lower AICc (1776.27 vs 1782.67), while the damped model is marginally better on MAE (0.4444 vs 0.4497) and MAPE (5.06 vs 5.15). Since the question is about RMSE and AICc agrees, I prefer HW.

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

best <- accuracy(fit) |> slice_min(RMSE) |> pull(.model)

fit |> select(all_of(best)) |> gg_tsresiduals()

augment(fit) |>
  filter(.model == best) |>
  features(.innov, ljung_box, lag = 24)
# A tibble: 1 × 5
  State              Industry                           .model lb_stat lb_pvalue
  <chr>              <chr>                              <chr>    <dbl>     <dbl>
1 Northern Territory Clothing, footwear and personal a… HW        34.5    0.0765

The residuals are centered on 0 with no trend and a fairly steady spread, and the histogram is close to normal with a slight right tail. In the ACF almost every lag sits inside the blue bounds. Only lag 12 ( ~0.14) clearly crosses the bound, while lags 8 and 18 just touch it at ~ -0.11 and -0.10. With 24 lags we expect ~ 1 to cross by chance (24 × 0.05 = 1.2), so the residuals are close to white noise. The spike at lag 12 points to a small yearly pattern the model has not fully captured.

e. Now find the test set RMSE, while training the model to the end of 2010. Can you beat the seasonal naive approach from Exercise 7 in Section 5.11?

myseries_train <- myseries |>
  filter(year(Month) < 2011)

fit_train <- myseries_train |>
  model(
    SNAIVE = SNAIVE(Turnover),
    HW = ETS(Turnover ~ error("M") + trend("A") + season("M")),
    HW_damped = ETS(Turnover ~ error("M") + trend("Ad") + season("M"))
  )

fc_train <- fit_train |>
  forecast(new_data = anti_join(myseries, myseries_train))
Joining with `by = join_by(State, Industry, `Series ID`, Month, Turnover)`
fc_train |>
  accuracy(myseries) |>
  select(.model, RMSE, MAE, MAPE)
# A tibble: 3 × 4
  .model     RMSE   MAE  MAPE
  <chr>     <dbl> <dbl> <dbl>
1 HW         1.78 1.60  12.6 
2 HW_damped  1.15 0.878  6.45
3 SNAIVE     1.55 1.24   9.06

On the test set HW_damped is the clear winner with an RMSE of 1.151, vs 1.552 for seasonal naive and 1.783 for HW. HW_damped beats seasonal naive by 0.401 (1.552 - 1.151), ~ 26% lower, so yes it can be beaten. The undamped HW is worse than seasonal naive by 0.231 (1.783 - 1.552). Its additive trend keeps the full slope through the whole test period, while the damped trend flattens, which suggests the series grew less after 2010 than the undamped trend assumed. It also shows that the better model in sample (HW) was not the better forecaster.

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?

lambda <- myseries_train |>
  features(Turnover, features = guerrero) |>
  pull(lambda_guerrero)

lambda
[1] 0.1971431

Lambda is ~ 0.197, between a log (0) and a square root (0.5) transformation.

fit_stl <- myseries_train |>
  model(
    STL_ETS = decomposition_model(
      STL(box_cox(Turnover, lambda) ~ season(window = "periodic"),
          robust = TRUE),
      ETS(season_adjust ~ season("N"))
    )
  )

fit_stl |> gg_tsresiduals()
Warning: Removed 12 rows containing missing values or values outside the scale range
(`geom_line()`).
Warning: Removed 12 rows containing missing values or values outside the scale range
(`geom_point()`).
Warning: Removed 12 rows containing non-finite outside the scale range
(`stat_bin()`).
Warning: Removed 12 rows containing missing values or values outside the scale range
(`geom_rug()`).

fc_stl <- fit_stl |>
  forecast(new_data = anti_join(myseries, myseries_train))
Joining with `by = join_by(State, Industry, `Series ID`, Month, Turnover)`
fc_stl |> autoplot(myseries |> filter(year(Month) >= 2008))

bind_rows(fc_train, fc_stl) |>
  accuracy(myseries) |>
  select(.model, RMSE, MAE, MAPE) |>
  arrange(RMSE)
# A tibble: 4 × 4
  .model     RMSE   MAE  MAPE
  <chr>     <dbl> <dbl> <dbl>
1 HW_damped  1.15 0.878  6.45
2 SNAIVE     1.55 1.24   9.06
3 HW         1.78 1.60  12.6 
4 STL_ETS    4.24 3.81  29.4 

The Box Cox step handles the growing seasonal swings, STL pulls the seasonal part out, and ETS forecasts only the seasonally adjusted data. Even so, STL with ETS does much worse than the earlier models, with a test RMSE of 4.236 vs 1.151 for HW_damped, 1.552 for seasonal naive and 1.783 for HW. That is ~ 3.7 times the error of the best model (4.236 /1.151 = 3.68), and the MAPE is 29.4% vs 6.4%. One likely reason is the back transformation: with lambda at 0.197 the original scale is the transformed value raised to about the power 1 / 0.197 = 5.1, so even a small trend in the ETS forecast gets magnified when it is converted back to turnover. The best forecasts are still the HW_damped ones.