Python异常值检测:Z-Score、IQR、LOF与马氏距离
数据处理里,统计指标飘红或模型效果突然下降,很多时候是异常值导致。原文用 Python 给出五种常见异常值检测技术:Z-Score、IQR、LOF、Isolation Forest 和马氏距离,并分别结合单变量、偏态分布、多变量局部密度、高维大数据、变量相关场景做示例。下面按原理、代码、输出和适用场景整理。代码依赖 numpy;LOF 和 Isolation Forest 使用 scikit-learn;马氏距离使用 scipy。一、Z-Score:用标准差倍数判断异常值
Z-Score 计算每个点与均值的距离,相当于几个标准差。通常 |Z| > 3 视为异常,适合单变量、接近正态分布的数据。示例用北京地铁某站早高峰每分钟进站人数:正常值围绕 120 波动,再插入 280、20、300 三个极端值。
import numpy as np
np.random.seed(42)
normal = np.random.normal(120, 15, 200)
data = np.concatenate(])
mean = np.mean(data)
std = np.std(data)
z_scores = (data - mean) / std
outlier_indices = np.where(np.abs(z_scores) > 3)
print('Z-Score 异常值:')
for idx in outlier_indices:
print(f'索引 {idx}, 值 {data:.2f}, Z={z_scores:.2f}')
输出:
Z-Score 异常值:
索引 200, 值 280.00, Z=6.95
索引 201, 值 20.00, Z=-4.38
索引 202, 值 300.00, Z=7.82
从输出看,280、20、300 的 Z 值绝对值都超过 3,被标为异常;正常点基本没有误报。单变量、近似正态时,Z-Score 简单直接。
二、IQR:用四分位距判断异常值
IQR 不依赖均值和标准差,而是看中间 50% 数据的范围:低于 Q1 - 1.5 * IQR 或高于 Q3 + 1.5 * IQR 的点算异常,适合偏态分布或存在极端值的数据。示例用成都某小区住户月度电费,大多数住户几十到两三百,少数电费特别高。
import numpy as np
np.random.seed(1)
normal = np.random.gamma(shape=4, scale=30, size=300)
data = np.concatenate(])
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 = data[(data < lower_bound) | (data > upper_bound)]
print(f'Q1={q1:.2f}, Q3={q3:.2f}, IQR={iqr:.2f}')
print(f'下界={lower_bound:.2f}, 上界={upper_bound:.2f}')
print('IQR 异常值:', outliers)
输出:
Q1=77.89, Q3=161.82, IQR=83.93
下界=-48.01, 上界=287.72
IQR 异常值: [ 289.37700599289.53075874289.93834029328.543792 2000.
5000. 8000. ]
上界通常在两三百左右,2000、5000、8000 这类高额电费会被抓出来,少量正常偏高的电费也可能被带上。IQR 对偏态数据抗干扰较好,适合先做一轮粗筛。
三、LOF:用局部密度判断异常值
LOF 比较一个点与邻居的密度,如果明显比周围稀疏,就认为异常。它不要求全局统一分布,适合多变量、局部密度差异明显的数据。示例用杭州共享单车停放点早高峰借还车量:正常站点借车和还车量大致在 30 左右,少数站点组合异常。
import numpy as np
from sklearn.neighbors import LocalOutlierFactor
np.random.seed(42)
normal = np.random.normal(, , size=(200, 2))
outliers = np.array([, , , ])
data = np.vstack()
lof = LocalOutlierFactor(n_neighbors=20, contamination=0.02)
labels = lof.fit_predict(data)
outlier_indices = np.where(labels == -1)
print('LOF 异常点:')
for idx in outlier_indices:
print(f'索引 {idx}, 借车量={data:.1f}, 还车量={data:.1f}')
输出:
LOF 异常点:
索引 104, 借车量=32.6, 还车量=49.3
索引 200, 借车量=5.0, 还车量=80.0
索引 201, 借车量=85.0, 还车量=10.0
索引 202, 借车量=90.0, 还车量=90.0
索引 203, 借车量=10.0, 还车量=10.0
(5,80)、(85,10)、(90,90)、(10,10) 这类和正常站点密度明显不同的点会被标出。LOF 的优势是能找出局部行为异常的站点,而不是只看单列数值。
四、Isolation Forest:用随机隔离判断异常值
Isolation Forest 通过随机切分数据来隔离样本,异常点因为稀疏,通常几步就能被单独隔离出来,适合数据量大、维度高的场景。示例用 618 电商订单:正常订单金额几十到几百,商品件数 1 到 5 件,异常订单则可能是额采购或恶意测试。
import numpy as np
from sklearn.ensemble import IsolationForest
np.random.seed(7)
n = 1000
amount = np.random.normal(200, 80, n).clip(20, 800)
items = np.random.poisson(2, n) + 1
normal = np.column_stack()
outliers = np.array([
,
,
,
])
data = np.vstack()
iso = IsolationForest(contamination=0.01, random_state=42)
labels = iso.fit_predict(data)
outlier_indices = np.where(labels == -1)
print('Isolation Forest 异常订单:')
for idx in outlier_indices:
print(f'索引 {idx}, 金额={data:.2f}, 件数={data:.0f}')
输出:
Isolation Forest 异常订单:
索引 47, 金额=379.81, 件数=1
索引 186, 金额=90.41, 件数=7
索引 267, 金额=224.73, 件数=9
索引 658, 金额=356.52, 件数=6
索引 660, 金额=109.25, 件数=8
索引 662, 金额=329.58, 件数=9
索引 888, 金额=60.75, 件数=9
索引 1000, 金额=9999.00, 件数=100
索引 1001, 金额=0.01, 件数=500
索引 1002, 金额=5000.00, 件数=1
索引 1003, 金额=3000.00, 件数=200
9999 元 100 件、0.01 元 500 件、5000 元 1 件、3000 元 200 件等订单会被标出。因为 contamination 设为 0.01,也可能误伤少量正常订单。它胜在速度快,适合先在大数据中筛可疑样本。
五、马氏距离:用协方差结构判断异常值
马氏距离会考虑变量之间的协方差。单看某一项可能正常,组合起来很怪时也能抓出来,适合多变量且变量相关的数据。示例用某高中体测数据,身高、体重、肺活量三个变量有关联:身高越高,体重和肺活量通常也会高一些。
import numpy as np
from scipy.spatial.distance import mahalanobis
np.random.seed(42)
n = 300
height = np.random.normal(170, 6, n)
weight = 60 + 0.7 * (height - 170) + np.random.normal(0, 5, n)
lung = 3500 + 30 * (height - 170) + np.random.normal(0, 300, n)
normal = np.column_stack()
outliers = np.array([
,
,
])
data = np.vstack()
mean = np.mean(data, axis=0)
cov = np.cov(data, rowvar=False)
inv_cov = np.linalg.inv(cov)
distances = np.array()
threshold = np.percentile(distances, 97.5)
outlier_indices = np.where(distances > threshold)
print('马氏距离异常体测记录:')
for idx in outlier_indices:
print(f'索引 {idx}, 身高={data:.1f}, 体重={data:.1f}, 肺活量={data:.0f}, 距离={distances:.2f}')
输出:
马氏距离异常体测记录:
索引 46, 身高=167.2, 体重=50.8, 肺活量=2608, 距离=2.87
索引 74, 身高=154.3, 体重=59.8, 肺活量=2606, 距离=3.33
索引 179, 身高=186.3, 体重=77.0, 肺活量=4176, 距离=3.08
索引 209, 身高=193.1, 体重=72.0, 肺活量=3925, 距离=3.82
索引 262, 身高=150.6, 体重=50.7, 肺活量=3381, 距离=3.31
索引 300, 身高=170.0, 体重=120.0, 肺活量=2000, 距离=10.35
索引 301, 身高=150.0, 体重=80.0, 肺活量=5000, 距离=8.29
索引 302, 身高=190.0, 体重=45.0, 肺活量=3000, 距离=6.06
身高 170、体重 120、肺活量 2000;身高 150、体重 80、肺活量 5000;身高 190、体重 45、肺活量 3000 这类组合会被标出。因为阈值取 97.5 百分位,也会带上少量边缘正常记录。马氏距离适合变量有关联的多维异常检测,但协方差估计不准时结果会受影响。
总结:五种方法怎么选
Z-Score:单变量、近似正态分布,最省事。IQR:单变量、偏态分布或有极端值,比较稳。LOF:多变量、存在局部密度差异,适合找周围不一样的点。Isolation Forest:数据量大、维度高,想快速筛异常时优先。马氏距离:多变量之间有明显相关性,需要把相关性纳入判断时使用。无论使用哪种方法,最终都要结合业务进一步确认是否真的异常,不能只看算法结果。
Re: Python异常值检测:Z-Score、IQR、LOF与马氏距离
楼主整理得很清楚,按原理、代码、输出和适用场景来梳理,读起来很顺。Z-Score 和 IQR 用来做单变量粗筛很直观,LOF 能看局部密度、Isolation Forest 适合高维大数据,马氏距离考虑变量相关性,这几个点区分得挺实用。实际用的时候我比较纠结组合场景,比如数据既偏态又有多变量相关,是不是先 IQR 粗筛再上 LOF 或马氏距离会更稳?另外 Isolation Forest 的代码好像还没贴完,马氏距离部分也还没展开,期待后续补上。contamination 这个参数一般怎么定比较合理,是按业务预估异常比例,还是先看异常分数排序再人工定阈值?Re: Python异常值检测:Z-Score、IQR、LOF与马氏距离
整理得挺系统的,Z-Score、IQR、LOF 这三段的例子也很接地气,地铁客流、小区电费、共享单车都容易代入,代码拿过去基本能直接跑。提个小细节:LOF 那段输出循环里写的是 data 和 data,按这个写法每次应该打印同一行,但输出里每个索引对应的值又不一样,估计是笔误,改成 data 或者 data、data 会更一致。另外帖子好像到 Isolation Forest 的 amount、items 那里就断了,后面的 Isolation Forest 和马氏距离还没贴完,期待补全。整体看很适合当异常检测的速查,先收下了。Re: Python异常值检测:Z-Score、IQR、LOF与马氏距离
感谢整理,这几种异常值检测放在一起对比很清晰。Z-Score 和 IQR 的例子很直观,一个用标准差倍数,一个用四分位距,尤其是电费那种偏态数据,IQR 确实比 Z-Score 更稳,不容易被极端值把均值和标准差带偏。LOF 用共享单车借还车量来展示局部密度异常也很贴合多变量场景,不过输出里索引 104 也被标出来,说明 n_neighbors 和 contamination 的取值对结果影响不小,实际用的时候估计还得结合业务判断。Isolation Forest 那段好像还没贴完,马氏距离也还没看到代码和示例,期待后续补齐。实际做异常检测时,感觉可以先可视化或做一轮 IQR 粗筛,再根据数据维度、分布和变量相关性选择 LOF、Isolation Forest 或马氏距离,最后几种方法交叉验证,避免单一规则误杀。
页:
[1]