将数据拆分为训练集和测试集后,下一个重要步骤是应用预处理变换,例如特征缩放或类别变量编码。这里的一个常见问题是在变换过程中独立处理训练集和测试集,这可能导致误导性结果和模型泛化能力差。所有用于数据变换的信息都只能从训练集中学习,这一点非常必要。问题:从测试集学习(数据泄漏)想象你正在准备一场考试(你的测试集)。如果你在学习(训练模型和准备数据)时看到了考题和答案(来自测试集的信息),那么你在那场特定考试上的表现可能看起来很出色,但这并不能反映你在另一场未曾见过的考试中表现如何。如果在测试集上单独拟合一个数据变换器,比如 StandardScaler,那么该缩放器将学习到测试集的均值和标准差。这会将测试数据的信息纳入你的预处理流程,而这发生在模型评估阶段之前。这是一种数据泄漏。模型在这个“被污染”的测试集上的性能会被人为地抬高,因为预处理步骤获取了在有新的、未见过的数据到达的情形下本不应获得的信息。考虑 StandardScaler,它通过减去均值来居中数据,并通过除以标准差来缩放数据。$$ z = \frac{x - \mu}{\sigma} $$其中 $x$ 是原始特征值,$\mu$ 是均值,$\sigma$ 是标准差。如果你从训练集计算 $\mu_{train}$ 和 $\sigma_{train}$,并从测试集计算 $\mu_{test}$ 和 $\sigma_{test}$,然后独立进行缩放:X_train_scaled 使用 $\mu_{train}$ 和 $\sigma_{train}$。X_test_scaled 使用 $\mu_{test}$ 和 $\sigma_{test}$。测试数据现在是根据其自身属性进行缩放的,而不是根据在训练期间学习到的属性。这打破了测试集代表以与训练数据完全相同方式处理的未见过数据这一假设。正确的方法:在训练集上拟合,对两者进行变换正确的方法确保在拟合过程中,测试集真正保持“未见过”状态。Scikit-learn 的变换器在设计时考虑到了这一原理,提供了独立的方法:fit(X_train):此方法仅从训练数据学习所需参数。对于 StandardScaler,它计算均值 ($\mu$) 和标准差 ($\sigma$)。对于 OneHotEncoder,它确定每个特征的唯一类别。transform(X):此方法使用在 fit 步骤中学到的参数应用变换。它不会重新计算任何内容。正确的工作流程是:将数据拆分为 X_train、X_test、y_train、y_test。实例化变换器(例如,scaler = StandardScaler())。仅在训练数据上拟合变换器:scaler.fit(X_train)。变换训练数据:X_train_transformed = scaler.transform(X_train)。使用相同的已拟合缩放器变换测试数据:X_test_transformed = scaler.transform(X_test)。请注意,fit 只调用一次,作用于 X_train。X_train 和 X_test 都使用完全来源于 X_train 的参数进行变换。Scikit-learn 还提供了一个便捷方法:fit_transform(X_train):这会将步骤 3 和 4(fit 和 transform)合并为一个调用,但只能在训练数据上使用。我们来看一个实际例子:import pandas as pd import numpy as np from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler # 示例数据 data = {'feature1': [10, 15, 12, 18, 20, 5, 25, 22], 'feature2': [100, 110, 105, 120, 125, 90, 130, 128]} df = pd.DataFrame(data) y = np.array([0, 1, 0, 1, 1, 0, 1, 1]) # 示例目标变量 # 1. 拆分数据 X_train, X_test, y_train, y_test = train_test_split(df, y, test_size=0.25, random_state=42) print("原始训练数据:") print(X_train) print("\n原始测试数据:") print(X_test) # 2. 实例化缩放器 scaler = StandardScaler() # 3. 仅在训练数据上拟合缩放器 scaler.fit(X_train) # 打印学到的参数(均值和缩放/标准差) print(f"\n缩放器均值(从训练集学习):{scaler.mean_}") print(f"缩放器缩放比例(从训练集学习到的标准差):{scaler.scale_}") # 4. 变换训练数据 X_train_scaled = scaler.transform(X_train) print("\n已缩放的训练数据:") print(X_train_scaled) # 5. 使用相同的拟合变换测试数据 X_test_scaled = scaler.transform(X_test) print("\n已缩放的测试数据:") print(X_test_scaled) # --- 错误方法:在测试数据上单独拟合 --- # scaler_test = StandardScaler() # scaler_test.fit(X_test) # 这是错误的 - 使用了测试集信息 # X_test_scaled_wrong = scaler_test.transform(X_test) # print("\n错误缩放的测试数据(在测试集上拟合):") # print(X_test_scaled_wrong) # 请注意,如果单独拟合,均值和缩放比例会有何不同。输出清楚地显示了缩放器从 X_train 中学习参数($\mu$ 和 $\sigma$),然后将这些精确参数应用于标准化 X_train 和 X_test。可视化一致的变换流程以下图表显示了应用变换的正确数据流程:digraph G { rankdir=LR; node [shape=box, style=rounded, fontname="helvetica", fontsize=10]; edge [fontname="helvetica", fontsize=9]; subgraph cluster_data { label = "数据"; style=filled; color="#e9ecef"; RawData [label="原始数据"]; } subgraph cluster_split { label = "拆分"; style=filled; color="#a5d8ff"; TrainData [label="训练数据\n(X_train, y_train)", shape=cylinder, style=filled, color="#ced4da"]; TestData [label="测试数据\n(X_test, y_test)", shape=cylinder, style=filled, color="#ced4da"]; } subgraph cluster_transform { label = "变换"; style=filled; color="#b2f2bb"; ScalerFit [label="拟合变换器\n(例如,StandardScaler)\n学习参数 μ, σ", shape=invhouse, style=filled, color="#8ce99a"]; ScalerTransformTrain [label="变换\n应用 μ, σ", shape=cds, style=filled, color="#8ce99a"]; ScalerTransformTest [label="变换\n应用 μ, σ", shape=cds, style=filled, color="#8ce99a"]; } subgraph cluster_model { label = "模型训练与评估"; style=filled; color="#bac8ff"; TrainModel [label="训练模型", shape=ellipse, style=filled, color="#91a7ff"]; EvaluateModel [label="评估模型", shape=ellipse, style=filled, color="#91a7ff"]; TransformedTrain [label="已变换的\n训练数据", shape=cylinder, style=filled, color="#dee2e6"]; TransformedTest [label="已变换的\n测试数据", shape=cylinder, style=filled, color="#dee2e6"]; } RawData -> TrainData; RawData -> TestData; TrainData -> ScalerFit [label="拟合"]; ScalerFit -> ScalerTransformTrain [style=dashed, label="参数 (μ, σ)"]; ScalerFit -> ScalerTransformTest [style=dashed, label="参数 (μ, σ)"]; TrainData -> ScalerTransformTrain [label="变换"]; TestData -> ScalerTransformTest [label="变换"]; ScalerTransformTrain -> TransformedTrain; ScalerTransformTest -> TransformedTest; TransformedTrain -> TrainModel; TransformedTest -> EvaluateModel; TrainModel -> EvaluateModel [style=dotted, label="已训练模型"]; }数据首先被拆分。变换器仅在训练数据上拟合以学习参数(如均值 $\mu$ 和标准差 $\sigma$)。然后,这些学习到的参数被一致地用于变换训练数据集和测试数据集,之后再进行模型训练和评估。使用 Scikit-learn 流水线确保一致性手动管理多个变换器的 fit 和 transform 步骤可能变得繁琐且易出错。这就是 Scikit-learn Pipeline 对象变得非常有用之处。如前所述,Pipeline 将多个步骤(变换器和最终估计器)连接起来。当你调用 pipeline.fit(X_train, y_train) 时:流水线对第一步(变换器)调用 fit_transform,使用 X_train。变换后的数据传递给下一步。流水线中的所有变换器都以此方式继续。每个变换器仅在传递给它的数据(源自 X_train)上进行拟合。最后,完全变换后的训练数据用于 fit 估计器(最后一步)。最重要地是,当你之后调用 pipeline.predict(X_test) 或 pipeline.score(X_test, y_test) 时:流水线对第一步调用 transform,使用 X_test。它使用在 fit 阶段(在 X_train 上)学到的参数。变换后的 X_test 数据被传递到下一步的 transform 方法,同样使用在拟合期间学到的参数。这种情况一直持续到最终的估计器,它随后使用完全变换后的 X_test 数据进行预测或评估性能。流水线自动确保变换的一致应用,通过严格分离拟合过程(在训练数据上)与应用于训练和测试数据的变换过程,从而防止数据泄漏。from sklearn.pipeline import Pipeline from sklearn.linear_model import LogisticRegression # 示例估计器 from sklearn.compose import ColumnTransformer from sklearn.preprocessing import OneHotEncoder # 包含类别特征的示例数据 data_cat = {'feature1': [10, 15, 12, 18, 20, 5, 25, 22], 'category': ['A', 'B', 'A', 'C', 'B', 'A', 'C', 'B']} df_cat = pd.DataFrame(data_cat) y_cat = np.array([0, 1, 0, 1, 1, 0, 1, 1]) # 拆分数据 X_train_cat, X_test_cat, y_train_cat, y_test_cat = train_test_split( df_cat, y_cat, test_size=0.25, random_state=42 ) # 使用 ColumnTransformer 创建预处理器 # 缩放数值特征,对类别特征进行独热编码 preprocessor = ColumnTransformer( transformers=[ ('num', StandardScaler(), ['feature1']), ('cat', OneHotEncoder(handle_unknown='ignore'), ['category']) # handle_unknown 对于训练集中未见的测试集类别很重要 ], remainder='passthrough' # 保留其他列(此处无) ) # 创建完整的流水线 pipeline = Pipeline([ ('preprocess', preprocessor), ('classifier', LogisticRegression()) ]) # 拟合流水线 - 变换器仅在此处在 X_train_cat 上拟合 pipeline.fit(X_train_cat, y_train_cat) # 在测试数据上预测 - 变换器仅使用学习到的参数变换 X_test_cat predictions = pipeline.predict(X_test_cat) score = pipeline.score(X_test_cat, y_test_cat) print(f"\n流水线训练成功。") print(f"测试集预测结果: {predictions}") print(f"测试集准确度: {score:.4f}") # 你可以查看流水线步骤中已拟合的参数 print("\n已拟合 StandardScaler 的均值(来自流水线):") print(pipeline.named_steps['preprocess'].transformers_[0][1].mean_) print("\n已拟合 OneHotEncoder 的类别(来自流水线):") print(pipeline.named_steps['preprocess'].transformers_[1][1].categories_)此示例显示了 Pipeline 结合 ColumnTransformer 如何处理不同列类型的不同变换,同时严格保持在训练数据上拟合与变换测试数据之间的分离。一致地应用变换不仅是一种最佳实践;它对于构建可信赖地在新、未见过数据上可靠运行的机器学习模型是根本的。正确使用 Scikit-learn 的变换器 API(fit/transform),尤其是在流水线中封装,是实现这种一致性的标准方法。