3. 特征缺失与异常排查:从数据层面揪出识别率下降的元凶
模型识别率突然掉了,很多人第一反应是调参数、改网络结构。但我个人的经验是——先别急着动模型,八成是数据出了问题。特征缺失和异常值,就像水管里的沙子,你不清理它,水流再大也白搭。
这一章,我们就来聊聊怎么系统性地排查特征层面的问题。我会把我在项目中踩过的坑、用过的招,都摊开来讲。
3.1 特征缺失率统计:别让“空值”悄悄拖垮模型
特征缺失,说白了就是某个特征在样本里没值。比如用户年龄字段是空的,或者传感器某时刻没采集到数据。模型训练时,如果缺失率过高,模型要么学不到有效信息,要么学到错误规律。
我建议的排查步骤是这样的:
- 全量统计:对训练集、验证集、测试集分别统计每个特征的缺失率。
- 设定阈值:我个人习惯把缺失率超过 50% 的特征标为“高危”,超过 80% 的直接考虑剔除。
- 对比分布:看看缺失率在训练集和线上数据里是否一致。如果不一致,那模型上线后大概率会崩。
核心原则:缺失率不是越低越好,而是训练/线上分布要一致。我曾经遇到一个项目,训练集里“收入”字段缺失率只有 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 特征工程链路回溯:从源头定位问题
有时候,特征缺失和异常不是数据采集的问题,而是特征工程链路本身出了问题。比如:
- 某个特征依赖上游表,上游表延迟更新了
- 特征计算逻辑改了,但没同步到所有环境
- 数据分桶的边界变了,导致分布偏移
我画了一张图,帮你理清特征工程链路的排查思路:
链路回溯的具体做法,我总结为三步:
- 从后往前查:先看模型输入的特征值,如果发现异常,回溯到特征存储层,看存储的值对不对。
- 对比不同环境:把线上特征和离线特征做对比。我遇到过线上特征计算代码少了一个归一化步骤,导致分布完全偏移。
- 检查时间戳:很多特征缺失是因为时间窗口没对齐。比如模型用的是 T 时刻的特征,但特征表只更新到 T-1 时刻。
我曾经踩过的坑:有一次模型识别率从 92% 掉到 85%,排查了三天,最后发现是特征工程代码里一个 groupby 的 key 写错了,导致所有用户特征都串到了同一个聚合值上。所以,代码 review 和版本对比一定要做。
3.4 综合排查清单:照着做就行
为了方便你落地,我把这一章的内容整理成一个排查清单。下次遇到识别率下降,直接按这个顺序查:
| 步骤 | 检查项 | 操作 | 预期结果 |
|---|---|---|---|
| 1 | 特征缺失率统计 | 对训练/验证/测试集分别统计 | 缺失率分布一致,且低于阈值 |
| 2 | 异常值检测 | 使用 IQR 或 3-sigma 检测 | 异常值比例 < 1% |
| 3 | 特征分布对比 | 对比线上和离线特征的分布 | 分布无明显偏移 |
| 4 | 特征计算逻辑 | 检查代码版本和计算逻辑 | 线上/离线逻辑一致 |
| 5 | 数据源时效性 | 检查上游表更新时间 | 数据源及时更新 |
我的建议:把这个清单做成自动化脚本,每天跑一次。一旦发现异常,立刻告警。别等到模型掉点了才去查,那时候已经晚了。