查看全课大纲(第 10 / 35 节)

异常值处理实操

约 12 分钟

小象实战讲义 · 特征工程精讲与实践

在上一节我们学习了缺失值的处理方法,本节我们将聚焦数据预处理的另一个核心任务——异常值处理。与缺失值不同,异常值往往“隐藏”在数据中,需要我们通过特定的方法去检测和识别。本节将系统讲解多种异常值检测方法(从简单规则到高级算法)以及检测后的处理策略,并通过泰坦尼克数据集的票价特征进行实操演示,帮助你掌握工业场景下异常值处理的完整流程。

💡 核心导读

  • 异常值检测的挑战:理解为什么异常值没有绝对标准,需要结合业务场景选择方法。
  • 四大经典检测方法:掌握指定边界法、IQR(箱线图)法、均值标准差法(3σ原则)和中位数绝对偏差法(MAD)的原理与应用。
  • 异常值处理策略:学习对检测出的异常值进行填充(缩尾、均值填充)、删除等操作。
  • 高级检测方法概览:了解基于机器学习的异常值检测工具(如孤立森林、KNN)及其适用场景。

一、异常值检测:没有绝对的标准

异常值(Outlier),也称为离群点,是指那些与数据集中其他观测值显著不同的数据点。与缺失值“一眼就能看出来”不同,异常值通常需要借助统计方法或模型进行检测。

一个关键认知是:异常值的判定没有绝对的标准。 如果我们有绝对的标准(比如业务规则明确规定超过某个值就是错误),那么直接按规则处理即可,根本不需要复杂的检测方法。因此,异常值处理很大程度上依赖于我们对业务场景的理解。例如,在泰坦尼克数据中,票价0美元和512美元都可能被视为异常,但前者可能是船员或特殊赠票,后者则是豪华舱位的真实票价。是否处理、如何处理,都需要结合背景知识判断。

下面,我们将以泰坦尼克数据集中的Fare(票价)特征为例,演示几种常见的异常值检测方法。首先,我们构造一个用于演示的小规模数据样本。

import pandas as pd
import numpy as np

# 构造一个简化的泰坦尼克票价数据样本(基于真实数据分布简化)
data = pd.DataFrame({
    'PassengerId': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 179, 258, 680, 737, 806, 1234],
    'Fare': [7.25, 71.28, 7.92, 53.10, 8.05, 8.46, 51.86, 21.07, 11.13, 30.07, 16.70, 26.55, 0.0, 13.0, 50.0, 110.88, 0.0, 512.33]
})
print("演示数据样本(包含已知的潜在异常值0和512):")
print(data[['PassengerId', 'Fare']].sort_values('Fare'))
演示数据样本(包含已知的潜在异常值0和512):
    PassengerId    Fare
12          179    0.00
16          806    0.00
0             1    7.25
2             3    7.92
4             5    8.05
5             6    8.46
8             9   11.13
13          258   13.00
10           11   16.70
7             8   21.07
11           12   26.55
9            10   30.07
14          680   50.00
6             7   51.86
3             4   53.10
1             2   71.28
15          737  110.88
17         1234  512.33

可以看到,数据中包含了0美元和512.33美元这两个极端票价,它们是我们接下来要检测的目标。

二、经典异常值检测方法

1. 指定边界检测法

最简单直接的方法是根据业务知识或经验,人为设定一个正常值的范围(上界和下界),超出此范围即视为异常。

import pandas as pd
import numpy as np

# 演示数据(与本章首块一致)
data = pd.DataFrame({
    'PassengerId': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 179, 258, 680, 737, 806, 1234],
    'Fare': [7.25, 71.28, 7.92, 53.10, 8.05, 8.46, 51.86, 21.07, 11.13, 30.07, 16.70, 26.55, 0.0, 13.0, 50.0, 110.88, 0.0, 512.33]
})

