《sklearn机器学习——管道和复合估算器》异构数据的列转换器

在实际的机器学习项目中,数据集往往包含多种类型的数据列:数值型(如年龄、收入)、类别型(如性别、城市)、文本型(如评论、描述),甚至可能还有日期、图像路径等。这些不同类型的列需要不同的预处理方式:

  • 数值列: 通常需要缩放(如 StandardScalerMinMaxScaler)或填补缺失值(SimpleImputer)。
  • 类别列: 通常需要编码(如 OneHotEncoderOrdinalEncoder)或填补缺失值。
  • 文本列: 通常需要向量化(如 CountVectorizerTfidfVectorizer)。
  • 其他列: 可能需要自定义转换,或者直接丢弃。

手动对每一列或每一组列分别进行预处理,然后再拼接起来,不仅代码冗长、容易出错,而且难以与 scikit-learn 的管道(Pipeline)和交叉验证等工具无缝集成。

ColumnTransformer 就是为了解决这个问题而设计的。它允许你为数据框(DataFrame)或数组中的不同列子集指定不同的转换器,并将所有转换后的结果自动拼接成一个单一的特征矩阵,供后续的机器学习模型使用。

一、核心概念与优势

  • 异构数据(Heterogeneous Data): 指数据集中包含多种不同数据类型或需要不同处理方式的列。
  • 列转换器(ColumnTransformer): 一个 scikit-learn 的转换器(Transformer),它接受一个转换器列表,每个转换器负责处理数据的一个特定子集(列)。

· 优势:

  • 简洁性: 用一个对象管理所有列的预处理逻辑。
  • 一致性: 保证训练集和测试集使用完全相同的预处理步骤,避免数据泄露。
  • 可组合性: 可以轻松嵌入到 pipeline中,构建端到端的机器学习工作流。
  • 效率: 内部优化,通常比手动操作更高效。
  • 灵活性: 支持按列名、列索引、数据类型或自定义函数选择列。

二、ColumnTransformer 详解

1. 基本语法

python 复制代码
from sklearn.compose import ColumnTransformer

transformer = ColumnTransformer(
    transformers,              # 核心参数:转换器列表
    remainder='drop',          # 如何处理未指定的列
    sparse_threshold=0.3,      # 控制输出是否为稀疏矩阵
    n_jobs=None,               # 并行处理
    transformer_weights=None,  # 为不同转换器的输出赋予权重
    verbose=False,             # 是否打印进度
    verbose_feature_names_out=True  # 是否修改输出特征名
)

2. 核心参数 transformers

这是最重要的参数,它是一个列表 ,列表中的每个元素是一个三元组

python 复制代码
(name, transformer, columns)
  • name (str): 该转换步骤的唯一标识符。主要用于调试、错误信息和 -get_feature_names_out() 方法。可以是任意字符串,但建议具有描述性(如 'num','cat','text')。

  • transformer: 要应用的转换器对象。这个对象必须实现 fit 和 transform 方法(即符合 scikit-learn 的 Transformer API)。常见的有:

  • sklearn.preprocessing 中的各种预处理器(StandardScaler, -OneHotEncoder, LabelEncoder 注意:LabelEncoder 通常不用于特征,而用于目标变量)。

  • sklearn.impute.SimpleImputer

  • sklearn.feature_extraction.text 中的向量化器(CountVectorizer, TfidfVectorizer)。

  • 自定义的转换器。

  • 特殊值 'passthrough': 表示选定的列不进行任何转换,直接保留原样。

  • 特殊值 'drop': 表示丢弃选定的列。

  • columns: 指定该转换器应用于哪些列。有多种指定方式:

    • 字符串列表: ['col1', 'col2', 'col3'] (最常用,当数据是 DataFrame 且你知道列名时)。
    • 整数列表: [0, 1, 2] (按列索引选择,适用于 NumPy 数组或按位置选择)。
    • 切片: slice(0, 3)0:3 (选择前3列)。
    • 布尔掩码: [True, False, True] (选择第1列和第3列)。
    • 可调用函数 (Callable) : 一个接收列名数组并返回布尔掩码或列索引列表的函数。例如:lambda x: x.isin(['A', 'B'])lambda cols: [i for i, col in enumerate(cols) if col.startswith('feature_')]
    • 数据类型 (dtype) : 例如 np.number(选择所有数值型列),'category'(选择所有类别型列,如果 DataFrame 的 dtype 是 category)。注意:这种方式依赖于数据的 dtype 设置。

