1.20 管道与组合估计器
真实流程通常是一串步骤:标准化、填补缺失、特征选择、最后训练模型。手工一步步写很容易出错,尤其容易在测试集上误用训练集学到的参数。sklearn.pipeline 的 Pipeline 把多个转换器和一个估计器串成一条流水线,一次 fit、一次 predict,且在交叉验证中自动保证每折只在训练部分 fit、在验证部分只 transform,从机制上杜绝数据泄漏。sklearn.compose 进一步提供对列做不同处理的 ColumnTransformer,以及转换目标变量的 TransformedTargetRegressor。
1.20.1 Pipeline 管道
Pipeline 接收 steps 列表,每项是 (名称, 估计器) 元组。除最后一项外都是转换器(有 transform),最后一项是估计器(有 predict)。调用 pipe.fit(X, y) 时,数据依次流过每个转换器,最后交给估计器训练;调用 pipe.predict(X) 时,同样的转换流程再走一遍再预测:
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
from sklearn.svm import SVC
from sklearn.datasets import load_digits
X, y = load_digits(return_X_y=True)
pipe = Pipeline([
('scaler', StandardScaler()), # 第 1 步:标准化
('pca', PCA(n_components=20)), # 第 2 步:降维
('svc', SVC()), # 第 3 步:分类器
])
pipe.fit(X, y)
print(pipe.score(X, y))named_steps 访问中间步骤
管道内部每一步的结果可以通过 named_steps 按名称访问,例如查看 PCA 解释的方差:
print(pipe.named_steps['pca'].explained_variance_ratio_[:5])named_steps 支持两种访问方式:pipe.named_steps['pca'] 或 pipe['pca']。
set_params 调参语法
管道里某一步的参数用双下划线语法 步骤名__参数名 设置,交叉验证网格搜索也用它:
pipe.set_params(svc__C=10, pca__n_components=30)
print(pipe.named_steps['svc'].C) # 10Pipeline 防止数据泄漏
管道在交叉验证中的关键价值:GridSearchCV 的每折内,转换器的 fit 只发生在该折的训练部分,验证部分只 transform。若把「标准化后再交叉验证」写成先整体 fit 再划分,测试集信息会进入转换器。用管道包装后,这个过程自动且安全:
from sklearn.model_selection import GridSearchCV
param_grid = {'svc__C': [0.1, 1, 10], 'svc__kernel': ['linear', 'rbf']}
grid = GridSearchCV(pipe, param_grid, cv=5)
grid.fit(X, y)
print(grid.best_params_)1.20.2 make_pipeline 简化构造
make_pipeline 省略步骤命名,自动按类名生成 'standardscaler'、'pca' 这种小写名称,代码更短:
from sklearn.pipeline import make_pipeline
pipe2 = make_pipeline(StandardScaler(), PCA(n_components=20), SVC())
print(pipe2.steps) # [('standardscaler', ...), ('pca', ...), ('svc', ...)]
print(pipe2.named_steps['standardscaler'])调参语法不变,只是步骤名变为自动生成的类名小写形式,例如 svc__C。不传参数想用 set_params 时,也可通过 pipe2['svc'] 直接拿到步骤。
1.20.3 FeatureUnion 并行合并
FeatureUnion 并行应用多个转换器,把它们的输出按列拼接成一个矩阵。适合同时提取不同类型的特征,例如既做文本词频又做文本长度:
from sklearn.pipeline import FeatureUnion
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler
from sklearn.datasets import load_digits
X, y = load_digits(return_X_y=True)
union = FeatureUnion([
('pca', PCA(n_components=10)),
('scaled', make_pipeline(StandardScaler(), PCA(n_components=5))),
])
X_u = union.fit_transform(X)
print(X_u.shape) # (1797, 15),两个分支的输出按列合并FeatureUnion 的每个分支必须是转换器,n_jobs 参数可并行运行各分支。注意它不是「分列处理不同列」,而是「同一数据并行提取多种表示」。
1.20.4 ColumnTransformer 分列转换
真实表格数据往往混合数值列与类别列,需要对不同列做不同转换:数值列标准化、类别列独热编码。ColumnTransformer 正是为此设计。transformers 列表每项是 (名称, 转换器, 列):
import pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
df = pd.DataFrame({
'年龄': [25, 30, 35],
'收入': [5000, 8000, 12000],
'城市': ['北京', '上海', '北京'],
'性别': ['男', '女', '男'],
})
preprocessor = ColumnTransformer(
transformers=[
('num', StandardScaler(), ['年龄', '收入']), # 数值列标准化
('cat', OneHotEncoder(), ['城市', '性别']), # 类别列独热
])
X_processed = preprocessor.fit_transform(df)
print(X_processed.shape) # 数值 2 列 + 独热 4 列 = 6 列
print(preprocessor.named_transformers_['num']) # 可访问各转换器transformers 的参数:第一项是名称,第二项是转换器('passthrough' 表示原样保留该列,'drop' 表示丢弃),第三项是列(列名列表或整数索引)。ColumnTransformer 在 fit 时对指定列训练对应转换器,transform 时各自应用并拼接输出,因此同样遵循「训练集 fit、测试集 transform」的防泄漏原则。
make_column_transformer 简化构造
make_column_transformer 省略名称,按转换器类型自动命名:
from sklearn.compose import make_column_transformer
preprocessor2 = make_column_transformer(
(StandardScaler(), ['年龄', '收入']),
(OneHotEncoder(), ['城市', '性别']),
remainder='drop', # 未指定列的处理方式,默认丢弃
)remainder 控制未在 transformers 中指定的列:默认 'drop' 丢弃,设为 'passthrough' 则原样保留。
make_column_selector 按类型选择列
列很多时手写列名麻烦,make_column_selector 按 dtype 或列名模式自动选列:
from sklearn.compose import make_column_selector
from sklearn.preprocessing import StandardScaler, OneHotEncoder
selector = make_column_selector(dtype_include='number') # 选数值列
print(selector(df)) # ['年龄', '收入']
preprocessor3 = make_column_transformer(
(StandardScaler(), make_column_selector(dtype_include='number')),
(OneHotEncoder(), make_column_selector(dtype_include='object')),
)make_column_selector(dtype_include='number') 选出所有数值列,dtype_include='object' 选出所有类别列;也可用 pattern='前缀' 按列名模式选择。配合 DataFrame 输入非常省事。
1.20.5 TransformedTargetRegressor 目标变量变换
有时目标变量也需要变换,比如偏斜的房价做对数变换后再回归,预测时再还原。TransformedTargetRegressor 自动完成「变换目标、训练、预测时逆变换」的全过程:
import numpy as np
from sklearn.compose import TransformedTargetRegressor
from sklearn.linear_model import LinearRegression
from sklearn.datasets import make_regression
from sklearn.model_selection import train_test_split
X, y = make_regression(n_samples=200, noise=0.1, random_state=0)
y = np.abs(y) + 1 # 构造正目标,适合对数变换
ttr = TransformedTargetRegressor(
regressor=LinearRegression(),
func=np.log, # 训练时对目标取对数
inverse_func=np.exp, # 预测时用指数还原
)
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)
ttr.fit(X_train, y_train)
print(ttr.predict(X_test)[:5])func 是训练时对 y 施加的变换,inverse_func 是预测时把预测值逆变换回原尺度。相比自己手动「先 log y 再训练、再 exp 还原」,TransformedTargetRegressor 把这个流程封装成普通估计器,可直接放进 Pipeline 或 GridSearchCV。
1.20.6 完整示例:ColumnTransformer + Pipeline + GridSearchCV
把上面各组件组合成一个完整流程:混合类型的房价数据,数值列标准化、类别列独热,再接一个回归模型,最后网格调参。关键点:所有预处理都放进管道,交叉验证时每折自动只在训练部分 fit:
import pandas as pd
import numpy as np
from sklearn.compose import ColumnTransformer, make_column_selector
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.linear_model import Ridge
from sklearn.model_selection import GridSearchCV, train_test_split
from sklearn.metrics import r2_score
# 构造混合类型数据:数值列 + 类别列 + 连续目标
rng = np.random.RandomState(0)
df = pd.DataFrame({
'面积': rng.normal(100, 20, 300),
'房龄': rng.randint(0, 40, 300),
'城市': rng.choice(['北京', '上海', '广州'], 300),
'朝向': rng.choice(['南', '北', '东西'], 300),
})
df['房价'] = 3 * df['面积'] - 2 * df['房龄'] + \
df['城市'].map({'北京': 100, '上海': 80, '广州': 60}) + rng.normal(0, 10, 300)
X = df.drop(columns='房价')
y = df['房价']
# 第 1 步:定义预处理,数值列标准化,类别列独热,自动选择列
preprocessor = ColumnTransformer([
('num', StandardScaler(), make_column_selector(dtype_include='number')),
('cat', OneHotEncoder(), make_column_selector(dtype_include='object')),
])
# 第 2 步:预处理 + 回归模型组装成管道
pipe = Pipeline([
('prep', preprocessor),
('model', Ridge()),
])
# 第 3 步:网格调参,参数名用 步骤名__参数名
param_grid = {'model__alpha': [0.1, 1, 10, 100]}
grid = GridSearchCV(pipe, param_grid, cv=5, scoring='r2')
grid.fit(X, y)
print(grid.best_params_)
print(grid.best_score_)
# 第 4 步:在留出的测试集上验证
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)
grid.fit(X_train, y_train)
print(r2_score(y_test, grid.predict(X_test)))整个流程中,预处理和建模绑定为一个估计器:grid.predict(X_test) 会自动先对测试集做同样的列变换,且变换参数全部来自训练部分,不会有数据泄漏。后续若增加特征选择、缺失值填补等步骤,只需往 steps 里继续追加即可。
练习题
第1题 概念理解
说明 Pipeline 中转换器与估计器的区别;说明 named_steps 与 set_params('步骤__参数') 的用途;说明把预处理放进管道为什么能防止交叉验证中的数据泄漏。
参考答案
管道中除最后一项外都是转换器(实现 transform),最后一项是估计器(实现 predict),fit 时数据依次流过转换器再到估计器。named_steps 按名称访问中间步骤,set_params('步骤__参数') 用双下划线语法设置某步参数。放进管道后,交叉验证每折只在训练部分 fit 转换器、验证部分只 transform,测试集信息不会进入训练过程。
第2题 代码编写
用 make_pipeline(StandardScaler(), SVC()) 训练并评估;再用 Pipeline([...]) 构造「标准化 + 主成分 + 逻辑回归」管道并用 GridSearchCV 调参。
参考答案
from sklearn.pipeline import make_pipeline, Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
from sklearn.svm import SVC
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import GridSearchCV, cross_val_score
from sklearn.datasets import load_digits
X, y = load_digits(return_X_y=True)
pipe1 = make_pipeline(StandardScaler(), SVC())
print(cross_val_score(pipe1, X, y, cv=5).mean())
pipe2 = Pipeline([
('scaler', StandardScaler()),
('pca', PCA(n_components=20)),
('lr', LogisticRegression(max_iter=1000)),
])
grid = GridSearchCV(pipe2, {'lr__C': [0.1, 1, 10]}, cv=5)
grid.fit(X, y)
print(grid.best_params_, grid.best_score_)第3题 进阶练习
构造含数值列与类别列的 DataFrame,用 ColumnTransformer 分别标准化与独热编码;把 TransformedTargetRegressor(对数变换目标)装进管道;最后用 make_column_selector 自动选列并跑一遍 GridSearchCV。
参考答案
import numpy as np
import pandas as pd
from sklearn.compose import ColumnTransformer, TransformedTargetRegressor, make_column_selector
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.linear_model import Ridge
from sklearn.model_selection import GridSearchCV
rng = np.random.RandomState(0)
df = pd.DataFrame({
'面积': rng.normal(100, 20, 200),
'城市': rng.choice(['A', 'B'], 200),
})
df['价格'] = np.abs(df['面积'] + df['城市'].map({'A': 10, 'B': -10})) + 1
X = df.drop(columns='价格')
y = df['价格']
preprocessor = ColumnTransformer([
('num', StandardScaler(), make_column_selector(dtype_include='number')),
('cat', OneHotEncoder(), make_column_selector(dtype_include='object')),
])
ttr = TransformedTargetRegressor(regressor=Ridge(), func=np.log, inverse_func=np.exp)
pipe = Pipeline([('prep', preprocessor), ('model', ttr)])
grid = GridSearchCV(pipe, {'model__regressor__alpha': [0.1, 1, 10]}, cv=5)
grid.fit(X, y)
print(grid.best_params_)
print(grid.best_score_)常见错误
错误 1 · Pipeline 的 steps 里所有项都写了估计器
原因:管道要求除最后一项外都是转换器,估计器没有 transform 方法。
解决:把估计器只放在列表最后,前面的步骤都用转换器。
错误 2 · set_params 参数名写错,如 svc_C
原因:步骤参数用双下划线 步骤名__参数名 连接。
解决:写 pipe.set_params(svc__C=10),并确保步骤名与 steps 里一致。
错误 3 · ColumnTransformer 没处理完的列被静默丢弃
原因:remainder 默认 'drop',未指定的列直接丢掉。
解决:想保留所有列就设 remainder='passthrough',或明确在 transformers 中列出。
错误 4 · 对测试集手动重复做预处理时复用了测试集的统计量
原因:手工流程容易把 scaler.fit_transform(X_test) 写成整段重新 fit。
解决:把预处理放进 Pipeline 或 ColumnTransformer,预测时统一 pipe.predict(X_test)。
错误 5 · TransformedTargetRegressor 逆变换写错导致预测尺度不对
原因:func 与 inverse_func 必须互逆,且 inverse_func 只作用于预测值。
解决:检查 np.log 对应 np.exp、np.sqrt 对应 np.square,确保互逆。
错误 6 · make_column_selector 在普通 DataFrame 上直接调用报错
原因:它是给 ColumnTransformer 用的选择器对象,单独使用时需要传入 DataFrame。
解决:在 make_column_transformer 或 ColumnTransformer 内部使用,或按 selector(df) 方式手动传数据。