3、时间序列切分基础:时间序列数据的特点、滚动窗口切分、扩展窗口切分、固定窗口切分

在绩效归因模型训练中,样本数据通常以时间序列形式存在——收益率、因子暴露、行业权重等指标按时间顺序排列。与独立同分布数据不同,时间序列样本之间存在时间依赖性和序列相关性,这决定了我们不能使用随机打乱(shuffle)的方式进行样本切分,而必须采用时间感知的窗口切分策略。

3.1 时间序列数据的特点

理解时间序列数据的固有特性,是选择正确切分方法的前提。下表总结了时间序列数据与横截面数据在样本切分时的关键差异:

对比维度 横截面数据 时间序列数据
样本独立性 样本间独立,可随机打乱 样本间存在自相关,不可随机打乱
时间顺序 无时间顺序要求 必须保持时间顺序
信息泄露风险 低(随机切分即可) 高(未来信息不可用于预测过去)
训练/测试划分 随机抽样 按时间点截断
交叉验证方式 K-Fold 交叉验证 滚动/扩展/固定窗口验证
核心原则:在时间序列切分中,训练集的时间必须早于验证集/测试集的时间。任何使用未来数据训练、过去数据验证的做法都会导致“未来信息泄露”,使模型在回测中表现虚高,实盘时大幅失效。

时间序列数据还常表现出非平稳性(均值、方差随时间变化)和季节性(如月度效应、季度效应),这些特点要求切分策略能够捕捉不同市场环境下的模型表现,而非仅依赖单一时间段的拟合结果。

3.2 三种核心窗口切分方法

针对绩效归因模型训练,业界主要采用三种时间序列切分策略:滚动窗口、扩展窗口和固定窗口。下图展示了三种方法的切分逻辑对比:

三种时间序列窗口切分方法对比 t 滚动窗口 训练集 1 验证集 1 训练集 2 验证集 2 训练集 3 验证集 3 扩展窗口 训练集 1 验证集 1 训练集 2(扩展) 验证集 2 训练集 3(扩展) 验证集 3 固定窗口 训练集 验证集 测试集 滚动训练 扩展训练 固定训练 验证/测试

3.3 滚动窗口切分(Rolling Window)

滚动窗口(又称滑动窗口)保持训练集长度固定,每次向前滑动一个步长,丢弃最旧的数据,加入最新的数据。这种方法适用于市场环境变化较快、旧数据对当前预测价值较低的场景。

适用场景:高频因子归因、短期动量策略、市场微观结构分析。当模型需要快速适应最新市场状态时,滚动窗口能有效避免“过时数据”对模型训练的干扰。

关键参数:

  • 窗口长度(W):每次训练使用的样本数量。过短会导致模型欠拟合,过长则无法及时捕捉市场变化。
  • 步长(S):每次向前移动的时间单位。步长=1时,每次只新增一个样本;步长较大时,计算效率更高但验证次数减少。
  • 验证集长度(V):每次验证使用的样本数量,通常为1个时间步或一个固定周期(如1个月)。

Python 实现示例:

import numpy as np

def rolling_window_split(dates, window_len=252, step=21, val_len=21):
    """
    滚动窗口切分生成器
    :param dates: 时间序列索引(已排序)
    :param window_len: 训练窗口长度(交易日数)
    :param step: 滑动步长
    :param val_len: 验证集长度
    :yield: (train_indices, val_indices)
    """
    total = len(dates)
    start = 0
    while start + window_len + val_len <= total:
        train_end = start + window_len
        val_end = train_end + val_len
        train_idx = list(range(start, train_end))
        val_idx = list(range(train_end, val_end))
        yield train_idx, val_idx
        start += step

3.4 扩展窗口切分(Expanding Window)

扩展窗口的训练集起始点固定,每次向前滑动时只增加新数据,不丢弃旧数据。训练集长度随时间不断增长。这种方法适用于数据量较小、或市场长期趋势稳定的场景,能够最大化利用历史信息。

