scikit-learn 数据预处理技术探讨:从缺失值填充到特征缩放的实战指南

在机器学习项目中,数据预处理是至关重要的一环。原始数据往往包含噪声、缺失值、量纲不一致等问题,直接使用这些数据训练模型会导致模型性能下降。scikit-learn 作为 Python 中最流行的机器学习库之一,提供了丰富的数据预处理工具。本文将深入探讨 scikit-learn 中的数据预处理技术,从缺失值填充到特征缩放,并通过实战代码示例详细说明每个步骤的实现方法。
1. 数据预处理的重要性
数据预处理是机器学习流程中的第一步,其目的是将原始数据转换为适合模型训练的格式。良好的数据预处理可以:
提高模型的准确性和稳定性
减少训练时间
避免模型过拟合
使模型更具可解释性
在 scikit-learn 中,所有预处理操作都位于
sklearn.preprocessing
模块中,同时
sklearn.impute
模块专门处理缺失值。
2. 缺失值处理
缺失值是数据集中常见的问题,处理不当会导致模型训练失败或产生偏差。
2.1 识别缺失值
首先,我们需要识别数据中的缺失值。使用 pandas 可以方便地查看缺失值情况:
import pandas as pd
import numpy as np

# 创建示例数据集
data = {
‘年龄’: [25, 30, np.nan, 35, 40],
‘收入’: [50000, 60000, 70000, np.nan, 80000],
‘教育程度’: [‘本科’, ‘硕士’, ‘博士’, ‘本科’, np.nan]
}

df = pd.DataFrame(data)
print(“原始数据:”)
print(df)
print(“\n缺失值统计:”)
print(df.isnull().sum())
输出结果:
原始数据:
年龄 收入 教育程度
0 25.0 50000.0 本科
1 30.0 60000.0 硕士
2 NaN 70000.0 博士
3 35.0 NaN 本科
4 40.0 80000.0 NaN

缺失值统计:
年龄 1
收入 1
教育程度 1
dtype: int64
2.2 删除缺失值
对于缺失值较少的情况,可以直接删除含有缺失值的行或列:
# 删除含有缺失值的行
df_drop_rows = df.dropna()
print(“删除缺失值行后的数据:”)
print(df_drop_rows)

# 删除含有缺失值的列
df_drop_cols = df.dropna(axis=1)
print(“\n删除缺失值列后的数据:”)
print(df_drop_cols)
2.3 均值/中位数/众数填充
对于数值型特征,常用均值或中位数填充;对于分类型特征,常用众数填充:
from sklearn.impute import SimpleImputer

# 数值型特征填充
num_imputer = SimpleImputer(strategy=’mean’) # 也可以用’median’
df_num = df[[‘年龄’, ‘收入’]].copy()
df_num_imputed = pd.DataFrame(num_imputer.fit_transform(df_num),
columns=df_num.columns)
print(“数值型特征均值填充结果:”)
print(df_num_imputed)

# 分类型特征填充
cat_imputer = SimpleImputer(strategy=’most_frequent’)
df_cat = df[[‘教育程度’]].copy()
df_cat_imputed = pd.DataFrame(cat_imputer.fit_transform(df_cat),
columns=df_cat.columns)
print(“\n分类型特征众数填充结果:”)
print(df_cat_imputed)
2.4 基于模型的填充
对于更复杂的场景,可以使用基于模型的填充方法,如 KNN 填充或迭代填充:
from sklearn.impute import KNNImputer, IterativeImputer

# KNN 填充
knn_imputer = KNNImputer(n_neighbors=2)
df_knn = df[[‘年龄’, ‘收入’]].copy()
df_knn_imputed = pd.DataFrame(knn_imputer.fit_transform(df_knn),
columns=df_knn.columns)
print(“KNN 填充结果:”)
print(df_knn_imputed)

