3. 数据基础:坏账率数据集结构、字段含义、数据加载与初步探索
量化回测的可靠性,首先取决于底层数据的质量与结构。坏账率哨兵(Drift Monitoring)系统所依赖的数据集,并非简单的“是否逾期”二值标签,而是一套包含时间切片、资产分组、风险状态与统计口径的标准化宽表。本章将系统拆解该数据集的核心字段、加载方式与探索性分析方法,为后续的漂移检测与回测引擎奠定数据基础。
3.1 数据集结构概览
坏账率监控数据集采用“时间-分组”双索引结构,每一行代表一个特定资产分组在某个观测时间点的坏账表现。整体结构可抽象为三层:时间维度(观测日期)、分组维度(产品线、账龄、风控策略版本等)、指标维度(坏账金额、在贷余额、坏账率等)。
核心设计原则: 每一行必须包含“分子”(坏账金额)与“分母”(在贷余额),而非仅存储计算后的坏账率。保留原始计数可支持后续的置信区间计算与分层聚合。
3.2 核心字段含义与数据类型
以下表格列出了坏账率监控数据集的标准字段定义。实际生产环境中可能根据业务需要增加扩展字段(如渠道来源、授信额度区间等),但以下 8 个字段构成最小必要集合。
| 字段名 | 类型 | 含义 | 示例值 |
|---|---|---|---|
dt |
date | 观测日期(月末快照日) | 2024-03-31 |
product |
string | 产品线标识 | cash_loan |
vintage |
string | 账龄区间(如 M1、M3、M6+) | M3 |
strategy_version |
string | 风控策略版本号 | v2.1 |
balance |
float | 在贷余额(分母) | 12500000.00 |
bad_amt |
float | 坏账金额(分子,逾期 90+ 天未还本金) | 187500.00 |
count_active |
int | 活跃合同数 | 3421 |
bad_rate |
float | 坏账率 = bad_amt / balance | 0.015 |
字段设计要点:
bad_rate 虽然可由 bad_amt / balance 计算得出,但建议在 ETL 阶段预先计算并存储。原因有二:一是避免每次查询重复计算;二是便于后续校验数据一致性(如发现 bad_rate 与分子分母不匹配时可触发告警)。
3.3 数据加载:从 CSV 到 DataFrame
本教程以 CSV 文件作为数据源载体,使用 Pandas 进行加载。以下代码演示了标准加载流程,包含数据类型强制转换与索引设置。
import pandas as pd
import numpy as np
# 加载原始 CSV
df = pd.read_csv(
'bad_rate_monitor_data.csv',
parse_dates=['dt'],
dtype={
'product': 'category',
'vintage': 'category',
'strategy_version': 'category',
'balance': 'float64',
'bad_amt': 'float64',
'count_active': 'int32',
'bad_rate': 'float64'
}
)
# 设置多级索引(时间 + 分组)
df = df.set_index(['dt', 'product', 'vintage', 'strategy_version']).sort_index()
# 验证加载结果
print(f"数据集形状: {df.shape}")
print(f"时间范围: {df.index.get_level_values('dt').min()} ~ {df.index.get_level_values('dt').max()}")
print(f"产品线数量: {df.index.get_level_values('product').nunique()}")
print(f"账龄区间: {sorted(df.index.get_level_values('vintage').unique())}")
常见陷阱: 如果 CSV 中的
bad_rate 字段存在精度截断(如 0.015 被存储为 0.01),务必在加载后重新计算校验:df['bad_rate_check'] = df['bad_amt'] / df['balance'],并检查最大偏差是否超过容忍阈值(如 1e-6)。
3.4 初步探索:数据质量与分布特征
加载完成后,需进行系统性探索,确认数据满足以下三个前提:完整性(无缺失时间切片)、一致性(分子分母逻辑自洽)、稳定性(坏账率未出现极端跳跃)。
3.4.1 时间序列完整性检查
检查每个分组组合是否都有完整的月度观测序列。缺失月份可能导致漂移检测出现假阳性。
# 检查每个分组的时间点数量
time_counts = df.groupby(['product', 'vintage', 'strategy_version']).size()
incomplete_groups = time_counts[time_counts < time_counts.max()]
if len(incomplete_groups) > 0:
print(f"发现 {len(incomplete_groups)} 个分组存在缺失月份:")
print(incomplete_groups.head())
else:
print("所有分组的时间序列完整。")
3.4.2 坏账率分布概览
通过描述性统计与分组聚合,快速掌握坏账率的整体水平与波动范围。
# 全局描述性统计
print("坏账率全局统计:")
print(df['bad_rate'].describe())
# 按产品线 + 账龄分组统计
group_stats = df.groupby(['product', 'vintage'])['bad_rate'].agg(['mean', 'std', 'min', 'max'])
print("\n按产品线 & 账龄分组统计:")
print(group_stats)
3.5 数据质量校验清单
在进入回测环节之前,建议执行以下 5 项校验,确保数据集满足哨兵系统的输入要求。
- 校验 1:负值检测 ——
balance和bad_amt不应出现负值。若存在,需追溯 ETL 逻辑。 - 校验 2:坏账率边界 ——
bad_rate应在 [0, 1] 区间内。超出此范围的数据点应标记为异常。 - 校验 3:分组唯一性 —— 同一
(dt, product, vintage, strategy_version)组合不应出现重复行。 - 校验 4:时间连续性 —— 每个分组的时间序列应等间隔(月度),无跳月或重复月。
- 校验 5:分子分母一致性 —— 重新计算
bad_amt / balance并与存储的bad_rate对比,容忍误差 < 1e-6。
自动化校验脚本建议: 将上述 5 项校验封装为
validate_badrate_dataset(df) -> dict 函数,返回包含 passed: bool 和 errors: list 的字典。在数据加载流水线中作为强制前置步骤,校验不通过则中断后续流程并告警。