卷 II · 画出来CH 05深度 5/24

箭头不是等号

你从小学起写了几十万个等号,它们都可以两边对调。因果的箭头不行。这一章拆掉那个习惯,然后你会发现:你其实早就懂这件事——因为你写过代码。

结构因果模型 SCM赋值 :=卷 II 开篇

▷ 先判一次

某地的数据显示:每卖出 1 台空调,当月冰淇淋销量增加约 800 支。反过来问:如果冰淇淋销量增加 800 支,空调销量会增加几台?

A 1 台,因为关系是对称的B 小于 1 台,需要重新算一遍回归C 0 台,因为是气温导致的,两者互相不影响D 这个问题本身就问错了

四个选项里有两个是对的,但它们对的层次不同。

你已经知道这件事了,只是没往这边想

先看两行代码:

// 第一行:数学里的等式
//   a = b  和  b = a  说的是同一件事

// 第二行:代码里的赋值
y = 2 * x;      // 把 x 读出来,算一下,写进 y
x = y / 2;      // 完全是另一条语句,做的是另一件事

第二行你一眼就懂:y = 2 * x 执行之后,x 会影响下一次算出来的 y,但改 yx 一点影响都没有。赋值是有方向的,等式没有。

因果模型用的就是赋值那一套。为了强调,我们写成 :=

Y := 2 × X + 噪声

# 读作:「Y 的值由 X 算出来」
# 不读作:「Y 等于 2X」
◆ 结构因果模型(SCM)

一个结构因果模型就是一组这样的赋值语句,每个变量一条:

C := 噪声₁                        # 没有父变量,自己决定自己
X := −1.2 × C + 噪声₂             # X 由 C 算出来
Y := 60 + 5 × X + 9 × C + 噪声₃   # Y 由 X 和 C 算出来

每条语句右边出现的变量,就是左边那个变量的父变量;把「父 → 子」画成箭头,就得到一张因果图(causal graph / DAG)。

「噪声」代表所有你没写进模型的其他影响。它必须存在——否则世界就是完全确定的。

这个东西在软件工程师眼里应该非常眼熟:它就是一个依赖图,一张 DAG,一个数据流图。你每天都在画。区别只在于,这张图上的节点不是模块,是这个世界上的量。

两个方向的回归,不是互为倒数

说「箭头有方向」很容易,但它有一个可以量出来的后果,而这个后果多数人没想过。

我写一台世界机器:Y := 2X + 噪声,X 和噪声都是标准正态。然后跑四万个样本,做两个方向的回归。

YX 回归,斜率 2.0005——正是结构里那个 2。

XY 回归,斜率是 0.3992

不是 1/2 = 0.5差了 20%。

为什么?因为 Y 里混进了噪声,比 X 「脏」。拿脏的去预测干净的,斜率会被系统性地压小(这个现象叫回归稀释)。数值上:Var(Y) = 2² × 1 + 1 = 5Cov(X, Y) = 2,于是 2/5 = 0.4——和跑出来的 0.3992 对得上。

◆ 两种系数,不是一回事
  • 结构系数:把 X 拨动一个单位,Y 会变多少。这是你想要的。
  • 回归系数:看到 X 大一个单位时,Y 平均大多少。这是数据给你的。

它们有时候相等(比如上面 Y ~ X 那个方向),有时候不等(X ~ Y 那个方向),有时候差到符号相反(第 2 章那根滑杆)。

这本书剩下的部分,全部是在回答一个问题:什么时候回归系数等于结构系数?

那么「拨动」到底是什么操作

有了赋值语句这个框架,「拨动」就有了精确的定义,而且干净得让人惊讶:

✂ do 就是替换一行代码

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 章从信息论那边讲了同一件事——好的调节器必然是被控对象的模型。两本书在这里碰头。)

✗ 这个直觉是错的
既然 Y 由 X 决定,那 X 和 Y 之间的关系,从哪个方向看都一样。 观察它们的关系是对称的(相关系数只有一个),但结构是不对称的,而干预的结果由结构决定。

相关系数 corr(X, Y) = corr(Y, X),永远相等。这正是它测不出因果方向的原因——一个对称的量,装不下一个不对称的事实。

顺带说一句:确实有一整个研究方向(因果发现,causal discovery)在尝试从纯观察数据里判断箭头方向,靠的是噪声的非对称性、非高斯性这类线索。它在特定假设下真的能用,但它不是「数据里本来就有方向」——它是又引入了一组新假设。第 24 章会给入口。

◇ 判词

C 和 D 都对C 说的是事实(气温是共同原因,两者互不影响,所以答案是 0 台);D 说的是更根本的一层——这个问题预设了「关系可以反过来读」,而这个预设本身就是错的。

「每卖出 1 台空调,冰淇淋增加 800 支」这句话,从一开始就不该被读成因果。它是一个观察到的关联,而背后的结构是 空调 ← 气温 → 冰淇淋do(冰淇淋 = +800) 会把指向冰淇淋的箭头剪断,剩下的图里空调和冰淇淋之间什么都没有。

A 「关系是对称的」——相关是对称的,结构不是。这一章就是为了拆掉这个直觉。 B 「重新算一遍回归」——你确实会算出一个数(大约 1/800 那个量级),但那个数是预测的斜率,不是干预的效应。算出来也用不了。

这一章的一句话

因果模型是一组赋值语句而不是一组等式;do 就是把其中一行换掉,在图上就是把指向它的箭头剪断。

下一章开始拆图。好消息是这件事出奇地简单:不管一张因果图有多大、多复杂,它都只由三个零件拼成。三个。你会看到其中两个的行为完全符合直觉,而第三个正好相反——控制它,两个本来毫无关系的变量之间的相关系数会从 −0.004 变成 −0.501