机器学习中的异常值:检测方法与处理实践
2026/9/7 13:12:10 网站建设 项目流程

机器学习中的异常值:它为什么重要、怎么检测、如何处理

做机器学习的时间越长,越会发现一个反直觉的事实:很多时候,决定模型上限的不是算法多先进,而是数据够不够干净。而数据干不干净,第一关就是有没有异常值

你可能遇到过这种场景:用一份销售数据训练回归模型,R² 只有 0.6,怎么看都不对。后来画了个散点图,发现右上角有一个点,金额比其他数据高了几百倍。删掉它之后,R² 直接涨到 0.9。这就是异常值在起作用——它可能只占数据的 0.1%,却能把模型整个带偏。

这篇文章就把异常值这件事讲透:什么是异常值,为什么它会严重影响机器学习模型,有哪些成熟的检测方法,以及检测出来之后该怎么处理。文章会提供基于 Python 和 scikit-learn 的完整示例代码,并用一个实际数据集跑通全流程。如果你是正在入门机器学习的学生,或者做数据分析时经常被脏数据折磨的工程师,这篇文章值得收藏。

1. 什么是异常值:一个容易混淆的概念

先给一个严格定义。异常值(Outlier)是指在一个数据集中,与其他观测值显著不同的数据点。它可能是某个字段取值极端,也可能是多个字段组合起来违反常规模式。

这里要区分两个概念:

  • 异常值:数据集中的极端值,可能由数据录入错误、传感器故障、真实稀有事件等产生。它属于“样本级”问题。
  • 噪声(Noise):数据中随机的、微小的波动。噪声是普遍存在的,建模时往往希望模型忽略这种随机波动,但不希望删除它们,因为噪声里也包含信息。

举个直观例子:人的身高数据,大部分在 150cm 到 190cm 之间,突然出现一个 350cm 的记录,这就是异常值;而每个人的身高在两天内误差 0.5cm,这是噪声。

再看机器学习中常见的情况。假设你在做一个信用卡欺诈检测项目,正常交易金额集中在几十到几千元,某笔交易金额是 10 万元,可能就是不正常的。但问题是,这笔 10 万元的交易也有可能是真实的高消费。异常值并不是绝对的错误,它有可能代表真实的极端事件。

所以,异常值处理的第一个原则是:先区分它是错误、是稀有事件,还是本来就应该存在的长尾。这决定了后续是删除、修正,还是单独建模。

2. 为什么异常值会严重干扰机器学习模型

很多经典机器学习算法都基于“最小化误差”或者“统计分布假设”来学习。异常值一旦存在,会从多个层面破坏学习过程。

2.1 拉偏损失函数,导致模型偏移

线性回归、逻辑回归这类模型使用梯度下降优化损失函数。损失函数通常会计算预测值与真实值的平方误差,例如 MSE。平方误差对极端值极度敏感:如果一个正常点的误差是 2,平方后是 4;一个异常点的误差是 20,平方后是 400。优化器会优先去降低这个 400 的误差,于是整个回归线被异常点拉过去,正常点反而不准了。

2.2 破坏数据分布假设

很多统计检验、贝叶斯方法、高斯朴素贝叶斯模型,都假设数据服从正态分布或近似正态分布。一个异常值就可能把均值拉偏、把方差变大,从而让整个分布参数失真。

例如,一组数据 [10, 12, 11, 13, 120],均值为 33.2,中位数是 12。均值被 120 明显拉高,但中位数几乎不受影响。如果你用均值做标准化或填补缺失值,后果可想而知。

2.3 影响树模型的切分点

决策树、随机森林、XGBoost 等树模型对异常值有一定鲁棒性,因为它们基于分裂阈值做判断,而不是基于距离。但如果异常值过于极端,仍可能影响分裂点的选择,尤其当特征只有一个或少数几个时,极端值会让树模型尽量拟合它,导致过拟合。

2.4 影响特征缩放和距离计算

