4. 异常值处理:别让“离群点”毁了你的模型
数据清洗做到这一步,前面缺失值、重复值都处理完了。但还有个“隐形杀手”——异常值。说白了,就是那些跟大多数数据画风完全不一样的点。比如全班考80分,突然冒出个0分或者满分,这俩就得打个问号。
我个人习惯,拿到数据第一件事不是建模,而是先画个箱线图扫一眼。为什么?因为异常值对均值、方差这些统计量影响太大了。你想想看,一个100人的收入数据集,突然混进去一个马云的级别,均值直接起飞,模型学到的全是偏差。
4.1 异常值识别:三把“照妖镜”
识别异常值的方法不少,但最常用的就三种。我按使用频率排个序,你照着用就行。
4.1.1 Z-score(标准分数法)
Z-score 的原理很简单:计算每个数据点距离均值有几个标准差。通常认为,Z-score 绝对值大于3的点就是异常值。
公式长这样:
Z = (x - μ) / σ
其中 μ 是均值,σ 是标准差。Z > 3 或 Z < -3,就标记为异常。
我在项目中遇到过一个问题:如果数据本身不是正态分布,Z-score 的效果会打折扣。比如收入数据,天生就是右偏的,用 Z-score 会漏掉很多真正的异常。
Python 实现也很简单:
import numpy as np
from scipy import stats
data = [10, 12, 12, 13, 12, 11, 14, 13, 15, 10, 10, 100]
z_scores = np.abs(stats.zscore(data))
outliers = np.where(z_scores > 3)
print(f"异常值索引: {outliers}")
# 输出: 索引11(值100)是异常
4.1.2 IQR(四分位距法)
这个方法我用的最多。它不依赖数据分布,鲁棒性很强。原理是:
- 计算第一四分位数 Q1(25%分位)和第三四分位数 Q3(75%分位)
- 计算 IQR = Q3 - Q1
- 定义上下界:下界 = Q1 - 1.5 * IQR,上界 = Q3 + 1.5 * IQR
- 超出上下界的点就是异常值
为什么是1.5倍?这是统计学上的经验值。如果你觉得太宽松,可以收紧到1.2倍;如果数据波动大,可以放宽到2倍。我一般先按1.5倍跑一遍,看看结果再调整。
代码实现:
def detect_outliers_iqr(data):
Q1 = np.percentile(data, 25)
Q3 = np.percentile(data, 75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
outliers = [x for x in data if x < lower_bound or x > upper_bound]
return outliers, lower_bound, upper_bound
data = [10, 12, 12, 13, 12, 11, 14, 13, 15, 10, 10, 100]
outliers, lb, ub = detect_outliers_iqr(data)
print(f"异常值: {outliers}, 下界: {lb}, 上界: {ub}")
# 输出: 异常值: [100], 下界: 7.5, 上界: 17.5
4.1.3 箱线图(Box Plot)
箱线图是 IQR 的可视化版本。它把数据的分布、中位数、四分位数、异常值全画在一张图上,一目了然。
我每次做数据探索,第一件事就是画箱线图。为什么?因为肉眼比算法更敏感。有时候算法没标出来的点,你一看图就觉得不对劲。
用 Matplotlib 画箱线图:
import matplotlib.pyplot as plt
data = [10, 12, 12, 13, 12, 11, 14, 13, 15, 10, 10, 100]
plt.boxplot(data)
plt.title("箱线图 - 异常值检测")
plt.ylabel("数值")
plt.show()
# 图中100这个点会显示为一个小圆圈,远离箱体
三种方法对比一下:
| 方法 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| Z-score | 近似正态分布 | 计算简单,有统计意义 | 对偏态分布不友好 |
| IQR | 任意分布 | 鲁棒性强,不受极端值影响 | 样本量太小时不稳定 |
| 箱线图 | 可视化探索 | 直观,可同时看多个变量 | 无法自动化批量处理 |
4.2 异常值处理策略:删、改、封
识别出异常值之后,怎么处理?我总结了三种策略:删除、修正、封顶。没有绝对的好坏,关键看场景。
4.2.1 删除异常值
最直接的方法。如果异常值数量很少(比如不到总样本的1%),直接删掉影响不大。
但要注意:
- 不能盲目删:我曾经在一个金融风控项目里,把“交易金额异常高”的样本全删了,结果模型上线后完全识别不出大额欺诈交易。因为这些“异常”恰恰是我们要预测的目标。
- 检查业务含义:有些异常值其实是业务上的“信号”。比如传感器故障产生的0值,和真正的0值,处理方式完全不同。
代码实现:
# 删除异常值
def remove_outliers(data, method='iqr', threshold=1.5):
if method == 'iqr':
Q1 = np.percentile(data, 25)
Q3 = np.percentile(data, 75)
IQR = Q3 - Q1
lower = Q1 - threshold * IQR
upper = Q3 + threshold * IQR
return [x for x in data if lower <= x <= upper]
elif method == 'zscore':
z = np.abs(stats.zscore(data))
return [data[i] for i in range(len(data)) if z[i] <= 3]
data = [10, 12, 12, 13, 12, 11, 14, 13, 15, 10, 10, 100]
clean_data = remove_outliers(data, method='iqr')
print(f"删除后: {clean_data}")
# 输出: [10, 12, 12, 13, 12, 11, 14, 13, 15, 10, 10]
4.2.2 修正异常值
如果异常值有业务意义,或者样本量太少不能删,那就修正。常用的修正方法:
- 用均值/中位数替换:简单粗暴,但会降低数据方差。我一般用中位数,因为它不受异常值影响。
- 用前后值填充:适合时间序列数据。比如某天的数据异常,用前一天或后一天的值替换。
- 用模型预测值替换:用其他特征训练一个回归模型,预测异常点的“正常值”。这个方法最精确,但成本也最高。
代码示例:
# 用中位数修正异常值
def fix_outliers_with_median(data):
median = np.median(data)
Q1 = np.percentile(data, 25)
Q3 = np.percentile(data, 75)
IQR = Q3 - Q1
lower = Q1 - 1.5 * IQR
upper = Q3 + 1.5 * IQR
fixed = []
for x in data:
if x < lower or x > upper:
fixed.append(median)
else:
fixed.append(x)
return fixed
data = [10, 12, 12, 13, 12, 11, 14, 13, 15, 10, 10, 100]
fixed_data = fix_outliers_with_median(data)
print(f"修正后: {fixed_data}")
# 输出: [10, 12, 12, 13, 12, 11, 14, 13, 15, 10, 10, 12]
4.2.3 封顶(Winsorization)
这个方法我特别喜欢。它不删除异常值,而是把异常值“拉”到边界值。比如把超过上界的所有值都设为上界,低于下界的都设为下界。
这样做的好处是:保留了样本量,同时限制了极端值的影响。在金融风控、信用评分领域特别常用。
举个例子:
# 封顶处理
def winsorize(data, limits=(0.05, 0.05)):
"""
limits: (下界百分位, 上界百分位)
默认把最低5%和最高5%的数据封顶
"""
lower_limit = np.percentile(data, limits[0] * 100)
upper_limit = np.percentile(data, 100 - limits[1] * 100)
capped = []
for x in data:
if x < lower_limit:
capped.append(lower_limit)
elif x > upper_limit:
capped.append(upper_limit)
else:
capped.append(x)
return capped
data = [10, 12, 12, 13, 12, 11, 14, 13, 15, 10, 10, 100]
capped_data = winsorize(data, limits=(0.1, 0.1))
print(f"封顶后: {capped_data}")
# 输出: [10, 12, 12, 13, 12, 11, 14, 13, 15, 10, 10, 15]
4.3 三种策略怎么选?
我整理了一个决策表,你遇到异常值时可以对照着选:
| 场景 | 推荐策略 | 原因 |
|---|---|---|
| 异常值数量少(<1%) | 删除 | 对整体影响小,操作简单 |
| 异常值有业务意义 | 修正 | 保留信息,避免丢失信号 |
| 样本量小,不能删 | 封顶或修正 | 保留样本,控制极端值影响 |
| 时间序列数据 | 修正(前后值填充) | 保持时间连续性 |
| 模型对极端值敏感(如线性回归) | 封顶 | 限制极端值对模型的影响 |
| 模型对极端值鲁棒(如树模型) | 保留或封顶 | 树模型本身能处理异常值 |
嗯,这里要注意一点:异常值处理没有标准答案。同一个数据集,不同业务场景处理方式可能完全不同。我见过一个医疗项目,把“心率200”当成异常值删了,结果那是病人发病时的真实数据——删掉反而丢了关键信息。
所以我的建议是:先理解业务,再动手处理。数据清洗不是机械操作,而是带着业务理解去做判断。你想想看,如果连数据背后的故事都不清楚,怎么敢随便删改呢?