Skill Tracer
Skills › Scientific Agent Skills › statsmodels › time_series.md

time_series.md

743 lines · skills/statsmodels/references/time_series.md at 5f2bc03

Open on GitHub
1# Time Series Analysis Reference
2
3This document provides comprehensive guidance on time series models in statsmodels, including ARIMA, state space models, VAR, exponential smoothing, and forecasting methods.
4

Time Series Analysis Reference

This document provides comprehensive guidance on time series models in statsmodels, including ARIMA, state space models, VAR, exponential smoothing, and forecasting methods.

Raw lines 1 to 4
5## Overview
6
7Statsmodels offers extensive time series capabilities:
8- **Univariate models**: AR, ARIMA, SARIMAX, Exponential Smoothing
9- **Multivariate models**: VAR, VARMAX, Dynamic Factor Models
10- **State space framework**: Custom models, Kalman filtering
11- **Diagnostic tools**: ACF, PACF, stationarity tests, residual analysis
12- **Forecasting**: Point forecasts and prediction intervals
14Use a sorted, unique regular time grid with a recorded frequency; do not drop interior missing dates and silently compress time. State-space models can handle missing responses, but exogenous data must be finite/aligned, and ADF/KPSS reject missing values. For date-labeled forecasting, preserve the frequency and provide future exogenous rows in exact model-column order.

Overview

Statsmodels offers extensive time series capabilities:

  • Univariate models: AR, ARIMA, SARIMAX, Exponential Smoothing
  • Multivariate models: VAR, VARMAX, Dynamic Factor Models
  • State space framework: Custom models, Kalman filtering
  • Diagnostic tools: ACF, PACF, stationarity tests, residual analysis
  • Forecasting: Point forecasts and prediction intervals

Use a sorted, unique regular time grid with a recorded frequency; do not drop interior missing dates and silently compress time. State-space models can handle missing responses, but exogenous data must be finite/aligned, and ADF/KPSS reject missing values. For date-labeled forecasting, preserve the frequency and provide future exogenous rows in exact model-column order.

Raw lines 5 to 15
16## Univariate Time Series Models

Univariate Time Series Models

Raw lines 16 to 17
18### AutoReg (AR Model)
20Autoregressive model: current value depends on past values.
22**When to use:**
23- Univariate time series
24- Past values predict future
25- Stationary series
27**Model**: yₜ = c + φ₁yₜ₋₁ + φ₂yₜ₋₂ + ... + φₚyₜ₋ₚ + εₜ
29```python
30from statsmodels.tsa.ar_model import AutoReg
31import pandas as pd
33# Fit AR(p) model
34model = AutoReg(y, lags=5) # AR(5)
35results = model.fit()
37print(results.summary())
38```
40**With exogenous regressors:**
41```python
42# AR with exogenous variables (ARX)
43model = AutoReg(y, lags=5, exog=X_exog)
44results = model.fit()
45```
47**Seasonal AR:**
48```python
49# Lags 1..12 plus seasonal dummies; use lags=[1, 12] for sparse seasonal lags
50model = AutoReg(y, lags=12, seasonal=True, period=12)
51results = model.fit()
52```

AutoReg (AR Model)

Autoregressive model: current value depends on past values.

When to use:

  • Univariate time series
  • Past values predict future
  • Stationary series

Model: yₜ = c + φ₁yₜ₋₁ + φ₂yₜ₋₂ + ... + φₚyₜ₋ₚ + εₜ

from statsmodels.tsa.ar_model import AutoReg
import pandas as pd

# Fit AR(p) model
model = AutoReg(y, lags=5)  # AR(5)
results = model.fit()

print(results.summary())

With exogenous regressors:

# AR with exogenous variables (ARX)
model = AutoReg(y, lags=5, exog=X_exog)
results = model.fit()

Seasonal AR:

# Lags 1..12 plus seasonal dummies; use lags=[1, 12] for sparse seasonal lags
model = AutoReg(y, lags=12, seasonal=True, period=12)
results = model.fit()
Raw lines 18 to 53
54### ARIMA (Autoregressive Integrated Moving Average)
56Combines AR, differencing (I), and MA components.
58**When to use:**
59- Non-stationary time series (needs differencing)
60- Past values and errors predict future
61- Flexible model for many time series
63**Model**: ARIMA(p,d,q)
64- p: AR order (lags)
65- d: differencing order (to achieve stationarity)
66- q: MA order (lagged forecast errors)
68```python
69from statsmodels.tsa.arima.model import ARIMA
71# Fit ARIMA(p,d,q)
72model = ARIMA(y, order=(1, 1, 1)) # ARIMA(1,1,1)
73results = model.fit()
75print(results.summary())
76```
78**Choosing p, d, q:**
801. **Determine d (differencing order)**:
81```python
82from statsmodels.tsa.stattools import adfuller
84# ADF test for stationarity
85def check_stationarity(series):
86 result = adfuller(series, result_object=True)
87 print(f"ADF Statistic: {result.statistic:.4f}")
88 print(f"p-value: {result.pvalue:.4f}")
89 if result.pvalue <= 0.05:
90 print("Reject unit-root null under the chosen deterministic specification")
91 return True
92 else:
93 print("Do not reject unit root; this does not prove nonstationarity")
94 return False
96# Combine the test with trend/seasonal plots, sampling frequency and domain knowledge.
97check_stationarity(y)
98# Compare a prespecified candidate difference; do not repeatedly difference
99# until a p-value crosses a threshold. ARIMA(y, order=(p,d,q)) uses original y.
100y_diff = y.diff().dropna()
101check_stationarity(y_diff)
102```
1042. **Determine p and q (ACF/PACF)**:
105```python
106from statsmodels.graphics.tsaplots import plot_acf, plot_pacf
107import matplotlib.pyplot as plt
109# After differencing to stationarity
110fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8))
112# ACF: helps determine q (MA order)
113plot_acf(y_stationary, lags=40, ax=ax1)
114ax1.set_title('Autocorrelation Function (ACF)')
116# PACF: helps determine p (AR order)
117plot_pacf(y_stationary, lags=40, ax=ax2)
118ax2.set_title('Partial Autocorrelation Function (PACF)')
120plt.tight_layout()
121plt.show()
123# Rules of thumb:
124# - PACF cuts off at lag p → AR(p)
125# - ACF cuts off at lag q → MA(q)
126# - Both decay → ARMA(p,q)
127```
1293. **Model selection (AIC/BIC)**:
130```python
131# Grid search for best (p,q) given d
132import numpy as np
134best_aic = np.inf
135best_order = None
136best_result = None
137candidates = []
139for p in range(5):
140 for q in range(5):
141 try:
142 model = ARIMA(y, order=(p, d, q))
143 results = model.fit()
144 converged = results.mle_retvals.get("converged", False)
145 candidates.append({"order": (p, d, q), "converged": converged, "aic": results.aic})
146 if converged and np.isfinite(results.aic) and results.aic < best_aic:
147 best_aic = results.aic
148 best_order = (p, d, q)
149 best_result = results
150 except (ValueError, np.linalg.LinAlgError) as exc:
151 candidates.append({"order": (p, d, q), "error": str(exc)})
153if best_order is None:
154 raise RuntimeError("No converged finite-likelihood candidate; inspect failures")
155results = best_result # Keep the selected fit, not the last grid candidate
156print(pd.DataFrame(candidates))
157print(f"Best order: {best_order} with AIC: {best_aic:.2f}")
158```