def outlier_detect_arbitrary(data, col, upper_fence, lower_fence):
    """
    基于指定边界检测异常值。
    参数:
        data: DataFrame
        col: 待检测的列名
        upper_fence: 上边界
        lower_fence: 下边界
    返回:
        outlier_index: 布尔序列,True表示异常
        para: 使用的边界元组 (upper_fence, lower_fence)
    """
    para = (upper_fence, lower_fence)
    # 判断是否超过上界或低于下界
    tmp = pd.concat([data[col] > upper_fence, data[col] < lower_fence], axis=1)
    outlier_index = tmp.any(axis=1)
    print(f'检测到异常值数量: {outlier_index.sum()}')
    print(f'异常值占比: {outlier_index.sum()/len(outlier_index):.2%}')
    return outlier_index, para

# 假设我们认为票价低于5美元或高于300美元为异常
outlier_index_arb, bounds_arb = outlier_detect_arbitrary(data, 'Fare', upper_fence=300, lower_fence=5)
print(f"\n边界: {bounds_arb}")
print("检测出的异常值样本:")
print(data.loc[outlier_index_arb, ['PassengerId', 'Fare']])
检测到异常值数量: 3
异常值占比: 16.67%

边界: (300, 5)
检测出的异常值样本:
    PassengerId    Fare
12          179    0.00
16          806    0.00
17         1234  512.33

该方法检测出3个异常样本:两个0美元票价和一个512美元票价。方法简单直观,但高度依赖先验知识。

2. IQR(箱线图)检测法

箱线图(Box plot)是可视化识别异常值的经典工具。其原理基于四分位距(IQR):

  • IQR = 上四分位数(Q3,75%) - 下四分位数(Q1,25%)
  • 上界 = Q3 + 1.5 * IQR
  • 下界 = Q1 - 1.5 * IQR 落在上界之上或下界之下的点被视为异常值(离群点)。
import pandas as pd
import numpy as np

# 演示数据(与本章首块一致)
data = pd.DataFrame({
    'PassengerId': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 179, 258, 680, 737, 806, 1234],
    'Fare': [7.25, 71.28, 7.92, 53.10, 8.05, 8.46, 51.86, 21.07, 11.13, 30.07, 16.70, 26.55, 0.0, 13.0, 50.0, 110.88, 0.0, 512.33]
})

def outlier_detect_IQR(data, col, threshold=1.5):
    """
    使用IQR(箱线图)规则检测异常值。
    参数:
        data: DataFrame
        col: 待检测的列名
        threshold: IQR倍数,默认1.5
    返回:
        outlier_index: 布尔序列,True表示异常
        para: 使用的边界元组 (upper_fence, lower_fence)
    """
    Q1 = data[col].quantile(0.25)
    Q3 = data[col].quantile(0.75)
    IQR = Q3 - Q1
    lower_fence = Q1 - threshold * IQR
    upper_fence = Q3 + threshold * IQR
    para = (upper_fence, lower_fence)
    
    tmp = pd.concat([data[col] > upper_fence, data[col] < lower_fence], axis=1)
    outlier_index = tmp.any(axis=1)
    print(f'检测到异常值数量: {outlier_index.sum()}')
    print(f'异常值占比: {outlier_index.sum()/len(outlier_index):.2%}')
    print(f'Q1={Q1:.2f}, Q3={Q3:.2f}, IQR={IQR:.2f}, 上界={upper_fence:.2f}, 下界={lower_fence:.2f}')
    return outlier_index, para

outlier_index_iqr, bounds_iqr = outlier_detect_IQR(data, 'Fare', threshold=1.5)
print("\n检测出的异常值样本:")
print(data.loc[outlier_index_iqr, ['PassengerId', 'Fare']])
检测到异常值数量: 1
异常值占比: 5.56%
Q1=8.15, Q3=51.39, IQR=43.24, 上界=116.26, 下界=-56.71

