3. 数据准备:回测数据集的结构、时间序列划分、坏账标签的定义与清洗

回测数据的质量直接决定哨兵告警阈值的可信度。本节从数据集结构、时间序列划分方法、坏账标签定义与清洗三个维度,构建标准化的数据准备流程。

3.1 回测数据集的标准结构

一个完整的回测数据集应包含以下四类字段,缺一不可:

字段类别 典型字段 用途
唯一标识 loan_id, user_id 关联多张表、去重、追踪个体生命周期
时间戳 loan_date, due_date, overdue_date 时间序列划分、计算账龄、定义观察窗口
特征变量 授信额度、利率、收入、历史逾期次数 作为告警阈值的输入维度
标签变量 is_bad, default_flag, loss_amount 坏账判定的最终依据
实践建议:回测数据集至少应覆盖 24 个月以上的完整信贷周期,确保包含至少一个完整的经济上行与下行阶段,避免阈值在极端场景下失效。

3.2 时间序列划分方法

量化回测必须严格遵循时间顺序,防止未来信息泄露。标准划分方式如下:

  1. 训练窗口(Training Window):用于计算历史坏账率分布、拟合告警阈值基线。通常取前 12–18 个月。
  2. 验证窗口(Validation Window):用于调整阈值参数、避免过拟合。紧接训练窗口之后,长度 3–6 个月。
  3. 测试窗口(Test Window):用于最终评估告警效果。必须与训练/验证窗口无时间重叠,长度 3–6 个月。
关键规则:三个窗口之间必须按时间顺序严格排列,不可打乱或随机抽样。任何跨窗口的特征计算都只能使用窗口内及之前的数据。

下图展示了回测数据集的时间序列划分结构:

回测数据集时间序列划分结构 训练窗口 12–18 个月 验证窗口 3–6 个月 测试窗口 3–6 个月 T₀ T₁ T₂ T₃ ⚠ 时间序列划分注意事项 窗口之间不可重叠;特征计算仅使用窗口内及历史数据;测试窗口必须为最新时间段

3.3 坏账标签的定义

坏账标签是哨兵告警的核心监督信号。定义不清晰会导致阈值偏移、误报率升高。标准定义包含以下要素:

  • 逾期天数阈值:通常设定为 M3+(逾期 90 天以上)或 M6+(逾期 180 天以上)作为坏账标记。
  • 观察期长度:从首次逾期到判定为坏账的固定窗口,通常为 90 天或 180 天。
  • 核销标记:已进入核销流程的贷款直接标记为坏账,不受观察期限制。
  • 展期/重组处理:展期或重组的贷款应单独标记,不可直接归入正常或坏账。
标签值 定义 示例条件
0 正常/好账 到期后 90 天内全额还款,且未发生展期
1 坏账 逾期 ≥ 90 天,或已核销,或确认欺诈
-1 不确定/截断 观察期不足 90 天,或处于展期/重组中
常见陷阱:将观察期不足的样本直接标记为“正常”会严重低估坏账率,导致告警阈值偏松、漏报率上升。建议将此类样本标记为“不确定”并从回测中剔除。

3.4 坏账标签的清洗流程

原始数据中的标签噪声会直接污染告警阈值。清洗流程分为以下四个步骤:

  1. 去重:按 loan_id + loan_date 去重,保留最新一条记录。
  2. 时间一致性校验:检查 overdue_date 是否晚于 loan_date,due_date 是否合理。
  3. 观察期过滤:剔除观察期不足的样本(如距离当前时间不足 90 天)。
  4. 异常值处理:对 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)
执行要点:清洗后的数据集应输出一份清洗报告,记录剔除样本的数量、原因分布,便于后续审计与阈值回溯。

下图总结了数据准备阶段的完整流程:

数据准备完整流程 原始数据采集 时间序列划分 坏账标签定义 清洗 ✅ 标准化回测数据集 输出包含:去重后样本、时间窗口标签、坏账标记、清洗报告