4. 训练集切分原理:时间序列数据切分原则、避免数据泄露、训练/验证/测试集划分策略

在实时风控建模中,数据切分不是简单的随机抽样。由于欺诈行为具有时间演化特性,错误的切分方式会导致模型在线上表现远低于离线评估结果。本章聚焦时间序列数据特有的切分原则、数据泄露的常见来源以及标准的三集划分策略。

4.1 时间序列数据切分原则

风控数据天然带有时间戳(交易时间、登录时间、申请时间),样本之间存在时间依赖关系。切分时必须遵循以下三条核心原则:

  1. 时间顺序不可逆:训练集必须全部在验证集和测试集之前,不能使用未来数据预测过去。
  2. 保持时间连续性:切分点应选择在业务低峰期或自然边界(如每日零点、每周一),避免在交易高峰期切分导致数据分布突变。
  3. 样本时间跨度一致:训练集、验证集、测试集应覆盖完整的业务周期(包含工作日、周末、节假日),防止因时间窗口偏差引入评估误差。
关键指标:切分后各集合的时间区间不应重叠,且验证集与测试集的时间跨度至少覆盖 7 天,以捕捉周度周期性模式。

4.2 避免数据泄露

数据泄露是风控建模中最隐蔽的错误来源。当模型在训练阶段接触到了本应属于未来或测试集的信息时,评估指标会虚高,上线后效果断崖式下跌。常见泄露类型如下:

泄露类型 典型场景 后果
时间泄露 使用未来交易数据构造特征(如 T+1 的还款行为) 模型学到未来信息,线上无此特征
特征泄露 使用全局统计量(如全量用户平均逾期率)作为特征 训练集与测试集特征分布完全一致,丧失泛化能力
样本泄露 同一用户的多笔交易被分到训练集和测试集 模型记忆用户行为模式,评估结果偏乐观
标签泄露 使用事后标签(如 30 天后逾期)作为当前特征 特征与目标高度相关,模型无法用于实时决策
避坑提示:在实时风控中,特征工程必须在切分之前完成,且每个时间窗口内的特征只能使用该窗口之前的数据计算。建议使用 groupby('user_id').shift(1) 等操作确保特征不包含未来信息。

4.3 训练/验证/测试集划分策略

基于时间序列的切分策略主要有三种,适用于不同规模和数据频率的风控场景:

  • 固定时间窗口切分:按绝对时间点切分,如 1-60 天训练,61-75 天验证,76-90 天测试。适用于数据量充足、业务稳定的场景。
  • 滚动时间窗口切分:训练集使用固定长度的滑动窗口,验证集和测试集紧随其后。适用于数据量有限、需要多次验证的场景。
  • 扩展时间窗口切分:训练集从起始点不断向后扩展,验证集和测试集保持固定长度。适用于模型需要持续学习新模式的场景。

以下 SVG 展示了三种切分策略的对比结构:

时间序列数据切分策略对比 策略一:固定时间窗口切分 训练集(1-60天) 验证集(61-75天) 测试集(76-90天) 策略二:滚动时间窗口切分 训练窗口1(1-30天) 验证(31-40天) 测试(41-50天) 训练窗口2(11-40天) 验证(41-50天) 测试(51-60天) 滚动方向 策略三:扩展时间窗口切分 训练集(1-40天) 验证(41-50天) 测试(51-60天) 训练集扩展(1-50天) 验证(51-60天) 测试(61-70天)

4.4 标准划分策略与代码实现

推荐使用 扩展时间窗口 + 用户去重 的组合策略,这是实时风控场景中最稳健的切分方式。以下代码展示了基于 Pandas 的完整实现:

import pandas as pd
import numpy as np
from datetime import timedelta

def time_based_split(df, date_col='trans_date', user_col='user_id',
                     train_days=60, val_days=15, test_days=15):
    """
    时间序列风控数据切分函数
    参数:
        df: 原始数据,必须包含日期列和用户ID列
        date_col: 日期列名
        user_col: 用户ID列名
        train_days: 训练集天数
        val_days: 验证集天数
        test_days: 测试集天数
    返回:
        train_df, val_df, test_df
    """
    # 1. 按日期排序
    df = df.sort_values(date_col).reset_index(drop=True)

    # 2. 确定切分时间点
    max_date = df[date_col].max()
    test_start = max_date - timedelta(days=test_days)
    val_start = test_start - timedelta(days=val_days)
    train_start = val_start - timedelta(days=train_days)

    # 3. 按时间窗口切分
    train_df = df[(df[date_col] >= train_start) & (df[date_col] < val_start)].copy()
    val_df = df[(df[date_col] >= val_start) & (df[date_col] < test_start)].copy()
    test_df = df[df[date_col] >= test_start].copy()

    # 4. 用户去重:确保同一用户只出现在一个集合中
    train_users = set(train_df[user_col].unique())
    val_users = set(val_df[user_col].unique())
    test_users = set(test_df[user_col].unique())

    # 移除验证集中出现在训练集的用户
    val_df = val_df[~val_df[user_col].isin(train_users)]
    # 移除测试集中出现在训练集或验证集的用户
    test_df = test_df[~test_df[user_col].isin(train_users | val_users)]

    print(f"训练集样本数: {len(train_df)}, 用户数: {train_df[user_col].nunique()}")
    print(f"验证集样本数: {len(val_df)}, 用户数: {val_df[user_col].nunique()}")
    print(f"测试集样本数: {len(test_df)}, 用户数: {test_df[user_col].nunique()}")

    return train_df, val_df, test_df

# 使用示例
# train, val, test = time_based_split(transaction_data)
实践建议:切分完成后,务必检查各集合的标签分布(正负样本比例)是否与全量数据一致。若差异超过 5%,说明切分点选择不当或存在时间漂移,需要调整窗口大小或重新选择切分日期。

4.5 切分后的验证流程

完成切分后,建议按以下步骤验证切分的合理性:

  • 时间区间检查:确认训练集最大日期 < 验证集最小日期 < 测试集最小日期。
  • 用户重叠检查:计算三个集合之间的用户交集,理想情况下应为空集。
  • 特征分布检查:对关键特征(如交易金额、设备指纹)绘制时间序列分布图,观察是否存在突变点。
  • 标签稳定性检查:计算各集合的逾期率/欺诈率,若波动超过 2%,考虑使用分层切分或调整窗口。

以下 SVG 展示了切分验证的完整流程:

切分验证流程 原始数据 时间窗口切分 用户去重 验证通过 验证失败 调整窗口/重新切分
总结:时间序列切分的核心是“时间不可逆、用户不重叠、特征不泄露”。采用扩展时间窗口 + 用户去重 + 分布验证的三步策略,可以最大程度保证离线评估与线上效果的一致性。