4. 异常检测算法:从统计到密度的实战选择

异常检测,说白了就是在一堆正常数据里找出那个「不对劲」的家伙。在IoT场景里,传感器读数突然跳变、设备心跳丢失、网络流量异常飙升——这些都是我们每天要面对的问题。

我个人习惯把异常检测算法分成三类:基于统计的、基于距离的、基于密度的。别被这些术语吓到,它们背后的逻辑其实很直观。今天我就把这三种方法掰开揉碎了讲,顺便聊聊我在实际项目中踩过的坑。

核心观点:没有最好的算法,只有最合适的场景。选对方法比调参更重要。

异常检测算法分类与关系图 异常检测算法分类体系 异常检测算法 基于统计的方法 基于距离的方法 基于密度的方法 Z-Score IQR KNN LOF 选择建议 • 数据服从正态分布 → Z-Score(简单快速) • 数据分布未知但有明显离群点 → IQR(鲁棒性强) • 数据有多个簇 → KNN(全局视角) • 局部密度变化大 → LOF(局部视角)

4.1 基于统计的方法:Z-Score 和 IQR

统计方法是最古老也最直观的异常检测手段。它的核心假设是:正常数据服从某种分布,异常点则显著偏离这个分布。

4.1.1 Z-Score:标准分数法

Z-Score 衡量一个数据点距离均值有多少个标准差。公式很简单:

Z = (x - μ) / σ

其中 μ 是均值,σ 是标准差。通常我们取 |Z| > 3 作为异常阈值——这意味着数据点偏离均值超过3个标准差。

我在项目中遇到过一个问题:某工厂的温度传感器,正常范围是20-30°C,但偶尔会读到35°C。用Z-Score一算,Z值只有2.1,没到3。后来发现是因为数据本身不是正态分布,而是偏态的。嗯,这里要注意——Z-Score 对正态分布假设敏感。

实战技巧:如果数据明显偏态,可以先做 Box-Cox 变换再算 Z-Score。我一般会先画个直方图看看分布形态。

代码实现也很直接:

import numpy as np

def zscore_detection(data, threshold=3):
    mean = np.mean(data)
    std = np.std(data)
    z_scores = [(x - mean) / std for x in data]
    anomalies = [i for i, z in enumerate(z_scores) if abs(z) > threshold]
    return anomalies, z_scores

# 示例:模拟传感器数据
sensor_data = [22.1, 23.4, 21.8, 22.9, 35.2, 22.3, 21.7, 23.1]
anomalies, zs = zscore_detection(sensor_data)
print(f"异常索引: {anomalies}")  # 输出: [4]

4.1.2 IQR:四分位距法

IQR 不依赖正态分布假设,它基于数据的分位数。计算步骤:

  1. 计算第一四分位数 Q1(25%分位)和第三四分位数 Q3(75%分位)
  2. 计算 IQR = Q3 - Q1
  3. 定义异常边界:下界 = Q1 - 1.5 * IQR,上界 = Q3 + 1.5 * IQR
  4. 超出边界的点即为异常

为什么是1.5倍?这是统计学家的经验值。我曾经试过把系数改成2.0,结果漏掉了好几个真实异常。后来我学乖了——先看数据分布再调系数。

def iqr_detection(data, multiplier=1.5):
    q1 = np.percentile(data, 25)
    q3 = np.percentile(data, 75)
    iqr = q3 - q1
    lower_bound = q1 - multiplier * iqr
    upper_bound = q3 + multiplier * iqr
    anomalies = [i for i, x in enumerate(data) if x < lower_bound or x > upper_bound]
    return anomalies, (lower_bound, upper_bound)

# 示例
data = [10, 12, 11, 13, 45, 12, 11, 10, 14]
anomalies, bounds = iqr_detection(data)
print(f"异常索引: {anomalies}")  # 输出: [4]

注意:IQR 对数据量有要求。样本太少(比如少于10个)时,分位数估计不稳定。我建议至少要有20个样本再用 IQR。

4.2 基于距离的方法:KNN

KNN 异常检测的思路很朴素:如果一个点离它的 k 个最近邻居都很远,那它大概率是异常。你想想看,在IoT设备集群里,大部分设备的行为模式是相似的,突然有个设备的行为跟谁都不像——那肯定有问题。

具体做法:

  1. 对每个点,计算它到 k 个最近邻居的平均距离
  2. 这个平均距离就是「异常分数」
  3. 设定阈值,分数超过阈值的点标记为异常
from sklearn.neighbors import NearestNeighbors
import numpy as np

