示例代码 针对数值列

def remove_outliers(X_train, y_train, X_test, y_test, method='iqr', threshold=None):
    """去除异常值"""
    # 1. 设置默认阈值
    if threshold is None:
        threshold = 1.5 if method == 'iqr' else 3.0

    # 2. 数据格式转换
    if not isinstance(X_train, pd.DataFrame):
        X_train = pd.DataFrame(X_train)
    if not isinstance(X_test, pd.DataFrame):
        X_test = pd.DataFrame(X_test)

    # 3. 验证维度
    if len(X_train) != len(y_train):
        raise ValueError(f"X_train 长度 ({len(X_train)}) 与 y_train ({len(y_train)}) 不匹配")

    # 4. 识别数值列(取训练集和测试集共有列)
    train_numeric_cols = X_train.select_dtypes(include=[np.number]).columns
    test_numeric_cols = X_test.select_dtypes(include=[np.number]).columns
    numeric_cols = [col for col in train_numeric_cols if col in test_numeric_cols]

    if len(numeric_cols) == 0:
        print("⚠️ 没有共同的数值列,跳过异常值处理")
        return X_train, y_train, X_test, y_test

    print(f"  对 {len(numeric_cols)} 个数值列进行异常值处理 (方法: {method}, 阈值: {threshold})")
    X_train_numeric = X_train[numeric_cols]

    # 5. 检测异常
    if method == 'iqr':
        Q1 = X_train_numeric.quantile(0.25)
        Q3 = X_train_numeric.quantile(0.75)
        IQR = Q3 - Q1
        lower_bound = Q1 - threshold * IQR
        upper_bound = Q3 + threshold * IQR
        mask = ~((X_train_numeric < lower_bound) | (X_train_numeric > upper_bound)).any(axis=1)

        # 对测试集进行截断
        for col in numeric_cols:
            X_test[col] = X_test[col].clip(lower=lower_bound[col], upper=upper_bound[col])

    elif method == 'zscore':
        # 稳健 Z-Score(基于中位数和 MAD)
        median = X_train_numeric.median()
        mad = (X_train_numeric - median).abs().median() * 1.4826  # 1/0.6745,转换为标准差估计

        # 处理常数列
        constant_cols = (mad == 0)
        if constant_cols.any():
            print(f"⚠️ 检测到 {constant_cols.sum()} 个常数列: {list(mad[mad == 0].index)}")
            mad = mad.replace(0, 1e-10)  # 避免除零

        z_scores = ((X_train_numeric - median) / mad).abs()
        mask = (z_scores < threshold).all(axis=1)

        # 对测试集进行截断
        for col in numeric_cols:
            X_test[col] = X_test[col].clip(
                lower=median[col] - threshold * mad[col],
                upper=median[col] + threshold * mad[col]
            )
    else:
        raise ValueError(f"不支持的方法: {method}")

    # 6. 检查结果
    if mask.sum() == 0:
        print("⚠️ 所有样本都被标记为异常,请调整阈值!返回原始数据")
        return X_train, y_train, X_test, y_test

    # 7. 应用掩码
    removed_count = (~mask).sum()
    if removed_count > 0:
        print(f"  移除了 {removed_count} 个异常值样本 ({removed_count / len(mask) * 100:.2f}%)")
        print(f"  处理前: {len(X_train)} 样本, 处理后: {mask.sum()} 样本")

    X_train_clean = X_train.loc[mask]

    if isinstance(y_train, (pd.Series, pd.DataFrame)):
        y_train_clean = y_train.loc[mask]  # 使用 loc 而不是 iloc
    elif isinstance(y_train, np.ndarray):
        y_train_clean = y_train[mask]
    else:  # list, tuple 等
        y_train_clean = np.array(y_train)[mask]

    return X_train_clean, y_train_clean, X_test, y_test

多列同时处理的原理详解

关键在于 Pandas 的向量化运算广播机制

核心原理:向量化运算

1. 数据形状示例

假设有 5 个样本,3 个数值列:

X_train_numeric = 
     col1  col2  col3
0     1     2     3
1     4     5     6
2     7     8     9
3    10    11    12
4    13    14    15

# 形状: (5, 3)  # 5行3列

2. IQR 方法的多列计算

Q1 = X_train_numeric.quantile(0.25)
Q3 = X_train_numeric.quantile(0.75)
IQR = Q3 - Q1

Q1 的结果(Series):

Q1 =
col1    4.0
col2    5.0
col3    6.0
dtype: float64
# 形状: (3,)  # 每列一个值

IQR 的结果(Series):

IQR =
col1    6.0
col2    6.0
col3    6.0
dtype: float64
# 形状: (3,)

3. 广播机制(Broadcasting)

lower_bound = Q1 - threshold * IQR
upper_bound = Q3 + threshold * IQR

计算过程:

