4. 异常值检测与处理:三种实战方法

光谱数据里最让人头疼的是什么?我个人觉得,不是噪声,不是基线漂移,而是那些「不速之客」——异常值。

你想想看,一个异常值可能让你整个模型跑偏。我在项目中遇到过好几次,明明模型训练得好好的,一上测试数据就崩了。查了半天,原来是几个异常点在捣鬼。

今天咱们就聊聊三种最常用的异常值检测方法。每种方法都有它的脾气,选对了事半功倍。

4.1 基于标准差的方法

这个方法最简单,也最直观。说白了,就是假设你的数据符合正态分布。

正态分布有个特点:大部分数据都集中在均值附近。距离均值越远,数据越少。具体来说:

  • 约68%的数据在 ±1σ 范围内
  • 约95%的数据在 ±2σ 范围内
  • 约99.7%的数据在 ±3σ 范围内

所以,那些落在 ±3σ 之外的点,大概率就是异常值。

核心公式:
异常值判定条件:|x - μ| > k × σ
其中 μ 是均值,σ 是标准差,k 通常取 2 或 3

代码实现也很简单:

import numpy as np

def detect_outliers_std(data, threshold=3):
    mean = np.mean(data)
    std = np.std(data)
    z_scores = np.abs((data - mean) / std)
    return z_scores > threshold

# 使用示例
spectrum = np.array([1.2, 1.3, 1.1, 1.4, 5.8, 1.2, 1.3])
outliers = detect_outliers_std(spectrum)
print(f"异常值索引: {np.where(outliers)[0]}")
我的经验: 对于光谱数据,k=3 是个不错的起点。但如果你的数据本身波动就大,可以试试 k=2.5。我曾经处理过一批近红外光谱,用 k=3 死活检测不出异常,降到 2.5 就全出来了。
注意: 这个方法对极端值非常敏感。因为均值和标准差本身就会被异常值影响。如果异常值太多,这个方法可能失效。

4.2 基于IQR的方法

这个方法比标准差法更稳健。为什么?因为它用的是中位数和四分位数,不受极端值影响。

IQR 就是四分位距,计算公式:

IQR = Q3 - Q1

其中 Q1 是下四分位数(25%分位点),Q3 是上四分位数(75%分位点)。

异常值的判定标准:

  • 低于 Q1 - 1.5 × IQR
  • 高于 Q3 + 1.5 × IQR

这些点就被认为是异常值。

def detect_outliers_iqr(data):
    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
    
    return (data < lower_bound) | (data > upper_bound)

# 使用示例
spectrum = np.array([1.2, 1.3, 1.1, 1.4, 5.8, 1.2, 1.3])
outliers = detect_outliers_iqr(spectrum)
print(f"异常值索引: {np.where(outliers)[0]}")
避坑指南: 我曾经用 IQR 方法处理一批拉曼光谱,发现 1.5 倍 IQR 太宽松了,很多明显的异常点没抓到。后来改成 1.0 倍,效果好了很多。具体用多少倍,建议你先画个箱线图看看分布。

4.3 基于LOF的方法

前面两种方法都是针对一维数据的。但光谱数据往往是高维的——每个样本有几百上千个波长点。

这时候就需要 LOF(局部异常因子)了。它的核心思想是:比较一个点的密度和它邻居的密度。

如果一个点的密度明显低于邻居,那它就很可能是异常值。

LOF 的计算步骤:

  1. 计算每个点的 k-距离和 k-邻域
  2. 计算局部可达密度
  3. 计算 LOF 值

LOF 值大于 1,说明该点密度低于邻居,可能是异常值。LOF 值越大,异常程度越高。

from sklearn.neighbors import LocalOutlierFactor

def detect_outliers_lof(data, contamination=0.1):
    lof = LocalOutlierFactor(
        n_neighbors=20,
        contamination=contamination
    )
    outliers = lof.fit_predict(data)
    # LOF 返回 -1 表示异常,1 表示正常
    return outliers == -1

# 使用示例
# 假设 spectrum_data 是 (n_samples, n_features) 的二维数组
spectrum_data = np.random.randn(100, 500)  # 100个样本,500个波长点
outliers = detect_outliers_lof(spectrum_data)
print(f"异常样本数: {np.sum(outliers)}")
关键参数:
- n_neighbors:邻居数量,通常取 10-30
- contamination:预期异常比例,默认 0.1
- metric:距离度量方式,常用 'euclidean' 或 'manhattan'
注意: LOF 的计算量比较大。如果你的数据量很大(比如几万个样本),建议先降维再用 LOF。我试过用 PCA 降到 50 维,速度提升了 10 倍,效果基本没变。

4.4 三种方法对比

方法 适用场景 优点 缺点
标准差法 单变量、正态分布 简单快速 对极端值敏感
IQR法 单变量、任意分布 稳健、不受极端值影响 只适用于一维
LOF法 多变量、高维数据 能发现局部异常 计算量大、参数敏感

4.5 知识体系图

异常值检测方法体系 异常值检测 基于标准差法 基于IQR法 基于LOF法 假设数据服从正态分布 使用Z-score判定 阈值通常取2或3 基于四分位数 不受极端值影响 适用于任意分布 基于局部密度比较 适用于高维数据 计算量较大 选择建议:一维用IQR,高维用LOF

4.6 我的选择建议

说了这么多,到底该用哪种?我个人的习惯是这样的:

  • 先看数据维度: 如果只是单个波长点的异常检测,用 IQR 法最稳。如果是全光谱的异常样本检测,用 LOF。
  • 再看数据量: 数据量小(几百个样本)用 LOF 没问题。数据量大(几万个样本)先用 PCA 降维再用 LOF,或者直接用 IQR 法逐个波长检测。
  • 最后看分布: 如果数据明显不是正态分布,别用标准差法。IQR 法和 LOF 法对分布没有要求。
一个小技巧: 我经常把三种方法结合起来用。先用 IQR 法快速筛一遍,再用 LOF 法精细检测。这样既快又准,效果比单独用任何一种都好。

好了,异常值检测就聊到这儿。记住,没有万能的方法,关键是要理解你的数据。多试试,多对比,总能找到最适合的那一个。

专注资料整理