第 6 章 · 04 时序交叉验证 MultipleTimeSeriesCV


文档摘要

第 6 章 · 04 时序交叉验证 MultipleTimeSeriesCV 本节摘要:本节讲金融机器学习的灵魂工具——时序交叉验证。普通的 K-Fold 随机切数据,在金融时序上等于「用未来训练去预测过去」,是数据泄露的元凶。本节讲三件事:为什么金融必须用时序 CV(训练段必须严格在测试段之前,且要 purge 掉标签重叠的样本);sklearn 自带的 TimeSeriesSplit 的局限;以及项目自带的 MultipleTimeSeriesCV——它支持多支股票的面板数据、固定训练/测试窗长度、purge 间隔(lookahead)防止标签泄露。最后讨论 Lopez de Prado 提出的 purge 与 embargo 高级技巧。掌握这一节,你才能在第 7 章做可信的模型评估。

第 6 章 · 04 时序交叉验证 MultipleTimeSeriesCV

本节摘要:本节讲金融机器学习的灵魂工具——时序交叉验证。普通的 K-Fold 随机切数据,在金融时序上等于「用未来训练去预测过去」,是数据泄露的元凶。本节讲三件事:为什么金融必须用时序 CV(训练段必须严格在测试段之前,且要 purge 掉标签重叠的样本);sklearn 自带的 TimeSeriesSplit 的局限;以及项目自带的 MultipleTimeSeriesCV——它支持多支股票的面板数据、固定训练/测试窗长度、purge 间隔(lookahead)防止标签泄露。最后讨论 Lopez de Prado 提出的 purge 与 embargo 高级技巧。掌握这一节,你才能在第 7 章做可信的模型评估。

内容来源:原项目 ch06/04_cross_validation.py、根目录 utils.pyMultipleTimeSeriesCV,汉化并套用体系化模板。

⚠️ 风险提示:数据泄露是金融 ML 最常见、最致命的陷阱。任何「CV 分数奇高」的模型都先怀疑泄露——大部分 ML4T 的「神奇收益」都来自这里。

学习目标

阅读完本节,你应当能够:

  1. 解释为什么普通 K-Fold 在时序上等于泄露
  2. 说清 sklearn TimeSeriesSplit 的局限。
  3. MultipleTimeSeriesCV 给面板数据做滚动 CV。
  4. 解释 purge(标签泄露)和 embargo(标签污染)的作用。
  5. 选对 train_period_length / test_period_length / lookahead 三个参数。

一、为什么 K-Fold 在金融上是错的

普通 K-Fold 把数据随机切成 K 份,每次留一份当验证。这在独立同分布数据上没问题,但金融时序不是 iid:

两大泄露源:

  1. 时间错位:训练集里有 2017、2019,验证集是 2016——模型已经「见过未来」,验证分数虚高。
  2. 标签重叠:预测「未来 5 天收益」时,今天和明天的标签有 4 天重叠,训练集和验证集的标签共享了同一段未来,等于答案漏题。

💡 核心心法:金融 CV 的铁律是「训练段的任何信息都不能来自验证段之后」。时间只能向前流动。

二、sklearn 的 TimeSeriesSplit

sklearn 自带的 TimeSeriesSplit 解决了第一个问题:每个 fold 的训练段严格在验证段之前:

from sklearn.model_selection import TimeSeriesSplit tscv = TimeSeriesSplit(n_splits=5) for train, validate in tscv.split(data): print(train, validate) # 输出(以 1..10 为例): # [1 2 3 4 5] [6] # [1 2 3 4 5 6] [7] # ...

训练集逐 fold 扩大(expanding window),验证集始终在末尾。但它有三个局限:

局限 说明
单一时间序列 假设数据是一维时序,不支持 (date, ticker) 面板
expanding only 训练窗只增不减,无法做 rolling window(固定窗长)
无 purge 不处理标签重叠,预测多日收益时会泄露

三、sklearn 的其他切分器

04_cross_validation.py 演示了几个 sklearn 切分器:

from sklearn.model_selection import (train_test_split, KFold, LeaveOneOut, LeavePOut, ShuffleSplit, TimeSeriesSplit) train_test_split(data, train_size=0.8) # 单次切,无 CV KFold(n_splits=5) # 随机切,金融禁用 KFold(n_splits=5, shuffle=True) # 显式 shuffle,金融更禁用 LeaveOneOut() # 留一,小样本用,金融禁用 LeavePOut(p=2) # 留 p,组合爆炸 ShuffleSplit(n_splits=3, test_size=2) # 随机,金融禁用 TimeSeriesSplit(n_splits=5) # 时序扩展窗,金融可用但有限