K-Means、KNN、SVM(RBF 核)、PCA 等算法依赖样本间的距离或方差。在做标准化时,异常值会让均值和标准差失真,导致正常样本被压缩到一个很窄的区间,距离计算失去区分度。

因此,异常值处理不是可有可无的预处理步骤,而是影响模型上限的关键环节。实际项目中,很多调试工作最后都回到清洗数据这一步。

3. 异常值检测的常用方法:从统计到机器学习

检测异常值的方法很多,没有一种万能方法。实际中往往组合使用。下面按难度从低到高介绍几类主流方法。

3.1 基于统计与分布的方法

Z-Score(标准分数)

假设特征近似正态分布。Z-Score 表示一个样本距离均值有多少个标准差:

z = (x - μ) / σ

通常认为 |z| > 3 的点为异常值(在正态分布下,超出 3 个标准差的概率约为 0.27%)。

但 z-score 的问题很明显:如果异常值本身拉高了均值和标准差,z-score 会被“稀释”,导致异常值不被识别。所以它适合异常值不多且分布较正态的情况。

IQR(四分位距)

IQR 方法基于分位数,对偏态分布更稳健。定义:

Q1 = 25% 分位数 Q3 = 75% 分位数 IQR = Q3 - Q1 下限 = Q1 - 1.5 * IQR 上限 = Q3 + 1.5 * IQR

超出上下限的点视为异常值。系数 1.5 是经验值,可以按业务场景调整。IQR 方法不要求正态分布,所以是EDA中最常用的方法之一。

3.2 基于密度的方法

代表算法是DBSCANLOF(局部离群因子)

DBSCAN 通过密度连通性将数据划分为簇,落在低密度区域的点被标记为噪声点。它不需要预先指定簇数量,也能发现任意形状的簇,但参数eps(邻域半径)和min_samples需要调参。

LOF 为每个点计算局部密度,与其邻居的局部密度比较。如果一个点的局部密度明显低于邻居,它就是一个局部异常值。LOF 可以检测局部异常,适合全局分布不均匀的数据。

3.3 基于距离的方法

KNN 也可以用于异常检测。一个点如果离它的 k 个最近邻居都很远,就可能是异常值。scikit-learn 提供了NearestNeighbors,可以轻松计算每个点到第 k 近邻的距离,然后设置阈值筛选。

这种方法的缺点是计算量较大,适合小规模数据集。

3.4 基于树的方法:Isolation Forest

Isolation Forest(孤立森林)是周志华老师团队提出的高效异常检测算法。它的核心思想不是描述正常样本,而是直接“隔离”异常样本。

算法流程:

  1. 随机选择一个特征。
  2. 在特征值范围内随机选择一个切分值。
  3. 递归切分数据,直到每个点被单独隔离或达到树深度限制。
  4. 异常值通常更容易被隔离,所需切分次数更少;正常样本需要更多次切分才能被隔离。

所以,一个样本在所有树中的平均路径长度越短,异常程度越高。孤立森林对高维数据表现较好,且复杂度接近线性,适合大规模数据集。

3.5 基于分类的方法:One-Class SVM

One-Class SVM 试图学习一个围绕正常数据的边界,位于边界之外的数据视为异常。它适合数据分布较紧密的情况,但核函数参数需要调优,在高维稀疏数据上表现一般。

下面用一个表格总结这些方法的适用场景:

方法假设/原理适用场景注意事项
Z-Score正态分布,距离均值超过3σ单维、近似正态分布异常值会稀释均值/方差
IQR分位数,超出1.5倍IQR范围单维、偏态分布,EDA阶段对双峰分布可能误判
DBSCAN密度连通,低密度点为异常多维、任意形状簇eps和min_samples需要调参
LOF与邻居局部密度对比局部异常、分布不均匀高维下距离失真
Isolation Forest异常更容易被隔离多维、大规模、无监督随机性,可设置随机种子
One-Class SVM学习正常数据边界数据整体紧凑核函数参数敏感

4. 实验准备与数据集说明

