Risk-Premium PCA for estimating latent asset-pricing factors
Project description
RPPCA — Risk-Premium PCA for Asset Pricing
Estimate latent asset-pricing factors that explain both covariance and expected returns.
Installation • Quick Start • Algorithm • API Reference • Look-Ahead Bias • Citation
Overview
RPPCA is a Python implementation of the Risk-Premium PCA (RP-PCA) method from:
Lettau, M. & Pelger, M. (2020). "Estimating latent asset-pricing factors." Journal of Econometrics, 218(1), 1–31.
Standard PCA finds factors that maximize explained variance, but ignores expected returns. RP-PCA generalizes PCA by adding a penalty term for pricing errors, enabling it to:
- 🔍 Detect weak factors with high Sharpe ratios that PCA misses entirely
- 📈 Produce factors with higher Sharpe ratios — often 2× those of PCA
- 📉 Achieve smaller out-of-sample pricing errors
- 📊 Explain the same amount of variance as conventional PCA
Installation
pip install rppca
Requirements: Python ≥ 3.9, NumPy ≥ 1.21
Quick Start
In-Sample Estimation
import numpy as np
from rppca import RPPCA
# X: excess returns matrix (T periods × N assets)
X = np.random.randn(600, 200)
# Estimate 5 factors with γ=10 (paper's recommended value)
model = RPPCA(n_factors=5, gamma=10)
model.fit(X)
# Access results
print(model.factors_.shape) # (600, 5)
print(model.loadings_.shape) # (200, 5)
print(model.eigenvalues_) # Top 5 eigenvalues
# Evaluation metrics
print(f"Max Sharpe Ratio: {model.get_max_sharpe_ratio():.4f}")
print(f"RMS Pricing Error: {model.get_rms_pricing_error(X):.6f}")
print(f"Explained Variation: {model.get_explained_variation(X):.2%}")
Out-of-Sample Estimation (No Look-Ahead Bias)
from rppca import RollingRPPCA
# Rolling window: estimate loadings on past 240 months, project forward
rolling = RollingRPPCA(n_factors=5, gamma=10, window=240)
rolling.fit(X)
# Out-of-sample factors (strictly causal — no future information used)
print(rolling.oos_factors_.shape) # (360, 5) — T minus window
print(rolling.oos_start_) # 240
# Out-of-sample evaluation
print(f"OOS Max Sharpe Ratio: {rolling.get_oos_max_sharpe_ratio():.4f}")
print(f"OOS RMS α: {rolling.get_oos_rms_pricing_error(X):.6f}")
Compare RP-PCA vs Standard PCA
from rppca import RPPCA
# Standard PCA (γ = -1)
pca = RPPCA(n_factors=5, gamma=-1)
pca.fit(X)
# RP-PCA (γ = 10)
rppca = RPPCA(n_factors=5, gamma=10)
rppca.fit(X)
print(f"PCA Max SR: {pca.get_max_sharpe_ratio():.4f}")
print(f"RP-PCA Max SR: {rppca.get_max_sharpe_ratio():.4f}") # typically 2× higher
Functional API
from rppca import rppca_decompose, pca_decompose
# RP-PCA
loadings, factors, eigenvalues = rppca_decompose(X, n_factors=5, gamma=10)
# Standard PCA (convenience wrapper)
loadings, factors, eigenvalues = pca_decompose(X, n_factors=5)
Algorithm
Factor Model
Excess returns follow an approximate factor model:
X = F · Λᵀ + e
where X is T×N (periods × assets), F is T×K (latent factors), Λ is N×K (loadings), and e is the idiosyncratic residual.
RP-PCA Matrix
RP-PCA performs eigendecomposition on a modified second-moment matrix:
S = (1/T) XᵀX + γ · X̄ X̄ᵀ
where X̄ is the N×1 vector of time-series means of each asset.
This is equivalent to PCA on:
(1/NT) Xᵀ (I_T + (γ/T) 𝟏𝟏ᵀ) X
Estimation Steps
| Step | Operation | Output |
|---|---|---|
| 1 | Compute S = (1/T) XᵀX + γ · X̄X̄ᵀ |
N×N matrix |
| 2 | Eigendecompose S, take top K eigenvectors | v₁, ..., v_K |
| 3 | Loadings: Λ̂ = √N · [v₁ ... v_K] |
N×K matrix |
| 4 | Factors: F̂ = X · Λ̂ · (Λ̂ᵀΛ̂)⁻¹ |
T×K matrix |
Role of γ (Risk-Premium Weight)
| Value | Effect |
|---|---|
γ = -1 |
Standard PCA on the covariance matrix |
γ = 0 |
PCA on the second-moment matrix (1/T)XᵀX |
γ > 0 |
Overweight the mean → strengthens weak-factor signal |
γ = 10 |
Recommended by the paper for empirical applications |
The key insight: a larger γ increases the eigenvalue signal of factors with high Sharpe ratios, making weak-but-important pricing factors detectable.
Signal Strengthening
The population limit of S converges to:
Λ (Σ_F + (1+γ) μ_F μ_Fᵀ) Λᵀ + Var(e)
For PCA (γ=-1), the signal is driven only by variance Σ_F. For RP-PCA (γ>0), the mean μ_F also contributes, boosting weak factors that have high Sharpe ratios (large μ_F relative to Σ_F).
Look-Ahead Bias
⚠️ Critical for quantitative strategies
The Problem
The in-sample RP-PCA estimation uses the full sample to compute:
- The mean vector X̄ = (1/T) Σ X_t — includes future returns
- The second-moment matrix (1/T) XᵀX — includes future returns
- The eigendecomposition — depends on all of the above
This introduces look-ahead bias (未来函数). If you estimate factors at time t using data that includes t+1, t+2, ..., your strategy is peeking into the future.
The Solution
Use RollingRPPCA for any application where causality matters:
from rppca import RollingRPPCA
rolling = RollingRPPCA(
n_factors=5,
gamma=10,
window=240, # 20-year rolling window (as in the paper)
)
rolling.fit(X) # Internally: at each t, only uses X[t-240:t]
At each time step t, the rolling estimator:
- Estimates loadings from
X[t-window : t]only (historical data) - Projects
X[t]onto those loadings → out-of-sample factor at t - Never uses any data from t+1 onwards
In-Sample vs Out-of-Sample Summary
RPPCA (in-sample) |
RollingRPPCA (out-of-sample) |
|
|---|---|---|
| Data used | Full sample [1, T] | Rolling window [t-w, t] |
| Look-ahead bias | ⚠️ Yes | ✅ No |
| Use case | Academic research, model evaluation | Live trading, backtesting |
| Sharpe ratio | Upward-biased | Unbiased |
| Paper reference | Sections 2–6 | Section 7 |
API Reference
Classes
RPPCA(n_factors=5, gamma=10.0, normalize=False)
In-sample RP-PCA estimator.
| Method | Description |
|---|---|
.fit(X) |
Estimate loadings & factors from full sample |
.transform(X_new) |
Project new data onto estimated loadings |
.fit_transform(X) |
Fit and return in-sample factors |
.get_max_sharpe_ratio() |
Maximum Sharpe ratio of the factors |
.get_pricing_errors(X) |
Per-asset pricing errors (alphas) |
.get_rms_pricing_error(X) |
Root-mean-squared pricing error |
.get_idiosyncratic_variance(X) |
Average unexplained variance |
.get_explained_variation(X) |
Fraction of variance explained |
RollingRPPCA(n_factors=5, gamma=10.0, window=240, normalize=False, min_window=None)
Rolling-window estimator — strictly look-ahead-bias free.
| Method | Description |
|---|---|
.fit(X) |
Run rolling-window estimation |
.get_oos_max_sharpe_ratio() |
Out-of-sample max Sharpe ratio |
.get_oos_pricing_errors(X) |
Out-of-sample pricing errors |
.get_oos_rms_pricing_error(X) |
Out-of-sample RMS α |
.get_oos_idiosyncratic_variance(X) |
Out-of-sample idiosyncratic variance |
| Attribute | Description |
|---|---|
.oos_factors_ |
(T_oos, K) out-of-sample factor estimates |
.oos_start_ |
Index where OOS estimates begin |
.loadings_history_ |
List of loading matrices per step |
.sharpe_weights_history_ |
Max-Sharpe weights per step |
Functions
| Function | Description |
|---|---|
rppca_decompose(X, n_factors, gamma=10) |
Core decomposition (returns loadings, factors, eigenvalues) |
pca_decompose(X, n_factors) |
Standard PCA (γ=-1 shortcut) |
max_sharpe_ratio(factors) |
Maximum Sharpe ratio |
pricing_errors(X, factors) |
Per-asset alphas |
rms_pricing_error(X, factors) |
RMS pricing error |
idiosyncratic_variance(X, factors) |
Average unexplained variance |
explained_variation_ratio(X, factors) |
R² of the factor model |
Utilities
from rppca.utils import eigenvalue_ratio_test, variance_signal, eigenvalue_spectrum
# Suggest number of factors (Ahn & Horenstein, 2013)
n_factors, ratios = eigenvalue_ratio_test(X, max_factors=10, gamma=10)
# Variance signal diagnostic (strong vs weak factors)
signals = variance_signal(X, n_factors=5, gamma=10)
# Scree plot data
evals = eigenvalue_spectrum(X, gamma=10, n_top=10)
Examples
Run the simulation example:
python scripts/example_simulation.py
Sample output:
1. IN-SAMPLE COMPARISON
PCA (γ=-1) | Max SR: 0.0799 | RMS α: 0.001088
RP-PCA (γ=10) | Max SR: 0.5728 | RMS α: 0.000715
4. LOOK-AHEAD BIAS CHECK
In-sample Max SR: 0.5728 (uses future data — biased)
Out-of-sample Max SR: 0.1528 (no look-ahead — unbiased)
Citation
If you use this package in your research, please cite the original paper:
@article{lettau2020estimating,
title={Estimating latent asset-pricing factors},
author={Lettau, Martin and Pelger, Markus},
journal={Journal of Econometrics},
volume={218},
number={1},
pages={1--31},
year={2020},
publisher={Elsevier}
}
License
MIT License
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file rppca-0.1.0.tar.gz.
File metadata
- Download URL: rppca-0.1.0.tar.gz
- Upload date:
- Size: 21.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
704ba3fe996907ae49714b49579a3f931728463cb709e4060f4122632f238225
|
|
| MD5 |
9016b4cf8ec1434b8fa364f404abae30
|
|
| BLAKE2b-256 |
027365a46366058fdf281d140781b308db4a5b06780cdd72ac51d995bbeaad99
|
File details
Details for the file rppca-0.1.0-py3-none-any.whl.
File metadata
- Download URL: rppca-0.1.0-py3-none-any.whl
- Upload date:
- Size: 17.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0573b43dd5099dddcd66723fb230c5daf2bd71440f61074ded1dffd254251bff
|
|
| MD5 |
a3f09de042a4b9734c82e8d062ca307b
|
|
| BLAKE2b-256 |
d34f15d1b6fee23e2f1ad27b00e0cffa516514db684dfaa184fa26509274ab70
|