检测出的异常值样本:
    PassengerId    Fare
17         1234  512.33

IQR法只将512.33美元检测为异常,因为0美元虽然低,但仍在计算出的下界(-56.71)之内。阈值(1.5)可以调整,更严格的阈值(如3)会减少异常值数量。

3. 均值与标准差法(3σ原则)

对于近似服从正态分布的数据,可以使用均值(μ)和标准差(σ)来定义异常值范围。通常认为,距离均值超过3倍标准差的数据点概率极小,可能为异常。

  • 上界 = μ + 3σ
  • 下界 = μ - 3σ
import pandas as pd
import numpy as np

# 演示数据(与本章首块一致)
data = pd.DataFrame({
    'PassengerId': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 179, 258, 680, 737, 806, 1234],
    'Fare': [7.25, 71.28, 7.92, 53.10, 8.05, 8.46, 51.86, 21.07, 11.13, 30.07, 16.70, 26.55, 0.0, 13.0, 50.0, 110.88, 0.0, 512.33]
})

def outlier_detect_mean_std(data, col, threshold=3):
    """
    使用均值与标准差法检测异常值。
    参数:
        data: DataFrame
        col: 待检测的列名
        threshold: 标准差的倍数,默认3
    返回:
        outlier_index: 布尔序列,True表示异常
        para: 使用的边界元组 (upper_fence, lower_fence)
    """
    mean = data[col].mean()
    std = data[col].std()
    upper_fence = mean + threshold * std
    lower_fence = mean - threshold * std
    para = (upper_fence, lower_fence)
    
    tmp = pd.concat([data[col] > upper_fence, data[col] < lower_fence], axis=1)
    outlier_index = tmp.any(axis=1)
    print(f'检测到异常值数量: {outlier_index.sum()}')
    print(f'异常值占比: {outlier_index.sum()/len(outlier_index):.2%}')
    print(f'均值={mean:.2f}, 标准差={std:.2f}, 上界={upper_fence:.2f}, 下界={lower_fence:.2f}')
    return outlier_index, para

outlier_index_3sigma, bounds_3sigma = outlier_detect_mean_std(data, 'Fare', threshold=3)
print("\n检测出的异常值样本:")
print(data.loc[outlier_index_3sigma, ['PassengerId', 'Fare']])
检测到异常值数量: 1
异常值占比: 5.56%
均值=55.54, 标准差=117.63, 上界=408.43, 下界=-297.35

检测出的异常值样本:
    PassengerId    Fare
17         1234  512.33

同样,只有512美元被检出。注意:此方法对异常值本身敏感,因为异常值会拉高标准差,从而使边界变宽,可能“掩盖”其他异常点。

4. 中位数绝对偏差法(MAD)

为了克服均值标准差法对异常值敏感的缺点,可以使用更稳健的统计量——中位数(Median)和中位数绝对偏差(MAD)。

  • MAD = median(|X_i - median(X)|)
  • 修正Z分数 = 0.6745 * (X_i - median) / MAD 通常认为修正Z分数的绝对值超过3.5(可调阈值)时为异常。
import pandas as pd
import numpy as np

# 演示数据(与本章首块一致)
data = pd.DataFrame({
    'PassengerId': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 179, 258, 680, 737, 806, 1234],
    'Fare': [7.25, 71.28, 7.92, 53.10, 8.05, 8.46, 51.86, 21.07, 11.13, 30.07, 16.70, 26.55, 0.0, 13.0, 50.0, 110.88, 0.0, 512.33]
})