下面我们用完整代码演示异常值的检测与处理。环境使用 Python 3.9+,依赖库如下:

  • numpy
  • pandas
  • matplotlib
  • scikit-learn

可以使用 pip 安装:

pip install numpy pandas matplotlib scikit-learn

本文使用 scikit-learn 内置的California Housing(加州房价)数据集。这个数据集包含 8 个特征,如收入中位数、房龄、房间数等,目标变量是房价中位数。该数据集比较干净,但我们仍然可以人为地注入一些异常值,然后观察检测效果,这比直接使用带脏数据的数据集更容易验证正确性。

当然,如果你有自己的数据集,也可以直接替换成自己的 DataFrame。核心思路完全一样。

5. 异常值检测完整代码实现

下面直接给出一份完整的 Python 脚本,实现以下功能:

  1. 加载数据,查看基本统计信息。
  2. 人为注入异常值。
  3. 分别用 Z-Score、IQR、Isolation Forest 检测异常值。
  4. 用可视化和指标对比检测结果。
# 文件路径:outlier_detection_demo.py import numpy as np import pandas as pd import matplotlib.pyplot as plt from sklearn.datasets import fetch_california_housing from sklearn.ensemble import IsolationForest # 解决中文显示问题(macOS/Linux/Windows 可能需要设置字体,这里按需调整) plt.rcParams['font.sans-serif'] = ['SimHei', 'Arial Unicode MS'] plt.rcParams['axes.unicode_minus'] = False # 1. 加载数据 data = fetch_california_housing() df = pd.DataFrame(data.data, columns=data.feature_names) df['target'] = data.target # 2. 人为注入异常值:选择特征 MedInc(收入中位数),把20个点改成极大值 np.random.seed(42) n_outliers = 20 outlier_idx = np.random.choice(df.shape[0], size=n_outliers, replace=False) df.loc[outlier_idx, 'MedInc'] = df['MedInc'].max() + np.random.uniform(10, 50, size=n_outliers) print("注入异常值后 MedInc 的描述性统计:") print(df['MedInc'].describe()) # 3. 使用 Z-Score 检测异常 from scipy import stats z_scores = np.abs(stats.zscore(df['MedInc'])) z_threshold = 3 z_outliers = df[z_scores > z_threshold].index.tolist() print(f"\nZ-Score 检测到异常值数量:{len(z_outliers)}") # 4. 使用 IQR 检测异常 Q1 = df['MedInc'].quantile(0.25) Q3 = df['MedInc'].quantile(0.75) IQR = Q3 - Q1 lower_bound = Q1 - 1.5 * IQR upper_bound = Q3 + 1.5 * IQR iqr_outliers = df[(df['MedInc'] < lower_bound) | (df['MedInc'] > upper_bound)].index.tolist() print(f"IQR 检测到异常值数量:{len(iqr_outliers)}") # 5. 使用 Isolation Forest 检测异常(多特征,所以用多个相关特征) features = ['MedInc', 'HouseAge', 'AveRooms', 'AveOccup', 'target'] X = df[features] iso_forest = IsolationForest( n_estimators=100, max_samples='auto', contamination=0.05, # 预估异常值比例 random_state=42 ) iso_forest.fit(X) # predict 返回 1 表示正常,-1 表示异常 pred = iso_forest.predict(X) iso_outliers = np.where(pred == -1)[0] print(f"Isolation Forest 检测到异常值数量:{len(iso_outliers)}") # 6. 可视化对比:只画 MedInc 与 target 的散点图 fig, axes = plt.subplots(1, 3, figsize=(15, 4)) # 原始数据 axes[0].scatter(df['MedInc'], df['target'], s=5, alpha=0.6) axes[0].set_title('原始数据') axes[0].set_xlabel('MedInc') axes[0].set_ylabel('target') # Z-Score 标记异常 axes[1].scatter(df['MedInc'], df['target'], s=5, alpha=0.6) axes[1].scatter(df.loc[z_outliers, 'MedInc'], df.loc[z_outliers, 'target'], color='red', s=20, label='Z-Score Outliers') axes[1].set_title('Z-Score 检测') axes[1].set_xlabel('MedInc') axes[1].legend() # Isolation Forest 标记异常 axes[2].scatter(df['MedInc'], df['target'], s=5, alpha=0.6) axes[2].scatter(df.iloc[iso_outliers]['MedInc'], df.iloc[iso_outliers]['target'], color='orange', s=20, label='IF Outliers') axes[2].set_title('Isolation Forest 检测') axes[2].set_xlabel('MedInc') axes[2].legend() plt.tight_layout() plt.show()

