4. 特征回放概述

在特征平台与模型迭代的日常工作中,特征回填和特征回放经常被混用,甚至被当成同一件事。但从工程语义上看,两者解决的问题、触发时机、数据来源和一致性风险完全不同。本节先把“特征回放”讲清楚:它是什么、什么时候必须用、它与回填的边界在哪里。

一、核心定义:回放是“按历史时点重演特征计算”

特征回放(Feature Replay)是指:在给定一批历史时间点或历史样本的前提下,使用与当时相同或等价的特征计算逻辑、数据源版本和参数配置,重新生成这些时间点上的特征值,并用于验证、对比或修正线上/离线特征结果的过程。

它强调三个关键词:

  • 历史时点:不是“最新状态”,而是某个业务发生时刻,例如用户下单时间、曝光时间、申请时间。
  • 逻辑等价:回放使用的特征定义、算子、窗口、依赖表版本,应尽量还原当时的生产逻辑,而不是用今天的最新逻辑去覆盖。
  • 可对比:回放结果通常要与线上落库特征、离线训练样本或另一套计算链路做逐行比对,因此必须保留主键、时间戳和版本标识。

从数据流角度看,回放不是简单“再算一遍”,而是把样本时间 → 特征逻辑版本 → 数据源快照 → 特征值这条链路重新走通。它的本质是时间维度上的可复现性工程。

特征回放:按历史时点重演 历史样本 user_id + event_time 逻辑版本 feature_def_v3 数据源快照 T-1 分区 / 事件流 回放引擎 point-in-time join 回放特征结果 与线上/离线逐行比对 特征回填:补历史缺失值 缺失时间范围 2024-01-01 ~ 01-31 最新逻辑 feature_def_latest 批量计算 Spark / Flink 批任务 写入特征库 覆盖或补齐分区 回填后特征表 追求“补齐”,不强调逐时点复现

二、典型场景:什么时候必须做特征回放

回放不是日常必跑任务,它通常出现在以下四类场景中:

  1. 线上特征异常排查:线上模型效果突然下降,怀疑某特征在某个时间段取值错误。此时需要按当时的时间点回放,确认是数据源延迟、逻辑变更还是缓存污染。
  2. 特征逻辑变更验证:新版本特征定义上线前,用历史样本回放新旧两套逻辑,对比差异率和分布偏移,评估是否会影响模型。
  3. 训练样本一致性校验:离线训练样本中的特征值,与线上服务时实际落库的特征值不一致,导致训练/推理 skew。回放可以定位是 join 时点错误还是数据版本错误。
  4. 模型复现与审计:监管或内部审计要求解释某次决策,需要还原当时特征值。回放提供可追溯的计算证据链。
判断口诀:如果目标是“解释过去某个时点为什么是这个值”,用回放;如果目标是“把缺失的历史区间补上”,用回填。前者重“复现”,后者重“补齐”。

三、与特征回填的区别:一张表看清边界

回填(Backfill)通常指:当特征表在某个历史区间缺失、延迟或需要重算时,用批量任务把该区间的特征值计算出来并写入存储。它更关注数据完整性,而回放更关注计算可复现性。

对比维度 特征回放 特征回填
核心目标 按历史时点复现特征计算过程与结果 补齐历史缺失区间的特征值
时间语义 严格 point-in-time,强调事件发生时刻 按分区/区间批量处理,时间粒度较粗
逻辑版本 尽量使用当时的生产逻辑版本 通常使用当前最新逻辑
数据源 历史快照或可回溯的事件流 当前可访问的离线表/数据湖
输出用途 比对、验证、审计、排查 skew 写入特征库,供训练或线上读取
一致性风险 逻辑版本错配、数据快照缺失 用新逻辑覆盖旧区间,造成时间穿越
最常见的误区:把回填当成回放。用最新特征逻辑去回填三个月前的数据,然后拿这份数据去验证当时的线上效果。这等于用“今天的规则”解释“昨天的决策”,结论必然失真。

四、方法流程:一次标准回放怎么做

下面给出一个可落地的回放流程,适用于离线批式回放,也可迁移到流式回放。

# 1. 准备回放样本:主键 + 事件时间 + 标签(可选)
replay_samples = read_table("replay_samples/dt=2024-06-01")
# 字段:user_id, item_id, event_time, request_id

# 2. 锁定逻辑版本与数据源版本
feature_def_version = "feature_def_v3"
source_snapshot = "ods_user_behavior/dt=2024-05-31"
# 注意:快照日期应覆盖 event_time 所需的最大回溯窗口

# 3. 执行 point-in-time 回放计算
replay_df = replay_engine.compute(
    samples=replay_samples,
    feature_def=feature_def_version,
    sources=[source_snapshot],
    join_mode="as_of",          # 按 event_time 做 as-of join
    window_policy="event_time"  # 窗口以事件时间为基准
)

# 4. 与线上落库特征或离线样本比对
diff = compare(
    left=replay_df,
    right=online_feature_table,
    keys=["request_id"],
    metrics=["value_diff_rate", "null_rate", "psi"]
)

# 5. 输出回放报告
write_report(diff, path="replay_report/2024-06-01/")

关键控制点有三个:样本时间必须精确到事件时刻;数据源快照必须覆盖最大回溯窗口;join 必须使用 as-of 语义而不是普通等值 join。任何一点做错,回放结果都会出现系统性偏差。

五、复盘:回放失败的三个典型信号

  • 差异率随时间递增:越靠近当前时间差异越小,越早差异越大。通常是数据源快照不完整或窗口回溯不足。
  • 空值率异常升高:回放结果大量为 null,说明 as-of join 没有匹配到历史分区,或事件时间格式/时区不一致。
  • 分布整体平移:PSI 很高但逐行差异不大,往往是逻辑版本用错,例如把新版本的默认值策略带入了旧回放。

回放的价值不在于“再算一遍”,而在于让历史可解释、让差异可定位、让一致性可验证。理解它与回填的边界,是后续做特征一致性治理的前提。