4. 数据准备:坏账率数据来源与预处理

回测系统的可靠性高度依赖输入数据的质量。本章将系统讲解坏账率回测所需的数据来源、字段定义以及清洗预处理流程,确保后续策略计算建立在干净、一致的数据基础之上。

4.1 数据来源:CSV 与数据库

实际项目中,坏账数据通常来自两个渠道:离线导出的 CSV 文件或在线业务数据库。两种来源各有适用场景,下表对比了核心差异:

对比维度 CSV 文件 关系数据库
适用阶段 原型验证、本地回测 生产环境、持续监控
数据量级 百万行以内 千万至亿级
读取方式 pd.read_csv() pd.read_sql() + SQL 查询
版本管理 Git 可追踪 需快照或 CDC
实时性 离线快照 准实时
推荐做法:课程案例统一使用 CSV 作为数据源,便于复现和版本控制。生产环境迁移时,只需将 pd.read_csv() 替换为数据库连接查询即可。

4.2 数据字段定义

回测坏账率告警系统至少需要三个核心字段:时间戳、逾期天数、坏账标记。下表给出完整字段规范:

字段名 类型 示例值 说明
date datetime64 2024-01-15 观测日期,粒度通常为日
loan_id string LN20240115001 每笔借款的唯一标识
due_date datetime64 2024-02-14 应还款日
overdue_days int 30 截至观测日已逾期天数
is_bad int (0/1) 1 坏账标记:1=坏账,0=正常
principal float 10000.00 借款本金(可选,用于计算损失率)
关键约定:坏账标记 is_bad 的判定标准必须与业务规则一致。本课程统一采用“逾期天数 ≥ 90 天”作为坏账阈值。若业务规则不同,请在预处理阶段调整。

4.3 数据清洗与预处理流程

原始数据通常包含缺失值、重复记录、异常日期等问题。以下流程图为清洗预处理的完整步骤:

数据清洗与预处理流程 原始 CSV / 数据库 缺失值处理(删除/填充) 重复记录去重 日期格式统一 & 排序 坏账标记生成(逾期≥90天) 清洗后数据(DataFrame)

4.4 核心清洗步骤详解

4.4.1 缺失值处理

常见缺失场景及处理策略:

  • 日期缺失:直接删除该行,无法回填。
  • 逾期天数缺失:若 due_date 存在,可计算 (观测日 - 应还款日).days 填充。
  • 坏账标记缺失:根据逾期天数重新生成。
# 缺失值处理示例
import pandas as pd

df = pd.read_csv('loan_data.csv')
# 删除日期缺失的行
df.dropna(subset=['date', 'due_date'], inplace=True)
# 根据 due_date 计算 overdue_days
df['overdue_days'] = (pd.to_datetime(df['date']) - pd.to_datetime(df['due_date'])).dt.days
df['overdue_days'] = df['overdue_days'].clip(lower=0)  # 未到期设为0

4.4.2 重复记录去重

同一笔借款在同一天不应出现两条记录。去重逻辑:

# 按 loan_id + date 去重,保留第一条
df.drop_duplicates(subset=['loan_id', 'date'], keep='first', inplace=True)

4.4.3 坏账标记生成

根据业务规则,逾期天数 ≥ 90 天标记为坏账:

# 生成坏账标记
df['is_bad'] = (df['overdue_days'] >= 90).astype(int)
验证提示:生成后建议检查 df['is_bad'].value_counts(),确保坏账比例在合理范围(通常 1%~10%)。若比例异常,需回溯逾期天数计算逻辑。

4.5 数据字段与清洗流程总览

以下 SVG 图展示数据从原始字段到清洗后字段的映射关系:

原始字段 → 清洗后字段映射 原始字段 date (string) loan_id (string) due_date (string) overdue_days (int?) is_bad (int?) 清洗操作 类型转换 缺失填充 去重 规则生成 清洗后字段 date (datetime) loan_id (string) due_date (datetime) overdue_days (int) is_bad (int)

4.6 完整预处理代码模板

将上述步骤整合为一个可复用的预处理函数:

def preprocess_loan_data(filepath: str) -> pd.DataFrame:
    """
    加载并清洗借款数据,返回可直接用于回测的 DataFrame。
    
    Parameters
    ----------
    filepath : str
        CSV 文件路径
    
    Returns
    -------
    pd.DataFrame
        包含 date, loan_id, due_date, overdue_days, is_bad 的清洗后数据
    """
    df = pd.read_csv(filepath)
    
    # 1. 删除关键字段缺失的行
    df.dropna(subset=['date', 'due_date'], inplace=True)
    
    # 2. 日期类型转换
    df['date'] = pd.to_datetime(df['date'])
    df['due_date'] = pd.to_datetime(df['due_date'])
    
    # 3. 计算逾期天数
    df['overdue_days'] = (df['date'] - df['due_date']).dt.days
    df['overdue_days'] = df['overdue_days'].clip(lower=0)
    
    # 4. 去重
    df.drop_duplicates(subset=['loan_id', 'date'], keep='first', inplace=True)
    
    # 5. 生成坏账标记
    df['is_bad'] = (df['overdue_days'] >= 90).astype(int)
    
    # 6. 按日期排序
    df.sort_values(['date', 'loan_id'], inplace=True)
    df.reset_index(drop=True, inplace=True)
    
    return df
使用建议:将上述函数保存为 data_prep.py,后续回测脚本直接 from data_prep import preprocess_loan_data 调用,保持代码模块化。

4.7 数据质量检查清单

预处理完成后,建议逐项检查以下指标,确保数据可用:

  • 时间范围:df['date'].min() 至 df['date'].max() 是否覆盖回测区间。
  • 无空值:df.isnull().sum().sum() 应为 0。
  • 无重复:df.duplicated(subset=['loan_id','date']).sum() 应为 0。
  • 坏账比例:df['is_bad'].mean() 应在预期范围。
  • 逾期天数分布:检查 df['overdue_days'].describe(),最大值不应异常。