# threshold * IQR 是标量乘法
# Q1 - threshold * IQR 是 Series 之间的逐元素运算

lower_bound =
col1    4.0 - 1.5*6.0 = -5.0
col2    5.0 - 1.5*6.0 = -4.0
col3    6.0 - 1.5*6.0 = -3.0

upper_bound =
col1    10.0 + 1.5*6.0 = 19.0
col2    11.0 + 1.5*6.0 = 20.0
col3    12.0 + 1.5*6.0 = 21.0

4. 多列比较(最关键的步骤)

python

mask = ~((X_train_numeric < lower_bound) | (X_train_numeric > upper_bound)).any(axis=1)

逐步拆解:

步骤 1:X_train_numeric < lower_bound
# X_train_numeric 形状: (5, 3)
# lower_bound 形状: (3,)
# Pandas 自动广播:每列与对应的 lower_bound 值比较

X_train_numeric < lower_bound =
        col1   col2   col3
0    1 < -5  → False    2 < -4  → False    3 < -3  → False
1    4 < -5  → False    5 < -4  → False    6 < -3  → False
2    7 < -5  → False    8 < -4  → False    9 < -3  → False
3   10 < -5  → False   11 < -4  → False   12 < -3  → False
4   13 < -5  → False   14 < -4  → False   15 < -3  → False

结果形状: (5, 3)  # 每列独立的比较结果

步骤 2:X_train_numeric > upper_bound
X_train_numeric > upper_bound =
        col1    col2    col3
0    1 > 19  → False    2 > 20  → False    3 > 21  → False
1    4 > 19  → False    5 > 20  → False    6 > 21  → False
2    7 > 19  → False    8 > 20  → False    9 > 21  → False
3   10 > 19  → False   11 > 20  → False   12 > 21  → False
4   13 > 19  → False   14 > 20  → False   15 > 21  → False

结果形状: (5, 3)
步骤 3:|(或运算)
(X_train_numeric < lower_bound) | (X_train_numeric > upper_bound) =
        col1    col2    col3
0    False    False    False
1    False    False    False
2    False    False    False
3    False    False    False
4    False    False    False

结果形状: (5, 3)  # 每列独立判断是否异常
步骤 4:.any(axis=1)
# axis=1 表示按行(横向)检查
# 只要该行有任何一列为 True,结果就是 True

结果 = 
0    False  # 该行所有列都是正常
1    False  # 该行所有列都是正常
2    False  # 该行所有列都是正常
3    False  # 该行所有列都是正常
4    False  # 该行所有列都是正常

形状: (5,)  # 每行一个布尔值
步骤 5:~(取反)
mask = 
0    True   # 正常样本
1    True   # 正常样本
2    True   # 正常样本
3    True   # 正常样本
4    True   # 正常样本

形状: (5,)

5. 截断操作(多列同时处理)

for col in numeric_cols:
    X_train[col] = X_train[col].clip(lower=lower_bound[col], upper=upper_bound[col])
    X_test[col] = X_test[col].clip(lower=lower_bound[col], upper=upper_bound[col])

循环处理每列:

列名 lower_bound upper_bound 操作
col1 -5.0 19.0 截断到 [-5, 19]
col2 -4.0 20.0 截断到 [-4, 20]
col3 -3.0 21.0 截断到 [-3, 21]

每列独立截断,互不影响:

# col1 原始: [1, 4, 7, 10, 13]
# 截断后:   [1, 4, 7, 10, 13]  # 都在范围内

# col2 原始: [2, 5, 8, 11, 14]
# 截断后:   [2, 5, 8, 11, 14]  # 都在范围内

6. Z-Score 方法的多列处理

median = X_train_numeric.median()
# median =
# col1    7.0
# col2    8.0
# col3    9.0

mad = (X_train_numeric - median).abs().median() * 1.4826
# (X_train_numeric - median) 广播运算:(5,3) - (3,) = (5,3)
# .abs() 取绝对值
# .median() 按列计算中位数
# * 1.4826 标量乘法

z_scores = ((X_train_numeric - median) / mad).abs()
# (X_train_numeric - median) 形状: (5,3)
# mad 形状: (3,)
# 广播除法:每列除以对应的 mad 值
# 结果形状: (5,3)

mask = (z_scores < threshold).all(axis=1)
# z_scores < threshold 形状: (5,3)
# .all(axis=1) 按行检查:该行所有列是否都 < threshold
# 结果形状: (5,)

可视化理解

原始数据 (5行3列):
┌─────┬──────┬──────┬──────┐
│     │ col1 │ col2 │ col3 │
├─────┼──────┼──────┼──────┤
│ 0   │  1   │  2   │  3   │
│ 1   │  4   │  5   │  6   │
│ 2   │  7   │  8   │  9   │
│ 3   │  10  │  11  │  12  │
│ 4   │  13  │  14  │  15  │
└─────┴──────┴──────┴──────┘