ARIMA (Autoregressive Integrated Moving Average)

Combines AR, differencing (I), and MA components.

When to use:

  • Non-stationary time series (needs differencing)
  • Past values and errors predict future
  • Flexible model for many time series

Model: ARIMA(p,d,q)

  • p: AR order (lags)
  • d: differencing order (to achieve stationarity)
  • q: MA order (lagged forecast errors)
from statsmodels.tsa.arima.model import ARIMA

# Fit ARIMA(p,d,q)
model = ARIMA(y, order=(1, 1, 1))  # ARIMA(1,1,1)
results = model.fit()

print(results.summary())

Choosing p, d, q:

  1. Determine d (differencing order):
from statsmodels.tsa.stattools import adfuller

# ADF test for stationarity
def check_stationarity(series):
    result = adfuller(series, result_object=True)
    print(f"ADF Statistic: {result.statistic:.4f}")
    print(f"p-value: {result.pvalue:.4f}")
    if result.pvalue <= 0.05:
        print("Reject unit-root null under the chosen deterministic specification")
        return True
    else:
        print("Do not reject unit root; this does not prove nonstationarity")
        return False

# Combine the test with trend/seasonal plots, sampling frequency and domain knowledge.
check_stationarity(y)
# Compare a prespecified candidate difference; do not repeatedly difference
# until a p-value crosses a threshold. ARIMA(y, order=(p,d,q)) uses original y.
y_diff = y.diff().dropna()
check_stationarity(y_diff)
  1. Determine p and q (ACF/PACF):
from statsmodels.graphics.tsaplots import plot_acf, plot_pacf
import matplotlib.pyplot as plt

# After differencing to stationarity
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8))

# ACF: helps determine q (MA order)
plot_acf(y_stationary, lags=40, ax=ax1)
ax1.set_title('Autocorrelation Function (ACF)')

# PACF: helps determine p (AR order)
plot_pacf(y_stationary, lags=40, ax=ax2)
ax2.set_title('Partial Autocorrelation Function (PACF)')

plt.tight_layout()
plt.show()

# Rules of thumb:
# - PACF cuts off at lag p → AR(p)
# - ACF cuts off at lag q → MA(q)
# - Both decay → ARMA(p,q)
  1. Model selection (AIC/BIC):
# Grid search for best (p,q) given d
import numpy as np

best_aic = np.inf
best_order = None
best_result = None
candidates = []

for p in range(5):
    for q in range(5):
        try:
            model = ARIMA(y, order=(p, d, q))
            results = model.fit()
            converged = results.mle_retvals.get("converged", False)
            candidates.append({"order": (p, d, q), "converged": converged, "aic": results.aic})
            if converged and np.isfinite(results.aic) and results.aic < best_aic:
                best_aic = results.aic
                best_order = (p, d, q)
                best_result = results
        except (ValueError, np.linalg.LinAlgError) as exc:
            candidates.append({"order": (p, d, q), "error": str(exc)})

if best_order is None:
    raise RuntimeError("No converged finite-likelihood candidate; inspect failures")
results = best_result  # Keep the selected fit, not the last grid candidate
print(pd.DataFrame(candidates))
print(f"Best order: {best_order} with AIC: {best_aic:.2f}")
Raw lines 54 to 159
160### SARIMAX (Seasonal ARIMA with Exogenous Variables)
162Extends ARIMA with seasonality and exogenous regressors.
164**When to use:**
165- Seasonal patterns (monthly, quarterly data)
166- External variables influence series
167- Most flexible univariate model
169**Model**: SARIMAX(p,d,q)(P,D,Q,s)
170- (p,d,q): Non-seasonal ARIMA
171- (P,D,Q,s): Seasonal ARIMA with period s
173```python
174from statsmodels.tsa.statespace.sarimax import SARIMAX
176# Seasonal ARIMA for monthly data (s=12)
177model = SARIMAX(y,
178 order=(1, 1, 1), # (p,d,q)
179 seasonal_order=(1, 1, 1, 12)) # (P,D,Q,s)
180results = model.fit()
182print(results.summary())
183```
185**With exogenous variables:**
186```python
187# SARIMAX with external predictors
188model = SARIMAX(y,
189 exog=X_exog,
190 order=(1, 1, 1),
191 seasonal_order=(1, 1, 1, 12))
192results = model.fit()
193```
195**Example: Monthly sales with trend and seasonality**
196```python
197# Typical for monthly data: (p,d,q)(P,D,Q,12)
198# Start with (1,1,1)(1,1,1,12) or (0,1,1)(0,1,1,12)
200model = SARIMAX(monthly_sales,
201 order=(0, 1, 1),
202 seasonal_order=(0, 1, 1, 12),
203 enforce_stationarity=False,
204 enforce_invertibility=False)
205results = model.fit()
206```

