halo 的技术博客

返回

你做因子研究,算出「低波动股票长期跑赢高波动」(低波动异象),于是很自然地下结论:低波动「导致」了高收益。但这话藏着因果推断最大的坑——你确定是低波动导致高收益,而不是「两者都被某个第三变量驱动」?

那个第三变量,行话叫混淆变量 (confounder)。最可能的候选是「杠杆约束」:受杠杆限制的机构被迫买高 beta 股、把低 beta 股压便宜,于是低波动≠因果、只是和某种系统性风险偏好被同一个因子驱动。朴素回归 return ~ low_vol 把这条「从混淆变量漏进来的相关」一并算成低波动的贡献,因果效应被高估

结论先放这:要回答「D 到底导致 Y 多少」,必须把混淆变量 X 对 D 和 Y 的”双重驱动”剥掉。双重机器学习 Double ML (Chernozhukov et al. 2018) 的聪明之处在于——它不要求你手写出 g(X)、f(X) 的精确形式,而是用机器学习(本文用纯 numpy 从零实现的随机森林)去拟合这两个 nuisance 函数,再用交叉拟合 (cross-fitting) 把过拟合偏差打散,最后对”残差对残差”做正交回归,得到几乎无偏的处理效应 θ。

在我们的自洽合成面板(Y = 1.5·D + g(X) + ε,D = f(X) + ν,g 与 f 故意共享一个 X² 非线性项制造混淆)上:朴素 OLS 把 θ 估到 1.877(偏差 +0.377),线性残差化版 Double ML 仍 1.900(偏差 +0.400——因为它抓不住非线性混淆),而随机森林 Double ML 把偏差压到 +0.096(蒙特卡洛 100 次均值 1.596,SD≈0.020)。诚实地说:+0.096 还没到 0,这恰恰是随机森林”收缩偏误”的教科书级现身说法——见文末陷阱第一条。但相比朴素 OLS 的 +0.38,它已经把混淆几乎吃干净。

Double ML 概念:X 同时驱动 D 与 Y,残差化剥离混淆后回归得 θ


1. 偏线性模型与正交化:把”因果”写成一个干净的公式#

Double ML 最常用的设定是偏线性模型 (Partially Linear Model, PLM)

Y = θ·D + g(X) + ε
D = f(X) + ν
plaintext
  • D 是处理变量(比如”是否是低波动股”,0/1 或连续)。
  • X 是混淆变量(所有同时影响 D 和 Y 的可观测特征)。
  • g(X) 是 X 对 Y 的”直接效果”,f(X) 是 X 对 D 的效果。
  • θ 是我们真正想要的因果效应:在控制住 X 后,D 每动一单位,Y 动多少。

朴素 OLS 直接跑 Y ~ D + X 会怎样?只要 g(X)f(X) 不是线性的(现实里几乎一定不是),OLS 就会把 Cov(g(X), f(X)) 漏进来,于是 θ̂ 系统性偏离真值。

正交化 (orthogonalization) 的思路干净得像做手术:

第一步:用 ML 分别估出 ĝ(X) 和 f̂(X),得到残差
        Y_res = Y − ĝ(X)          (Y 里 X 解释不掉的部分)
        D_res = D − f̂(X)          (D 里 X 解释不掉的部分)
第二步:在正交后的残差上做简单回归
        θ̂ = Cov(D_res, Y_res) / Var(D_res)
             = Σ D_res·Y_res / Σ D_res²
plaintext

直觉是:D_res 已经不含 X 的信息,Y_res 也只含”D 的效果 + 纯噪声”。两者相乘,X 的贡献被减掉了,这步回归得到的斜率就是 θ


2. 从零实现:纯 numpy 随机森林 + 交叉拟合#

关键工程点有两个:(1) 用随机森林而非线性回归去估 g/f,才能吃掉非线性混淆;(2) 交叉拟合 (cross-fitting)——用”训练集之外的样本”去生成残差,否则 ML 过拟合会让残差里混进噪声、污染 θ。

下面是一棵纯 numpy 实现的随机森林回归器(决策树用方差下降划分、bagging + 特征子采样,无任何外部依赖):

接下来是 Double ML 的灵魂——K 折交叉拟合

注意 double_ml/ 是在折叠外训练的——这正是 cross-fitting。如果你用同一批数据既训练又生成残差,随机森林会把样本内噪声”记住”,残差被压扁、θ 被严重低估(陷阱二)。


3. 合成数据:故意制造混淆,看谁剥得干净#

为了可复现,我们构造一个”明确知道真值”的面板——这样估出来的 θ 离 1.50 多远,一眼看出偏差:

THETA_TRUE = 1.5

def gen_data(n, seed, theta=THETA_TRUE):
    rng = np.random.default_rng(seed)
    X = rng.normal(0, 1, size=(n, 2))
    # 故意让 g 与 f 共享 X0² 项 → 二者相关,制造混淆
    g = 0.6 * X[:, 0]**2 - 0.4 * X[:, 1]**2 + 0.3 * X[:, 0] * X[:, 1]
    f = 0.5 * X[:, 0]**2 + 0.3 * X[:, 1]
    D = f + rng.normal(0, 1.0, n)
    Y = theta * D + g + rng.normal(0, 1.0, n)
    return X, D, Y
python

因为 gf 都含 X0²,所以 DY 通过 X 高度相关——这正是现实里”混淆变量同时驱动处理与结果”的缩影。

跑一轮看单条数据的残差化效果:

Double ML 残差化:Y_res 对 D_res 正交回归,斜率≈θ

