4. 数据清洗:异常值检测与处理
做特征工程,数据清洗是绕不开的一步。而异常值处理,又是数据清洗里最考验经验的部分。为什么?因为异常值并不总是“错误”——它可能是欺诈信号,也可能是录入失误。你怎么判断?这章我就把实战中常用的三个方法——IQR、Z-score、箱线图——摊开来聊聊,再讲怎么处理。
核心原则:异常值 ≠ 坏数据。先判断产生原因,再选处理方法。
4.1 异常值识别方法
4.1.1 IQR(四分位距法)
IQR是箱线图的基础。说白了,就是取数据的第25百分位数(Q1)和第75百分位数(Q3),两者之差叫IQR。超出1.5倍IQR范围的点,通常视为异常。
计算公式:
- 下限 = Q1 - 1.5 × IQR
- 上限 = Q3 + 1.5 × IQR
- 超出上下限的数据点为异常值
我建议在特征分布偏态明显时用IQR。比如信用评分中“收入”字段,大部分人收入在5万到50万之间,但偶尔会有几百万甚至上千万的收入——这种用IQR抓出来很稳。
import numpy as np
def detect_outliers_iqr(data):
Q1 = np.percentile(data, 25)
Q3 = np.percentile(data, 75)
IQR = Q3 - Q1
lower = Q1 - 1.5 * IQR
upper = Q3 + 1.5 * IQR
return (data < lower) | (data > upper)
# 示例
scores = [320, 450, 520, 600, 650, 700, 750, 800, 850, 900, 9999]
outliers = detect_outliers_iqr(scores)
print(f"异常索引: {np.where(outliers)[0]}")
我的经验:我曾用IQR做信用卡交易金额的异常检测,发现一笔100万的交易。进一步核查发现是客户误操作——截尾后排除,模型效果提升了2.3%的AUC。
4.1.2 Z-score(标准分数法)
Z-score假设数据近似正态分布。计算每个点与均值的距离,除以标准差。通常 |Z| > 3 视为异常。
公式: Z = (x - μ) / σ
我一般在对数变换后的特征上使用Z-score。比如信用评分中的“负债率”经过log后接近正态分布,这时候Z-score特别适合。
from scipy import stats
def detect_outliers_zscore(data, threshold=3):
z = np.abs(stats.zscore(data))
return z > threshold
outliers_z = detect_outliers_zscore(scores)
print(f"Z-score异常索引: {np.where(outliers_z)[0]}")
注意:Z-score对异常值本身敏感(因为均值和标准差会被异常点拉偏)。建议先做一次IQR粗筛,再用Z-score精筛。
4.1.3 箱线图(Box Plot)
箱线图是IQR的可视化表达。你一眼就能看到:上边缘、下边缘、中位数、以及“飞出去”的点。它在EDA阶段特别好用——我习惯把每个特征的箱线图拼在一张大图上,快速定位哪个字段异常最多。
import matplotlib.pyplot as plt
import seaborn as sns
# 绘制单个箱线图
plt.figure(figsize=(8, 4))
sns.boxplot(x=scores)
plt.title('信用分箱线图 - 异常值一目了然')
plt.show()
嗯,有一点要注意:箱线图默认的1.5倍IQR阈值,对极稀疏分布不太适用。如果数据只有几十个点,IQR可能很窄,把正常点错判为异常。你想想看,一个只有20条记录的收入字段,用1.5倍IQR会把几乎所有值都标成异常——这就尴尬了。
| 方法 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| IQR | 偏态分布、非正态数据 | 稳健,不受极值影响 | 对稀疏数据阈值过严 |
| Z-score | 近似正态分布 | 统计意义清晰 | 对异常值敏感(均值标准差被污染) |
| 箱线图 | EDA快速探查 | 可视化直观,可批量对比 | 主观判断,无法自动标记 |
4.2 异常值处理方法
识别出来之后怎么处理?三种主流策略:截尾、删除、变换。
- 截尾(Winsorization / Capping):将异常值替换为上下限临界值。比如把超出上限的值一律设为Q3 + 1.5×IQR。适合数据量少、删除会损失信息的场景。我曾经在一个只有2000条样本的信用评分项目里,用截尾保留了所有样本,模型稳定性反而比删除更好。
- 删除(Deletion):直接移除异常值所在行。适合异常值比例低(<5%)、且确信是录入错误或噪声的场景。注意:删除后要重新检查分布是否变正常。
- 变换(Transformation):对特征做对数、平方根、Box-Cox等变换,使异常值的影响被压缩。适合偏态严重、且异常值其实是业务合理存在的场景(比如高收入、大额交易)。
# 截尾示例
def capping(data, lower, upper):
data_clipped = np.clip(data, lower, upper)
return data_clipped
# 删除示例
def drop_outliers(data, outlier_mask):
return data[~outlier_mask]
# 对数变换
def log_transform(data):
return np.log1p(data) # log(1+x) 避免0值问题
我以前踩过的一个坑:在一个信用评分项目中,我直接用Z-score把所有|Z|>3的样本删掉了,结果发现删掉的全是逾期客户(违约率最高的人群)。模型后续预测违约率直接崩了。后来才意识到,这些“异常值”恰恰是业务关键信号。所以:永远先结合业务判断,不要机械地执行统计规则。
4.3 异常值处理策略总结
| 方法 | 适用条件 | 优点 | 风险 |
|---|---|---|---|
| 截尾 | 样本量小,异常比例高 | 保留样本量,不改变分布形状 | 可能掩盖真实异常 |
| 删除 | 确信异常且比例低 | 干净,无干扰 | 损失样本,可能丢失信息 |
| 变换 | 偏态分布,异常为业务常态 | 压缩异常影响,保留自然特征 | 解释性变差,需要后处理 |
其实异常值检测没有银弹。我个人习惯的做法是:先画箱线图看全局,再用IQR和Z-score交叉验证,最后根据业务知识人工挑出“真异常”。你想想看,信用评分里,黑名单客户的得分往往极低——但你敢直接把它当异常删掉吗?不敢吧。
最后说一句:做完异常值处理后,一定要做个前后分布对比图(如KDE密度图)。如果处理后分布出现明显断层或变形,说明你处理过头了。调低阈值,重来。