时序 CV 与特征泄漏:泄漏有多严重,取决于标签重不重叠
同一批数据,随机 K 折让 AUC 从 0.4845 虚高到 0.6132——而这个虚高随标签窗口从 1 日拉到 20 日,放大 64 倍
p7.3 用严格的时序切分,得到 AUC 约 0.5 的诚实结果。
这一节做反面实验:同样的数据、同样的特征、同样的模型,只改「怎么切分」。
sh.000300 未来 20 日标签
随机 K 折 AUC 0.6132 ← 看起来很不错的模型
滚动前推 AUC 0.4845 ← 比抛硬币还差
虚高 +0.1287
而这个虚高不是一个常数,它随标签窗口长度变化:
未来 1 日标签(重叠 0/1 ) 平均虚高 +0.0019
未来 5 日标签(重叠 4/5 ) 平均虚高 +0.0675
未来 20 日标签(重叠 19/20) 平均虚高 +0.1224 ← 放大 64 倍
「泄漏」不是一个恒定的量。它的大小,由标签窗口重叠多少决定。
这一节是按实测重写过的——第一版的实验设计是错的。
我最初预设"随机 K 折 + 全样本标准化"会让 AUC 明显虚高。跑出来几乎没有差别,标准化那一项的贡献是 +0.0000。
排查后发现两个设计错误:
- 随机森林对单调变换免疫——树的分裂只看阈值顺序,标准化对它根本不起作用。用它演示"标准化泄漏",从一开始就测不出东西
- 1 日标签没有重叠——相邻样本的标签窗口不相交,随机 K 折能泄漏的东西很有限
把这两条修正之后,现象才出现。
而修正过程本身就是本节的主要内容:
一个"泄漏实验"没测出泄漏,先怀疑实验设计,而不是宣布"泄漏是个伪问题"。
考试前老师说"这次不划重点"。
但如果同一批题目在三次模拟考里反复出现,而你做过其中两次——第三次的高分说明不了什么。
标签重叠,就是"同一道题出现了多次"。
随机 K 折把这些几乎相同的题目分到训练和测试两边,等于让你拿着答案考试。
一、实验 A:标签重叠 × 切分方式
模型固定为随机森林(200 棵/深度 5),特征与 p7.3 完全相同。唯一的变量是标签窗口与切分方式。
| 标的 | 标签 | 随机 K 折 AUC | 滚动前推 AUC | 虚高 | 相邻样本标签重叠 |
|---|---|---|---|---|---|
| sh.000300 | 未来 1 日 | 0.5414 | 0.5404 | +0.0011 | 0(不重叠) |
| sh.000300 | 未来 5 日 | 0.5739 | 0.5257 | +0.0482 | 4/5 |
| sh.000300 | 未来 20 日 | 0.6132 | 0.4845 | +0.1287 | 19/20 |
| sh.600000 | 未来 1 日 | 0.5119 | 0.5057 | +0.0063 | 0 |
| sh.600000 | 未来 5 日 | 0.5599 | 0.4888 | +0.0711 | 4/5 |
| sh.600000 | 未来 20 日 | 0.6310 | 0.5190 | +0.1119 | 19/20 |
| sh.600019 | 未来 1 日 | 0.5209 | 0.5227 | -0.0017 | 0 |
| sh.600019 | 未来 5 日 | 0.5707 | 0.4876 | +0.0832 | 4/5 |
| sh.600019 | 未来 20 日 | 0.6213 | 0.4949 | +0.1265 | 19/20 |
平均虚高:
未来 1 日(不重叠) +0.0019
未来 5 日(重叠 4/5) +0.0675
未来 20 日(重叠19/20)+0.1224
sh.000300 那一行是最刺眼的:随机 K 折给出 0.6132,而真实是 0.4845——比抛硬币还差。
一个 AUC 0.61 的模型,在任何 PPT 里都会被当成"有效"。而它的全部有效性,来自切分方式。
二、为什么随机 K 折在金融数据上必然虚高
| 原因 | 机制 | 后果 |
|---|---|---|
| 标签重叠 | 未来 20 日收益的标签,相邻样本共享 19/20 的窗口 | 训练集里已经包含了测试集标签的绝大部分 |
| 样本不独立 | 20 日均线在相邻两天几乎一样 | K 折把近乎相同的样本分到两边 |
| 市场状态泄漏 | 同一段行情被拆到训练和测试 | 学到的是"这段时间在涨",不是规律 |
| 时间箭头 | 用 2025 年的样本预测 2015 年 | 现实中永远不可能发生 |
第一条是主因,而且它是可以量化的——这正是实验 A 那张表在做的事。
🔧 动手做:同一批数据,换个 CV 方式,AUC 就虚高(6 分钟)
跑 python leak_cv.py。同一批数据、同一个模型,只换交叉验证的切分方式:随机 K 折 vs 滚动前推。
你会看到(实测):随机 K 折把 AUC 从滚动前推的 0.4845 虚高到 0.6132。而且这个虚高随标签窗口从 1 日拉到 20 日放大了 64 倍(1 日 +0.0019、20 日 +0.1224)。
想明白:随机 K 折会把未来的样本混进训练集——在时序数据上这是致命的,标签窗口越长(样本越重叠)漏得越狠。所以你看到的"AUC 0.61 好模型",可能纯粹是验证方式漏了未来。时序数据的 CV 必须用滚动前推,绝不能随机打乱。任何 ML 择时的漂亮指标,先问:它的 CV 是怎么切的?
三、实验 B:禁运期的边际作用,比想象中小
又一个与教科书式说法不同的实测结果。
在滚动前推框架内加禁运期(0 → 40 日):
sh.000300 未来 20 日 0.4845 → 0.4990 +0.0145
sh.600000 未来 20 日 0.5190 → 0.5143 -0.0047
sh.600019 未来 20 日 0.4949 → 0.4941 -0.0008
变化在 -0.0047 ~ +0.0145 之间,方向还不一致。
原因不难理解:主漏已经被"训练在前、测试在后"堵住了。禁运期只处理接缝处那几十个样本,占比很小。
所以禁运期该加(成本几乎为零),但别指望它救回一个用了随机 K 折的实验。
四、实验 C:一个树模型也躲不过的泄漏
标准化对树无效,但分位数分箱有效——因为分箱边界本身就是从数据里学来的。
| 标的 | 全样本分箱 AUC | 仅训练集分箱 AUC | 虚高 |
|---|---|---|---|
| sh.000300 | 0.5262 | 0.5223 | +0.0039 |
| sh.600000 | 0.4929 | 0.4929 | -0.0001 |
| sh.600019 | 0.4964 | 0.4934 | +0.0030 |
同样与预期不同:本样本上这个泄漏很小(≤ 0.004 AUC),因为分箱边界在几千个样本上已经很稳定。
但原则不变:凡是"从数据里学出来的东西"——分箱边界、标准化参数、缺失值填充值、特征选择结果、PCA 主成分——都只能在训练集上 fit。
样本越短、特征越多、变换越复杂(尤其是特征选择),这个泄漏就越大。
五、把三个实验放在一起:泄漏要按量级排序
| 泄漏来源 | 本样本实测量级(AUC) | 处置 |
|---|---|---|
| 随机 K 折 + 20 日重叠标签 | +0.1224 | 必须消除 |
| 随机 K 折 + 5 日重叠标签 | +0.0675 | 必须消除 |
| 随机 K 折 + 1 日不重叠标签 | +0.0019 | 影响小,但仍不该用 |
| 全样本分位数分箱 | ≤ +0.0039 | 该改,量级小 |
| 禁运期 0 → 40 日 | -0.0047 ~ +0.0145 | 该加,几乎免费 |
不要把所有"原则"等同对待。 本节实测下来,决定性的只有一条:
不要用随机 K 折——而且标签窗口越长,这一条越致命。
其余几条该做,但它们救不回一个用了随机 K 折的实验。
六、正确做法:Purged Walk-Forward CV
这是 p5.5 那条纪律在 ML 上的版本,四条:
- 训练永远在测试之前,不打乱顺序
- 训练集与测试集之间留禁运期 ≥ 标签窗口长度
- 所有从数据里学出来的变换只能在训练集上 fit,再 transform 到测试集
- 超参数搜索也要在这个框架内做——否则搜索本身就是泄漏(p5.4/p3.4)
第 ③ 条最容易被违反,因为 sklearn 里最顺手的写法就是先对全量 fit_transform。
一行 X = scaler.fit_transform(X) 写在切分之前,整个实验就作废了。
而本节实测的一个副产品是:它对树模型无效、对线性模型有效——
所以"我测了没测出来"也不代表你没犯。
📌 免责:本课为技术教学,模型与特征均为演示口径,不构成任何交易信号或投资建议。
四件事:
- 泄漏不是恒定的量,它由标签重叠决定。随机 K 折的平均虚高:1 日标签(不重叠)+0.0019、5 日(重叠 4/5)+0.0675、20 日(重叠 19/20)+0.1224——放大 64 倍。其中 sh.000300 从 0.6132 掉到 0.4845,一个"看起来有效"的模型,全部有效性来自切分方式
- 禁运期的边际作用比想象中小:滚动前推框架内 0→40 日的变化只有 -0.0047 ~ +0.0145,方向还不一致——因为主漏已经被"训练在前"堵住了。该加,但救不回一个用了随机 K 折的实验
- 全样本分箱泄漏在本样本上也很小(≤ 0.004)。但原则不变:凡是"从数据里学出来的东西"只能在训练集 fit;样本越短、特征越多、变换越复杂,这个泄漏越大
- 泄漏要按量级排序,不要把所有"原则"等同对待。决定性的只有一条:不要用随机 K 折
外加一条方法论:这一节是按实测重写过的。第一版预设的两个"错误"都没测出效果(标准化那项 +0.0000),排查发现是实验设计错了(树模型对标准化免疫、1 日标签不重叠)。一个泄漏实验没测出泄漏,先怀疑实验设计,而不是宣布"泄漏是个伪问题"。
正确做法 Purged Walk-Forward CV 四条:训练在前 · 禁运期 ≥ 标签窗口 · 变换只在训练集 fit · 超参数搜索也在框架内。其中第三条最容易违反——而它对树模型无效、对线性模型有效,所以"我测了没测出来"也不代表你没犯。
下一节转向工程:用 AI 结对开发的可复现工作流——规格 → 测试 → 实现 → 审查。
🔎 来源与核验· 4 条,点开核对
