箭头不是等号
你从小学起写了几十万个等号,它们都可以两边对调。因果的箭头不行。这一章拆掉那个习惯,然后你会发现:你其实早就懂这件事——因为你写过代码。
某地的数据显示:每卖出 1 台空调,当月冰淇淋销量增加约 800 支。反过来问:如果冰淇淋销量增加 800 支,空调销量会增加几台?
四个选项里有两个是对的,但它们对的层次不同。
你已经知道这件事了,只是没往这边想
先看两行代码:
// 第一行:数学里的等式 // a = b 和 b = a 说的是同一件事 // 第二行:代码里的赋值 y = 2 * x; // 把 x 读出来,算一下,写进 y x = y / 2; // 完全是另一条语句,做的是另一件事
第二行你一眼就懂:y = 2 * x 执行之后,改 x 会影响下一次算出来的 y,但改 y 对 x 一点影响都没有。赋值是有方向的,等式没有。
因果模型用的就是赋值那一套。为了强调,我们写成 :=:
Y := 2 × X + 噪声 # 读作:「Y 的值由 X 算出来」 # 不读作:「Y 等于 2X」
一个结构因果模型就是一组这样的赋值语句,每个变量一条:
C := 噪声₁ # 没有父变量,自己决定自己 X := −1.2 × C + 噪声₂ # X 由 C 算出来 Y := 60 + 5 × X + 9 × C + 噪声₃ # Y 由 X 和 C 算出来
每条语句右边出现的变量,就是左边那个变量的父变量;把「父 → 子」画成箭头,就得到一张因果图(causal graph / DAG)。
「噪声」代表所有你没写进模型的其他影响。它必须存在——否则世界就是完全确定的。
这个东西在软件工程师眼里应该非常眼熟:它就是一个依赖图,一张 DAG,一个数据流图。你每天都在画。区别只在于,这张图上的节点不是模块,是这个世界上的量。
两个方向的回归,不是互为倒数
说「箭头有方向」很容易,但它有一个可以量出来的后果,而这个后果多数人没想过。
我写一台世界机器:Y := 2X + 噪声,X 和噪声都是标准正态。然后跑四万个样本,做两个方向的回归。
Y 对 X 回归,斜率 2.0005——正是结构里那个 2。
X 对 Y 回归,斜率是 0.3992。
不是 1/2 = 0.5。差了 20%。
为什么?因为 Y 里混进了噪声,比 X 「脏」。拿脏的去预测干净的,斜率会被系统性地压小(这个现象叫回归稀释)。数值上:Var(Y) = 2² × 1 + 1 = 5,Cov(X, Y) = 2,于是 2/5 = 0.4——和跑出来的 0.3992 对得上。
- 结构系数:把 X 拨动一个单位,Y 会变多少。这是你想要的。
- 回归系数:看到 X 大一个单位时,Y 平均大多少。这是数据给你的。
它们有时候相等(比如上面 Y ~ X 那个方向),有时候不等(X ~ Y 那个方向),有时候差到符号相反(第 2 章那根滑杆)。
这本书剩下的部分,全部是在回答一个问题:什么时候回归系数等于结构系数?
那么「拨动」到底是什么操作
有了赋值语句这个框架,「拨动」就有了精确的定义,而且干净得让人惊讶:
do(X = 1) 的意思是:把 X 那条赋值语句整个删掉,换成 X := 1。
# 原来的世界 C := 噪声₁ X := −1.2 × C + 噪声₂ ← 删掉这一行 Y := 60 + 5 × X + 9 × C + 噪声₃ # do(X = 1) 之后的世界 C := 噪声₁ X := 1 ← 换成这一行 Y := 60 + 5 × X + 9 × C + 噪声₃
在图上,这一步就是把所有指向 X 的箭头剪断——因为 X 不再由任何东西决定了,它由你决定。
这就是这本书的书名。剩下的 19 章,做的都是同一件事:手上只有原来那个世界跑出来的数据,怎么算出换掉那一行之后的结果。
注意这个定义有多局部:只有 X 那一行被换掉,其他所有行原封不动。Y 还是照 60 + 5X + 9C 算,C 还是照原样生成。这个「其余不变」的假定叫模块性(modularity)——它是整门学问的地基,也是它可能出错的地方之一(第 23 章会看到一个反例)。
软件工程师对这个假定应该有天然的警惕:你改一个模块,真的不会影响别的模块吗?好的答案是「如果接口没变,就不会」。因果模型的赌注也一样——它赌的是这些赋值语句之间是真正解耦的。
为什么不能「解方程」
有人会问:既然 Y := 2X + e,那我把它反过来写成 X := (Y − e)/2 不就行了?
因为这两个模型对干预给出完全不同的预测,而只有一个是对的:
do(X=1) 之后 Y 是多少 do(Y=100) 之后 X 是多少
─────────────────── ────────────────────
Y := 2X + e (真的) 2 + e 不变,还是它自己
X := (Y−e)/2(反着写) 不变 50 − e/2
数据分不清这两个模型——它们在观察分布上完全一样。分得清它们的只有干预,或者你对这个世界的知识(气温影响冰淇淋,冰淇淋不影响气温)。
这就是第 1 章那句「因果结论必然包含数据之外的输入」的具体形态:箭头的方向,就是那个输入。
KPI 为什么会失效,在这里就能看到根。
假设真实世界是:代码质量 := f(团队水平),单测覆盖率 := g(代码质量)。于是覆盖率和质量高度相关,是一个很好的指标。
现在管理层说:覆盖率必须到 80%。这是 do(覆盖率 = 80%)——把覆盖率那行赋值语句整个换掉了,它不再由代码质量决定。原来那条 质量 → 覆盖率 的箭头被剪断,于是覆盖率和质量之间的相关也就没了。指标还在,含义没了。
这就是古德哈特定律(Goodhart's law)的因果版本:一个指标之所以有用,是因为它是某个东西的后果;而你一旦去拧它,它就不再是那个东西的后果了。(《意外》第 23 章从信息论那边讲了同一件事——好的调节器必然是被控对象的模型。两本书在这里碰头。)
相关系数 corr(X, Y) = corr(Y, X),永远相等。这正是它测不出因果方向的原因——一个对称的量,装不下一个不对称的事实。
顺带说一句:确实有一整个研究方向(因果发现,causal discovery)在尝试从纯观察数据里判断箭头方向,靠的是噪声的非对称性、非高斯性这类线索。它在特定假设下真的能用,但它不是「数据里本来就有方向」——它是又引入了一组新假设。第 24 章会给入口。
C 和 D 都对C 说的是事实(气温是共同原因,两者互不影响,所以答案是 0 台);D 说的是更根本的一层——这个问题预设了「关系可以反过来读」,而这个预设本身就是错的。
「每卖出 1 台空调,冰淇淋增加 800 支」这句话,从一开始就不该被读成因果。它是一个观察到的关联,而背后的结构是 空调 ← 气温 → 冰淇淋。do(冰淇淋 = +800) 会把指向冰淇淋的箭头剪断,剩下的图里空调和冰淇淋之间什么都没有。
这一章的一句话
因果模型是一组赋值语句而不是一组等式;do 就是把其中一行换掉,在图上就是把指向它的箭头剪断。
下一章开始拆图。好消息是这件事出奇地简单:不管一张因果图有多大、多复杂,它都只由三个零件拼成。三个。你会看到其中两个的行为完全符合直觉,而第三个正好相反——控制它,两个本来毫无关系的变量之间的相关系数会从 −0.004 变成 −0.501。