SARIMAX (Seasonal ARIMA with Exogenous Variables)

Extends ARIMA with seasonality and exogenous regressors.

When to use:

  • Seasonal patterns (monthly, quarterly data)
  • External variables influence series
  • Most flexible univariate model

Model: SARIMAX(p,d,q)(P,D,Q,s)

  • (p,d,q): Non-seasonal ARIMA
  • (P,D,Q,s): Seasonal ARIMA with period s
from statsmodels.tsa.statespace.sarimax import SARIMAX

# Seasonal ARIMA for monthly data (s=12)
model = SARIMAX(y,
                order=(1, 1, 1),           # (p,d,q)
                seasonal_order=(1, 1, 1, 12))  # (P,D,Q,s)
results = model.fit()

print(results.summary())

With exogenous variables:

# SARIMAX with external predictors
model = SARIMAX(y,
                exog=X_exog,
                order=(1, 1, 1),
                seasonal_order=(1, 1, 1, 12))
results = model.fit()

Example: Monthly sales with trend and seasonality

# Typical for monthly data: (p,d,q)(P,D,Q,12)
# Start with (1,1,1)(1,1,1,12) or (0,1,1)(0,1,1,12)

model = SARIMAX(monthly_sales,
                order=(0, 1, 1),
                seasonal_order=(0, 1, 1, 12),
                enforce_stationarity=False,
                enforce_invertibility=False)
results = model.fit()
Raw lines 160 to 207
208### Exponential Smoothing
210Weighted averages of past observations with exponentially decreasing weights.
212**When to use:**
213- Simple, interpretable forecasts
214- Trend and/or seasonality present
215- No need for explicit model specification
217**Types:**
218- Simple Exponential Smoothing: no trend, no seasonality
219- Holt's method: with trend
220- Holt-Winters: with trend and seasonality
222```python
223from statsmodels.tsa.holtwinters import ExponentialSmoothing
225# Simple exponential smoothing
226model = ExponentialSmoothing(y, trend=None, seasonal=None)
227results = model.fit()
229# Holt's method (with trend)
230model = ExponentialSmoothing(y, trend='add', seasonal=None)
231results = model.fit()
233# Holt-Winters (trend + seasonality)
234model = ExponentialSmoothing(y,
235 trend='add', # 'add' or 'mul'
236 seasonal='add', # 'add' or 'mul'
237 seasonal_periods=12) # e.g., 12 for monthly
238results = model.fit()
240print(results.summary())
241```
243**Additive vs Multiplicative:**
244```python
245# Additive: constant seasonal variation
246# yₜ = Level + Trend + Seasonal + Error
248# Multiplicative: proportional seasonal variation
249# yₜ = Level × Trend × Seasonal × Error
251# Choose based on data:
252# - Additive: seasonal variation constant over time
253# - Multiplicative: seasonal variation increases with level
254```
256**Innovations state space (ETS):**
257```python
258from statsmodels.tsa.exponential_smoothing.ets import ETSModel
260# Innovations state-space ETS, with an explicit error model
261model = ETSModel(y,
262 error='add', # 'add' or 'mul'
263 trend='add', # 'add', 'mul', or None
264 seasonal='add', # 'add', 'mul', or None
265 seasonal_periods=12)
266results = model.fit()
267```

Exponential Smoothing

Weighted averages of past observations with exponentially decreasing weights.

When to use:

  • Simple, interpretable forecasts
  • Trend and/or seasonality present
  • No need for explicit model specification

Types:

  • Simple Exponential Smoothing: no trend, no seasonality
  • Holt's method: with trend
  • Holt-Winters: with trend and seasonality
from statsmodels.tsa.holtwinters import ExponentialSmoothing

# Simple exponential smoothing
model = ExponentialSmoothing(y, trend=None, seasonal=None)
results = model.fit()

# Holt's method (with trend)
model = ExponentialSmoothing(y, trend='add', seasonal=None)
results = model.fit()

# Holt-Winters (trend + seasonality)
model = ExponentialSmoothing(y,
                            trend='add',           # 'add' or 'mul'
                            seasonal='add',        # 'add' or 'mul'
                            seasonal_periods=12)   # e.g., 12 for monthly
results = model.fit()

print(results.summary())

Additive vs Multiplicative:

# Additive: constant seasonal variation
# yₜ = Level + Trend + Seasonal + Error

# Multiplicative: proportional seasonal variation
# yₜ = Level × Trend × Seasonal × Error

# Choose based on data:
# - Additive: seasonal variation constant over time
# - Multiplicative: seasonal variation increases with level

Innovations state space (ETS):

from statsmodels.tsa.exponential_smoothing.ets import ETSModel

# Innovations state-space ETS, with an explicit error model
model = ETSModel(y,
                error='add',           # 'add' or 'mul'
                trend='add',           # 'add', 'mul', or None
                seasonal='add',        # 'add', 'mul', or None
                seasonal_periods=12)
results = model.fit()
Raw lines 208 to 268
269## Multivariate Time Series

Multivariate Time Series

