4. 数据清洗:缺失值处理、异常值检测、数据类型转换与标准化
原始回测数据通常包含噪声、缺失字段、异常跳变以及不一致的格式。在将数据送入漂移哨兵(Drift Monitoring)系统之前,必须执行标准化的清洗流程。本章将围绕四个核心环节展开:缺失值处理、异常值检测、数据类型转换与标准化。
核心原则:清洗策略必须可复现、可审计。每次清洗操作都应记录转换日志,确保回测结果可追溯。
4.1 缺失值处理
缺失值在回测数据中常见于逾期天数、还款金额、用户信用分等字段。处理不当会导致统计偏差或模型训练失败。
| 缺失类型 | 常见字段 | 推荐处理方式 | 适用场景 |
|---|---|---|---|
| 完全随机缺失 | 用户收入、职业 | 删除(行删除) | 缺失比例 < 5% |
| 随机缺失 | 逾期天数、还款金额 | 中位数 / 均值填充 | 连续型变量,分布较对称 |
| 非随机缺失 | 信用评分、坏账标签 | 模型预测填充 / 标记缺失 | 缺失与目标变量相关 |
| 时间序列缺失 | 日收益率、交易量 | 前向填充 / 插值 | 时间依赖性强 |
实践建议:对于坏账率哨兵系统,坏账标签字段严禁随意填充。若缺失比例超过 10%,应单独标记为“未知”并评估其对回测结果的影响。
import pandas as pd
import numpy as np
# 示例:缺失值处理
df = pd.read_csv('loan_data.csv')
# 1. 删除缺失比例过高的列
df.dropna(thresh=len(df) * 0.8, axis=1, inplace=True)
# 2. 连续变量用中位数填充
df['overdue_days'].fillna(df['overdue_days'].median(), inplace=True)
# 3. 分类变量用众数填充
df['employment_status'].fillna(df['employment_status'].mode()[0], inplace=True)
# 4. 时间序列前向填充
df['daily_return'].fillna(method='ffill', inplace=True)
print(f"清洗后缺失值总数: {df.isnull().sum().sum()}")
4.2 异常值检测
异常值会严重扭曲回测的坏账率统计,导致哨兵系统产生误报。常用的检测方法包括统计阈值法、箱线图法和孤立森林。
- Z-Score 法:适用于近似正态分布的数据,通常取阈值 3。
- IQR 法:适用于偏态分布,定义异常为超出 Q1 - 1.5×IQR 或 Q3 + 1.5×IQR 的值。
- 孤立森林:适用于高维数据,无需假设分布。
from scipy import stats
import numpy as np
# Z-Score 异常检测
z_scores = np.abs(stats.zscore(df['loan_amount']))
outliers_z = df[z_scores > 3]
print(f"Z-Score 检测异常数: {len(outliers_z)}")
# IQR 异常检测
Q1 = df['loan_amount'].quantile(0.25)
Q3 = df['loan_amount'].quantile(0.75)
IQR = Q3 - Q1
outliers_iqr = df[(df['loan_amount'] < Q1 - 1.5 * IQR) | (df['loan_amount'] > Q3 + 1.5 * IQR)]
print(f"IQR 检测异常数: {len(outliers_iqr)}")
注意:不要机械地删除所有异常值。在回测坏账率场景中,某些“异常”可能代表真实的极端风险事件(如突发性大规模逾期)。建议先标记,再结合业务逻辑判断是否剔除。
4.3 数据类型转换
回测数据常因来源不同导致类型混乱:日期存为字符串、金额存为对象、分类变量存为整数。类型转换是后续计算和建模的基础。
| 原始类型 | 目标类型 | 转换方法 | 常见问题 |
|---|---|---|---|
| 字符串日期 | datetime64 | pd.to_datetime() | 格式不一致、时区缺失 |
| 对象型金额 | float64 | pd.to_numeric() | 含逗号、货币符号 |
| 整数分类 | category | astype('category') | 编码顺序错误 |
| 布尔型 | int (0/1) | astype(int) | 缺失值转为 -1 |
# 数据类型转换示例
df['loan_date'] = pd.to_datetime(df['loan_date'], errors='coerce')
df['loan_amount'] = pd.to_numeric(df['loan_amount'].str.replace(',', ''), errors='coerce')
df['credit_rating'] = df['credit_rating'].astype('category')
df['is_default'] = df['is_default'].astype(int)
print(df.dtypes)
4.4 标准化与归一化
标准化(Standardization)和归一化(Normalization)用于消除量纲影响,使不同尺度的特征在哨兵模型中具有可比性。坏账率回测中,标准化更常用,因为它对异常值不敏感。
- Z-Score 标准化:\( x' = \frac{x - \mu}{\sigma} \),适用于数据近似正态分布。
- Min-Max 归一化:\( x' = \frac{x - min}{max - min} \),将数据缩放到 [0, 1] 区间。
- Robust 标准化:使用中位数和 IQR,对异常值鲁棒。
from sklearn.preprocessing import StandardScaler, MinMaxScaler, RobustScaler
# Z-Score 标准化
scaler_std = StandardScaler()
df['amount_std'] = scaler_std.fit_transform(df[['loan_amount']])
# Min-Max 归一化
scaler_minmax = MinMaxScaler()
df['amount_norm'] = scaler_minmax.fit_transform(df[['loan_amount']])
# Robust 标准化
scaler_robust = RobustScaler()
df['amount_robust'] = scaler_robust.fit_transform(df[['loan_amount']])
print("标准化后统计量:")
print(df[['amount_std', 'amount_norm', 'amount_robust']].describe())
选择建议:如果后续使用线性模型或距离度量(如 KNN、K-Means),优先使用标准化;如果使用树模型(如 XGBoost、LightGBM),通常不需要标准化,但建议对数值型特征做分箱或排序转换。
4.5 清洗流程总结
完整的数据清洗流水线应按照以下顺序执行,以确保每一步的输出质量可控:
- 加载与预览:检查数据形状、字段列表、缺失概况。
- 缺失值处理:按字段类型和缺失机制选择填充或删除策略。
- 异常值检测:至少使用两种方法交叉验证,标记而非直接删除。
- 数据类型转换:统一日期、数值、分类字段的 dtype。
- 标准化/归一化:根据模型需求选择合适方法,保存 scaler 对象用于回测。
- 质量验证:检查清洗后数据的统计分布、相关性、缺失归零。
重要提醒:在回测坏账率哨兵系统中,清洗参数(如填充值、异常阈值、scaler 参数)必须与训练集保持一致。严禁在回测阶段使用未来数据计算填充值或标准化参数,否则将导致前视偏差,使回测结果失真。