Seasonal models in R

library(forecast)
library(MASS)
library(tseries)

Time series functions used in this document

In the table below, packages with italicized names will need to be installed, while the package names in a standard font face can be found in most base R distributions (though they may need to be loaded into your workspace).

Time series functions used in the code below
Package Function name Purpose
stats window Subset a time series
forecast ets Fit exponential smoothing models
forecast forecast Look-ahead prediction of a ts model
tseries adf.test ADF stationarity test
tseries kpss.test KPSS stationarity test
forecast plot.Arima Plot the inverse AR and MA roots
forecast plot.forecast Plot predictions from a ts model
stats Box.test Box-Ljung test for autocorrelation

Fitting ETS models in R

The workhorse function forecast::ets (or its tidyverse equivalent, fable::ETS) can fit any of the exponential smoothing models described in Forecasting: Principles and Practice 2nd edition or 3rd edition, respectively.

Let’s apply it to a quarterly series of UK quarterly gas consumption:1

plot(UKgas)

gas.train <- window(UKgas,end=1985.25)
gas.test <- window(UKgas,start=1985.5)

The data are clearly non-stationary, with a multiplicative seasonality (perhaps corresponding to a growing population). We might be able to control this multiplicative effect with a Box-Cox transformation, or through the explicit use of multiplicative errors and seasonal effects. Let’s examine those two options, holding the last six quarters back as a prediction testbed.

Fitting an additive-only model with a Box-Cox transformation

ets.aaa <- ets(gas.train,model='AAA',lambda='auto')
summary(ets.aaa)
ETS(A,Ad,A) 

Call:
ets(y = gas.train, model = "AAA", lambda = "auto")

  Box-Cox transformation: lambda= -0.4329 

  Smoothing parameters:
    alpha = 0.0306 
    beta  = 0.0281 
    gamma = 0.5365 
    phi   = 0.98 

  Initial states:
    l = 2.0142 
    b = 6e-04 
    s = -3e-04 -0.0452 0.0084 0.0371

  sigma:  0.0102

      AIC      AICc       BIC 
-453.1036 -450.6861 -426.8539 

Training set error measures:
                   ME     RMSE      MAE       MPE     MAPE      MASE       ACF1
Training set 3.536558 30.18087 19.39172 0.5496738 6.064928 0.7319039 -0.1072051
plot(ets.aaa)

The low values of the smoothing parameters \(\alpha\) and \(\beta\) suggest that long-ago observations are still quite useful in setting the level and the trend for the current quarter’s gas prices. In contrast, the relatively high value of the smoothing parameter \(\gamma\) suggests that the most recent seasonal pattern is more helpfully informative than prior years’ seasonal patterns.

A damped trend was selected over an undamped trend — we can manually change this with the damped=FASE argument, or keep it. Since the damping parameter \(\phi\) is 0.98, the difference between the two models will be relatively small.

Note the significant transformation achieved with the Box-Cox transformation (where \(\lambda \approx -0.43\)), which changes the series to something more closely approximating trend-stationarity. And yet, the seasonal plot still shows nonconstant variance (which is likely why the parameter \(\gamma\) is relatively high). It’s hard to know whether this is a bug or a feature, i.e. whether a transformation could draw useful more information out of the past or not.

Fitting a multiplicative model without Box-Cox transformation

ets.zam <- ets(gas.train,model='ZAM',lambda=NULL)
summary(ets.zam)
ETS(M,A,M) 

Call:
ets(y = gas.train, model = "ZAM", lambda = NULL)

  Smoothing parameters:
    alpha = 0.0279 
    beta  = 0.0277 
    gamma = 0.5994 

  Initial states:
    l = 123.9697 
    b = 0.7846 
    s = 0.9453 0.6771 1.0464 1.3312

  sigma:  0.1164

     AIC     AICc      BIC 
1170.080 1172.037 1193.705 

Training set error measures:
                   ME     RMSE     MAE        MPE     MAPE     MASE       ACF1
Training set 3.835205 30.89018 20.1769 0.06514202 6.418552 0.761539 -0.1309169
plot(ets.zam)

By using the argument ‘Z’, we can tell the ets() function to choose additive, multiplicative, or none for each component. In this case, I was sure about requesting an additive trend (ets() won’t actually fit most multiplicative trends), and a multiplicative seasonality, but was unsure about whether the error process was best viewed as additive or multiplicative. R chose multiplicative.

However, the summary is disappointing. This looks like a strictly worse version of our earlier model. Similar smoothing parameters, similar (but worse) model metrics, and a more chaotic slope plot which suggests that the model is trying to cram a lot of information into a slope in order to incorporate large and unexpected changes in the series. Let’s try restoring the Box-Cox transformation.

Letting R decide

ets.zzz <- ets(gas.train,model='ZZZ',lambda='auto',biasadj=TRUE)
summary(ets.zzz)
ETS(A,Ad,A) 

Call:
ets(y = gas.train, model = "ZZZ", lambda = "auto", biasadj = TRUE)

  Box-Cox transformation: lambda= -0.4329 

  Smoothing parameters:
    alpha = 0.0306 
    beta  = 0.0281 
    gamma = 0.5365 
    phi   = 0.98 

  Initial states:
    l = 2.0142 
    b = 6e-04 
    s = -3e-04 -0.0452 0.0084 0.0371

  sigma:  0.0102

      AIC      AICc       BIC 
-453.1036 -450.6861 -426.8539 

Training set error measures:
                    ME     RMSE      MAE        MPE     MAPE      MASE
Training set -1.311548 30.33744 19.56546 -0.5017562 6.134186 0.7384615
                    ACF1
Training set -0.08345354

