同一张训练表里有年龄、金额和城市时,应让数值列与类别列走不同的转换,再合成模型输入。ColumnTransformer 可以按列指定流程,避免给城市名做均值缩放,也避免把整张表转成无来源的数字数组。本文的目标是把混合列管道跑通,并核对新城市、空值和样本编号的处理。
定义每一列在预测时的职责
示例核对环境为 Python 3.11、scikit-learn 1.9.1、NumPy 2.4.6;涉及表格的代码还使用 pandas 3.0.6。资料核对日期为 2026 年 10 月 1 日。代码在 CPU 上运行,不需要账号、API 密钥或下载预训练权重。

本例使用 8 条人为设定的训练记录和 2 条新记录。age、spend 是数值特征,city 是类别特征,row_id 只用于追溯,标签单独放在 y 中。演示的 0/1 标签及预测不代表真实消费人群,也没有用这么少的数据测量模型效果。
| 列 | 转换方式 | 需要核对的结果 |
|---|---|---|
| age、spend | 中位数填补,再标准化 | 新数据空值使用训练集的中位数 |
| city | 众数填补,再独热编码 | 已知城市有对应列;未知城市不新增列 |
| row_id | 不参与训练 | 输出特征名称中没有编号列 |
组合估计器官方指南解释了 ColumnTransformer 按指定列拼接输出,以及 remainder 默认丢弃其余列的行为。官方混合类型示例给出了数值分支和类别分支分别处理、再连接分类器的方式。
建立完整混合列管道
python -m venv .venv
# Windows PowerShell 中使用这个环境的解释器:
.\.venv\Scripts\python.exe -m pip install scikit-learn==1.9.1 numpy==2.4.6 pandas==3.0.6
.\.venv\Scripts\python.exe demo.py
把下方完整 Python 代码保存为 demo.py,再运行最后一条命令。macOS/Linux 使用 .venv/bin/python 代替 Windows 路径。安装需要网络;完成安装后,这些示例的数据在代码中生成,可离线运行。使用其他版本时先打印 sklearn.__version__,不要把两个环境的报错混在一起排查。
下方把填补、缩放、编码和分类器连接为一个 Pipeline。只调用一次 model.fit(train, y);新记录调用 predict,不重新 fit。类别分支设置 handle_unknown=”ignore”,让示例中的武汉不因未见过而报错,但其城市编码会全为零。
import numpy as np
import pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.linear_model import LogisticRegression
train = pd.DataFrame({
"row_id": [f"R{i}" for i in range(8)],
"age": [24, 31, 29, np.nan, 45, 33, 28, 40],
"spend": [30, 60, 35, 95, 120, 110, 20, 105],
"city": ["北京", "上海", "北京", "杭州", "上海", "杭州", "北京", "上海"],
})
y = [0, 0, 0, 1, 1, 1, 0, 1]
new = pd.DataFrame({"row_id": ["T0", "T1"], "age": [np.nan, 38],
"spend": [90, 25], "city": ["武汉", "北京"]})
numeric = Pipeline([("fill", SimpleImputer(strategy="median")),
("scale", StandardScaler())])
categorical = Pipeline([("fill", SimpleImputer(strategy="most_frequent")),
("encode", OneHotEncoder(handle_unknown="ignore", sparse_output=False))])
prep = ColumnTransformer([("num", numeric, ["age", "spend"]),
("cat", categorical, ["city"])], remainder="drop")
model = Pipeline([("prep", prep), ("clf", LogisticRegression(max_iter=1000))])
model.fit(train, y)
matrix = model.named_steps["prep"].transform(new)
names = model.named_steps["prep"].get_feature_names_out()
print("features", names.tolist())
print("shape", matrix.shape)
print("unknown-city onehot", matrix[0, 2:].tolist())
print("prediction", model.predict(new).tolist())
assert matrix.shape == (2, 5) and np.isfinite(matrix).all()
assert all("row_id" not in x for x in names)
assert np.array_equal(matrix[0, 2:], np.zeros(3))
检查转换矩阵和未知城市
features ['num__age', 'num__spend', 'cat__city_上海', 'cat__city_北京', 'cat__city_杭州']
shape (2, 5)
unknown-city onehot [0.0, 0.0, 0.0]
prediction [1, 0]
本例输出 2 行、5 列:2 个数值特征加上训练中出现的 3 个城市。第一行武汉的三列独热编码全为零;这表示“没有任何已知城市匹配”,不等于武汉和北京具有同样属性。row_id 不在 get_feature_names_out() 的结果中,避免模型把 R0、R1 当作可泛化特征。
用同一个 prep.transform(new) 核对输出列数、列名和有限数,再调用模型预测。不要手工重新排列转换矩阵的列,否则模型接收的特征含义会变化。实际业务应统计未知类别比例;比例突然上升时,先查编码、字段变化或新数据来源,再决定是否重新训练。
空值和列变动的处理边界
中位数填补只解决“按定义允许缺失”的数值,不负责判断金额是否为负、年龄是否合理或金额字符串是否含货币符号。这样的错误要在转换前检查。训练时某列全部为空、预测时缺少 age 列、字符串混入数值列,都需要先修正数据规则,再运行转换。
类别缺失应统一表示。上例没有为 city 加缺失数据;若自己的 CSV 用空字符串、None 和文字“未知”混合表示,应明确哪些需要作为缺失送给填补器,哪些是合法类别。不要假设所有写法都会自动被同一种填补策略处理。
这里使用列名选择,所以输入是带名称的 DataFrame。替换成没有列名的数组后要重新定义索引选择规则。扩展列时同时检查允许进入模型的字段清单;答案列和事后统计列即使能被成功编码,也不能因此用于真实预测。
Ai菜鸟网。发布者:AI小管家,转载请注明出处:https://www.alyyhw.com/30566.html