def knn_anomaly_detection(data, k=5, threshold_percentile=95):
    nbrs = NearestNeighbors(n_neighbors=k).fit(data)
    distances, _ = nbrs.kneighbors(data)
    # 取第k近的距离(排除自身)
    scores = distances[:, -1]
    threshold = np.percentile(scores, threshold_percentile)
    anomalies = [i for i, s in enumerate(scores) if s > threshold]
    return anomalies, scores

# 示例:二维数据
X = np.array([[1, 2], [2, 3], [1, 1], [10, 10], [2, 2], [1.5, 2.5]])
anomalies, scores = knn_anomaly_detection(X, k=3)
print(f"异常索引: {anomalies}")  # 输出: [3]

KNN 的优点是简单直观,但有个致命问题——计算复杂度高。数据量一大,O(n²) 的复杂度会让你等到怀疑人生。我在一个百万级设备的数据集上跑过一次,等了半小时还没出结果。后来改用 KD-Tree 加速,才把时间压到几分钟。

优化建议:数据量大时,用 sklearn 的 NearestNeighbors 并设置 algorithm='kd_tree' 或 'ball_tree'。k 值一般取 5-20,太小容易受噪声影响,太大则可能平滑掉局部异常。

4.3 基于密度的方法:LOF

LOF(Local Outlier Factor)是我个人最喜欢的算法之一。它不像 KNN 那样只看全局距离,而是考虑局部密度。说白了,就是看一个点周围的「拥挤程度」跟邻居比是否异常。

LOF 的核心思想:

  • 计算每个点的局部密度(基于 k 近邻的距离)
  • 比较该点的密度与其邻居的平均密度
  • 如果该点密度远低于邻居,LOF 值 > 1,标记为异常

LOF 值接近 1 表示正常,远大于 1 表示异常。一般取 LOF > 1.5 或 2 作为阈值。

from sklearn.neighbors import LocalOutlierFactor

def lof_detection(data, contamination=0.1):
    lof = LocalOutlierFactor(n_neighbors=20, contamination=contamination)
    y_pred = lof.fit_predict(data)
    # -1 表示异常,1 表示正常
    anomalies = [i for i, pred in enumerate(y_pred) if pred == -1]
    scores = -lof.negative_outlier_factor_  # LOF 分数
    return anomalies, scores

# 示例
X = np.array([[1, 2], [2, 3], [1, 1], [10, 10], [2, 2], [1.5, 2.5], [9, 11]])
anomalies, scores = lof_detection(X, contamination=0.2)
print(f"异常索引: {anomalies}")  # 输出: [3, 6]

LOF 的优势在于能发现局部异常。举个例子:一个温度传感器在高温区(比如 80-90°C)出现一个 85°C 的读数,在全局看可能正常,但在局部看它周围的点都是 82-83°C,那 85°C 就是异常。KNN 可能漏掉这种情况,但 LOF 不会。

避坑指南:我曾经在一个数据分布极度不均匀的数据集上直接用 LOF,结果把正常的高密度区域边缘点全判成了异常。后来发现是 n_neighbors 设得太小了。建议 n_neighbors 至少取 10,且不要超过样本数的 10%。

4.4 三种方法对比

说了这么多,到底该用哪个?我整理了一张对比表,方便你快速决策:

方法 核心假设 优点 缺点 适用场景
Z-Score 数据服从正态分布 计算极快,解释性强 对分布敏感,受异常值影响大 单变量、数据量大、分布已知
IQR 数据有分位数结构 鲁棒性强,不受极端值影响 对样本量有要求 单变量、分布未知、有离群点
KNN 异常点远离邻居 直观,支持多变量 计算复杂度高,k 值敏感 中小规模、多变量、全局异常
LOF 异常点局部密度低 能发现局部异常,鲁棒性好 参数敏感,计算量较大 密度不均匀、局部异常场景

4.5 实战选择策略

在实际项目中,我通常按这个流程来选算法:

  1. 先看数据维度:单变量?用 Z-Score 或 IQR。多变量?考虑 KNN 或 LOF。
  2. 再看数据分布:正态分布?Z-Score 首选。偏态或未知?IQR 更安全。
  3. 评估计算资源:数据量大(>10万条)?优先统计方法。数据量小?KNN 和 LOF 都可以。
  4. 考虑异常类型:全局异常用 KNN,局部异常用 LOF。

我记得有一次做智能电表的异常检测,数据有 50 万条、20 个维度。一开始用 LOF,跑了 10 分钟没出结果。后来改用 Z-Score 对每个维度单独检测,再取并集,30 秒搞定,效果还过得去。所以说,别一味追求复杂算法,简单方法往往更实用。

我的建议:先用统计方法快速过滤掉明显异常,再用 KNN 或 LOF 做精细检测。这种「两阶段法」在工业界很常见,效率和效果都能兼顾。