3. 关键参数 remainder

  • 作用 : 定义如何处理那些没有被 transformers 列表中任何一项指定的列。
  • 可选值 :
    • 'drop' (默认值): 丢弃所有未指定的列。这是最安全、最常用的选择,确保你只处理了明确指定的列。
    • 'passthrough': 保留所有未指定的列,不进行任何转换,直接拼接到最终结果中。注意:这要求这些列已经是数值型或可以被后续模型直接处理的格式,否则可能会导致错误。
    • 一个转换器对象 : 为所有剩余的列应用同一个转换器。例如,你可以用 StandardScaler() 来缩放所有剩下的数值列。

4. 参数 sparse_threshold

  • 作用 : 控制 ColumnTransformer 的输出是稠密数组(numpy.ndarray)还是稀疏矩阵(scipy.sparse matrix)。
  • 原理 : 如果所有转换器的输出都是稠密的,最终输出是稠密的。如果至少有一个转换器输出稀疏矩阵(如 OneHotEncoder 默认输出稀疏矩阵),则计算稀疏矩阵在最终输出中所占的比例。
  • 默认值 0.3: 如果稀疏输出的比例 >= 0.3,则最终输出为稀疏矩阵;否则,转换为稠密数组。
  • 设置为 0: 强制输出为稠密数组(即使有稀疏转换器)。
  • 设置为 1: 只有当所有转换器输出都是稀疏时,最终输出才是稀疏矩阵。
  • 建议 : 如果后续模型支持稀疏输入(如 sklearn.linear_model 中的很多模型),保留稀疏性可以节省大量内存,特别是当 OneHotEncoder 产生大量零时。如果不支持,则设为 0。

5. 参数 verbose_feature_names_out

  • 作用 : 控制 get_feature_names_out() 方法返回的特征名称格式。

  • True (默认): 输出的特征名会包含转换器的 name 作为前缀,格式为 {transformer_name}__{original_feature_name}{transformer_name}__{transformed_feature_name}。这有助于追踪特征来源,避免不同转换器产生的特征名冲突。

  • False: 尝试直接使用转换器输出的原始特征名。注意:如果不同转换器产生了相同的特征名,或者转换器本身不提供特征名,这可能会导致错误或混淆。

三、常用方法

ColumnTransformer 是一个标准的 scikit-learn Transformer,主要方法有:

  • .fit(X[, y]): 根据数据 X(和可选的 y)拟合所有指定的转换器。例如,StandardScaler 会计算均值和标准差,OneHotEncoder 会学习类别。

  • .transform(X): 对数据 X 应用已拟合的转换器,返回转换后的特征矩阵(通常是 numpy.ndarrayscipy.sparse matrix)。

  • .fit_transform(X[, y]): 先 fittransform,一步到位,更高效。

  • .get_feature_names_out([input_features]): 获取转换后输出特征的名称。这对于理解模型输入和调试非常有用。input_features 通常不需要传,它会从 X 中推断。

  • .set_output(*, transform=None): (较新版本) 设置输出容器类型,例如设置为 'pandas' 可以让 transform 返回 DataFrame。注意:这要求所有内部转换器也支持此设置。

四、实战示例

让我们通过一个详细的例子来演示 ColumnTransformer 的使用。

示例数据

假设我们有一个包含客户信息的 DataFrame:

python 复制代码
import pandas as pd
import numpy as np
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.impute import SimpleImputer
from sklearn.pipeline import Pipeline
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split

# 创建示例数据
data = {
    'age': [25, np.nan, 35, 45, 23],          # 数值列,有缺失值
    'salary': [50000, 60000, 75000, np.nan, 48000], # 数值列,有缺失值
    'city': ['New York', 'London', np.nan, 'Tokyo', 'New York'], # 类别列,有缺失值
    'department': ['IT', 'HR', 'IT', 'Finance', 'HR'], # 类别列,无缺失
    'experience': [2, 5, 10, 15, 1]            # 数值列,无缺失
}

