跳到内容

第 12 卷 / 第 7 章 / 第 3 课

7.3 特征选择与生产一致性:从候选列到可监控接口 ​

离线七日补货次数可能用了历史预测之后才回填的记录,线上值却正确地没有算入;未知类别编码为全零也可能是约定策略。问题在于两侧是否遵守相同的可用时间与兜底规则,要先按契约核对,不能看哪个数字大就修哪个。

本课把特征选择放回验证流程,再检查选中的特征能否在生产环境中复现、监控和回滚。

本课目标 ​

  • 区分过滤式、包裹式与嵌入式选择;
  • 把特征选择放入交叉验证;
  • 检查选择稳定性和真正的增量价值;
  • 设计离线/在线一致性测试与监控。

1. 选择目标不是“列越少越好” ​

特征选择可能服务于不同目标:

  • 降低采集、计算和在线延迟成本;
  • 减少高维噪声和过拟合;
  • 提高可解释性或满足合规约束;
  • 消除预测时不可得、不稳定或冗余的输入。

先确定约束,再比较方案。不存在“样本数必须是特征数十倍”的通用法则:可识别性取决于模型、正则化、稀疏性、噪声、相关结构和验证设计。

2. 三类选择方法 ​

过滤式 ​

按缺失率、近零方差、相关性或单变量统计量筛选,计算便宜,但忽略特征组合。任何使用标签的评分都必须在训练折内计算。

包裹式 ​

递归消除、顺序选择等反复训练模型,以验证表现选择子集。能考虑模型交互,但计算昂贵,候选多时容易对验证过程过拟合。

嵌入式 ​

Lasso、树的分裂和某些稀疏模型在拟合时完成选择。结果依赖模型偏好:lasso 在相关特征间可能不稳定,树的重要性也不等于因果作用。

领域审查是第四道门:合法但昂贵、难以解释或可能编码敏感属性的特征,不能只凭分数进入生产。

3. 选择必须在交叉验证内部 ​

下面的完整 scikit-learn 示例使用独立合成观测中的六个连续测量列,在训练 CV 内搜索可行的特征数量,选定后再评估留出的测试集。

先在全数据中挑出“与标签最相关的 20 列”,再做交叉验证,验证折标签已经参与选列。正确做法是把选择器放进 pipeline:

python
from functools import partial
import numpy as np
import pandas as pd
from sklearn.feature_selection import SelectKBest, mutual_info_classif
from sklearn.impute import SimpleImputer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import GridSearchCV, StratifiedKFold, train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler

rng = np.random.default_rng(9)
X = pd.DataFrame(rng.normal(size=(120, 6)), columns=[f"measurement_{i}" for i in range(6)])
y = (X["measurement_0"] - .8 * X["measurement_1"] + rng.normal(0, .5, 120) > 0).astype(int)
X.loc[4, "measurement_2"] = np.nan
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=.25, stratify=y, random_state=0)
preprocess = Pipeline([
    ("impute", SimpleImputer(strategy="median")), ("scale", StandardScaler()),
])
mi_score = partial(mutual_info_classif, discrete_features=False, random_state=0)
candidate = Pipeline([
    ("preprocess", preprocess),
    ("select", SelectKBest(mi_score, k=3)),
    ("model", LogisticRegression(max_iter=1000)),
])
search = GridSearchCV(
    candidate, {"select__k": [2, 3, "all"]}, scoring="balanced_accuracy",
    cv=StratifiedKFold(n_splits=3, shuffle=True, random_state=0),
)
search.fit(X_train, y_train)
selected = X_train.columns[search.best_estimator_.named_steps["select"].get_support()]
print(selected.tolist())
print(search.score(X_test, y_test))

若还要搜索 k、编码方法和模型超参数,它们都属于内层选择。最终性能应用外层交叉验证或未参与选择的测试集估计。

mutual_info_classif 需要知道每个转换后特征是离散还是连续。auto 把稠密输入当连续、稀疏输入当离散,直接用于混合 one-hot 与数值列的矩阵可能出错。示例明确六列都连续并固定随机种子;混合表示需要对齐的类型掩码或合适的其他评分方法,k 也不能超过可用列数。参见互信息接口。

4. 检查稳定性与增量价值 ​

单次被选中不代表可靠。记录不同时间窗口、bootstrap 样本或交叉验证折中的:

  • 入选频率;
  • 系数方向和范围;
  • permutation importance 的分布;
  • 加入/移除后的验证指标变化;
  • 对延迟、成本和群体误差的影响。

高度相关的替代特征会互相抢占重要性。可以按语义分组评估,而不是把某一列的偶然胜出解释成唯一机制。

5. 一份可检查的特征定义 ​

生产特征需要的不只是 SQL 片段:

yaml
name: team_supply_count_7d
entity: team_id
value_type: int64
event_source: confirmed_supply_events
event_time: confirmed_at
available_time: ingested_at
window: "(cutoff - 7d, cutoff]"
default_when_complete: 0
oncomplete_or_lookup_failure: missing
event_key: event_id
revision_order: increasing_revision
retraction_status: cancelled
freshness_slo: 15m
owner: logistics-data
version: 3

定义还应绑定代码版本、依赖数据、权限、废弃计划和允许的使用场景。特征仓库可以帮助注册、物化和复用,但不会自动保证定义正确。