Raw lines 269 to 270
271### VAR (Vector Autoregression)
273System of equations where each variable depends on past values of all variables.
275**When to use:**
276- Multiple interrelated time series
277- Bidirectional relationships
278- Granger causality testing
280**Model**: Each variable is AR on all variables:
281- y₁ₜ = c₁ + φ₁₁y₁ₜ₋₁ + φ₁₂y₂ₜ₋₁ + ... + ε₁ₜ
282- y₂ₜ = c₂ + φ₂₁y₁ₜ₋₁ + φ₂₂y₂ₜ₋₁ + ... + ε₂ₜ
284```python
285from statsmodels.tsa.api import VAR
286import pandas as pd
288# Data should be DataFrame with multiple columns
289# Each column is a time series
290df_multivariate = pd.DataFrame({'series1': y1, 'series2': y2, 'series3': y3})
292# Fit VAR
293model = VAR(df_multivariate)
295# Select lag order using AIC/BIC
296lag_order_results = model.select_order(maxlags=15)
297print(lag_order_results.summary())
299# Fit with optimal lags
300results = model.fit(maxlags=5, ic='aic')
301print(results.summary())
302```
304**Granger causality testing:**
305```python
306# Test if series1 Granger-causes series2
307from statsmodels.tsa.stattools import grangercausalitytests
309# Requires 2D array [series2, series1]
310test_data = df_multivariate[['series2', 'series1']]
312# Test up to max_lag
313max_lag = 5
314granger_tests = grangercausalitytests(test_data, max_lag)
316# P-values for each lag
317for lag in range(1, max_lag + 1):
318 p_value = granger_tests[lag][0]['ssr_ftest'][1]
319 print(f"Lag {lag}: p-value = {p_value:.4f}")
320```
322Granger predictability does not identify an intervention effect. Prespecify lags or correct for searching across them. Orthogonalized IRFs depend on Cholesky variable ordering and structural assumptions. Forecast intervals condition on the fitted parameters/model and are not guarantees of out-of-sample coverage.
324**Impulse Response Functions (IRF):**
325```python
326# Trace effect of shock through system
327irf = results.irf(10) # 10 periods ahead
329# Plot IRFs
330irf.plot(orth=True) # Orthogonalized (Cholesky decomposition)
331plt.show()
333# Cumulative effects
334irf.plot_cum_effects(orth=True)
335plt.show()
336```
338**Forecast Error Variance Decomposition:**
339```python
340# Contribution of each variable to forecast error variance
341fevd = results.fevd(10) # 10 periods ahead
342fevd.plot()
343plt.show()
344```

VAR (Vector Autoregression)

System of equations where each variable depends on past values of all variables.

When to use:

  • Multiple interrelated time series
  • Bidirectional relationships
  • Granger causality testing

Model: Each variable is AR on all variables:

  • y₁ₜ = c₁ + φ₁₁y₁ₜ₋₁ + φ₁₂y₂ₜ₋₁ + ... + ε₁ₜ
  • y₂ₜ = c₂ + φ₂₁y₁ₜ₋₁ + φ₂₂y₂ₜ₋₁ + ... + ε₂ₜ
from statsmodels.tsa.api import VAR
import pandas as pd

# Data should be DataFrame with multiple columns
# Each column is a time series
df_multivariate = pd.DataFrame({'series1': y1, 'series2': y2, 'series3': y3})

# Fit VAR
model = VAR(df_multivariate)

# Select lag order using AIC/BIC
lag_order_results = model.select_order(maxlags=15)
print(lag_order_results.summary())

# Fit with optimal lags
results = model.fit(maxlags=5, ic='aic')
print(results.summary())

Granger causality testing:

# Test if series1 Granger-causes series2
from statsmodels.tsa.stattools import grangercausalitytests

# Requires 2D array [series2, series1]
test_data = df_multivariate[['series2', 'series1']]

# Test up to max_lag
max_lag = 5
granger_tests = grangercausalitytests(test_data, max_lag)

# P-values for each lag
for lag in range(1, max_lag + 1):
    p_value = granger_tests[lag][0]['ssr_ftest'][1]
    print(f"Lag {lag}: p-value = {p_value:.4f}")

Granger predictability does not identify an intervention effect. Prespecify lags or correct for searching across them. Orthogonalized IRFs depend on Cholesky variable ordering and structural assumptions. Forecast intervals condition on the fitted parameters/model and are not guarantees of out-of-sample coverage.

Impulse Response Functions (IRF):

# Trace effect of shock through system
irf = results.irf(10)  # 10 periods ahead

# Plot IRFs
irf.plot(orth=True)  # Orthogonalized (Cholesky decomposition)
plt.show()

# Cumulative effects
irf.plot_cum_effects(orth=True)
plt.show()

Forecast Error Variance Decomposition:

# Contribution of each variable to forecast error variance
fevd = results.fevd(10)  # 10 periods ahead
fevd.plot()
plt.show()
Raw lines 271 to 345
346### VARMAX (VAR with Moving Average and Exogenous Variables)
348Extends VAR with MA component and external regressors.
350**When to use:**
351- VAR inadequate (MA component needed)
352- External variables affect system
353- More flexible multivariate model
355```python
356from statsmodels.tsa.statespace.varmax import VARMAX
358# VARMAX(p, q) with exogenous variables
359model = VARMAX(df_multivariate,
360 order=(1, 0), # VAR example; unrestricted VARMA(p>0,q>0) is not identified
361 exog=X_exog)
362results = model.fit()
364print(results.summary())
365```

VARMAX (VAR with Moving Average and Exogenous Variables)

Extends VAR with MA component and external regressors.

When to use:

  • VAR inadequate (MA component needed)
  • External variables affect system
  • More flexible multivariate model
from statsmodels.tsa.statespace.varmax import VARMAX

# VARMAX(p, q) with exogenous variables
model = VARMAX(df_multivariate,
               order=(1, 0),        # VAR example; unrestricted VARMA(p>0,q>0) is not identified
               exog=X_exog)
results = model.fit()

print(results.summary())
Raw lines 346 to 366
367## State Space Models
369Flexible framework for custom time series models.
371**When to use:**
372- Custom model specification
373- Unobserved components
374- Kalman filtering/smoothing
375- Missing data
377```python
378from statsmodels.tsa.statespace.mlemodel import MLEModel
380# Extend MLEModel for custom state space models
381# Example: Local level model (random walk + noise)
382```
384**Dynamic Factor Models:**
385```python
386from statsmodels.tsa.statespace.dynamic_factor import DynamicFactor
388# Extract common factors from multiple time series
389model = DynamicFactor(df_multivariate,
390 k_factors=2, # Number of factors
391 factor_order=2) # AR order of factors
392results = model.fit()
394# Estimated factors
395factors = results.factors.filtered
396```

State Space Models

Flexible framework for custom time series models.

When to use:

  • Custom model specification
  • Unobserved components
  • Kalman filtering/smoothing
  • Missing data
from statsmodels.tsa.statespace.mlemodel import MLEModel

# Extend MLEModel for custom state space models
# Example: Local level model (random walk + noise)

Dynamic Factor Models:

from statsmodels.tsa.statespace.dynamic_factor import DynamicFactor

# Extract common factors from multiple time series
model = DynamicFactor(df_multivariate,
                      k_factors=2,          # Number of factors
                      factor_order=2)       # AR order of factors
results = model.fit()

# Estimated factors
factors = results.factors.filtered
Raw lines 367 to 397
398## Forecasting
400### Point Forecasts
402```python
403# ARIMA forecasting
404model = ARIMA(y, order=(1, 1, 1))
405results = model.fit()
407# Forecast h steps ahead
408h = 10
409forecast = results.forecast(steps=h)
411# With exogenous variables (SARIMAX)
412model = SARIMAX(y, exog=X, order=(1, 1, 1))
413results = model.fit()
415# Need future exogenous values
416forecast = results.forecast(steps=h, exog=X_future)
417```

Point Forecasts

# ARIMA forecasting
model = ARIMA(y, order=(1, 1, 1))
results = model.fit()

# Forecast h steps ahead
h = 10
forecast = results.forecast(steps=h)

# With exogenous variables (SARIMAX)
model = SARIMAX(y, exog=X, order=(1, 1, 1))
results = model.fit()

# Need future exogenous values
forecast = results.forecast(steps=h, exog=X_future)
Raw lines 400 to 418
419### Prediction Intervals
421```python
422# Continuing the SARIMAX fit with exogenous regressors above:
423forecast_obj = results.get_forecast(steps=h, exog=X_future)
424forecast_df = forecast_obj.summary_frame()
426print(forecast_df)
427# Contains: mean, mean_se, mean_ci_lower, mean_ci_upper
429# Extract components
430forecast_mean = forecast_df['mean']
431forecast_ci_lower = forecast_df['mean_ci_lower']
432forecast_ci_upper = forecast_df['mean_ci_upper']
434# Plot
435import matplotlib.pyplot as plt
437plt.figure(figsize=(12, 6))
438plt.plot(y.index, y, label='Historical')
439plt.plot(forecast_df.index, forecast_mean, label='Forecast', color='red')
440plt.fill_between(forecast_df.index,
441 forecast_ci_lower,
442 forecast_ci_upper,
443 alpha=0.3, color='red', label='95% prediction interval')
444plt.legend()
445plt.title('Forecast with Prediction Intervals')
446plt.show()
447```
449The state-space summary uses `mean_ci_*` labels for forecast uncertainty. By default, forecast-error variance includes future observation uncertainty conditional on fitted parameters; it is not the same target as a GLM mean confidence interval.

Prediction Intervals

# Continuing the SARIMAX fit with exogenous regressors above:
forecast_obj = results.get_forecast(steps=h, exog=X_future)
forecast_df = forecast_obj.summary_frame()

print(forecast_df)
# Contains: mean, mean_se, mean_ci_lower, mean_ci_upper

# Extract components
forecast_mean = forecast_df['mean']
forecast_ci_lower = forecast_df['mean_ci_lower']
forecast_ci_upper = forecast_df['mean_ci_upper']

# Plot
import matplotlib.pyplot as plt

plt.figure(figsize=(12, 6))
plt.plot(y.index, y, label='Historical')
plt.plot(forecast_df.index, forecast_mean, label='Forecast', color='red')
plt.fill_between(forecast_df.index,
                 forecast_ci_lower,
                 forecast_ci_upper,
                 alpha=0.3, color='red', label='95% prediction interval')
plt.legend()
plt.title('Forecast with Prediction Intervals')
plt.show()

The state-space summary uses mean_ci_* labels for forecast uncertainty. By default, forecast-error variance includes future observation uncertainty conditional on fitted parameters; it is not the same target as a GLM mean confidence interval.

Raw lines 419 to 450
451### Dynamic vs Static Forecasts
453```python
454# Static (one-step-ahead, using actual values)
455static_forecast = results.get_prediction(start=split_point, end=len(y)-1)
457# Dynamic (multi-step, using predicted values)
458dynamic_forecast = results.get_prediction(start=split_point,
459 end=len(y)-1,
460 dynamic=True)
462# Plot comparison
463fig, ax = plt.subplots(figsize=(12, 6))
464y.plot(ax=ax, label='Actual')
465static_forecast.predicted_mean.plot(ax=ax, label='Static forecast')
466dynamic_forecast.predicted_mean.plot(ax=ax, label='Dynamic forecast')
467ax.legend()
468plt.show()
469```

Dynamic vs Static Forecasts

# Static (one-step-ahead, using actual values)
static_forecast = results.get_prediction(start=split_point, end=len(y)-1)

# Dynamic (multi-step, using predicted values)
dynamic_forecast = results.get_prediction(start=split_point,
                                          end=len(y)-1,
                                          dynamic=True)

# Plot comparison
fig, ax = plt.subplots(figsize=(12, 6))
y.plot(ax=ax, label='Actual')
static_forecast.predicted_mean.plot(ax=ax, label='Static forecast')
dynamic_forecast.predicted_mean.plot(ax=ax, label='Dynamic forecast')
ax.legend()
plt.show()
Raw lines 451 to 470
471## Diagnostic Tests

Diagnostic Tests