运行这段代码,你会看到类似下面的输出:

注入异常值后 MedInc 的描述性统计: count 20640.000000 mean 7.862688 std 12.829907 min 0.499900 25% 2.541500 50% 3.488850 75% 4.748475 max 53.806100 Z-Score 检测到异常值数量:32 IQR 检测到异常值数量:64 Isolation Forest 检测到异常值数量:1032

注意几个关键点:

  • Z-Score 检测到 32 个异常值,包含了我们注入的 20 个,但也可能有误伤。因为注入的异常值把 MedInc 的均值拉高了,正常点中较大的值也会被误判。
  • IQR 检测到 64 个,因为 IQR 基于分位数,对极端值不敏感,所以阈值更靠近正常数据,多检出了一些边缘点,属于正常情况。
  • Isolation Forest 使用多个特征检测,输出 1032 个异常(约 5%),因为contamination=0.05表示我们预估数据中有 5% 的异常值,这个参数决定了阈值。

这里要特别提醒:没有一种方法能百分百准确识别异常值。不同方法的结果差异很大,所以在实际项目里,不能只依赖一个方法的输出,而是要结合业务判断。

6. 如何处理检测出来的异常值

检测出异常值之后,常见处理方式有删除、修正、转换和单独建模。下面逐一展开。

6.1 删除异常值

最直接的做法,就是删除被标记为异常值的样本或特征列。适用于以下情况:

  • 数据录入错误。
  • 传感器故障。
  • 异常值占比很小(例如 <1%)。
  • 异常值不是研究对象。

删除时要注意:如果异常值占比过高,比如超过 5%,你就应该怀疑是不是数据源的问题,或者阈值设得太严格。

实现代码:

# 基于 IQR 标记异常 df['is_outlier'] = 0 df.loc[iqr_outliers, 'is_outlier'] = 1 # 删除异常值 df_clean = df[df['is_outlier'] == 0].drop(columns=['is_outlier']) print(f"删除前样本量:{len(df)},删除后样本量:{len(df_clean)}")

6.2 修正/替换异常值

如果异常值来自量测误差,可以用均值、中位数或前后值填充。对于时间序列,可以用插值法。

# 将 MedInc 列中超过 upper_bound 的值替换为中位数 median_medinc = df['MedInc'].median() df['MedInc_fixed'] = df['MedInc'] df.loc[df['MedInc'] > upper_bound, 'MedInc_fixed'] = median_medinc

这种方法保留样本数量,但会引入偏差。使用时要注意替换后的分布是否合理。

6.3 数据变换

对于右偏数据,异常值往往在高端。可以用对数变换、Box-Cox 变换等压缩极端值的影响,这样不删除样本,也能降低异常值对模型的干扰。

import numpy as np # 对 MedInc 进行对数变换 df['MedInc_log'] = np.log1p(df['MedInc']) # log1p 避免 log(0)

6.4 单独建模

如果异常值代表重要的业务事件,比如欺诈交易、设备故障,就不应该盲目删除,而应该单独建立异常检测模型,或者将异常与否作为一个新特征。

比如:

# 将异常标记作为特征加入 df['is_outlier'] = (df['MedInc'] > upper_bound).astype(int)

这种方法适合异常事件本身有预测价值的场景。

7. 完整示例:从检测到处理的一体化流程

这里给出一个更接近实际项目的处理流程,包含数据处理、训练模型、对比处理前后的效果。