df = pd.DataFrame(data)
print("原始数据:")
print(df)
#    age   salary      city department  experience
# 0  25.0  50000.0  New York         IT           2
# 1   NaN  60000.0    London         HR           5
# 2  35.0  75000.0       NaN         IT          10
# 3  45.0      NaN     Tokyo    Finance          15
# 4  23.0  48000.0  New York         HR           1

目标

  • 对数值列 (age, salary, experience):先用均值填补缺失值,再进行标准化。
  • 对类别列 (city, department):先用常数 'Unknown' 填补缺失值,再进行独热编码。
  • 将处理后的数据输入到一个逻辑回归模型中。

步骤 1: 定义转换器

python 复制代码
# 1. 定义数值列转换器:先填补,再缩放
#    可以使用 Pipeline 将多个步骤组合成一个转换器
num_pipeline = Pipeline([
    ('imputer', SimpleImputer(strategy='mean')),  # 填补缺失值
    ('scaler', StandardScaler())                  # 标准化
])

# 2. 定义类别列转换器:先填补,再编码
cat_pipeline = Pipeline([
    ('imputer', SimpleImputer(strategy='constant', fill_value='Unknown')), # 填补缺失
    ('encoder', OneHotEncoder(drop='first', sparse_output=False))          # 独热编码,避免多重共线性,输出稠密
])
# 注意:drop='first' 移除第一个类别以避免多重共线性。sparse_output=False 确保输出稠密数组,方便演示。
# 在较新版本的 sklearn 中,参数是 sparse_output,在旧版本中是 sparse。

步骤 2: 创建 ColumnTransformer

python 复制代码
# 创建 ColumnTransformer
preprocessor = ColumnTransformer(
    transformers=[
        ('num', num_pipeline, ['age', 'salary', 'experience']),  # 处理数值列
        ('cat', cat_pipeline, ['city', 'department'])           # 处理类别列
    ],
    remainder='drop',                # 丢弃其他未指定列(本例中没有)
    sparse_threshold=0,              # 强制输出稠密数组(因为 cat_pipeline 已设 sparse_output=False)
    verbose_feature_names_out=True    # 输出带前缀的特征名
)

# 查看转换器结构
print("\nColumnTransformer 结构:")
print(preprocessor)

步骤 3: 使用 ColumnTransformer

python 复制代码
# 假设我们有一个目标变量(这里随机生成用于演示)
y = np.array([0, 1, 1, 0, 1])

# 拟合并转换数据
X_transformed = preprocessor.fit_transform(df)

print("\n转换后的特征矩阵 (X_transformed):")
print(X_transformed)
print("形状:", X_transformed.shape) # (5, 6) 3个数值列 + city(3-1=2个编码列) + department(3-1=2个编码列) - 1(因为drop='first')? 等等,我们来算一下

# 实际上:
# - 数值列: 3列 (age, salary, experience)
# - city: 原本有 'New York', 'London', 'Tokyo', 'Unknown' (填补后) -> OneHotEncoder(drop='first') 会生成 3 列 (去掉一个基准类别)
# - department: 'IT', 'HR', 'Finance' -> OneHotEncoder(drop='first') 生成 2 列
# 总共 3 + 3 + 2 = 8 列?等等,为什么上面输出是 (5, 6)?

# 啊,问题出在:我们的数据只有5行,且 city 列有缺失,填补后是 ['New York', 'London', 'Unknown', 'Tokyo', 'New York']。
# OneHotEncoder 会学习到4个唯一值:'New York', 'London', 'Unknown', 'Tokyo'。drop='first' 后剩下3列。
# department 有3个唯一值:'IT', 'HR', 'Finance'。drop='first' 后剩下2列。
# 3(数值) + 3(city) + 2(department) = 8列。但上面代码可能因为数据量小或版本问题显示不同,我们重新检查。