R confirms our earlier choice of the additive Holt-Winters model with damped trend. By using the biasadj=TRUE argument, we can remove bias from our forecast estimates (after Box-Cox transforming the data to fit the model, the inverse-Box-Cox forecasts will be for the median value of \(\hat{y}_t\), not the mean).

How would this model finish out our series?

plot(forecast(ets.zzz,h=6),include=12,PI=FALSE)
points(gas.test)

In the code above I’ve set h=6 to define the number of forecasted periods, include=12 to limit how much of the prior oobserved series appears on the plot, and PI=FALSE to remove the prediction intervals (which rely upon distributional theory we have not yet decided to use).

On balance, I would say these predictions are quite good! The seasonal cycle and past exponential growth both carry forward well into 1986.

Fitting SARIMA models in R

We can try to fit a SARIMA model to the same dataset. Because ARIMA models assume a stationary series, we will first have to do what we can to achieve stationarity:

gas.sarima <- auto.arima(gas.train,approximation=FALSE,stepwise=FALSE,seasonal=TRUE,lambda='auto')
summary(gas.sarima)
Series: gas.train 
ARIMA(2,0,2)(0,1,1)[4] with drift 
Box Cox transformation: lambda= -0.4328729 

Coefficients:
         ar1      ar2      ma1     ma2     sma1   drift
      1.4412  -0.4796  -1.6261  0.7314  -0.4244  0.0013
s.e.  0.1918   0.1956   0.1498  0.1548   0.1151  0.0004

sigma^2 = 9.742e-05:  log likelihood = 316.18
AIC=-618.37   AICc=-617.12   BIC=-600.27

Training set error measures:
                    ME    RMSE    MAE        MPE     MAPE      MASE      ACF1
Training set -1.358139 30.5077 19.739 -0.3803901 6.282248 0.7450115 0.1790798

By using the arguments approximation=FALSE and stepwise=FALSE, I ensure that R takes its time to fully calculate every reasonable model before choosing the best one. These arguments may be inconvenient or even impossible with large datasets or with long-period seasonality, where R may take several minutes to find the best model.

R returns a reasonable idea: The changes in gas prices vs. the same quarter last year are weakly mean-reverting around an exponential drift, but with some persistence (when this quarter’s year-over-year change is negative or positive, the next one will likely be similar) and some error correction (unexpected anomalies rarely happen in consecutive years). But is it a reasonable model?

plot(gas.sarima)

plot(gas.sarima$residuals)

suppressWarnings(adf.test(gas.sarima$residuals))

    Augmented Dickey-Fuller Test

data:  gas.sarima$residuals
Dickey-Fuller = -3.6612, Lag order = 4, p-value = 0.03112
alternative hypothesis: stationary
suppressWarnings(kpss.test(gas.sarima$residuals))

    KPSS Test for Level Stationarity

data:  gas.sarima$residuals
KPSS Level = 0.19341, Truncation lag parameter = 4, p-value = 0.1
Box.test(gas.sarima$residuals,type='Ljung')

    Box-Ljung test

data:  gas.sarima$residuals
X-squared = 0.041022, df = 1, p-value = 0.8395
par(mfrow=c(1,2))
acf(gas.sarima$residuals)
pacf(gas.sarima$residuals)

We see that none of the roots are perilously close to the edge of the unit circle, which is a good sign (R will always try to estimate a stationary ARIMA model, so when the roots are very close to the unit circle we have some evidence that the true process is actually nonstationary). We also see that the estimated innovations are relatively “clean” (stationary, no signs of remaining autocorrelation).

plot(forecast(gas.sarima,h=6),include=12)
points(gas.test)

Our predictions are very good with this method too! We should not really be surprised: as described by Hyndman and Athanasopoulos, purely additive ETS models can be represented as ARIMA models and vice versa. Although the exact form of our chosen ETS model and our SARIMA model are not equivalent, they are close.

Note that these predictions come with confidence intervals, since ARIMA models suppose a Gaussian white noise error process. We can recover these intervals and/or select new ones using the forecast function or alternatively with the predict.Arima function.

forecast(gas.sarima,h=6)
        Point Forecast    Lo 80     Hi 80    Lo 95     Hi 95
1985 Q3       240.4994 210.7762  276.6159 197.1367  298.9494
1985 Q4       785.2351 630.3943 1000.9633 565.6710 1150.9506
1986 Q1      1163.0804 898.4302 1555.4689 792.2109 1843.8957
1986 Q2       561.4655 463.5813  691.9078 421.4312  779.1419
1986 Q3       252.2123 214.0962  300.8558 197.1917  332.2017
1986 Q4       851.1074 649.1042 1156.9429 569.1086 1386.0739

Which model we use, ETS or SARIMA, is a judgment call at this point: neither model is terribly flawed nor much better or worse than the other. We might prioritize in-sample RMSE, out-of-sample RMSE, other measures such as MAE, domain expertise, parsimony, prior literature, etc. We should not use AIC or other likelihood-based techniques, as these are sensitive to how we represent Y, which can change with orders of differencing or model classes.

#ETS metrics
c(is.rmse=sqrt(mean((gas.train-ets.zzz$fitted)^2)),
  oos.rmse=sqrt(mean((gas.test-forecast(ets.zzz,h=6)$mean)^2)))
 is.rmse oos.rmse 
30.33744 54.96811 
#SARIMA metrics
c(is.rmse=sqrt(mean((gas.train-gas.sarima$fitted)^2)),
  oos.rmse=sqrt(mean((gas.test-forecast(gas.sarima,h=6)$mean)^2)))
 is.rmse oos.rmse 
30.50770 54.93136 

  1. Gas here meaning natural gas, and not gasoline, which in the UK would be called petrol.↩︎