Fama-MacBeth 截面回归:两步法把因子暴露变成风险溢价
Fama-MacBeth 把「因子能不能赚到风险溢价」拆成两步:先估每家公司对因子的暴露、再逐月截面回归把暴露定价成 λ̂。合成里 λ̂ 精确还原真值,Newey-West 修正压低虚高 t 统计,单因子定价误差 |α|=0.78、补全因子后塌到 0.003,附完整 Python 与六类真实陷阱(高阶)。
「这个因子到底有没有风险溢价?」——这是因子研究里最朴素也最容易被搞砸的问题。直接把股票按因子排序看未来收益(组合法),简单但有偏;用全样本 OLS 把 N 只股票叠在一起回归,又会因为截面相关把标准误严重低估、制造一堆「显著」假阳性。Fama & MacBeth (1973) 给了一条干净的路径:第一步时间序列估暴露、第二步逐月截面回归定价,最后对逐月斜率取平均。它天然处理了截面相关,还能用 Newey-West 修正斜率序列相关带来的标准误失真。
结论先放这:在 400 家公司、240 个月的模拟里,真实风险溢价 λ = (0.5, 0.8, 0.6, 0.0)(Size / Value / Momentum 有溢价,Noise 因子无溢价);Fama-MacBeth 第二步估出的 λ̂ 精确还原为 (0.5, 0.8, 0.6, 0.0)。逐月 γ_t 经 Newey-West(1) 修正后的 t 统计为 (16.1, 12.3, 14.6, 1.5)——前三个远超 2、Noise 因子 1.5 不显著,恰好对应『哪些因子真有溢价』;而朴素同方差 t 偏高到 (21.2, 16.8, 19.9, 1.5),说明 N-W 修正把虚高的显著性压了下来。更关键的是定价误差:只用近似 CAPM 的单因子定价,平均 |α|=0.78(严重遗漏);补全四个因子后 |α|=0.003≈0——多因子才把横截面收益解释干净。*

一、两步法的动机:为什么不能直接叠 OLS#
最常见的错误是「把 N 只股票、T 期收益叠成一个大面板,跑一个 OLS」。问题在于:同一月内的公司收益彼此相关(共同因子、行业冲击),残差不满足「独立同分布」,OLS 标准误会被系统性低估——你以为 t=8 的因子,可能只是被截面相关放大出来的幻觉。
Fama-MacBeth 的聪明在于把『跨期变化』和『跨公司横截面』拆开:
- 第一步(时间序列):对每只公司 i,用
r_it = α_i + β_i·f_t + ε_it估计它对每个因子的暴露 β_i(这里为演示,我们跳过这层、直接把已知暴露 f_i 当 β_i,避免第一步估计误差污染第二步要展示的机制); - 第二步(截面):每个月 t,用横截面回归
r_it = a_t + β_i·γ_t + u_it把当月的暴露「定价」成斜率 γ_t; - 汇总:λ̂ = mean_t(γ_t),标准误用逐月 γ_t 的样本分布算,并可选 N-W 修正。
第二步里每一月都是一次独立横截面回归——截面相关被「每月一次」天然吸收了(残差相关只存在于同月,不跨月),而跨期自相关由汇总步骤的标准误处理。
二、构造数据:让某些因子有真溢价#
模拟 400 家公司、240 个月。每家公司有 4 个固定因子暴露 f_i = (size, value, mom, noise)。真实风险溢价 λ* 给为 (0.5, 0.8, 0.6, 0.0)。逐月溢价 λ_t 在真值附近做持久波动(AR(1),φ=0.85,模拟因子溢价真实的时变性),保证其时间平均恰好等于 λ*:
import numpy as np
rng = np.random.default_rng(20260715)
N, T, K = 400, 240, 4
lambda_true = np.array([0.50, 0.80, 0.60, 0.0]) # size, value, mom, noise
names = ["Size", "Value", "Momentum", "Noise"]
F = rng.normal(0, 1, (N, K)); F[:, 0] = rng.normal(0, 1, N) # 公司因子暴露
phi = 0.85
theta = np.zeros((T, K))
for t in range(1, T):
theta[t] = phi * theta[t-1] + rng.normal(0, 1, K)
theta -= theta.mean(0) # 去均:平均溢价 = 真实 λ*
lambda_t = lambda_true[None, :] * (1 + 0.45 * theta) # T×K 逐月溢价
common = rng.normal(0, 0.012, T) # 共同冲击(截面相关来源)
beta_common = rng.normal(0, 0.5, N)
idio = rng.normal(0, 0.060, (T, N))
R = (F[None, :, :] * lambda_t[:, None, :]).sum(-1) + beta_common[None, :]*common[:, None] + idiopython注意 beta_common 让所有公司共享一个逐月共同冲击——这正是真实世界里截面相关的来源,也是直接叠 OLS 会翻车的地方。
三、第二步:逐月截面回归得到 γ_t#
每个月跑一次横截面 OLS,R[t] 对 F 回归,取斜率:
Gamma = np.zeros((T, K))
for t in range(T):
X = np.hstack([np.ones((N, 1)), F])
co = np.linalg.lstsq(X, R[t], rcond=None)[0]
Gamma[t] = co[1:] # 逐月斜率 γ_t
lambda_fm = Gamma.mean(0) # Fama-MacBeth 风险溢价估计python结果:λ̂ = (0.50, 0.80, 0.60, 0.00)——小数点不差地还原了真实 λ*。这不是巧合:在「暴露已知、溢价均值 = λ*」的设定下,截面回归的斜率均值本就该收敛到 λ*。F-M 的正确性,在这里被数学校验了。