# 让我们正确获取特征名来看看
feature_names = preprocessor.get_feature_names_out()
print("\n转换后的特征名称:")
print(feature_names)
# 输出类似:
# ['num__age' 'num__salary' 'num__experience' 'cat__city_London' 'cat__city_New York'
#  'cat__city_Tokyo' 'cat__city_Unknown' 'cat__department_HR' 'cat__department_IT']
# 等等,这看起来是9列?因为 city 有4个类别,drop='first' 应该去掉一个,比如去掉 'New York',那么剩下 'London', 'Tokyo', 'Unknown' 3列。
# department 有3个类别,drop='first' 去掉一个,比如去掉 'Finance',剩下 'HR', 'IT' 2列。
# 3 + 3 + 2 = 8列。但 get_feature_names_out 可能会列出所有,实际矩阵是8列。

# 修正:让我们打印形状和特征名数量
print("X_transformed shape:", X_transformed.shape) # 应该是 (5, 8)
print("Number of feature names:", len(feature_names)) # 应该是 8

# 如果确实是8列,那么之前的 (5, 6) 可能是我的笔误或旧版本行为。我们按8列继续。

步骤 4: 与 Pipeline 结合 (推荐做法)

将 ColumnTransformer 作为预处理步骤,与机器学习模型组合成一个完整的 Pipeline。这是最佳实践!

python 复制代码
# 创建完整管道:预处理 + 模型
full_pipeline = Pipeline([
    ('preprocessor', preprocessor),      # 第一步:预处理
    ('classifier', LogisticRegression()) # 第二步:分类器
])

# 划分训练/测试集 (虽然数据很少,仅作演示)
X_train, X_test, y_train, y_test = train_test_split(df, y, test_size=0.2, random_state=42)

# 训练整个管道
full_pipeline.fit(X_train, y_train)

# 预测
predictions = full_pipeline.predict(X_test)
print("\n预测结果:", predictions)

# 获取预测概率
probabilities = full_pipeline.predict_proba(X_test)
print("预测概率:", probabilities)

# 评估 (同样,数据少,仅演示)
score = full_pipeline.score(X_test, y_test)
print("模型在测试集上的得分:", score)

步骤 5: 处理 remainder

假设我们的 DataFrame 多了一列 'employee_id',我们想直接丢弃它:

python 复制代码
df_with_id = df.copy()
df_with_id['employee_id'] = [101, 102, 103, 104, 105]

# 方法1: 在 ColumnTransformer 中不指定它,remainder='drop' (默认) 会自动丢弃
preprocessor_v2 = ColumnTransformer(
    transformers=[
        ('num', num_pipeline, ['age', 'salary', 'experience']),
        ('cat', cat_pipeline, ['city', 'department'])
        # 'employee_id' 未被指定,且 remainder='drop' -> 被丢弃
    ]
)

X_transformed_v2 = preprocessor_v2.fit_transform(df_with_id)
print("\n丢弃 employee_id 后的形状:", X_transformed_v2.shape) # 应该还是 (5, 8)

# 方法2: 显式指定丢弃
preprocessor_v3 = ColumnTransformer(
    transformers=[
        ('num', num_pipeline, ['age', 'salary', 'experience']),
        ('cat', cat_pipeline, ['city', 'department']),
        ('drop_id', 'drop', ['employee_id']) # 显式丢弃
    ],
    remainder='passthrough' # 或者 'drop',但显式指定更好
)

如果我们想保留 'employee_id' (假设它是数值且无需处理):

python 复制代码
preprocessor_v4 = ColumnTransformer(
    transformers=[
        ('num', num_pipeline, ['age', 'salary', 'experience']),
        ('cat', cat_pipeline, ['city', 'department'])
        # 不指定 'employee_id'
    ],
    remainder='passthrough' # 保留未指定列
)

X_transformed_v4 = preprocessor_v4.fit_transform(df_with_id)
print("\n保留 employee_id 后的形状:", X_transformed_v4.shape) # (5, 9) 8 + 1

# 查看特征名
feature_names_v4 = preprocessor_v4.get_feature_names_out()
print("特征名 (包含 passthrough 列):", feature_names_v4)
# 输出会包含类似 'remainder__employee_id' 的名称

五、高级技巧与注意事项

  • 使用 make_column_transformersklearn.compose 还提供了一个函数 make_column_transformer,语法更简洁,适合快速创建。
