你盯着屏幕上 50 张宏观图表:CPI、PMI、社融、工业产值、10Y 国债收益率、M1、M2、失业率……每一张都在动,每一张都带噪声,每一张还各自有惯性。
你想回答一个简单的问题:「经济现在到底处在一个什么状态?」 但这 50 个数字互相打架——PMI 说扩张、M1 说收缩。你没法用 50 个互相矛盾的数字做决策。
动态因子模型(Dynamic Factor Model, DFM)的答案很激进:这 50 个指标背后,其实只有少数几个共同因子在驱动。PMI 涨、工业产值涨、股价涨,不是因为它们各自有独立的原因,而是因为同一个「经济周期因子」在推着它们一起走。DFM 的任务,就是用统计方法把那几个因子从噪声面板里挤出来。
本文用自包含合成数据,把 DFM 两步法(Stock-Watson 2002)从生成数据推到可运行代码,并诚实拆穿三类真实陷阱。
一、模型:共同因子 + 噪声#
标准(静态)因子模型假设每个指标 x_{i,t} 由共同因子 f_t 线性驱动,外加独立的特质噪声 u_{i,t}:
x_{i,t} = λ_i · f_t + u_{i,t} # λ_i 是指标 i 对因子的敏感度(载荷)plaintext但宏观指标不是「瞬时响应」因子——CPI 的反应滞后半年。所以 动态 版本给每个指标加自己的惯性:
x_{i,t} = λ_i · f_t + φ_i · x_{i,t-1} + e_{i,t} # φ_i 是自身 AR(1) 惯性plaintext因子本身也是动态的,通常是 AR(1):f_t = a · f_{t-1} + ε_t。
直觉:50 个指标 = (少数量共同因子)+ (每个指标自己的滞后)+ (纯噪声)。DFM 把这三层拆开,留下干净的因子。
二、数据生成:埋一个共同因子,看 DFM 能不能挖出来#
我们先按上面的 DGP 造 8 个指标,背后只有一个共同因子 f_t,再混进各自的惯性和大量噪声。然后假装不知道 f_t 的存在,看 DFM 能不能把它还原。
import numpy as np
def generate_panel(N=8, T=240, a=0.90, seed=0):
r = np.random.default_rng(seed)
# 共同因子:AR(1)
f = np.zeros(T)
f[0] = r.normal()
for t in range(1, T):
f[t] = a * f[t-1] + r.normal() * np.sqrt(1 - a**2)
lam = r.uniform(0.6, 1.4, size=N) # 各指标对因子的敏感度
phi = r.uniform(0.3, 0.7, size=N) # 各自惯性
x = np.zeros((N, T))
for i in range(N):
x[i, 0] = r.normal()
for t in range(1, T):
x[i, t] = lam[i]*f[t] + phi[i]*x[i, t-1] + r.normal()*1.3
# 标准化
x = (x - x.mean(axis=1, keepdims=True)) / x.std(axis=1, keepdims=True)
return x, f, lampython
肉眼看,这 8 条曲线各走各的——共同因子 f_t 完全被噪声吞没了。这正是真实宏观面板的样子。
三、估计:Stock-Watson 两步法#
DFM 的估计精髓是先去各序列自相关,再用截面主成分提取共同因子:
第一步:对每个指标 x_{i,t} 对它的滞后 x_{i,t-1} 回归,取残差。这一步把「自己惯出来的部分」剥掉,留下「共同因子 + 纯噪声」。
第二步:对残差在每一时刻 t 做截面第一主成分(PCA)——因为共同因子是让所有指标「一起动」的那个方向,而 PCA 第一主成分恰好是方差最大的共同方向。
from numpy.linalg import svd
def estimate_factor(x):
N, T = x.shape
xlag = x[:, :-1]
xnow = x[:, 1:]
resid = np.zeros_like(xnow)
for i in range(N):
b = np.polyfit(xlag[i], xnow[i], 1)[0] # 剥掉各自惯性
resid[i] = xnow[i] - b * xlag[i]
# 截面 PCA:变量在行,协方差 N×N
cov = resid @ resid.T / T
U, S, Vt = svd(cov, full_matrices=False)
lam_est = U[:, 0] * np.sign(U[0, 0]) # 载荷(长度 N)
f_est = resid.T @ lam_est / (lam_est @ lam_est)
# 第二步迭代:用载荷再合成更干净的因子
lam2 = xnow @ f_est / (f_est @ f_est)
f2 = xnow.T @ lam2 / (lam2 @ lam2)
return f_est, f2, lam_est
x, f_true, lam = generate_panel(N=8, T=240, seed=7)
f_est, f2, lam_est = estimate_factor(x)
print(np.corrcoef(f2, f_true[1:])[0, 1]) # 0.91 <- 还原得相当干净python
红实线是 DFM 从「完全不知道 f_t 存在」的面板里挤出的因子,蓝虚线是真实因子——两者几乎重合,相关 0.91。注意:单步 PCA(f_est,相关仅 0.64)被各指标的惯性污染了;两步法的迭代修正把相关从 0.64 拉到 0.91,这就是「先去自相关」的价值。
四、为什么 DFM 比单条指标更贴真因子#
一个自然的问题:既然每条指标都含因子成分,我直接拿 PMI 当因子不行吗?
不行——因为每条指标都混了自己的惯性和自己的噪声。我们造数据时已知「真因子」,所以可以直接比:谁与真因子的相关最高?
corr_raw = [np.corrcoef(x[i], f_true)[0, 1] for i in range(x.shape[0])]
corr_dfm = np.corrcoef(f2, f_true[1:])[0, 1]
# 各指标: 0.51~0.82 | DFM 因子: 0.91python
单条指标与真因子相关落在 0.51~0.82(各自带噪声),而 DFM 因子聚合了 8 条指标的信息、平均掉了各自的特质噪声,相关冲到 0.91。这就是降维的本质:用相关性把噪声互相抵消,把共同信号放大。
拿到干净因子后,预测就简单了:用 AR(1) 外推 f_{t+h},再经载荷 λ 映射回「下个月 CPI 大概怎么走、PMI 大概怎么走」——一个因子驱动 50 个指标的预测,而不是 50 个独立模型各自瞎猜。
五、因子个数 K:用特征值碎石图定,别拍脑袋#
前面的演示里我们「假装知道」只有 1 个共同因子。实战中你不知道 K 是几。判据很简单:看残差协方差矩阵的特征值。共同因子方向方差大(特征值大),噪声方向方差均匀(特征值小)。特征值从「大平台」骤降到「小平台」的拐点,就标记了 K。
cov = resid @ resid.T / T
eig = np.linalg.eigvalsh(cov)[::-1] # 降序特征值
# 碎石图:前 K 个特征值明显甩开后面的噪声台阶
print(np.round(eig, 3))
# 例: [3.10, 0.98, 0.95, 0.93, 0.91, 0.90, 0.88, 0.85]
# ^第1个遥遥领先 -> K=1;若第2个也明显高,则 K=2python更稳妥的是配信息准则(IC):对每个候选 K 拟合 DFM,算 BIC,取最小者。两条经验法则:
- 碎石图拐点给出一个直观 K;
- IC 最小值做交叉验证式的确认。
两者冲突时,优先听 IC——碎石图在因子载荷接近时(两个因子方差差不多)会糊掉,而 IC 对过拟合更敏感。本文单因子 DGP 下碎石图第 1 个特征值 3.10 遥遥领先其余(0.85~0.98),干净地指向 K=1。
六、三类真实陷阱#
陷阱 1:因子旋转与「因子是谁」的不确定性 PCA 第一主成分只保证「方差最大」,不保证「经济含义清晰」。真实宏观里,第一主成分常常混了「增长 + 通胀」两个真因子,结果你以为挖到一个因子,其实是两个因子的线性混合。补救:用旋转(如 varimax)或先验约束,必要时上多因子 DFM(不止一个共同因子)。本文单因子是演示,真实面板通常 2~3 个因子。
陷阱 2:混频与发布滞后 真实宏观指标不是同一天发布的:GDP 季频、CPI 月频、股价日频,且都有发布滞后。把日频股价和季频 GDP 直接拼成「月度面板」会假装你当时就知道还没发布的数据(前视偏差)。正解是 混频 DFM(MF-DFM / Bridge equation):用高频指标 nowcast 低频因子。本文合成数据同频同步,落地必须处理混频。
陷阱 3:结构性断点让因子「叛变」
2020 年疫情、2008 年危机,宏观联动结构会突变——平时负相关的指标突然一起跌。DFM 估计的载荷 λ 是基于全样本的,断点后旧载荷失效,因子退化成噪声。补救:滚动窗口估计、或上时变参数 DFM、或显式加区制切换。本文平稳 DGP 不触发,实盘必须警惕。
七、落地路径#
- 数据:从
westock-data取宏观序列(CPI、PMI、利率、M1/M2、社融等),注意混频与发布日历。 - 实现:小规模用
sklearn.decomposition.PCA做截面主成分;严肃研究用statsmodels的DynamicFactor或econml的 MF-DFM。 - 因子数选择:用特征值碎石图(scree plot)+ 信息准则(IC)定 K,别拍脑袋。
- 预测:因子 AR 外推后,经载荷映射回各指标 nowcast,再喂进资产配置/择时信号。
八、要点速记#
- 两步法顺序不能反:先剥各序列自相关(回归取残差),再做截面 PCA——否则惯性和因子混在一起,相关从 0.91 掉到 0.64。
- K 怎么定:特征值碎石图拐点 + 信息准则(IC)双确认,别拍脑袋。
- 因子是谁要小心:第一主成分只保证方差最大,真实宏观常混了增长+通胀,必要时上旋转或多因子 DFM。
- 混频是落地的第一道坎:GDP 季频、股价日频、发布滞后,直接拼面板会前视。
- 断点会让载荷叛变:危机时联动突变,旧载荷失效,需滚动/时变估计。
结论#
宏观面板不是 50 个独立问题,而是少数几个共同因子的回声。DFM 用两步法把噪声平均掉、把共同信号挤出来——你拿到的不是又一条带噪声的指标,而是驱动整张面板的那条主线。一句口诀:别盯 50 张图吵架,把它们的公因子挖出来,让它替你说话。
文中所有图表与数字均由
gen_dynamic_factor_model.py用合成数据真实计算生成;数字仅用于演示方法,不构成任何投资建议。