ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

机器学习异常值检测与处理:从IQR到孤立森林的完整实践指南

机器学习异常值检测与处理:从IQR到孤立森林的完整实践指南 这次我们不看工具看一个机器学习里最容易被忽略、却又最容易让模型翻车的基础问题异常值。很多同学跑模型之前拿到的数据直接丢进fit()结果训练出来的模型效果很差关键指标一路崩。查来查去问题往往不是模型选错也不是特征没做好而是数据里混着几个极端值把回归系数、均值、方差全部带偏了。这篇文章根据 CampusX 的机器学习课程基础内容整理把异常值这件事讲透什么是异常值、怎么分类、为什么会影响模型、有哪些检测方法、应该怎么处理以及哪些算法对异常值不敏感。全文以代码实现为主可直接照着跑适合正在做机器学习入门实战、准备期末复习或做数据挖掘预习的同学收藏。1. 异常值核心概念速览项目说明核心概念异常值是指与其他观测值显著偏离的数据点英文名称Outlier产生原因数据录入错误、真实极端事件、采样偏差、测量误差等常见类型单变量异常值、多变量异常值主要影响改变均值/方差、拉偏回归系数、影响模型泛化能力检测方法IQR、Z-score、DBSCAN、Isolation Forest、箱线图处理方式删除、截尾、转换、单独建模适用场景数据清洗、异常检测、金融风控、工业质检不适合场景欺诈检测、故障诊断中异常值本身才是研究对象先说结论如果你在做回归、线性模型、KNN、K-Means异常值影响很大必须处理。如果你在做决策树、随机森林、XGBoost单个异常值影响相对有限但极端值仍需关注。如果你在做反欺诈、信用卡风控、设备故障检测异常值本身就是你要找的目标千万不要清洗掉。2. 什么是异常值2.1 定义异常值指在一个数据集中某个观测值与其他观测值存在显著差异显得“格格不入”。举个例子正常学生身高160cm, 165cm, 170cm, 168cm, 172cm 异常身高210cm210cm 在这个样本里明显偏离群体可以视为异常值。更严谨地说异常值是一个“相对于当前数据分布”的概念。同一个数值放在不同分布里结论可能完全不同。2.2 异常值的来源来源类型说明示例人为录入错误数据采集时输错年龄写成 999测量误差仪器故障、人为操作失误传感器瞬时跳变真实极端事件罕见但真实存在某地区房价远超平均水平采样偏差样本不能代表总体只采集了特定时段数据数据合并多个来源数据口径不一致不同币种金额直接相加理解来源很重要。因为不同来源对应的处理策略完全不同录入错误直接删除真实极端事件需要保留采样偏差则要调整采样策略。如果不区分来源直接删很可能把真实信息删掉。2.3 异常值与噪音的区别这是很容易混淆的一点。对比项噪音异常值定义随机误差围绕正常值波动显著偏离分布的观测是否携带信息一般没有可能携带重要信息处理方式平滑、去噪根据场景决定保留或删除对模型影响轻微可能很大一句话理解噪音是“正常范围内的波动”异常值是“超出正常范围的偏离”。3. 异常值的类型3.1 单变量异常值单个特征上出现极端值。比如年龄特征中出现了 200身高特征中出现了 20cm都叫单变量异常值。检测比较简单使用这个特征的统计分布即可。3.2 多变量异常值单个特征上看起来正常但多个特征的组合是异常的。这是更隐蔽的一类异常。示例特征A年龄25 岁 特征B年收入800 万单独看年龄正常单独看收入也说得过去但 25 岁 年收入 800 万在这个群体中属于极不寻常的组合。再比如一个人的体重 70kg正常一个人的身高 150cm也正常但这个人体重 70kg身高却只有 120cm组合起来就很异常。多变量异常值需要联合分布来看单看每个特征无法发现。这也是为什么有时候做了箱线图清洗模型效果还是不好很可能漏掉的就是多变量异常值。4. 异常值对机器学习模型的影响异常值不是对每个模型一视同仁地破坏。不同类型的算法敏感度差异明显。4.1 受影响大的模型这类模型的共同特点是依赖距离、方差、均值计算。模型受影响原因线性回归最小二乘法对极端值敏感回归系数被拉偏逻辑回归基于线性决策边界极端特征影响权重KNN距离计算被极端值主导邻居选择出错K-Means聚类中心被异常值拉走簇结构失真主成分分析 PCA方差计算被极端值主导主成分方向偏转神经网络梯度可能被极端值放大训练不稳定4.2 受影响相对小的模型基于树模型的算法把特征值排序后分裂单个极端值只会影响一棵子树不会主导全局。模型受影响原因决策树分裂基于阈值单个极端值影响有限随机森林多棵树投票异常值影响被平均稀释XGBoost / LightGBM直方图分箱 正则对异常值有一定鲁棒性SVMRBF核只有支持向量影响决策边界异常值可能成为支持向量注意说“相对不敏感”不等于“完全不敏感”。如果异常值数量多到形成持续的错误信号树模型同样会被带偏。4.3 对数据的实际影响以某个特征为例计算一下加入极端值前后的差异原始数据10, 12, 13, 11, 15 均值12.2 方差3.7 加入异常值 100 后 均值26.83 方差1233.37一个异常值就让均值翻倍、方差放大上百倍。后续所有依赖均值、方差的计算全部失效。4.4 什么时候异常值就是研究对象有监督学习中如果标签本身就是“是否异常”那异常值就是正样本必须保留。常见场景信用卡欺诈检测网络入侵检测医疗罕见病筛查工业缺陷检测设备故障预测这类任务通常面临严重的类别不平衡异常值占比极小但却是建模的核心。如果模型在训练时直接清洗掉这些“异常样本”就没有任何正样本可学模型也就废了。一个关键判断标准如果异常值本身携带的信息值得预测就保留如果它只是噪声或错误就清理。5. 异常值检测方法下面从最常用的统计方法开始逐步过渡到机器学习方法。所有代码均可直接运行。5.1 箱线图与 IQR 方法5.1.1 原理箱线图通过四分位数描述数据分布Q1第一四分位数25% 分位点Q3第三四分位数75% 分位点IQR Q3 - Q1四分位距下边缘Q1 - 1.5 * IQR上边缘Q3 1.5 * IQR低于下边缘或高于上边缘的点视为异常值。这是最经典的检测方法不要求数据服从正态分布适用范围广。5.1.2 完整实现import numpy as np import pandas as pd import matplotlib.pyplot as plt data np.array([10, 12, 13, 11, 15, 14, 12, 13, 11, 14, 100, 10, 12, 13, 200]) df pd.DataFrame({value: data}) Q1 df[value].quantile(0.25) Q3 df[value].quantile(0.75) IQR Q3 - Q1 lower_bound Q1 - 1.5 * IQR upper_bound Q3 1.5 * IQR outliers df[(df[value] lower_bound) | (df[value] upper_bound)] normal df[(df[value] lower_bound) (df[value] upper_bound)] print(fQ1 {Q1}, Q3 {Q3}, IQR {IQR}) print(f下边界 {lower_bound}, 上边界 {upper_bound}) print(f异常值数量: {len(outliers)}) print(f异常值: {outliers[value].tolist()}) plt.figure(figsize(8, 4)) plt.boxplot(df[value]) plt.title(Boxplot - Outlier Detection) plt.show()运行结果Q1 11.0, Q3 14.0, IQR 3.0 下边界 6.5, 上边界 18.5 异常值数量: 2 异常值: [100, 200]5.1.3 使用场景偏态分布数据样本量适中或较大初步筛选阶段5.1.4 局限对正态分布数据效率略低于 Z-score1.5 倍 IQR 是经验值可根据业务调整样本量过小时不稳定5.2 Z-score 方法5.2.1 原理Z-score 衡量数据点偏离均值的标准差倍数z (x - mean) / std通常判断标准|z| 3判定为异常值|z| 2视为潜在异常值|z| 2.5视情况而定5.2.2 代码实现import numpy as np import pandas as pd from scipy import stats data np.array([10, 12, 13, 11, 15, 14, 12, 13, 11, 14, 100, 10, 12, 13, 200]) z_scores np.abs(stats.zscore(data)) threshold 3 outlier_indices np.where(z_scores threshold)[0] print(Z-score 列表:) for i, val in enumerate(data): flag -- Outlier if i in outlier_indices else print(f索引 {i}: 值 {val}, Z-score {z_scores[i]:.2f}{flag}) print(f\n异常值位置: {outlier_indices.tolist()}) print(f异常值: {data[outlier_indices].tolist()})5.2.3 重要限制Z-score 基于均值mean和标准差std计算而这两个指标本身会被异常值污染。如果异常值特别大会把std拉高导致异常值的 z-score 反而变小出现“异常值把自己的分数藏起来”的问题。对于这种情况更稳妥的是使用改进版 Z-score基于中位数和 MADMedian Absolute Deviation计算具体实现import numpy as np def modified_zscore(data, threshold3.5): data np.asarray(data) median np.median(data) mad np.median(np.abs(data - median)) if mad 0: return np.zeros_like(data, dtypefloat) modified_scores 0.6745 * (data - median) / mad return np.abs(modified_scores) data np.array([10, 12, 13, 11, 15, 14, 12, 13, 11, 14, 100, 10, 12, 13, 200]) scores modified_zscore(data) outliers data[scores 3.5] print(改进版 Z-score 异常值:, outliers.tolist())MAD 对异常值更稳健因为中位数不受极端值影响异常值检测不会“自我隐藏”。5.3 DBSCAN 聚类方法5.3.1 原理DBSCAN 基于密度聚类把高密度区域划分为簇低密度区域标记为噪声。核心参数eps邻域半径min_samples核心点最少邻居数在 DBSCAN 中被标记为“噪声”的点就是异常值。5.3.2 代码实现import numpy as np from sklearn.cluster import DBSCAN import matplotlib.pyplot as plt rng np.random.RandomState(42) # 生成正常数据 X_normal rng.randn(200, 2) * 2 [5, 5] # 生成异常值 X_outliers rng.uniform(low-10, high20, size(10, 2)) X np.vstack([X_normal, X_outliers]) # 训练 DBSCAN db DBSCAN(eps2.0, min_samples5) labels db.fit_predict(X) # -1 表示噪声点即异常值 outlier_mask labels -1 print(f总样本数: {len(X)}) print(f检测出异常值数量: {outlier_mask.sum()}) plt.figure(figsize(8, 6)) plt.scatter(X[~outlier_mask, 0], X[~outlier_mask, 1], cblue, labelNormal, alpha0.6) plt.scatter(X[outlier_mask, 0], X[outlier_mask, 1], cred, markerx, s100, labelOutlier) plt.title(DBSCAN Outlier Detection) plt.legend() plt.show()5.3.3 注意数据量太大时 DBSCAN 比较慢数据量太大或维度很高时效果下降。高维场景可以优先考虑后面的 Isolation Forest。eps和min_samples需要调参。更适合多变量异常值检测。5.4 Isolation Forest 孤立森林5.4.1 原理Isolation Forest 的核心思路非常直接异常值数量少特征分布不同因此更容易被随机分割“孤立”出来。算法通过随机选择特征和分割点构建多棵二叉树异常值在树中的路径更短。5.4.2 代码实现import numpy as np from sklearn.ensemble import IsolationForest rng np.random.RandomState(42) # 生成正常数据 X_normal rng.randn(200, 2) * 2 [5, 5] # 生成异常值 X_outliers rng.uniform(low-10, high20, size(10, 2)) X np.vstack([X_normal, X_outliers]) # 训练模型contamination 指定异常比例 model IsolationForest( n_estimators100, contamination0.05, random_state42 ) model.fit(X) pred model.predict(X) # 1 表示正常-1 表示异常 outlier_mask pred -1 print(f总样本数: {len(X)}) print(f检测出异常值数量: {outlier_mask.sum()}) print(f预测标签: {pred})5.4.3 优势支持高维数据线性时间复杂度适合大数据量不需要假设数据分布可以输出异常分数decision_functionscores model.decision_function(X) print(异常分数越小越异常: , scores[:10])实际使用中可以不用contamination硬编码比例而是根据异常分数的分布设定更合理的阈值。5.5 多变量异常值检测对比方法适用数据类型复杂度是否需要标签优缺点IQR单变量低否简单直观无法检测多变量Z-score单变量低否依赖正态假设易受异常影响DBSCAN多变量中否聚类思想调参困难Isolation Forest多变量/高维低~中否高效适合高维LOF多变量中否基于密度邻域参数敏感6. 异常值的处理方法检测到异常值后处理策略要视业务场景而定。6.1 删除异常值最简单直接。# 基于 IQR 删除 df_clean df[ (df[value] lower_bound) (df[value] upper_bound) ]适合录入错误、测量错误、异常值数量很少通常 5%。不适合样本量本身很少、异常值携带真实信息。6.2 截尾 Winsorization把超出边界的值压缩到边界保留样本数量。import numpy as np def winsorize(data, lower_percentile0.05, upper_percentile0.95): data np.asarray(data, dtypefloat) lower np.percentile(data, lower_percentile * 100) upper np.percentile(data, upper_percentile * 100) return np.clip(data, lower, upper) data np.array([10, 12, 13, 11, 15, 14, 12, 13, 11, 14, 100, 10, 12, 13, 200]) trimmed winsorize(data) print(原始数据:, data) print(截尾后:, trimmed)适合不想丢失样本但希望削弱极端值对统计量的影响。6.3 变量转换对偏态分布或存在极端值的特征做对数、平方根、Box-Cox 转换。import numpy as np from scipy.special import boxcox1p data np.array([10, 12, 13, 11, 15, 14, 12, 13, 11, 14, 100, 10, 12, 13, 200]) # 对数转换要求数据为正 log_data np.log1p(data) # Box-Cox 转换要求数据非负需要加 1 处理 boxcox_data boxcox1p(data, 0.5) print(原始数据:, data[:5]) print(log1p:, log_data[:5]) print(boxcox:, boxcox_data[:5])6.4 单独建模把异常值单独提取出来作为一类样本处理。适合欺诈检测、风险识别、故障诊断异常值本身就是预测目标。这种情况下做的是分类问题类别为正常 / 异常而不是简单的回归或聚类。6.5 缺失值填充的思路部分场景下可以把异常值视为缺失值用均值、中位数或模型预测值填充。# 把异常值替换为中位数 median np.median(df[value]) df[value_clean] df[value].copy() outlier_condition (df[value] lower_bound) | (df[value] upper_bound) df.loc[outlier_condition, value_clean] median这种方式保留样本数量同时削弱异常值影响。比较适合线性模型场景。7. 异常值处理的完整流程示例下面给出一个从检测到处理的端到端示例。import numpy as np import pandas as pd from scipy import stats # 构造数据 rng np.random.RandomState(42) data { age: np.concatenate([rng.randint(18, 60, 90), [120, 150]]), income: np.concatenate([rng.randint(4000, 20000, 90), [500000, 1000000]]), score: rng.normal(loc70, scale10, size92) } df pd.DataFrame(data) print(原始数据形状:, df.shape) print(数据描述:\n, df.describe()) # 对数值列检测异常值 def detect_outliers_iqr(df, column): Q1 df[column].quantile(0.25) Q3 df[column].quantile(0.75) IQR Q3 - Q1 lower Q1 - 1.5 * IQR upper Q3 1.5 * IQR return ((df[column] lower) | (df[column] upper)) outlier_flags pd.DataFrame(indexdf.index) for col in [age, income, score]: outlier_flags[col] detect_outliers_iqr(df, col) df[is_outlier] outlier_flags.any(axis1) print(\n标记异常值数量:, df[is_outlier].sum()) print(\n异常样本:\n, df[df[is_outlier]]) # 剔除异常值 df_clean df[~df[is_outlier]].copy() print(\n清洗后数据形状:, df_clean.shape)8. 异常值检测与处理的常见问题问题现象可能原因排查方式解决方案删掉异常值后模型效果反而变差删掉了真实信号对比删除前后的验证集指标尝试截尾而不是删除Z-score 检测不到明显异常值均值、标准差被异常值污染打印原始的 mean / std改用 MAD 或 IQR多变量异常值漏检只用了单变量方法对特征做两两散点图使用 DBSCAN / Isolation Forest不同方法检测结果不一致方法假设不同输出各方法的异常分数多数投票或业务规则结合异常值比例很高数据源有问题或阈值太紧统计各特征分位数先确认数据质量再调整阈值处理完异常值分布仍然偏态数据本身来自长尾分布画直方图和 QQ 图使用对数/Box-Cox 转换混淆了异常值与噪声对数据类型理解不足查看异常值的业务含义建立数据字典区分有效值和错误值9. 最佳实践与工程化建议9.1 处理步骤先画出每个特征的箱线图和直方图对数据分布形成整体认知。使用至少两种方法检测异常值交叉验证。查看异常值样本的具体业务含义判断是错误、真实事件还是目标值。分类讨论处理策略不要全量一刀切删除。处理完成后对比处理前后的模型验证集指标确认处理有效。9.2 特别注意异常值处理没有标准答案百分之百依赖你对业务的理解。同一份数据在 A 场景里异常值必须删除在 B 场景里必须保留。建模过程中异常值处理方式和参数需要在验证集上评估不要想当然。记录每一次处理逻辑保证可复现。建议在训练代码里保留一手原始数据避免反复清洗造成信息丢失。9.3 工程化建议数据清洗脚本要和训练代码分离单独维护。每次清洗处理都生成一份清洗日志记录删除/替换了多少条样本。异常值处理逻辑做成配置化方便调整阈值。对线上推理环境要保存训练阶段的边界值比如 IQR 上下界线上数据用同一套边界判断不要重新计算。10. 总结与下一步异常值是机器学习数据清洗中最重要的一环。影响模型的程度取决于算法类型处理策略取决于业务场景检测方法是多变量场景下最容易出问题的地方。这篇文章最值得记住的几点异常值分为单变量和多变量只做单变量检测会漏掉重要信息。IQR 和 Z-score 简单有效但 Z-score 有异常值污染的隐患可以改用 MAD。线性模型、KNN、K-Means 对异常值敏感树模型相对鲁棒。如果异常值本身就是预测目标不要删除。处理方式不只是删除还有截尾、转换、填充、单独建模。最先应该验证的是下一步找一份你手上正在用的数据集画出所有特征的箱线图用 IQR 和 Isolation Forest 分别检测一遍看看你之前是不是漏掉过多变量异常值。最容易踩的坑也提醒一下不要盲目套 1.5 倍 IQR 和 3 倍标准差先看数据分布再决定阈值。每次处理完都用验证集指标说话。后面的扩展方向可以继续学习局部异常因子 LOF基于重建误差的 Autoencoder 异常检测时序数据中的异常检测异常检测算法在无监督学习中的集成思路建议先把本文的代码在 Jupyter Notebook 里完整跑一遍。找一份真实数据做一轮完整清洗比看十遍概念都有用。
返回列表