如何在数据分析中检测异常值?有哪些常见的方法?
·
数据分析中的异常值检测方法
什么是异常值
异常值(Outlier)是明显偏离数据整体模式的观测值,可能来自数据录入错误、测量误差,也可能本身就是有意义的极端现象。
[12, 15, 14, 13, 16, 14, 500, 15, 12]
↑
明显偏离正常范围
方法分类
异常值检测
├── 统计方法
│ ├── 3σ 原则
│ ├── 箱线图(IQR)法
│ ├── Z-Score 法
│ └── Grubbs 检验
├── 距离方法
│ ├── KNN 距离法
│ └── 局部异常因子(LOF)
├── 密度方法
│ ├── DBSCAN
│ └── 孤立森林
└── 模型方法
├── 残差分析
└── One-Class SVM
一、3σ 原则
适用前提:数据近似正态分布
μ - 3σ ─── μ - 2σ ─── μ - σ ─── μ ─── μ + σ ─── μ + 2σ ─── μ + 3σ
| | | | | | |
0.15% 2.35% 13.5% 34% 13.5% 2.35% 0.15%
→ 超出 μ ± 3σ 的概率仅 0.3%,视为异常值
import numpy as np
def detect_3sigma(data):
mean = np.mean(data)
std = np.std(data, ddof=1)
lower = mean - 3 * std
upper = mean + 3 * std
outliers = [x for x in data if x < lower or x > upper]
return outliers, lower, upper
data = [12, 15, 14, 13, 16, 14, 500, 15, 12, 14, 13, 15]
outliers, lower, upper = detect_3sigma(data)
# outliers: [500],mean≈54.6,std≈139.7,范围 ≈ [-364.5, 473.7]
# 注意:500 被 3σ 刚好包住!说明异常值本身拉大了 σ
3σ 的致命问题:异常值会拉偏均值和标准差
修正:可用中位数和 MAD 替代
修正 Z-Score = 0.6745 × (x - median) / MAD
超过 ±3.5 视为异常
二、箱线图(IQR)法
最常用、最稳健的方法,不要求数据正态分布
┌─────────────┐
│ │ ← Q3
最大非异常 ┤ ├──── 上须 = min(最大值, Q3 + 1.5×IQR)
│ │
│ IQR区域 │ ← Q2(中位数)
│ │
最小非异常 ┤ ├──── 下须 = max(最小值, Q1 - 1.5×IQR)
│ │ ← Q1
└─────────────┘
↑
下须以下 / 上须以上 = 异常值
def detect_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
outliers = [x for x in data if x < lower or x > upper]
return outliers, lower, upper
data = [12, 15, 14, 13, 16, 14, 500, 15, 12, 14, 13, 15]
outliers, lower, upper = detect_iqr(data)
# Q1=13, Q3=15, IQR=2, 范围=[10, 18]
# outliers: [500] ✓ 稳健识别
箱线图法不会被异常值拉偏,因为 Q1/Q3 本身是基于百分位数的稳健统计量。
三、Z-Score 法
xᵢ - x̄
zᵢ = ─────────
s
|zᵢ| > 2 → 可能异常
|zᵢ| > 3 → 高度异常
from scipy import stats
def detect_zscore(data, threshold=3):
z_scores = stats.zscore(data)
outliers = [(x, z) for x, z in zip(data, z_scores) if abs(z) > threshold]
return outliers
data = [12, 15, 14, 13, 16, 14, 500, 15, 12, 14, 13, 15]
outliers = detect_zscore(data)
# 与3σ本质相同,同样会被异常值拉偏均值和标准差
修正 Z-Score(MAD 法)
def detect_modified_zscore(data, threshold=3.5):
median = np.median(data)
mad = np.median(np.abs(data - median))
modified_z = 0.6745 * (data - median) / mad
outliers = [(x, z) for x, z in zip(data, modified_z) if abs(z) > threshold]
return outliers
# 用中位数和 MAD 代替均值和标准差,不受异常值影响
四、Grubbs 检验
适用:逐一检测单变量中的异常值(样本量 < 50)
from scipy import stats
def grubbs_test(data, alpha=0.05):
"""逐个检测,每次剔除最极端的值,直到没有异常"""
outliers = []
remaining = list(data)
while len(remaining) > 2:
n = len(remaining)
mean = np.mean(remaining)
std = np.std(remaining, ddof=1)
# 找离均值最远的点
deviations = [abs(x - mean) for x in remaining]
max_idx = np.argmax(deviations)
g = deviations[max_idx] / std
# 临界值
t = stats.t.ppf(1 - alpha / (2 * n), n - 2)
critical = (n - 1) / np.sqrt(n) * np.sqrt(t**2 / (n - 2 + t**2))
if g > critical:
outliers.append(remaining.pop(max_idx))
else:
break
return outliers
data = [12, 15, 14, 13, 16, 14, 500, 15, 12, 14, 13, 15]
outliers = grubbs_test(data)
# [500]
五、孤立森林(Isolation Forest)
适用:多维数据,无监督,最常用的机器学习方法
核心思想:异常值是"少数且不同"的 → 更容易被孤立
正常点:需要多次随机切割才能分离
异常值:少量切割就能分离
特征1
│
┌────┼────────┐
│ │ ╳ │ ← 异常点,切1次就分离
│ ┌─┼──┐ │
│ │ ·│· │ │ ← 正常点,需要多切几次
│ │· │ ·│ │
│ └──┴──┘ │
└──────────────┘
特征2
from sklearn.ensemble import IsolationForest
# 多维数据
data = np.array([
[25, 5000], [28, 6000], [30, 5500], [27, 5200],
[26, 4800], [29, 5800], [80, 50000], # 异常
[24, 4600], [31, 6200], [22, 4400],
])
model = IsolationForest(
n_estimators=100,
contamination=0.1, # 预期异常比例
random_state=42
)
model.fit(data)
pred = model.predict(data) # 1=正常, -1=异常
scores = model.decision_function(data) # 异常分数(越小越异常)
for i, (p, s) in enumerate(zip(pred, scores)):
if p == -1:
print(f"异常值: {data[i]}, 分数: {s:.4f}")
# 异常值: [80, 50000], 分数: -0.18
六、LOF(局部异常因子)
适用:密度不均匀的数据,检测局部异常
核心思想:比较每个点的局部密度与邻居的局部密度
正常点:和邻居密度相近 → LOF ≈ 1
异常点:局部密度远低于邻居 → LOF >> 1
密集区域中的稀疏点 → 全局方法可能漏掉 → LOF 能识别
from sklearn.neighbors import LocalOutlierFactor
data = np.array([
[1, 1], [1.1, 1], [1, 1.1], [1.1, 1.1], # 密集区
[5, 5], [5.1, 5], [5, 5.1], # 另一个密集区
[3, 3], # 两区之间,全局看不太异常
])
model = LocalOutlierFactor(n_neighbors=5, contamination=0.1)
pred = model.fit_predict(data) # 1=正常, -1=异常
lof_scores = -model.negative_outlier_factor_ # LOF 值
for i, (p, s) in enumerate(zip(pred, lof_scores)):
if p == -1:
print(f"异常: {data[i]}, LOF={s:.2f}")
七、DBSCAN 密度聚类
适用:非规则形状的簇,噪声点即为异常值
from sklearn.cluster import DBSCAN
data = np.array([
[1, 1], [1.1, 1.1], [0.9, 1], [1, 0.9],
[5, 5], [5.1, 5.1], [4.9, 5], [5, 4.9],
[15, 15], # 噪声点
])
model = DBSCAN(eps=1.5, min_samples=3)
labels = model.fit_predict(data)
# -1 表示噪声/异常
for i, label in enumerate(labels):
if label == -1:
print(f"异常: {data[i]}")
# 异常: [15, 15]
方法选择指南
单变量检测:
数据近似正态 → 3σ / Z-Score
数据偏斜或有异常值 → IQR 法 / 修正 Z-Score
需要严格统计检验 → Grubbs / Dixon 检验
多变量检测:
数据维度低、量适中 → IQR 对每列逐个检测
数据量大、特征多 → 孤立森林(首选)
密度不均匀 → LOF
非规则簇结构 → DBSCAN
时序数据异常:
移动平均 + 残差法
STL 分解:趋势 + 季节 + 残差 → 对残差检测
处理异常值的策略
检测到异常值之后,不是直接删除,而是根据原因选择处理方式:
# 1. 确认异常原因
# 数据录入错误 → 修正或删除
# 测量设备故障 → 删除
# 真实极端现象 → 保留,单独分析
# 2. 删除
df_clean = df[~df['value'].isin(outlier_values)]
# 3. 截断(Winsorize)— 把极端值压到边界
from scipy.stats.mstats import winsorize
df['value_winsorized'] = winsorize(df['value'], limits=[0.05, 0.05])
# 下方 5% 的值压到 P5,上方 5% 的值压到 P95
# 4. 对数变换 — 压缩右偏分布
df['value_log'] = np.log1p(df['value'])
# 5. 分箱 — 把极端值归入边界箱
df['value_bin'] = pd.cut(df['value'], bins=[0, 10, 20, 30, float('inf')])
# 6. 单独标记
df['is_outlier'] = df['value'].apply(lambda x: x < lower or x > upper)
# 分析时可以对比含/不含异常值的结果
实践中的完整流程
① 先画图看分布
→ 直方图、箱线图、散点图
② 选择检测方法
→ 单变量:IQR 法
→ 多变量:孤立森林
③ 检测并标记异常值
④ 分析异常原因
→ 业务确认 / 交叉验证
⑤ 选择处理策略
→ 删除 / 截断 / 变换 / 保留标记
⑥ 对比处理前后的分析结果
→ 确保处理没有引入偏差
总结
异常值检测的核心是统计方法识别偏离、机器学习方法识别孤立。单变量首选 IQR 法(稳健、不依赖分布),多变量首选孤立森林(高效、适合高维)。检测到异常后不要盲目删除,要分析原因:数据错误则修正删除,真实极端值则保留并特殊处理。最终用处理前后的对比分析验证处理是否合理。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐
所有评论(0)