halo 的技术博客

返回

上一篇讲 PDP 时留了个大坑:因子彼此相关时,PDP 会说谎。 它把目标因子强行设成某个值、其余因子保留原样,可如果两个因子相关系数 0.9,“目标因子拉到 +2、相关因子还停在 -2”这种组合现实里根本不存在——模型在这些不存在的组合上纯外推,输出幻觉,PDP 却把幻觉平均进了曲线。结果就是一个对收益毫无独立贡献的冗余因子,被 PDP 画出一条漂亮的向上斜坡。

结论先放这:ALE(Accumulated Local Effects,累积局部效应)用『局部差分 + 累加』彻底绕开外推。 它把因子取值切成一串窄箱,在每个箱内只取落在这个箱里的真实样本,把它们的目标因子分别挪到箱的左右边界、算两次预测之差(局部效应),再沿因子方向把这些局部差分累加起来。因为每一步都只用箱内真实存在的样本,ALE 永远不会外推到不存在的组合。本文用相关系数 0.92 的一对因子做对照实验:一个真驱动、一个纯冗余,看 PDP 怎么被骗、ALE 怎么把伪效应修正回近零(附纯 numpy 实现,中阶)。

为什么 PDP 会被相关因子骗#

再快速回顾 PDP 的定义:

PDPj(v)=1ni=1nf^(xj=v,  xj(i))\text{PDP}_j(v) = \frac{1}{n}\sum_{i=1}^{n}\hat{f}(x_j = v,\; x_{-j}^{(i)})

问题出在 xj=vx_j = vxj(i)x_{-j}^{(i)}强行拼接上。当 xjx_j 与某个 xkx_k 高度相关,把 xjx_j 设成边缘分布里的极端值、xkx_k 却保留原值,得到的就是联合分布里概率近乎为零的点。模型没在那种点附近见过训练数据,只能外推。

ALE 的修正思路很直接:不问”把因子设成 v 会怎样”,改问”因子从 v 变到 v+dv,在那些本来就在这附近的样本上会怎样”。 后者永远只用真实存在的样本,天然免疫外推。

ALE 的数学:局部差分再累加#

一阶 ALE 的定义(离散版)是这样的:把因子 xjx_j 的取值按分位数切成 KK 个箱,边界记为 z0<z1<<zKz_0 < z_1 < \dots < z_K。对第 kk 个箱,取所有落在 [zk1,zk)[z_{k-1}, z_k) 里的样本,计算它们在箱两端的局部效应

Δk=1nki:xj(i)[zk1,zk)[f^(zk,xj(i))f^(zk1,xj(i))]\Delta_k = \frac{1}{n_k}\sum_{i:\,x_j^{(i)}\in[z_{k-1},z_k)}\Big[\hat{f}(z_k, x_{-j}^{(i)}) - \hat{f}(z_{k-1}, x_{-j}^{(i)})\Big]

然后累加(这就是”Accumulated”的来历):

ALEj(zk)=m=1kΔm\text{ALE}_j(z_k) = \sum_{m=1}^{k}\Delta_m

最后减去加权均值做居中,让曲线的平均高度为零、方便比较。

关键差别:PDP 里 xj(i)x_{-j}^{(i)} 遍历全体样本(含不真实组合),ALE 里 xj(i)x_{-j}^{(i)} 只遍历恰好落在当前箱的样本——这些样本的其他因子取值,本来就和当前箱的 xjx_j 值共存过,是真实的联合分布。移动幅度只有一个窄箱的宽度 zkzk1z_k - z_{k-1},是”局部”扰动,不是把整列拉到极端。

构造对照实验:一个真驱动、一个纯冗余#

要看清 ALE 的修正力,得造一个”陷阱数据”:两个高度相关的因子,其中一个对目标完全没有独立贡献。

实测 x1 与 x2 相关系数 0.921y 的公式里没有 x2——它对收益零独立贡献。一个诚实的解释工具应该告诉你”x2 的效应≈0”。我们来看 PDP 和 ALE 谁做到了。

纯 numpy 实现 ALE#

ALE 的实现比公式看着简单,核心就是”分箱 → 箱内挪到边界算差 → 累加 → 居中”:

注意 Xlo/Xhi 只对箱内样本X[mask])操作,且只把它们挪动一个箱宽——这就是”局部、不外推”的全部秘密。

对照实验:ALE 修正了 PDP 的谎言#

先看冗余因子 x2。理论上它的效应应该是零,但 PDP 会怎么画?

PDP vs ALE:相关因子上 PDP 虚报效应,ALE 修正回近零

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

那真驱动 x1 呢?如果 ALE 只会把什么都压平,那它就没用了。好在不是:

真实驱动因子 x1:PDP 与 ALE 一致,都还原斜率≈1

对真正有效的 x1,PDP(红)和 ALE(绿)高度一致,都还原出斜率≈1 的直线,和埋进去的真实效应(蓝虚线,斜率恰好 1)几乎重合。这正是 ALE 的可贵之处:它不是无差别地把曲线压平,而是精准地区分”真效应”和”借来的伪效应”——真的留住,假的修掉。

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 近零)→ 该因子的表观效应是从相关因子借来的冗余,应从因子库剔除或做正交化处理。这个”背离检测”本身就是一个实用的因子去冗余筛选器。
累积局部效应 ALE:用条件期望修正相关因子下的边际效应
https://blog.halo26812.eu.org/blog/accumulated-local-effects
Author halo
Published at 2026年7月25日
版权声明 CC BY-NC-SA 4.0
Comment seems to stuck. Try to refresh?✨