Raw lines 471 to 472
473### Stationarity Tests
475```python
476from statsmodels.tsa.stattools import adfuller, kpss
478# Augmented Dickey-Fuller (ADF) test
479# H0: unit root (non-stationary)
480adf_result = adfuller(y, autolag='AIC', result_object=True)
481print(f"ADF Statistic: {adf_result.statistic:.4f}")
482print(f"p-value: {adf_result.pvalue:.4f}")
483if adf_result.pvalue <= 0.05:
484 print("Reject the unit-root null for this specification")
485else:
486 print("Unit-root null not rejected; stationarity remains unresolved")
488# KPSS test
489# H0: stationary (opposite of ADF)
490kpss_result = kpss(y, regression='c', nlags='auto', result_object=True)
491print(f"KPSS Statistic: {kpss_result.statistic:.4f}")
492print(f"p-value: {kpss_result.pvalue:.4f}")
493if kpss_result.pvalue <= 0.05:
494 print("Reject level-stationarity null for this specification")
495else:
496 print("Level-stationarity null not rejected; not proof of stationarity")
497```

Stationarity Tests

from statsmodels.tsa.stattools import adfuller, kpss

# Augmented Dickey-Fuller (ADF) test
# H0: unit root (non-stationary)
adf_result = adfuller(y, autolag='AIC', result_object=True)
print(f"ADF Statistic: {adf_result.statistic:.4f}")
print(f"p-value: {adf_result.pvalue:.4f}")
if adf_result.pvalue <= 0.05:
    print("Reject the unit-root null for this specification")
else:
    print("Unit-root null not rejected; stationarity remains unresolved")

# KPSS test
# H0: stationary (opposite of ADF)
kpss_result = kpss(y, regression='c', nlags='auto', result_object=True)
print(f"KPSS Statistic: {kpss_result.statistic:.4f}")
print(f"p-value: {kpss_result.pvalue:.4f}")
if kpss_result.pvalue <= 0.05:
    print("Reject level-stationarity null for this specification")
else:
    print("Level-stationarity null not rejected; not proof of stationarity")
Raw lines 473 to 498
499### Residual Diagnostics
501```python
502# Ljung-Box test for autocorrelation in residuals
503from statsmodels.stats.diagnostic import acorr_ljungbox
505# For the ARIMA(1,1,1) example, p+q=2 fitted dynamic parameters.
506# Use standardized one-step forecast errors after initialization burn-in.
507innov = results.filter_results.standardized_forecasts_error[0]
508innov = innov[results.loglikelihood_burn:]
509lb_test = acorr_ljungbox(innov, lags=[10], model_df=2, return_df=True)
510print(lb_test)
511# A large p-value is failure to reject, not evidence that the model is correct.
512# Adjust model_df to the fitted dynamic structure; lags must exceed model_df.
514# Plot residual diagnostics
515results.plot_diagnostics(figsize=(12, 8))
516plt.show()
518# Components:
519# 1. Standardized residuals over time
520# 2. Histogram + KDE of residuals
521# 3. Q-Q plot for normality
522# 4. Correlogram (ACF of residuals)
523```

Residual Diagnostics

# Ljung-Box test for autocorrelation in residuals
from statsmodels.stats.diagnostic import acorr_ljungbox

# For the ARIMA(1,1,1) example, p+q=2 fitted dynamic parameters.
# Use standardized one-step forecast errors after initialization burn-in.
innov = results.filter_results.standardized_forecasts_error[0]
innov = innov[results.loglikelihood_burn:]
lb_test = acorr_ljungbox(innov, lags=[10], model_df=2, return_df=True)
print(lb_test)
# A large p-value is failure to reject, not evidence that the model is correct.
# Adjust model_df to the fitted dynamic structure; lags must exceed model_df.

# Plot residual diagnostics
results.plot_diagnostics(figsize=(12, 8))
plt.show()

# Components:
# 1. Standardized residuals over time
# 2. Histogram + KDE of residuals
# 3. Q-Q plot for normality
# 4. Correlogram (ACF of residuals)
Raw lines 499 to 524
525### Heteroskedasticity Tests
527```python
528from statsmodels.stats.diagnostic import het_arch
530# ARCH test for heteroskedasticity
531arch_test = het_arch(innov, nlags=10, ddof=2, result_object=True)
532print(f"ARCH test statistic: {arch_test.lm:.4f}")
533print(f"p-value: {arch_test.lmpval:.4f}")
535# If significant, consider GARCH model
536```

Heteroskedasticity Tests

from statsmodels.stats.diagnostic import het_arch

# ARCH test for heteroskedasticity
arch_test = het_arch(innov, nlags=10, ddof=2, result_object=True)
print(f"ARCH test statistic: {arch_test.lm:.4f}")
print(f"p-value: {arch_test.lmpval:.4f}")

# If significant, consider GARCH model
Raw lines 525 to 537
538## Seasonal Decomposition
540```python
541from statsmodels.tsa.seasonal import seasonal_decompose
543# Decompose into trend, seasonal, residual
544decomposition = seasonal_decompose(y,
545 model='additive', # or 'multiplicative'
546 period=12) # seasonal period
548# Plot components
549fig = decomposition.plot()
550fig.set_size_inches(12, 8)
551plt.show()
553# Access components
554trend = decomposition.trend
555seasonal = decomposition.seasonal
556residual = decomposition.resid
558# STL decomposition (more robust)
559from statsmodels.tsa.seasonal import STL
561stl = STL(y, period=12, seasonal=13, robust=True) # seasonal smoother length is odd
562stl_result = stl.fit()
564fig = stl_result.plot()
565plt.show()
567# Multiple seasonalities, e.g., hourly data with daily and weekly patterns
568from statsmodels.tsa.seasonal import MSTL
570mstl_result = MSTL(hourly_series, periods=(24, 24 * 7)).fit()
571fig = mstl_result.plot()
572plt.show()
573```

Seasonal Decomposition

from statsmodels.tsa.seasonal import seasonal_decompose

# Decompose into trend, seasonal, residual
decomposition = seasonal_decompose(y,
                                   model='additive',  # or 'multiplicative'
                                   period=12)         # seasonal period

# Plot components
fig = decomposition.plot()
fig.set_size_inches(12, 8)
plt.show()

# Access components
trend = decomposition.trend
seasonal = decomposition.seasonal
residual = decomposition.resid

# STL decomposition (more robust)
from statsmodels.tsa.seasonal import STL

stl = STL(y, period=12, seasonal=13, robust=True)  # seasonal smoother length is odd
stl_result = stl.fit()

