← 返回目录
p7.4动手⏱ 约 17 分钟

时序 CV 与特征泄漏:泄漏有多严重,取决于标签重不重叠

同一批数据,随机 K 折让 AUC 从 0.4845 虚高到 0.6132——而这个虚高随标签窗口从 1 日拉到 20 日,放大 64 倍

🔎 最后验证 2026-07📚 来源:三组标签窗口 × 三种切分/变换方式的泄漏量级实测,2026-07-31 于 ECS 实测,输出见 code/outputs/stdout.txt🧰 scikit-learn 1.9.0、pandas 3.0.5、Python 3.12.13
为什么学这个

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. 随机森林对单调变换免疫——树的分裂只看阈值顺序,标准化对它根本不起作用。用它演示"标准化泄漏",从一开始就测不出东西
  2. 1 日标签没有重叠——相邻样本的标签窗口不相交,随机 K 折能泄漏的东西很有限

把这两条修正之后,现象才出现。

而修正过程本身就是本节的主要内容:

一个"泄漏实验"没测出泄漏,先怀疑实验设计,而不是宣布"泄漏是个伪问题"

💡 打个比方

考试前老师说"这次不划重点"。

但如果同一批题目在三次模拟考里反复出现,而你做过其中两次——第三次的高分说明不了什么。

标签重叠,就是"同一道题出现了多次"。

随机 K 折把这些几乎相同的题目分到训练和测试两边,等于让你拿着答案考试

一、实验 A:标签重叠 × 切分方式

模型固定为随机森林(200 棵/深度 5),特征与 p7.3 完全相同。唯一的变量是标签窗口与切分方式。

标的标签随机 K 折 AUC滚动前推 AUC虚高相邻样本标签重叠
sh.000300未来 1 日0.54140.5404+0.00110(不重叠)
sh.000300未来 5 日0.57390.5257+0.04824/5
sh.000300未来 20 日0.61320.4845+0.128719/20
sh.600000未来 1 日0.51190.5057+0.00630
sh.600000未来 5 日0.55990.4888+0.07114/5
sh.600000未来 20 日0.63100.5190+0.111919/20
sh.600019未来 1 日0.52090.5227-0.00170
sh.600019未来 5 日0.57070.4876+0.08324/5
sh.600019未来 20 日0.62130.4949+0.126519/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.0003000.52620.5223+0.0039
sh.6000000.49290.4929-0.0001
sh.6000190.49640.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 上的版本,四条:

  1. 训练永远在测试之前,不打乱顺序
  2. 训练集与测试集之间留禁运期 ≥ 标签窗口长度
  3. 所有从数据里学出来的变换只能在训练集上 fit,再 transform 到测试集
  4. 超参数搜索也要在这个框架内做——否则搜索本身就是泄漏(p5.4/p3.4)
⚠️ 避坑

第 ③ 条最容易被违反,因为 sklearn 里最顺手的写法就是先对全量 fit_transform

一行 X = scaler.fit_transform(X) 写在切分之前,整个实验就作废了。

而本节实测的一个副产品是:它对树模型无效、对线性模型有效——

所以"我测了没测出来"也不代表你没犯。

❓ 测验
你的标签是「未来 10 日收益方向」,用 5 折随机 K 折交叉验证得到 AUC 0.62。按本节的结论,这个数最可能是多少?
✏️ 填空
随机 K 折造成的虚高随标签窗口放大:1 日标签 +0.0019,5 日 +0.0675,20 日 ___ AUC。

📌 免责:本课为技术教学,模型与特征均为演示口径,不构成任何交易信号或投资建议。

✅ 小结

四件事:

  1. 泄漏不是恒定的量,它由标签重叠决定。随机 K 折的平均虚高:1 日标签(不重叠)+0.0019、5 日(重叠 4/5)+0.0675、20 日(重叠 19/20)+0.1224——放大 64 倍。其中 sh.000300 从 0.6132 掉到 0.4845,一个"看起来有效"的模型,全部有效性来自切分方式
  2. 禁运期的边际作用比想象中小:滚动前推框架内 0→40 日的变化只有 -0.0047 ~ +0.0145,方向还不一致——因为主漏已经被"训练在前"堵住了。该加,但救不回一个用了随机 K 折的实验
  3. 全样本分箱泄漏在本样本上也很小(≤ 0.004)。但原则不变:凡是"从数据里学出来的东西"只能在训练集 fit;样本越短、特征越多、变换越复杂,这个泄漏越大
  4. 泄漏要按量级排序,不要把所有"原则"等同对待。决定性的只有一条:不要用随机 K 折

外加一条方法论:这一节是按实测重写过的。第一版预设的两个"错误"都没测出效果(标准化那项 +0.0000),排查发现是实验设计错了(树模型对标准化免疫、1 日标签不重叠)。一个泄漏实验没测出泄漏,先怀疑实验设计,而不是宣布"泄漏是个伪问题"。

正确做法 Purged Walk-Forward CV 四条:训练在前 · 禁运期 ≥ 标签窗口 · 变换只在训练集 fit · 超参数搜索也在框架内。其中第三条最容易违反——而它对树模型无效、对线性模型有效,所以"我测了没测出来"也不代表你没犯

下一节转向工程:用 AI 结对开发的可复现工作流——规格 → 测试 → 实现 → 审查。

下一节 → AI 结对开发工作流
🔎 来源与核验· 4 条,点开核对
本节每个关键论断都对应一个可追溯的来源 —— 这是本课程"靠谱、不过时"的底线。
「标签重叠 × 切分方式(随机森林 200 棵/深度 5,特征同 p7.3):随机 K 折与滚动前推的 AUC 差,1 日标签平均 +0.0019、5 日 +0.0675、20 日 +0.1224;其中 sh.000300 20 日标签为 0.6132 vs 0.4845(+0.1287)、sh.600000 0.6310 vs 0.5190、sh.600019 0.6213 vs 0.4949」
📚 本节 code/leak_cv.py,2026-07-31 于 ECS 实测,输出见 code/outputs/stdout.txt✓ 已核验 2026-07
「禁运期(滚动前推框架内,20 日标签)0→40 日的 AUC 变化:sh.000300 +0.0145、sh.600000 -0.0047、sh.600019 -0.0008」
📚 同上实测✓ 已核验 2026-07
「全样本分位数分箱 vs 仅训练集分箱(5 日标签):sh.000300 0.5262 vs 0.5223、sh.600000 0.4929 vs 0.4929、sh.600019 0.4964 vs 0.4934,虚高 ≤ +0.0039」
📚 同上实测✓ 已核验 2026-07
「第一版实验设计失败记录:随机森林对单调变换免疫,全样本标准化的贡献为 +0.0000;1 日标签不重叠时随机 K 折虚高仅 +0.0019」
📚 本节 code/leak_cv.py(第一版实验设计失败原样保留在代码 docstring L3 与 L85 的 print 中,输出见 code/outputs/stdout.txt)✓ 已核验 2026-07
结论仅针对本样本、本特征集与随机森林;泄漏量级会随样本长度、特征数量与变换复杂度变化。禁运期的边际作用小,是在「已经使用滚动前推」的前提下测得的,不适用于随机 K 折。
智图软件的赞赏码
都看到这了,打个赏呗!
接下来 · p7.5
AI 结对开发工作流:把审查变成一段能跑的代码
继续读下一节 →