Principal Component Analysis and Factor Extraction › PCA Applied to Yield Curves
Quantitative FinanceAll chapters Principal Component Analysis and Factor Extraction › PCA Applied to Yield CurvesOne section · Sections in this chapter5 / 13 ↓
Reading tools Definitions & inline links
Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.
Article links
Make inline references clickable
The term structure of interest rates is a standard application of PCA in finance. As we discussed in Part II when covering bond pricing and the term structure, yield curves across maturities move together in systematic ways. PCA extracts the dominant patterns of yield curve movements.
Simulating Yield Curve Data
We'll simulate realistic yield curve data based on the well-documented empirical patterns of level, slope, and curvature movements:
import numpy as np
import pandas as pd
np.random.seed(123)
# Define maturities (in years)
maturities = np.array([0.25, 0.5, 1, 2, 3, 5, 7, 10, 15, 20, 30])
n_maturities = len(maturities)
n_days = 500
# Simulate three factors: level, slope, curvature
level_factor = np.cumsum(np.random.randn(n_days) * 0.05) # Random walk
slope_factor = np.cumsum(np.random.randn(n_days) * 0.03)
curvature_factor = np.cumsum(np.random.randn(n_days) * 0.02)
# Define factor loadings that create realistic yield curve shapes
# Level: affects all maturities equally
level_loadings = np.ones(n_maturities)
# Slope: increases with maturity (negative at short end, positive at long end)
slope_loadings = np.log(maturities + 1) - np.log(maturities + 1).mean()
# Curvature: peaks at intermediate maturities
curvature_loadings = -((maturities - 5) ** 2) / 400 + 0.5
# Generate yield curves
base_curve = 2 + 0.5 * np.log(maturities + 1) # Upward sloping base curve
yields = np.zeros((n_days, n_maturities))
for t in range(n_days):
yields[t, :] = (
base_curve
+ level_factor[t] * level_loadings
+ slope_factor[t] * slope_loadings
+ curvature_factor[t] * curvature_loadings
+ np.random.randn(n_maturities) * 0.003
) # Idiosyncratic noise
# Store as DataFrame
yield_df = pd.DataFrame(yields, columns=[f"{m}Y" for m in maturities])Let's visualize several yield curves from our simulated data:

The yield curves show the typical upward slope with variations in their overall level, steepness, and curvature. This is exactly what our three factors produce.
Applying PCA to Yield Changes
In practice, we usually apply PCA to yield changes rather than yield levels because changes are more stationary. This follows the same logic as our discussion of returns versus prices in the time series chapter.
# Compute daily yield changes
yield_changes = np.diff(yields, axis=0)
# Center the changes (mean is already close to zero for stationary data)
yield_changes_centered = yield_changes - yield_changes.mean(axis=0)
# Compute covariance matrix of yield changes
cov_yields = np.cov(yield_changes_centered, rowvar=False)
# Eigendecomposition
eigenvalues_yield, eigenvectors_yield = np.linalg.eigh(cov_yields)
# Sort descending
sort_idx = np.argsort(eigenvalues_yield)[::-1]
eigenvalues_yield = eigenvalues_yield[sort_idx]
eigenvectors_yield = eigenvectors_yield[:, sort_idx]
# Calculate variance metrics
total_var_yield = eigenvalues_yield.sum()
explained_pct_yield = 100 * eigenvalues_yield / total_var_yield
cumulative_pct_yield = np.cumsum(explained_pct_yield)Variance Explained by Principal Components: --------------------------------------------- PC1: 70.38% (Cumulative: 70.38%) PC2: 28.19% (Cumulative: 98.56%) PC3: 1.08% (Cumulative: 99.65%) PC4: 0.06% (Cumulative: 99.70%) PC5: 0.05% (Cumulative: 99.75%)
The first three principal components explain over 99% of yield curve variation, confirming the well-known empirical result that yield curves have very low effective dimensionality. This finding has practical implications: despite having 11 maturities, we can capture virtually all yield curve dynamics with just three factors.


Interpreting the Principal Components
The loading vectors reveal the economic interpretation of each component:

The three components have clear economic interpretations:
-
PC1 (Level): Nearly constant loadings across maturities. When this component increases, all yields move up by similar amounts, representing a parallel shift of the entire curve.
-
PC2 (Slope): Loadings change monotonically with maturity, positive at one end and negative at the other. A positive shock to this component steepens the curve (short rates fall while long rates rise, or vice versa).
-
PC3 (Curvature): Loadings peak at intermediate maturities with opposite signs at the extremes. This component captures "twist" movements where the middle of the curve moves differently from the ends.
These interpretations are consistent across different countries, time periods, and yield curve datasets. The level factor typically explains 80-90% of variance, slope explains 5-10%, and curvature explains most of the remainder.
Reconstructing Yield Curves
We can reconstruct yield curve changes using only the first few principal components. This demonstrates how dimension reduction works in practice:
# Project yield changes onto first 3 PCs
pc_scores_yield = yield_changes_centered @ eigenvectors_yield[:, :3]
# Reconstruct yield changes using only first 3 PCs
reconstructed_changes = pc_scores_yield @ eigenvectors_yield[:, :3].T
# Add back the mean
reconstructed_changes += yield_changes.mean(axis=0)
# Calculate reconstruction error
reconstruction_error = yield_changes - reconstructed_changes
rmse = np.sqrt((reconstruction_error**2).mean())
max_error = np.abs(reconstruction_error).max()Reconstruction Quality (using 3 PCs): RMSE: 0.003538 Max absolute error: 0.012394 Original data std: 0.059524 Error as % of std: 5.94%
The reconstruction error is tiny compared to the magnitude of yield changes, confirming that three components capture the essential dynamics.