fig = stl_result.plot()
plt.show()

# Multiple seasonalities, e.g., hourly data with daily and weekly patterns
from statsmodels.tsa.seasonal import MSTL

mstl_result = MSTL(hourly_series, periods=(24, 24 * 7)).fit()
fig = mstl_result.plot()
plt.show()
Raw lines 538 to 574
575## Model Evaluation

Model Evaluation

Raw lines 575 to 576
577### In-Sample Metrics
579```python
580# From results object
581print(f"AIC: {results.aic:.2f}")
582print(f"BIC: {results.bic:.2f}")
583print(f"Log-likelihood: {results.llf:.2f}")
585# MSE on training data
586from sklearn.metrics import mean_squared_error
588mse = mean_squared_error(y, results.fittedvalues)
589rmse = np.sqrt(mse)
590print(f"RMSE: {rmse:.4f}")
592# MAE
593from sklearn.metrics import mean_absolute_error
594mae = mean_absolute_error(y, results.fittedvalues)
595print(f"MAE: {mae:.4f}")
596```

In-Sample Metrics

# From results object
print(f"AIC: {results.aic:.2f}")
print(f"BIC: {results.bic:.2f}")
print(f"Log-likelihood: {results.llf:.2f}")

# MSE on training data
from sklearn.metrics import mean_squared_error

mse = mean_squared_error(y, results.fittedvalues)
rmse = np.sqrt(mse)
print(f"RMSE: {rmse:.4f}")

# MAE
from sklearn.metrics import mean_absolute_error
mae = mean_absolute_error(y, results.fittedvalues)
print(f"MAE: {mae:.4f}")
Raw lines 577 to 597
598### Out-of-Sample Evaluation
600```python
601# Train-test split for time series (no shuffle!)
602train_size = int(0.8 * len(y))
603y_train = y.iloc[:train_size]
604y_test = y.iloc[train_size:]
606# Fit on training data
607model = ARIMA(y_train, order=(1, 1, 1))
608results = model.fit()
610# Forecast test period
611forecast = results.forecast(steps=len(y_test))
613# Metrics
614from sklearn.metrics import mean_squared_error, mean_absolute_error
616rmse = np.sqrt(mean_squared_error(y_test, forecast))
617mae = mean_absolute_error(y_test, forecast)
618mape = (np.mean(np.abs((y_test - forecast) / y_test)) * 100
619 if np.all(np.asarray(y_test) > 0) else np.nan)
621print(f"Test RMSE: {rmse:.4f}")
622print(f"Test MAE: {mae:.4f}")
623print(f"Test MAPE: {mape:.2f}%")
624```

Out-of-Sample Evaluation

# Train-test split for time series (no shuffle!)
train_size = int(0.8 * len(y))
y_train = y.iloc[:train_size]
y_test = y.iloc[train_size:]

# Fit on training data
model = ARIMA(y_train, order=(1, 1, 1))
results = model.fit()

# Forecast test period
forecast = results.forecast(steps=len(y_test))

# Metrics
from sklearn.metrics import mean_squared_error, mean_absolute_error

rmse = np.sqrt(mean_squared_error(y_test, forecast))
mae = mean_absolute_error(y_test, forecast)
mape = (np.mean(np.abs((y_test - forecast) / y_test)) * 100
        if np.all(np.asarray(y_test) > 0) else np.nan)

print(f"Test RMSE: {rmse:.4f}")
print(f"Test MAE: {mae:.4f}")
print(f"Test MAPE: {mape:.2f}%")
Raw lines 598 to 625
626### Rolling Forecast
628```python
629# More realistic evaluation: rolling one-step-ahead forecasts
630forecasts = []
632for t in range(len(y_test)):
633 # Refit or update with new observation
634 y_current = y.iloc[:train_size + t]
635 model = ARIMA(y_current, order=(1, 1, 1))
636 fit = model.fit()
638 # One-step forecast
639 fc = np.asarray(fit.forecast(steps=1))[0]
640 forecasts.append(fc)
642forecasts = np.array(forecasts)
644rmse = np.sqrt(mean_squared_error(y_test, forecasts))
645print(f"Rolling forecast RMSE: {rmse:.4f}")
646```

Rolling Forecast

# More realistic evaluation: rolling one-step-ahead forecasts
forecasts = []

for t in range(len(y_test)):
    # Refit or update with new observation
    y_current = y.iloc[:train_size + t]
    model = ARIMA(y_current, order=(1, 1, 1))
    fit = model.fit()

    # One-step forecast
    fc = np.asarray(fit.forecast(steps=1))[0]
    forecasts.append(fc)

forecasts = np.array(forecasts)

rmse = np.sqrt(mean_squared_error(y_test, forecasts))
print(f"Rolling forecast RMSE: {rmse:.4f}")
Raw lines 626 to 647
648### Cross-Validation
650```python
651# Time series cross-validation (expanding window)
652from sklearn.model_selection import TimeSeriesSplit
654tscv = TimeSeriesSplit(n_splits=5)
655rmse_scores = []
657for train_idx, test_idx in tscv.split(y):
658 y_train_cv = y.iloc[train_idx]
659 y_test_cv = y.iloc[test_idx]
661 model = ARIMA(y_train_cv, order=(1, 1, 1))
662 results = model.fit()
664 forecast = results.forecast(steps=len(test_idx))
665 rmse = np.sqrt(mean_squared_error(y_test_cv, forecast))
666 rmse_scores.append(rmse)
668print(f"CV RMSE: {np.mean(rmse_scores):.4f} ± {np.std(rmse_scores):.4f}")
669```

Cross-Validation

# Time series cross-validation (expanding window)
from sklearn.model_selection import TimeSeriesSplit

tscv = TimeSeriesSplit(n_splits=5)
rmse_scores = []