每列独立计算边界:
lower_bound = [col1:-5.0, col2:-4.0, col3:-3.0]
upper_bound = [col1:19.0, col2:20.0, col3:21.0]

截断操作 (逐列):
col1: [1, 4, 7, 10, 13] → [1, 4, 7, 10, 13]  (无变化)
col2: [2, 5, 8, 11, 14] → [2, 5, 8, 11, 14]  (无变化)
col3: [3, 6, 9, 12, 15] → [3, 6, 9, 12, 15]  (无变化)

关键函数总结

函数/操作 输入形状 输出形状 作用
.quantile(0.25) (n, m) (m,) 每列计算 Q1
df < Series (n, m) < (m,) (n, m) 广播比较,每列独立
.any(axis=1) (n, m) (n,) 按行检查是否有 True
.all(axis=1) (n, m) (n,) 按行检查是否全为 True
.clip() (n,) (n,) 单列截断

为什么能同时处理多个列?

  1. Pandas 的向量化运算:所有操作都是逐元素执行的,无需显式循环

  2. 广播机制:Series(每列一个值)自动扩展到 DataFrame(每行都使用该值)

  3. 按列独立计算:每列的统计量(Q1, Q3, median, mad)独立计算

  4. 按行聚合.any(axis=1) 将多列结果聚合成单列

示例:

# 假设 3 个样本,2 列
data = [[1, 2], [100, 3], [4, 5]]
lower = [0, 0]
upper = [10, 10]

# 步骤 1-2: 标记异常
异常标记 = [[False, False], [True, False], [False, False]]
# 步骤 3: any(axis=1)
每行有异常 = [False, True, False]  # 第 2 行有异常
# 步骤 4: 取反
mask = [True, False, True]  # 保留第 1、3 行

移除版本

def remove_outliers(X_train, y_train, X_test, y_test, method='iqr', threshold=None):
    """去除异常值(统一截断处理)"""
    # 1. 设置默认阈值
    if threshold is None:
        threshold = 1.5 if method == 'iqr' else 3.0

    # 2. 数据格式转换
    if not isinstance(X_train, pd.DataFrame):
        X_train = pd.DataFrame(X_train)
    if not isinstance(X_test, pd.DataFrame):
        X_test = pd.DataFrame(X_test)

    # 3. 验证维度
    if len(X_train) != len(y_train):
        raise ValueError(f"X_train 长度 ({len(X_train)}) 与 y_train ({len(y_train)}) 不匹配")

    # 4. 识别数值列(取训练集和测试集共有列)
    train_numeric_cols = X_train.select_dtypes(include=[np.number]).columns
    test_numeric_cols = X_test.select_dtypes(include=[np.number]).columns
    numeric_cols = [col for col in train_numeric_cols if col in test_numeric_cols]

    if len(numeric_cols) == 0:
        print("⚠️ 没有共同的数值列,跳过异常值处理")
        return X_train, y_train, X_test, y_test

    print(f"  对 {len(numeric_cols)} 个数值列进行异常值处理 (方法: {method}, 阈值: {threshold})")
    X_train_numeric = X_train[numeric_cols]

    # 5. 计算边界并截断
    if method == 'iqr':
        Q1 = X_train_numeric.quantile(0.25)
        Q3 = X_train_numeric.quantile(0.75)
        IQR = Q3 - Q1
        lower_bound = Q1 - threshold * IQR
        upper_bound = Q3 + threshold * IQR

        # 对训练集和测试集都进行截断
        for col in numeric_cols:
            X_train[col] = X_train[col].clip(lower=lower_bound[col], upper=upper_bound[col])
            X_test[col] = X_test[col].clip(lower=lower_bound[col], upper=upper_bound[col])

    elif method == 'zscore':
        # 稳健 Z-Score(基于中位数和 MAD)
        median = X_train_numeric.median()
        mad = (X_train_numeric - median).abs().median() * 1.4826  # 1/0.6745,转换为标准差估计

        # 处理常数列
        constant_cols = (mad == 0)
        if constant_cols.any():
            print(f"⚠️ 检测到 {constant_cols.sum()} 个常数列: {list(mad[mad == 0].index)}")
            mad = mad.replace(0, 1e-10)  # 避免除零

        # 对训练集和测试集都进行截断
        for col in numeric_cols:
            lower = median[col] - threshold * mad[col]
            upper = median[col] + threshold * mad[col]
            X_train[col] = X_train[col].clip(lower=lower, upper=upper)
            X_test[col] = X_test[col].clip(lower=lower, upper=upper)

    else:
        raise ValueError(f"不支持的方法: {method}")

    print(f"  处理完成,保留所有 {len(X_train)} 个训练样本")
    return X_train, y_train, X_test, y_test

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