3. 数据清洗基础:缺失值处理与异常值检测
数据清洗是特征工程中最基础、最耗时但回报最高的环节。原始数据通常包含缺失值、异常值和重复值,若不处理,后续的分箱、编码和建模都会产生偏差。本章聚焦缺失值处理和异常值检测与处理两大核心任务,提供从判断到实施的全流程指南。
核心原则:缺失与异常处理没有绝对正确的唯一方法,必须结合业务含义、数据分布和模型鲁棒性综合决策。先理解“为什么缺失/异常”,再选择“如何处理”。
3.1 缺失值处理
缺失值产生的原因包括:设备故障、用户未填写、数据合并错位、隐私脱敏等。处理前需先判断缺失机制,再选择删除、填充或插值策略。
3.1.1 缺失机制分类
| 类型 | 含义 | 示例 | 处理倾向 |
|---|---|---|---|
| MCAR(完全随机缺失) | 缺失与否完全独立于任何变量 | 传感器随机断电 | 可直接删除或简单填充 |
| MAR(随机缺失) | 缺失依赖于其他观测变量 | 高收入人群更少填写收入 | 需借助其他变量建模填充 |
| MNAR(非随机缺失) | 缺失依赖于缺失变量本身 | 疼痛越严重越不愿报告 | 需业务假设或敏感度分析 |
3.1.2 删除法
- 行删除(listwise deletion):直接删除含缺失值的样本。适用于缺失率低(<5%)且MCAR场景。
- 列删除:删除缺失率超过阈值(如70%)的特征。适用于特征信息量极低的情况。
- 成对删除(pairwise deletion):只在具体计算时排除缺失值,保留其他非缺失部分。适用于相关性分析。
注意:删除法会损失样本量,当缺失率较高或数据量有限时,可能导致模型过拟合或代表性不足。删除前务必评估剩余样本是否仍能覆盖业务场景。
3.1.3 填充法
| 方法 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 固定值填充(0/均值/中位数/众数) | 数值型、分布稳定 | 简单快速 | 扭曲分布、降低方差 |
| 前后向填充(ffill/bfill) | 时间序列、有序数据 | 保留时序趋势 | 不适用于无序数据 |
| KNN填充 | 特征间存在相关性 | 利用相似样本信息 | 计算量大、需调K值 |
| 回归/模型填充 | 特征间线性/非线性关系明确 | 精度高、保留关系 | 可能引入模型偏差 |
| 多重插补(MICE) | 缺失机制复杂、需不确定性估计 | 考虑填充不确定性 | 实现复杂、计算成本高 |
3.1.4 插值法
插值法主要用于时间序列或有序数据,利用已知数据点之间的数学关系估算缺失值。
- 线性插值:假设缺失点前后数据呈线性变化,适用于趋势平稳的序列。
- 多项式插值:使用低阶多项式拟合局部窗口,适用于曲线变化较缓的场景。
- 样条插值:分段低阶多项式平滑连接,适用于非平稳、有波动的序列。
- 时间加权插值:根据时间间隔权重填充,适用于不等间隔时间序列。
# Python 示例:缺失值处理
import pandas as pd
import numpy as np
from sklearn.impute import KNNImputer
# 创建示例数据
df = pd.DataFrame({
'age': [25, np.nan, 30, 35, np.nan, 40],
'income': [50000, 60000, np.nan, 80000, 90000, np.nan],
'education': [1, 2, 2, 3, np.nan, 3]
})
# 1. 均值填充
df['age_filled'] = df['age'].fillna(df['age'].mean())
# 2. 前向填充(时间序列场景)
df['income_ffill'] = df['income'].fillna(method='ffill')
# 3. KNN填充
imputer = KNNImputer(n_neighbors=2)
df_knn = pd.DataFrame(imputer.fit_transform(df[['age', 'income', 'education']]),
columns=['age_knn', 'income_knn', 'education_knn'])
print("原始数据:\n", df)
print("\nKNN填充结果:\n", df_knn)
实践建议:对于分类变量,优先使用众数填充或新增“缺失”类别;对于连续变量,中位数填充比均值更稳健;对于时间序列,先尝试线性插值,再评估是否需要更复杂的样条插值。
3.2 异常值检测与处理
异常值(outlier)是指显著偏离数据集中趋势的观测值。异常可能源于测量误差、数据录入错误、或真实的稀有事件。检测异常值的方法分为统计方法、距离方法和密度方法。
3.2.1 IQR(四分位距法)
IQR法基于数据分布的分位数,不假设正态分布,适用于偏态数据。
- 计算步骤:
- 计算第一四分位数(Q1)和第三四分位数(Q3)。
- 计算IQR = Q3 - Q1。
- 定义异常阈值:下限 = Q1 - 1.5×IQR,上限 = Q3 + 1.5×IQR。
- 超出上下限的值视为异常。
- 适用场景:单变量、分布非对称、对极端值不敏感的场景。
- 系数调整:使用3×IQR可识别极端异常(far outlier)。
# Python 示例:IQR异常检测
import numpy as np
data = np.array([10, 12, 13, 15, 16, 18, 20, 22, 25, 30, 100, 200])
Q1 = np.percentile(data, 25)
Q3 = np.percentile(data, 75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
outliers = data[(data < lower_bound) | (data > upper_bound)]
print(f"Q1={Q1:.2f}, Q3={Q3:.2f}, IQR={IQR:.2f}")
print(f"下限={lower_bound:.2f}, 上限={upper_bound:.2f}")
print(f"异常值: {outliers}")
3.2.2 Z-score(标准分数法)
Z-score基于均值和标准差,假设数据近似正态分布。
- 计算公式:Z = (x - μ) / σ,其中μ为均值,σ为标准差。
- 判断标准:通常|Z| > 3视为异常(对应正态分布99.7%置信区间)。
- 局限性:对均值和标准差敏感,小样本或存在多个异常时可能失效(掩蔽效应)。
- 改进版:MAD(中位数绝对偏差)法,使用中位数替代均值,更稳健。
# Python 示例:Z-score异常检测
from scipy import stats
import numpy as np
data = np.array([10, 12, 13, 15, 16, 18, 20, 22, 25, 30, 100, 200])
z_scores = np.abs(stats.zscore(data))
threshold = 3
outliers = data[z_scores > threshold]
print(f"Z-scores: {z_scores}")
print(f"异常值: {outliers}")
3.2.3 DBSCAN(基于密度的空间聚类)
DBSCAN将高密度区域聚为一类,低密度点标记为异常(噪声点)。无需预设聚类数量,能识别任意形状的簇。
| 参数 | 含义 | 调参建议 |
|---|---|---|
| eps | 邻域半径 | 使用k距离图(k-distance graph)找到拐点 |
| min_samples | 核心点最少邻域样本数 | 一般取特征维度的2倍,或根据经验设置 |
| metric | 距离度量(euclidean/manhattan等) | 高维数据考虑cosine或曼哈顿距离 |
# Python 示例:DBSCAN异常检测
from sklearn.cluster import DBSCAN
import numpy as np
# 生成示例数据(含异常点)
np.random.seed(42)
X = np.random.randn(200, 2)
X = np.vstack([X, np.array([[5, 5], [-5, -5], [6, -4]])]) # 加入3个异常点
# DBSCAN聚类
db = DBSCAN(eps=0.5, min_samples=5)
labels = db.fit_predict(X)
# 标签为-1的点视为异常
outliers = X[labels == -1]
print(f"异常点数量: {len(outliers)}")
print(f"异常点坐标:\n{outliers}")
方法对比:IQR法简单直观,适合快速筛查;Z-score适合正态分布数据;DBSCAN能检测多变量异常和局部异常,但需调参且高维数据效果下降。实际项目中常组合使用:先用IQR或Z-score做单变量初筛,再用DBSCAN做多变量精细检测。
3.2.4 异常值处理方法
| 方法 | 操作 | 适用场景 |
|---|---|---|
| 删除 | 直接移除异常样本 | 异常比例低(<1%)、确认是录入错误 |
| 截尾(winsorize) | 将异常值替换为阈值(如上下1%分位数) | 保留样本量、减少极端值影响 |
| 填充 | 用均值/中位数/众数替换异常值 | 异常值明显不合理但无法删除 |
| 离散化 | 将连续变量分箱,异常值归入首尾箱 | 后续需要分箱建模的场景 |
| 单独建模 | 将异常值作为独立类别或单独处理 | 异常值本身包含业务信息(如欺诈检测) |
业务优先原则:异常值不一定是“错误”。例如信用卡交易中的大额消费、医疗数据中的危急值,都是需要保留并单独分析的重要信号。处理前务必与业务方确认异常值的业务含义。
3.3 综合实践建议
- 先理解,再处理:对每个缺失或异常值,先问“为什么”,再决定“怎么办”。
- 记录处理日志:记录缺失率、异常比例、处理方法、替换值等,便于复现和审计。
- 交叉验证影响:在建模前,对比处理前后的模型性能,避免过度清洗导致信息损失。
- 管道化处理:将缺失和异常处理封装为sklearn Transformer或pipeline组件,确保训练集和测试集使用相同逻辑。
- 业务规则优先:当业务有明确阈值(如年龄>120视为异常)时,优先使用业务规则而非统计方法。
本章小结:缺失值处理的核心是“判断缺失机制 → 选择删除/填充/插值”;异常值检测的核心是“根据数据特征选择IQR/Z-score/DBSCAN → 结合业务决定处理方式”。两者都是特征工程中不可跳过的数据准备步骤,直接影响后续分箱和建模的质量。