python 复制代码
from sklearn.compose import make_column_transformer

preprocessor_simple = make_column_transformer(
    (num_pipeline, ['age', 'salary', 'experience']),
    (cat_pipeline, ['city', 'department']),
    remainder='drop'
)
# 它会自动生成 name,如 'pipeline-1', 'pipeline-2'
  • 按数据类型选择列: 如果你的 DataFrame 的 dtype 设置正确,可以直接用 np.number, 'category' 等。
python 复制代码
# 确保 dtype 正确
df_typed = df.copy()
df_typed['city'] = df_typed['city'].astype('category')
df_typed['department'] = df_typed['department'].astype('category')

preprocessor_dtype = ColumnTransformer(
    transformers=[
        ('num', num_pipeline, np.number), # 选择所有数值列
        ('cat', cat_pipeline, 'category') # 选择所有类别列
    ]
)
  • 调试: 如果转换出错,检查:

    • 列名是否拼写正确。
    • 转换器是否适合该列的数据类型(例如,不能对数值列用 OneHotEncoder)。
    • remainder 的设置是否符合预期。
    • 使用 get_feature_names_out() 查看输出特征,帮助理解转换结果。
  • 性能 : 如果转换器支持并行化(如 n_jobs 参数),可以在 ColumnTransformer 或内部转换器中设置 n_jobs 来加速。

  • 稀疏性 : 谨慎管理 sparse_threshold。如果后续模型(如 RandomForest, SVM with non-linear kernel)不支持稀疏输入,务必设置 sparse_threshold=0 或在转换器内部(如 OneHotEncoder(sparse_output=False))处理。

  • 与 Pipeline 结合 : 强烈推荐将 ColumnTransformer 作为 Pipeline 的第一步。这确保了整个流程(预处理+建模)可以用 fit / predict 统一调用,并且可以方便地进行交叉验证和超参数调优。

  • 特征名称 : verbose_feature_names_out=True 是默认且推荐的,它提供了清晰的特征来源信息。在部署或特征重要性分析时非常有用。


六、总结

ColumnTransformerscikit-learn 中处理异构数据的利器。它通过将不同的预处理步骤并行应用于数据的不同列集合,并自动拼接结果,极大地简化了复杂数据预处理的代码,提高了可维护性和可复用性。

掌握 ColumnTransformer 的核心在于理解其 transformers 参数(三元组:名称、转换器、列选择器)和 remainder 参数。结合 Pipeline 使用,可以构建出强大、清晰、端到端的机器学习工作流。

通过本文的超详细讲解和实战示例,你应该能够熟练地在自己的项目中应用 ColumnTransformer 来处理各种复杂的异构数据了。

相关推荐
非门由也4 小时前
《sklearn机器学习——管道和复合估计器》联合特征(FeatureUnion)
人工智能·机器学习·sklearn
l12345sy4 小时前
Day21_【机器学习—决策树(1)—信息增益、信息增益率、基尼系数】
人工智能·决策树·机器学习·信息增益·信息增益率·基尼指数
计算机毕业设计指导4 小时前
基于ResNet50的智能垃圾分类系统
人工智能·分类·数据挖掘
飞哥数智坊4 小时前
终端里用 Claude Code 太难受?我把它接进 TRAE,真香!
人工智能·claude·trae
java1234_小锋4 小时前
Scikit-learn Python机器学习 - 特征降维 压缩数据 - 特征提取 - 主成分分析 (PCA)
python·机器学习·scikit-learn
java1234_小锋4 小时前
Scikit-learn Python机器学习 - 特征降维 压缩数据 - 特征提取 - 线性判别分析 (LDA)
python·机器学习·scikit-learn
小王爱学人工智能4 小时前
OpenCV的阈值处理
人工智能·opencv·计算机视觉
新智元5 小时前
刚刚,光刻机巨头 ASML 杀入 AI!豪掷 15 亿押注「欧版 OpenAI」,成最大股东
人工智能·openai
机器之心5 小时前
全球图生视频榜单第一,爱诗科技PixVerse V5如何改变一亿用户的视频创作
人工智能·openai