# 迭代填充(基于回归模型)
iter_imputer = IterativeImputer(random_state=42)
df_iter = df[[‘年龄’, ‘收入’]].copy()
df_iter_imputed = pd.DataFrame(iter_imputer.fit_transform(df_iter),
columns=df_iter.columns)
print(“\n迭代填充结果:”)
print(df_iter_imputed)
2.5 填充策略选择
选择填充策略时需要考虑:
数据缺失机制
:完全随机缺失、随机缺失还是非随机缺失
数据量大小
:数据量大时,删除缺失值可能更合适
特征重要性
:重要特征应谨慎处理
业务背景
:结合业务知识选择填充值
3. 特征编码
对于分类特征,需要将其转换为数值形式,以便模型处理。
3.1 标签编码(Label Encoding)
将每个类别映射为一个整数:
from sklearn.preprocessing import LabelEncoder

# 标签编码
label_encoder = LabelEncoder()
df[‘教育程度_encoded’] = label_encoder.fit_transform(df[‘教育程度’].fillna(‘未知’))
print(“标签编码结果:”)
print(df[[‘教育程度’, ‘教育程度_encoded’]])
print(“\n编码映射:”)
for i, category in enumerate(label_encoder.classes_):
print(f”{category}: {i}”)
3.2 独热编码(One-Hot Encoding)
为每个类别创建一个新的二进制特征:
from sklearn.preprocessing import OneHotEncoder

# 独热编码
onehot_encoder = OneHotEncoder(sparse_output=False, handle_unknown=’ignore’)
df_onehot = onehot_encoder.fit_transform(df[[‘教育程度’]].fillna(‘未知’))
df_onehot_df = pd.DataFrame(df_onehot,
columns=onehot_encoder.get_feature_names_out([‘教育程度’]))
print(“独热编码结果:”)
print(df_onehot_df)
3.3 目标编码(Target Encoding)
对于高基数分类特征,目标编码是一种有效的编码方式:
from category_encoders import TargetEncoder

# 目标编码(需要目标变量)
# 假设我们有一个目标变量
df[‘目标’] = [0, 1, 1, 0, 1]

