## 异常值类型:全局异常、局部异常、上下文异常

异常值到底有哪几种?先搞清楚分类

说起异常值检测,很多人第一反应就是“找离群点”。但说实话,我在实际项目里踩过不少坑才明白——异常值不是铁板一块。不同场景、不同数据结构,异常值的“长相”完全不一样。

我个人习惯先把异常值分成三类:全局异常、局部异常和上下文异常。为什么要区分它们?因为每种类型对应的检测思路、算法选择、甚至业务解释都差得很远。

全局异常:一眼就能看出来的“异类”

全局异常,说白了就是整个数据集里最扎眼的那几个点。这些点的数值跟绝大多数样本差距巨大,你画个散点图就能一眼扫出来。

嗯,我给大家举个具体的例子。假设咱们在监控一台服务器的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会给出一个很高的异常分数。因为它周围的局部密度跟邻居差距太大。

我曾经踩过这个坑: 在客户体验项目里,一开始我用Z-score去检测异常用户,结果把一批低频高价值的高端用户全打成了异常。后来换成LOF,才挽回了这批用户的数据分析。所以,局部方法不是万能的,但处理密度不均的数据时,它是全局方法很好的替代方案。

上下文异常:特定条件下的“反常”

上下文异常是我觉得最考验业务理解的一类。同一个数值,放在不同时间、不同地点、不同用户群下,含义完全不一样。

举个例子,某电商平台在双11当天的销售额是5亿,你说是异常吗?放在全年数据里,5亿绝对是天大的全局异常;但如果把“双11”当作一个上下文,这个数值恰恰是正常甚至偏低的——因为去年双11卖了6亿。

上下文异常有三个核心要素:

  1. 上下文属性: 决定了数据所处的条件(如时间、地点、用户身份)。
  2. 行为属性: 我们要判断是否异常的数值本身(如销售额、点击率)。
  3. 上下文参考范围: 在相同上下文下,行为属性的正常波动区间。

在时序数据里,上下文异常最常见。我个人处理过很多IOT设备的数据,每个设备的“正常”范围都不同——工厂里的一台机器在深夜可以超低频运行,但白天必须高频运转。你拿深夜的数值去比白天,那肯定是找错上下文了。

怎么找出上下文异常?一般流程是:

  1. 定义上下文: 通常按时间滑动窗口(比如过去24小时)或按类别分组(比如按用户ID)来做。
  2. 建立上下文基线: 在同一个上下文内,计算行为属性的均值和标准差。
  3. 比较与标记: 当前数值偏离上下文基线超过阈值,就标记为上下文异常。

举个实际的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图展示了三类异常在整个知识体系里的位置和相互关系,能帮你在做数据分析时快速定位该用哪类方法。

异常值类型分类与检测体系 异常值检测 全局异常 局部异常 上下文异常 常用方法 Z-score、IQR、DBSCAN 常用方法 LOF、LOCI 常用方法 滑动窗口、分组基线 实战决策要点 单峰 → 全局;多密度 → 局部;带上下文 → 上下文异常

最后给大家一个实战中的取舍建议:

  • 想一劳永逸? 不存在的。三类异常往往共存于一业务场景,比如电商订单数据里既有全局性的大金额诈骗(全局异常),也有区域性的价格异常(局部异常),还包含节假日相关的销售波动(上下文异常)。
  • 优先顺序怎么定? 我的做法是:先做上下文分解(时间/空间分段),再做全局检测,最后局部精调。这样可以由粗到细,逐步收敛。
  • 别太迷信单一指标。 不管哪类异常,最终都要落到业务可解释性上。你用一个深度模型打出来的异常分数很高,但业务人员看不懂,那也是白搭。
一个提醒: 有些异常的“类型”是会随着数据更新而变的。你今天认为是全局异常的样本,随着新数据加入,可能变成局部异常。所以检测结果需要持续验证和重新标定,别做完一次就收工。