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 本章知识图谱

下面这张图整理了三种异常值检测方法的核心关系,面试时你可以照着这个思路去组织回答:先分三类,再讲每类的适用条件和优缺点,最后强调组合使用。

异常值检测三类方法 输入数据 统计方法 基于模型的方法 业务规则 3σ 法则 Z-Score IQR 方法 孤立森林 LOF One-Class SVM 取值范围约束 逻辑约束 频率约束 最佳实践:三层组合过滤 第一层:业务规则快速排除明显异常 第二层:统计方法处理常规分布数据 第三层:模型方法处理复杂关联场景