跳到正文

净化 k 折:推导

训练样本什么时候会泄露到测试集?答案取决于三个区间:标签窗口、特征窗口和测试窗口。下面是完整几何推导。

第 6 篇 · 共 12 篇 约 10 分钟

净化 k 折:从第一性原理推导

上一篇文章解释了为什么标准 CV 在时间序列上会失败。本文严格推导什么时候需要净化。结果是一条精确的几何规则:当且仅当训练观测的标签窗口或特征窗口与测试窗口相交时,它就会泄露到测试折。净化宽度取决于三个区间:标签跨度 hh、特征回看 ww、测试折的大小。

推导本身很直接。得出的规则在实务中经常被违反。很多“净化 CV”实现的几何都弄错了:净化不够(留下泄露)或净化太多(浪费数据)。正确的规则是精确的。

(López de Prado, M., Advances in Financial Machine Learning, §7.4.2。ISBN 978-1119482086。)

设定

我们有一个以时点 tt 索引的时间序列观测。每条观测有:

  • 标签窗口 [t,t+h][t, t+h]:标签计算的时间区间(例如交易的持仓期)。
  • 特征窗口 [t−w,t−1][t-w, t-1]:特征计算的时间区间(例如动量的滚动回看)。

时点 tt 的标签依赖 [t,t+h][t, t+h] 的数据。时点 tt 的特征依赖 [t−w,t−1][t-w, t-1] 的数据。

测试折是时点的连续区间 [Ts,Te][T_s, T_e]。训练集是其他所有时段。

我们想知道:对哪些训练观测 tt,测试折会“泄露”到该观测的标签或特征里?

两条泄露通道

训练观测可能通过两条通道泄露测试信息。

通道 1:标签泄露。

时点 tt 的标签依赖 [t,t+h][t, t+h] 的数据。如果这个区间和测试折 [Ts,Te][T_s, T_e] 相交,标签就被测试期数据污染了。这个训练观测在打标签时用到了测试折的信息。

通道 2:特征泄露。

时点 tt 的特征依赖 [t−w,t−1][t-w, t-1] 的数据。如果这个区间和测试折相交,特征就被测试期数据污染了。这个训练观测在算特征时用到了测试折的信息。

只要任一通道泄露,就必须净化这个训练观测。两条通道都必须干净,观测才安全。

通道 1:标签泄露的细节

我们想找所有满足 [t,t+h]∩[Ts,Te]≠∅[t, t+h] \cap [T_s, T_e] \neq \emptyset 的 tt。

两个区间 [a,b][a, b] 和 [c,d][c, d] 相交,当且仅当 a≤da \leq d 且 c≤bc \leq b。等价地,左端点的最大值要小于或等于右端点的最小值。

应用到这里:

[t,t+h]∩[Ts,Te]≠∅  ⟺  max⁡(t,Ts)≤min⁡(t+h,Te)[t, t+h] \cap [T_s, T_e] \neq \emptyset \iff \max(t, T_s) \leq \min(t+h, T_e)

对训练观测(t<Tst < T_s,因为训练在测试折之前),可以简化为:

t≤Ts≤t+h  ⟺  Ts−h≤t≤Tst \leq T_s \leq t+h \iff T_s - h \leq t \leq T_s

所以标签泄露影响 [Ts−h,Ts][T_s - h, T_s] 区间内的训练观测。

标签泄露区间的宽度恰好是 hh(标签跨度)。

(AFML, Eq. 7.1。)

通道 2:特征泄露的细节

我们想找所有满足 [t−w,t−1]∩[Ts,Te]≠∅[t-w, t-1] \cap [T_s, T_e] \neq \emptyset 的 tt。

同样的逻辑:

[t−w,t−1]∩[Ts,Te]≠∅  ⟺  max⁡(t−w,Ts)≤min⁡(t−1,Te)[t-w, t-1] \cap [T_s, T_e] \neq \emptyset \iff \max(t-w, T_s) \leq \min(t-1, T_e)

