SARIMA & Seasonal Extensions

SARIMA & Seasonal Extensions#

Navigation: ← Previous: ARMA & ARIMA | Next: State-Space Models →

SARIMA extends ARIMA with seasonal differencing and seasonal AR/MA terms. The Box-Jenkins airline model ARIMA(0,1,1)(0,1,1)₁₂ remains the canonical benchmark.

from pathlib import Path
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import warnings
warnings.filterwarnings('ignore')
plt.style.use('seaborn-v0_8-whitegrid')

DATA_DIR = Path('data')
if not DATA_DIR.exists():
    DATA_DIR = Path('projects/time-series-analysis/data')

def load_series(name):
    """Load a bundled CSV as a DatetimeIndex Series."""
    df = pd.read_csv(DATA_DIR / name, parse_dates=['date'])
    return df.set_index('date')['value'].sort_index()

def load_air_passengers():
    path = DATA_DIR / 'air_passengers.csv'
    if path.exists():
        return load_series('air_passengers.csv')
    from statsmodels.datasets import get_rdataset
    air = get_rdataset('AirPassengers', 'datasets').data
    s = air['value']
    s.index = pd.date_range('1949-01', periods=len(s), freq='MS')
    return s

def load_milk():
    path = DATA_DIR / 'monthly_milk.csv'
    if path.exists():
        return load_series('monthly_milk.csv')
    url = 'https://raw.githubusercontent.com/jbrownlee/Datasets/master/monthly-milk-production-pounds.csv'
    return pd.read_csv(url, index_col=0, parse_dates=True).squeeze()

def load_co2():
    from statsmodels.datasets import co2
    return co2.load().data.resample('MS').mean().ffill().squeeze()

def load_sunspots():
    from statsmodels.datasets import sunspots
    d = sunspots.load_pandas().data
    return d.set_index('YEAR')['SUNACTIVITY']

def load_retail():
    path = DATA_DIR / 'uk_retail_sales.csv'
    if path.exists():
        return load_series('uk_retail_sales.csv')
    raise FileNotFoundError('uk_retail_sales.csv not found in data/')
from statsmodels.tsa.statespace.sarimax import SARIMAX
from statsmodels.stats.diagnostic import acorr_ljungbox
from pmdarima import auto_arima

The Airline Model#

ARIMA\((0,1,1)(0,1,1)_{12}\): one regular and one seasonal difference, one MA and one seasonal MA.

air = load_air_passengers()
airline = SARIMAX(air, order=(0, 1, 1), seasonal_order=(0, 1, 1, 12)).fit(disp=False)
print(airline.summary().tables[1])
print(f'AIC={airline.aic:.1f}')
==============================================================================
                 coef    std err          z      P>|z|      [0.025      0.975]
------------------------------------------------------------------------------
ma.L1         -0.3087      0.068     -4.507      0.000      -0.443      -0.174
ma.S.L12      -0.1075      0.091     -1.184      0.236      -0.285       0.070
sigma2       135.4236     15.443      8.769      0.000     105.156     165.691
==============================================================================
AIC=1021.0
/opt/hostedtoolcache/Python/3.11.16/x64/lib/python3.11/site-packages/statsmodels/tsa/base/tsa_model.py:480: ValueWarning: No frequency information was provided, so inferred frequency MS will be used.
  self._init_dates(dates, freq)
/opt/hostedtoolcache/Python/3.11.16/x64/lib/python3.11/site-packages/statsmodels/tsa/base/tsa_model.py:480: ValueWarning: No frequency information was provided, so inferred frequency MS will be used.
  self._init_dates(dates, freq)
# Residual diagnostics
fig = airline.plot_diagnostics(figsize=(12, 8))
fig.suptitle('Airline Model Diagnostics', y=1.02)
fig
../../_images/0ddd4d21c10c913b4c41476dcc3d8da702b69e3929e19884a423aa87f271a6b6.png ../../_images/0ddd4d21c10c913b4c41476dcc3d8da702b69e3929e19884a423aa87f271a6b6.png
# auto-ARIMA with constrained search for CI speed
auto = auto_arima(
    air, seasonal=True, m=12, stepwise=True, suppress_warnings=True,
    max_p=2, max_q=2, max_P=1, max_Q=1, information_criterion='aic'
)
print(auto.summary())
                                      SARIMAX Results                                      
