7.3 Time Series Decomposition
"Decomposition is a powerful tool for understanding complex time series."— Cleveland et al., STL Decomposition Authors
Breaking down time series into trend, seasonal, and random components
Section Objectives
Upon completing this section, you will be able to:
- Understand the basic principles and applications of time series decomposition
- Master classical decomposition methods (additive/multiplicative models)
- Use STL decomposition (Seasonal-Trend decomposition using Loess)
- Perform seasonal adjustment and trend extraction
- Apply HP filtering and other detrending methods
- Implement various decomposition techniques using Python
Basic Principles of Time Series Decomposition
Why Decompose Time Series?
Core Idea: Many time series data are combinations of multiple components
Classic Case: Retail sales data
- Trend: Long-term growth trend (economic development, population growth)
- Seasonality: Annual recurring patterns (holidays, quarterly effects)
- Cycle: Non-fixed length fluctuations (business cycles)
- Randomness: Unpredictable disturbances
Benefits of Decomposition:
- Understand data structure: Identify main drivers
- Seasonal adjustment: Remove seasonality to see true trends
- Improve forecasting: Model different components separately
- Anomaly detection: Identify unusual fluctuations
Classical Decomposition Models
1. Additive Model
Applicable When: Seasonal fluctuation amplitude does not change over time
Components:
- : Observed value
- : Trend component
- : Seasonal component
- : Random/Residual component
Characteristics:
- Seasonal fluctuations are fixed amplitude (e.g., December sales increase by 1 million)
- Components are mutually independent
2. Multiplicative Model
Applicable When: Seasonal fluctuation amplitude grows with trend
Equivalent Logarithmic Form (converts to additive):
Characteristics:
- Seasonal fluctuations are proportional changes (e.g., December sales increase by 20%)
- Suitable for exponential growth data
How to Choose the Model?
| Characteristic | Additive Model | Multiplicative Model |
|---|---|---|
| Seasonal Fluctuation | Fixed amplitude | Proportional change |
| Data Type | Linear growth | Exponential growth |
| Seasonal Plot | Constant fluctuation amplitude | Increasing fluctuation amplitude |
| Use Cases | Temperature, precipitation | GDP, stock prices, sales |
Python Implementation: Classical Decomposition
Using statsmodels for Decomposition
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from statsmodels.tsa.seasonal import seasonal_decompose
# Settings
plt.rcParams['font.sans-serif'] = ['Arial Unicode MS'] # macOS
plt.rcParams['axes.unicode_minus'] = False
sns.set_style("whitegrid")
# 1. Generate simulated data: trend + seasonality + random
np.random.seed(42)
n = 120 # 10 years monthly data
dates = pd.date_range('2014-01', periods=n, freq='M')
# Trend component: linear growth
trend = 100 + 0.5 * np.arange(n)
# Seasonal component: 12-month cycle
seasonal = 10 * np.sin(2 * np.pi * np.arange(n) / 12)
# Random component
random = np.random.normal(0, 3, n)
# Additive model
y_additive = trend + seasonal + random
# Multiplicative model
seasonal_mult = 1 + 0.1 * np.sin(2 * np.pi * np.arange(n) / 12)
y_multiplicative = trend * seasonal_mult * (1 + np.random.normal(0, 0.03, n))
# Create time series
ts_add = pd.Series(y_additive, index=dates)
ts_mult = pd.Series(y_multiplicative, index=dates)
print("="*70)
print("Time Series Data Preview")
print("="*70)
print(ts_add.head(12))Additive Decomposition
# Additive decomposition
decomposition_add = seasonal_decompose(ts_add, model='additive', period=12)
# Visualization
fig, axes = plt.subplots(4, 1, figsize=(14, 10))
# Original series
axes[0].plot(ts_add, linewidth=1.5, color='black')
axes[0].set_ylabel('Observed', fontsize=12, fontweight='bold')
axes[0].set_title('Original Time Series (Additive Model)', fontsize=14, fontweight='bold')
axes[0].grid(True, alpha=0.3)
# Trend component
axes[1].plot(decomposition_add.trend, linewidth=2, color='blue')
axes[1].set_ylabel('Trend', fontsize=12, fontweight='bold')
axes[1].set_title('Trend Component', fontsize=12, fontweight='bold')
axes[1].grid(True, alpha=0.3)
# Seasonal component
axes[2].plot(decomposition_add.seasonal, linewidth=2, color='green')
axes[2].set_ylabel('Seasonality', fontsize=12, fontweight='bold')
axes[2].set_title('Seasonal Component', fontsize=12, fontweight='bold')
axes[2].axhline(y=0, color='black', linestyle='--', alpha=0.5)
axes[2].grid(True, alpha=0.3)
# Random component
axes[3].plot(decomposition_add.resid, linewidth=1, color='red', alpha=0.7)
axes[3].set_ylabel('Residual', fontsize=12, fontweight='bold')
axes[3].set_title('Random Component (Residual)', fontsize=12, fontweight='bold')
axes[3].axhline(y=0, color='black', linestyle='--', alpha=0.5)
axes[3].set_xlabel('Time', fontsize=12, fontweight='bold')
axes[3].grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('time_series_decomposition_additive.png', dpi=300, bbox_inches='tight')
plt.show()
# Output statistics
print("\n" + "="*70)
print("Decomposition Results Statistics")
print("="*70)
print(f"Trend component range: {decomposition_add.trend.min():.2f} - {decomposition_add.trend.max():.2f}")
print(f"Seasonal component range: {decomposition_add.seasonal.min():.2f} - {decomposition_add.seasonal.max():.2f}")
print(f"Random component standard deviation: {decomposition_add.resid.std():.2f}")STL Decomposition (Seasonal-Trend decomposition using Loess)
Advantages of STL
Limitations of Classical Decomposition:
- Trend estimation unstable at series endpoints
- Assumes constant seasonal component
- Sensitive to outliers
STL Improvements:
- Uses LOESS (locally weighted regression) smoothing
- Allows seasonality to change over time
- Robust to outliers
Mathematical Principles of STL
Core Idea: Iteratively separate trend and seasonality
Algorithm Steps:
- Initial detrending:
- Extract seasonality: smooth each seasonal period
- Deseasonalize:
- Re-estimate trend: smooth deseasonalized data
- Iterate until convergence
Python Implementation of STL
from statsmodels.tsa.seasonal import STL
# STL decomposition
stl = STL(ts_add, seasonal=13, trend=51, robust=True)
result = stl.fit()
# Visualization
fig, axes = plt.subplots(4, 1, figsize=(14, 10))
axes[0].plot(ts_add, linewidth=1.5, color='black')
axes[0].set_ylabel('Observed', fontsize=12, fontweight='bold')
axes[0].set_title('STL Decomposition: Original Series', fontsize=14, fontweight='bold')
axes[0].grid(True, alpha=0.3)
axes[1].plot(result.trend, linewidth=2, color='blue')
axes[1].set_ylabel('Trend', fontsize=12, fontweight='bold')
axes[1].set_title('STL Trend (Smoother)', fontsize=12, fontweight='bold')
axes[1].grid(True, alpha=0.3)
axes[2].plot(result.seasonal, linewidth=2, color='green')
axes[2].set_ylabel('Seasonality', fontsize=12, fontweight='bold')
axes[2].set_title('STL Seasonal Component (Can Vary Over Time)', fontsize=12, fontweight='bold')
axes[2].axhline(y=0, color='black', linestyle='--', alpha=0.5)
axes[2].grid(True, alpha=0.3)
axes[3].plot(result.resid, linewidth=1, color='red', alpha=0.7)
axes[3].set_ylabel('Residual', fontsize=12, fontweight='bold')
axes[3].set_title('STL Residual', fontsize=12, fontweight='bold')
axes[3].axhline(y=0, color='black', linestyle='--', alpha=0.5)
axes[3].set_xlabel('Time', fontsize=12, fontweight='bold')
axes[3].grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('stl_decomposition.png', dpi=300, bbox_inches='tight')
plt.show()
# Compare classical decomposition and STL
print("\n" + "="*70)
print("Classical Decomposition vs STL Comparison")
print("="*70)
print(f"Classical decomposition residual std: {decomposition_add.resid.std():.4f}")
print(f"STL residual std: {result.resid.std():.4f}")
print(f"\nSTL improvement ratio: {(1 - result.resid.std() / decomposition_add.resid.std()) * 100:.2f}%")HP Filter (Hodrick-Prescott Filter)
Principles of HP Filter
Objective: Extract smooth trend from time series
Optimization Problem:
Explanation:
- First term: Trend should fit data (minimize deviation)
- Second term: Trend should be smooth (penalize second-order difference)
- : Smoothing parameter (larger = smoother)
Standard λ Values:
- Annual data: λ = 100
- Quarterly data: λ = 1,600
- Monthly data: λ = 14,400
Python Implementation of HP Filter
from statsmodels.tsa.filters.hp_filter import hpfilter
# HP filter
cycle_add, trend_hp = hpfilter(ts_add, lamb=14400)
# Visualization
fig, axes = plt.subplots(3, 1, figsize=(14, 10))
# Original data + HP trend
axes[0].plot(ts_add, linewidth=1.5, alpha=0.6, color='gray', label='Original Data')
axes[0].plot(trend_hp, linewidth=2.5, color='blue', label='HP Trend (λ=14400)')
axes[0].plot(decomposition_add.trend, linewidth=2, color='red', linestyle='--',
alpha=0.7, label='Classical Decomposition Trend')
axes[0].set_title('HP Filter Trend Extraction', fontsize=14, fontweight='bold')
axes[0].set_ylabel('Value', fontsize=11)
axes[0].legend()
axes[0].grid(True, alpha=0.3)
# HP cycle component
axes[1].plot(cycle_add, linewidth=1.5, color='purple')
axes[1].axhline(y=0, color='black', linestyle='--', alpha=0.5)
axes[1].set_title('HP Cycle Component (Detrended)', fontsize=12, fontweight='bold')
axes[1].set_ylabel('Deviation from Trend', fontsize=11)
axes[1].grid(True, alpha=0.3)
# Comparison of different λ
lambdas = [1600, 14400, 100000]
for lamb in lambdas:
_, trend_temp = hpfilter(ts_add, lamb=lamb)
axes[2].plot(trend_temp, linewidth=2, label=f'λ={lamb}', alpha=0.7)
axes[2].plot(ts_add, linewidth=0.5, alpha=0.3, color='black', label='Original Data')
axes[2].set_title('HP Filter Comparison with Different λ Parameters', fontsize=12, fontweight='bold')
axes[2].set_xlabel('Time', fontsize=12, fontweight='bold')
axes[2].set_ylabel('Value', fontsize=11)
axes[2].legend()
axes[2].grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('hp_filter.png', dpi=300, bbox_inches='tight')
plt.show()
print("\n" + "="*70)
print("HP Filter Results Statistics")
print("="*70)
print(f"Cycle component mean: {cycle_add.mean():.4f} (should be close to 0)")
print(f"Cycle component std: {cycle_add.std():.4f}")
print(f"Trend component std: {trend_hp.std():.4f}")Section Summary
Comparison of Decomposition Methods
| Method | Advantages | Disadvantages | Use Cases |
|---|---|---|---|
| Classical Decomposition | Simple and intuitive, fast computation | Unstable at endpoints, sensitive to outliers | Initial exploration |
| STL | Flexible, robust to outliers | Complex computation, parameter tuning | Complex seasonality |
| HP Filter | Extracts smooth trend | Does not separate seasonality | Macroeconomic cycle analysis |
| X-13 | Official standard, comprehensive | Complex, requires specialized software | Government statistics |
Practice Checklist
When decomposing time series, ensure you complete the following steps:
- [ ] Plot original data, observe trend and seasonality
- [ ] Choose appropriate decomposition model (additive vs multiplicative)
- [ ] Check if residuals are white noise (no autocorrelation)
- [ ] Verify decomposition quality:
- [ ] Plot seasonal pattern, check if reasonable
- [ ] Analyze seasonally adjusted data
- [ ] Record decomposition parameters for reproducibility
Next Section Preview
In the next section, we will learn how to use ARIMA models for time series forecasting.
Extended Reading
Cleveland, R. B., et al. (1990). "STL: A Seasonal-Trend Decomposition Procedure Based on Loess." Journal of Official Statistics, 6(1), 3-73.
Hodrick, R. J., & Prescott, E. C. (1997). "Postwar U.S. Business Cycles: An Empirical Investigation." Journal of Money, Credit and Banking, 29(1), 1-16.
Hyndman, R. J., & Athanasopoulos, G. (2021). Forecasting: Principles and Practice (3rd ed.). OTexts. Chapter 3.
Hamilton, J. D. (2018). "Why You Should Never Use the Hodrick-Prescott Filter." Review of Economics and Statistics, 100(5), 831-843.
Understanding data starts with decomposition!