for train_idx, test_idx in tscv.split(y):
    y_train_cv = y.iloc[train_idx]
    y_test_cv = y.iloc[test_idx]

    model = ARIMA(y_train_cv, order=(1, 1, 1))
    results = model.fit()

    forecast = results.forecast(steps=len(test_idx))
    rmse = np.sqrt(mean_squared_error(y_test_cv, forecast))
    rmse_scores.append(rmse)

print(f"CV RMSE: {np.mean(rmse_scores):.4f} ± {np.std(rmse_scores):.4f}")
Raw lines 648 to 670
671## Advanced Topics

Advanced Topics

Raw lines 671 to 672
673### ARDL (Autoregressive Distributed Lag)
675Bridges univariate and multivariate time series.
677```python
678from statsmodels.tsa.ardl import ARDL
680# ARDL(p, q) model
681# y depends on its own lags and lags of X
682model = ARDL(y, lags=2, exog=X, order=2)
683results = model.fit()
684```

ARDL (Autoregressive Distributed Lag)

Bridges univariate and multivariate time series.

from statsmodels.tsa.ardl import ARDL

# ARDL(p, q) model
# y depends on its own lags and lags of X
model = ARDL(y, lags=2, exog=X, order=2)
results = model.fit()
Raw lines 673 to 685
686### Error Correction Models
688For cointegrated series.
690```python
691from statsmodels.tsa.vector_ar.vecm import coint_johansen
693# Test for cointegration
694johansen_test = coint_johansen(df_multivariate, det_order=0, k_ar_diff=1)
696# Fit VECM if cointegrated
697from statsmodels.tsa.vector_ar.vecm import VECM
699model = VECM(df_multivariate, k_ar_diff=1, coint_rank=1)
700results = model.fit()
701```

Error Correction Models

For cointegrated series.

from statsmodels.tsa.vector_ar.vecm import coint_johansen

# Test for cointegration
johansen_test = coint_johansen(df_multivariate, det_order=0, k_ar_diff=1)

# Fit VECM if cointegrated
from statsmodels.tsa.vector_ar.vecm import VECM

model = VECM(df_multivariate, k_ar_diff=1, coint_rank=1)
results = model.fit()
Raw lines 686 to 702
703### Regime Switching Models
705For structural breaks and regime changes.
707```python
708from statsmodels.tsa.regime_switching.markov_regression import MarkovRegression
710# Switching intercept/variance; MarkovRegression.order does not add AR terms.
711# Use MarkovAutoregression for regime-switching autoregressive coefficients.
712model = MarkovRegression(y, k_regimes=2, switching_variance=True)
713results = model.fit()
715# Smoothed probabilities of regimes
716regime_probs = results.smoothed_marginal_probabilities
717```

Regime Switching Models

For structural breaks and regime changes.

from statsmodels.tsa.regime_switching.markov_regression import MarkovRegression

# Switching intercept/variance; MarkovRegression.order does not add AR terms.
# Use MarkovAutoregression for regime-switching autoregressive coefficients.
model = MarkovRegression(y, k_regimes=2, switching_variance=True)
results = model.fit()

# Smoothed probabilities of regimes
regime_probs = results.smoothed_marginal_probabilities
Raw lines 703 to 718
719## Best Practices
7211. **Check stationarity**: Difference if needed, verify with ADF/KPSS tests
7222. **Plot data**: Always visualize before modeling
7233. **Identify seasonality**: Use appropriate seasonal models (SARIMAX, Holt-Winters)
7244. **Model selection**: Use AIC/BIC and out-of-sample validation
7255. **Residual diagnostics**: Check for autocorrelation, normality, heteroskedasticity
7266. **Forecast evaluation**: Use rolling forecasts and proper time series CV
7277. **Avoid overfitting**: Prefer simpler models, use information criteria
7288. **Document assumptions**: Note any data transformations (log, differencing)
7299. **Prediction intervals**: Always provide uncertainty estimates
73010. **Refit regularly**: Update models as new data arrives

Best Practices

  1. Check stationarity: Difference if needed, verify with ADF/KPSS tests
  2. Plot data: Always visualize before modeling
  3. Identify seasonality: Use appropriate seasonal models (SARIMAX, Holt-Winters)
  4. Model selection: Use AIC/BIC and out-of-sample validation
  5. Residual diagnostics: Check for autocorrelation, normality, heteroskedasticity
  6. Forecast evaluation: Use rolling forecasts and proper time series CV
  7. Avoid overfitting: Prefer simpler models, use information criteria
  8. Document assumptions: Note any data transformations (log, differencing)
  9. Prediction intervals: Always provide uncertainty estimates
  10. Refit regularly: Update models as new data arrives
Raw lines 719 to 731
732## Common Pitfalls
7341. **Misusing stationarity tests**: ADF/KPSS have different nulls and deterministic specifications; ARIMA can model integration through d
7352. **Data leakage**: Using future data in transformations
7363. **Wrong seasonal period**: S=4 for quarterly, S=12 for monthly
7374. **Overfitting**: Too many parameters relative to data
7385. **Ignoring residual autocorrelation**: Model inadequate
7396. **Using inappropriate metrics**: MAPE fails with zeros or negatives
7407. **Not handling missing data**: Affects model estimation
7418. **Extrapolating exogenous variables**: Need future X values for SARIMAX
7429. **Confusing static vs dynamic forecasts**: Dynamic more realistic for multi-step
74310. **Not validating forecasts**: Always check out-of-sample performance

Common Pitfalls

  1. Misusing stationarity tests: ADF/KPSS have different nulls and deterministic specifications; ARIMA can model integration through d
  2. Data leakage: Using future data in transformations
  3. Wrong seasonal period: S=4 for quarterly, S=12 for monthly
  4. Overfitting: Too many parameters relative to data
  5. Ignoring residual autocorrelation: Model inadequate
  6. Using inappropriate metrics: MAPE fails with zeros or negatives
  7. Not handling missing data: Affects model estimation
  8. Extrapolating exogenous variables: Need future X values for SARIMAX
  9. Confusing static vs dynamic forecasts: Dynamic more realistic for multi-step
  10. Not validating forecasts: Always check out-of-sample performance
Raw lines 732 to 743