跳到内容

第 12 卷 / 第 7 章 / 第 2 课

7.2 数值、类别与时间编码:转换参数属于模型 ​

通过可用时间检查后,气温、装备等级、任务类型和出发时刻可以作为输入,怎样表示仍有讲究:类别编号不是数量,23 点与午夜在日周期上只差一小时,缩放也会改变正则化惩罚。

转换不是数据表的永久“清洗结果”,而是模型的一部分。它必须随训练一起拟合、版本化和部署。

本课目标 ​

  • 根据模型机制决定是否缩放数值;
  • 正确处理无序、有序和高基数类别;
  • 理解目标编码为何必须交叉拟合;
  • 用 pipeline 保证各训练折与线上执行同一转换。

1. 缩放解决什么问题 ​

对距离、内积、梯度或系数惩罚敏感的模型,特征尺度会影响结果:

  • k-nearest neighbors 与聚类的距离由大尺度列主导;
  • ridge/lasso 对系数施加惩罚,尺度不一致时惩罚含义不同;
  • 神经网络和梯度优化常因尺度接近而更易训练。

树模型按阈值切分,通常不要求为了预测而标准化;但具体实现、正则化或后续解释工具仍可能带来例外。

  • StandardScaler 使用训练集均值与标准差;
  • RobustScaler 使用中位数与分位距,对极端值影响较小;
  • MinMaxScaler 映射训练范围,但不会让未来值自动留在区间内。

“有异常值就一定用 RobustScaler”不是规则。选择应由模型、损失、分布和验证结果共同决定。

2. 无序类别:One-Hot 与稀有值 ​

以下代码使用 scikit-learn 1.9 及以上版本、NumPy 和 pandas,按顺序运行。TargetEncoder 从 1.3 起提供;示例使用 1.9 新增的 CV 对象接口,在 KFold 上配置打乱规则。

任务类型没有自然顺序,把 auditor=1, rescue=2, transport=3 直接交给线性模型,会虚构距离与大小关系。One-hot 为各类别创建指示列。

python
import numpy as np
from sklearn.preprocessing import OneHotEncoder

encoder = OneHotEncoder(
    handle_unknown="infrequent_if_exist", min_frequency=2, sparse_output=False,
)
encoder.fit(np.array([["survey"]] * 5 + [["rescue"], ["transport"]], dtype=object))
print(encoder.get_feature_names_out())
print(encoder.transform([["survey"], ["rescue"], ["new_type"]]))

min_frequency 应由训练规模和业务语义确定,不存在“类别少于 10 就 one-hot”的通用边界。合并稀有类别能限制维度,却也可能把风险不同的群体混在一起。

未知类别策略必须明确:忽略、归入稀有组、拒绝请求,还是回退旧模型。离线验证要模拟线上可能出现的新类别。

样本中 survey 独占一列,rescue、transport 和 new_type 都进入稀有列。infrequent_if_exist 只有在拟合时确实形成稀有组才会使用该列;否则未知值也会像 ignore 一样变成全零。应检查拟合类别并测试两种情况。参见 OneHotEncoder。

在线性模型中随意丢掉 one-hot 类别会改变系数参照;带惩罚模型下,删除类别还可能引入不对称。编码参数应和模型解释方式一起设计。

3. 有序类别:顺序不等于等距 ​

low < medium < high 有顺序,可以使用显式类别表:

python
from sklearn.preprocessing import OrdinalEncoder

ordinal = OrdinalEncoder(
    categories=[["low", "medium", "high"]],
    handle_unknown="use_encoded_value", unknown_value=-1,
)
ordinal.fit([["low"], ["medium"], ["high"]])
print(ordinal.transform([["low"], ["high"], ["unknown"]]))  # 0, 2, -1

编码为 0、1、2 同时暗示相邻级别距离相等。若这个假设不合理,可以 one-hot、单调约束,或基于领域定义数值。不要用字母排序代替业务顺序。

-1 表示未知,不代表“比 low 更低”;直接输入线性模型时,它仍会按数值参与运算,需要单独处理未知状态。序数编码本身也不会强制模型输出单调。

4. 高基数类别与目标编码 ​

  • 合并有业务意义的层级;
  • 频次/计数编码;
  • 哈希编码;
  • 学习式 embedding;
  • 平滑目标编码。

目标编码用某类别的标签统计替换类别,最容易泄漏。训练行若参与计算自己的类别均值,罕见类别会近似记住标签。正确流程是:

  1. 在训练集内部划分折;
  2. 每一折的编码只由其他折标签计算;
  3. 验证集、测试集和线上数据只使用完整训练集得到的映射;
  4. 用全局先验与类别样本量做平滑;
  5. 未见类别回退到预先定义的先验。

这就是 cross-fitting。仅仅把目标编码器放进外层 pipeline,不代表其内部已经交叉拟合;要确认所用实现的 fit_transform 语义。

python
from sklearn.preprocessing import TargetEncoder
from sklearn.model_selection import KFold

rare_categories = np.array([[f"category_{i}"] for i in range(12)], dtype=object)
labels = np.array([0., 1.] * 6)
target_encoder = TargetEncoder(
    target_type="continuous", smooth=0.0,
    cv=KFold(n_splits=3, shuffle=True, random_state=0),
)
out_of_fold = target_encoder.fit_transform(rare_categories, labels)
full_mapping = target_encoder.transform(rare_categories)
print(out_of_fold.ravel())
print(full_mapping.ravel())  # exactly labels here: every category occurs once
print(target_encoder.transform([["unseen"]]))  # [[0.5]]

