跳到主要内容

数据预处理

数据预处理是机器学习流程中至关重要的一环。现实世界中的原始数据往往是"脏"的:存在缺失值、特征尺度不一致、类别特征未编码、可能包含异常值等问题。未经处理的数据直接输入模型,往往会导致训练困难、收敛缓慢甚至结果不准确。

核心原则

预处理的目的是让数据更适合机器学习算法。记住一个关键点:预处理参数(如均值、标准差、最大最小值)必须从训练集学习,然后应用到训练集和测试集。绝不能用测试集的信息来预处理训练数据,这会造成数据泄露(Data Leakage)。

为什么需要预处理?

特征尺度问题

不同特征的度量单位和取值范围可能差异很大。比如"年龄"可能在 0-100 之间,而"收入"可能在 0-1000000 之间。这种差异会影响基于距离的算法(如 KNN、SVM)和梯度下降优化的算法(如神经网络、逻辑回归)。

对于 KNN 算法,距离计算时取值范围大的特征会主导距离值,导致取值范围小的特征被忽略。对于梯度下降,不同尺度的特征会导致损失函数的等高线呈狭长形状,收敛速度变慢。

类别特征问题

大多数机器学习算法只能处理数值输入。像"红色"、"蓝色"、"绿色"这样的类别特征,必须转换为数值形式才能被模型理解。

缺失值问题

真实数据集中经常存在缺失值,可能是数据采集过程中的遗漏,也可能是某些信息确实不存在。大多数算法无法直接处理缺失值,需要进行填充或删除。

异常值问题

异常值可能是数据录入错误,也可能是真实的极端情况。异常值会影响均值和标准差的计算,进而影响模型的训练。

特征缩放

特征缩放是预处理中最常用的操作之一。sklearn 提供了多种缩放方法,适用于不同场景。

StandardScaler(标准化)

StandardScaler 将特征转换为均值为 0、标准差为 1 的分布。这是最常用的缩放方法。

数学原理

z=xμσz = \frac{x - \mu}{\sigma}

其中 μ\mu 是特征的均值,σ\sigma 是特征的标准差。

适用场景

  • 特征近似服从正态分布
  • 使用梯度下降优化的算法(逻辑回归、神经网络)
  • 使用距离度量的算法(KNN、SVM、PCA)

注意事项

  • 标准化不改变数据的分布形状,只是进行了平移和缩放
  • 对异常值敏感,极端值会影响均值和标准差的计算
from sklearn.preprocessing import StandardScaler
import numpy as np

# 示例数据
X = np.array([[1, 100],
[2, 200],
[3, 300],
[4, 400],
[5, 500]])

scaler = StandardScaler()

# fit 学习均值和标准差
scaler.fit(X)

# transform 应用转换
X_scaled = scaler.transform(X)

print("原始数据均值:", scaler.mean_)
print("原始数据标准差:", scaler.scale_)
print("标准化后均值:", X_scaled.mean(axis=0)) # 接近 [0, 0]
print("标准化后标准差:", X_scaled.std(axis=0)) # 接近 [1, 1]

fit_transform vs fit + transform

# 这两种方式等价
X_scaled = scaler.fit_transform(X)

# 等价于
scaler.fit(X)
X_scaled = scaler.transform(X)

fit_transform 方法在内部做了优化,通常比分开调用更高效。但在处理训练集和测试集时,必须分开调用:

# 正确做法:用训练集的参数转换测试集
scaler.fit(X_train)
X_train_scaled = scaler.transform(X_train)
X_test_scaled = scaler.transform(X_test) # 使用训练集的参数

# 错误做法:分别标准化(会导致测试集泄露信息)
# scaler.fit(X_test) # 不要这样做

MinMaxScaler(归一化)

MinMaxScaler 将特征缩放到指定范围(默认 [0, 1])。

数学原理

xscaled=xxminxmaxxminx_{scaled} = \frac{x - x_{min}}{x_{max} - x_{min}}

如果要缩放到 [a, b] 范围:

xscaled=a+(xxmin)(ba)xmaxxminx_{scaled} = a + \frac{(x - x_{min})(b - a)}{x_{max} - x_{min}}

