3. 特征缺失与异常排查:从数据层面揪出识别率下降的元凶

模型识别率突然掉了,很多人第一反应是调参数、改网络结构。但我个人的经验是——先别急着动模型,八成是数据出了问题。特征缺失和异常值,就像水管里的沙子,你不清理它,水流再大也白搭。

这一章,我们就来聊聊怎么系统性地排查特征层面的问题。我会把我在项目中踩过的坑、用过的招,都摊开来讲。

3.1 特征缺失率统计:别让“空值”悄悄拖垮模型

特征缺失,说白了就是某个特征在样本里没值。比如用户年龄字段是空的,或者传感器某时刻没采集到数据。模型训练时,如果缺失率过高,模型要么学不到有效信息,要么学到错误规律。

我建议的排查步骤是这样的:

  1. 全量统计:对训练集、验证集、测试集分别统计每个特征的缺失率。
  2. 设定阈值:我个人习惯把缺失率超过 50% 的特征标为“高危”,超过 80% 的直接考虑剔除。
  3. 对比分布:看看缺失率在训练集和线上数据里是否一致。如果不一致,那模型上线后大概率会崩。

核心原则:缺失率不是越低越好,而是训练/线上分布要一致。我曾经遇到一个项目,训练集里“收入”字段缺失率只有 5%,但线上缺失率飙到 40%,模型直接废了。

这里给一段我常用的统计代码,你可以直接拿去用:

import pandas as pd

def missing_rate_report(df, name="dataset"):
    """
    统计每个特征的缺失率,并排序输出
    """
    missing = df.isnull().sum()
    missing_rate = (missing / len(df)) * 100
    report = pd.DataFrame({
        "feature": missing.index,
        "missing_count": missing.values,
        "missing_rate(%)": missing_rate.values
    })
    report = report.sort_values("missing_rate(%)", ascending=False)
    print(f"=== {name} 特征缺失率报告 ===")
    print(report[report["missing_rate(%)"] > 0])
    return report

# 使用示例
train_report = missing_rate_report(train_df, "train")
test_report = missing_rate_report(test_df, "test")

小技巧:如果某个特征缺失率很高但业务上很重要,别急着删。试试用均值/中位数填充,或者用模型预测填充。但记住——填充后一定要在验证集上评估效果,别盲目操作。

3.2 异常值检测:3-sigma 与 IQR 的实战对比

异常值,就是那些明显偏离正常范围的数据点。比如用户年龄填了 200 岁,或者某个传感器读数突然跳变 100 倍。这些异常值会严重干扰模型训练,让模型去拟合那些“不存在的规律”。

常用的检测方法有两种:3-sigma 法则和IQR(四分位距)法。我分别说说它们的适用场景。

3.2.1 3-sigma 法则:适合正态分布的数据

3-sigma 的原理很简单:假设数据服从正态分布,那么 99.7% 的数据会落在均值 ±3 个标准差范围内。超出这个范围的,就视为异常。

代码实现:

import numpy as np

def detect_outliers_3sigma(data, feature):
    """
    使用 3-sigma 法则检测异常值
    返回异常值的索引
    """
    mean = np.mean(data[feature])
    std = np.std(data[feature])
    lower_bound = mean - 3 * std
    upper_bound = mean + 3 * std
    
    outliers = data[(data[feature] < lower_bound) | (data[feature] > upper_bound)]
    print(f"{feature}: 均值={mean:.2f}, 标准差={std:.2f}")
    print(f"下界={lower_bound:.2f}, 上界={upper_bound:.2f}")
    print(f"检测到 {len(outliers)} 个异常值")
    return outliers.index.tolist()

注意:3-sigma 对异常值本身很敏感。如果数据里已经有大量异常值,均值和标准差会被“带偏”,导致检测失效。我遇到过这种情况——第一次检测只发现 10 个异常,剔除后再检测,又发现 50 个。所以建议迭代检测,直到稳定。

3.2.2 IQR 法:对非正态分布更鲁棒

