3. 数据基础:坏账率数据集结构、字段含义、数据加载与初步探索

量化回测的可靠性,首先取决于底层数据的质量与结构。坏账率哨兵(Drift Monitoring)系统所依赖的数据集,并非简单的“是否逾期”二值标签,而是一套包含时间切片、资产分组、风险状态与统计口径的标准化宽表。本章将系统拆解该数据集的核心字段、加载方式与探索性分析方法,为后续的漂移检测与回测引擎奠定数据基础。

3.1 数据集结构概览

坏账率监控数据集采用“时间-分组”双索引结构,每一行代表一个特定资产分组在某个观测时间点的坏账表现。整体结构可抽象为三层:时间维度(观测日期)、分组维度(产品线、账龄、风控策略版本等)、指标维度(坏账金额、在贷余额、坏账率等)。

核心设计原则: 每一行必须包含“分子”(坏账金额)与“分母”(在贷余额),而非仅存储计算后的坏账率。保留原始计数可支持后续的置信区间计算与分层聚合。
坏账率数据集三层结构 时间维度(观测日期) dt = 2024-01-31 | 2024-02-29 | 2024-03-31 | ... 分组维度(资产切片) product = 'A' | vintage = 'M3' | strategy_v = 'v2.1' 指标维度(坏账表现) bad_amt | balance | bad_rate | count_active

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)
坏账率分组分布对比(均值 ± 1 标准差) 产品A / M1 均值 1.2% std 0.15% 产品A / M3 均值 3.8% std 0.42% 产品B / M1 均值 0.9% std 0.11% 产品B / M3 均值 2.1% std 0.28% 探索性分析关键发现 • 产品A的坏账率整体高于产品B,M3账龄坏账率约为M1的3倍 • 各分组标准差均在可接受范围(CV < 20%),未发现极端波动

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 的字典。在数据加载流水线中作为强制前置步骤,校验不通过则中断后续流程并告警。

3.6 本章小结