1. 缺失值基础概念
1.1 什么是缺失值
缺失值是指在数据记录中,某个或某些字段的值未被观测、未被记录或不存在的情况。在金融风控场景中,缺失值并非简单的“数据空白”,它往往隐含着客户行为、系统处理逻辑或业务规则的重要信号。例如,客户未填写收入字段可能意味着收入不稳定或不愿披露;征信查询记录缺失可能表示客户从未使用过信贷产品。
核心认知:缺失值不是错误,而是一种特殊的数据状态。在金融建模中,缺失值本身可能携带预测信息,不能简单粗暴地删除或填充。
1.2 缺失值在金融特征中的常见表现形式
金融数据源多样(业务库、征信、三方数据、埋点日志),缺失值的表现形式也各不相同。下表总结了四种最常见的表现形式及其典型场景:
| 表现形式 | 数据来源示例 | 典型金融场景 | 检测难度 |
|---|---|---|---|
| NULL | 关系型数据库(MySQL、Oracle) | 客户未申请某产品,该产品字段为 NULL | 低(数据库原生标记) |
| NaN | Python 数值计算、Pandas DataFrame | 计算收入负债比时分母为 0 或空值 | 低(isna() 直接检测) |
| 空字符串 "" | 前端表单提交、ETL 字符型字段 | 客户未填写职业信息,系统存入空字符串 | 中(需区分空串与有效值) |
| 占位符(-9999, -1, 999999) | 历史遗留系统、三方数据供应商 | 征信报告中用 -9999 表示“未知”或“未查询到” | 高(需业务知识识别) |
巡检陷阱:占位符是最容易被忽略的缺失形式。很多风控团队只检查 NULL 和 NaN,却漏掉了用 -9999 或 999999 填充的“伪有效值”,导致模型在推理时学到错误的分布。
1.3 缺失值对模型稳定性的影响
缺失值对金融模型稳定性的影响是多维度的,主要体现在以下三个层面:
- 训练-推理分布偏移:训练集缺失率 5%,线上推理时缺失率飙升到 30%,模型未见过的缺失模式会导致评分剧烈波动。
- 特征重要性失真:树模型对缺失值有默认分支处理,若缺失率随时间变化,特征重要性排序会漂移,模型解释性下降。
- PSI 指标虚高:缺失率变化会直接推高群体稳定性指标(PSI),触发不必要的模型重训警报,增加运维成本。
下图展示了缺失率变化如何通过三个路径影响模型稳定性:
1.4 缺失值对模型性能的量化影响
以下表格展示了在同一个信贷评分模型上,不同缺失率场景下的性能对比(基于某消费金融产品 10 万样本测试集):
| 缺失率场景 | AUC | KS | PSI(相对训练集) | 通过率变化 |
|---|---|---|---|---|
| 训练集基准(缺失率 3%) | 0.782 | 0.45 | — | — |
| 线上低缺失(缺失率 5%) | 0.779 | 0.44 | 0.02 | +0.5% |
| 线上中缺失(缺失率 15%) | 0.751 | 0.38 | 0.08 | +3.2% |
| 线上高缺失(缺失率 30%) | 0.703 | 0.29 | 0.21 | +8.7% |
关键结论:当缺失率超过 10% 时,模型 AUC 下降超过 3 个百分点,PSI 突破 0.1 的预警线,通过率出现异常上升(因为缺失值被模型默认分支处理为“安全”方向)。这正是为什么缺失巡检必须成为模型监控的常态化动作。
1.5 缺失值巡检的起点:识别与分类
在实际工程中,第一步不是填充或删除,而是准确识别缺失值的类型。以下 Python 代码展示了如何对 DataFrame 中的缺失形式进行初步扫描:
import pandas as pd
import numpy as np
def scan_missing_types(df):
"""
扫描 DataFrame 中各列的缺失表现形式
返回每列的 NULL/NaN/空字符串/占位符 统计
"""
report = pd.DataFrame(index=df.columns)
report['null_count'] = df.isnull().sum()
report['nan_count'] = df.apply(lambda x: x.isna().sum() if x.dtype in ['float64','int64'] else 0)
report['empty_str_count'] = df.apply(lambda x: (x == '').sum() if x.dtype == 'object' else 0)
# 常见占位符检测
placeholders = [-9999, -1, 999999, 9999, -999]
report['placeholder_count'] = df.apply(
lambda x: x.isin(placeholders).sum() if x.dtype in ['float64','int64'] else 0
)
report['total_missing'] = report.sum(axis=1)
return report.sort_values('total_missing', ascending=False)
# 使用示例
# missing_report = scan_missing_types(feature_df)
# print(missing_report.head(10))
工程建议:将上述扫描函数封装为每日巡检任务,输出到监控看板。当某列 total_missing 占比超过 5% 或环比增长超过 2 倍时,自动触发告警。