def outlier_detect_MAD(data, col, threshold=3.5):
    """
    使用中位数绝对偏差法(MAD)检测异常值。
    参数:
        data: DataFrame
        col: 待检测的列名
        threshold: 修正Z分数的阈值,默认3.5
    返回:
        outlier_index: 布尔序列,True表示异常
    """
    median = data[col].median()
    # 计算绝对偏差的中位数(MAD)
    deviations = np.abs(data[col] - median)
    mad = deviations.median()
    # 计算修正Z分数
    modified_z_scores = 0.6745 * (data[col] - median) / mad if mad != 0 else pd.Series([0]*len(data))
    outlier_index = np.abs(modified_z_scores) > threshold
    
    print(f'检测到异常值数量: {outlier_index.sum()}')
    print(f'异常值占比: {outlier_index.sum()/len(outlier_index):.2%}')
    print(f'中位数={median:.2f}, MAD={mad:.2f}')
    return outlier_index

outlier_index_mad = outlier_detect_MAD(data, 'Fare', threshold=3.5)
print("\n检测出的异常值样本:")
print(data.loc[outlier_index_mad, ['PassengerId', 'Fare']])
检测到异常值数量: 2
异常值占比: 11.11%
中位数=18.88, MAD=11.41

检测出的异常值样本:
    PassengerId    Fare
15          737  110.88
17         1234  512.33

在我们的小样本中,MAD 法检出了 2 个异常值(110.88 美元与 512.33 美元),比 IQR 法和 3σ 法更敏感——原因在于 MAD 基于中位数统计,边界不会被 512 这个极端值本身拉偏,因此更紧;而 3σ 的标准差被极端值拉大后,反而“掩盖”了 110.88 这样的偏离点。这正是 MAD 常被用作 3σ 稳健替代的原因。

四种方法敏感度对比(原课全量数据参照):在原课程 891 条全量泰坦尼克数据上,四种方法对同一 Fare 列的检出规模分别为:指定边界法(5~300 美元)19 个(占比约 2.13%)、IQR 法 31 个(原课此处阈值取 5,比本节示例的 1.5 严格——若用 1.5 会检出 116 个)、3σ 法 20 个、MAD 法 160 个。不同方法的敏感度差异在小样本中同样存在,实际选用时需要结合业务判断:是“宁可多检出再逐个核实”,还是“只处理最极端的明显错误”。

三、异常值的处理策略

检测出异常值后,我们有以下几种常见的处理方式:

1. 删除异常值

直接将含有异常值的样本从数据集中移除。适用于异常值数量很少,且确定其为错误数据的情况。

import pandas as pd
import numpy as np

# 演示数据(与本章首块一致)
data = pd.DataFrame({
    'PassengerId': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 179, 258, 680, 737, 806, 1234],
    'Fare': [7.25, 71.28, 7.92, 53.10, 8.05, 8.46, 51.86, 21.07, 11.13, 30.07, 16.70, 26.55, 0.0, 13.0, 50.0, 110.88, 0.0, 512.33]
})

def outlier_detect_IQR(data, col, threshold=1.5):
    Q1 = data[col].quantile(0.25)
    Q3 = data[col].quantile(0.75)
    IQR = Q3 - Q1
    lower_fence = Q1 - threshold * IQR
    upper_fence = Q3 + threshold * IQR
    para = (upper_fence, lower_fence)
    tmp = pd.concat([data[col] > upper_fence, data[col] < lower_fence], axis=1)
    outlier_index = tmp.any(axis=1)
    return outlier_index, para

outlier_index_iqr, bounds_iqr = outlier_detect_IQR(data, 'Fare', threshold=1.5)

def drop_outlier(data, outlier_index):
    """
    删除异常值样本。
    参数:
        data: DataFrame
        outlier_index: 异常值索引(布尔序列)
    返回:
        data_copy: 删除异常值后的DataFrame副本
    """
    data_copy = data[~outlier_index].copy()
    return data_copy

# 使用之前IQR法检测的结果进行删除
data_dropped = drop_outlier(data, outlier_index_iqr)
print("删除异常值后的数据形状:", data_dropped.shape)
print("剩余数据中Fare的范围: [{:.2f}, {:.2f}]".format(data_dropped['Fare'].min(), data_dropped['Fare'].max()))
删除异常值后的数据形状: (17, 2)
剩余数据中Fare的范围: [0.00, 110.88]

