异常值处理:IQR、Z-score与业务规则

数据预处理里,异常值处理是个绕不开的活儿。我接手过的风控项目,十有八九原始数据都带着“脏东西”——不是字段值离谱,就是分布形态诡异。如果不处理,后续的变量衍生和分箱效果都会大打折扣。

说白了,异常值就是那些“格格不入”的数据点。它们可能是录入错误、设备故障,也可能是真实的金融欺诈行为。怎么区分?这就是今天要聊的内容。

一、IQR法:中位数的“铁杆护卫”

我个人习惯先用IQR法做第一轮粗筛。为什么?因为它对极端值不敏感,鲁棒性强。

IQR的计算超简单:

  • 把数据从小到大排序
  • 找到下四分位数Q1(25%分位点)和上四分位数Q3(75%分位点)
  • IQR = Q3 - Q1
  • 正常范围:[Q1 - 1.5×IQR, Q3 + 1.5×IQR]

在这个区间之外的值,统统视为异常。

核心经验:在金融数据里,我通常会把系数从1.5放宽到3.0。因为很多变量的分布本身就是偏态的,比如“近30天交易金额”,高价值客户的数据天然就会拉长右尾。系数调成3.0后,误杀率会明显降低。

# Python 示例:IQR 异常值检测
import numpy as np

def iqr_outlier_detection(data, multiplier=1.5):
    Q1 = np.percentile(data, 25)
    Q3 = np.percentile(data, 75)
    IQR = Q3 - Q1
    lower_bound = Q1 - multiplier * IQR
    upper_bound = Q3 + multiplier * IQR
    outliers = (data < lower_bound) | (data > upper_bound)
    return outliers, lower_bound, upper_bound

# 实战例子
sample_data = np.array([100, 150, 200, 220, 250, 300, 350, 2000])
outliers, lb, ub = iqr_outlier_detection(sample_data, multiplier=1.5)
print(f"正常范围: [{lb:.2f}, {ub:.2f}]")
print(f"异常值索引: {np.where(outliers)[0]}")

避坑指南:我曾经在“近90天最大逾期天数”这个变量上用了默认的1.5倍IQR,结果把一批逾期30天的次级客户全标记成了异常,流失了一个优质产品线。后来我意识到——在金融风控中,有些“异常”正是风险识别的关键信号,不能一刀切。

二、Z-score法:正态假设下的“数学正义”

Z-score的思路很直白:看某个数据点距离均值有多少个标准差。

公式:Z = (x - μ) / σ

一般取|Z| > 3 作为异常阈值。为什么是3?因为正态分布下,超出3个标准差的数据只占0.3%左右,足够稀有了。

⚠️ 注意:Z-score对极端值非常敏感!如果数据里已经存在几个极端大值,均值和标准差会被严重拉偏,导致正常的点也被判为异常。这就是所谓的“掩盖效应”——你想想看,一个巨大异常值把均值拖跑了,其他点反而显得“正常”了。

我在项目中遇到过一个问题:一个“用户月交易笔数”的字段,按理说Z-score很稳定。但仔细一看,很多正常用户的笔数也被标记为异常——后来发现是因为新用户注册后刚激活交易,前几天的数据量自然小,却被“数学正义”制裁了。

# Python 示例:Z-score 异常值检测
from scipy import stats

def zscore_outlier_detection(data, threshold=3):
    z_scores = np.abs(stats.zscore(data))
    outliers = z_scores > threshold
    return outliers, z_scores

z_outliers, z_scores = zscore_outlier_detection(sample_data)
print(f"Z-score异常值索引: {np.where(z_outliers)[0]}")

一个实际对比:对于同一个“近6个月月均收入”字段,IQR法筛出了12个异常,Z-score法筛出了7个。原因是该字段有少量极高收入客户(年入千万),拉高了均值,Z-score反而“放过”了部分合理的偏高值。所以,两个方法的结果一定要交叉验证。

三、业务规则法:不讲道理,只看现实

做金融风控这么多年,我越来越觉得:纯统计方法只是工具,业务知识才是灵魂。

举个例子:一个客户的“年龄”字段显示为-5岁,IQR和Z-score可能因为样本量大会把它当成正常——但业务上这就是个无效值。又比如“单笔交易金额”超过了监管规定的上限(比如10万美元),无论统计上是否异常,都必须标记。

业务规则法的核心逻辑:

  • 硬性阈值:监管要求、产品上限、物理约束(如年龄0-120岁)
  • 逻辑一致性:“授信额度”不能小于“已用额度”;“贷款发放日期”不能晚于“首期还款日期”
  • 业务经验:“月交易笔数”超过1000笔在中小企业贷里是合理的,但在消费贷里就是异常信号

我最怕那种全依赖统计方法的团队。他们做出来的模型在测试集上表现很好,一上线就崩——因为真实的业务异常模式,统计方法根本没见过。

血泪教训:我见过太多“拍脑袋”的阈值了。比如某个团队直接把“交易金额超过100万就是异常”写死了,结果发现高端理财客户的单笔交易经常在200万以上,这个规则直接把VIP客户全干掉了。业务规则必须跟着业务跑,定期review。

# 业务规则示例:年龄、交易金额、逻辑一致性
def business_rule_check(row):
    errors = []
    # 硬性约束
    if row['age'] < 0 or row['age'] > 120:
        errors.append('年龄超出合理范围')
    # 监管约束
    if row['transaction_amount'] > 100000:  # 假设10万是监管上限
        errors.append('单笔交易金额超过监管上限')
    # 逻辑一致性
    if row['credit_limit'] < row['used_limit']:
        errors.append('已用额度超过授信额度')
    return errors

四、三种方法的“选型指南”

方法 适用场景 核心参数 优点 缺点
IQR 偏态分布、有边界值的变量 IQR乘数(建议1.5~3.0) 鲁棒性强,不受极端值影响 对分布对称性有要求,太偏态会误判
Z-score 近似正态分布的变量 阈值(通常取3) 计算简单,标准化后可比较 对极端值敏感,有掩盖效应
业务规则 监管、逻辑、物理约束 业务定义的硬性阈值 解释性强,风险可控 无法覆盖所有异常模式

实际项目中,我通常的流程是这样:先跑业务规则,把“肉眼可见的错误”清理掉;然后用IQR做粗筛,把统计上的异常圈出来;最后对Z-score结果做交叉验证。三种方法如果同时标记某个样本为异常,那基本可以确认有问题。

但这就够了吗?显然不行。异常值处理完之后,还有一个关键动作——记录处理逻辑。我曾经因为没有记录“为什么删掉了某笔数据”,导致模型上线后效果回滚时找不到根因,整整排查了两周。嗯,这里要注意:

  • 每次异常标记都要存log
  • 保留原始值和处理后的值
  • 记录使用的方法和参数

这样一来,后续的数据审计、模型排查、业务验收都能有条不紊。

异常值处理流程框架图 异常值处理三种方法并行流程 原始数据集 IQR 方法 Q1-1.5×IQR / Q3+1.5×IQR Z-score 方法 |Z| > 3 为异常 业务规则方法 硬性阈值+逻辑一致性 交叉验证 + 业务判断 标准数据集 建议流程:先跑业务规则 → IQR粗筛 → Z-score交叉验证 → 人工复核

最后说一句:异常值处理没有银弹。不同场景、不同变量、不同数据质量,选择的方法千差万别。我现在的习惯是——拿到一份新数据,先画分布图,再看业务含义,最后才选工具。工具永远只是工具,理解数据才是核心。