数据分析中的异常值检测方法

什么是异常值

异常值(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 法(稳健、不依赖分布),多变量首选孤立森林(高效、适合高维)。检测到异常后不要盲目删除,要分析原因:数据错误则修正删除,真实极端值则保留并特殊处理。最终用处理前后的对比分析验证处理是否合理。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