4. 异常值检测与处理:三种实战方法
光谱数据里最让人头疼的是什么?我个人觉得,不是噪声,不是基线漂移,而是那些「不速之客」——异常值。
你想想看,一个异常值可能让你整个模型跑偏。我在项目中遇到过好几次,明明模型训练得好好的,一上测试数据就崩了。查了半天,原来是几个异常点在捣鬼。
今天咱们就聊聊三种最常用的异常值检测方法。每种方法都有它的脾气,选对了事半功倍。
4.1 基于标准差的方法
这个方法最简单,也最直观。说白了,就是假设你的数据符合正态分布。
正态分布有个特点:大部分数据都集中在均值附近。距离均值越远,数据越少。具体来说:
- 约68%的数据在 ±1σ 范围内
- 约95%的数据在 ±2σ 范围内
- 约99.7%的数据在 ±3σ 范围内
所以,那些落在 ±3σ 之外的点,大概率就是异常值。
核心公式:
异常值判定条件:|x - μ| > k × σ
其中 μ 是均值,σ 是标准差,k 通常取 2 或 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 的计算步骤:
- 计算每个点的 k-距离和 k-邻域
- 计算局部可达密度
- 计算 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'
- n_neighbors:邻居数量,通常取 10-30
- contamination:预期异常比例,默认 0.1
- metric:距离度量方式,常用 'euclidean' 或 'manhattan'
注意: LOF 的计算量比较大。如果你的数据量很大(比如几万个样本),建议先降维再用 LOF。我试过用 PCA 降到 50 维,速度提升了 10 倍,效果基本没变。
4.4 三种方法对比
| 方法 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 标准差法 | 单变量、正态分布 | 简单快速 | 对极端值敏感 |
| IQR法 | 单变量、任意分布 | 稳健、不受极端值影响 | 只适用于一维 |
| LOF法 | 多变量、高维数据 | 能发现局部异常 | 计算量大、参数敏感 |
4.5 知识体系图
4.6 我的选择建议
说了这么多,到底该用哪种?我个人的习惯是这样的:
- 先看数据维度: 如果只是单个波长点的异常检测,用 IQR 法最稳。如果是全光谱的异常样本检测,用 LOF。
- 再看数据量: 数据量小(几百个样本)用 LOF 没问题。数据量大(几万个样本)先用 PCA 降维再用 LOF,或者直接用 IQR 法逐个波长检测。
- 最后看分布: 如果数据明显不是正态分布,别用标准差法。IQR 法和 LOF 法对分布没有要求。
一个小技巧: 我经常把三种方法结合起来用。先用 IQR 法快速筛一遍,再用 LOF 法精细检测。这样既快又准,效果比单独用任何一种都好。
好了,异常值检测就聊到这儿。记住,没有万能的方法,关键是要理解你的数据。多试试,多对比,总能找到最适合的那一个。