STL decomposition in practice

Today let's take one real retail series and pull it apart with STL, a decomposition method flexible enough to bend where the data actually bends.

Victoria's monthly turnover for cafes, restaurants and takeaway food services is tracked by the Australian Bureau of Statistics every month. The series runs 441 months, from April 1982 to December 2018, climbing from $81.3 million a month to $1,066.2 million a month over that time.

Here is the whole series, plotted in the order the months actually happened.

The turnover keeps rising for decades, dips hard around 2008 and 2009, then keeps climbing again. Any decomposition of this series has to make three real choices: how much to smooth the trend, how much to let the yearly pattern drift, and what to do about a month like that 2008-2009 dip that does not behave like the rest. STL is the method built to make all three of those choices explicit.

What STL stands for, and what its formula computes

STL stands for Seasonal and Trend decomposition using Loess. Loess (pronounced "low-ess") fits a smooth curve through data by working point by point: at each month it fits a small weighted regression using only the months nearby, reads off the fitted value there, then slides forward and repeats. Because it never assumes one fixed shape for the whole series, it can bend where the data actually bends.

Build the Victoria series as a tsibble first, the data structure feasts expects, keeping just the month and the turnover.

RInteractive R
# Build the Victoria cafe, restaurant and takeaway turnover series as a tsibble library(tsibble) library(tsibbledata) library(dplyr) cafe <- tsibbledata::aus_retail |> filter(State == "Victoria", Industry == "Cafes, restaurants and takeaway food services") |> select(Month, Turnover) cafe #> # A tsibble: 441 x 2 [1M] #> Month Turnover #> <mth> <dbl> #> 1 1982 Apr 85.1 #> 2 1982 May 85.1 #> 3 1982 Jun 82.8 #> 4 1982 Jul 82.1 #> 5 1982 Aug 81.8 #> 6 1982 Sep 84.6 #> 7 1982 Oct 91.7 #> 8 1982 Nov 97.7 #> 9 1982 Dec 109. #> 10 1983 Jan 94.6 #> # ℹ 431 more rows

  

Fit STL with feasts' STL() function inside model(). It takes a formula: the series on the left, and on the right a trend() term that sets how many months the trend's Loess window covers, and a season() term that sets the same thing for the yearly pattern.

RInteractive R
# Fit STL with a first choice of windows and preview components() library(feasts) library(fabletools) fit0 <- cafe |> model(STL(Turnover ~ trend(window = 21) + season(window = 11))) comp0 <- fit0 |> components() comp0 |> as_tibble() |> select(Month, Turnover, trend, season_year, remainder, season_adjust) |> head(5) #> # A tibble: 5 × 6 #> Month Turnover trend season_year remainder season_adjust #> <mth> <dbl> <dbl> <dbl> <dbl> <dbl> #> 1 1982 Apr 85.1 87.2 -3.48 1.35 88.6 #> 2 1982 May 85.1 87.6 -1.53 -0.944 86.6 #> 3 1982 Jun 82.8 87.9 -8.56 3.43 91.4 #> 4 1982 Jul 82.1 88.3 -4.19 -1.98 86.3 #> 5 1982 Aug 81.8 88.6 -1.93 -4.85 83.7

  

components() hands back four columns beyond the ones you already had.

  • trend is the smoothed level.
  • season_year is STL's own name for the repeating yearly pattern (not seasonal, which is what classical decomposition calls the same idea).
  • remainder is whatever is left once trend and season are taken out.
  • season_adjust is the observed turnover with only the season removed, trend and remainder still inside it.

STL always adds these three together to rebuild the observed value: Turnover = trend + season_year + remainder, in every row, every month. Unlike the classical decomposition, STL never offers a multiplicative version. If a series' season grows with its level, you would transform the series first and let STL fit the additive model on the transformed scale.