适用场景

  • 需要特征在固定范围内(如图像像素值 0-255)
  • 数据不服从正态分布
  • 对异常值敏感的场景(需要先处理异常值)

注意事项

  • 对异常值非常敏感,一个极端值就会压缩其他值的分布
  • 新数据可能超出训练集的范围,需要处理
from sklearn.preprocessing import MinMaxScaler

# 缩放到 [0, 1]
scaler = MinMaxScaler()
X_normalized = scaler.fit_transform(X)

print("缩放范围:", scaler.feature_range) # (0, 1)
print("最小值:", scaler.data_min_)
print("最大值:", scaler.data_max_)

# 缩放到 [-1, 1]
scaler = MinMaxScaler(feature_range=(-1, 1))
X_scaled = scaler.fit_transform(X)

RobustScaler(稳健缩放)

RobustScaler 使用中位数和四分位距进行缩放,对异常值不敏感。

数学原理

xscaled=xmedianIQRx_{scaled} = \frac{x - median}{IQR}

其中 IQR(Interquartile Range)是四分位距,即第 75 百分位数减去第 25 百分位数。

适用场景

  • 数据包含异常值
  • 不想因为异常值而丢弃数据
from sklearn.preprocessing import RobustScaler

scaler = RobustScaler()
X_scaled = scaler.fit_transform(X)

print("中位数:", scaler.center_)
print("四分位距:", scaler.scale_)

MaxAbsScaler(最大绝对值缩放)

MaxAbsScaler 将特征除以其最大绝对值,缩放到 [-1, 1] 范围。这是专门为稀疏数据设计的缩放方法。

适用场景

  • 稀疏数据(大量零值)
  • 需要保持稀疏性
from sklearn.preprocessing import MaxAbsScaler
from scipy import sparse

# 稀疏矩阵
X_sparse = sparse.csr_matrix([[1, 0, 0], [0, 2, 0], [0, 0, 3]])

scaler = MaxAbsScaler()
X_scaled = scaler.fit_transform(X_sparse)

print("稀疏性保持:", sparse.issparse(X_scaled)) # True

缩放方法对比

方法公式异常值敏感性稀疏数据输出范围
StandardScaler(xμ)/σ(x-\mu)/\sigma敏感不适合无固定范围
MinMaxScaler(xxmin)/(xmaxxmin)(x-x_{min})/(x_{max}-x_{min})敏感不适合[0, 1]
RobustScaler(xmedian)/IQR(x-median)/IQR稳健不适合无固定范围
MaxAbsScalerx/xmaxx/\|x\|_{max}敏感支持[-1, 1]

如何选择

  • 数据近似正态分布,无严重异常值 → StandardScaler
  • 需要固定范围,数据分布不明确 → MinMaxScaler
  • 数据包含异常值 → RobustScaler
  • 稀疏数据 → MaxAbsScaler

样本标准化(Normalizer)

Normalizer 与前面的缩放器不同,它是对每个样本(每行)进行标准化,而不是对每个特征(每列)。它将每个样本缩放到单位范数。

数学原理

对于样本 xx,L2 标准化为:

xnormalized=xx2=xx12+x22+...+xn2x_{normalized} = \frac{x}{\|x\|_2} = \frac{x}{\sqrt{x_1^2 + x_2^2 + ... + x_n^2}}

适用场景

  • 文本分类和聚类(TF-IDF 向量)
  • 需要比较样本相似度而非特征大小的场景
  • 使用点积或核函数计算相似度的场景
from sklearn.preprocessing import Normalizer

X = [[1, -1, 2],
[2, 0, 0],
[0, 1, -1]]

# L2 标准化(默认)
normalizer = Normalizer()
X_normalized = normalizer.transform(X)

print("原始数据:")
print(X)
print("\nL2 标准化后:")
print(X_normalized)
# 每行的 L2 范数都是 1

# L1 标准化
normalizer_l1 = Normalizer(norm='l1')
X_l1 = normalizer_l1.transform(X)

# Max 标准化
normalizer_max = Normalizer(norm='max')
X_max = normalizer_max.transform(X)

Normalizer vs StandardScaler