对训练观测(t<Tst < T_s):

t−w≤Ts≤t−1  ⟺  Ts+1≤t≤Ts+wt-w \leq T_s \leq t-1 \iff T_s + 1 \leq t \leq T_s + w

所以特征泄露影响 [Ts+1,Ts+w][T_s + 1, T_s + w] 区间内的训练观测。

特征泄露区间的宽度是 w−1w - 1(略小于特征窗口,因为特征窗口的上端点是 t−1t-1 而不是 tt)。

一个细节:这是来自测试折前缘的泄露(测试折开始后的数据)。对严格在测试折之前的训练观测,特征窗口完全在测试折之前。对测试折之后紧邻的训练观测,特征窗口向后延伸到测试折里。这种不对称性正是我们需要禁运(下一篇文章)而不是仅做净化的原因。

合并两条通道

只要任一通道泄露,就要净化。合并后的净化区间是:

[Ts−h,Ts]∪[Ts+1,Ts+w][T_s - h, T_s] \cup [T_s + 1, T_s + w]

但是在 walk-forward CV 中,训练集严格在测试折之前。区间 [Ts+1,Ts+w][T_s + 1, T_s + w] 在测试折之后,本来就不在训练集里。相关的泄露区间只有:

[Ts−h,Ts][T_s - h, T_s]

所以对训练观测来说,净化宽度就是 hh,也就是标签跨度。

规则是:删除测试折之前紧邻的 hh 天的训练观测。

(AFML, §7.4.2。)

禁运的方向

区间 [Ts+1,Ts+w][T_s + 1, T_s + w] 是测试折之后的训练数据。在 walk-forward CV 中,没有这种数据。在滚动 CV(训练集包含测试折之后的数据)或者重拟合场景(在测试折之后的数据上重新训练)中,测试折之后的数据有特征泄露。

这就是禁运的情况。禁运宽度是 w−1w - 1(或 ww,看约定)。禁运施加在测试折的后缘,也就是测试折之后的训练数据上。

下一篇文章会详细讲禁运。目前的规则是:在 walk-forward CV 中,净化前缘 hh 天。在滚动 CV 或重拟合场景中,还要禁运后缘 ww 天。

几何可视化

想象时间轴。测试折从 TsT_s 到 TeT_e。

        |--h--|                              |---w---|
--------[-----]--------------------------------[-----]---------
       T_s-h   T_s                          T_e+1   T_e+w
         净化                                 禁运

净化区是测试折之前紧邻的 hh 天。禁运区是之后紧邻的 ww 天。净化区内的训练观测,标签会延伸到测试折。禁运区内的训练观测,特征会向后延伸到测试折。

在 walk-forward CV(扩张窗口,测试折内不重训练)中,只用净化。训练集在 Ts−h−1T_s - h - 1 处结束(或者更早)。

在滚动 CV(固定大小训练窗口)或重拟合场景中,两者都用。训练集在 Ts−h−1T_s - h - 1 到 Te+w+1T_e + w + 1 之间有一个缺口(具体取决于滚动窗口大小,可能有重叠)。

三重障碍标签的特殊情况

上面的推导假设标签窗口恰好是 [t,t+h][t, t+h]。对三重障碍标签(上一篇文章),标签窗口是动态的:在上障碍、下障碍和纵向障碍中,哪个先被触发就以哪个结束。

纵向障碍是 hh 天。动态障碍可能更早就被触发。标签窗口是 [t,thit][t, t_{\text{hit}}],其中 thit≤t+ht_{\text{hit}} \leq t + h。

对净化推导,用最大可能的标签窗口,也就是 hh。这是保守的:可能净化得比严格需要的更多,但保证了没有标签泄露。

实际上差异很小。如果纵向障碍是 5 天,动态障碍平均 1–2 天触发,保守净化就用 h=5h = 5,而平均 thit−t=1.5t_{\text{hit}} - t = 1.5。出于安全考虑,保守规则更可取。

