Time Series Forecasting in R. Exponential Smoothing, TBATS, ARIMA... | by François St-Amant | Towards Data Science

Python is great, but when it comes to forecasting, I personally think R still has the upper hand. The Forecast package is the most complete forecasting package available on R or Python, and it’s worth knowing about it.
Here is what we will see in this article:
Naive methodsExponential Smoothing (State-space models and DSHW)BATS and TBATSARIMA/SARIMA modelsHow to set up a one-step-ahead forecast
Naive methods
Exponential Smoothing (State-space models and DSHW)
BATS and TBATS
ARIMA/SARIMA models
How to set up a one-step-ahead forecast
For every method, we will build a model on a validation set, forecast with it for the duration of the validation set and compare the forecast with the real observations to obtain a Mean Absolute Percentage Error (MAPE).
The data used will be coming from the Air Passengers Dataset, available on R.
library(forecast)library(MLmetrics)data=AirPassengers#Create samplestraining=window(data, start = c(1949,1), end = c(1955,12))validation=window(data, start = c(1956,1))
Any forecasting method should be evaluated by being compared to a naive method. This helps ensure that the efforts put in having a more complex model are worth it in terms of performance.
The simplest of all methods is called simple naive. Extremely simple: the forecast for tomorrow is what we are observing today.
Another approach, seasonal naive, is a little more “complex”: the forecast for tomorrow is what we observed the week/month/year (depending what horizon we are working with) before.
Here is how to do a seasonal naive forecast:
naive = snaive(training, h=length(validation))MAPE(naive$mean, validation) * 100
That gives us an MAPE of 27.04%. That’s the score to beat. By the way, remove the s from “snaive” and you have the code for simple naive. Here is how to plot the forecast:
plot(data, col="blue", xlab="Year", ylab="Passengers", main="Seasonal Naive Forecast", type='l')lines(naive$mean, col="red", lwd=2)
We see that what happened in the last year of data is repeated as a forecast for the entire validation set.
There are many ways to do exponential smoothing. The idea is always to have a declining weight given to observations. The more recent an observation, the more importance it will have in our forecast.
Parameters can also be added. You can for instance add a trend paramenter (Holt method) or add a seasonality (Holt-Winters).
With the Forecast Package, smoothing methods can be placed within the structure of state space models. By using this structure, we can find the optimal exponential smoothing model, using the ets function.
ets_model = ets(training, allow.multiplicative.trend = TRUE)summary(ets_model)
We see ETS (M, Md, M). This means we have an ets model with multiplicative errors, a multiplicative trend and a multiplicative seasonality. Basically, multiplicative means that the parameter is “amplified” over time. For instance the trend is getting bigger and bigger as time goes by (our case here).
Here is how to forecast using the estimated optimal smoothing model:
ets_forecast = forecast(ets_model, h=length(validation))MAPE(ets_forecast$mean, validation) *100
We see that the upward trend in demand is being capture a little bit (far from perfect, better than naive). It gives an MAPE of 12.6%.
The ets function is good, but it only allows for one seasonality. Sometimes, the data we have can be composed of multiple seasonalities (monthly and yearly for instance).
Double Seasonal Holt-Winters (DSHW) allows for two seasonalities: a smaller one repeated often and a bigger one repeated less often. For the method to work however, the seasonalities need to be nested, meaning one must be an integer multiple of the other (2 and 4, 24 and 168, etc.).
The code here is a bit different since we need to specify the lenghts of our two seasonalities (which is not always something we know) and the forecast is computed directly when creating the model with the dshw function.
dshw_model = dshw(training, period1=4, period2 = 12, h=length(validation))MAPE(dshw_model$mean, validation)*100
We get a MAPE of 3.7% with this method!
DSHW is good, but some processes have more complex seasonalities, which our previous functions cannot handle. Indeed, you could have both a weekly and yearly seasonality. You could even have more than 2!
BATS and TBATS allow for multiple seasonalities. TBATS is a modification (an improvement really) of BATS that allows for multiple non-integer seasonality cycles.
Here is how to build a TBATS model and forecast with it:
tbats_model = tbats(training)tbats_forecast = forecast(tbats_model, h=length(validation))MAPE(tbats_forecast$mean, validation) * 100
We get a MAPE of 12.9% for this method.
ARIMA models contain three things:
AR(p): autoregressive part of the model. Means that we use p past observations from the timeseries as predictors.
Differencing (d): Used to transform the timeseries into a stationary one by taking the differences between successive observations at appropriate lags d.
MA(q): uses q past forecast errors as predictors.
That’s it for ARIMA but if you know the data you have is seasonal, then you need more. That’s where SARIMA comes into play.
SARIMA adds a seasonal part to the model.
P represents the seasonal AR order, D the seasonal differencing order, Q the seasonal MA order and m the number of observations per year.
The auto.arima function can be used to return the best estimated model. Here is the code:
arima_optimal = auto.arima(training)
The function returned the following model: ARIMA(0,1,1)(1,1,0)[12].
To forecast a SARIMA model (which is what we have here since we have a seasonal part), we can use the sarima.for function from the astsa package.
library(astsa)sarima_forecast = sarima.for(training, n.ahead=length(validation),                              p=0,d=1,q=1,P=1,D=1,Q=0,S=12)MAPE(sarima_forecast$pred, validation) * 100
We get a MAPE of 6.5% with this SARIMA model.
Just so you know, we could in theory complexify things even more by adding exogenous variables (explanatory variables) to an ARIMA/SARIMA model, which would make it SARIMAX.
For this data, DSHW gave the best results. Keep in mind however that no model does best all the time.
In all the previous examples, I forecasted 5 years into the future. However, if you want to forecast on a daily basis, why would you use a forecasted value from 5 years ago when you could use the real observations to predict tomorrow?
The idea of setting up a one-step-ahead forecast is to evaluate how well a model would have done if you were forecasting for one day ahead, during 5 years, using latest observations to make your forecast.
Simply put: instead of forecasting once for the 60 months ahead, we forecast 60 times for the upcoming month, using latest observations.
Coding this is quite simple. All we need is to iteratively add the latest observation to the training dataset, forecast from there and repeat. Here is the code to do it using the SARIMA model we found earlier.
one_step_ahead_sarima = matrix(ncol = 2, nrow = 60)for (i in 1:60){    training_observed = window(data, start = c(1949,1), end = c(1955,(12+i)), frequency = 12)    forecasted.sarima = sarima.for(training_observed,n.ahead=1,p=0,d=1,q=1,P=1,D=1,Q=0,S=12)    demandforecast = forecasted.sarima$pred  observed = validation[[i]]    one_step_ahead_sarima[i,1]= observed  one_step_ahead_sarima[i,2]= demandforecast}MAPE(one_step_ahead_sarima[,1], one_step_ahead_sarima[,2]) * 100
As you see, for every iteration, the training set grows by one observation, a new forecast for n.ahead=1 is computed and results are stored in the one_step_ahead_sarima matrix.
Here is how to get the plot:
plot(data, col="blue", xlab="Year", ylab="Passengers", main="SARIMA Forecast", type='l')lines(ts(one_step_ahead_sarima[,2], start = c(1956,1), frequency = 12), col="red", lwd=3)
We get a MAPE of 4.1%, which is an improvement of more than 2% on what we had when using the same SARIMA model to forecast 5 years ahead.
That’s not surprising considering that the shorter the forecasting horizon, the better the forecast should be. That is why you should always set up a one-step-ahead forecast if it is possible!
I hope this helped. Thanks a lot for reading!
To become a member: https://francoisstamant.medium.com/membership