===========================================================================================
Dep. Variable:                                   y   No. Observations:                  144
Model:             SARIMAX(2, 1, 1)x(0, 1, [], 12)   Log Likelihood                -504.923
Date:                             Thu, 03 Sep 2026   AIC                           1017.847
Time:                                     22:08:01   BIC                           1029.348
Sample:                                 01-01-1949   HQIC                          1022.520
                                      - 12-01-1960                                         
Covariance Type:                               opg                                         
==============================================================================
                 coef    std err          z      P>|z|      [0.025      0.975]
------------------------------------------------------------------------------
ar.L1          0.5960      0.085      6.987      0.000       0.429       0.763
ar.L2          0.2143      0.091      2.343      0.019       0.035       0.394
ma.L1         -0.9819      0.038    -25.602      0.000      -1.057      -0.907
sigma2       129.3133     14.556      8.884      0.000     100.783     157.843
===================================================================================
Ljung-Box (L1) (Q):                   0.00   Jarque-Bera (JB):                 7.68
Prob(Q):                              0.98   Prob(JB):                         0.02
Heteroskedasticity (H):               2.33   Skew:                            -0.01
Prob(H) (two-sided):                  0.01   Kurtosis:                         4.19
===================================================================================

Warnings:
[1] Covariance matrix calculated using the outer product of gradients (complex-step).
# 24-month forecast with intervals
forecast = airline.get_forecast(steps=24)
fc_mean = forecast.predicted_mean
fc_ci = forecast.conf_int()
fig, ax = plt.subplots(figsize=(12, 5))
air.plot(ax=ax, label='Observed')
fc_mean.plot(ax=ax, label='Forecast', color='crimson')
ax.fill_between(fc_ci.index, fc_ci.iloc[:, 0], fc_ci.iloc[:, 1], alpha=0.3, color='crimson')
ax.legend()
ax.set_title('Air Passengers — 24-month SARIMA Forecast')
fig
../../_images/30a0635962cb300e036f65688eeae041212fbcdb0223ef1846f1b84da5670b7f.png ../../_images/30a0635962cb300e036f65688eeae041212fbcdb0223ef1846f1b84da5670b7f.png

Production seasonal adjustment systems (X-13ARIMA-SEATS, TRAMO-SEATS, JDemetra+) implement these models at scale for official statistics — the airline model is their intellectual ancestor.

milk = load_milk()
milk_sarima = SARIMAX(milk, order=(1, 1, 1), seasonal_order=(1, 1, 1, 12)).fit(disp=False)
print(f'Milk SARIMA AIC={milk_sarima.aic:.1f}')
/opt/hostedtoolcache/Python/3.11.16/x64/lib/python3.11/site-packages/statsmodels/tsa/base/tsa_model.py:480: ValueWarning: No frequency information was provided, so inferred frequency MS will be used.
  self._init_dates(dates, freq)
/opt/hostedtoolcache/Python/3.11.16/x64/lib/python3.11/site-packages/statsmodels/tsa/base/tsa_model.py:480: ValueWarning: No frequency information was provided, so inferred frequency MS will be used.
  self._init_dates(dates, freq)
Milk SARIMA AIC=980.7

Key Takeaways#

  • Seasonal differencing: \((1-B^{12})y_t\) removes fixed monthly season; combine with regular differencing.

  • Airline model: Still competitive after 50 years — parsimony beats complexity on short series.

  • auto-ARIMA: Useful for exploration; always verify with residual diagnostics.

  • Production link: ONS seasonal adjustment pipelines extend these ideas with regARIMA and calendar effects.


Navigation: ← Previous: ARMA & ARIMA | Next: State-Space Models →