图 2 画出三个有溢价因子的逐月 γ_t:它们都在真值虚线上下波动,振幅来自逐月溢价波动 + 截面噪声。F-M 做的事就是「对这 240 条线取平均」,把噪声平均掉、留下真值。
四、标准误与 Newey-West:别被虚高的 t 骗了#
逐月 γ_t 是时间序列,自带自相关(因为溢价 λ_t 是 AR(1) 持久过程)。朴素同方差标准误 se = std(γ_t)/√T 忽略了这种自相关,会低估标准误、抬高 t。Newey-West(1) 修正把滞后 1 的自协方差加回来:
def nw_se(g, L=1):
Tn = len(g); g = g - g.mean()
acov0 = np.sum(g*g)/Tn
adj = 0.0
for l in range(1, L+1):
w = 1 - l/(L+1)
adj += 2*w*(np.sum(g[l:]*g[:-l])/Tn)
return np.sqrt((acov0+adj)/Tn)
t_naive = lambda_fm / (Gamma.std(0, ddof=1)/np.sqrt(T))
t_nw = np.array([lambda_fm[k]/nw_se(Gamma[:, k], 1) for k in range(K)])
# naive t = (21.2, 16.8, 19.9, 1.5)
# NW t = (16.1, 12.3, 14.6, 1.5)pythonN-W 把三个有溢价因子的 t 从 1621 压到 1216——仍然高度显著,但更诚实。Noise 因子无论哪种都只有 ≈1.5,正确地「不显著」。结论:F-M 的 t 统计必须经自相关修正,否则你会系统性地高估因子显著性、把噪声当 alpha。
五、定价误差:单因子漏了多少,多因子补了多少#
F-M 还能直接检验「因子集是否完整」。用估出的因子暴露做全样本横截面定价,看截距 α(定价误差)是否为零:
def pricing_error(use_cols):
X = np.hstack([np.ones((N,1)), F[:, use_cols]])
co = np.linalg.lstsq(X, R.mean(0), rcond=None)[0]
return R.mean(0) - X @ co # 平均收益 - 因子解释部分 = α
alpha_single = pricing_error([0]) # 仅近似 CAPM(size 当市场代理)
alpha_multi = pricing_error([0,1,2,3]) # 四个因子全上python| 定价模型 | 平均 |α| | 解读 | |---|---|---| | 单因子(近似 CAPM) | 0.78 | Value / Momentum 的溢价完全没被解释 → α 巨大 | | 多因子(4 因子) | 0.003 | 横截面收益基本被因子解释干净 |
图 3 右图直观对比:单因子留下 0.78 的定价误差(意味着还有一堆收益没被解释,即被遗漏的因子在作祟),补全因子后误差塌到 0.003。这正是 F-M 框架最锋利的地方——它不只是给溢价估值,还给你一把尺子量『你的因子集还差多少才够』。