方面NormalizerStandardScaler
标准化对象每个样本(行)每个特征(列)
目的使样本向量长度为 1使特征均值为 0,方差为 1
适用场景文本分析、相似度计算大多数机器学习算法

非线性变换

非线性变换可以将数据映射到更接近正态分布的形式,这对于需要正态分布假设的算法(如线性回归、判别分析)非常重要。

幂变换(PowerTransformer)

PowerTransformer 提供两种幂变换方法,将任意分布的数据映射到尽可能接近正态分布:

Box-Cox 变换:只能用于严格正数数据

y={xλ1λλ0log(x)λ=0y = \begin{cases} \frac{x^\lambda - 1}{\lambda} & \lambda \neq 0 \\ \log(x) & \lambda = 0 \end{cases}

Yeo-Johnson 变换:可以处理正数和负数

from sklearn.preprocessing import PowerTransformer
import numpy as np

# 生成对数正态分布数据
np.random.seed(42)
X = np.random.lognormal(mean=0, sigma=1, size=(100, 2))

# Yeo-Johnson 变换(默认,可处理正负数)
pt_yj = PowerTransformer(method='yeo-johnson', standardize=True)
X_yj = pt_yj.fit_transform(X)

print("原始数据均值:", X.mean(axis=0))
print("变换后均值:", X_yj.mean(axis=0))
print("变换后方差:", X_yj.std(axis=0))
# 标准化后均值接近 0,标准差接近 1

# Box-Cox 变换(只能用于正数)
pt_bc = PowerTransformer(method='box-cox', standardize=True)
X_bc = pt_bc.fit_transform(X)

# 查看最优 lambda 值
print("\n最优 lambda 值:", pt_yj.lambdas_)

什么时候使用幂变换?

  • 目标变量高度偏斜(如收入、房价)
  • 模型假设数据服从正态分布
  • 需要稳定方差

分位数变换(QuantileTransformer)

分位数变换将数据映射到均匀分布或正态分布,通过分位数函数实现:

from sklearn.preprocessing import QuantileTransformer
import numpy as np

# 生成偏斜数据
np.random.seed(42)
X = np.random.exponential(scale=2, size=(1000, 1))

# 映射到均匀分布
qt_uniform = QuantileTransformer(output_distribution='uniform', random_state=42)
X_uniform = qt_uniform.fit_transform(X)

# 映射到正态分布
qt_normal = QuantileTransformer(output_distribution='normal', random_state=42)
X_normal = qt_normal.fit_transform(X)

print("原始数据范围:", X.min(), "-", X.max())
print("均匀分布范围:", X_uniform.min(), "-", X_uniform.max())
print("正态分布均值:", X_normal.mean())

QuantileTransformer 的特点

  • 对异常值非常稳健
  • 可以将任何分布映射到目标分布
  • 但会扭曲特征之间的相关性

PowerTransformer vs QuantileTransformer

方面PowerTransformerQuantileTransformer
数学基础参数化变换非参数化变换
异常值敏感性中等稳健
保持相关性否(会扭曲)
适用数据中等偏斜高度偏斜或异常值多

高级组合器

除了 Pipeline 和 ColumnTransformer,sklearn 还提供了其他组合工具。

FeatureUnion(特征联合)

FeatureUnion 将多个转换器的输出并列拼接,适用于需要从同一数据生成多种特征表示的场景。

from sklearn.pipeline import FeatureUnion
from sklearn.decomposition import PCA, KernelPCA
from sklearn.datasets import load_iris

iris = load_iris()
X = iris.data

# 同时使用 PCA 和 核PCA,将结果拼接
union = FeatureUnion([
('linear_pca', PCA(n_components=2)),
('kernel_pca', KernelPCA(n_components=2, kernel='rbf'))
])

X_combined = union.fit_transform(X)
print(f"原始特征数: {X.shape[1]}")
print(f"组合后特征数: {X_combined.shape[1]}") # 2 + 2 = 4

FeatureUnion vs ColumnTransformer

  • FeatureUnion:每个转换器处理相同的输入,输出并列拼接
  • ColumnTransformer:每个转换器处理不同的列,输出并列拼接

TransformedTargetRegressor(目标变量转换)

在回归任务中,有时需要先对目标变量进行变换(如对数变换),预测后再逆变换回原始空间。TransformedTargetRegressor 自动处理这个过程。

from sklearn.compose import TransformedTargetRegressor
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import QuantileTransformer
from sklearn.datasets import make_regression
from sklearn.model_selection import train_test_split

# 生成回归数据
X, y = make_regression(n_samples=200, n_features=5, noise=10, random_state=42)
# 让目标变量呈指数分布
y = np.exp((y - y.min()) / (y.max() - y.min()) * 4)

X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)