这份 YAML 是契约示意,不是所有特征仓库都能直接识别的标准配置。只有相关来源已确认完整,才允许把没有事件记为零;超时、缺分区和查询失败不能变成“零次补货”。下面的参考计算接着前面的 Python 代码运行:

python
def supply_count_7d(events, team_id, cutoff, source_complete):
    if not source_complete:
        return {"value": None, "reason": "incomplete_source"}
    visible = events.loc[events["ingested_at"] <= cutoff]
    if visible.duplicated(["event_id", "revision"]).any():
        raise ValueError("Resolve duplicate event revisions before feature computation")
    latest = visible.sort_values(["event_id", "revision"]).drop_duplicates("event_id", keep="last")
    matches = (
        latest["team_id"].eq(team_id) & latest["status"].eq("confirmed")
        & latest["confirmed_at"].gt(cutoff - pd.Timedelta(days=7))
        & latest["confirmed_at"].le(cutoff)
    )
    return {"value": int(matches.sum()), "reason": "ok"}

cutoff = pd.Timestamp("2026-08-08T12:00:00Z")
events = pd.DataFrame([
    ("a", 1, "T1", cutoff - pd.Timedelta(days=7), cutoff, "confirmed"),
    ("b", 1, "T1", cutoff, cutoff, "confirmed"),
    ("c", 1, "T1", cutoff - pd.Timedelta(days=1), cutoff + pd.Timedelta(hours=1), "confirmed"),
    ("d", 1, "T1", cutoff - pd.Timedelta(hours=1), cutoff - pd.Timedelta(hours=1), "confirmed"),
    ("d", 2, "T1", cutoff - pd.Timedelta(hours=1), cutoff, "cancelled"),
], columns=["event_id", "revision", "team_id", "confirmed_at", "ingested_at", "status"])
print(supply_count_7d(events, "T1", cutoff, source_complete=True))  # value=1
print(supply_count_7d(events, "T1", cutoff, source_complete=False))  # value=None

只有 b 计入:a 恰在不包含的左边界,c 尚不可用,d 已有可见撤销。来源契约要求非空 UTC 时刻、不可变事件 ID 和严格递增修订号,后续修订可以修改或撤销旧事实。这里假设 ingested_at 就是预测链路可用时间,生产来源必须另行验证该假设及完整性标记。

6. 离线与在线为何会偏斜 ​

常见 training-serving skew 包括:

  • 训练用批处理 SQL,线上用另一份应用代码;
  • 两边时区、窗口边界或舍入规则不同;
  • 离线数据已回填,线上当时看到的是迟到前状态;
  • 类别词表、填充值和缩放参数版本不同;
  • 在线查不到值时使用了未记录的默认值。

优先复用同一转换产物或同一声明式定义。无法共用执行引擎时,至少用固定样本做 parity test:给定实体与 cutoff,两条路径应得到相同结果或在明确容差内一致。

7. 上线前的验证 ​

历史回放 ​

按过去每个预测时点重建当时可见数据,不使用后来回填的真相快照。

阴影运行 ​

新特征在线计算但不参与决策,对比分布、延迟、缺失和可用率。

契约测试 ​

检查类型、范围、唯一性、窗口边界、默认值和未知类别行为。

可回滚发布 ​

特征版本与模型版本绑定;旧版本保留到模型停止使用,不能在原定义上静默改语义。

8. 监控三类变化 ​

数据质量 ​

缺失率、未知类别率、重复实体、延迟、刷新失败和范围违规。

分布与一致性 ​

线上/离线差异、分位数、类别占比、漂移统计和按群体分布。漂移是调查信号,不自动等于模型失效。

模型关联 ​

特征覆盖、预测质量、校准、群体误差以及标签延迟后的真实表现。某特征分布稳定,也可能失去与标签的关系。

告警要绑定负责人、处置步骤和 fallback,例如使用上一个成功快照、降级模型或停止自动决策。

9. 特征评审清单 ​

text
[ ] 预测时合法且可获得
[ ] 实体、时间与窗口边界明确
[ ] 转换和选择只在训练折拟合
[ ] 验证切分符合上线场景
[ ] 增量收益超过计算与治理成本
[ ] 离线/在线 parity test 通过
[ ] 默认值、未知值和迟到数据策略明确
[ ] 版本、负责人、监控和回滚路径齐全

常见误区 ​

  • 模型能自动忽略无用特征:无关或泄漏特征仍会增加方差、成本与风险。
  • 选择后再交叉验证就客观:选择本身已经用过标签。
  • 有了 feature store 就一致:工具不能修复错误窗口和双份实现。
  • 分布漂移就是立即重训:先判断数据故障、季节变化还是关系失效。

练习 ​

  1. 把一个标签相关筛选器移入 pipeline,比较修复前后的交叉验证分数。
  2. 对 10 次重采样记录特征入选频率,解释不稳定来源。
  3. 为一个滚动特征写 YAML 定义和五个边界测试。
  4. 设计训练/服务偏斜监控,并为三类告警规定降级动作。

小结 ​

特征选择是模型选择流程的一部分,不能提前偷看验证答案;生产特征则是一套带时间语义、版本和运维责任的接口。真正可用的特征,不只提升离线分数,还能在预测现场被相同地重建。

下一章转向抽样与因果推断:即使特征和模型都可靠,观察到的样本仍可能无法代表目标总体,相关关系也未必支持行动。

Built with VitePress | Software Systems Atlas