Complete and submit exercises 8.1, 8.5, 8.6, 8.7, 8.8 and 8.9 for Homework 5 from the Hyndman online Forecasting book. We have to submit both our Rpubs link as well as attach the .pdf file with our code.
Homework 3 fit benchmark methods that carry forward a single number or a straight line. This homework moves to exponential smoothing, which is the first family of methods in the book that actually reacts to how the series has been behaving recently rather than just repeating the past.
The core idea across every method in this chapter is a weighted average of past observations, where the weights decay exponentially the further back in time they go. Simple exponential smoothing (SES) does this for a level only, and is appropriate for series with no trend and no season. Holt’s method adds a second smoothing equation for the trend. Holt-Winters adds a third for the season. Each additional component lets the model track one more feature of the data, at the cost of one more smoothing parameter to estimate.
Three ideas run through the exercises below.
The first is that the smoothing parameters, alpha for the level, beta
for the trend, and gamma for the season, are not something I pick by
eye. ETS() estimates them, together with the initial
states, by maximum likelihood. For models with additive errors that is
equivalent to minimizing the sum of squared one-step errors, the same
way a regression coefficient is estimated; for multiplicative-error
models the likelihood weights the errors relative to the level instead.
A value of alpha near 1 means the model trusts the most recent
observation almost completely and barely remembers anything older. A
value near 0 means the opposite, the model barely moves and mostly
repeats its previous level.
The second is damping. An un-damped trend, whether additive or multiplicative, gets extended in a straight line or an exponential curve forever, which becomes unrealistic at long horizons. A damped trend flattens out gradually, which is usually a more honest forecast for anything beyond the very near term.
The third is that ETS is not one model but a state space framework
covering additive and multiplicative versions of error, trend and
season. Letting ETS() search over that space and pick the
best model by AICc is convenient, but it is still worth checking that
the chosen model matches what the series actually looks like, rather
than trusting the automatic selection blindly.
All of the data for this homework comes from packages, so nothing needs to be downloaded into the working directory. Only one package is required:
Question. Consider the number of pigs slaughtered in
New South Wales (data set aus_livestock).
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.My thought process. Part (a) is mechanical: filter
to the right series, fit
ETS(Count ~ error("A") + trend("N") + season("N")), which
is the state space equivalent of SES, and read off the parameters. Part
(b) asks me to reproduce the prediction interval by hand, which means
pulling the residual standard deviation out of the fitted model and
comparing my by-hand number against what hilo() returns.
The two should be close but not necessarily identical, and the gap is
worth explaining rather than ignoring.
nsw_pigs <- aus_livestock |>
filter(State == "New South Wales", Animal == "Pigs")
nsw_pigs |>
autoplot(Count) +
labs(title = "Pigs slaughtered in New South Wales",
x = "Month", y = "Count")
fit_pigs <- nsw_pigs |>
model(SES = ETS(Count ~ error("A") + trend("N") + season("N")))
report(fit_pigs)
## Series: Count
## Model: ETS(A,N,N)
## Smoothing parameters:
## alpha = 0.2655419
##
## Initial states:
## l[0]
## 107861.1
##
## sigma^2: 129400339
##
## AIC AICc BIC
## 13955.55 13955.59 13968.52
fc_pigs <- fit_pigs |>
forecast(h = 4)
fc_pigs |>
autoplot(nsw_pigs |> filter(year(Month) >= 2010)) +
labs(title = "NSW pig slaughter, 4 month SES forecast",
x = "Month", y = "Count")
fc_pigs |>
as_tibble() |>
select(Month, .mean)
## # A tibble: 4 × 2
## Month .mean
## <mth> <dbl>
## 1 2019 Jan 73470.
## 2 2019 Feb 73470.
## 3 2019 Mar 73470.
## 4 2019 Apr 73470.
Reading the fit. The optimal alpha is about 0.266 and the estimated initial level l0 is about 107,861. An alpha of 0.27 is moderate-to-low: the model gives real weight to recent months but still keeps a good amount of memory of the earlier level rather than chasing the most recent value alone. Because this is SES with no trend or season term, every one of the four forecasts is the same flat number, about 73,470, which is the final estimated level.
resid_pigs <- residuals(fit_pigs)$.resid
s <- sd(resid_pigs, na.rm = TRUE)
yhat1 <- fc_pigs$.mean[1]
by_hand_lower <- yhat1 - 1.96 * s
by_hand_upper <- yhat1 + 1.96 * s
sigma_pigs <- sqrt(glance(fit_pigs)$sigma2)
c(point_forecast = yhat1, s = s, sigma_model = sigma_pigs,
by_hand_lower = by_hand_lower, by_hand_upper = by_hand_upper)
## point_forecast s sigma_model by_hand_lower by_hand_upper
## 73470.44 11362.84 11375.43 51199.28 95741.60
fc_pigs |>
hilo(level = 95) |>
as_tibble() |>
slice(1) |>
select(Month, .mean, `95%`)
## # A tibble: 1 × 3
## Month .mean `95%`
## <mth> <dbl> <hilo>
## 1 2019 Jan 73470. [51175.01, 95765.87]95
Comparison. The residual standard deviation is about
11,363, so the by-hand 95% interval for the first forecast comes out to
roughly 51,199 to 95,742. R’s own interval from hilo() is
roughly 51,175 to 95,766, essentially the same bounds, off by only a few
tens of units on each side.
The small gap has a precise source. For SES the one-step forecast
variance is exactly the innovation variance \(\sigma^2\), so R’s interval is also \(\hat{y} \pm 1.96\hat{\sigma}\). The only
difference is how \(\hat{\sigma}\) is
estimated: sd() divides the sum of squared residuals by
\(T - 1\), while the model’s
sigma^2 divides by \(T\)
minus the number of estimated parameters (alpha and l0). That gives
\(\hat{\sigma} \approx\) 11,375 instead
of 11,363, and plugging that into \(\hat{y}
\pm 1.96\hat{\sigma}\) reproduces R’s interval. So the 1.96s
shortcut is not an approximation R is quietly correcting; it is how the
interval is built, with a slightly different degrees-of-freedom
adjustment.
Question. Data set global_economy
contains the annual Exports from many countries. Select one country to
analyse.
My thought process. I continue with Australia, the same country I have used in earlier homeworks, so the results stay comparable to the plots I have already discussed. The exercise is really asking me to check whether adding a trend term is worth the extra parameter, and RMSE on the training data plus a look at the forecasts is exactly how to check that.
aus_exports <- global_economy |>
filter(Country == "Australia") |>
select(Year, Exports)
aus_exports |>
autoplot(Exports) +
labs(title = "Australian exports as a percentage of GDP",
x = "Year", y = "Exports (% of GDP)")
Main features. The series is annual, so there is no seasonality to discuss. There is a clear but noisy upward trend across the full 1960 to 2017 span, from roughly 13 percent of GDP to roughly 21 percent, and it is not a clean straight line. Exports fall through the late 1960s, climb through the 1970s, 80s and 90s, then drop sharply in the early 2000s (from about 22 percent around 2001 to about 17 percent a few years later). They recover to a spike of about 23 percent around 2008, pull back during the financial crisis, drift down through the mid-2010s, and tick up again in the final year. There is no obvious structural break or single outlier, just a rising level with multi-year swings around it.
fit_ann <- aus_exports |>
model(ANN = ETS(Exports ~ error("A") + trend("N") + season("N")))
report(fit_ann)
## Series: Exports
## Model: ETS(A,N,N)
## Smoothing parameters:
## alpha = 0.5659948
##
## Initial states:
## l[0]
## 12.98943
##
## sigma^2: 1.3621
##
## AIC AICc BIC
## 257.3943 257.8387 263.5756
fit_ann |>
forecast(h = 10) |>
autoplot(aus_exports) +
labs(title = "Australian exports, ETS(A,N,N) 10 year forecast",
x = "Year", y = "Exports (% of GDP)")
The fitted alpha is about 0.566, a fairly high value that leans heavily on recent observations. Since this model has no trend term, the forecast is a flat line at the final estimated level, about 20.6 percent of GDP.
accuracy(fit_ann) |>
select(.model, RMSE)
## # A tibble: 1 × 2
## .model RMSE
## <chr> <dbl>
## 1 ANN 1.15
fit_aan <- aus_exports |>
model(AAN = ETS(Exports ~ error("A") + trend("A") + season("N")))
report(fit_aan)
## Series: Exports
## Model: ETS(A,A,N)
## Smoothing parameters:
## alpha = 0.4407571
## beta = 1e-04
##
## Initial states:
## l[0] b[0]
## 12.74993 0.1371607
##
## sigma^2: 1.3395
##
## AIC AICc BIC
## 258.3124 259.4662 268.6146
accuracy(fit_aan) |>
select(.model, RMSE)
## # A tibble: 1 × 2
## .model RMSE
## <chr> <dbl>
## 1 AAN 1.12
bind_rows(
accuracy(fit_ann) |> select(.model, RMSE),
accuracy(fit_aan) |> select(.model, RMSE)
)
## # A tibble: 2 × 2
## .model RMSE
## <chr> <dbl>
## 1 ANN 1.15
## 2 AAN 1.12
Was adding a trend a good idea? Only marginally, and arguably not once the extra parameters are accounted for. The RMSE drops slightly, from about 1.147 for ANN to about 1.117 for AAN. Because ANN is nested inside AAN (set the slope and beta to zero), adding the trend term will normally lower the training RMSE whether or not the trend is actually earning its place, so a small drop is expected. The AIC, which penalizes the extra parameters, says this is not a clear win: ANN’s AIC (about 257.4) is lower, meaning better, than AAN’s (about 258.3).
The fitted beta for AAN is essentially zero (0.0001). That means the slope is never updated; AAN is effectively ANN plus a fixed drift of about 0.137 percentage points per year estimated from the start of the sample. So the small RMSE improvement on the training set is not enough to justify the added complexity.
aus_exports |>
model(ANN = ETS(Exports ~ error("A") + trend("N") + season("N")),
AAN = ETS(Exports ~ error("A") + trend("A") + season("N"))) |>
forecast(h = 15) |>
autoplot(aus_exports, level = NULL) +
labs(title = "Australian exports, ANN vs AAN 15 year forecasts",
x = "Year", y = "Exports (% of GDP)")
Which is best? ANN forecasts a flat line at roughly 20.6 percent forever. AAN forecasts a gentle continued rise, starting around 20.8 percent and climbing steadily, because it carries forward the fixed drift it estimated from the long-run history and extends it in a straight line indefinitely.
I prefer ANN here. The training RMSE gap is small and the AIC favors ANN once AAN’s extra parameters are penalized, so AAN is not clearly earning its extra complexity. An un-damped linear trend extended 15 years into the future is also exactly the kind of forecast that looks overconfident once you get past the first few years. Exports as a share of GDP cannot rise forever, and the series itself has already shown it can reverse direction for several years at a time, as it did in the late 1960s and the early 2000s. A flat forecast is the more conservative and, I think, the more defensible choice at this horizon. If I did want to keep a trend, a damped version, ETS(A,Ad,N), would be the better compromise, since it would capture the upward drift without committing to it running in a straight line for 15 years.
rmse_ann <- accuracy(fit_ann)$RMSE
rmse_aan <- accuracy(fit_aan)$RMSE
fc_ann1 <- fit_ann |> forecast(h = 1)
fc_aan1 <- fit_aan |> forecast(h = 1)
by_hand <- tibble(
model = c("ANN", "AAN"),
point_forecast = c(fc_ann1$.mean, fc_aan1$.mean),
rmse = c(rmse_ann, rmse_aan)
) |>
mutate(lower = point_forecast - 1.96 * rmse,
upper = point_forecast + 1.96 * rmse)
by_hand
## # A tibble: 2 × 5
## model point_forecast rmse lower upper
## <chr> <dbl> <dbl> <dbl> <dbl>
## 1 ANN 20.6 1.15 18.4 22.9
## 2 AAN 20.8 1.12 18.6 23.0
bind_rows(
fc_ann1 |> hilo(level = 95) |> as_tibble() |> mutate(model = "ANN"),
fc_aan1 |> hilo(level = 95) |> as_tibble() |> mutate(model = "AAN")
) |>
select(model, Year, .mean, `95%`)
## # A tibble: 2 × 4
## model Year .mean `95%`
## <chr> <dbl> <dbl> <hilo>
## 1 ANN 2018 20.6 [18.31970, 22.89462]95
## 2 AAN 2018 20.8 [18.57028, 23.10700]95
Comparison. For ANN, the by-hand interval is roughly
18.36 to 22.85, and R’s interval is roughly 18.32 to 22.89. For AAN the
by-hand interval is roughly 18.65 to 23.03, against R’s roughly 18.57 to
23.11. Both pairs are close but not identical, and R’s are slightly
wider in both cases. The gap comes from the same source as in Exercise
8.1. RMSE divides the sum of squared errors by the number of
observations, while the model’s sigma^2 divides by the
number of observations minus the number of estimated parameters, so it
is a little larger. The gap is bigger for AAN because it estimates more
parameters. The agreement is close enough to confirm the 1.96 times RMSE
shortcut is a reasonable approximation, with R applying a
degrees-of-freedom correction on top.
Question. Forecast the Chinese GDP from the
global_economy data set using an ETS model. Experiment with
the trend and damped parameters, and take a Box-Cox transformation of
the data first. Model the logarithmic growth rate. Compare your
forecasts with the actual growth rate.
My thought process. China’s GDP is the textbook case for why a Box-Cox transformation matters: it grows so fast, and the growth compounds so strongly, that additive-trend methods on the raw series cannot follow its shape. The exercise wants me to try several ETS specifications side by side and see which handles that acceleration most sensibly. I fit an un-damped trend, a damped trend, and versions on both the raw series and a Box-Cox transformed series so the effect of the transformation is visible on its own.
china_gdp <- global_economy |>
filter(Country == "China") |>
select(Year, GDP)
china_gdp |>
autoplot(GDP) +
labs(title = "Chinese GDP, current US dollars",
x = "Year", y = "GDP")
The series is not just trending up, it is accelerating: growth from 2000 onward dwarfs anything before it. That curvature is exactly what a plain additive trend struggles with.
lambda_china <- china_gdp |>
features(GDP, features = guerrero) |>
pull(lambda_guerrero)
lambda_china
## [1] -0.03446284
fit_china <- china_gdp |>
model(
ETS_AAN = ETS(GDP ~ error("A") + trend("A") + season("N")),
ETS_AAdN = ETS(GDP ~ error("A") + trend("Ad") + season("N")),
ETS_BoxCox_AAN = ETS(box_cox(GDP, lambda_china) ~
error("A") + trend("A") + season("N")),
ETS_BoxCox_AAdN = ETS(box_cox(GDP, lambda_china) ~
error("A") + trend("Ad") + season("N"))
)
glance(fit_china) |>
select(.model, AIC, AICc, BIC)
## # A tibble: 4 × 4
## .model AIC AICc BIC
## <chr> <dbl> <dbl> <dbl>
## 1 ETS_AAN 3258. 3259. 3268.
## 2 ETS_AAdN 3260. 3262. 3273.
## 3 ETS_BoxCox_AAN -139. -137. -128.
## 4 ETS_BoxCox_AAdN -135. -133. -123.
The information criteria can only be compared within a scale. The two raw-GDP models are fitted to dollars and the two Box-Cox models to transformed values, so the gap between roughly 3,260 and roughly -137 says nothing about which pair is better. Within each pair, the un-damped model has the slightly lower AICc.
fc_china <- fit_china |>
forecast(h = 20)
fc_china |>
autoplot(china_gdp, level = NULL) +
labs(title = "Chinese GDP, 20 year ETS forecasts",
x = "Year", y = "GDP")
last_gdp <- last(china_gdp$GDP)
implied_growth <- fc_china |>
as_tibble() |>
group_by(.model) |>
summarise(gdp_2037 = last(.mean)) |>
mutate(multiple_of_2017 = gdp_2037 / last_gdp,
avg_log_growth = (log(gdp_2037) - log(last_gdp)) / 20)
implied_growth
## # A tibble: 4 × 4
## .model gdp_2037 multiple_of_2017 avg_log_growth
## <chr> <dbl> <dbl> <dbl>
## 1 ETS_AAN 2.74e13 2.24 0.0404
## 2 ETS_AAdN 2.45e13 2.00 0.0347
## 3 ETS_BoxCox_AAN 2.24e14 18.3 0.145
## 4 ETS_BoxCox_AAdN 5.11e13 4.18 0.0715
g_boxcox_aan <- implied_growth |> filter(.model == "ETS_BoxCox_AAN") |> pull(avg_log_growth)
g_boxcox_aadn <- implied_growth |> filter(.model == "ETS_BoxCox_AAdN") |> pull(avg_log_growth)
g_raw_aan <- implied_growth |> filter(.model == "ETS_AAN") |> pull(avg_log_growth)
m_boxcox_aan <- implied_growth |> filter(.model == "ETS_BoxCox_AAN") |> pull(multiple_of_2017)
Effect of damping and the Box-Cox transformation. The four forecasts fan out in a way that is the opposite of what I first expected.
The two raw-scale models are the most conservative. A linear trend on the dollar scale adds a fixed dollar amount every year, so from 2018 onward both lines grow in a straight line and sit almost on top of each other. The damping parameter barely changes anything over 20 years. Because GDP compounds, a straight line in dollars means the implied growth rate keeps shrinking. The raw AAN path averages only about 4 percent log growth a year, well below both the full-sample average and the average since 2008 (shown below). So these models under-forecast rather than over-forecast.
The Box-Cox transformation changes the picture completely. With a lambda of -0.034, the transformation is very close to a log, and an additive trend on the log scale is a constant percentage growth rate on the original scale. That matches how GDP behaves, because economic growth compounds. But the un-damped Box-Cox AAN model takes the rapid growth of the last few decades and compounds it forward for 20 years without limit. It averages about 14.5 percent log growth a year and ends at roughly 18 times the 2017 level. That is not a believable forecast for an economy that is already one of the largest in the world.
The damped Box-Cox model sits in between. It keeps the compounding shape but lets the growth rate fade, averaging about 7.1 percent a year. Of the four, this is the forecast I find most defensible: the transformation gets the shape right and damping stops the growth rate from being projected forward unchanged forever.
china_growth <- china_gdp |>
mutate(log_gdp = log(GDP),
growth_rate = difference(log_gdp)) |>
filter(!is.na(growth_rate))
china_growth |>
autoplot(growth_rate) +
labs(title = "Chinese GDP, log growth rate (first difference of log GDP)",
x = "Year", y = "Log growth rate")
fit_growth <- china_growth |>
model(ETS_growth = ETS(growth_rate ~ error("A") + trend("N") + season("N")))
report(fit_growth)
## Series: growth_rate
## Model: ETS(A,N,N)
## Smoothing parameters:
## alpha = 0.1088357
##
## Initial states:
## l[0]
## 0.04310116
##
## sigma^2: 0.0087
##
## AIC AICc BIC
## -36.33972 -35.88689 -30.21057
fc_growth <- fit_growth |>
forecast(h = 10)
fc_growth |>
autoplot(china_growth) +
labs(title = "Forecast of Chinese log GDP growth rate",
x = "Year", y = "Log growth rate")
g_forecast <- fc_growth$.mean[1]
g_hist <- mean(china_growth$growth_rate)
g_recent <- china_growth |> filter(Year >= 2008) |> pull(growth_rate) |> mean()
c(growth_forecast = g_forecast, historical_mean = g_hist, mean_since_2008 = g_recent)
## growth_forecast historical_mean mean_since_2008
## 0.11552989 0.09338016 0.12369592
Comparing forecasts with the actual growth rate. The historical log growth rate is noisy and trendless, swinging between roughly -0.18 and 0.26 with no clear direction across the sample. It includes sharp drops around 1961 and 1978 and a softening in the mid-2010s. An SES model on the growth rate forecasts a flat line at about 0.116, against a full-sample average of about 0.093, rather than extrapolating a trend that is not really there in the growth rate itself. The fitted alpha is small, about 0.109, so the model updates its estimate of the average growth rate only slowly in response to new data. That is appropriate for a quantity that fluctuates around a roughly stable mean rather than trending.
Lining this up against the level models makes the comparison concrete:
The actual growth rate since 2008 has averaged about 12.4 percent and has been slowing. That is another reason to distrust the un-damped Box-Cox forecast: it projects forward a growth rate the recent data no longer supports.
That is the real value of modelling the growth rate instead of the level. On the level scale, GDP looks like it has an ever-steepening trend, which tempts a model into extrapolating something explosive or, on the raw scale, into flattening it out. On the log-difference scale, the trend disappears and what is left looks close to a stable process fluctuating around an average growth rate, which is a far more defensible basis for a long-run forecast. One caveat: this series is nominal GDP in current US dollars, so its growth rate mixes real growth with inflation and exchange-rate movements.
Question. Find an ETS model for the Gas data from
aus_production. Why is multiplicative seasonality necessary
here? Experiment with making the trend damped. Does it improve the
forecasts?
My thought process. Gas production has a seasonal
swing that grows as the overall level of the series grows, which is
exactly the signature that calls for multiplicative rather than additive
seasonality. I check that visually first, then let ETS()
confirm it, then compare damped and un-damped trend versions on accuracy
and on how sensible the long-run forecast looks.
gas <- aus_production |>
select(Quarter, Gas)
gas |>
autoplot(Gas) +
labs(title = "Australian quarterly gas production",
x = "Quarter", y = "Gas production (petajoules)")
Why multiplicative seasonality. The seasonal swings are small and roughly constant in the early part of the series, then grow visibly larger as production rises through the 1970s onward. When the size of the seasonal fluctuation scales with the level of the series rather than staying a fixed absolute amount, additive seasonality cannot represent it, because additive seasonal indices are constant amounts added on top of the trend regardless of how high the series has climbed. Multiplicative seasonality instead expresses the season as a percentage of the current level, so the swing automatically grows as the series grows. That is the pattern visible in this plot.
fit_gas <- gas |>
model(
ETS_auto = ETS(Gas),
ETS_MAM = ETS(Gas ~ error("M") + trend("A") + season("M")),
ETS_MAdM = ETS(Gas ~ error("M") + trend("Ad") + season("M"))
)
glance(fit_gas) |>
select(.model, AIC, AICc, BIC)
## # A tibble: 3 × 4
## .model AIC AICc BIC
## <chr> <dbl> <dbl> <dbl>
## 1 ETS_auto 1681. 1682. 1711.
## 2 ETS_MAM 1681. 1682. 1711.
## 3 ETS_MAdM 1684. 1685. 1718.
accuracy(fit_gas) |>
select(.model, RMSE, MAPE, MASE)
## # A tibble: 3 × 4
## .model RMSE MAPE MASE
## <chr> <dbl> <dbl> <dbl>
## 1 ETS_auto 4.60 4.08 0.542
## 2 ETS_MAM 4.60 4.08 0.542
## 3 ETS_MAdM 4.59 4.10 0.544
fit_gas |>
select(ETS_auto) |>
report()
## 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
ETS() run with no restrictions selects ETS(M,A,M) on its
own, confirming that multiplicative error and multiplicative season are
what the automatic search prefers, matching the visual read above.
fit_gas |>
select(ETS_MAdM) |>
report()
## Series: Gas
## Model: ETS(M,Ad,M)
## Smoothing parameters:
## alpha = 0.6489044
## beta = 0.1551275
## gamma = 0.09369372
## phi = 0.98
##
## Initial states:
## l[0] b[0] s[0] s[-1] s[-2] s[-3]
## 5.858941 0.09944006 0.9281912 1.177903 1.07678 0.8171255
##
## sigma^2: 0.0033
##
## AIC AICc BIC
## 1684.028 1685.091 1717.873
phi_gas <- tidy(fit_gas) |>
filter(.model == "ETS_MAdM", term == "phi") |>
pull(estimate)
fit_gas |>
forecast(h = 12) |>
autoplot(gas |> filter(year(Quarter) >= 2000), level = NULL) +
labs(title = "Gas production, damped vs un-damped ETS forecasts",
x = "Quarter", y = "Gas production (petajoules)")
Does damping improve the forecasts? Not on this series. In-sample accuracy is essentially tied: RMSE, MAPE and MASE differ only in the second or third decimal place. The information criteria actually lean the other way. MAM has the lower AIC, AICc and BIC by a few points, so once the extra damping parameter is penalized, the un-damped model is preferred.
The estimated damping parameter explains why the two forecasts are so similar: phi is about 0.98. That is consistent with the plot of the series, which does not show the trend slowing down through the sample, so there is little in the data pushing phi away from 1. When phi is that close to 1, the damped model behaves almost exactly like its un-damped counterpart over a short horizon, and the two forecast lines in the plot nearly overlap.
Damping would matter more at a longer horizon, where an un-damped
trend keeps extending while a damped one gradually flattens. For this
data and a 12-quarter horizon, though, damping does not improve the
forecasts, and the AICc favors the un-damped MAM model that
ETS() chose automatically. I would only reach for the
damped version as a deliberate hedge if I needed forecasts many years
out.
Question. Recall your retail time series data (from Exercise 7 in Section 2.10).
My thought process. This is the same NSW takeaway food series from Homeworks 1, 2 and 3, drawn with the same seed so the results stay comparable. The exercise walks through the same damped-vs-not comparison as 8.7 but now on a series I already know has both a strong trend and a growing seasonal swing, and it adds a genuine test set comparison against the SNAIVE benchmark from Homework 3, which is the sharpest check of all: does exponential smoothing actually forecast better, not just fit better.
set.seed(624)
myseries <- aus_retail |>
filter(`Series ID` == sample(aus_retail$`Series ID`, 1))
myseries |>
as_tibble() |>
distinct(State, Industry, `Series ID`)
## # A tibble: 1 × 3
## State Industry `Series ID`
## <chr> <chr> <chr>
## 1 New South Wales Takeaway food services A3349792X
myseries |>
autoplot(Turnover) +
labs(title = "NSW takeaway food services turnover",
x = "Month", y = "Turnover ($ million)")
Same reasoning as the gas series in Exercise 8.7. Turnover has both a strong upward trend and a December peak that grows in absolute size as the overall level of the series rises: the seasonal swing in the 1980s is a small fraction of the swing visible by the 2010s. Since the seasonal amplitude scales with the level rather than staying fixed, only multiplicative seasonality can represent it correctly.
fit_hw <- myseries |>
model(
HW_MAM = ETS(Turnover ~ error("M") + trend("A") + season("M")),
HW_MAdM = ETS(Turnover ~ error("M") + trend("Ad") + season("M"))
)
fit_hw |>
select(HW_MAM) |>
report()
## Series: Turnover
## Model: ETS(M,A,M)
## Smoothing parameters:
## alpha = 0.7150896
## beta = 0.03088603
## gamma = 0.1055336
##
## Initial states:
## l[0] b[0] s[0] s[-1] s[-2] s[-3] s[-4] s[-5]
## 82.38423 -0.8137261 1.006568 0.9563809 1.061494 1.080698 0.9912616 0.9966824
## s[-6] s[-7] s[-8] s[-9] s[-10] s[-11]
## 0.961185 0.9961148 0.9945169 0.9640538 1.001246 0.9897988
##
## sigma^2: 0.0017
##
## AIC AICc BIC
## 4634.521 4635.968 4704.035
fit_hw |>
select(HW_MAdM) |>
report()
## Series: Turnover
## Model: ETS(M,Ad,M)
## Smoothing parameters:
## alpha = 0.7751114
## beta = 0.007524738
## gamma = 0.09682012
## phi = 0.9799997
##
## Initial states:
## l[0] b[0] s[0] s[-1] s[-2] s[-3] s[-4] s[-5]
## 83.44206 0.2885412 1.011822 0.9520354 1.055695 1.079874 0.9936296 1.000433
## s[-6] s[-7] s[-8] s[-9] s[-10] s[-11]
## 0.9745973 0.9915392 0.9945614 0.9683761 0.9932686 0.9841682
##
## sigma^2: 0.0017
##
## AIC AICc BIC
## 4625.238 4626.859 4698.841
fit_hw |>
forecast(h = "3 years") |>
autoplot(myseries |> filter(year(Month) >= 2005), level = NULL) +
labs(title = "NSW takeaway turnover, Holt-Winters damped vs un-damped",
x = "Month", y = "Turnover ($ million)")
accuracy(fit_hw) |>
select(.model, RMSE, MAPE, MASE)
## # A tibble: 2 × 4
## .model RMSE MAPE MASE
## <chr> <dbl> <dbl> <dbl>
## 1 HW_MAM 9.74 3.08 0.308
## 2 HW_MAdM 9.73 3.03 0.307
Which do I prefer? The two are essentially tied on the training data: RMSE of about 9.74 for HW_MAM against 9.73 for HW_MAdM, with MAPE and MASE equally close. Unlike the gas series, the information criteria here favor the damped model: HW_MAdM’s AIC is about 4625 against about 4634 for HW_MAM, so the damping parameter earns its place even after the penalty. I prefer HW_MAdM. It fits at least as well, the AIC supports it, and it protects against overconfident straight-line extrapolation at longer horizons.
fit_hw |>
select(HW_MAdM) |>
gg_tsresiduals()
augment(fit_hw) |>
filter(.model == "HW_MAdM") |>
features(.innov, ljung_box, lag = 24)
## # A tibble: 1 × 5
## State Industry .model lb_stat lb_pvalue
## <chr> <chr> <chr> <dbl> <dbl>
## 1 New South Wales Takeaway food services HW_MAdM 25.3 0.392
Do the residuals look like white noise? Yes. The residual time plot shows roughly constant spread with no strong pattern, and the ACF sits almost entirely inside the significance bounds, with only an isolated spike or two of the kind expected by chance across 24 lags. The Ljung-Box test at lag 24 gives a statistic of about 25.3 with a p-value of about 0.39, well above 0.05. That means there is no significant evidence against the residuals being uncorrelated. This is a real contrast with the seasonal naive residuals from Homework 3, which showed strong, undecaying autocorrelation at almost every lag and a Ljung-Box p-value indistinguishable from zero. Holt-Winters, by explicitly modelling level, trend and multiplicative season together, has captured essentially all of the structure that seasonal naive left behind in its residuals.
myseries_train <- myseries |> filter(year(Month) < 2011)
myseries_test <- myseries |> filter(year(Month) >= 2011)
fit_compare <- myseries_train |>
model(
SNAIVE = SNAIVE(Turnover),
HW_MAM = ETS(Turnover ~ error("M") + trend("A") + season("M")),
HW_MAdM = ETS(Turnover ~ error("M") + trend("Ad") + season("M"))
)
fc_compare <- fit_compare |>
forecast(new_data = myseries_test)
fc_compare |>
autoplot(myseries, level = NULL) +
labs(title = "NSW takeaway turnover, test period: SNAIVE vs Holt-Winters",
x = "Month", y = "Turnover ($ million)")
accuracy(fc_compare, myseries) |>
select(.model, RMSE, MAE, MAPE, MASE)
## # A tibble: 3 × 5
## .model RMSE MAE MAPE MASE
## <chr> <dbl> <dbl> <dbl> <dbl>
## 1 HW_MAM 70.4 60.9 15.3 3.17
## 2 HW_MAdM 69.0 62.8 14.7 3.27
## 3 SNAIVE 96.8 79.5 16.3 4.14
Can Holt-Winters beat seasonal naive? Yes, though the margin on the test set is more modest than the training fit alone would suggest. In Homework 3, SNAIVE trained through 2010 gave a test RMSE of 96.8 on this exact split. HW_MAM comes in at 70.4 and HW_MAdM at 69.0, roughly 28 to 29 percent lower RMSE. MASE is around 3.2 for both against SNAIVE’s 4.14, and MAE is about 61 to 63 against 79.5. MAPE improves too, but only modestly (14.7 and 15.3 against 16.3). The two Holt-Winters models trade places depending on the measure: the un-damped model has the lower MAE (60.9 vs 62.8), while the damped model has the lower RMSE and MAPE (69.0 and 14.7 vs 70.4 and 15.3). Both beat the benchmark on every measure, clearly on RMSE, MAE and MASE.
This is the payoff for the extra complexity in this chapter, even if it is smaller than the near-perfect training residuals might suggest. SNAIVE only ever repeats last year’s same month, so on a series with a strong ongoing trend it falls further and further behind as the forecast horizon grows, which is exactly the failure documented in Homework 3. Holt-Winters explicitly models the trend, so it keeps climbing along with the actual series instead of forecasting a flat repeat of 2010, and it models the multiplicative season on top of that climbing level, keeping the December peaks in roughly the right proportion. The gap between the excellent training fit (RMSE under 10) and the much larger test RMSE (around 70) is also a useful reminder from Homework 3’s lesson: training accuracy is not test accuracy, and an eight year-ahead forecast is a genuinely harder problem than the one-step-ahead errors the model was fitted on, even for a model this much better specified than a naive benchmark.
Question. 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 this compare with your best previous forecasts on the test set?
My thought process. This combines two ideas from
earlier in the book: the Box-Cox transformation from Homework 2 and STL
decomposition, together with the ETS approach just used in Exercise 8.8.
decomposition_model() handles the mechanics: it
STL-decomposes the Box-Cox transformed series, fits an ETS model to only
the seasonally adjusted component, and forecasts the seasonal component
separately (with a seasonal naive method) before adding it back in. I
fit it on the same pre-2011 training window so the test set RMSE lines
up directly against the SNAIVE and Holt-Winters numbers from part (e). I
first let ETS() choose the error and trend for the
seasonally adjusted series, then try a damped trend version for
comparison.
lambda_retail <- myseries_train |>
features(Turnover, features = guerrero) |>
pull(lambda_guerrero)
lambda_retail
## [1] -0.1011142
stl_spec <- STL(box_cox(Turnover, lambda_retail) ~ season(window = "periodic"),
robust = TRUE)
fit_stl_ets <- myseries_train |>
model(
STL_ETS = decomposition_model(
stl_spec,
ETS(season_adjust ~ season("N"))
),
STL_ETS_damped = decomposition_model(
stl_spec,
ETS(season_adjust ~ error("A") + trend("Ad") + season("N"))
)
)
fit_stl_ets |>
select(STL_ETS) |>
report()
## Series: Turnover
## Model: STL decomposition model
## Transformation: box_cox(Turnover, lambda_retail)
## Combination: ETS(A,A,N) + SNAIVE
##
## ================================
##
## Series: season_adjust
## Model: ETS(A,A,N)
## Smoothing parameters:
## alpha = 0.7369909
## beta = 0.0001000001
##
## Initial states:
## l[0] b[0]
## 3.587272 0.002469821
##
## sigma^2: 6e-04
##
## AIC AICc BIC
## -533.0937 -532.9167 -513.8760
##
## Series: season_year
## Model: SNAIVE
##
## sigma^2: 0
stl_report <- capture.output(fit_stl_ets |> select(STL_ETS) |> report())
stl_has_trend <- any(grepl("beta", stl_report))
stl_has_damped <- any(grepl("phi", stl_report))
fc_stl_ets <- fit_stl_ets |>
forecast(new_data = myseries_test)
fc_stl_ets |>
autoplot(myseries |> filter(year(Month) >= 2000), level = NULL) +
labs(title = "NSW takeaway turnover, STL + Box-Cox + ETS on the test period",
x = "Month", y = "Turnover ($ million)")
acc_all <- bind_rows(
accuracy(fc_compare, myseries),
accuracy(fc_stl_ets, myseries)
) |>
select(.model, RMSE, MAE, MAPE, MASE) |>
arrange(RMSE)
acc_all
## # A tibble: 5 × 5
## .model RMSE MAE MAPE MASE
## <chr> <dbl> <dbl> <dbl> <dbl>
## 1 HW_MAdM 69.0 62.8 14.7 3.27
## 2 HW_MAM 70.4 60.9 15.3 3.17
## 3 STL_ETS_damped 76.5 67.3 14.6 3.50
## 4 STL_ETS 93.9 90.2 20.9 4.69
## 5 SNAIVE 96.8 79.5 16.3 4.14
get_acc <- function(m, col) acc_all |> filter(.model == m) |> pull({{ col }})
stl_rmse <- get_acc("STL_ETS", RMSE)
stl_mae <- get_acc("STL_ETS", MAE)
stl_mape <- get_acc("STL_ETS", MAPE)
stl_mase <- get_acc("STL_ETS", MASE)
dmp_rmse <- get_acc("STL_ETS_damped", RMSE)
dmp_mase <- get_acc("STL_ETS_damped", MASE)
hw_rmse <- get_acc("HW_MAdM", RMSE)
sn_rmse <- get_acc("SNAIVE", RMSE)
sn_mase <- get_acc("SNAIVE", MASE)
How does it compare? Not well, and this is the more interesting and slightly humbling result of the two exercises. The STL plus Box-Cox plus ETS model gives a test RMSE of about 93.9, an MAE of about 90.2, a MAPE of about 20.9 and a MASE of about 4.69. That is only marginally better than SNAIVE on RMSE (96.8) and clearly worse than either Holt-Winters model from Exercise 8.8. On MAE, MAPE and MASE it is actually worse than SNAIVE. Because MASE scales every model’s errors by the same in-sample seasonal naive MAE, it always ranks models the same way MAE does, so the MASE and MAE results tell the same story: on a typical month this model misses by more than seasonal naive does. It only edges ahead on RMSE because SNAIVE’s errors include a few larger misses that RMSE punishes more heavily.
This is not the outcome I expected going in, and it is worth being honest about why rather than explaining it away. The plot shows the problem directly: the forecast does not lag behind the series, it overshoots it. The STL forecast climbs well above the actual turnover through the whole test period and ends far above where the series actually finished in 2018.
The report() output above shows the ETS model chosen for
the seasonally adjusted series, which includes an un-damped trend. The
Box-Cox lambda of -0.101 is close to zero, so the transformed series
behaves almost like a log. A linear trend on that scale becomes
compounding percentage growth back on the original scale. The model
takes the fast growth of the 2000s, including the jump in 2010 right at
the end of the training window, and compounds it forward for eight
years. Meanwhile the actual series dipped from 2011 to about 2014 before
recovering, so the forecast starts too high and the gap widens with the
horizon. A second, smaller factor is that
season(window = "periodic") fixes one seasonal shape for
the whole horizon and cannot let it drift.
The damped version tests whether reining in the trend helps. It gives a test RMSE of about 76.5 and a MASE of about 3.5, an improvement over the automatically selected model. It still does not beat the best Holt-Winters model on RMSE. Unlike the original STL model, it beats SNAIVE on every measure, and its MAPE is essentially level with the Holt-Winters models. The best forecast on this test set by RMSE remains HW_MAdM, with an RMSE of 69. The jump from the un-damped to the damped STL model is the clearest evidence in this homework that the problem was the over-extrapolated trend, not the decomposition step itself.
Overall verdict across the whole homework. The two approaches in 8.8 and 8.9 are not interchangeable, and the honest conclusion is that neither exponential smoothing method is automatically better just because it is more sophisticated than a benchmark. Holt-Winters, which estimates level, trend and multiplicative season jointly in one set of smoothing equations, clearly beat the seasonal naive floor from Homework 3, cutting test RMSE by close to 30 percent. The STL-plus-ETS pipeline with an automatically chosen ETS model barely beat the benchmark on RMSE and lost to it on MAE, MAPE and MASE, because the growth it extrapolated on a near-log scale ran well ahead of what actually happened. Damping that trend recovered most of the gap and beat the benchmark on every measure, though it still trailed Holt-Winters on RMSE. The lesson is the same one that ran through Homework 3: a model only outperforms its benchmark to the extent that it captures the real structure in the series without over-reading it, and simply layering more machinery into the forecasting pipeline does not guarantee that happens. Getting the specification right, here especially how the trend is allowed to behave over a long horizon, matters more than which general family of methods is used.