# 普通线性回归
lr = LinearRegression()
lr.fit(X_train, y_train)
print(f"普通线性回归 R²: {lr.score(X_test, y_test):.3f}")

# 对目标变量进行变换的回归
regr = TransformedTargetRegressor(
regressor=LinearRegression(),
transformer=QuantileTransformer(output_distribution='normal')
)
regr.fit(X_train, y_train)
print(f"目标变换后 R²: {regr.score(X_test, y_test):.3f}")

# 预测会自动逆变换
y_pred = regr.predict(X_test)

使用函数进行简单变换

import numpy as np

# 使用函数进行对数变换
regr_log = TransformedTargetRegressor(
regressor=LinearRegression(),
func=np.log, # 正变换
inverse_func=np.exp # 逆变换
)

regr_log.fit(X_train, y_train)
print(f"对数变换后 R²: {regr_log.score(X_test, y_test):.3f}")

典型应用场景

  • 房价预测:目标变量取对数
  • 收入预测:目标变量高度右偏
  • 任何目标变量分布偏斜的回归问题

类别特征编码

机器学习模型通常只能处理数值输入,因此需要将类别特征转换为数值。根据类别的性质,选择不同的编码方式。

LabelEncoder

LabelEncoder 将类别标签转换为 0 到 n-1 的整数。主要用于编码目标变量(标签)。

from sklearn.preprocessing import LabelEncoder

# 目标变量编码
y = ["cat", "dog", "cat", "bird", "dog"]
encoder = LabelEncoder()
y_encoded = encoder.fit_transform(y)

print("编码结果:", y_encoded) # [0 1 0 2 1]
print("类别列表:", encoder.classes_) # ['bird' 'cat' 'dog']

# 反向转换:从编码值得到原始标签
y_original = encoder.inverse_transform([0, 1, 2])
print("反向转换:", y_original) # ['bird' 'cat' 'dog']

注意事项

  • LabelEncoder 引入了隐含的顺序关系(0 < 1 < 2),但这对于类别特征通常没有意义
  • 不推荐用于特征编码,仅用于目标变量

OrdinalEncoder

OrdinalEncoder 将类别特征转换为有序整数。适用于有序类别(如"低"、"中"、"高")。

from sklearn.preprocessing import OrdinalEncoder

# 有序类别
X = [["低"], ["中"], ["高"], ["低"], ["高"]]

encoder = OrdinalEncoder(categories=[["低", "中", "高"]])
X_encoded = encoder.fit_transform(X)

print("编码结果:", X_encoded)
# [[0.], [1.], [2.], [0.], [2.]]

OneHotEncoder

OneHotEncoder 将每个类别转换为二进制向量,避免引入顺序关系。适用于无序类别。

原理:对于有 kk 个类别的特征,创建 kk 个二进制特征,每个样本只在对应的类别位置为 1,其余为 0。

from sklearn.preprocessing import OneHotEncoder

# 无序类别
X = [["红色"], ["蓝色"], ["绿色"], ["红色"]]

encoder = OneHotEncoder(sparse_output=False) # 返回密集数组
X_encoded = encoder.fit_transform(X)

print("类别列表:", encoder.categories_)
print("编码结果:\n", X_encoded)
# [[1. 0. 0.] 红色
# [0. 1. 0.] 蓝色
# [0. 0. 1.] 绿色
# [1. 0. 0.]] 红色

# 获取特征名称
print("特征名称:", encoder.get_feature_names_out())

处理未知类别

# 设置 handle_unknown='ignore' 忽略未知类别
encoder = OneHotEncoder(handle_unknown='ignore', sparse_output=False)
encoder.fit([["猫"], ["狗"], ["鸟"]])

