07 — Factor Risk Model¶
Learning objectives¶
Reconstruct a covariance matrix from a factor model by hand; split portfolio variance into factor (systematic) and specific (idiosyncratic) pieces and confirm they sum to the total; understand why factor models scale to large universes where a full covariance matrix doesn't.
Free learning pack¶
- CFA Analysis of Active Portfolio Management https://www.cfainstitute.org/insights/professional-learning/refresher-readings/2026/analysis-active-portfolio-management
resources/active_management.mdreference/concepts/factor_risk.md
Do not search for more material until these are insufficient.
PREDICT¶
A full covariance matrix for n assets needs n*(n+1)/2 numbers. A
factor model with k factors needs n*k exposures, k*(k+1)/2 factor
covariances, and n specific variances. For n=3, k=2 (the example
below), which needs more numbers? What about at n=500, k=10 — a
realistic large-universe risk model?
Model¶
r = Bf + epsilon
Sigma = B F B^T + D
where B is exposures (assets x factors), F is the factor covariance
matrix, and D = diag(specific variances).
import numpy as np
B = np.array([
[1.0, 0.2],
[0.8, -0.1],
[0.1, 1.2],
])
F = np.array([[0.04, 0.005], [0.005, 0.01]])
specific = np.array([0.02, 0.025, 0.01])
# MANUAL FIRST: reconstruct Sigma.
# Step 1: B @ F (a 3x2 intermediate)
BF = None
# Step 2: (B @ F) @ B.T, then add diag(specific)
Sigma = None
print(Sigma)
# CHECK (uncomment after your attempt):
# assert np.allclose(Sigma, Sigma.T), "a covariance matrix must be symmetric"
# assert np.allclose(np.diag(Sigma), [0.0624, 0.0499, 0.026], atol=1e-3)
Parameter count, for real¶
Confirm your PREDICT answer with the actual formulas: n*(n+1)//2 for
the full covariance vs. n*k + k*(k+1)//2 + n for the factor model, at
both the toy size above and a realistic institutional universe.
# MANUAL FIRST:
def full_covariance_params(n):
return None
def factor_model_params(n, k):
return None
print("n=3, k=2 -> full:", full_covariance_params(3), " factor:", factor_model_params(3, 2))
print("n=500, k=10 -> full:", full_covariance_params(500), " factor:", factor_model_params(500, 10))
# CHECK (uncomment after your attempt):
# assert full_covariance_params(3) == 6 and factor_model_params(3, 2) == 12
# assert full_covariance_params(500) == 125250 and factor_model_params(500, 10) == 5555
Interpretation¶
At n=3, the factor model actually needs more numbers than the full
covariance — factor models don't pay off at toy scale. At n=500, the
factor model needs about 4% of the full covariance's parameter count.
That gap is the entire reason factor models exist: not because they're
mathematically prettier, but because estimating 125,250 independent
covariance entries from noisy historical data is far less reliable than
estimating 5,555.
Split variance into factor vs. specific¶
MANUAL FIRST: for a portfolio w = [0.5, 0.3, 0.2], compute the factor
variance contribution (exposure' F exposure, where
exposure = w' B) and the specific variance contribution
(sum(w_i^2 * specific_i)). Confirm they sum to total portfolio
variance.
from pm.risk import portfolio_variance
w = np.array([0.5, 0.3, 0.2])
# MANUAL FIRST:
exposure = None # w @ B
factor_variance = None # exposure @ F @ exposure
specific_variance = None # sum(w**2 * specific)
print("exposure:", exposure)
print("factor var:", factor_variance, " specific var:", specific_variance)
total_variance = portfolio_variance(w, Sigma)
print("sum:", factor_variance + specific_variance, " total:", total_variance)
# CHECK (uncomment after your attempt):
# assert np.allclose(exposure, [0.76, 0.31], atol=1e-3)
# assert np.isclose(factor_variance + specific_variance, total_variance)
# assert factor_variance / total_variance > 0.7, "most of this portfolio's risk should be factor-driven, not stock-specific"
Reference¶
reference/concepts/factor_risk.md
reference/concepts/factor_risk_contribution.md
Promote¶
Compare with src/pm/factors.py (factor_model_covariance,
portfolio_factor_exposure, factor_variance_contribution,
specific_variance_contribution).
Test¶
pytest tests/test_factors.py
ORAL CHECK¶
Explain to a PM what "common" and "specific" risk mean using this portfolio's actual 70/30-ish split, and why a factor model is the only practical way to build a risk model for a 500-name universe.
Try /tutor factor risk for an adaptive walkthrough. This notebook uses
the same B, F, and specific as notebook 18 (factor risk
contribution) — that notebook picks up where this one leaves off.