零平滑、每类只有一行时,全量映射会逐项复写训练标签;out-of-fold 值只使用其他折的先验。这里故意设零平滑以展示问题,不是通用建议。TargetEncoder明确区分 fit_transform(X,y) 与 fit(X,y).transform(X)。随机内部折不会自动满足时间或实体边界,内层切分和各折先验仍需遵守外层评估的信息范围。

5. 时间不是一个整数 ​

从时间戳可构造:

  • 小时、星期、月份与节假日;
  • 距离某事件的时长;
  • 滚动计数、速率与趋势;
  • 业务周期和季节。

小时具有周期性。对线性模型可以表示为:

xsin=sin⁡(2πh/24),xcos=cos⁡(2πh/24).

但周期编码不负责处理时区和夏令时。先明确业务使用的是当地时间还是 UTC,并处理重复或不存在的本地时刻。elapsed_days 还可能让模型记住训练期趋势,部署到新时期前要做时间外验证。

python
import pandas as pd

hours = np.array([23., 0., 6., 12., 18.])
cyclic = np.column_stack((np.sin(2 * np.pi * hours / 24), np.cos(2 * np.pi * hours / 24)))
print(np.linalg.norm(cyclic[0] - cyclic[1]))  # approximately 0.2611
utc_times = pd.to_datetime(["2026-11-01T05:30:00Z", "2026-11-01T06:30:00Z"], utc=True)
local_times = utc_times.tz_convert("America/New_York")
print(local_times)  # two different instants, both local 01:30, with different offsets

这两个本地 01:30 在 UTC 中相隔一小时,小时特征有意丢弃了这层区别;事件排序与经过时长必须保留真实时刻。一对正余弦只表示平滑的一阶日周期,不会表达任意逐小时差异。

6. 非线性变换与交互 ​

  • 对数变换可压缩右偏正值,但零值、负值和单位需要明确;
  • 分箱可表达阈值,却损失箱内信息,边界应在训练折学习;
  • 交互项表达“一个变量的作用依赖另一个变量”;
  • 比率特征需要稳定分母,并保留分子、分母以便诊断。

变换应对应合理机制或在独立验证中证明收益。大量自动交叉会扩大维度和过拟合空间。

7. 把转换放入 Pipeline ​

python
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler

numeric = ["temperature", "days_since_supply"]
categorical = ["mission_type", "equipment_level"]

preprocess = ColumnTransformer([
    ("num", Pipeline([
        ("impute", SimpleImputer(strategy="median", add_indicator=True)),
        ("scale", StandardScaler()),
    ]), numeric),
    ("cat", Pipeline([
        ("impute", SimpleImputer(strategy="most_frequent")),
        ("encode", OneHotEncoder(handle_unknown="ignore")),
    ]), categorical),
])

model = Pipeline([
    ("features", preprocess),
    ("classifier", LogisticRegression(max_iter=1000)),
])

交叉验证会在每个训练折重新拟合填充、缩放和类别词表。保存并部署整个 pipeline,而不是只保存最后的分类器。

python
training = pd.DataFrame({
    "temperature": [10., 20., np.nan, 30., 15., 35., 18., 28.],
    "days_since_supply": [1., 2., 3., 9., np.nan, 8., 2., 7.],
    "mission_type": ["survey", "repair"] * 4,
    "equipment_level": ["low", "high", "low", "high", "low", "high", "low", "high"],
})
y_training = np.array([0, 0, 1, 1, 0, 1, 0, 1])
model.fit(training, y_training)
new_rows = pd.DataFrame({
    "temperature": [np.nan], "days_since_supply": [4.],
    "mission_type": ["unseen"], "equipment_level": ["high"],
})
print(model.predict_proba(new_rows))

这个小样本只核对拟合后的 Pipeline 与一个未知类别,不估计预测质量。缺失指示列根据拟合时出现过的缺失模式生成,监控也要发现“训练从未缺过、线上开始缺”的字段。业务预定义的固定阈值或日历周期不必从数据学习,数据估计的边界与参数才需要折内拟合。

常见误区 ​

  • 所有模型都要标准化:取决于模型机制,不是数据科学仪式。
  • 整数编码类别最节省空间:它可能向模型注入虚假的顺序和距离。
  • 目标编码只要先切分就安全:训练集内部仍需 out-of-fold 编码。
  • 时间拆得越细越好:高维日历特征可能记住历史噪声。

练习 ​

  1. 为线性模型与树模型分别解释哪些数值列需要缩放。
  2. 用训练集中未出现的任务类型测试 unknown category 行为。
  3. 实现 out-of-fold 目标编码,并与错误的全训练集编码比较验证分数。
  4. 用 UTC 和当地时间各生成小时特征,检查夏令时边界。

小结 ​

编码和缩放都在引入数学假设。参数必须只从训练数据学习,未知值和时间边界必须预先定义,完整转换链要与模型一起交付。

下一课把候选特征收束成可维护接口:选择必须发生在验证过程内,训练与服务还要保持同一份语义。

Built with VitePress | Software Systems Atlas