# 测试时遇到未知类别 "鱼",会生成全零向量
X_test = [["猫"], ["鱼"]]
X_test_encoded = encoder.transform(X_test)
print(X_test_encoded)
# [[1. 0. 0.] 猫
# [0. 0. 0.]] 鱼(未知类别,全零)

处理高基数特征

当类别数量很多时,OneHotEncoder 会产生大量特征。可以考虑:

  • 合并低频类别为"其他"
  • 使用 Target Encoding(需要第三方库)
  • 保留 Top-K 常见类别
import numpy as np

# 使用 min_frequency 参数合并低频类别
encoder = OneHotEncoder(min_frequency=0.1, sparse_output=False)
# 出现频率低于 10% 的类别会被合并

编码方法对比

方法适用场景优点缺点
LabelEncoder目标变量编码简单引入虚假顺序
OrdinalEncoder有序类别保留顺序信息需要指定顺序
OneHotEncoder无序类别不引入顺序关系高基数时特征膨胀

缺失值处理

真实数据集中经常存在缺失值,sklearn 提供了多种填充策略。

SimpleImputer

SimpleImputer 使用固定策略填充缺失值。

填充策略

  • mean:用列均值填充(数值特征)
  • median:用列中位数填充(数值特征)
  • most_frequent:用众数填充(类别特征)
  • constant:用指定常量填充
from sklearn.impute import SimpleImputer
import numpy as np

X = [[1, 2],
[np.nan, 3],
[7, np.nan],
[4, 5]]

# 使用均值填充
imputer = SimpleImputer(strategy='mean')
X_imputed = imputer.fit_transform(X)

print("均值:", imputer.statistics_) # 每列的均值
print("填充后:\n", X_imputed)

# 使用中位数填充(对异常值更稳健)
imputer_median = SimpleImputer(strategy='median')
X_median = imputer_median.fit_transform(X)

# 使用常数填充
imputer_const = SimpleImputer(strategy='constant', fill_value=-1)
X_const = imputer_const.fit_transform(X)

KNNImputer

KNNImputer 使用 K 近邻算法填充缺失值,根据相似样本的值进行估计。这种方法通常比简单填充更准确,但计算成本更高。

原理:对于含缺失值的样本,找到 K 个最相似的完整样本,用它们的加权平均(或中位数)来填充。

from sklearn.impute import KNNImputer

X = [[1, 2, 3],
[4, np.nan, 6],
[7, 8, 9],
[np.nan, 5, np.nan]]

imputer = KNNImputer(n_neighbors=2)
X_imputed = imputer.fit_transform(X)

print("KNN 填充结果:\n", X_imputed)

迭代填充(IterativeImputer)

IterativeImputer 使用其他特征作为预测器,迭代地填充每个特征的缺失值。这种方法可以捕捉特征之间的关系。

from sklearn.experimental import enable_iterative_imputer
from sklearn.impute import IterativeImputer
from sklearn.linear_model import BayesianRidge

X = [[1, 2, 3],
[4, np.nan, 6],
[7, 8, 9],
[np.nan, 5, np.nan]]

imputer = IterativeImputer(
estimator=BayesianRidge(),
max_iter=10,
random_state=0
)
X_imputed = imputer.fit_transform(X)

print("迭代填充结果:\n", X_imputed)

缺失值处理策略选择

策略适用场景优点缺点
均值填充缺失值较少,近似正态分布简单快速会低估方差
中位数填充存在异常值稳健可能损失信息
众数填充类别特征保持类别性质可能引入偏差
KNN 填充特征间有相关性考虑样本相似性计算成本高
迭代填充多特征缺失利用特征关系复杂度高

特征选择

特征选择是从原始特征中选择最相关特征的过程,可以减少维度、提高模型性能、加快训练速度。

方差阈值(VarianceThreshold)

移除方差低于阈值的特征。这可以过滤掉几乎不变的特征(如大部分样本都是同一值)。

from sklearn.feature_selection import VarianceThreshold

# 假设特征 0 有 90% 的值是 0,方差很低
X = [[0, 1, 2],
[0, 3, 4],
[0, 5, 6],
[1, 7, 8]] # 特征 0 只有 1 个 1