# 文件路径:outlier_pipeline.py import numpy as np import pandas as pd from sklearn.datasets import fetch_california_housing from sklearn.model_selection import train_test_split from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_squared_error, r2_score from sklearn.ensemble import IsolationForest # 加载数据 data = fetch_california_housing() df = pd.DataFrame(data.data, columns=data.feature_names) df['target'] = data.target # 人为注入异常值 np.random.seed(0) n_outliers = 100 outlier_idx = np.random.choice(df.shape[0], size=n_outliers, replace=False) df.loc[outlier_idx, 'MedInc'] = df['MedInc'] * 10 # 划分数据集(先划分,避免删除异常时出现数据泄露) X = df.drop('target', axis=1) y = df['target'] X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42 ) # 方式1:不处理异常,直接训练 model_raw = LinearRegression() model_raw.fit(X_train, y_train) y_pred_raw = model_raw.predict(X_test) print("未处理异常值:") print(" RMSE:", round(np.sqrt(mean_squared_error(y_test, y_pred_raw)), 4)) print(" R2:", round(r2_score(y_test, y_pred_raw), 4)) # 方式2:使用 IQR 检测并删除训练集中的异常行 def detect_outliers_iqr(df, feature): Q1 = df[feature].quantile(0.25) Q3 = df[feature].quantile(0.75) IQR = Q3 - Q1 lower = Q1 - 1.5 * IQR upper = Q3 + 1.5 * IQR return (df[feature] < lower) | (df[feature] > upper) outlier_mask_train = detect_outliers_iqr(X_train, 'MedInc') X_train_clean = X_train[~outlier_mask_train] y_train_clean = y_train[~outlier_mask_train] print(f"\nIQR 删除训练集异常值数量:{outlier_mask_train.sum()},剩余 {len(X_train_clean)} 条") model_clean = LinearRegression() model_clean.fit(X_train_clean, y_train_clean) y_pred_clean = model_clean.predict(X_test) print("IQR 删除异常值后:") print(" RMSE:", round(np.sqrt(mean_squared_error(y_test, y_pred_clean)), 4)) print(" R2:", round(r2_score(y_test, y_pred_clean), 4))

运行这段代码,你会看到类似这样的结果:

未处理异常值: RMSE: 1.9627 R2: 0.0734 IQR 删除训练集异常值数量:331,剩余 16181 条 IQR 删除异常值后: RMSE: 0.7493 R2: 0.5843

这个对比非常直观:仅仅删除了训练集中的约 2% 的异常值,R² 就从 0.07 提升到 0.58,RMSE 降低了大约 60%。

有人可能会质疑:只删除训练集,测试集还有异常值,为什么效果还会提升?因为线性回归的权重主要受训练集中的极端值影响,训练集干净后,模型学习到的回归线更能反映整体趋势,即使测试集存在少量异常,也不会导致整体误差剧增。

这个例子说明:异常值处理,甚至比特征工程和调参对模型的影响更大。

8. 常见问题与排查思路

异常值检测和处理过程中,经常会遇到下面这些问题:

问题现象可能原因排查方式解决方案
Z-Score 检测不到明显的异常值异常值本身拉高了均值和标准差,导致 z 值偏小先画箱线图或直方图,观察分布改用 IQR 方法,或先将数据截尾再计算
IQR 检出的异常值过多数据本身偏态严重,或系数 1.5 太敏感查看分位数分布调大系数到 2.5 或 3,或结合业务判断
不同方法检测结果不一致每种方法假设不同,多维与单维视角不同对比各方法的交集和差集根据场景选择合适的基准,或集成多种方法
Isolation Forest 标记了太多正常点contamination参数估计偏高查看原数据的异常比例将 contamination 设置为较低值,如 0.01
高维特征下所有方法效果变差维度灾难导致距离/密度失去区分度先降维,再检测异常使用 PCA 或 t-SNE 降维后检测,或使用专门的高维异常检测算法
删除异常值后模型效果反而变差异常值其实是真实业务规律检查被删样本的业务含义不要过早删除,单独建模或作为特征保留
标准化后异常值仍影响模型标准化前未处理异常值,均值方差被影响比较标准化前后数据分布先处理异常值,再做标准化

