Seasonal Decomposition — STL and Classical
Statistics
Separating Trend, Seasonality, and Residual Components
Seasonal decomposition breaks a time series into interpretable components — underlying trend, repeating seasonal patterns, and random noise. STL and classical methods each offer advantages for different data characteristics.
-
Retail Planning — Isolate holiday shopping trends from underlying growth
-
Energy Forecasting — Separate daily and weekly cycles from long-term demand shifts
-
Healthcare — Detect flu season patterns from baseline hospital admission trends
Understanding the parts makes the whole pattern clear.
Seasonal decomposition separates a time series into trend, seasonal, and residual components, making patterns easier to identify and model.
Classical Decomposition
Moving Average Method
For additive decomposition with period :
Steps:
-
Compute the centered moving average to estimate
-
Detrend: (additive) or (multiplicative)
-
Average detrended values by season to get
-
Residual:
STL Decomposition
Seasonal and Trend decomposition using Loess (Cleveland et al., 1990) is a more robust and flexible method.
STL Advantages
| Feature | Classical | STL |
|---------|-----------|-----|
| Handles any period | No (fixed s) | Yes |
| Robust to outliers | No | Yes |
| Seasonal pattern adapts | No | Yes |
| Handles missing data | Limited | Yes |
| Computational speed | Fast | Moderate |
Strength of Seasonal Component
The strength of the seasonal component measures how much of the variation is due to seasonality.
| Value | Interpretation |
|-------|---------------|
| 0.6 - 1.0 | Strong seasonality |
| 0.3 - 0.6 | Moderate seasonality |
| 0.0 - 0.3 | Weak seasonality |
Python Implementation
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from statsmodels.tsa.seasonal import seasonal_decompose, STL
# Simulate monthly data with trend and seasonality
np.random.seed(42)
n = 240
t = np.arange(n)
trend = 100 + 0.5 * t
seasonal = 10 * np.sin(2 * np.pi * t / 12)
noise = np.random.randn(n) * 2
y = trend + seasonal + noise
dates = pd.date_range('2005', periods=n, freq='M')
ts = pd.Series(y, index=dates)
# Classical decomposition
result_classical = seasonal_decompose(ts, model='additive', period=12)
# STL decomposition
stl = STL(ts, period=12, robust=True)
result_stl = stl.fit()
# Plot comparison
fig, axes = plt.subplots(4, 2, figsize=(14, 10))
for i, (comp, label) in enumerate(zip(['observed','trend','seasonal','resid'],
['Observed','Trend','Seasonal','Residual'])):
axes[i, 0].plot(result_classical.observed if comp=='observed'
else getattr(result_classical, comp))
axes[i, 0].set_title(f'Classical - {label}')
axes[i, 1].plot(result_stl.observed if comp=='observed'
else getattr(result_stl, comp))
axes[i, 1].set_title(f'STL - {label}')
plt.tight_layout()
plt.show()
# Seasonal strength
var_resid = np.var(result_stl.resid)
var_season_resid = np.var(result_stl.seasonal + result_stl.resid)
F_s = 1 - var_resid / var_season_resid
print(f"Seasonal Strength: {F_s:.3f}")
Worked Example
Key Takeaways
Related Topics
-
See Stationarity for testing stationarity of components
-
See ARIMA Models for forecasting after decomposition
-
See Exponential Smoothing for Holt-Winters with seasonality