异常值到底有哪几种?先搞清楚分类
说起异常值检测,很多人第一反应就是“找离群点”。但说实话,我在实际项目里踩过不少坑才明白——异常值不是铁板一块。不同场景、不同数据结构,异常值的“长相”完全不一样。
我个人习惯先把异常值分成三类:全局异常、局部异常和上下文异常。为什么要区分它们?因为每种类型对应的检测思路、算法选择、甚至业务解释都差得很远。
全局异常:一眼就能看出来的“异类”
全局异常,说白了就是整个数据集里最扎眼的那几个点。这些点的数值跟绝大多数样本差距巨大,你画个散点图就能一眼扫出来。
嗯,我给大家举个具体的例子。假设咱们在监控一台服务器的CPU使用率,正常波动在30%~70%之间。突然有一天,某个时刻的CPU冲到了98%。这时候98%就是全局异常——因为它在全局分布里离群了。
常见检测方法也很直接:
- Z-score法: 计算每个样本偏离均值的标准差数,超过3或-3就算异常。这个方法我经常用在正态性较好的数据上。
- IQR(四分位距)法: 用Q1-1.5*IQR和Q3+1.5*IQR做边界,超出即异常。对非正态分布特别友好。
- 基于距离的方法(如DBSCAN): 靠聚类密度来区分,核心点是“离任何簇都够远”。
不过,这里我要提醒一句——全局异常检测有一个致命缺陷。当数据存在多个密度不同的簇时,稀疏簇里的正常点很容易被误判为全局异常。你想想看,一个本来属于稀疏簇的正常样本,放在全局视角下可能离均值很远,但它确实不是异常。
局部异常:在“圈子”里才显出异常
局部异常的概念更有意思。一个点在全局视角下可能平平无奇,但在它周围的局部邻域里,却显得格格不入。
为什么会这样?我给大家讲个项目经历。有一次我做电商平台的行为日志分析,发现一个用户的点击量是200次/天,放在3万用户里根本不算高——因为有几个刷单用户每天点几千次。但仔细看这位用户的粉丝群,同类型用户的平均点击才50次。200次在这个局部群体里,就是明显的异常。
这就是局部异常的精髓:异常与否,取决于你跟谁比。
| 对比维度 | 全局异常 | 局部异常 |
|---|---|---|
| 判断参照 | 整个数据集 | 局部邻域 |
| 适用场景 | 单一密度、单峰分布 | 多密度、多簇、流形结构 |
| 典型方法 | Z-score、IQR | LOF、LOCI |
| 误报风险 | 稀疏簇正常点易误报 | 边界点需要谨慎 |
局部异常检测里最经典的算法是LOF(Local Outlier Factor)。它的思路很简单——比较一个点的“局部密度”和它邻居的“局部密度”。如果自己的密度明显低于邻居,那就是异常。
来看看LOF怎么操作:
# 简单示例:用sklearn计算LOF
from sklearn.neighbors import LocalOutlierFactor
import numpy as np
# 生成示例数据:两个密度不同的簇 + 一个局部异常点
data = np.array([
[1, 1], [1.2, 1.1], [0.9, 0.9], # 密度较高的簇
[10, 10], [10.2, 9.8], [9.9, 10.1], # 密度较低的簇
[5, 5] # 局部异常点:在两个簇之间,但离哪个都不算远
])
# 初始化LOF,设定邻居数k=3
clf = LocalOutlierFactor(n_neighbors=3)
lof_scores = clf.negative_outlier_factor_
# 输出LOF值(负数,绝对值越大越异常)
print("LOF scores:", lof_scores)
# 判断结果:-1表示正常,-1.5表示异常(默认阈值)
predictions = clf.fit_predict(data)
print("Predictions (1=normal, -1=anomaly):", predictions)
运行这段代码你会发现,[5,5]这个点虽然从全局看离均值的距离不算特别夸张,但LOF会给出一个很高的异常分数。因为它周围的局部密度跟邻居差距太大。
上下文异常:特定条件下的“反常”
上下文异常是我觉得最考验业务理解的一类。同一个数值,放在不同时间、不同地点、不同用户群下,含义完全不一样。
举个例子,某电商平台在双11当天的销售额是5亿,你说是异常吗?放在全年数据里,5亿绝对是天大的全局异常;但如果把“双11”当作一个上下文,这个数值恰恰是正常甚至偏低的——因为去年双11卖了6亿。
上下文异常有三个核心要素:
- 上下文属性: 决定了数据所处的条件(如时间、地点、用户身份)。
- 行为属性: 我们要判断是否异常的数值本身(如销售额、点击率)。
- 上下文参考范围: 在相同上下文下,行为属性的正常波动区间。
在时序数据里,上下文异常最常见。我个人处理过很多IOT设备的数据,每个设备的“正常”范围都不同——工厂里的一台机器在深夜可以超低频运行,但白天必须高频运转。你拿深夜的数值去比白天,那肯定是找错上下文了。
怎么找出上下文异常?一般流程是:
- 定义上下文: 通常按时间滑动窗口(比如过去24小时)或按类别分组(比如按用户ID)来做。
- 建立上下文基线: 在同一个上下文内,计算行为属性的均值和标准差。
- 比较与标记: 当前数值偏离上下文基线超过阈值,就标记为上下文异常。
举个实际的Python实现思路:
# 上下文异常检测:按用户ID分组,计算每个用户的Z-score
import pandas as pd
import numpy as np
# 模拟数据:用户ID、行为数值
df = pd.DataFrame({
'user_id': ['A']*5 + ['B']*5,
'value': [100, 105, 98, 102, 200, # 用户A的最后一个值200是异常?
10, 12, 9, 11, 50] # 用户B的最后一个值50是异常?
})
# 按用户分组计算Z-score
def context_zscore(group):
mean = group['value'].mean()
std = group['value'].std()
# 为防止std=0导致除零,加一个极小值
std = max(std, 1e-6)
group['z'] = (group['value'] - mean) / std
return group
df = df.groupby('user_id').apply(context_zscore)
print(df)
# 输出:用户A的200得出z≈1.9,用户B的50得出z≈2.1
# 如果阈值用3,那么都不是异常;如果用2,两个都是异常
# 这就是上下文的重要性——阈值选择也要结合业务来看
要注意的是,上下文异常检测里上下文定义的好坏直接决定检测效果。如果你把不相关的上下文强行绑定在一起,那结果比瞎猜还糟糕。
三类异常的对比与实战选择
聊到这里,咱们来张图收个尾。这张SVG图展示了三类异常在整个知识体系里的位置和相互关系,能帮你在做数据分析时快速定位该用哪类方法。
最后给大家一个实战中的取舍建议:
- 想一劳永逸? 不存在的。三类异常往往共存于一业务场景,比如电商订单数据里既有全局性的大金额诈骗(全局异常),也有区域性的价格异常(局部异常),还包含节假日相关的销售波动(上下文异常)。
- 优先顺序怎么定? 我的做法是:先做上下文分解(时间/空间分段),再做全局检测,最后局部精调。这样可以由粗到细,逐步收敛。
- 别太迷信单一指标。 不管哪类异常,最终都要落到业务可解释性上。你用一个深度模型打出来的异常分数很高,但业务人员看不懂,那也是白搭。