2. 视为缺失值并填充

将异常值视为一种特殊的缺失值,然后用处理缺失值的方法进行填充(如均值、中位数、众数或特定值)。

import pandas as pd
import numpy as np

# 演示数据(与本章首块一致)
data = pd.DataFrame({
    'PassengerId': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 179, 258, 680, 737, 806, 1234],
    'Fare': [7.25, 71.28, 7.92, 53.10, 8.05, 8.46, 51.86, 21.07, 11.13, 30.07, 16.70, 26.55, 0.0, 13.0, 50.0, 110.88, 0.0, 512.33]
})

def outlier_detect_IQR(data, col, threshold=1.5):
    Q1 = data[col].quantile(0.25)
    Q3 = data[col].quantile(0.75)
    IQR = Q3 - Q1
    lower_fence = Q1 - threshold * IQR
    upper_fence = Q3 + threshold * IQR
    para = (upper_fence, lower_fence)
    tmp = pd.concat([data[col] > upper_fence, data[col] < lower_fence], axis=1)
    outlier_index = tmp.any(axis=1)
    return outlier_index, para

outlier_index_iqr, bounds_iqr = outlier_detect_IQR(data, 'Fare', threshold=1.5)

def impute_outlier_with_avg(data, col, outlier_index, strategy='mean'):
    """
    使用均值/中位数/众数填充异常值。
    参数:
        data: DataFrame
        col: 列名
        outlier_index: 异常值索引(布尔序列)
        strategy: 'mean', 'median', 'mode'
    返回:
        data_copy: 处理后的DataFrame副本
    """
    data_copy = data.copy()
    if strategy == 'mean':
        fill_value = data_copy[col].mean()
    elif strategy == 'median':
        fill_value = data_copy[col].median()
    elif strategy == 'mode':
        fill_value = data_copy[col].mode()[0]
    else:
        fill_value = np.nan
    
    data_copy.loc[outlier_index, col] = fill_value
    print(f"使用{strategy}填充,填充值为: {fill_value:.2f}")
    return data_copy

# 用中位数填充IQR检出的异常值
data_imputed_median = impute_outlier_with_avg(data, 'Fare', outlier_index_iqr, strategy='median')
print("\n填充后,原异常值样本的Fare:")
print(data_imputed_median.loc[outlier_index_iqr, ['PassengerId', 'Fare']])
使用median填充,填充值为: 18.88

填充后,原异常值样本的Fare:
    PassengerId    Fare
17         1234  18.885

3. 缩尾处理(Winsorization)

不删除异常值,而是将其“拉回”到边界上。超过上界的用上界值替换,低于下界的用下界值替换。这种方法能保留样本量,同时削弱极端值的影响。

import pandas as pd
import numpy as np

# 演示数据(与本章首块一致)
data = pd.DataFrame({
    'PassengerId': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 179, 258, 680, 737, 806, 1234],
    'Fare': [7.25, 71.28, 7.92, 53.10, 8.05, 8.46, 51.86, 21.07, 11.13, 30.07, 16.70, 26.55, 0.0, 13.0, 50.0, 110.88, 0.0, 512.33]
})

def outlier_detect_IQR(data, col, threshold=1.5):
    Q1 = data[col].quantile(0.25)
    Q3 = data[col].quantile(0.75)
    IQR = Q3 - Q1
    lower_fence = Q1 - threshold * IQR
    upper_fence = Q3 + threshold * IQR
    para = (upper_fence, lower_fence)
    tmp = pd.concat([data[col] > upper_fence, data[col] < lower_fence], axis=1)
    outlier_index = tmp.any(axis=1)
    return outlier_index, para

outlier_index_iqr, bounds_iqr = outlier_detect_IQR(data, 'Fare', threshold=1.5)