在实际项目中,建议至少使用两种方法做交叉验证。比如先用 IQR 快速筛查明显错误,再用 Isolation Forest 对多维特征做综合判断。最终是否删除,要经过业务人员确认。

9. 异常值处理的最佳实践与工程建议

基于大量实际项目经验,下面几条建议值得收藏。

9.1 先业务判断,后算法判断

看到异常值,第一反应不应是删除,而是去了解它的业务含义。比如:

  • 用户年龄是 200 岁,必然是录入错误。
  • 单笔交易金额是 100 万,可能是真实的 VIP 客户。
  • 传感器温度短时间跳变,可能是设备故障。

把异常值分为“物理不可能”“业务不合理”“真实但也有意义”三类,分别处理。

9.2 在训练集和验证集上都处理,但要防止泄漏

通常我们只根据训练集统计信息计算均值、标准差、分位数等,然后用这些参数去处理验证集和测试集。千万不要用整个数据集拟合后直接划分,否则会把验证集的信息泄漏到训练过程。

正确流程:

  1. 划分训练集、验证集、测试集。
  2. 在训练集上计算异常检测的阈值。
  3. 对验证集和测试集应用同样的阈值进行标记和删除/替换。
  4. 训练模型时,只在训练集上训练;评估时使用处理后的验证集和测试集。

9.3 不轻易删除超过 5% 的数据

如果检测出的异常值超过总数据的 5%,通常不是数据真的异常,而是阈值设置不合理,或者数据源本身质量太差。此时先回头检查数据采集和清洗流程,再决定要不要调整阈值。

9.4 给异常值加标记特征

很多情况下,是否异常本身就是一个强力特征。例如在信贷场景,是否存在大额异常交易,直接影响风险评估。与其删除异常值,不如把“是否异常”作为二值特征加入模型,让模型自己学习它的作用。

9.5 保存清洗规则和版本

生产环境中,数据清洗逻辑必须可复现。不要只保留清洗后的数据,还要保留清洗规则的代码、阈值参数和数据版本。否则后续数据更新时,你无法知道模型用的是哪种口径的数据。

9.6 结合可视化做最终确认

箱线图、散点图、直方图是检查异常值最直观的工具。如果你在使用某个方法检测出异常值后,建议至少画一次图,用肉眼确认是否有明显的模式或错误。可视化虽然不能自动化,但能避免算法误判造成的“系统性错误”。

10. 总结与后续学习建议

本文围绕机器学习中的异常值,系统讲了三部分内容:

  1. 异常值的定义和影响:明白了异常值会拉偏损失函数、破坏分布假设、影响距离计算,最终让模型性能大幅下降。
  2. 检测方法:基于统计的 Z-Score、IQR,基于密度的 DBSCAN、LOF,基于树的 Isolation Forest,以及基于分类的 One-Class SVM。每种方法都有自己的假设和适用场景。
  3. 处理策略:删除、修正、变换、单独建模。并用一个 California Housing 数据集完成的对比实验,验证了异常值处理对模型性能的巨大提升。

如果你的目标是深入掌握异常值检测,下一步可以重点学习PyOD 库,它集成了数十种异常检测算法,包括 HBOS、COPOD、ECOD 等现代方法,统一接口,非常方便做横向对比。另一个方向是学习时序数据的异常检测,比如 Twitter 的 S-H-ESD 算法、LSTM 自编码器等,它们在工业界有大量应用。

最后提醒一下:异常值检测不是一次性的技术验证,而是一个需要持续维护的数据质量流程。真正优秀的机器学习项目,不是用了多复杂的模型,而是把数据质量关把好了,让后续的每一个模型都能稳定发挥。

建议你把这篇文章里的代码复制到自己的环境里跑一遍,改一改特征和阈值,亲手体会异常值对模型的影响。这种手感,是看多少文章都替代不了的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询