selector = VarianceThreshold(threshold=0.1)
X_selected = selector.fit_transform(X)

print("保留的特征:", selector.get_support())
print("选择后:\n", X_selected)

单变量特征选择

使用统计测试选择与目标变量最相关的特征。

SelectKBest:选择得分最高的 K 个特征

from sklearn.feature_selection import SelectKBest, f_classif
from sklearn.datasets import load_iris

iris = load_iris()
X, y = iris.data, iris.target

# 选择最好的 2 个特征
selector = SelectKBest(f_classif, k=2)
X_selected = selector.fit_transform(X, y)

print("原始特征数:", X.shape[1])
print("选择后特征数:", X_selected.shape[1])
print("各特征得分:", selector.scores_)
print("选择的特征:", selector.get_support())

常用评分函数

  • f_classif:方差分析 F 值,用于分类任务
  • f_regression:F 值,用于回归任务
  • chi2:卡方检验,用于非负特征(如词频)
  • mutual_info_classif:互信息,用于分类
  • mutual_info_regression:互信息,用于回归

递归特征消除(RFE)

RFE 递归地训练模型,每次移除最不重要的特征,直到达到指定数量。

from sklearn.feature_selection import RFE
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import load_iris

iris = load_iris()
X, y = iris.data, iris.target

# 使用逻辑回归作为基模型
estimator = LogisticRegression(max_iter=200)
selector = RFE(estimator, n_features_to_select=2)

X_selected = selector.fit_transform(X, y)

print("特征排名:", selector.ranking_) # 1 表示选中
print("选择的特征:", selector.support_)

基于模型的特征选择

使用模型的特征重要性进行选择。

from sklearn.feature_selection import SelectFromModel
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import load_iris

iris = load_iris()
X, y = iris.data, iris.target

# 使用随机森林的重要性
clf = RandomForestClassifier(n_estimators=100, random_state=42)
selector = SelectFromModel(clf, threshold='median')
X_selected = selector.fit_transform(X, y)

print("原始特征数:", X.shape[1])
print("选择后特征数:", X_selected.shape[1])
print("特征重要性:", clf.fit(X, y).feature_importances_)

构建预处理管道

实际项目中,不同类型的特征需要不同的预处理方法。sklearn 提供了 Pipeline 和 ColumnTransformer 来组织复杂的预处理流程。

Pipeline

Pipeline 将多个转换步骤串联起来,保证训练和预测时数据处理的一致性。

from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.impute import SimpleImputer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.datasets import load_iris

# 加载数据
iris = load_iris()
X, y = iris.data, iris.target
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# 创建管道
pipeline = Pipeline([
('imputer', SimpleImputer(strategy='mean')), # 缺失值填充
('scaler', StandardScaler()), # 标准化
('classifier', LogisticRegression()) # 分类器
])

# 训练和预测
pipeline.fit(X_train, y_train)
accuracy = pipeline.score(X_test, y_test)

print(f"测试集准确率: {accuracy:.2%}")

ColumnTransformer

ColumnTransformer 可以对不同的列应用不同的预处理方法。

from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.impute import SimpleImputer
from sklearn.pipeline import Pipeline
import pandas as pd
import numpy as np

# 创建示例数据
df = pd.DataFrame({
'age': [25, 30, np.nan, 35, 40],
'income': [50000, 60000, 70000, np.nan, 90000],
'city': ['北京', '上海', '北京', '广州', '上海'],
'purchased': [0, 1, 0, 1, 1] # 目标变量
})

X = df[['age', 'income', 'city']]
y = df['purchased']

# 定义数值列和类别列
numeric_features = ['age', 'income']
categorical_features = ['city']

# 为数值列创建预处理管道
numeric_transformer = Pipeline([
('imputer', SimpleImputer(strategy='mean')),
('scaler', StandardScaler())
])

# 为类别列创建预处理管道
categorical_transformer = Pipeline([
('imputer', SimpleImputer(strategy='most_frequent')),
('encoder', OneHotEncoder(handle_unknown='ignore', sparse_output=False))
])