金融实务中,几乎所有「shuffle/random」的切分器都要慎用。

四、MultipleTimeSeriesCV:面板数据的时序 CV

项目根目录 utils.pyMultipleTimeSeriesCV 是为金融面板数据量身定制的。它支持:

  • 多资产:MultiIndex 含 (symbol, date)
  • rolling window:固定 train_period_length + test_period_length,训练窗滚动而非扩展。
  • purge 间隔:lookahead 参数在训练和测试之间留出标签长度的间隔,防止标签重叠泄露。

核心参数

from utils import MultipleTimeSeriesCV cv = MultipleTimeSeriesCV(n_splits=3, train_period_length=126, # 训练窗 126 天(半年) test_period_length=21, # 测试窗 21 天(一个月) lookahead=1) # 标签是 1 天前向收益

split 的几何含义

每个 fold 的训练段在测试段之前,且中间隔了 lookahead - 1 天(purge)。

源码核心逻辑

class MultipleTimeSeriesCV: def __init__(self, n_splits=3, train_period_length=126, test_period_length=21, lookahead=None, date_idx='date', shuffle=False): self.n_splits = n_splits self.lookahead = lookahead self.test_length = test_period_length self.train_length = train_period_length ... def split(self, X, y=None, groups=None): unique_dates = X.index.get_level_values(self.date_idx).unique() days = sorted(unique_dates, reverse=True) # 从最近往回滚 split_idx = [] for i in range(self.n_splits): test_end_idx = i * self.test_length test_start_idx = test_end_idx + self.test_length train_end_idx = test_start_idx + self.lookahead - 1 # purge 间隔 train_start_idx = train_end_idx + self.train_length + self.lookahead - 1 split_idx.append([train_start_idx, train_end_idx, test_start_idx, test_end_idx]) # 把日期索引转回行索引 dates = X.reset_index()[[self.date_idx]] for train_start, train_end, test_start, test_end in split_idx: train_idx = dates[(dates[self.date_idx] > days[train_start]) & (dates.date <= days[train_end])].index test_idx = dates[(dates.date > days[test_start]) & (dates.date <= days[test_end])].index yield train_idx.to_numpy(), test_idx.to_numpy()

关键点:days 按日期倒序排序,从最近端往回滚动,保证「最近的测试集」对应的训练在它之前。lookahead 体现在 train_end_idx = test_start_idx + self.lookahead - 1——训练段末尾留出 lookahead 长度的间隔,因为训练段最后 lookahead 天的标签会延伸到测试段,必须剔除。

五、purge 与 embargo(Lopez de Prado)

MultipleTimeSeriesCVlookahead 实现了基本的 purge。Lopez de Prado 在《Advances in Financial Machine Learning》里把这套理论系统化:

  • Purge(清洗):删除训练集中那些「标签时间段与测试集重叠」的样本。预测 5 天收益时,训练集最后 4 天的标签会进入测试集,必须删。
  • Embargo(禁运):在测试集之后再加一段隔离区,因为测试集的标签可能延伸到下一段训练数据,反向污染。

⚠️ 实务陷阱:很多人用时序 CV 但忘了 lookahead,等于「我做了 CV 啊怎么还是泄露?」。lookahead 必须等于你标签的前向窗口长度

六、参数选择

参数 典型值 选择依据
train_period_length 63/126/252(季/半年/年) 足够学到稳定模式,又不跨多个制度
test_period_length 10/21(两周/月) 短到模式不过时,长到统计有意义
lookahead 标签天数 严格等于预测目标的前向窗口
n_splits 3~10 多则稳但慢,少则快但噪声大

经验法则:训练段要比测试段长 3~6 倍,保证模型有足够样本;n_splits 至少 5 折以降低单折方差。

本节要点回顾

  1. K-Fold 在金融等于泄露:训练集含未来、标签重叠,是「神奇高收益」的头号元凶。
  2. TimeSeriesSplit 解决时间错位,但不支持面板、不能 rolling、无 purge。
  3. MultipleTimeSeriesCV 支持多资产面板、固定窗长 rolling、lookahead purge。
  4. purge/embargo:训练段末尾留 lookahead 间隔防标签泄露,测试段后留禁运区防反向污染。
  5. 参数选择:训练段 = 测试段 × 3~6,lookahead 严格等于标签前向窗口,n_splits ≥ 5。

本章完。下一章我们进入「建模篇」——线性模型选股,从 OLS 到 Ridge/Lasso 正则化,再到 Fama-MacBeth 横截面回归。


发布者: 作者: 灏天文库 转发
评论区 (0)
U