Chapter 8 moves us from decomposition into a genuinely different way of thinking about a series: instead of splitting it into trend/season/remainder up front, exponential smoothing methods build the forecast directly out of weighted averages of past observations, where the weights decay exponentially the further back in time you go (Hyndman & Athanasopoulos, 2021). This chapter makes the progression really explicit, SES handles level only, Holt adds a trend term, and Holt-Winters adds a seasonal term on top of that. It’s basically a Lego-block build-up to the full ETS framework.
For the previous three weeks I’ve worked with MAUISA (Week 1),
Victorian liquor retailing (Week 2), and vic_elec (Week 3).
For this one I wanted a series where all three methods are actually
meaningful to fit side by side, not just technically possible, but where
you’d expect real separation in performance, so I picked
aus_production’s Beer column:
quarterly Australian beer production in megalitres, 1956 Q1 through 2010
Q2 (Australian Bureau of Statistics, Cat. 8301.0.55.001). It’s NSA, like
my previous picks, and unlike MAUISA it doesn’t have the BEA-adjustment
issue that flattened out seasonality in Week 1, this series has an
unmistakable quarterly cycle (Q4 spikes, presumably tied to summer
demand) layered on top of a trend that rises through the 1970s–80s and
then gently declines as the industry consolidates.
beer <- aus_production %>%
select(Quarter, Beer) %>%
filter(!is.na(Beer))
range(beer$Quarter)
## <yearquarter[2]>
## [1] "1956 Q1" "2010 Q2"
## # Year starts on: January
beer %>%
ggplot(aes(x = Quarter, y = Beer)) +
geom_line(color = ink, linewidth = 0.5) +
theme_bc() +
labs(title = "Australian Quarterly Beer Production",
subtitle = "1956 Q1 – 2010 Q2, megalitres",
x = NULL, y = "Megalitres",
caption = bc_badge)
You can see both ingredients right away: a trend that isn’t monotonic (it turns over around the mid-1970s), and seasonal swings that stay fairly constant in amplitude over time, which is why an additive seasonal component is the right call rather than multiplicative.
I held out the last two years (8 quarters) as a test set and trained all three models on everything before that, so the accuracy comparison is genuinely out-of-sample rather than in-sample fit.
train <- beer %>% filter(Quarter <= yearquarter("2008 Q2"))
test <- beer %>% filter(Quarter > yearquarter("2008 Q2"))
nrow(train); nrow(test)
## [1] 210
## [1] 8
fit <- train %>%
model(
SES = ETS(Beer ~ error("A") + trend("N") + season("N")),
Holt = ETS(Beer ~ error("A") + trend("A") + season("N")),
HW = ETS(Beer ~ error("A") + trend("A") + season("A"))
)
fc <- fit %>% forecast(h = 8)
params <- tidy(fit) %>%
filter(term %in% c("alpha", "beta", "gamma"))
params
## # A tibble: 6 × 3
## .model term estimate
## <chr> <chr> <dbl>
## 1 SES alpha 0.150
## 2 Holt alpha 0.0774
## 3 Holt beta 0.0215
## 4 HW alpha 0.206
## 5 HW beta 0.0298
## 6 HW gamma 0.248
The three smoothing parameters each control how fast a different component of the model updates as new data arrives:
acc <- fc %>% accuracy(beer)
acc %>% select(.model, RMSE, MAE, MAPE)
## # A tibble: 3 × 4
## .model RMSE MAE MAPE
## <chr> <dbl> <dbl> <dbl>
## 1 HW 13.2 11.6 2.73
## 2 Holt 41.0 28.0 6.16
## 3 SES 38.6 28.9 6.55
fc_df <- fc %>% as_tibble()
beer %>%
filter(Quarter >= yearquarter("2000 Q1")) %>%
ggplot(aes(x = Quarter, y = Beer)) +
geom_line(color = "grey45", linewidth = 0.4) +
geom_line(data = fc_df, aes(x = Quarter, y = .mean), color = ink, linewidth = 0.7) +
facet_wrap(~.model, ncol = 1) +
theme_bc() +
labs(title = "Forecasts vs. Actuals by Method (last 8 quarters held out)",
subtitle = "Grey = actual production · Navy = point forecast",
x = NULL, y = "Megalitres",
caption = bc_badge)
acc %>%
select(.model, RMSE, MAE, MAPE) %>%
pivot_longer(-.model, names_to = "metric", values_to = "value") %>%
ggplot(aes(x = value, y = reorder(.model, value))) +
geom_segment(aes(x = 0, xend = value, yend = .model), color = "grey75") +
geom_point(color = ink, size = 3) +
facet_wrap(~metric, scales = "free_x") +
theme_bc() +
labs(title = "Out-of-Sample Accuracy by Method",
x = NULL, y = NULL,
caption = bc_badge)
The gap here is not subtle. Holt-Winters comes in around 13.2 RMSE and 2.7% MAPE on the test set, while SES sits around 38.6 RMSE and Holt around 41. That’s roughly a two-to-threefold RMSE reduction just from adding the seasonal term. What actually surprised me a little is that Holt’s linear trend doesn’t clearly beat plain SES here, on some metrics it’s essentially tied or even slightly worse. My read is that because beta is estimated so close to zero, Holt isn’t really adding much beyond SES’s level-only forecast except a small linear drift, and that drift alone can’t do anything about the fact that both methods are still trying to average over a strong quarterly cycle. It’s a nice concrete illustration of a point that also came up in my Week 3 work: a model component that isn’t actually present in the data-generating process (real seasonality here, real curvature there) doesn’t just fail to help, it can leave you worse off than a simpler model, because you’re spending “trust” on a component that isn’t earning its keep. Holt-Winters is the only one of the three whose functional form actually matches what’s in the data.
This is where I think exponential smoothing’s biggest weakness shows up. All three models here update their state (level, trend, season) through a fixed set of smoothing weights that were estimated to fit the average behavior of the training window. If beer production had, say, dropped 30% in a single quarter due to a supply shock, none of these models has any mechanism to recognize “this is a break” versus “this is normal noise”, they’d just nudge the level down a little each period, governed by whatever alpha happened to be estimated, and take several quarters to catch up. A low alpha (which is what we got for SES) makes this even slower, since the whole point of a low alpha is to not overreact to any single observation.
This is really the same lesson I ran into with COVID-19 in my final project on beer/wine/liquor retail sales: the structural break fell entirely in the test set there too, and every model, ETS included, systematically under-forecast the demand surge because nothing in the training data had prepared the smoothing parameters for a shock of that size. Exponential smoothing methods are good at tracking gradual evolution in level, trend, and season; they are not built to detect regime changes, and I don’t think there’s a version of alpha/beta/gamma tuning that fixes that without also making the model twitchy in normal periods.
A few ways I’d think about improving this for a genuinely disruption-prone series:
I’m still a little unsure where the line is between “this is worth building a dynamic regression for” and “this is rare enough that hand-adjusting after the fact is more practical”, that feels like it depends a lot on how often a given series actually gets hit with this kind of shock, which is hard to know in advance.
Hyndman, R. J., & Athanasopoulos, G. (2021). Forecasting: Principles and practice (3rd ed.). OTexts. https://otexts.com/fpp3/
Australian Bureau of Statistics. (n.d.). Manufacturing production, Australia (Cat. No. 8301.0.55.001).