# 组合预处理器
preprocessor = ColumnTransformer(
transformers=[
('num', numeric_transformer, numeric_features),
('cat', categorical_transformer, categorical_features)
]
)

# 应用预处理
X_processed = preprocessor.fit_transform(X)
print("预处理后的形状:", X_processed.shape)

完整示例

将预处理管道与模型结合:

from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.impute import SimpleImputer
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split, cross_val_score
import pandas as pd
import numpy as np

# 创建示例数据
np.random.seed(42)
n_samples = 200

df = pd.DataFrame({
'age': np.random.randint(18, 70, n_samples),
'income': np.random.randint(20000, 150000, n_samples),
'education': np.random.choice(['高中', '本科', '硕士', '博士'], n_samples),
'city': np.random.choice(['北京', '上海', '广州', '深圳'], n_samples),
'purchased': np.random.randint(0, 2, n_samples)
})

# 故意添加一些缺失值
df.loc[df.sample(frac=0.1).index, 'income'] = np.nan
df.loc[df.sample(frac=0.05).index, 'education'] = np.nan

X = df.drop('purchased', axis=1)
y = df['purchased']

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# 定义特征类型
numeric_features = ['age', 'income']
categorical_features = ['education', 'city']

# 数值特征预处理
numeric_transformer = Pipeline([
('imputer', SimpleImputer(strategy='median')),
('scaler', StandardScaler())
])

# 类别特征预处理
categorical_transformer = Pipeline([
('imputer', SimpleImputer(strategy='most_frequent')),
('onehot', OneHotEncoder(handle_unknown='ignore', sparse_output=False))
])

# 组合预处理
preprocessor = ColumnTransformer([
('num', numeric_transformer, numeric_features),
('cat', categorical_transformer, categorical_features)
])

# 完整管道:预处理 + 模型
pipeline = Pipeline([
('preprocessor', preprocessor),
('classifier', RandomForestClassifier(n_estimators=100, random_state=42))
])

# 交叉验证评估
cv_scores = cross_val_score(pipeline, X_train, y_train, cv=5)
print(f"交叉验证准确率: {cv_scores.mean():.2%} (+/- {cv_scores.std()*2:.2%})")

# 训练最终模型
pipeline.fit(X_train, y_train)
test_accuracy = pipeline.score(X_test, y_test)
print(f"测试集准确率: {test_accuracy:.2%}")

最佳实践

避免数据泄露

数据泄露是最常见的错误之一。关键原则是:预处理参数只能从训练集学习

# 错误示例:在整个数据集上标准化
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X) # 泄露了测试集信息
X_train, X_test, y_train, y_test = train_test_split(X_scaled, y, ...)

# 正确示例:只在训练集上学习参数
X_train, X_test, y_train, y_test = train_test_split(X, y, ...)
scaler = StandardScaler()
scaler.fit(X_train) # 只用训练集
X_train_scaled = scaler.transform(X_train)
X_test_scaled = scaler.transform(X_test)

使用 Pipeline 可以自动避免这个问题:

pipeline = Pipeline([
('scaler', StandardScaler()),
('model', LogisticRegression())
])

# cross_val_score 会正确处理预处理
cross_val_score(pipeline, X, y, cv=5)

保持可复现性

设置 random_state 确保结果可复现:

model = RandomForestClassifier(random_state=42)
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)

保存预处理器

训练好的预处理器需要保存,以便对新数据应用相同的转换:

import joblib

# 保存预处理器
joblib.dump(scaler, 'scaler.pkl')

# 加载预处理器
scaler = joblib.load('scaler.pkl')
X_new_scaled = scaler.transform(X_new)

处理新类别

对于类别编码,要考虑新数据中可能出现训练集没有的类别:

encoder = OneHotEncoder(handle_unknown='ignore', sparse_output=False)

小结

数据预处理是机器学习流程的基础环节。正确的预处理可以显著提升模型性能,而错误的预处理可能导致模型失效。关键点包括:

  1. 根据数据特点选择合适的缩放方法
  2. 根据类别性质选择合适的编码方式
  3. 始终注意避免数据泄露
  4. 使用 Pipeline 组织预处理流程
  5. 保存预处理器以便对新数据应用相同转换