def impute_outlier_with_boundary(data, col, outlier_index, para, strategy='both'):
    """
    使用边界值填充异常值(缩尾处理,Winsorization)。
    参数:
        data: DataFrame
        col: 列名
        outlier_index: 异常值索引(布尔序列)
        para: 边界元组 (upper_fence, lower_fence)
        strategy: 'both'(上下都缩尾), 'top'(只缩上尾), 'bottom'(只缩下尾)
    返回:
        data_copy: 处理后的DataFrame副本
    """
    data_copy = data.copy()
    upper_fence, lower_fence = para
    
    if strategy in ['both', 'top']:
        data_copy.loc[data_copy[col] > upper_fence, col] = upper_fence
    if strategy in ['both', 'bottom']:
        data_copy.loc[data_copy[col] < lower_fence, col] = lower_fence
    
    return data_copy

# 使用IQR法计算的边界进行缩尾处理(上下都缩)
data_winsorized = impute_outlier_with_boundary(data, 'Fare', outlier_index_iqr, bounds_iqr, strategy='both')
print("缩尾处理后,所有数据的Fare范围: [{:.2f}, {:.2f}]".format(data_winsorized['Fare'].min(), data_winsorized['Fare'].max()))
print("原异常值样本处理后的Fare:")
print(data_winsorized.loc[outlier_index_iqr, ['PassengerId', 'Fare']])
缩尾处理后,所有数据的Fare范围: [0.00, 116.26]
原异常值样本处理后的Fare:
    PassengerId       Fare
17         1234  116.25875

可以看到,512美元的票价被“拉回”到了116.26美元(IQR计算出的上界)。

四、高级异常值检测方法概览

除了上述基于统计规则的经典方法,还可以使用机器学习算法进行异常值检测,尤其适用于多维特征场景。这些方法通常将异常检测视为一个无监督学习问题。

讲师在视频中介绍了一个强大的Python工具库PyOD(Python Outlier Detection),它集成了多种异常检测算法,例如:

  • Isolation Forest(孤立森林):通过随机划分特征空间来“孤立”异常点,异常点通常更容易被快速隔离。
  • K Nearest Neighbors(KNN):基于距离,异常点的k近邻距离通常较大。
  • One-Class SVM(OCSVM):学习一个包围正常数据的边界,边界外的点视为异常。
  • Local Outlier Factor(LOF):基于局部密度,异常点相对于其邻居的密度较低。
  • 此外还集成了基于 PCA 投影距离、MCD(最小协方差行列式)、CBLOF、HBOS、ABOD 等多种算法。

使用模式:这些算法在PyOD中具有统一的接口:初始化检测器 -> fit 数据 -> 使用 decision_function 或 predict 获取异常得分或标签。

原课用一份构造的 200 样本数据(其中约 25% 为异常点)对比了多种检测器的效果:KNN 与孤立森林(Isolation Forest)的误检率为 0,表现最好;而 ABOD、CBLOF 等方法的误检率较高。因此在日常实践中,KNN 与孤立森林是用得最多的高级异常检测方法,其中孤立森林在工业场景中尤其常用。

注意:原课程使用了PyOD库进行演示。在实际工业场景中,若数据维度高、分布复杂,可考虑引入此类高级方法。但经典统计方法因其可解释性强、计算简单,在多数场景下仍是首选。

本章小结

本节我们深入探讨了特征工程中异常值处理的完整流程。我们首先明确了异常值检测没有金科玉律,需要结合业务理解。随后,我们系统学习并实践了四种经典检测方法:指定边界法、IQR(箱线图)法、均值标准差法以及中位数绝对偏差法(MAD)。每种方法都有其适用场景和优缺点。最后,我们掌握了检测后的三大处理策略:删除、视为缺失值填充以及缩尾处理,并了解了基于机器学习的高级检测方法的存在与价值。