注意:扩展窗口在数据量极大时会导致训练集过于庞大,增加计算开销。此外,如果市场存在结构性突变(如2008年金融危机、2020年新冠疫情),早期数据可能对当前模型产生负面影响,此时应考虑使用滚动窗口或加权扩展窗口。

扩展窗口 vs 滚动窗口:

对比维度 扩展窗口 滚动窗口
训练集长度 持续增长 固定不变
历史数据利用 全部保留 丢弃最旧数据
计算成本 随时间递增 相对稳定
对结构突变的适应性 弱(旧数据持续影响) 强(自动淘汰旧数据)
小样本场景 推荐使用 可能样本不足

Python 实现示例:

def expanding_window_split(dates, min_window_len=252, step=21, val_len=21):
    """
    扩展窗口切分生成器
    :param dates: 时间序列索引(已排序)
    :param min_window_len: 初始训练窗口最小长度
    :param step: 滑动步长
    :param val_len: 验证集长度
    :yield: (train_indices, val_indices)
    """
    total = len(dates)
    start = 0
    train_end = min_window_len
    while train_end + val_len <= total:
        train_idx = list(range(start, train_end))
        val_idx = list(range(train_end, train_end + val_len))
        yield train_idx, val_idx
        train_end += step

3.5 固定窗口切分(Fixed Window)

固定窗口是最简单的时间序列切分方式:在时间轴上选择一个截断点,之前的数据作为训练集,之后的数据作为验证集/测试集。通常还会进一步将验证集和测试集分开,形成“训练-验证-测试”三段式结构。

常见比例分配:

  • 70% / 15% / 15%:训练集 / 验证集 / 测试集,适用于数据量充足的情况。
  • 80% / 10% / 10%:训练集占比更高,适用于模型复杂度较高、需要更多训练样本的场景。
  • 按时间点截断:例如“2020年之前为训练集,2020-2021年为验证集,2022年之后为测试集”,更符合业务逻辑。
固定窗口的局限性:单次切分无法评估模型在不同市场周期(牛市、熊市、震荡市)下的表现稳定性。因此,固定窗口通常只作为最终测试的划分方式,而模型调参和选择应依赖滚动窗口或扩展窗口的交叉验证结果。

Python 实现示例:

def fixed_window_split(dates, train_ratio=0.7, val_ratio=0.15):
    """
    固定窗口切分
    :param dates: 时间序列索引(已排序)
    :param train_ratio: 训练集比例
    :param val_ratio: 验证集比例
    :return: (train_indices, val_indices, test_indices)
    """
    total = len(dates)
    train_end = int(total * train_ratio)
    val_end = train_end + int(total * val_ratio)
    train_idx = list(range(0, train_end))
    val_idx = list(range(train_end, val_end))
    test_idx = list(range(val_end, total))
    return train_idx, val_idx, test_idx

3.6 三种方法的选型决策

在实际绩效归因项目中,三种方法并非互斥,而是可以组合使用。下表给出了基于不同场景的选型建议:

场景特征 推荐方法 理由
数据量小(<3年日频数据) 扩展窗口 最大化利用有限历史数据
市场环境变化频繁 滚动窗口 自动淘汰过时数据,快速适应新环境
模型最终评估 固定窗口 模拟真实上线时的训练/预测时间点
超参数调优 滚动窗口 + 扩展窗口 多组验证结果,评估参数稳定性
存在结构性突变点 滚动窗口(短窗口) 避免突变前数据污染模型
低频数据(月度/季度) 扩展窗口 样本点少,不宜丢弃任何数据
实践建议:在绩效归因模型训练中,建议采用“滚动窗口调参 + 固定窗口终测”的组合策略。即使用滚动窗口交叉验证进行模型选择和超参数调优,最后使用固定窗口(以最新数据为测试集)评估模型的最终表现。这样既能保证调参过程的稳健性,又能模拟真实部署时的预测场景。