IQR 不依赖数据分布形态,它基于四分位数。具体来说:

  • 计算 Q1(25% 分位数)和 Q3(75% 分位数)
  • IQR = Q3 - Q1
  • 异常值定义为:小于 Q1 - 1.5*IQR 或大于 Q3 + 1.5*IQR

代码实现:

def detect_outliers_iqr(data, feature):
    """
    使用 IQR 法检测异常值
    """
    Q1 = data[feature].quantile(0.25)
    Q3 = data[feature].quantile(0.75)
    IQR = Q3 - Q1
    lower_bound = Q1 - 1.5 * IQR
    upper_bound = Q3 + 1.5 * IQR
    
    outliers = data[(data[feature] < lower_bound) | (data[feature] > upper_bound)]
    print(f"{feature}: Q1={Q1:.2f}, Q3={Q3:.2f}, IQR={IQR:.2f}")
    print(f"下界={lower_bound:.2f}, 上界={upper_bound:.2f}")
    print(f"检测到 {len(outliers)} 个异常值")
    return outliers.index.tolist()

我的经验:在工业场景中,大部分特征都不服从严格的正态分布。所以我个人更常用 IQR 法。但如果你确定数据是正态的(比如某些标准化后的特征),3-sigma 会更敏感一些。

3.2.3 两种方法的对比

对比维度 3-sigma 法则 IQR 法
分布假设 要求正态分布 无分布假设
对异常值敏感度 高(异常值会拉偏均值和标准差) 低(基于分位数,更鲁棒)
适用场景 标准化后的特征、误差项 大多数实际业务特征
阈值系数 固定 3 倍标准差 通常 1.5 倍 IQR(可调整)
计算效率 高 高

3.3 特征工程链路回溯:从源头定位问题

有时候,特征缺失和异常不是数据采集的问题,而是特征工程链路本身出了问题。比如:

  • 某个特征依赖上游表,上游表延迟更新了
  • 特征计算逻辑改了,但没同步到所有环境
  • 数据分桶的边界变了,导致分布偏移

我画了一张图,帮你理清特征工程链路的排查思路:

特征工程链路回溯流程图 原始数据采集 特征计算与加工 特征存储与版本管理 模型训练/推理 检查:数据源是否更新? 检查:计算逻辑是否一致? 检查:版本是否对齐? 每个环节都可能引入缺失或异常,需要逐级回溯

链路回溯的具体做法,我总结为三步:

  1. 从后往前查:先看模型输入的特征值,如果发现异常,回溯到特征存储层,看存储的值对不对。
  2. 对比不同环境:把线上特征和离线特征做对比。我遇到过线上特征计算代码少了一个归一化步骤,导致分布完全偏移。
  3. 检查时间戳:很多特征缺失是因为时间窗口没对齐。比如模型用的是 T 时刻的特征,但特征表只更新到 T-1 时刻。

我曾经踩过的坑:有一次模型识别率从 92% 掉到 85%,排查了三天,最后发现是特征工程代码里一个 groupby 的 key 写错了,导致所有用户特征都串到了同一个聚合值上。所以,代码 review 和版本对比一定要做。

3.4 综合排查清单:照着做就行

为了方便你落地,我把这一章的内容整理成一个排查清单。下次遇到识别率下降,直接按这个顺序查:

步骤 检查项 操作 预期结果
1 特征缺失率统计 对训练/验证/测试集分别统计 缺失率分布一致,且低于阈值
2 异常值检测 使用 IQR 或 3-sigma 检测 异常值比例 < 1%
3 特征分布对比 对比线上和离线特征的分布 分布无明显偏移
4 特征计算逻辑 检查代码版本和计算逻辑 线上/离线逻辑一致
5 数据源时效性 检查上游表更新时间 数据源及时更新

我的建议:把这个清单做成自动化脚本,每天跑一次。一旦发现异常,立刻告警。别等到模型掉点了才去查,那时候已经晚了。