(AFML, §3.6。)

并发性调整

一个进一步修正:当很多观测的标签窗口重叠时,独特信息量很小(回忆样本权重一文中的唯一性权重)。净化区应该考虑这一点。

如果你的数据集里每条观测平均有 cc 个并发标签(也就是每条标签平均和 c−1c - 1 个其他标签重叠),有效标签跨度大约是 c⋅hc \cdot h。净化宽度应该按 cc 缩放:

调整后的净化宽度=c⋅h\text{调整后的净化宽度} = c \cdot h

这是一个经验法则,不是严格推导。但它捕捉了“重叠标签产生简单 hh 天净化解决不了的复合泄露”的直觉。

(AFML, §4.5.2。)

测试折本身呢?

测试折在训练中被保留,所以我们要关心的是从训练到测试的泄露。但从测试到后续评估的泄露呢?

在单测试折的 walk-forward CV 中,这不是问题:测试折只评估一次,下一折重新开始。在多测试折的 CPCV 中,同一条观测可能出现在多条路径的测试集里。夏普比率因此相关。

净化方面,把每个测试折独立处理。测试折的标签不用来训练,所以不存在从测试到训练的泄露。泄露只是从训练到测试(通过重叠的特征或标签)。

实现

一个干净的实现:

def purge_train_obs(train_times, test_start, test_end, h, w=0):
    """Drop training observations that leak into the test fold.
    
    Label window: [t, t+h]
    Feature window: [t-w, t-1]
    Test fold: [test_start, test_end]
    
    Returns: filtered training times.
    """
    # Label leakage: training obs with label window overlapping test
    # For training obs (t < test_start), this is t in [test_start - h, test_start]
    label_leak = (train_times >= test_start - h) & (train_times <= test_start)
    
    # Feature leakage (post-test, only relevant for rolling CV or refit):
    # For training obs (t > test_end), this is t in [test_end + 1, test_end + w]
    feature_leak = (train_times >= test_end + 1) & (train_times <= test_end + w)
    
    # Drop the leaking observations
    purged = train_times[~(label_leak | feature_leak)]
    
    return purged

对 walk-forward CV,train_times 全部在 test_start 之前,所以 feature_leak 为空。只用 label_leak。

对滚动 CV 或重拟合场景,两者都用,还应该删除 test_end 之后的禁运区内的观测。

一个常见错误:两边都净化

实现中一个常见错误:在测试折两边施加同样的净化。这不对。

前缘(测试折之前)的净化针对的是标签泄露:标签延伸到测试折的训练观测。

后缘(测试折之后)的“净化”其实不是同一种净化。它是针对特征泄露的禁运。特征向后延伸到测试折的训练观测。

这是不同的机制,宽度也不同。前缘净化宽度是 hh(标签跨度)。后缘禁运宽度是 ww(特征回看)。两者不能混为一谈。

如果你的实现在两边用同样的宽度,要么在一边净化过度,要么在另一边净化不足。

(AFML, §7.4.2–7.4.3。)

如果标签和特征的窗口不同怎么办?

上面的推导假设所有特征有同样的回看窗口 ww。实际中,特征窗口不同:20 天波动率、60 天动量、250 天移动平均。净化区应该足够宽以覆盖最大窗口。

设 wmax⁡w_{\max} 是数据集中最大的特征回看。禁运宽度至少应该是 wmax⁡w_{\max}。一个保守规则:用所有特征中的最大值。

更紧的规则:对每条训练观测,检查它的特征窗口(针对具有最大 ww 的具体特征)是否和测试折相交。这更精确但计算量更大。

一个实务折中:先计算一次 wmax⁡w_{\max},再对所有观测使用。这快,但略偏保守。

与样本权重的交互

净化删除训练观测。样本权重(来自四种类型一文)给剩下的观测分配权重。这是两种不同的操作:

  • 净化:删除会泄露测试信息的观测。
  • 权重:对冗余、过时或高波动率的观测降权。