📋 行动清单

  • 尝试不同阈值:对同一份数据,尝试调整IQR法或3σ原则中的阈值(如将1.5改为3),观察检测出的异常值数量如何变化,思考不同阈值适用的业务场景。
  • 方法对比实验:在同一个特征上,依次运行本节介绍的四种经典检测方法,记录并对比它们检出的异常值有哪些异同,分析原因。
  • 处理策略影响分析:对检出的异常值,分别尝试删除、中位数填充和缩尾处理三种策略,然后用处理后的数据计算该特征的均值、标准差等统计量,直观感受不同处理策略对数据分布的影响。

📝 动手练一练

  1. 概念辨析:为什么说“异常值检测没有绝对的标准”?请结合泰坦尼克票价0美元和512美元的例子说明。
👉 点击查看参考答案

因为一个值是否“异常”取决于业务背景和建模目标,而非纯粹的数学统计。例如:

  • 0美元票价:从统计上看是极小值,可能是异常。但从业务上看,可能是船员、工作人员或获赠船票的乘客,是真实存在的合理情况。如果建模目标是预测乘客生存率,这些人的特征模式可能很有价值,删除或修改他们会损失信息。
  • 512美元票价:统计上是极大值,可能是异常。但业务上对应的是极其豪华的舱位,是真实的高消费记录。这个值本身是正确且重要的,它反映了乘客的社会经济地位,对预测生存率可能是强特征。

因此,不能仅凭统计规则武断地将其作为“错误数据”处理。我们需要先判断异常值是“数据错误”还是“业务事实”,再决定处理策略。

  1. 动手实验:在下面的代码中,我们构造了一个包含“潜在异常值”的小数据集。请使用均值标准差法,但将阈值从3改为2(即2σ原则),运行代码并回答:检测出的异常值有哪些?与3σ原则相比,异常值数量是更多还是更少?为什么?
import pandas as pd
import numpy as np

# 构造练习数据
data_exercise = pd.DataFrame({
    'Value': [10, 12, 11, 13, 9, 10, 100, 11, 12, 10, 9, 13, -50]
})

def outlier_detect_mean_std_exercise(data, col, threshold=3):
    mean = data[col].mean()
    std = data[col].std()
    upper = mean + threshold * std
    lower = mean - threshold * std
    outlier_index = (data[col] > upper) | (data[col] < lower)
    print(f'阈值={threshold}σ: 均值={mean:.2f}, 标准差={std:.2f}, 上界={upper:.2f}, 下界={lower:.2f}')
    print(f'检测到异常值数量: {outlier_index.sum()}')
    print(f'异常值: {data[col][outlier_index].tolist()}')
    return outlier_index

print("--- 3σ原则检测结果 ---")
outlier_3sigma = outlier_detect_mean_std_exercise(data_exercise, 'Value', 3)
print("\n--- 2σ原则检测结果 ---")
outlier_2sigma = outlier_detect_mean_std_exercise(data_exercise, 'Value', 2)
👉 点击查看参考答案

运行上述代码,输出如下:

--- 3σ原则检测结果 ---
阈值=3σ: 均值=13.08, 标准差=31.10, 上界=106.38, 下界=-80.22
检测到异常值数量: 0
异常值: []

--- 2σ原则检测结果 ---
阈值=2σ: 均值=13.08, 标准差=31.10, 上界=75.28, 下界=-49.12
检测到异常值数量: 2
异常值: [100, -50]

答案:

  • 使用2σ原则检测出的异常值是 100 和 -50。
  • 与3σ原则相比,2σ原则检测出的异常值数量更多(2个 vs 0个)。
  • 原因:阈值(σ的倍数)决定了判断异常的严格程度。阈值越小(如2σ),定义的正常值范围(μ ± 2σ)就越窄,因此更多偏离中心的数据点会被判定为异常。2σ原则比3σ原则更敏感、更严格。

—— 小象教研组