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),触发不必要的模型重训警报,增加运维成本。

下图展示了缺失率变化如何通过三个路径影响模型稳定性:

缺失率变化对模型稳定性的影响路径 缺失率变化 训练-推理分布偏移 缺失率 5% → 30% 特征重要性失真 树模型默认分支漂移 PSI 指标虚高 触发不必要重训警报 模型评分波动 / 决策不稳定 AUC 下降、KS 震荡、通过率异常 缺失巡检是模型监控的第一道防线,必须纳入日常监控体系

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 倍时,自动触发告警。