The world’s leading publication for data science, AI, and ML professionals.

8 Techniques to Model Seasonality

How to handle seasonality for forecasting

Photo by Clark Young on Unsplash
Photo by Clark Young on Unsplash

This article is a follow-up to a previous post. There, we identified 3 types of seasonal patterns.

Here, we’ll:

  • Learn how to describe the seasonality of a time series.
  • Go over 8 approaches you can use to model seasonality.

Modeling Seasonal Patterns

Seasonality refers to repeatable patterns that recur over some period. It is an important source of variation that is important to model.

A time series and its seasonally-adjusted version. The data source is in the next section. Image by author.
A time series and its seasonally-adjusted version. The data source is in the next section. Image by author.

There are several ways of handling seasonality. Some approaches remove the seasonal component before modeling. Seasonally-adjusted data (a time series minus the seasonal component) highlights long-term effects such as trends or business cycles. Other approaches add extra variables that capture the cyclical nature of seasonality.

Before going over different methods, let’s create a time series and describe its seasonal patterns.

Analysis example

We’ll use the same process we did in the previous article (see also reference [1]):

period = 12 # monthly series
size = 120

beta1 = np.linspace(-.6, .3, num=size)
beta2 = np.linspace(.6, -.3, num=size)
sin1 = np.asarray([np.sin(2 * np.pi * i / 12) for i in np.arange(1, size + 1)])
cos1 = np.asarray([np.cos(2 * np.pi * i / 12) for i in np.arange(1, size + 1)])

xt = np.cumsum(np.random.normal(scale=0.1, size=size))

yt = xt + beta1 * sin1 + beta2 * cos1 + np.random.normal(scale=0.1, size=size)

yt = pd.Series(yt)

Here’s what this series look like:

Artificial time series with a stochastic stationary seasonality. Image by author.
Artificial time series with a stochastic stationary seasonality. Image by author.

We can start by describing the seasonal pattern by its strength:

# https://github.com/vcerqueira/blog/tree/main/src
from src.seasonality import seasonal_strength

seasonal_strength(yt, period=12)
# 0.90

The result is 0.90 which suggests that seasonality is indeed strong.

The auto-correlation plot of this time series is shown in the following figure:

Auto-correlation up to 24 lags. Image by author
Auto-correlation up to 24 lags. Image by author

The auto-correlation shows an oscillatory behavior. It has a significant peak on the first seasonal lag (12). These patterns confirm the relevance of the seasonality.

Finally, we check for seasonal unit roots using the Canova-Hansen test:

from pmdarima.arima import nsdiffs

nsdiffs(x=yt, m=period, test='ch')
# 0

The result is 0, which indicates that there’s no seasonal unit root. So, we can conclude that the seasonal pattern is stationary.

So, how can we deal with seasonal patterns such as this one?


#

In the rest of this article, you’ll learn about 8 techniques to model seasonality.

1. Seasonal dummy variables

Seasonal dummies are a set of binary variables. These represent whether an observation belongs to a given period (e.g. January).

Here’s an example of how you can create these variables:

from sktime.transformations.series.date import DateTimeFeatures
from sklearn.preprocessing import OneHotEncoder

monthly_feats = DateTimeFeatures(ts_freq='M',
                                 keep_original_columns=False,
                                 feature_scope='efficient')

datetime_feats = monthly_feats.fit_transform(yt)
datetime_feats = datetime_feats.drop('year', axis=1)

encoder = OneHotEncoder(drop='first', sparse=False)
encoded_feats = encoder.fit_transform(datetime_feats)

encoded_feats_df = pd.DataFrame(encoded_feats,
                                columns=encoder.get_feature_names_out(),
                                dtype=int)

This code results in the data below.

Seasonal dummies before and after one-hot encoding. Image by author
Seasonal dummies before and after one-hot encoding. Image by author

First, we get information about the quarter and month in each observation (left-hand side table). This information is stored in the _datetimefeats object. Then, we use one-hot encoding to create dummy variables (right-hand side table).