常见混淆:以为样本权重可以替代净化。不能。权重调整损失函数中观测的重要性,净化完全删除观测。一条权重为零的观测仍然会贡献训练集的分布性质(例如特征的实证分布)。被净化掉的观测不会。

两个都用。先净化,再加权。权重作用在净化后的训练集上。

数学表达

净化规则用一条式子表达:

Tpurged={t∈Ttrain:[t,t+h]∩[Ts,Te]=∅ and [t−w,t−1]∩[Ts,Te]=∅}\mathcal{T}_{\text{purged}} = \{ t \in \mathcal{T}_{\text{train}} : [t, t+h] \cap [T_s, T_e] = \emptyset \text{ and } [t-w, t-1] \cap [T_s, T_e] = \emptyset \}

用文字说:净化后的训练集是那些标签窗口和特征窗口都不与测试折相交的训练观测。

对 walk-forward CV(训练在测试之前),对 t<Tst < T_s,第二个条件天然成立:

Tpurged={t∈Ttrain:t<Ts−h}\mathcal{T}_{\text{purged}} = \{ t \in \mathcal{T}_{\text{train}} : t < T_s - h \}

这就是规则:删除测试折开始前 hh 天的训练观测。

简单提一下禁运

后缘情况在下一篇文章中讲。规则是:

Tpurged, embargoed={t∈Ttrain:t<Ts−h or t>Te+w}\mathcal{T}_{\text{purged, embargoed}} = \{ t \in \mathcal{T}_{\text{train}} : t < T_s - h \text{ or } t > T_e + w \}

对 walk-forward CV(训练在测试之前),只用第一个条件。

对滚动 CV(训练包含测试之后的数据)或重拟合场景,两个条件都用,在 Ts−h−1T_s - h - 1 到 Te+w+1T_e + w + 1 之间有一个缺口。

常见错误

实现净化 CV 时的五种错误。

错误 1:净化宽度按日历日,而不是标签跨度。

如果你的观测是日频,但标签按 5 天窗口计算,净化宽度是 5,不是 1。观测之间的日历日间隔不重要,重要的是用观测索引单位衡量的标签跨度。

错误 2:忘记特征窗口的偏一。

特征窗口是 [t−w,t−1][t-w, t-1],长度为 ww,但只覆盖 ww 个不同的观测。别把 ww(窗口长度)和 w−1w - 1(观测数)搞混了。为了和标签窗口一致,禁运宽度用 ww。

错误 3:把所有特征当作同样窗口。

如果数据集里有 20 天和 250 天窗口的特征,禁运必须覆盖 250 天窗口。否则,250 天的特征会在后缘泄露。

错误 4:净化两次。

有些实现会先净化一次,然后在 CV 方案的每个测试折上再净化一次。这会重复净化,把本该保留的观测也删除了。每个测试折边界上做一次净化就够了。

错误 5:使用三重障碍标签时没有净化。

三重障碍标签的标签跨度是 hh(纵向障碍)。净化宽度应该是 hh,不是平均持仓期。用最大标签窗口。

更大的视角

净化 CV 推导是更广泛原则的具体应用:不要用未来信息预测过去。在时间序列机器学习中,这个原则表现为:

  • 时间划分(walk-forward CV):不要用未来数据预测过去。
  • 净化:训练观测的标签或特征不能触及测试折。
  • 禁运:测试折之后的训练观测,特征不能向后延伸到测试折。
  • 特征中不能有前视:不能使用依赖预测时间之后数据的特征。

每条规则针对一个具体的泄露通道。合起来,它们构成对时间污染的完整防御。

数学很简单,纪律很难。大部分实现至少在某一条规则上犯错。一个常用的审计问题:“对每条训练观测,你能写下来计算它的特征和标签时所用的具体数据,并验证这些数据和测试折没有重叠吗?”如果不能,CV 就在泄露。

(López de Prado, M., Advances in Financial Machine Learning, §7.4.2。ISBN 978-1119482086。)