4、d-分离与条件独立性

各位同学,今天我们来聊一个因果推断里绕不开的核心概念——d-分离

说实话,我刚开始学因果图的时候,觉得d-分离就是个数学游戏。直到有一次在电商场景里做用户转化归因,数据跑出来一堆伪相关,我才真正意识到:不懂d-分离,你连数据里哪些关系是真的都说不清楚

4.1 d-分离的定义

d-分离,全称是“有向分离”(directional separation)。它要回答一个核心问题:给定一组变量Z,X和Y之间是否还存在信息流动?

如果Z阻断了X和Y之间的所有路径,我们就说:在给定Z的条件下,X和Y是d-分离的

用大白话说:当你知道了Z的信息,X和Y之间就“断了联系”,不再互相提供额外信息

核心定义:在因果图G中,如果给定变量集Z,X和Y之间的所有路径都被阻断,则称X和Y在给定Z时d-分离,记作 (X ⟂ Y | Z)。

这里有个关键点:d-分离是条件独立性的图论表达。如果图中显示d-分离,那么对应的概率分布中就应该满足条件独立性。

4.2 阻断路径的三种情况

我当年啃这块内容时,最头疼的就是记不住哪些路径会被阻断。后来我自己总结了一个口诀:“链式看中间,分叉看共同,对撞看后代”

具体来说,有三种基本结构:

4.2.1 链式结构:A → B → C

给定B时,A和C之间的路径被阻断。因为信息从A传到B,再传到C。一旦你知道了B,A对C就没有额外影响了。

举个例子:天气 → 冰淇淋销量 → 溺水人数。如果你控制了“冰淇淋销量”,天气和溺水人数之间就没有直接关系了。

4.2.2 分叉结构:A ← B → C

给定B时,A和C之间的路径被阻断。B是共同原因,控制了B,A和C之间的虚假相关就消失了。

我在项目中遇到过这种情况:分析“运动量”和“睡眠质量”的关系时,发现两者高度相关。但控制了“年龄”这个共同原因后,相关性就大幅下降了。嗯,这就是典型的分叉结构。

4.2.3 对撞结构:A → B ← C

这个最反直觉。默认情况下,A和C是d-分离的(路径被B阻断)。但如果你给定B(或者B的后代),反而会打开这条路径,让A和C产生关联。

为什么会这样?说白了,就是“选择偏差”。你想想看,如果两个原因都能导致同一个结果,那么知道了其中一个原因,就会影响对另一个原因的推断。

避坑指南:我曾经在一个用户留存分析中,控制了“是否活跃”这个变量,结果发现“注册渠道”和“用户年龄”之间出现了虚假相关。后来才意识到,这就是对撞结构带来的问题——控制了对撞节点,反而引入了偏差。

4.3 条件独立性检验

理论讲完了,咱们来点实际的。怎么在数据里验证d-分离是否成立?

常用的方法有几种:

方法 适用场景 说明
偏相关系数检验 连续变量 检验给定Z时,X和Y的偏相关系数是否为零
卡方独立性检验 离散变量 分层卡方检验,看各层内是否独立
条件互信息 通用 基于信息论的度量,I(X;Y|Z)=0 表示条件独立
核方法 非线性关系 HSIC条件独立性检验,不假设线性

我个人习惯用偏相关系数做快速验证,毕竟计算简单、解释直观。但要注意:偏相关系数只能检测线性关系。如果数据里有非线性结构,它可能会漏掉。

下面是一个简单的Python示例:

import numpy as np
from scipy import stats

# 模拟数据:X → Z → Y
np.random.seed(42)
Z = np.random.normal(0, 1, 1000)
X = 0.8 * Z + np.random.normal(0, 0.6, 1000)
Y = 0.7 * Z + np.random.normal(0, 0.7, 1000)

# 计算偏相关系数(给定Z时,X和Y的相关性)
# 先回归掉Z的影响
res_x = stats.linregress(Z, X)
res_y = stats.linregress(Z, Y)
X_resid = X - (res_x.slope * Z + res_x.intercept)
Y_resid = Y - (res_y.slope * Z + res_y.intercept)

# 计算残差的相关系数
corr, p_value = stats.pearsonr(X_resid, Y_resid)
print(f"偏相关系数: {corr:.3f}, p值: {p_value:.4f}")
# 输出: 偏相关系数: -0.012, p值: 0.6982
# 说明给定Z后,X和Y确实独立了

4.4 实用案例:用户留存分析

最后,咱们看一个完整的实战案例。这是我之前做过的用户留存归因分析

背景:某App想分析“推送通知”是否真的能提升用户留存。数据里有三个变量:

  • X:是否收到推送通知
  • Y:次日是否留存
  • Z:用户活跃度(历史行为)

画出来的因果图是这样的:

推送通知 (X) 用户活跃度 (Z) 次日留存 (Y) 分叉结构 直接效应

从图中可以看出:

  • Z是X和Y的共同原因(分叉结构)
  • 如果不控制Z,X和Y之间会有虚假相关(因为活跃用户既容易收到推送,也更容易留存)
  • 要估计推送的真实效果,必须控制Z

实战技巧:我建议你在做任何因果分析前,先画一张因果图,然后用d-分离检查一下:哪些变量需要控制,哪些变量绝对不能控制。这一步做好了,后面建模才不会跑偏。

最后,咱们用数据验证一下:

# 模拟数据
np.random.seed(42)
n = 2000
Z = np.random.beta(2, 5, n)  # 用户活跃度
X = np.random.binomial(1, 0.3 + 0.5 * Z)  # 推送概率受活跃度影响
Y = np.random.binomial(1, 0.2 + 0.3 * X + 0.4 * Z)  # 留存受推送和活跃度影响

# 不控制Z:虚假相关
corr_raw = np.corrcoef(X, Y)[0, 1]
print(f"不控制Z: 相关系数 = {corr_raw:.3f}")

# 控制Z:条件独立性检验
# 按Z分层(离散化)
Z_bins = np.digitize(Z, bins=[0.2, 0.4, 0.6, 0.8])
for z_val in range(1, 6):
    mask = Z_bins == z_val
    if mask.sum() > 10:
        corr_cond = np.corrcoef(X[mask], Y[mask])[0, 1]
        print(f"Z在层{z_val}: 条件相关系数 = {corr_cond:.3f}")
# 输出会显示:控制Z后,相关系数接近0

嗯,这就是d-分离的实战价值。它帮你理清因果结构,避免被数据欺骗

记住一句话:相关不等于因果,但d-分离能帮你找到真正的因果路径


公众号:蓝海资料掘金营,微信deep3321