机器学习特征预处理:缺失值处理全攻略
- [📊 引言:为什么缺失值处理如此重要?](#📊 引言:为什么缺失值处理如此重要?)
- [📈 第一部分:理解缺失值的类型与模式](#📈 第一部分:理解缺失值的类型与模式)
-
- [1.1 缺失值类型分析](#1.1 缺失值类型分析)
- [1.2 缺失值检测与可视化](#1.2 缺失值检测与可视化)
- [🔧 第二部分:基于Pandas的缺失值处理](#🔧 第二部分:基于Pandas的缺失值处理)
-
- [2.1 Pandas基础检测方法](#2.1 Pandas基础检测方法)
- [2.2 Pandas处理策略详解](#2.2 Pandas处理策略详解)
- [2.3 Pandas方法优缺点总结](#2.3 Pandas方法优缺点总结)
- [🤖 第三部分:基于Scikit-learn的SimpleImputer处理](#🤖 第三部分:基于Scikit-learn的SimpleImputer处理)
-
- [3.1 SimpleImputer核心优势](#3.1 SimpleImputer核心优势)
- [3.2 SimpleImputer实战应用](#3.2 SimpleImputer实战应用)
- [3.3 构建智能填充管道](#3.3 构建智能填充管道)
- [3.4 高级技巧:自定义填充策略](#3.4 高级技巧:自定义填充策略)
- [🎯 第四部分:实战案例:房价预测中的缺失值处理](#🎯 第四部分:实战案例:房价预测中的缺失值处理)
-
- [4.1 案例背景](#4.1 案例背景)
- [4.2 处理流程设计](#4.2 处理流程设计)
- [4.3 完整实现代码](#4.3 完整实现代码)
- [📊 第五部分:性能对比与最佳实践](#📊 第五部分:性能对比与最佳实践)
-
- [5.1 Pandas vs Scikit-learn性能对比](#5.1 Pandas vs Scikit-learn性能对比)
- [5.2 最佳实践指南](#5.2 最佳实践指南)
📊 引言:为什么缺失值处理如此重要?
在现实世界的数据分析项目中,缺失数据 几乎是不可避免的!无论是传感器故障、用户未填写信息,还是数据采集过程中的技术问题,缺失值都会对我们的机器学习模型造成严重影响。根据统计,超过80%的数据科学项目都需要处理缺失值问题!
缺失值带来的挑战:
- 大多数机器学习算法无法直接处理缺失值
- 可能导致模型偏差和性能下降
- 影响特征工程和模型解释性
- 降低数据质量和分析可靠性
今天,我们将深入探讨两种主流的缺失值处理方法:基于Pandas 的传统方法和基于Scikit-learn的机器学习友好方法。
📈 第一部分:理解缺失值的类型与模式
1.1 缺失值类型分析
缺失值类型
完全随机缺失 MCAR
随机缺失 MAR
非随机缺失 MNAR
缺失与任何变量无关
处理相对简单
缺失与观测到的变量相关
需要条件处理
缺失与未观测到的变量相关
处理最复杂
1.2 缺失值检测与可视化
在开始处理之前,我们需要先了解数据中缺失值的分布情况:
| 特征名称 | 总样本数 | 缺失数量 | 缺失比例 | 缺失模式 |
|---|---|---|---|---|
| 年龄 | 10,000 | 1,200 | 12% | 随机缺失 |
| 收入 | 10,000 | 2,500 | 25% | 非随机缺失 |
| 教育程度 | 10,000 | 800 | 8% | 完全随机缺失 |
| 工作经验 | 10,000 | 1,500 | 15% | 随机缺失 |
关键洞察: 不同特征的缺失比例和模式可能完全不同,这决定了我们应该采用不同的处理策略!
🔧 第二部分:基于Pandas的缺失值处理
2.1 Pandas基础检测方法
python
import pandas as pd
import numpy as np
# 创建示例数据集
data = {
'年龄': [25, np.nan, 30, 35, np.nan, 40],
'收入': [50000, 60000, np.nan, 75000, 80000, np.nan],
'教育程度': ['本科', '硕士', np.nan, '博士', '本科', '硕士'],
'工作经验': [2, 5, 8, np.nan, 12, 15]
}
df = pd.DataFrame(data)
# 1. 检测缺失值
print("缺失值统计:")
print(df.isnull().sum())
print("\n缺失值比例:")
print(df.isnull().mean() * 100)
# 2. 可视化缺失值
import matplotlib.pyplot as plt
import seaborn as sns
plt.figure(figsize=(10, 6))
sns.heatmap(df.isnull(), cbar=False, cmap='viridis')
plt.title('缺失值热力图')
plt.show()
2.2 Pandas处理策略详解
策略1:直接删除法
适用场景: 缺失比例很小(通常<5%),且缺失为完全随机
python
# 删除包含缺失值的行
df_drop_rows = df.dropna()
# 删除包含缺失值的列
df_drop_cols = df.dropna(axis=1)
# 仅当某行所有值都缺失时才删除
df_drop_all = df.dropna(how='all')
print(f"原始数据形状: {df.shape}")
print(f"删除行后形状: {df_drop_rows.shape}")
print(f"删除列后形状: {df_drop_cols.shape}")
策略2:简单填充法
python
# 使用固定值填充
df_fixed = df.fillna({
'年龄': df['年龄'].mean(),
'收入': 50000, # 使用业务常识
'教育程度': '未知',
'工作经验': df['工作经验'].median()
})
# 前向填充(适用于时间序列)
df_ffill = df.fillna(method='ffill')
# 后向填充
df_bfill = df.fillna(method='bfill')
策略3:统计量填充
python
# 计算各列的统计量
age_mean = df['年龄'].mean()
age_median = df['年龄'].median()
income_mode = df['收入'].mode()[0] if not df['收入'].mode().empty else np.nan
# 根据数据分布选择填充策略
df_statistical = df.copy()
df_statistical['年龄'] = df_statistical['年龄'].fillna(age_median) # 对偏态分布用中位数
df_statistical['收入'] = df_statistical['收入'].fillna(income_mode)
策略4:高级填充方法
python
# 使用分组均值填充
# 假设我们根据教育程度分组填充收入
df_group = df.copy()
df_group['收入'] = df_group.groupby('教育程度')['收入'].transform(
lambda x: x.fillna(x.mean())
)
# 使用插值法(适用于有序数据)
df_interpolate = df.copy()
df_interpolate['年龄'] = df_interpolate['年龄'].interpolate(method='linear')
2.3 Pandas方法优缺点总结
| 方法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 直接删除 | 简单快速 | 可能丢失重要信息 | 缺失很少且随机 |
| 固定值填充 | 简单易实现 | 可能引入偏差 | 业务逻辑明确 |
| 统计量填充 | 保持数据分布 | 忽略特征相关性 | 数值型特征 |
| 前向/后向填充 | 保持数据顺序 | 仅适用于有序数据 | 时间序列数据 |
| 分组填充 | 考虑数据分组 | 需要足够的分组样本 | 分类特征相关 |
🤖 第三部分:基于Scikit-learn的SimpleImputer处理
3.1 SimpleImputer核心优势
为什么选择SimpleImputer?
- 管道集成友好:完美适配Scikit-learn的Pipeline
- 一致性保证:训练集和测试集使用相同的填充策略
- 高效处理:针对大数据集优化
- 多种策略:支持均值、中位数、众数、常数等填充
3.2 SimpleImputer实战应用
python
from sklearn.impute import SimpleImputer
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler, OneHotEncoder
# 创建更复杂的数据集
np.random.seed(42)
n_samples = 1000
complex_data = pd.DataFrame({
'年龄': np.random.normal(35, 10, n_samples),
'收入': np.random.lognormal(10.5, 0.8, n_samples),
'教育程度': np.random.choice(['高中', '本科', '硕士', '博士', np.nan], n_samples, p=[0.2, 0.4, 0.25, 0.1, 0.05]),
'工作经验': np.random.exponential(5, n_samples),
'城市': np.random.choice(['北京', '上海', '广州', '深圳', np.nan], n_samples),
'信用评分': np.random.randint(300, 850, n_samples)
})
# 人为添加缺失值
mask = np.random.random(complex_data.shape) < 0.15
complex_data = complex_data.mask(mask)
print("处理前缺失值统计:")
print(complex_data.isnull().sum())
3.3 构建智能填充管道
python
# 定义数值型和分类型特征
numeric_features = ['年龄', '收入', '工作经验', '信用评分']
categorical_features = ['教育程度', '城市']
# 创建数值型特征处理器
numeric_transformer = Pipeline(steps=[
('imputer', SimpleImputer(strategy='median')), # 对数值型使用中位数填充
('scaler', StandardScaler()) # 标准化
])
# 创建分类型特征处理器
categorical_transformer = Pipeline(steps=[
('imputer', SimpleImputer(strategy='most_frequent')), # 使用众数填充
('onehot', OneHotEncoder(handle_unknown='ignore', sparse_output=False))
])
# 组合所有处理器
preprocessor = ColumnTransformer(
transformers=[
('num', numeric_transformer, numeric_features),
('cat', categorical_transformer, categorical_features)
])
# 应用预处理
processed_data = preprocessor.fit_transform(complex_data)
print(f"原始数据形状: {complex_data.shape}")
print(f"处理后数据形状: {processed_data.shape}")
3.4 高级技巧:自定义填充策略
python
from sklearn.base import BaseEstimator, TransformerMixin
class AdvancedImputer(BaseEstimator, TransformerMixin):
"""自定义高级填充器"""
def __init__(self, numeric_strategy='median', categorical_strategy='most_frequent'):
self.numeric_strategy = numeric_strategy
self.categorical_strategy = categorical_strategy
self.numeric_imputer = None
self.categorical_imputer = None
def fit(self, X, y=None):
# 分离数值和分类特征
numeric_cols = X.select_dtypes(include=[np.number]).columns
categorical_cols = X.select_dtypes(exclude=[np.number]).columns
# 训练数值型填充器
if len(numeric_cols) > 0:
self.numeric_imputer = SimpleImputer(strategy=self.numeric_strategy)
self.numeric_imputer.fit(X[numeric_cols])
# 训练分类型填充器
if len(categorical_cols) > 0:
self.categorical_imputer = SimpleImputer(strategy=self.categorical_strategy)
self.categorical_imputer.fit(X[categorical_cols])
return self
def transform(self, X):
X_transformed = X.copy()
# 转换数值特征
if self.numeric_imputer is not None:
numeric_cols = X.select_dtypes(include=[np.number]).columns
X_transformed[numeric_cols] = self.numeric_imputer.transform(X[numeric_cols])
# 转换分类特征
if self.categorical_imputer is not None:
categorical_cols = X.select_dtypes(exclude=[np.number]).columns
X_transformed[categorical_cols] = self.categorical_imputer.transform(X[categorical_cols])
return X_transformed
# 使用自定义填充器
advanced_imputer = AdvancedImputer()
df_advanced = advanced_imputer.fit_transform(complex_data)
🎯 第四部分:实战案例:房价预测中的缺失值处理
4.1 案例背景
假设我们正在处理一个房价预测数据集,包含以下特征:
- 数值特征:房屋面积、卧室数量、建造年份、地理位置评分
- 分类特征:房屋类型、所在区域、装修状况
- 目标变量:房屋价格
4.2 处理流程设计
原始房价数据
缺失值分析
数值特征
分类特征
建造年份
缺失15%
地理位置评分
缺失8%
装修状况
缺失5%
所在区域
缺失2%
使用同区域房屋的
中位数填充
使用KNN填充
使用众数填充
使用模型预测填充
预处理完成的数据
机器学习模型训练
4.3 完整实现代码
python
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_absolute_error, r2_score
# 模拟房价数据集
def create_housing_data(n_samples=2000):
np.random.seed(42)
data = pd.DataFrame({
'面积': np.random.normal(120, 40, n_samples),
'卧室数': np.random.randint(1, 6, n_samples),
'建造年份': np.random.randint(1950, 2023, n_samples),
'地理位置评分': np.random.uniform(1, 10, n_samples),
'房屋类型': np.random.choice(['公寓', '别墅', '联排', '独栋'], n_samples),
'所在区域': np.random.choice(['A区', 'B区', 'C区', 'D区'], n_samples),
'装修状况': np.random.choice(['精装', '简装', '毛坯', '豪华'], n_samples)
})
# 生成房价(目标变量)
data['房价'] = (
data['面积'] * 5000 +
data['卧室数'] * 30000 +
(2023 - data['建造年份']) * -1000 +
data['地理位置评分'] * 10000 +
np.random.normal(0, 50000, n_samples)
)
# 添加缺失值
missing_mask = np.random.random(data.shape) < 0.12
data_with_missing = data.mask(missing_mask)
return data_with_missing
# 创建数据
housing_data = create_housing_data()
# 分离特征和目标
X = housing_data.drop('房价', axis=1)
y = housing_data['房价']
# 划分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
# 构建预处理管道
from sklearn.pipeline import Pipeline
from sklearn.impute import KNNImputer
# 对数值特征使用KNN填充(考虑特征相关性)
numeric_features = ['面积', '卧室数', '建造年份', '地理位置评分']
categorical_features = ['房屋类型', '所在区域', '装修状况']
preprocessor = ColumnTransformer(
transformers=[
('num', Pipeline([
('imputer', KNNImputer(n_neighbors=5)),
('scaler', StandardScaler())
]), numeric_features),
('cat', Pipeline([
('imputer', SimpleImputer(strategy='most_frequent')),
('encoder', OneHotEncoder(handle_unknown='ignore'))
]), categorical_features)
])
# 构建完整模型管道
model_pipeline = Pipeline([
('preprocessor', preprocessor),
('regressor', RandomForestRegressor(n_estimators=100, random_state=42))
])
# 训练模型
model_pipeline.fit(X_train, y_train)
# 评估模型
y_pred = model_pipeline.predict(X_test)
print("模型性能评估:")
print(f"R²分数: {r2_score(y_test, y_pred):.4f}")
print(f"平均绝对误差: {mean_absolute_error(y_test, y_pred):.2f}")
# 对比不同填充策略的效果
strategies = ['mean', 'median', 'most_frequent', 'constant']
results = []
for strategy in strategies:
if strategy == 'constant':
imputer = SimpleImputer(strategy=strategy, fill_value=0)
else:
imputer = SimpleImputer(strategy=strategy)
# 简单处理数值特征
X_train_processed = imputer.fit_transform(X_train[numeric_features])
X_test_processed = imputer.transform(X_test[numeric_features])
# 训练简单模型
model = RandomForestRegressor(n_estimators=50, random_state=42)
model.fit(X_train_processed, y_train)
y_pred_simple = model.predict(X_test_processed)
results.append({
'策略': strategy,
'R²': r2_score(y_test, y_pred_simple),
'MAE': mean_absolute_error(y_test, y_pred_simple)
})
results_df = pd.DataFrame(results)
print("\n不同填充策略效果对比:")
print(results_df)

📊 第五部分:性能对比与最佳实践
5.1 Pandas vs Scikit-learn性能对比
| 维度 | Pandas方法 | Scikit-learn SimpleImputer |
|---|---|---|
| 易用性 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| 管道集成 | ⭐⭐ | ⭐⭐⭐⭐⭐ |
| 大数据性能 | ⭐⭐⭐ | ⭐⭐⭐⭐ |
| 策略多样性 | ⭐⭐⭐⭐ | ⭐⭐⭐ |
| 一致性保证 | ⭐⭐ | ⭐⭐⭐⭐⭐ |
| 自定义灵活性 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
5.2 最佳实践指南
-
先分析后处理
- 可视化缺失模式
- 理解缺失机制(MCAR/MAR/MNAR)
- 评估缺失比例
-
分类型处理
python# 根据缺失比例采用不同策略 def smart_imputation_strategy(df, threshold=0.05): strategies = {} for col in df.columns: missing_ratio = df[col].isnull().mean() if missing_ratio < threshold: # 少量缺失:删除或简单填充 if df[col].dtype in ['int64', 'float64']: strategies[col] = 'median' else: strategies[col] = 'most_frequent' elif missing_ratio < 0.3: # 中等缺失:统计填充或模型填充 strategies[col] = 'knn' if df[col].dtype in ['int64', 'float64'] else 'most_frequent' else: # 大量缺失:考虑删除特征或高级方法 strategies[col] = 'consider_drop' return