4. 异常值检测:五把利刃,斩断数据中的“害群之马”
各位好,我是老蓝。咱们做金融数据清洗,说白了就是跟各种“脏数据”斗智斗勇。今天聊的异常值检测,就是找出那些明显偏离大部队的“害群之马”。
你想想看,一笔交易金额突然比平时高了100倍,或者一个客户的年龄填了200岁,这些数据如果不处理,模型跑出来就是一堆废纸。我个人习惯,在清洗流程里,异常值检测是必须前置的一步。
今天咱们就聊聊五把常用的“利刃”:3σ原则、箱线图法、IQR方法、Z-Score方法,还有DBSCAN聚类检测。每把刀都有它的脾气,用对了地方才能事半功倍。
核心逻辑:异常值检测的本质,就是定义一个“正常范围”,落在范围外的,就是异常。不同方法,只是定义这个“正常范围”的方式不同。
4.1 3σ原则:正态分布下的“铁律”
3σ原则,也叫拉依达准则。它假设数据服从正态分布,那么99.7%的数据会落在均值±3个标准差范围内。超出这个范围的,就是异常。
我在项目中遇到过,用这个方法处理银行日均交易量数据,效果特别好。因为交易量数据基本符合正态分布,那些突然暴增的“黑天鹅”交易,一抓一个准。
我的经验:3σ原则对数据分布要求严格。如果数据明显偏态,比如收入数据,用这个方法会误杀很多正常值。我一般先用直方图看一眼分布形态,再决定用不用。
import numpy as np
import pandas as pd
def detect_outliers_3sigma(data):
"""
3σ原则异常值检测
data: pandas Series
"""
mean = data.mean()
std = data.std()
lower_bound = mean - 3 * std
upper_bound = mean + 3 * std
outliers = data[(data < lower_bound) | (data > upper_bound)]
return outliers, lower_bound, upper_bound
# 示例
data = pd.Series([10, 12, 11, 13, 12, 11, 10, 12, 100, 11])
outliers, lb, ub = detect_outliers_3sigma(data)
print(f"正常范围: [{lb:.2f}, {ub:.2f}]")
print(f"异常值: {outliers.tolist()}")
4.2 Z-Score方法:标准化的“度量衡”
Z-Score说白了,就是把数据标准化到同一个尺度上。每个数据点减去均值,再除以标准差,得到的Z值就代表它偏离均值多少个标准差。
通常,|Z| > 3 的被认为是异常值。这和3σ原则本质一样,但Z-Score更灵活——你可以调整阈值。比如在风控场景,我经常把阈值设成2.5,宁可错杀也不放过。
避坑指南:我曾经用Z-Score处理一个包含极端值的客户资产数据,结果发现均值被“拉偏”了,导致很多正常值被判为异常。后来我改用中位数和MAD(绝对中位差)来替代均值和标准差,效果好了很多。
from scipy import stats
def detect_outliers_zscore(data, threshold=3):
"""
Z-Score异常值检测
"""
z_scores = np.abs(stats.zscore(data))
outliers = data[z_scores > threshold]
return outliers, z_scores
# 示例
data = pd.Series([10, 12, 11, 13, 12, 11, 10, 12, 100, 11])
outliers, zs = detect_outliers_zscore(data)
print(f"Z-Scores: {zs}")
print(f"异常值: {outliers.tolist()}")
4.3 箱线图法与IQR方法:不依赖分布的“稳健派”
箱线图法和IQR方法,是我个人最常用的组合。它们不假设数据分布,用四分位数来定义正常范围。
IQR = Q3 - Q1(上四分位数减下四分位数)。正常范围是 [Q1 - 1.5*IQR, Q3 + 1.5*IQR]。超出这个范围的,就是异常值。
你想想看,为什么是1.5倍?这个系数是统计学家的经验值。在金融领域,我有时候会把它调成3倍,因为金融数据的波动本来就大,1.5倍太敏感了。
核心优势:箱线图法对偏态分布和存在极端值的数据非常稳健。它不受少数极端值的影响,因为四分位数本身就有抗干扰性。
def detect_outliers_iqr(data, multiplier=1.5):
"""
IQR方法异常值检测
"""
Q1 = data.quantile(0.25)
Q3 = data.quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - multiplier * IQR
upper_bound = Q3 + multiplier * IQR
outliers = data[(data < lower_bound) | (data > upper_bound)]
return outliers, lower_bound, upper_bound, IQR
# 示例
data = pd.Series([10, 12, 11, 13, 12, 11, 10, 12, 100, 11])
outliers, lb, ub, iqr = detect_outliers_iqr(data)
print(f"IQR: {iqr}")
print(f"正常范围: [{lb:.2f}, {ub:.2f}]")
print(f"异常值: {outliers.tolist()}")
4.4 DBSCAN聚类检测:高维数据的“侦察兵”
前面几种方法,都是针对单维数据的。但金融数据往往是高维的——比如同时看交易金额、交易时间、交易频率。这时候,DBSCAN就派上用场了。
DBSCAN是一种基于密度的聚类算法。它把数据点分成三类:核心点、边界点、噪声点。噪声点就是我们要找的异常值。
我记得有一次处理信用卡欺诈数据,用IQR方法只能找出金额异常的,但DBSCAN能同时发现“金额正常但交易频率异常”的欺诈行为,这才是真正的高手。
参数调优:DBSCAN有两个关键参数:eps(邻域半径)和min_samples(最小样本数)。eps太小会把正常点判为异常,太大又会漏掉异常。我一般先用K距离图来估算eps,再根据业务场景调整。
from sklearn.cluster import DBSCAN
from sklearn.preprocessing import StandardScaler
def detect_outliers_dbscan(data, eps=0.5, min_samples=5):
"""
DBSCAN异常值检测
data: DataFrame,每列是一个特征
"""
# 标准化
scaler = StandardScaler()
data_scaled = scaler.fit_transform(data)
# DBSCAN聚类
db = DBSCAN(eps=eps, min_samples=min_samples)
labels = db.fit_predict(data_scaled)
# 标签为-1的是噪声点(异常值)
outliers = data[labels == -1]
return outliers, labels
# 示例:二维数据
import pandas as pd
data = pd.DataFrame({
'amount': [100, 102, 98, 101, 99, 500, 103, 97, 100, 600],
'frequency': [5, 4, 5, 6, 5, 1, 4, 5, 6, 2]
})
outliers, labels = detect_outliers_dbscan(data, eps=0.3, min_samples=2)
print(f"异常值索引: {outliers.index.tolist()}")
print(f"异常值数据:\n{outliers}")
4.5 方法对比与选择建议
说了这么多,到底该用哪个?我整理了一个对比表,方便你快速决策。
| 方法 | 适用场景 | 优点 | 缺点 | 我的推荐指数 |
|---|---|---|---|---|
| 3σ原则 | 正态分布数据 | 简单直观,计算快 | 对偏态数据不友好 | ⭐⭐⭐ |
| Z-Score | 标准化后的数据 | 阈值可调,灵活 | 受极端值影响大 | ⭐⭐⭐⭐ |
| 箱线图法 | 任意分布数据 | 稳健,不依赖分布 | 对少量异常不敏感 | ⭐⭐⭐⭐⭐ |
| IQR方法 | 任意分布数据 | 计算简单,解释性强 | 系数选择凭经验 | ⭐⭐⭐⭐⭐ |
| DBSCAN | 高维、复杂形状数据 | 能发现簇状异常 | 参数调优复杂 | ⭐⭐⭐⭐ |
我的实战建议:
- 第一步:先用箱线图或IQR方法快速筛查单维异常值,这能解决80%的问题。
- 第二步:如果数据分布接近正态,用3σ原则或Z-Score做交叉验证。
- 第三步:对于高维数据或复杂场景,上DBSCAN。但记得先做特征标准化。
- 第四步:永远不要完全依赖算法。把检测结果交给业务人员审核,他们最懂数据。
嗯,异常值检测这块,说白了就是“先粗筛,再精查”。没有万能的方法,只有最适合你数据的方法。多试几次,你就能找到感觉了。
公众号:蓝海资料掘金营,微信deep3321