4. 训练集切分原理:时间序列数据切分原则、避免数据泄露、训练/验证/测试集划分策略
在实时风控建模中,数据切分不是简单的随机抽样。由于欺诈行为具有时间演化特性,错误的切分方式会导致模型在线上表现远低于离线评估结果。本章聚焦时间序列数据特有的切分原则、数据泄露的常见来源以及标准的三集划分策略。
4.1 时间序列数据切分原则
风控数据天然带有时间戳(交易时间、登录时间、申请时间),样本之间存在时间依赖关系。切分时必须遵循以下三条核心原则:
- 时间顺序不可逆:训练集必须全部在验证集和测试集之前,不能使用未来数据预测过去。
- 保持时间连续性:切分点应选择在业务低峰期或自然边界(如每日零点、每周一),避免在交易高峰期切分导致数据分布突变。
- 样本时间跨度一致:训练集、验证集、测试集应覆盖完整的业务周期(包含工作日、周末、节假日),防止因时间窗口偏差引入评估误差。
关键指标:切分后各集合的时间区间不应重叠,且验证集与测试集的时间跨度至少覆盖 7 天,以捕捉周度周期性模式。
4.2 避免数据泄露
数据泄露是风控建模中最隐蔽的错误来源。当模型在训练阶段接触到了本应属于未来或测试集的信息时,评估指标会虚高,上线后效果断崖式下跌。常见泄露类型如下:
| 泄露类型 | 典型场景 | 后果 |
|---|---|---|
| 时间泄露 | 使用未来交易数据构造特征(如 T+1 的还款行为) | 模型学到未来信息,线上无此特征 |
| 特征泄露 | 使用全局统计量(如全量用户平均逾期率)作为特征 | 训练集与测试集特征分布完全一致,丧失泛化能力 |
| 样本泄露 | 同一用户的多笔交易被分到训练集和测试集 | 模型记忆用户行为模式,评估结果偏乐观 |
| 标签泄露 | 使用事后标签(如 30 天后逾期)作为当前特征 | 特征与目标高度相关,模型无法用于实时决策 |
避坑提示:在实时风控中,特征工程必须在切分之前完成,且每个时间窗口内的特征只能使用该窗口之前的数据计算。建议使用
groupby('user_id').shift(1) 等操作确保特征不包含未来信息。
4.3 训练/验证/测试集划分策略
基于时间序列的切分策略主要有三种,适用于不同规模和数据频率的风控场景:
- 固定时间窗口切分:按绝对时间点切分,如 1-60 天训练,61-75 天验证,76-90 天测试。适用于数据量充足、业务稳定的场景。
- 滚动时间窗口切分:训练集使用固定长度的滑动窗口,验证集和测试集紧随其后。适用于数据量有限、需要多次验证的场景。
- 扩展时间窗口切分:训练集从起始点不断向后扩展,验证集和测试集保持固定长度。适用于模型需要持续学习新模式的场景。
以下 SVG 展示了三种切分策略的对比结构:
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 展示了切分验证的完整流程:
总结:时间序列切分的核心是“时间不可逆、用户不重叠、特征不泄露”。采用扩展时间窗口 + 用户去重 + 分布验证的三步策略,可以最大程度保证离线评估与线上效果的一致性。