Seasonal dummies are especially effective if seasonality is deterministic. If we don’t expect changes in the seasonal pattern, both in intensity and periodicity.

We can check the coefficients of seasonal dummies to analyze the seasonal effects and changes therein. This can be beneficial for model interpretability.

The downside of seasonal dummies is that they assume that different periods are independent. January observations are correlated with those collected in December. But, dummy variables are blind to this correlation. So, if there are changes in the seasonal pattern, dummy variables may not be the best option.

2. Fourier series

Example sine and cosine terms. Image by author.
Example sine and cosine terms. Image by author.

Fourier series are periodic and deterministic variables based on sine and cosine waves. Contrary to seasonal dummies, these trigonometric functions model seasonality as a cyclical pattern. This structure reflects reality better.

This approach is implemented in sktime Python package:

from sktime.transformations.series.fourier import FourierFeatures

fourier = FourierFeatures(sp_list=[12],
                          fourier_terms_list=[4],
                          keep_original_columns=False)

fourier_feats = fourier.fit_transform(yt)

You need to specify two main parameters:

  • sp_list: the seasonal periods as a list (e.g. 12 for monthly data)
  • fourier_terms_list: the number of terms, which refers to the number of sine and cosine series to include. These affect the smoothness of the representation.

In practice, Fourier series are explanatory variables that you can add to the model. For example, you can combine these with lagged features.

3. Radial Basis Functions

3 example radial basis functions. Image by author.
3 example radial basis functions. Image by author.

Radial basis functions (RBF) are an alternative to the Fourier series. The idea is to create repeating bell-shaped curves to model repeating patterns.

RBFs are implemented in the scikit-lego Python package with the method RepeatingBasisFunction:

from sklego.preprocessing import RepeatingBasisFunction

rbf_encoder = RepeatingBasisFunction(n_periods=4,
                                     column='month_of_year',
                                     input_range=(1, 12),
                                     remainder='drop',
                                     width=0.25)

rbf_features = rbf_encoder.fit_transform(datetime_feats)
rbf_features_df = pd.DataFrame(rbf_features,
                               columns=[f'RBF{i}'
                                        for i in range(rbf_features.shape[1])])

The three most important parameters of this method are:

  • n_periods: number of basis functions to include
  • input_range: the input range of the column we want to create the basis functions with. For example, in the example above we use (1, 12) which is the range of the months;
  • width: Width of the radial basis functions, which controls their smoothness

Like the Fourier series, RBF variables can be used as explanatory variables in the model.

4. Seasonal Auto-regression

Auto-regression is the basis of most forecasting models. The idea is to use recent past observations (lags) to predict future values. This concept can be extended to model seasonality. Seasonal auto-regression models include past values of the same season as predictors.

SARIMA is a popular method that applies this idea:

import pmdarima as pm
model = pm.auto_arima(yt, m=12, trace=True)

model.summary()
# Best model:  ARIMA(0,1,0)(1,0,0)[12]

In the preceding code, we run _autoarima to find the best configuration of ARIMA. The resulting model includes the first lag of the same season as input.

Using seasonal lags as explanatory variables is an effective way of modeling seasonality. Note that you should deal with seasonal unit roots when using this approach. You can use nsdiffs and the Canova-Hansen test as we did before, and use seasonal differencing if necessary.

5. Adding Extra Variables

Methods such as seasonal dummies or Fourier series capture recurrent patterns. But, these approaches are proxies for the things that cause seasonality.

Exogenous variables such as temperature or the number of working days in each month can be useful to model seasonality.

Daily average temperature time series. Image by author. Data source in reference [2].
Daily average temperature time series. Image by author. Data source in reference [2].

You can include these variables in a model by using an ARDL formulation.

6. Adjustment via Seasonal Differencing

We can deal with seasonality by removing it from the data before modeling. This can be done with seasonal differencing or decomposition.

Seasonal differencing is the process of taking the difference between consecutive observations of the same season. This operation is especially useful to remove seasonal unit roots.