target_encoder = TargetEncoder(cols=[‘教育程度’])
df[‘教育程度_target_encoded’] = target_encoder.fit_transform(df[[‘教育程度’]], df[‘目标’])
print(“目标编码结果:”)
print(df[[‘教育程度’, ‘教育程度_target_encoded’]])
3.4 编码策略选择
标签编码
:适用于有序分类特征(如教育程度:小学<中学<大学) 独热编码 :适用于无序分类特征,但特征数量多时可能导致维度爆炸 目标编码 :适用于高基数分类特征,但需注意过拟合风险 4. 特征缩放 特征缩放是将不同量纲的特征转换到同一尺度,对基于距离的算法(如 KNN、SVM)和梯度下降算法至关重要。 4.1 标准化(Standardization) 将特征转换为均值为0、标准差为1的分布: from sklearn.preprocessing import StandardScaler # 标准化 scaler = StandardScaler() df_scaled = df[['年龄', '收入']].copy() df_scaled_values = scaler.fit_transform(df_scaled) df_scaled_df = pd.DataFrame(df_scaled_values, columns=['年龄_scaled', '收入_scaled']) print("标准化结果:") print(df_scaled_df) print(f"\n均值:{scaler.mean_}") print(f"标准差:{scaler.scale_}") 4.2 归一化(Normalization) 将特征缩放到[0, 1]区间: from sklearn.preprocessing import MinMaxScaler # 归一化 minmax_scaler = MinMaxScaler() df_normalized = df[['年龄', '收入']].copy() df_normalized_values = minmax_scaler.fit_transform(df_normalized) df_normalized_df = pd.DataFrame(df_normalized_values, columns=['年龄_normalized', '收入_normalized']) print("归一化结果:") print(df_normalized_df) print(f"\n最小值:{minmax_scaler.min_}") print(f"范围:{minmax_scaler.scale_}") 4.3 鲁棒缩放(Robust Scaling) 使用中位数和四分位数范围进行缩放,对异常值不敏感: from sklearn.preprocessing import RobustScaler # 鲁棒缩放 robust_scaler = RobustScaler() df_robust = df[['年龄', '收入']].copy() df_robust_values = robust_scaler.fit_transform(df_robust) df_robust_df = pd.DataFrame(df_robust_values, columns=['年龄_robust', '收入_robust']) print("鲁棒缩放结果:") print(df_robust_df) print(f"\n中位数:{robust_scaler.center_}") print(f"四分位距:{robust_scaler.scale_}") 4.4 缩放策略选择 标准化 :适用于数据近似正态分布的情况,对异常值敏感 归一化 :适用于需要固定范围的场景,对异常值敏感 鲁棒缩放 :适用于数据包含异常值的情况,更稳健 5. 特征工程 特征工程是从原始数据中提取新特征的过程,可以显著提升模型性能。 5.1 多项式特征 创建特征的多项式组合: from sklearn.preprocessing import PolynomialFeatures # 创建多项式特征 poly = PolynomialFeatures(degree=2, include_bias=False) df_poly = df[['年龄', '收入']].copy() df_poly_values = poly.fit_transform(df_poly) poly_feature_names = poly.get_feature_names_out(['年龄', '收入']) df_poly_df = pd.DataFrame(df_poly_values, columns=poly_feature_names) print("多项式特征结果:") print(df_poly_df.head()) 5.2 交互特征 创建特征之间的交互项: # 使用PolynomialFeatures创建交互特征 poly_interaction = PolynomialFeatures(degree=2, interaction_only=True, include_bias=False) df_interaction = df[['年龄', '收入']].copy() df_interaction_values = poly_interaction.fit_transform(df_interaction) interaction_feature_names = poly_interaction.get_feature_names_out(['年龄', '收入']) df_interaction_df = pd.DataFrame(df_interaction_values, columns=interaction_feature_names) print("交互特征结果:") print(df_interaction_df.head()) 5.3 分箱(Binning) 将连续特征转换为离散的类别: from sklearn.preprocessing import KBinsDiscretizer # 分箱 binner = KBinsDiscretizer(n_bins=3, encode='ordinal', strategy='uniform') df_binned = df[['年龄']].copy() df_binned_values = binner.fit_transform(df_binned) df_binned_df = pd.DataFrame(df_binned_values, columns=['年龄_binned']) print("分箱结果:") print(df_binned_df) print(f"\n分箱边界:{binner.bin_edges_}") 5.4 特征选择 在特征工程后,需要选择最重要的特征: from sklearn.feature_selection import SelectKBest, f_classif from sklearn.datasets import make_classification # 创建示例数据集 X, y = make_classification(n_samples=100, n_features=10, n_informative=5, n_redundant=2, random_state=42) # 选择前5个最佳特征 selector = SelectKBest(score_func=f_classif, k=5) X_selected = selector.fit_transform(X, y) print(f"原始特征数:{X.shape[1]}") print(f"选择后特征数:{X_selected.shape[1]}") print(f"特征得分:{selector.scores_}") 6. 完整的预处理流程 在实际项目中,通常需要将多个预处理步骤组合起来。scikit-learn 提供了 Pipeline 和 ColumnTransformer 来简化这个过程。 6.1 使用 Pipeline 组合预处理步骤 from sklearn.pipeline import Pipeline from sklearn.compose import ColumnTransformer from sklearn.model_selection import train_test_split from sklearn.ensemble import RandomForestClassifier # 创建示例数据集 data = { '年龄': [25, 30, 35, 40, 45, 50, 55, 60, 65, 70], '收入': [50000, 60000, 70000, 80000, 90000, 100000, 110000, 120000, 130000, 140000], '教育程度': ['本科', '硕士', '博士', '本科', '硕士', '博士', '本科', '硕士', '博士', '本科'], '目标': [0, 1, 1, 0, 1, 1, 0, 1, 1, 0] } df = pd.DataFrame(data) # 划分特征和目标 X = df.drop('目标', axis=1) y = df['目标'] # 划分训练集和测试集 X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) # 定义数值型和分类型特征 numeric_features = ['年龄', '收入'] categorical_features = ['教育程度'] # 创建预处理管道 preprocessor = ColumnTransformer( transformers=[ ('num', Pipeline([ ('imputer', SimpleImputer(strategy='mean')), ('scaler', StandardScaler()) ]), numeric_features), ('cat', Pipeline([ ('imputer', SimpleImputer(strategy='most_frequent')), ('onehot', OneHotEncoder(handle_unknown='ignore')) ]), categorical_features) ]) # 创建完整管道 pipeline = Pipeline([ ('preprocessor', preprocessor), ('classifier', RandomForestClassifier(random_state=42)) ]) # 训练模型 pipeline.fit(X_train, y_train) # 评估模型 score = pipeline.score(X_test, y_test) print(f"模型准确率:{score:.4f}") # 预测新数据 new_data = pd.DataFrame({ '年龄': [28, 32], '收入': [55000, 65000], '教育程度': ['硕士', '本科'] }) predictions = pipeline.predict(new_data) print(f"预测结果:{predictions}") 6.2 使用 FunctionTransformer 自定义预处理 对于更复杂的预处理需求,可以使用 FunctionTransformer : from sklearn.preprocessing import FunctionTransformer # 自定义对数变换函数 def log_transform(x): return np.log1p(x) # log(1+x),避免log(0) # 创建对数变换器 log_transformer = FunctionTransformer(log_transform, validate=False) # 在Pipeline中使用 pipeline_custom = Pipeline([ ('preprocessor', ColumnTransformer([ ('num', Pipeline([ ('imputer', SimpleImputer(strategy='mean')), ('log', log_transformer), ('scaler', StandardScaler()) ]), numeric_features), ('cat', Pipeline([ ('imputer', SimpleImputer(strategy='most_frequent')), ('onehot', OneHotEncoder(handle_unknown='ignore')) ]), categorical_features) ])), ('classifier', RandomForestClassifier(random_state=42)) ]) # 训练模型 pipeline_custom.fit(X_train, y_train) score_custom = pipeline_custom.score(X_test, y_test) print(f"自定义预处理模型准确率:{score_custom:.4f}") 7. 实战案例:房价预测 让我们通过一个房价预测的完整案例来演示所有预处理技术的综合应用。 7.1 加载和探索数据 from sklearn.datasets import fetch_california_housing # 加载加州房价数据集 housing = fetch_california_housing() X = pd.DataFrame(housing.data, columns=housing.feature_names) y = housing.target print("数据集形状:", X.shape) print("\n特征名称:", X.columns.tolist()) print("\n前5行数据:") print(X.head()) print("\n缺失值统计:") print(X.isnull().sum()) 7.2 数据预处理 from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler, OneHotEncoder from sklearn.impute import SimpleImputer from sklearn.compose import ColumnTransformer from sklearn.pipeline import Pipeline from sklearn.ensemble import GradientBoostingRegressor from sklearn.metrics import mean_squared_error, r2_score # 划分数据集 X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) # 定义数值型和分类型特征(这里假设所有特征都是数值型) numeric_features = X.columns.tolist() categorical_features = [] # 如果有分类特征,添加到这里 # 创建预处理管道 preprocessor = ColumnTransformer( transformers=[ ('num', Pipeline([ ('imputer', SimpleImputer(strategy='median')), # 中位数填充 ('scaler', StandardScaler()) # 标准化 ]), numeric_features), ('cat', Pipeline([ ('imputer', SimpleImputer(strategy='most_frequent')), ('onehot', OneHotEncoder(handle_unknown='ignore')) ]), categorical_features) ]) # 创建完整管道 pipeline = Pipeline([ ('preprocessor', preprocessor), ('regressor', GradientBoostingRegressor(random_state=42)) ]) # 训练模型 pipeline.fit(X_train, y_train) # 预测和评估 y_pred = pipeline.predict(X_test) mse = mean_squared_error(y_test, y_pred) r2 = r2_score(y_test, y_pred) print(f"均方误差:{mse:.4f}") print(f"R²分数:{r2:.4f}") 7.3 特征重要性分析 # 获取特征重要性 regressor = pipeline.named_steps['regressor'] feature_importances = regressor.feature_importances_ # 获取特征名称 preprocessor = pipeline.named_steps['preprocessor'] feature_names = [] for name, transformer, features in preprocessor.transformers_: if name == 'num': feature_names.extend(features) elif name == 'cat': # 获取独热编码后的特征名 onehot = transformer.named_steps['onehot'] cat_features = onehot.get_feature_names_out(features) feature_names.extend(cat_features) # 创建特征重要性DataFrame importance_df = pd.DataFrame({ 'feature': feature_names, 'importance': feature_importances }).sort_values('importance', ascending=False) print("特征重要性(前10):") print(importance_df.head(10)) 8. 最佳实践和注意事项 8.1 避免数据泄露 在预处理时,必须确保测试集不参与训练过程的任何计算: # 错误做法:在整个数据集上拟合预处理器 # scaler = StandardScaler() # X_scaled = scaler.fit_transform(X) # 错误!测试集信息泄露 # 正确做法:只在训练集上拟合 scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) # 只在训练集上拟合 X_test_scaled = scaler.transform(X_test) # 在测试集上只转换 8.2 处理高基数分类特征 对于类别数量很多的分类特征,可以考虑以下方法: # 方法1:限制类别数量 def limit_categories(series, max_categories=10): value_counts = series.value_counts() top_categories = value_counts.head(max_categories).index return series.apply(lambda x: x if x in top_categories else '其他') # 方法2:使用频率编码 def frequency_encoding(series): freq = series.value_counts() return series.map(freq) # 方法3:使用嵌入(需要深度学习) 8.3 处理时间序列数据 对于时间序列数据,需要特殊处理: # 提取时间特征 def extract_time_features(df, time_col): df[time_col] = pd.to_datetime(df[time_col]) df['year'] = df[time_col].dt.year df['month'] = df[time_col].dt.month df['day'] = df[time_col].dt.day df['dayofweek'] = df[time_col].dt.dayofweek df['hour'] = df[time_col].dt.hour return df # 滞后特征 def create_lag_features(df, column, lags=[1, 7, 30]): for lag in lags: df[f'{column}_lag_{lag}'] = df[column].shift(lag) return df 8.4 处理不平衡数据 对于不平衡数据集,可以使用以下方法: from imblearn.over_sampling import SMOTE from imblearn.under_sampling import RandomUnderSampler # 过采样 smote = SMOTE(random_state=42) X_resampled, y_resampled = smote.fit_resample(X_train, y_train) # 欠采样 undersampler = RandomUnderSampler(random_state=42) X_undersampled, y_undersampled = undersampler.fit_resample(X_train, y_train) 9. 总结 本文详细介绍了 scikit-learn 中的数据预处理技术,包括: 缺失值处理 :删除、均值/中位数/众数填充、KNN填充、迭代填充 特征编码 :标签编码、独热编码、目标编码 特征缩放 :标准化、归一化、鲁棒缩放 特征工程 :多项式特征、交互特征、分箱、特征选择 完整流程 :使用 Pipeline 和 ColumnTransformer 组合预处理步骤 实战案例 :房价预测的完整预处理流程 最佳实践 :避免数据泄露、处理高基数特征、时间序列处理等 数据预处理是机器学习成功的关键,选择合适的预处理方法需要根据数据特点、模型需求和业务背景综合考虑。通过 scikit-learn 提供的丰富工具,我们可以高效地构建健壮的预处理流程,为模型训练打下坚实的基础。 在实际项目中,建议: 始终使用 Pipeline 组合预处理和模型训练步骤 在训练集上拟合预处理器,在测试集上只进行转换 根据数据分布和模型需求选择合适的预处理方法 定期评估预处理对模型性能的影响 通过系统性的数据预处理,我们可以显著提升模型性能,使机器学习项目更加可靠和高效。