8.2 特征、目标与预处理
预测契约要通过两个对齐对象变成数据:特征矩阵 与目标向量 。这个过程绝不是机械地“删除标签列”。每一行都要符合契约中的预测单位,每个特征都必须在预测时刻存在,每项转换也只能从被允许的训练数据中学习。
用特征可用性审计构建 与
在表格型监督学习中, 的形状通常是 , 则为每个样本提供一个目标。二者的行对齐本身就是数据含义的一部分:
feature_columns = ["amount", "zone", "queue_size"]
X = orders.loc[:, feature_columns].copy()
y = orders.loc[X.index, "late"].copy()
assert X.index.equals(y.index)
assert "late" not in X.columns这里应使用显式特征白名单。orders.drop(columns="late") 会默默接纳其他所有列,包括下个月才新增的事后字段;白名单则能让模式变化在代码审查中显现。
批准一个字段之前,应记录它的来源、定义、最早生成时刻、数据类型、缺失行为和生产负责人,然后追问:线上系统能否在预测时刻以完全相同的定义生成这个值? 与目标高度相关,并不能回答这个问题。
三类特征陷阱值得单独标记:
1. 时间泄漏:actual_delivery_minutes 在派单后才产生,不能用于派单时预测延迟。
2. 目标代理泄漏:refund_after_delivery 很可能几乎揭示延迟标签。即使某个历史快照让它看起来“已经存储”,它的业务含义仍来自结果发生之后。
3. 标识符记忆:order_id 唯一且通常没有可复用规律;customer_id 可能让随机划分中的模型记住回头客。它是否合法,取决于部署人群与划分策略。
可以把这些判断记录成一张小型字段目录:
catalog = pd.DataFrame({
"field": ["amount", "zone", "queue_size",
"order_id", "actual_minutes", "refunded"],
"available_at": ["order", "order", "dispatch",
"order", "delivery", "after_delivery"],
"role": ["feature", "feature", "feature",
"identifier", "target", "target_proxy"],
})特征可用性取决于生产时间,而不是它在 dataframe 中的位置。历史表里某列可以紧挨目标列,却在数小时后才生成。还要检查训练与服务是否一致:一个数据仓库每晚计算的聚合值,并不会自动出现在实时 API 中。
让每类字段走自己的训练期预处理路径
多数估计器希望收到数值矩阵,原始表格却混合了数值、缺失、类别、布尔和日期。预处理(preprocessing)的任务,是把每项声明过的输入翻译成稳定表示。
数值列与类别列通常需要不同策略:
| 输入类型 | 常见操作 | fit 时学到的状态 |
|---|---|---|
| 数值 | 中位数填补、按需标准化 | 中位数、均值、标准差 |
| 无序类别 | 众数填补、one-hot 编码 | 填充值、类别词表 |
| 有序类别 | 显式序数映射 | 声明顺序或学得词表 |
| 日期时间 | 提取契约相关部分 | 通常没有;后续缩放/编码除外 |
距离模型和带正则化的线性模型会受到特征数量级影响,因此常常需要缩放;树模型的阈值排序通常不受线性缩放影响。缩放是由模型决定的选择,不是每个数值列都必须执行的仪式。
对 vehicle_type 这类无序类别,随意编号会制造虚假顺序:自行车为 0、汽车为 1、滑板车为 2,暗示了不存在的“倍数关系”。one-hot 编码会为不同类别建立独立指示列。线上还可能出现训练时没见过的新类别,因此要明确配置编码器:
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler
numeric_columns = ["amount", "queue_size"]
categorical_columns = ["zone", "vehicle_type"]
numeric_pipeline = Pipeline([
("impute", SimpleImputer(strategy="median")),
("scale", StandardScaler()),
])
categorical_pipeline = Pipeline([
("impute", SimpleImputer(strategy="most_frequent")),
("onehot", OneHotEncoder(handle_unknown="ignore")),
])
preprocess = ColumnTransformer(
transformers=[
("numeric", numeric_pipeline, numeric_columns),
("categorical", categorical_pipeline, categorical_columns),
],
remainder="drop",
)remainder="drop" 是刻意设置的模式边界:未声明字段不会流入模型。拟合后可用 get_feature_names_out() 把展开后的矩阵列追溯到来源。one-hot 结果通常是稀疏矩阵;把大型稀疏矩阵强行转成稠密表示,可能耗尽内存。
预处理学到的每个数值都是模型状态。数值列中位数、标准化所用标准差和类别词表,都必须只从训练部分估计。下面的顺序是错误的:
# 错误:验证行影响了预处理状态。
X_ready = preprocess.fit_transform(X)
cross_validate(model, X_ready, y, cv=5)修复方式不是提醒自己“下次注意顺序”,而是改变结构:把 preprocess 与估计器放进同一个 Pipeline。这样交叉验证会在每个训练折内拟合整条流水线的新副本。8.4 节会直接检查这条边界。
现在,我们已经有了明确目标、时间上合法的特征集合和可复现的预处理分支。下一步要决定哪些行可以教会工作流,哪些行必须保留为独立证据。