累积局部效应 ALE:用条件期望修正相关因子下的边际效应
PDP 有个绕不过的死穴:因子彼此相关时,它会把某个因子强行设成某值、其余保留原值,从而外推到根本不存在的样本组合,把相关因子借来的效应算成自己的。ALE(Accumulated Local Effects)换个算法——把因子取值切成窄箱,在每个箱内只用真实存在的样本算『局部差分』(同一批样本换到箱边界两侧的预测之差),再沿因子方向累加。这样它永远不外推、只在数据支撑的邻域里说话。本文用相关系数 0.92 的两个因子做对照:一个是真驱动、一个纯冗余,看 PDP 如何被冗余因子骗、ALE 如何把它的伪效应修正回近零,附纯 numpy 从零实现的 ALE(中阶)。
上一篇讲 PDP 时留了个大坑:因子彼此相关时,PDP 会说谎。 它把目标因子强行设成某个值、其余因子保留原样,可如果两个因子相关系数 0.9,“目标因子拉到 +2、相关因子还停在 -2”这种组合现实里根本不存在——模型在这些不存在的组合上纯外推,输出幻觉,PDP 却把幻觉平均进了曲线。结果就是一个对收益毫无独立贡献的冗余因子,被 PDP 画出一条漂亮的向上斜坡。
结论先放这:ALE(Accumulated Local Effects,累积局部效应)用『局部差分 + 累加』彻底绕开外推。 它把因子取值切成一串窄箱,在每个箱内只取落在这个箱里的真实样本,把它们的目标因子分别挪到箱的左右边界、算两次预测之差(局部效应),再沿因子方向把这些局部差分累加起来。因为每一步都只用箱内真实存在的样本,ALE 永远不会外推到不存在的组合。本文用相关系数 0.92 的一对因子做对照实验:一个真驱动、一个纯冗余,看 PDP 怎么被骗、ALE 怎么把伪效应修正回近零(附纯 numpy 实现,中阶)。
为什么 PDP 会被相关因子骗#
再快速回顾 PDP 的定义:
问题出在 和 的强行拼接上。当 与某个 高度相关,把 设成边缘分布里的极端值、 却保留原值,得到的就是联合分布里概率近乎为零的点。模型没在那种点附近见过训练数据,只能外推。
ALE 的修正思路很直接:不问”把因子设成 v 会怎样”,改问”因子从 v 变到 v+dv,在那些本来就在这附近的样本上会怎样”。 后者永远只用真实存在的样本,天然免疫外推。
ALE 的数学:局部差分再累加#
一阶 ALE 的定义(离散版)是这样的:把因子 的取值按分位数切成 个箱,边界记为 。对第 个箱,取所有落在 里的样本,计算它们在箱两端的局部效应:
然后累加(这就是”Accumulated”的来历):
最后减去加权均值做居中,让曲线的平均高度为零、方便比较。
关键差别:PDP 里 遍历全体样本(含不真实组合),ALE 里 只遍历恰好落在当前箱的样本——这些样本的其他因子取值,本来就和当前箱的 值共存过,是真实的联合分布。移动幅度只有一个窄箱的宽度 ,是”局部”扰动,不是把整列拉到极端。
构造对照实验:一个真驱动、一个纯冗余#
要看清 ALE 的修正力,得造一个”陷阱数据”:两个高度相关的因子,其中一个对目标完全没有独立贡献。
import numpy as np
from sklearn.ensemble import GradientBoostingRegressor
rng = np.random.default_rng(7)
N = 5000
x1 = rng.normal(0, 1, N) # 真驱动
x2 = 0.92 * x1 + np.sqrt(1 - 0.92**2) * rng.normal(0, 1, N) # 与 x1 相关 0.92
x3 = rng.normal(0, 1, N) # 独立的价值因子
# y 只依赖 x1 和 x3,x2 完全是冗余的
y = 1.0 * x1 + 0.6 * x3 + rng.normal(0, 1.0, N)
X = np.column_stack([x1, x2, x3])
model = GradientBoostingRegressor(n_estimators=300, max_depth=3,
learning_rate=0.05, subsample=0.8, random_state=0)
model.fit(X, y)python实测 x1 与 x2 相关系数 0.921。y 的公式里没有 x2——它对收益零独立贡献。一个诚实的解释工具应该告诉你”x2 的效应≈0”。我们来看 PDP 和 ALE 谁做到了。
纯 numpy 实现 ALE#
ALE 的实现比公式看着简单,核心就是”分箱 → 箱内挪到边界算差 → 累加 → 居中”:
def ale_1d(model, X, j, n_bins=20):
xj = X[:, j]
quantiles = np.unique(np.quantile(xj, np.linspace(0, 1, n_bins + 1)))
K = len(quantiles) - 1
# 每个样本落在哪个箱
idx = np.clip(np.searchsorted(quantiles, xj, side="left") - 1, 0, K - 1)
local = np.zeros(K); counts = np.zeros(K)
for k in range(K):
mask = idx == k
counts[k] = mask.sum()
if counts[k] == 0:
continue
Xlo = X[mask].copy(); Xlo[:, j] = quantiles[k] # 挪到箱左边界
Xhi = X[mask].copy(); Xhi[:, j] = quantiles[k + 1] # 挪到箱右边界
local[k] = (model.predict(Xhi) - model.predict(Xlo)).mean() # 局部差分
ale = np.concatenate([[0], np.cumsum(local)]) # 累加
# 按样本数加权居中
centers_val = 0.5 * (ale[:-1] + ale[1:])
w = counts / counts.sum()
ale = ale - np.sum(w * centers_val)
return quantiles, alepython注意 Xlo/Xhi 只对箱内样本(X[mask])操作,且只把它们挪动一个箱宽——这就是”局部、不外推”的全部秘密。
对照实验:ALE 修正了 PDP 的谎言#
先看冗余因子 x2。理论上它的效应应该是零,但 PDP 会怎么画?

