机器学习特征预处理之 移除异常值
·
示例代码 针对数值列
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,) | 单列截断 |
为什么能同时处理多个列?
-
Pandas 的向量化运算:所有操作都是逐元素执行的,无需显式循环
-
广播机制:Series(每列一个值)自动扩展到 DataFrame(每行都使用该值)
-
按列独立计算:每列的统计量(Q1, Q3, median, mad)独立计算
-
按行聚合:
.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
更多推荐



所有评论(0)