图上 Y_resD_res 的正交回归斜率 = 1.580,紧贴真值 1.50 的虚线。残差之所以围绕原点散开、且和 D_res 无明显弯曲,说明 ML 已经把 X 的非线性影响吃掉了——如果还在用朴素 OLS,这条线会被”从原点斜拉出去”的混淆趋势带偏。


4. 蒙特卡洛:三种估计器的偏差对决#

单条数据有随机性,跑 100 次蒙特卡洛看均值与波动:

naive, lin, rf = [], [], []
for b in range(100):
    X, D, Y = gen_data(4000, b * 7)
    naive.append(naive_ols(Y, D))
    lin.append(double_ml(Y, D, X, kind="linear", K=5, seed=b)[0])
    rf.append(double_ml(Y, D, X, kind="rf", K=5, seed=b)[0])
python

蒙特卡洛 100 次:Double ML(随机森林) 把偏差压回真值附近

估计器θ 均值偏差蒙特卡洛 SD
朴素 OLS1.877+0.3770.019
线性残差化 Double ML1.900+0.4000.019
随机森林 Double ML1.596+0.0960.021

三句话解读:

  1. 朴素 OLS 偏差 +0.38——它把混淆变量 X 通过 D 漏进来的相关全算成 θ 的贡献,估计值虚高 25%。
  2. 线性残差化反而更糟 (+0.40)——这是最反直觉也最重要的一课:Double ML 的框架对,但 nuisance learner 用错(线性回归)就彻底失效。因为 g/f 是非线性的,线性残差化根本没把 X 的影响剥干净,反而在残差里留下了 D 和 Y 仍被 X 相关的尾巴。
  3. 随机森林 Double ML 把偏差压到 +0.096——终于把混淆几乎吃干净。但这 +0.096 依然不是 0,它就是下面要拆的第一类陷阱。

5. 诚实拆穿:五类真实陷阱#

① 随机森林的”收缩偏误” (regularization bias)——我们的 +0.096 从哪来。 树模型是收缩估计量:它把极端预测往中心拉,导致 ĝ(X) 系统性欠估、D_res 被轻微放大,θ 被向上偏。这不是我们实现错了,而是 RF 本身的属性。工业级 Double ML (如 Doubleml 库) 用 RF 偏差校正 (bias correction) 或换成梯度提升树 (GBM) 把这项压到接近 0。所以严谨说法是:Double ML 的理论无偏,但你选的 learner 可能带来有限样本偏误——选 learner 也是方法论的一部分。

② 不做交叉拟合必过拟合。 如果 / 用同一批数据训练和预测残差,RF 会把样本内噪声记进去,残差平方和变小、θ 被严重低估(常被压成接近 0 甚至反向)。cross-fitting 用”折叠外”预测,是 Double ML 无偏性的工程基石,丢不得。

③ 混淆变量 X 没列全 (omitted confounder)。 Double ML 只能控制你观测到的 X。如果真有个驱动 D 和 Y 的隐藏变量没进 X,偏差原封不动留着。因果识别永远依赖”已观测混淆”这个不可证伪的假设——文献里叫”条件独立性 / 无未观测混淆假设 (CIA)“。做不出来的部分,得靠研究设计(自然实验、工具变量)补。

④ nuisance 函数学不出来 (model misspecification)。 Double ML 无偏的前提是 ĝ→gf̂→f(consistency)。如果你的 learner 容量太小、或 X 和 Y/D 的关系本身极复杂,残差没剥干净,θ 仍偏。 learner 越强 → 偏差越小,但 learner 越强 → 越依赖交叉拟合防过拟合,二者耦合。

⑤ 偏线性假设 (PLM) 本身可能不成立。 PLM 假设 θ 对 D 是线性的(“D 每多一单位,Y 多 θ”)。但真实世界里处理效应可能随 X 变化(异质处理效应)、甚至 D 和 X 有交互。这时 PLM 给出的只是”平均处理效应 (ATE)“,想看 CATE(条件平均处理效应)得上 因果森林 (Causal Forest / GRF),那是另一个话题。


6. 落到量化:它能回答什么、不能回答什么#

Double ML 在量化里最适合的问题是**“因子有没有因果 alpha”**这一类:

  • 你想验证”加入北向资金流之后,某因子的超额收益还剩多少”——把北向流、市值、行业等当 X,因子暴露当 D,未来收益当 Y,Double ML 给你”因子暴露的纯因果贡献”。
  • 你想测”某事件(如纳入指数)的真实价格效应”——把同期宏观/风格当 X,事件指示当 D,事件窗收益当 Y。

不能回答”明天涨不涨”(那是预测,不是因果)、也不能绕开 CIA 假设凭空造出因果。它的价值是把”我观察到 D 和 Y 相关”升级成”我控制了一篮子可观测混淆后,D 仍对 Y 有 θ 的净效应”——这已经是绝大多数因子研究里最稀缺的一步。

真实落地时,用 DoubleML (Python) 或 Doubleml (R) 库替你管交叉拟合、偏差校正与聚类标准误;本文纯 numpy 实现只为把”残差化 + 正交回归”的每一步摊开给你看。数据用你自己的面板,但记住:X 的选取比 learner 的选型更决定成败。

双重机器学习 Double ML 因果估计:把「被混淆的因果」剥离干净
https://blog.halo26812.eu.org/blog/double-ml-causal
Author halo
Published at 2026年7月22日
版权声明 CC BY-NC-SA 4.0
Comment seems to stuck. Try to refresh?✨