不看数据,就能判独立的那个算法
前两章给了你三条规则和一套串并联逻辑。这一章把它们变成一个能跑的算法——它不看一个数据点,只看一张图,就能告诉你哪些变量之间会有关联。这是「图」这个工具第一次真正证明自己。
一个只看图、不看任何数据的算法,能告诉你「这两个变量在数据里会不会相关」。你觉得这个说法?
这一题问的是这个工具的保证强度,而知道一个工具保证什么、不保证什么,通常比会用它更重要。
把上一章的规则写成算法
回忆第 6 章的规则。给定条件集 Z,一条路径是通的,当且仅当:
- 路径上每个非对撞机节点都不在
Z里; - 路径上每个对撞机节点,它自己或它的某个后代在
Z里。
两个变量 X 和 Y 之间如果一条通的路径都没有,我们就说它们在给定 Z 下是 d-分离的(d-separated,d 指 directional)。
d-分离是一个纯图论的概念——它只关于箭头怎么连,和数据一点关系都没有。
但它有一个统计学上的后果,而这个后果是整套理论的支点:
如果 X 和 Y 在图上被 Z d-分离,那么在任何与这张图相容的分布里,X 和 Y 在给定 Z 下都条件独立。
写成记号:X ⊥d Y | Z(图上) ⟹ X ⊥ Y | Z(数据上)。
这就是「画图」这件事的全部价值:你在纸上画的箭头,能推出数据里必须成立的独立关系。
算法本身:两趟遍历
朴素的做法是枚举所有路径,逐条判断。但路径数量随图的大小指数爆炸,这条路走不通。
正确的做法来自 Koller 和 Friedman 的《概率图模型》(算法 3.1,Reachable)。它是一趟带方向标记的图遍历,复杂度是 O(节点数 + 边数):
def reachable(G, X, Z):
# 第一趟:找出 Z 的所有祖先
# (对撞机只要自己或任何后代在 Z 里就是"打开"的,
# "节点在 Z 的祖先集里"判的正是这个条件)
A = 所有能顺着箭头走到 Z 的节点(含 Z 自己)
# 第二趟:从 X 出发沿活跃路径走,带一个方向标记
# ↑ = 我是从下面(子节点)上来的
# ↓ = 我是从上面(父节点)下来的
栈 = [(x, ↑) for x in X]
可达 = set()
while 栈:
(v, 方向) = 栈.pop()
if 访问过(v, 方向): continue
if v not in Z: 可达.add(v)
if 方向 == ↑ and v not in Z:
往上继续走 v 的父节点,标记 ↑
往下走 v 的子节点,标记 ↓
elif 方向 == ↓:
if v not in Z: 往下走 v 的子节点,标记 ↓
if v in A: 往上走 v 的父节点,标记 ↑ # ← 对撞机被打开
return 可达
最后:X 和 Y 被 Z d-分离,当且仅当 Y 里没有任何节点落在「可达」集合里。
值得停一下的是那个方向标记。为什么同一个节点要按「从上面来」和「从下面来」分别记录?因为一个节点是不是对撞机,取决于你是怎么走到它的——同一个节点在一条路径上是对撞机,在另一条路径上可能是中介。方向标记就是在编码这件事。
亲手用一下
下面这台判定器就是上面那段代码的真实实现。点图上的节点,把它放进/拿出条件集;上下两排按钮选要问的那一对变量。
几个值得亲手试的问题(用第一张「招牌图」):
X ⊥ C | {}→ 否。有一条边 C→X 直接连着。X ⊥ Y | {C}→ 否。控制了 C 之后后门被堵住了,但X → Y这条因果通路还在——而它就是我们想要的东西。这一点很重要:d-分离不区分「因果的关联」和「混淆的关联」,它只说通或不通。C ⊥ S | {X, Y}→ 是。C 到 S 的所有路都得经过 X 或 Y,两个都被堵了。
再换到「M-bias」那张图,试这一对:
U₁ ⊥ U₂ | {}→ 是。它们本来独立。U₁ ⊥ U₂ | {Z}→ 否。一控制 Z,两个本来独立的变量就连上了——这就是第 7 章那件事,被算法当场认出来。
怎么知道这台判定器是对的
这是本书对自己的一个要求:凡是能和现成实现对照的,一定去对照。
Python 的 networkx 里有一个 is_d_separator。它用的是另一条完全不同的路子——道德化(moralization):先取相关节点的祖先子图,把每个节点的父节点两两连边,再把所有边变成无向的,最后问 Z 是不是把 X 和 Y 分成了两个不连通的部分。
两个算法,两种思路,同一个答案。本书的验证器造了一批随机 DAG,加上书里用到的全部图,一共 1484 个 d-分离查询,逐题对答案:
networkx 3.6.1 查询总数 1484 对上 1484 对不上 0
这不是「差不多」,是一题不差。d-分离是布尔判定,没有浮点误差可以躲,只有对和错。
「X 和 Y 独立」在这本书里始终指统计独立:知道 X 的取值,对 Y 的分布不产生任何信息。记号 X ⊥ Y。
它不是「X 不影响 Y」的同义词。两个变量可以互不影响却相关(第 4、7 章的对撞机、第 2 章的混淆),也可以有因果关系却在某个条件下独立(比如效应正好被另一条路抵消,虽然这需要参数上的巧合)。
d-分离连接的正是这两个世界:图上的「分离」(关于结构)推出数据上的「独立」(关于分布)。这个桥是单向的,下一段说为什么。
它保证什么,不保证什么
这一节回答开头那道题,也是这一章最该带走的东西。
保证的方向:图上 d-分离 ⟹ 数据里条件独立。这个方向是定理,一定成立。
不保证的方向:图上 d-连通 ⟹ 数据里相关?不一定。
因为参数可能凑巧抵消。比如 X → Y 系数 +2,同时 X → M → Y 系数是 −2,两条路一正一负正好抵消,数据里 X 和 Y 就完全不相关,尽管图上通着。
这种巧合被称为非忠实(unfaithful)。多数分析会假设它不发生(「忠实性假设」),因为它需要参数精确抵消,在连续参数空间里是零测集。但它确实可能被人为造出来——比如一个自动调节系统,就是靠精确抵消工作的。
所以正确的读法是:d-分离给你的是「一定不相关」的保证,不是「一定相关」的保证。它能帮你排除,不能帮你断言。
这个不对称性有一个非常实用的推论:你可以用它来证伪一张图。如果图说 A ⊥ B | C,而你在数据里测到它们明显相关,那么这张图错了。这是因果图少有的、可以被数据打脸的地方,叫做「可检验的蕴涵」(testable implications)。第 24 章会告诉你怎么系统地用它。
d-分离最直接的工程用途,是在写分析代码之前做一次纸上推演。
假设你要评估「推送通知」对留存的影响。先画一张图(发不发推送受什么影响?留存受什么影响?),然后问几个 d-分离问题:控制了「历史活跃度」之后,「推送」和「留存」之间还剩几条路?其中有几条是因果的、几条是绕后门的?
这个推演不需要任何数据,五分钟就能做完,而它能在你跑完三天的作业之前告诉你:这个分析根本识别不了你想要的东西。
顺带一提,这套东西在贝叶斯网络推断里同样是核心:d-分离决定了哪些证据能影响哪些节点,也就决定了消息要往哪儿传。图模型和因果模型共用同一套骨架,只是问的问题不同。
把图当示意图,是很多人对这套方法最大的低估。它不是给论文配的插画,它是一种输入格式:你把关于这个世界的假设写成图,算法告诉你在这些假设下什么能算、什么不能算、该控制哪些变量。
更实在的是:图逼你把假设说出口。「我们控制了年龄、性别、地区」这句话背后其实有一整套关于谁影响谁的判断,但它藏在句子里,没人能反驳。画成图之后,别人可以指着某一条箭头说「这条不对」——这才是可讨论的科学。
C可以,而且方向正好反过来:它能保证「一定不相关」,不能保证「一定相关」。
d-分离 ⟹ 条件独立,是定理。反过来则要靠「忠实性」这个额外假设,而它有可能不成立(参数抵消、或者一个刻意设计的调节系统)。
A 「必须看数据」——这正是本章要推翻的。图上的结构本身就蕴涵了一批必须成立的独立关系,一个数据点都不用看。 B 方向反了。它保证的恰恰是「不相关」那一侧——而这正是它有用的原因:能被证伪的是这一侧。 D 「两个方向都能保证」——太强了。忠实性是一个额外的、不能从数据验证的假设。这一章的一句话
d-分离是一座单向的桥:图上分离一定推出数据上独立;因果图不是插画,是一种能跑算法的输入格式。
卷 II 到此结束。你现在会画图、会读图、会判独立了。但你还没算过任何一个因果效应。
卷 III 开始动刀。下一章把书名兑现:你会看到同一台世界机器、同一个随机种子,只差一刀——「看」给出 −3.318,「拨」给出 +5.000。并且你会看到那一刀在数据上留下的痕迹:拨之前两组的资历差是 −0.9279,拨之后是 0.000000。