金融机器学习防泄漏:Purged K-Fold Cross-Validation¶
在传统的机器学习领域,标准交叉验证(Standard K-Fold)是评估模型的基石。 金融样本具有时间顺序,标签区间还可能重叠。直接随机使用标准 K-Fold 可能造成数据泄漏(Data Leakage),使验证结果明显高估样本外表现。
量化巨擘 Marcos Lopez de Prado 提出了 Purged K-Fold 框架。
1. 金融时间序列交叉验证的死穴¶
- 窥视未来 (序列相关性泄漏):如果用 2019 年(测试集)的数据测试,而用 2018 以及 2020(训练集)训练。金融资产短时间内具有高度动量残余,2020年初的价格早包含了2019的结论反馈。
- 标签重叠:若标签是未来 5 日收益率,相邻样本的标签计算区间会共享价格。训练集与测试集边界附近的重叠区间需要通过 Purge 移除。
2. 解决方案:Purged K-Fold 净化交叉验证¶
- Purging (剔除机制): 如果测试集的任何标签观察窗口,与训练集的特征周期发生物理重合,必须强制从训练集中剔除(Purge)该样本。
- Embargo (禁闭机制): 测试集结束后的那段时间数据由于动量惯性依然包含了测试集的信息。在测试区间之后的那块训练集中,必须额外定义一段禁闭期(Embargo period)进行隔离。
1. 这节究竟解决什么实际问题?¶
金融机器学习防泄漏:Purged K-Fold Cross-Validation 的具体任务是:用 Purged K-Fold 和 embargo 隔离重叠标签造成的泄漏。 本节将明确输入数据、计算过程、输出及其在后续研究中的用途。
2. 前置知识¶
掌握特征、标签、训练集/测试集和基本回测概念。 对公式的理解应包括变量定义、单位、时间索引和适用条件。
3. 数据说明¶
特征默认来自固定样本或模拟样本;每个标签必须标明预测期限及其可交易时间。 运行前先检查 shape、日期范围、列名与缺失值。数据看起来能画图,不代表它适合回答研究问题。
4. 学习目标与完成标准¶
完成本节后,你应能用自己的话解释 金融机器学习防泄漏:Purged K-Fold Cross-Validation 的输入、计算步骤、输出和局限;尤其能说明:用 Purged K-Fold 和 embargo 隔离重叠标签造成的泄漏。 你还应能独立改动一个参数并解释结果为什么变化,指出至少一个会让结论失真的假设。
5. 核心概念与公式¶
机器学习模型从训练样本估计输入与目标之间的关系。训练集拟合程度不能代表样本外预测能力。
训练时只能看到训练区间;特征时间不得晚于决策时间,标签与测试区间必须隔离。 看到公式时依次确认:每个符号的单位是什么?它属于哪个时点?分母会不会为零?它是描述性统计量、预测量,还是交易规则?
6. 从零实现¶
原始代码按照“准备输入 → 计算中间量 → 得到结果 → 解释输出”的顺序组织。每个单元应检查变量类型、索引、维度和代表性数值。中间变量用于核对公式实现和定位计算错误。
7. 成熟库实现¶
sklearn/PyTorch 加速建模;管道、切分和经济解释仍由研究者负责。 使用库函数前先阅读参数含义、默认值和返回对象;库输出一个数字,并不说明这个数字适合你的交易假设。
8. 两种实现的差异¶
从零实现便于核对公式、调试和识别隐含假设;成熟库适合在计算逻辑已验证后复用。两种实现依赖相同的数据口径与时点假设,使用库函数不会改变输入数据的质量。
9. 预期结果与检查范围¶
你应看到维度正确的 Series/DataFrame、日期单调递增的索引,以及能支持“用 Purged K-Fold 和 embargo 隔离重叠标签造成的泄漏。”这一任务的表格或图形。金融日收益率均值通常接近 0;若出现极大的连续收益、空表、全 NaN 或索引错位,应先排查数据和对齐逻辑,而不是立即相信结果。
10. 常见错误与诊断¶
随机打乱时序、Scaler 在全样本 fit、目标泄漏、调参反复查看测试集、把准确率等同收益。
诊断顺序:先打印类型与 shape,再看 head()/tail(),接着检查日期和 NaN,最后才比较数值。这样能避免在错误的对象上反复改参数。
11. 研究偏差检查¶
时间泄漏、p-hacking、多重检验和样本选择会制造虚假的 Alpha;必须报告朴素基线。 结果至少应与一个简单基线比较,并明确哪些部分只是教学演示。
12. 三层练习与答案¶
- 基础练习:验证一个中间变量、补全一段小计算,并用一句话解释输出。
- 研究练习:改变窗口、样本区间、约束或标的,记录哪些结论仍成立。
- 开放项目:把本节方法嵌入模块项目,报告基线、失败结果和限制。
题目在 ../exercises/06_ml_trading/05_cross_validation_exercises.ipynb;完整推导、可运行参考实现与结果检查在 ../solutions/06_ml_trading/05_cross_validation_solutions.ipynb。请先独立作答,再打开答案。
13. 延伸阅读¶
延伸材料:scikit-learn 模型选择文档。阅读记录应包括研究假设、数据频率、样本范围和结论适用条件。
14. 下一节¶
下一节是 06_advanced_portfolio_optimization。本节的概念会成为理解它的输入,而不是孤立的名词。
def conceptual_purged_kfold(total_samples, num_folds=5, purge_window=3, embargo=5):
"""
这是对 Purged K-Fold 的原理演示(为了直观理解,并非 mlfinlab 等严格包源码)。
"""
test_size = total_samples // num_folds
print(f"总样本: {total_samples}, 折数: {num_folds}, 标签计算跨度(Purge): {purge_window}, 禁闭期(Embargo): {embargo}\n")
for fold in range(num_folds):
test_start = fold * test_size
test_end = test_start + test_size
# Purging 逻辑: 训练集不能包含距离测试集太近(可能偷看未来)的前置样本
# Embargo 逻辑: 训练集不能包含紧贴在测试集后方、具有强烈动量遗留的后置样本
train_indices = [i for i in range(total_samples)
if i < (test_start - purge_window) or i > (test_end + embargo)]
print(f"---------- Fold {fold} ----------")
print(f"[验证 测试区]: {test_start} 到 {test_end}")
print(f"[安全 训练区]: 包含 {len(train_indices)} 个样本, 剔除了 {(total_samples - len(train_indices) - test_size)} 个污染样本")
# 运行演示
conceptual_purged_kfold(100, num_folds=4)
总样本: 100, 折数: 4, 标签计算跨度(Purge): 3, 禁闭期(Embargo): 5 ---------- Fold 0 ---------- [验证 测试区]: 0 到 25 [安全 训练区]: 包含 69 个样本, 剔除了 6 个污染样本 ---------- Fold 1 ---------- [验证 测试区]: 25 到 50 [安全 训练区]: 包含 66 个样本, 剔除了 9 个污染样本 ---------- Fold 2 ---------- [验证 测试区]: 50 到 75 [安全 训练区]: 包含 66 个样本, 剔除了 9 个污染样本 ---------- Fold 3 ---------- [验证 测试区]: 75 到 100 [安全 训练区]: 包含 72 个样本, 剔除了 3 个污染样本
若标签区间重叠,未使用 Purge 或等价处理的 $R^2$ 与准确率可能受到泄漏影响,需要重新验证。
3. Standard K-Fold 与 Purged K-Fold 的切分对比¶
下图对比普通 K-Fold 与 Purged K-Fold 的时间序列切分位置,并标出被移除的重叠样本。
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Patch
# 定义一个可视化函数来对比 K-Fold 划分
def plot_cv_indices(cv_splits, num_samples, ax, lw=10, purge=0, embargo=0):
"""根据输入的 (train_idx, test_idx) 生成水平条形图"""
for ii, (tr, tt) in enumerate(cv_splits):
# 画测试集 (红色)
ax.scatter(tt, [ii] * len(tt), marker='_', s=1000, lw=lw, c='red')
# 画训练集 (蓝色)
ax.scatter(tr, [ii] * len(tr), marker='_', s=1000, lw=lw, c='blue')
# 标出被 Purge 和 Embargo 剔除的灰色区域
all_indices = set(range(num_samples))
used_indices = set(tr) | set(tt)
purged_indices = list(all_indices - used_indices)
if purged_indices:
ax.scatter(purged_indices, [ii] * len(purged_indices), marker='_', s=1000, lw=lw, c='gray')
ax.set_yticks(np.arange(len(cv_splits)))
ax.set_yticklabels([f'Fold {i}' for i in range(len(cv_splits))])
ax.set_xlabel('Sample Index (Time)')
legend_elements = [
Patch(facecolor='blue', label='Train Data'),
Patch(facecolor='red', label='Test Data'),
Patch(facecolor='gray', label='Purged/Embargoed Data (Dropped)')
]
ax.legend(handles=legend_elements, loc='best')
# 模拟生成切分数据
num_samples = 100
num_folds = 5
# 1. Standard K-Fold (没有剔除,存在重度泄漏)
standard_splits = []
test_size = num_samples // num_folds
for i in range(num_folds):
test_start = i * test_size
test_end = test_start + test_size
test_idx = list(range(test_start, test_end))
train_idx = list(range(0, test_start)) + list(range(test_end, num_samples))
standard_splits.append((train_idx, test_idx))
# 2. Purged K-Fold (严格剔除边界)
purge_window = 3
embargo_window = 5
purged_splits = []
for i in range(num_folds):
test_start = i * test_size
test_end = test_start + test_size
test_idx = list(range(test_start, test_end))
train_idx = [j for j in range(num_samples)
if j < (test_start - purge_window) or j > (test_end + embargo_window)]
purged_splits.append((train_idx, test_idx))
# 绘图对比
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8))
plot_cv_indices(standard_splits, num_samples, ax1)
ax1.set_title("Standard K-Fold (DANGER: High risk of data leakage at boundaries)")
plot_cv_indices(purged_splits, num_samples, ax2, purge=purge_window, embargo=embargo_window)
ax2.set_title(f"Purged K-Fold (SAFE: Purge={purge_window}, Embargo={embargo_window})")
plt.tight_layout()
plt.show()
上图显示两种切分方法的以下差异:
- 上图 (Standard K-Fold) 训练集和测试集无缝衔接。在金融中,边界处的数据完全混合了未来的信息,使得你的模型仿佛“开卷考试”。
- 下图 (Purged K-Fold) 灰色的断带如同防火墙。Purge(测试集左侧灰色) 阻止了带有跨越边界标签的训练样本进入训练;Embargo(测试集右侧灰色) 阻止了紧接着测试集发生的具有强惯性相关性的样本被模型提取特征。
本章结语¶
在金融预测中,若随机森林(Random Forest)、XGBoost 或 Transformer 使用普通的 sklearn.model_selection.KFold 或未考虑时间顺序的数据切分,准确率(Accuracy)和夏普比率(Sharpe Ratio)可能受到信息泄漏影响。
应先暂停模型比较并检查数据切分。 此时应检查训练样本与验证样本是否存在时间重叠、标签区间重叠或预处理泄漏,并改用符合时间顺序的验证方法。
**Purged K-Fold 与组合交叉验证(CPCV)**适用于标签区间可能重叠的金融样本。Purge 用于移除训练集与测试集之间的重叠标签,Embargo 用于保留额外时间间隔。使用自有实现或第三方库时,都需要核对样本区间和参数定义。
练习¶
- 使用
mlfinlab库的PurgedKFold类,对第 3 章的深度学习模型进行净化交叉验证,比较 IS 和 OOS 夏普。 - 将
embargo_window从 5 调整到 10 和 20,观察被剔除样本数量的变化和对验证集大小的影响。 - 查阅 CPCV(Combinatorial Purged Cross-Validation):它相比基础 Purged K-Fold 改进了什么?
下一节 → 06_advanced_portfolio_optimization.ipynb