第三节:样本内与样本外——如何科学划分训练集、验证集、测试集,避免未来信息泄露

做量化策略的朋友,十有八九都经历过这种场景:回测曲线漂亮得像教科书,一上实盘就崩。嗯,问题大概率出在数据划分上。

我刚开始做策略研发那会儿,也犯过这种错。把整个历史数据丢进去训练,然后拿同一批数据做回测,结果跑出来年化收益50%,回撤不到5%。当时我还以为自己发现了圣杯,结果实盘一个月就亏了20%。

说白了,这就是典型的「未来信息泄露」。你让模型看到了它本不该看到的数据。

为什么数据划分这么重要?

量化策略的本质,是用历史规律去预测未来。但这里有个坑:历史数据里包含的信息,有些是「可重复的规律」,有些只是「偶然的噪声」。

如果你不把数据分开,模型就会把噪声也学进去。结果就是——它在历史数据上表现完美,但一到新数据就失灵。

我个人习惯把数据划分比作「考试」:

  • 训练集 = 课本,模型用来学习知识
  • 验证集 = 模拟考,用来调参和选模型
  • 测试集 = 高考,只有最后才能看成绩

你想想看,如果一个人拿着高考答案去复习,考满分有意义吗?

⚠️ 核心原则:测试集在模型开发的整个过程中,只能碰一次。就是最后评估的时候。

三种常见的划分方式

不同的策略类型,适合不同的划分方法。我整理了一下,主要有三种:

划分方式 适用场景 优点 缺点
固定时间划分 日频、周频策略 简单直观,容易实现 可能错过市场风格切换
滚动时间窗口 高频策略、趋势跟踪 适应市场变化 计算量大,容易过拟合
随机划分 截面选股、因子测试 消除时间依赖 可能引入未来信息

我在项目中遇到过最坑的事,就是有人用随机划分做时间序列策略。结果训练集里混进了未来的数据,模型「提前知道」了明天的涨跌。这种错误特别隐蔽,新手很容易踩坑。

固定时间划分:最稳妥的做法

对于大多数量化策略,我建议用固定时间划分。具体比例可以参考:

  • 训练集:60% - 70%
  • 验证集:15% - 20%
  • 测试集:15% - 20%

举个例子,假设你有2010年到2023年的数据:

# 伪代码示例
data = load_data('2010-01-01', '2023-12-31')

train = data['2010-01-01':'2018-12-31']    # 训练集
valid = data['2019-01-01':'2021-06-30']    # 验证集
test  = data['2021-07-01':'2023-12-31']    # 测试集

这里有个细节要注意:验证集和测试集之间,最好留一段「缓冲期」。比如验证集到2021年6月,测试集从2021年7月开始。这样可以避免数据泄露。

💡 小技巧:缓冲期可以设为3-6个月。这段时间的数据既不参与训练,也不参与测试,纯粹用来「隔离」。

滚动时间窗口:应对市场风格切换

固定划分有个问题:如果市场风格发生了剧烈变化,模型可能就失效了。比如2015年的A股和2018年的A股,完全是两个世界。

这时候,滚动时间窗口就派上用场了。

具体做法是:

  1. 设定一个固定长度的窗口(比如3年)
  2. 每次训练用窗口内的数据
  3. 预测窗口外下一段时间的数据
  4. 窗口不断向前滚动

我记得有一次做CTA策略,市场从趋势行情突然变成震荡行情。固定划分的模型直接崩了,但滚动窗口的模型还能勉强撑住。虽然收益下降了,但至少没亏钱。

# 滚动窗口示例
window_size = 3 * 252  # 3年交易日
step_size = 63         # 每季度滚动一次

for start in range(0, len(data) - window_size, step_size):
    train = data[start:start + window_size]
    test = data[start + window_size:start + window_size + step_size]
    # 训练模型,预测测试集

避免未来信息泄露的五个要点

这部分是我最想强调的。很多看起来不起眼的操作,都会导致未来信息泄露。

🔑 五个关键要点

  1. 特征计算只能用历史数据——比如计算20日均线,只能用当天之前的数据
  2. 标准化/归一化要在训练集上做——然后用训练集的参数去处理验证集和测试集
  3. 因子筛选不能看测试集——选因子的时候,只能用训练集和验证集
  4. 回测和训练要严格分离——不要在回测过程中反复调整模型
  5. 警惕「幸存者偏差」——退市的股票也要包含在数据里

我曾经犯过一个低级错误:在计算因子的时候,用了整个数据集的最大值和最小值来做归一化。结果测试集的信息「提前」流入了训练集。后来发现这个问题的时候,我恨不得抽自己一巴掌。

验证集的使用技巧

很多人把验证集和测试集混为一谈,其实它们的作用完全不同。

验证集是用来做模型选择的。你训练了10个不同的模型,用验证集挑出最好的那个。测试集则是用来最终评估的。

我个人习惯的做法是:

  • 先用训练集训练多个模型
  • 在验证集上比较它们的表现
  • 选出最好的模型
  • 最后在测试集上做一次评估

如果测试集的结果和验证集差距很大,说明模型可能过拟合了。这时候不要急着调整模型,而是应该回去检查数据划分有没有问题。

⚠️ 重要提醒:千万不要根据测试集的结果来调整模型参数。一旦你这么做了,测试集就变成了验证集,你的「高考」就变成了「模拟考」。

实战中的常见陷阱

说了这么多理论,来点实际的。我总结了几种最常见的坑:

  • 陷阱一:用未来数据计算技术指标。比如用当天的收盘价计算当天的均线——这在实盘中根本做不到
  • 陷阱二:在训练集和测试集之间来回切换。今天用训练集A、测试集B,明天换成训练集B、测试集A
  • 陷阱三:数据划分后不做检查。有时候数据本身就有时间戳错误,导致划分失效
  • 陷阱四:忽略数据中的「断点」。比如股票停牌、数据缺失,这些都会影响划分

嗯,这些坑我基本都踩过。每次踩完都告诉自己:下次一定注意。但说实话,做策略研发的时候,太容易陷入「追求完美回测」的执念里了。

一个完整的划分流程

最后,我给大家整理了一个标准流程。每次做新策略的时候,按这个流程走一遍,基本不会出大问题:

  1. 加载原始数据,检查时间戳完整性
  2. 按时间顺序排序,确保没有乱序
  3. 划分训练集(60%)、验证集(20%)、测试集(20%)
  4. 在训练集上计算特征和标签
  5. 用训练集的参数标准化验证集和测试集
  6. 训练模型,在验证集上调参
  7. 最终在测试集上评估一次
  8. 记录结果,封存测试集

这个流程看起来简单,但真正做到位的人不多。我见过太多人,在第三步就偷懒了——随便切一刀就开始训练。结果嘛,可想而知。

💡 最后一个小建议:每次做完数据划分,把划分后的数据存成文件。这样下次用的时候直接加载,避免重复操作带来的风险。

数据划分这件事,说白了就是「管住自己的手」。不要因为回测结果好看就忍不住去调整,不要因为测试集表现差就去修改模型。保持纪律性,才是量化策略长期有效的根本。