You can use the diff method to do seasonal differencing:

from sklearn.model_selection import train_test_split
from sktime.forecasting.compose import make_reduction
from sklearn.linear_model import RidgeCV

train, test = train_test_split(yt, test_size=12, shuffle=False)

train_sdiff = train.diff(periods=12)[12:]

forecaster = make_reduction(estimator=RidgeCV(),
                            strategy='recursive',
                            window_length=3)

forecaster.fit(train_sdiff)
diff_pred = forecaster.predict(fh=list(range(1, 13)))

Above, we also build a Ridge regression model on the differenced series. You can get the forecasts on the original scale by reverting the differencing operation.

7. Adjustment via Decomposition

You can also remove seasonality with time series decomposition methods, such as STL.

What’s the difference between differencing and decomposition?

Both differencing and decomposition are used to remove seasonality from a time series. But, the transformed data is modeled differently.

When applying seasonal differencing, models work with the differenced data. You need to revert the differencing operation to get the forecasts on the original scale.

With a decomposition-based approach, you need two sets of forecasts. One for the seasonal part and another for the seasonally-adjusted data. The final forecast is the sum of the forecasts of each part. Most often, a seasonal naive method is used to forecast the seasonal component.

Here’s an example of how a decomposition-based approach works:

from statsmodels.tsa.api import STL
from sktime.forecasting.naive import NaiveForecaster

# fitting the seasonal decomposition method
series_decomp = STL(yt, period=period).fit()

# adjusting the data
seas_adj = yt - series_decomp.seasonal

# forecasting the non-seasonal part
forecaster = make_reduction(estimator=RidgeCV(),
                            strategy='recursive',
                            window_length=3)

forecaster.fit(seas_adj)

seas_adj_pred = forecaster.predict(fh=list(range(1, 13)))

# forecasting the seasonal part
seas_forecaster = NaiveForecaster(strategy='last', sp=12)
seas_forecaster.fit(series_decomp.seasonal)
seas_preds = seas_forecaster.predict(fh=list(range(1, 13)))

# combining the forecasts
preds = seas_adj_pred + seas_preds

In this example, we build a Ridge regression model to forecast the seasonally-adjusted data. A seasonal naive model forecasts the seasonal component. Then, both forecasts are added together.

8. Dynamic Linear Models (DLM)

The parameters of a regression model are usually static. They do not change over time or are time-invariant. A DLM is a particular case of linear regression. The main feature is that the parameters vary with time, instead of being static.

DLMs posit that the structure of a seasonal time series changes over the seasons. So, a reasonable approach is to build a model with time-varying parameters. Parameters that vary seasonally.

Chapter 15 of the book in reference [1] provides a neat R example of this approach. They use a time-varying MARSS (Multivariate Auto-Regressive State-Space) method to model changing seasonality.


Key Takeaways

In this article, you learned about 8 different ways of modeling seasonality. These are:

  • Seasonal dummies
  • Fourier series
  • Radial basis functions
  • Seasonal auto-regression
  • Exogenous variables
  • Seasonal differencing
  • Seasonal decomposition
  • Dynamic models with seasonally-varying parameters

We used a time series with stochastic stationary seasonality as an example. Yet, some of the methods may not be the most appropriate for this type of seasonality. You should test different approaches and select one (or more) that suits your data.

Thank you for reading and see you in the next story!


Related Articles

Code

References

[1] Holmes, Elizabeth E., Mark D. Scheuerell, and E. J. Ward. "Applied Time Series Analysis for fisheries and environmental data." NOAA Fisheries, Northwest Fisheries Science Center, Seattle, WA (2020).

[2] Weather, Snow, and Streamflow data from four western juniper-dominated Experimental Catchments in south western Idaho, USA. (Licence: U.S. Public Domain)

[3] Sims, Christopher A. "Seasonality in regression." Journal of the American Statistical Association 69.347 (1974): 618–626.


Related Articles