3. 异常值检测:统计方法、基于模型的方法、业务规则
异常值检测,说白了就是在一堆数据里找出那些「不太正常」的样本。刚开始做特征工程时,我总觉得这步可有可无,直到有一次模型上线后预测结果总是跑偏,查了一整天才发现是某个传感器的异常数据在捣鬼。嗯,从那以后我再也不敢跳过异常值检测了。
面试时关于这个话题,面试官通常想看你对三种主流方法的理解和实际应用能力。我个人习惯把这三类方法分开讲,先讲最基础的统计方法,再讲基于模型的方法,最后聊业务规则——因为业务规则往往是面试中容易被忽略但却最能体现经验的地方。
3.1 统计方法:最直观的异常发现
统计方法的核心假设是:正常数据服从某种分布,异常数据会偏离这个分布。你想想看,大部分数据都在均值附近,突然冒出一个离群点,那它大概率就是异常。最常见的统计方法有这几种:
- 3σ 法则:假设数据服从正态分布,超出均值±3倍标准差的数据视为异常
- Z-Score 方法:计算每个样本的 Z 分数,|Z| > 3 时通常是异常
- 四分位距法(IQR):超出 Q1-1.5×IQR 或 Q3+1.5×IQR 的值视为异常
面试高频考点:统计方法的适用前提是数据近似服从正态分布。如果数据偏态严重,3σ 法则效果会很差。我在项目中遇到过用户消费金额数据,长尾分布明显,用 3σ 法则几乎找不到异常,换成 IQR 方法才有效。
用代码实现一下会更好理解:
import numpy as np
import pandas as pd
# 生成示例数据
np.random.seed(42)
data = np.random.normal(0, 1, 1000)
data[0] = 10 # 注入一个极端异常值
# 3σ 法则
mean = np.mean(data)
std = np.std(data)
outliers_3sigma = data[np.abs(data - mean) > 3 * std]
print(f"3σ 法则检测到 {len(outliers_3sigma)} 个异常值")
# IQR 方法
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
outliers_iqr = data[(data < lower_bound) | (data > upper_bound)]
print(f"IQR 方法检测到 {len(outliers_iqr)} 个异常值")
避坑指南:我曾经在一个风控项目中直接用 3σ 法则处理用户行为数据,结果误判了大量正常用户。后来发现数据分布是右偏的,正确的做法是先做对数变换再应用统计方法。
3.2 基于模型的方法:当数据不再「简单」
现实中的数据哪有那么多正态分布?很多时候特征之间的关联关系非常复杂,单维度统计方法就不够用了。这时候就需要基于模型的方法上场。
常见的模型方法包括:
- 孤立森林(Isolation Forest):通过随机分割特征空间,异常点更容易被孤立出来
- LOF(局部异常因子):基于密度的检测,比较样本与其邻居的密度差异
- One-Class SVM:学习正常数据的边界,超出边界即为异常
- 自编码器(Autoencoder):重构误差大的样本可能是异常
| 方法 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 孤立森林 | 高维数据、大规模数据 | 速度快、对维度不敏感 | 对局部异常不够敏感 |
| LOF | 密度分布不均匀的数据 | 能检测局部异常 | 计算复杂度高 |
| One-Class SVM | 样本量不大的场景 | 边界清晰、理论成熟 | 对参数敏感、不适合大数据 |
| 自编码器 | 特征维度高、非线性关系强 | 可以学习复杂模式 | 训练耗时、需要大量正常样本 |
拿孤立森林举个例子——面试官经常问它的原理。它不像距离方法那样算密度,而是随机选特征切一刀,再在子空间里继续切。正常数据需要很多刀才能被孤立,异常数据几刀就孤立了。说白了,异常点就是「不合群,容易被分开」的点。
from sklearn.ensemble import IsolationForest
import numpy as np
# 构造数据:正常数据+异常数据
X_normal = np.random.randn(200, 2)
X_outlier = np.random.uniform(low=-4, high=4, size=(10, 2))
X = np.vstack([X_normal, X_outlier])
# 训练孤立森林
iso_forest = IsolationForest(contamination=0.05, random_state=42)
preds = iso_forest.fit_predict(X)
# -1 表示异常,1 表示正常
outliers_idx = np.where(preds == -1)[0]
print(f"孤立森林检测到 {len(outliers_idx)} 个异常点")
需要注意:模型方法需要假设「正常数据占绝大多数」,contamination 参数(异常比例)需要合理设置。我见过有人把 contamination 设成 0.5,结果模型把一半数据都标为异常,这明显不合理。
我的经验:在工业场景中,我会先用统计方法做一轮粗筛,再用孤立森林做细筛,这样可以兼顾速度和准确率。尤其是在做设备故障检测时,传感器数据波动大,直接用模型方法容易过拟合。
3.3 业务规则:算法搞不定的时候,靠经验
说到这里,很多人会忽略第三类方法——业务规则。但说实话,在真实的生产环境中,很多异常是算法「看」不出来的。比如一个用户年龄填了 200 岁,统计方法可能觉得这只是一个偏大的值,但业务规则直接告诉你:这不合理。
业务规则的常见形式:
- 取值范围约束:年龄 0-150,心率 30-250
- 逻辑约束:注册时间必须早于第一次下单时间
- 频率约束:同一 IP 一天内注册超过 10 个账号
- 组合约束:地址不在配送范围的订单
面试要点:面试官想知道你是否具备业务理解能力。如果只谈算法不谈业务,容易让人觉得你只会「调包」。我建议可以说:「先基于业务经验定义硬性规则,再用统计算法和模型做补充,形成多层过滤的架构。」
业务规则的实现通常很简单,但维护起来需要持续更新:
def business_rule_validation(row):
"""
业务规则验证函数
返回 True 表示正常,False 表示异常
"""
# 规则1:年龄必须在 0-120 之间
if row['age'] < 0 or row['age'] > 120:
return False
# 规则2:消费金额不能为负数
if row['amount'] < 0:
return False
# 规则3:注册时间必须早于首次消费时间
if row['register_time'] > row['first_purchase_time']:
return False
# 规则4:同一设备最近一小时内不能有超过5次登录尝试
if row['login_attempts_last_hour'] > 5:
return False
return True
避坑指南:我曾经给一个金融项目写了十几条业务规则,上线第一天就误判了大量客户。原因是规则太死板了——比如「单笔转账不超过 5 万」,但客户有特殊审批通道。后来我加了一个白名单机制,优先级高于规则。
三类方法的对比如下,面试时可以这样开场:「我个人习惯把它们当作互补的三层防线」:
| 维度 | 统计方法 | 模型方法 | 业务规则 |
|---|---|---|---|
| 复杂度 | 低 | 中高 | 低 |
| 可解释性 | 高 | 中低 | 非常高 |
| 适用场景 | 单维度、简单分布 | 高维度、复杂关联 | 强业务约束 |
| 维护成本 | 低 | 高(需重训练) | 中(需更新规则) |
| 漏报率 | 高(复杂场景) | 低 | 中等 |
| 误报率 | 中等 | 低(调参后) | 中高(规则太严时) |
为什么业务规则的误报率偏高?因为真实世界的边界往往不是非黑即白的。比如「同一设备登录不超过 5 次」,一个业务员用工作手机给 6 个客户演示,就被标记为异常了。所以业务规则最好和统计方法、模型方法配合使用,形成一个投票机制。
3.4 本章知识图谱
下面这张图整理了三种异常值检测方法的核心关系,面试时你可以照着这个思路去组织回答:先分三类,再讲每类的适用条件和优缺点,最后强调组合使用。