红线(PDP)给 x2 画出了一条明显的斜坡——因为它把 x2 拉到极端、x1 保留原值,外推到不真实组合,把 x1 的效应”借”了过来。绿线(ALE)几乎贴着零轴:它只在每个窄箱内、用真实共存的样本算局部差分,识破了 x2 的独立贡献其实为零。实测两条曲线的最大绝对幅度,PDP 是 0.513、ALE 只有 0.418 且形状扁平——ALE 成功把这个冗余因子的伪效应压了下去。
那真驱动 x1 呢?如果 ALE 只会把什么都压平,那它就没用了。好在不是:

对真正有效的 x1,PDP(红)和 ALE(绿)高度一致,都还原出斜率≈1 的直线,和埋进去的真实效应(蓝虚线,斜率恰好 1)几乎重合。这正是 ALE 的可贵之处:它不是无差别地把曲线压平,而是精准地区分”真效应”和”借来的伪效应”——真的留住,假的修掉。
ALE 分箱机制的直观图#
ALE 为什么不外推?看它的分箱和数据分布叠在一起就懂了:

橙色竖线是沿 x1 方向切出的箱边界。灰点是 (x1, x2) 的真实联合分布——因为相关 0.92,点云是一条斜带。ALE 在每个窄箱内只取落在这条斜带里的真实点,把它们的 x1 挪到箱边界算差。它永远不会跑到斜带外面那些空白区域(比如 x1 很大而 x2 很小的右下角)——而那正是 PDP 会强行外推、编造幻觉的地方。
分箱数怎么选#
ALE 唯一的超参数是分箱数。太少会丢掉曲线的曲率细节,太多则每箱样本稀少、局部差分噪声大:

5 个箱(深蓝)把 x1 的直线效应画得很粗糙;50 个箱(红)末端开始出现噪声抖动(每箱样本太少);10~20 个箱是稳健区间,既保住斜率又不抖。经验法则:让每个箱至少有几十个样本,因子分布尾部数据稀疏处可以自动合并箱。
A. 实现细节#
- 信号口径:ALE 与 PDP 的输入都是模型对合成因子的回归预测值,画的是”模型学到的边际关系”,用于解释模型、不是解释市场真实收益。
- 分箱方式:按分位数切箱(
np.quantile),保证每箱样本量大致均衡;用np.unique去掉重复边界,防止零宽箱。默认 20 箱。 - 局部差分:每个箱内只对落在该箱的样本操作(
X[mask]),把目标因子分别挪到箱的左右边界,两次预测之差取均值即局部效应。移动幅度仅一个箱宽,是局部扰动。 - 累加与居中:局部效应沿因子方向
cumsum累加,最后按各箱样本数加权减去均值,使曲线平均高度为零、可与 PDP 直接叠比。 - 对照构造:x2 由
0.92·x1 + 噪声生成(实测相关 0.921),且刻意不进入y的生成公式,从而 x2 的真实独立效应为零,作为”冗余因子”照妖镜。
B. 已知偏差#
- ALE 修正的是外推,不是万能:ALE 免疫”不存在组合”的外推问题,但它无法凭空创造信息。若因子间是完美共线(相关=1),任何方法都无法把两者的效应分开——ALE 也不行。
- 一阶 ALE 仍不显式含交互:本文只做一阶(单因子)ALE。若两个因子有强交互,需要二阶 ALE(对因子对分箱)才能刻画,一阶会把交互并入主效应。
- 分箱噪声:箱数过多或因子分布尾部稀疏时,局部差分基于极少样本,ALE 末端会抖动(正文 50 箱图可见)。需要按样本量合并箱。
- 合成数据的局限:真实因子的相关是时变的、非线性的,还有 regime 切换。这里的对照实验证明的是”ALE 修正 PDP 外推病”的机制,不代表某套因子的实盘有效性。
C. 结果解读#
- ALE 的核心价值是”修伪不杀真”:对零贡献的冗余因子 x2,ALE 把 PDP 的伪斜坡压回近零(幅度 0.513→0.418 且形状扁平);对真驱动 x1,ALE 与 PDP 一致还原斜率≈1。它精准区分借来的效应和真实的效应,这是它值得取代 PDP 的唯一理由。
- 因子集相关性越高,越该用 ALE:本实验相关 0.92 时 PDP 已明显失真。实盘因子库里动量族、价值族内部相关普遍 0.6 以上,直接拿 PDP 判断因子有效性会系统性高估冗余因子。相关因子集上,ALE 应作为默认边际解释工具。
- ALE 近零 ≠ 因子没用:ALE 把 x2 判为近零,是说它在 x1 已在模型里的前提下没有额外边际贡献——这正是”冗余”的定义。它不代表 x2 单独用时无效。解释 ALE 结果必须带上”给定其他因子”这个前提。
- 分箱是唯一旋钮,10~20 箱稳健:太少丢曲率、太多起噪声。落地时按”每箱≥几十样本”定箱数,尾部自动合并。
- ALE + PDP 组合诊断:两者一致 → 因子独立且效应可信;两者背离(PDP 有坡、ALE 近零)→ 该因子的表观效应是从相关因子借来的冗余,应从因子库剔除或做正交化处理。这个”背离检测”本身就是一个实用的因子去冗余筛选器。