halo 的技术博客

返回

你盯着屏幕上 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 能不能把它还原。

8 条宏观指标:各自的噪声把共同趋势淹没在面板里

肉眼看,这 8 条曲线各走各的——共同因子 f_t 完全被噪声吞没了。这正是真实宏观面板的样子。

三、估计:Stock-Watson 两步法#

DFM 的估计精髓是先去各序列自相关,再用截面主成分提取共同因子

第一步:对每个指标 x_{i,t} 对它的滞后 x_{i,t-1} 回归,取残差。这一步把「自己惯出来的部分」剥掉,留下「共同因子 + 纯噪声」。

第二步:对残差在每一时刻 t 做截面第一主成分(PCA)——因为共同因子是让所有指标「一起动」的那个方向,而 PCA 第一主成分恰好是方差最大的共同方向。

共同因子:DFM 估计 vs 真实

红实线是 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.91
python

捕获共同信号:DFM 因子 vs 单条原始指标

单条指标与真因子相关落在 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=2
python

更稳妥的是配信息准则(IC):对每个候选 K 拟合 DFM,算 BIC,取最小者。两条经验法则:

  1. 碎石图拐点给出一个直观 K;
  2. 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 做截面主成分;严肃研究用 statsmodelsDynamicFactoreconml 的 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 用合成数据真实计算生成;数字仅用于演示方法,不构成任何投资建议。

动态因子模型(DFM):用少数共同因子把宏观面板压缩成可预测信号
https://blog.halo26812.eu.org/blog/dynamic-factor-model-dfm
Author halo
Published at 2026年7月18日
版权声明 CC BY-NC-SA 4.0
Comment seems to stuck. Try to refresh?✨