六、分层验证:把溢价画成单调曲线#
把公司按 Value 暴露五分位分层,看年化收益是否随因子单调:
order = np.argsort(F[:, 1])
quint = np.array_split(order, 5)
qret = np.array([R[:, q].mean(1).mean() for q in quint]) * 12 # 年化
# Q1(最低) -13.05, Q2 -4.20, Q3 +1.03, Q4 +4.87, Q5(最高) +12.98python从最低到最高五分位,年化收益 −13.05 → +12.98,严格单调递增——和第二步估出的 +0.80 正溢价方向一致。这种「排序单调性」是因子有效性的最朴素证据,也是很多顶刊的入门检验。
七、六类真实陷阱(高阶必读)#
1. 第一步暴露的估计误差。 本文为演示把暴露 f_i 当已知。真实里暴露(β)用滚动时间序列回归估,本身有误差,且 β 时变。估计误差会衰减第二步 γ_t 的估计、压低 λ̂(attenuation bias),N-W 标准误也不再精确。落地必须用稳健的 β 估计(收缩 / 状态空间)。
2. 截面相关的残差。 即使第二步每月独立,同一月公司残差仍相关(共同因子未被完全控制)。F-M 用「每月一次横截面」吸收了同月相关,但月间的共同冲击(如系统性风险偏好)会让 γ_t 序列整体漂移,需要 N-W 或 bootstrap 校正。
3. 因子暴露的同步性偏差(errors-in-variables 的近亲)。 若因子本身由同时期收益构造(如动量用过去收益),暴露与残差相关,第二步 OLS 不一致。应使用前视无关的暴露(上一期账面价值、上一期市值)。
4. 组合法 vs F-M 的口径差异。 本文用连续暴露做回归,而业界常用「五分位组合收益」代替。两者估的是同一经济量,但组合法对非线性、极端值更稳健,回归法对小市值尾部更敏感。务必两种都跑、看是否一致。
5. 多重共线性与冗余因子。 若因子间高度相关(如 Size 与 Value 常相关),第二步 γ_t 的方差会暴增、个别 t 失真。应先看因子相关矩阵,必要时做 PCA 或正交化。本文 Noise 因子恰好不显著,但若它和某真因子相关,会污染真因子的 t。
6. 溢价可能是风险还是错误定价? F-M 只证明「暴露有正溢价」,不区分这是对风险的补偿还是对错误定价的套利收益。要进一步,需把 λ̂ 和市场组合、宏观状态做联立检验(如 GRS 检验多因子模型是否零alpha)。
八、代码清单与落地路径#
完整可运行脚本:generate_fama_macbeth_images.py,含数据生成、逐月截面回归、N-W 标准误、定价误差与分层(配图同源)。落地到真实研究时:
- 用 point-in-time 数据库取个股收益与因子暴露(市值、账面市值比、动量等,全部用上一期值避免同步偏差);
- 第一步:对每只股票滚动时间序列回归估 β_i(建议收缩估计);
- 第二步:逐月横截面回归
r_it ~ β_i,存 γ_t; - 汇总 λ̂ = mean(γ_t),标准误用 Newey-West 或 block bootstrap 修正自相关;
- 跑 GRS / α 检验看多因子模型是否零截距;用五分位组合做单调性复核;
- 对每一步结果做样本外或 walk-forward 验证,避免把样本内显著当真 alpha。
Fama-MacBeth 的价值,不只是「算出风险溢价」四个数字,而是它给你一套**能区分『真溢价』与『截面相关幻觉』、能度量『因子集还差多少』**的严谨语言。会读 λ̂ 和 N-W t,才算真正入门了现代资产定价实证。
本文为量化专栏第 N 篇。所有数值来自自洽合成(暴露已知、溢价均值等于真实 λ),仅用于演示两步法与 N-W 修正机制;真实 β 估计误差、同步偏差与样本外显著性请以 point-in-time 无幸存者偏差数据库为准。*