3. 数据准备:回测数据集的结构、时间序列划分、坏账标签的定义与清洗
回测数据的质量直接决定哨兵告警阈值的可信度。本节从数据集结构、时间序列划分方法、坏账标签定义与清洗三个维度,构建标准化的数据准备流程。
3.1 回测数据集的标准结构
一个完整的回测数据集应包含以下四类字段,缺一不可:
| 字段类别 | 典型字段 | 用途 |
|---|---|---|
| 唯一标识 | loan_id, user_id | 关联多张表、去重、追踪个体生命周期 |
| 时间戳 | loan_date, due_date, overdue_date | 时间序列划分、计算账龄、定义观察窗口 |
| 特征变量 | 授信额度、利率、收入、历史逾期次数 | 作为告警阈值的输入维度 |
| 标签变量 | is_bad, default_flag, loss_amount | 坏账判定的最终依据 |
实践建议:回测数据集至少应覆盖 24 个月以上的完整信贷周期,确保包含至少一个完整的经济上行与下行阶段,避免阈值在极端场景下失效。
3.2 时间序列划分方法
量化回测必须严格遵循时间顺序,防止未来信息泄露。标准划分方式如下:
- 训练窗口(Training Window):用于计算历史坏账率分布、拟合告警阈值基线。通常取前 12–18 个月。
- 验证窗口(Validation Window):用于调整阈值参数、避免过拟合。紧接训练窗口之后,长度 3–6 个月。
- 测试窗口(Test Window):用于最终评估告警效果。必须与训练/验证窗口无时间重叠,长度 3–6 个月。
关键规则:三个窗口之间必须按时间顺序严格排列,不可打乱或随机抽样。任何跨窗口的特征计算都只能使用窗口内及之前的数据。
下图展示了回测数据集的时间序列划分结构:
3.3 坏账标签的定义
坏账标签是哨兵告警的核心监督信号。定义不清晰会导致阈值偏移、误报率升高。标准定义包含以下要素:
- 逾期天数阈值:通常设定为 M3+(逾期 90 天以上)或 M6+(逾期 180 天以上)作为坏账标记。
- 观察期长度:从首次逾期到判定为坏账的固定窗口,通常为 90 天或 180 天。
- 核销标记:已进入核销流程的贷款直接标记为坏账,不受观察期限制。
- 展期/重组处理:展期或重组的贷款应单独标记,不可直接归入正常或坏账。
| 标签值 | 定义 | 示例条件 |
|---|---|---|
| 0 | 正常/好账 | 到期后 90 天内全额还款,且未发生展期 |
| 1 | 坏账 | 逾期 ≥ 90 天,或已核销,或确认欺诈 |
| -1 | 不确定/截断 | 观察期不足 90 天,或处于展期/重组中 |
常见陷阱:将观察期不足的样本直接标记为“正常”会严重低估坏账率,导致告警阈值偏松、漏报率上升。建议将此类样本标记为“不确定”并从回测中剔除。
3.4 坏账标签的清洗流程
原始数据中的标签噪声会直接污染告警阈值。清洗流程分为以下四个步骤:
- 去重:按 loan_id + loan_date 去重,保留最新一条记录。
- 时间一致性校验:检查 overdue_date 是否晚于 loan_date,due_date 是否合理。
- 观察期过滤:剔除观察期不足的样本(如距离当前时间不足 90 天)。
- 异常值处理:对 loss_amount 为负值、逾期天数超过贷款期限等异常记录进行标记或剔除。
清洗前后的数据分布对比:
| 指标 | 清洗前 | 清洗后 | 变化说明 |
|---|---|---|---|
| 总样本数 | 100,000 | 85,200 | 剔除 14.8% 无效/不确定样本 |
| 坏账率 | 3.2% | 4.1% | 剔除正常误标后坏账率回归真实水平 |
| 观察期不足占比 | 12.5% | 0% | 全部过滤 |
以下代码展示了坏账标签清洗的核心逻辑:
import pandas as pd
import numpy as np
def clean_bad_label(df, observation_days=90):
"""
坏账标签清洗函数
:param df: 原始回测数据集
:param observation_days: 观察期天数,默认90天
:return: 清洗后的数据集
"""
# 1. 去重
df = df.drop_duplicates(subset=['loan_id', 'loan_date'], keep='last')
# 2. 时间一致性校验
df = df[df['overdue_date'] >= df['loan_date']]
df = df[df['due_date'] > df['loan_date']]
# 3. 观察期过滤
max_date = df['loan_date'].max()
df = df[df['loan_date'] <= max_date - pd.Timedelta(days=observation_days)]
# 4. 异常值处理
df = df[df['loss_amount'] >= 0]
df = df[df['overdue_days'] <= df['loan_term_days']]
return df.reset_index(drop=True)
执行要点:清洗后的数据集应输出一份清洗报告,记录剔除样本的数量、原因分布,便于后续审计与阈值回溯。
下图总结了数据准备阶段的完整流程: