Skip to content

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

DifficultyImportance


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:

  1. Understand data structure: Identify main drivers
  2. Seasonal adjustment: Remove seasonality to see true trends
  3. Improve forecasting: Model different components separately
  4. 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?

CharacteristicAdditive ModelMultiplicative Model
Seasonal FluctuationFixed amplitudeProportional change
Data TypeLinear growthExponential growth
Seasonal PlotConstant fluctuation amplitudeIncreasing fluctuation amplitude
Use CasesTemperature, precipitationGDP, stock prices, sales

Python Implementation: Classical Decomposition

Using statsmodels for Decomposition

python
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

python
# 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:

  1. Initial detrending:
  2. Extract seasonality: smooth each seasonal period
  3. Deseasonalize:
  4. Re-estimate trend: smooth deseasonalized data
  5. Iterate until convergence

Python Implementation of STL

python
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

python
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

MethodAdvantagesDisadvantagesUse Cases
Classical DecompositionSimple and intuitive, fast computationUnstable at endpoints, sensitive to outliersInitial exploration
STLFlexible, robust to outliersComplex computation, parameter tuningComplex seasonality
HP FilterExtracts smooth trendDoes not separate seasonalityMacroeconomic cycle analysis
X-13Official standard, comprehensiveComplex, requires specialized softwareGovernment 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

  1. Cleveland, R. B., et al. (1990). "STL: A Seasonal-Trend Decomposition Procedure Based on Loess." Journal of Official Statistics, 6(1), 3-73.

  2. 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.

  3. Hyndman, R. J., & Athanasopoulos, G. (2021). Forecasting: Principles and Practice (3rd ed.). OTexts. Chapter 3.

  4. 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!

Released under the MIT License. Content © Author.