4、异常值检测与处理:基于统计的方法(Z-score、IQR)、基于距离的方法(LOF)、基于密度的方法(DBSCAN)、向量异常值的影响
异常值检测,说白了就是在一堆数据里找出那些「格格不入」的点。做向量数据清洗时,这一步我从来不敢跳过。为什么?因为几个异常向量就能把整个模型的训练带偏,你辛辛苦苦调参,结果全白费了。
我个人习惯把异常值检测方法分成三类:统计的、距离的、密度的。每种方法都有自己的脾气,选对了事半功倍,选错了…嗯,我踩过不少坑,下面一个个说。
4.1 基于统计的方法:Z-score 与 IQR
统计方法是最直观的。你想想看,如果数据服从正态分布,那绝大多数点都会集中在均值附近。离均值太远的点,自然就是异常。
4.1.1 Z-score 方法
Z-score 的计算公式很简单:Z = (x - μ) / σ。它衡量的是数据点偏离均值多少个标准差。
我记得有一次做传感器数据清洗,采集到的温度值里混了几个 200°C 的读数——正常范围明明是 20-40°C。用 Z-score 一算,这些点的 Z 值都超过 10 了,妥妥的异常。
import numpy as np
def zscore_detection(data, threshold=3):
"""Z-score 异常检测"""
mean = np.mean(data)
std = np.std(data)
z_scores = np.abs((data - mean) / std)
outliers = np.where(z_scores > threshold)[0]
return outliers, z_scores
# 示例:检测向量中的异常值
vector = np.array([1.2, 1.5, 1.3, 1.4, 1.6, 15.2, 1.3, 1.5])
outliers, scores = zscore_detection(vector)
print(f"异常索引: {outliers}") # [5]
print(f"异常Z值: {scores[outliers]}") # [~12.3]
4.1.2 IQR 方法
IQR(四分位距)方法不依赖正态分布假设,鲁棒性更好。它用 Q1(25% 分位数)和 Q3(75% 分位数)来定义正常范围。
判定规则:小于 Q1 - 1.5 * IQR 或大于 Q3 + 1.5 * IQR 的点视为异常。
def iqr_detection(data, multiplier=1.5):
"""IQR 异常检测"""
q1 = np.percentile(data, 25)
q3 = np.percentile(data, 75)
iqr = q3 - q1
lower_bound = q1 - multiplier * iqr
upper_bound = q3 + multiplier * iqr
outliers = np.where((data < lower_bound) | (data > upper_bound))[0]
return outliers, (lower_bound, upper_bound)
# 示例
vector = np.array([1.2, 1.5, 1.3, 1.4, 1.6, 15.2, 1.3, 1.5])
outliers, bounds = iqr_detection(vector)
print(f"异常索引: {outliers}") # [5]
print(f"正常范围: [{bounds[0]:.2f}, {bounds[1]:.2f}]")
| 方法 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| Z-score | 正态分布数据 | 计算简单,有统计意义 | 对异常值敏感,要求正态性 |
| IQR | 偏态分布、有离群点 | 鲁棒性强,不依赖分布 | 对尾部数据不敏感 |
4.2 基于距离的方法:LOF(局部异常因子)
统计方法处理一维数据还行,但到了高维向量空间就力不从心了。这时候我一般用 LOF。
LOF 的核心思想:比较一个点周围的「密度」和它邻居的「密度」。如果这个点比邻居稀疏得多,那它就是异常。
为什么会这样?你想想看,正常点周围应该有很多同类,密度高;异常点孤零零的,密度低。LOF 值越大,异常可能性越高。
- LOF ≈ 1:密度和邻居差不多,正常点
- LOF > 1:比邻居稀疏,可能是异常
- LOF < 1:比邻居密集,可能是簇内点
from sklearn.neighbors import LocalOutlierFactor
def lof_detection(data, contamination=0.1, n_neighbors=20):
"""LOF 异常检测"""
lof = LocalOutlierFactor(
n_neighbors=n_neighbors,
contamination=contamination
)
# -1 表示异常,1 表示正常
labels = lof.fit_predict(data)
outliers = np.where(labels == -1)[0]
# 获取 LOF 分数(负值,值越大越异常)
scores = -lof.negative_outlier_factor_
return outliers, scores
# 示例:二维向量异常检测
np.random.seed(42)
normal = np.random.randn(100, 2) * 0.5
outlier = np.array([[5, 5], [4.5, 4.8], [5.2, 4.7]])
data = np.vstack([normal, outlier])
outliers, scores = lof_detection(data)
print(f"检测到 {len(outliers)} 个异常点")
print(f"异常点索引: {outliers[-3:]}") # 最后三个是注入的异常
4.3 基于密度的方法:DBSCAN
DBSCAN 本来是个聚类算法,但它的副产品——噪声点——恰好就是异常值。我个人觉得这是最优雅的方法之一,因为它不需要预先指定簇的数量。
DBSCAN 有两个关键参数:eps(邻域半径)和 min_samples(最小样本数)。如果一个点周围 eps 半径内少于 min_samples 个点,它就被标记为噪声。
from sklearn.cluster import DBSCAN
def dbscan_detection(data, eps=0.5, min_samples=5):
"""DBSCAN 异常检测"""
db = DBSCAN(eps=eps, min_samples=min_samples)
labels = db.fit_predict(data)
# -1 表示噪声(异常)
outliers = np.where(labels == -1)[0]
return outliers, labels
# 示例
np.random.seed(42)
normal = np.random.randn(200, 2) * 0.3
outlier = np.random.randn(5, 2) * 2 + [3, 3]
data = np.vstack([normal, outlier])
outliers, labels = dbscan_detection(data, eps=0.4, min_samples=5)
print(f"检测到 {len(outliers)} 个异常点")
print(f"簇数量: {len(set(labels)) - (1 if -1 in labels else 0)}")
4.4 向量异常值的影响
聊完检测方法,咱们得说说为什么这事这么重要。向量里的异常值到底会造成什么后果?
我总结了三类影响,你看看是不是深有体会:
| 影响类型 | 具体表现 | 严重程度 |
|---|---|---|
| 统计量扭曲 | 均值、方差被拉偏,标准化失效 | 高 |
| 模型训练偏差 | 梯度被异常点主导,收敛到错误方向 | 极高 |
| 相似度计算失真 | 余弦相似度、欧氏距离被异常维度支配 | 中 |
举个例子。假设你有一个 100 维的向量,其中 99 维都是 0.1 左右,但有一维是 100。计算余弦相似度时,这一维会完全主导结果。你想想看,两个向量在其他 99 维上完全一致,就因为这一维不同,相似度就变成 0.01 了——这合理吗?
- 删除:简单粗暴,适合异常比例小(< 5%)的情况
- 截尾:用分位数边界替换异常值,适合保留样本量
- 插补:用均值/中位数/邻近值填充,适合后续需要完整数据
- 变换:取对数或 Box-Cox 变换,压缩异常值的影响
def handle_outliers(vector, method='clip', lower_percentile=1, upper_percentile=99):
"""处理向量中的异常值"""
if method == 'clip':
lower = np.percentile(vector, lower_percentile)
upper = np.percentile(vector, upper_percentile)
return np.clip(vector, lower, upper)
elif method == 'remove':
outliers, _ = iqr_detection(vector)
return np.delete(vector, outliers)
elif method == 'median_fill':
outliers, _ = iqr_detection(vector)
median = np.median(vector)
cleaned = vector.copy()
cleaned[outliers] = median
return cleaned
else:
raise ValueError("不支持的方法")
# 示例
vector = np.array([1.2, 1.5, 1.3, 1.4, 1.6, 15.2, 1.3, 1.5])
print(f"原始: {vector}")
print(f"截尾: {handle_outliers(vector, 'clip')}")
print(f"删除: {handle_outliers(vector, 'remove')}")
print(f"中位数填充: {handle_outliers(vector, 'median_fill')}")
嗯,异常值检测这块就聊到这儿。记住一点:检测方法选对了,后续的清洗工作就轻松一半。别偷懒,多试试不同方法,找到最适合你数据的那一个。