Skip to content

第 32 章 · 下一本:从离线到在线 ​

这一章要解决什么:这本书全程假设"数据是静态的一张表"。 真实生产环境是流式的。这一章说明那个差距在哪、为什么它值得单独一本书。

最容易踩的坑:以为"离线 R² = 0.999"就可以直接上线。 离线和在线之间隔着一个世界:训练-服务偏差。

本章你需要完成的代码:demos/ch32-next.py

32.1 回顾:这本书教了什么 ​

真实输出:

text
=== ① 全书的能力清单 ===

  能力                              靠哪几章              拿走什么
  ---------------------------------------------------------------------------
  把"过去"变成特征                   05~10                会安全地造时序特征
  判断一个特征值不值得做              11~16                消融 + 特征账本
  守住不泄漏                        17~22                三种泄漏 + 检查清单
  知道该停手                        23~27                调参上限 = 特征的余量
  交付一份结论                      28~31                报告 + 流程卡点

  ★ 五个能力的关系:
      造特征 → 判断价值 → 守纪律 → 知边界 → 能交付

★ 三条线索,回到最开始的三个反转

text
反转一:加 10 列特征 → 分数没动(+0.00006)
    → 学到:加特征 ≠ 加信息(第 04、15 章)

反转二:加 4 列对的特征 → 分数跳 0.04(+0.03998)
    → 学到:对的特征能改变问题的信息结构(第 04、10 章)

反转三:加 1 列泄漏特征 → R² 恰好 1.000000
    → 学到:分数不是判据(第 20、21 章)

★ 如果只能记住一句话

特征工程不是"加列",而是"判断哪一列值得加、 并且随时准备好发现自己加了假的那一列"。

32.2 ★ 离线和在线之间,隔着什么 ​

真实输出:

text
=== ② 三个"离线看不出来"的问题 ===

  ① 训练-服务偏差(training-serving skew)
       离线:用 pandas 算特征,一行一行的
       在线:用实时流算特征,可能用 SQL / Flink / 服务接口
           ↓
       两边的实现【不是同一份代码】
           ↓
       于是同一个特征,离线和在线算出来的值【不一样】

  ② 特征漂移(feature drift)
       离线:数据是快照,分布固定
       在线:传感器老化、工况变化、上游改口径
           ↓
       模型没见过的新分布会进来

  ③ "预测时点"在流式系统里怎么定义
       离线:一行 = 一个窗口,很好定义
       在线:数据是连续来的,"现在"是哪一刻?
           ↓
       窗口还没攒够时怎么办?迟到数据怎么办?

  ★ 这三个问题,本书全程回避了 —— 因为离线数据里它们不存在。

★ ① 最危险,因为它"看起来对"

离线评估:R² = 0.999
    ↓
上线后:R² 掉到 0.80
    ↓
【模型没变、数据没变、代码没变 —— 但特征值不一样了】

为什么会不一样?

环节离线(pandas)在线(流式计算)
窗口边界groupby.rolling(20)每次事件到达时维护状态
缺失值dropna()没有"丢掉这一行"这个选项
时间基准数据里的 cycle 列事件时间 vs 处理时间
回溯窗口整表都在内存里只能回查有限的时间范围

★ 一个具体的例子

python
# 离线:用整表算"过去 20 个窗口的均值"
df["res_win_mean"] = (df.groupby("cell_id")["resistance_mohm"]
                        .transform(lambda s: s.shift(1).rolling(20).mean()))

# 在线:必须维护一个滚动状态(伪代码)
class RollingMean:
    def __init__(self, w=20):
        self.buf = deque(maxlen=w)
    def update(self, x):
        self.buf.append(x)
    def value(self):
        return sum(self.buf) / len(self.buf) if self.buf else None
        # ↑ 注意:这里的"过去 20 个"是【到达顺序】的 20 个,
        #   而离线是【按 cycle 排序】的 20 个 —— 顺序不一致时结果就不同

★ 所以"训练-服务偏差"的根源是:

同一个特征有两份实现(离线一份、在线一份), 而没有任何机制保证它们算出同一个值。

32.3 一个可以现在就做的检查 ​

真实输出:

text
=== ③ 离线内部的自洽性检查(在一个文件里就能做)===

  检查:用两种方式算同一个特征,看是否一致
        方式 A:vectorized(整表 / pandas)
        方式 B:loop(逐行 / 模拟在线)

  特征:resistance_mohm(过去 20 个窗口的均值)
    方式 A(pandas)与方式 B(逐行循环)的最大差 = 1.42e-14
    两者都有值的行数 = 20,039
    → 一致 ✅(1.42e-14 是浮点误差,不是逻辑差异)

  ★ 但这个检查只证明了"离线的两种实现一致"。
    在线实现是【第三份代码】,它仍然可能与这两份都不同。

★ 这个检查的价值与边界

价值:它能在离线阶段就发现"两种实现不一致"(这是最常见的一类偏差)
边界:它证明不了"在线实现也对" —— 那需要别的手段

★ 在线实现怎么验证?两条路

手段做法代价
影子模式(shadow)线上算一份、离线算一份,把两份值都记下来比对需要额外存储与比对流程
特征回放(replay)把线上收到的原始事件存下来,事后用离线代码重算,比对需要存原始事件

★ 两条路的共同点:

都必须"同时拿到两份特征值"并逐条比对。 没有比对,偏差就永远存在且不可见。

★ 一个直观的不一致实验

真实输出:

text
=== ④ 一个真实的不一致来源:按到达顺序 vs 按事件时间 ===

  同一批记录,只把两条的【到达顺序】对调:
    按事件时间算 vs 按到达顺序算,最大差 = 0.4050
    受影响的窗口数 = 1

只对调两条记录的到达顺序,特征值就差了 0.4050。

这个差距有多大?

text
resistance_mohm 的全表跨度 ≈ 53 mΩ
    ↓
0.4050 ≈ 全表跨度的 0.76%
    ↓
而对"相邻两行只差 2%"的特征来说,
   这 0.76% 已经是一个可观的扰动

★ 所以在线实现必须显式决定一件事:

按 event time 排序(准确,但要等迟到数据 → 引入延迟)
    vs
按到达顺序(无延迟,但与离线不一致)
    ↓
【没有免费的选择 —— 必须选一个,并把它写进模型卡】

如果不做这个决定,会发生什么?

离线用 event time 训练 → 模型假设"窗口是按时间顺序的"
    ↓
在线用到达顺序 → 窗口偶尔是乱序的
    ↓
【模型收到的特征分布与训练时不同 → 精度下降,而且很难排查】

32.4 这套纪律里,哪些能直接搬过去 ​

真实输出:

text
=== ④ 离线纪律 → 在线的可迁移性 ===

  离线纪律                     在线对应物                        可迁移性
  ---------------------------------------------------------------------------
  按实体划分(避免泄漏)         按实体/时间划分 + 防"回填未来"       ★★★★★
  "预测这一刻拿得到吗"          特征的"可用时刻"(event time)       ★★★★★
  shift(1) 只用历史            禁止 look-ahead join                ★★★★★
  特征账本                      特征注册表(Feature Registry)        ★★★★☆
  数据指纹                      特征版本 + 数据版本                   ★★★★☆
  消融实验                      在线 A/B 测试                       ★★★☆☆
  精度报告                      模型卡(Model Card)                ★★★★☆

  ★ 前三条【原样可搬】—— 因为它们是"时间纪律",与系统形态无关。

★ 最重要的三条,为什么原样可搬

① 按实体划分
   离线:同一支设备不能跨训练/测试
   在线:同一位用户的历史不能泄漏到他的未来预测里
      → 同一个道理,只是"回填未来"比"跨集"更隐蔽

② "预测这一刻拿得到吗"
   离线:这一列在预测时点存在吗
   在线:这一列的 event time 早于预测时点吗
      → 换了个说法,判据完全一样

③ 禁止 look-ahead
   离线:shift(1)
   在线:join 时只能用 t' <= t 的记录
      → 同一个动作

★ 而"精度报告"的在线版本叫"模型卡":

markdown
# 模型卡(Model Card)

## 基本信息
- 模型版本 / 训练数据版本 / 上线日期

## 性能
- 离线评估:R² = 0.99904(按设备划分,2026-10-10)
- 在线监控:过去 7 天 MAE = 0.0034(与离线一致 ✅)

## 特征(来自特征账本)
| 特征 | 可得性 | 在线延迟 | 漂移监控 |
|---|---|---|---|
| soh_lag1 | ⚠️ 依赖历史标签表 | < 5 min | 有 |
| res_win_mean | ✅ 实时计算 | < 1 s | 有 |

## 适用范围与局限
(与第 30 章报告第七节相同)

## 已知问题
- 历史实测标签表有 15 分钟延迟 → 该特征在"准实时"场景不可用

32.5 常见坑 ​

text
① 以为离线评估好就能上线 → 训练-服务偏差会让它掉一大截
② 离线在线各写一份特征代码 → 没有任何机制保证一致
③ 只做离线检查不做在线比对 → 偏差永远不可见
④ 忘了"event time" → 用了处理时刻之后的记录(在线版的特征泄漏)
⑤ 窗口没攒够时给默认值 → 冷启动问题(第 07 章那 1,260 行 NaN 的在线版)
⑥ 没有漂移监控 → 分布变了三个月才发现
⑦ 以为"在线学习"能自动解决一切 → 它只会让问题更难排查

★ ⑤ 特别值得注意 —— 它是第 07 章的在线版

离线:每支设备最早 20 个窗口没有历史 → 1,260 行 NaN → dropna
    ↓
在线:传感器刚上线时没有历史
    ↓
离线可以 dropna(丢掉这些行)
在线【不能丢掉这个请求】—— 必须给一个答案
    ↓
【同一件事,离线是"过滤",在线是"兜底"】

这就是为什么"离线做对"不等于"在线做对" —— 很多在离线里被 dropna 悄悄丢掉的行,在线上必须被处理。

32.6 完整示例与输出 ​

bash
cd code
python demos/ch32-next.py

真实输出(节选):

text
=== ② 三个"离线看不出来"的问题 ===
  ① 训练-服务偏差:同一特征有两份实现,没有任何机制保证一致
  ② 特征漂移:传感器老化、工况变化、上游改口径
  ③ "预测时点"在流式系统里定义困难

=== ③ 离线内部的自洽性检查 ===
  方式 A(pandas)与方式 B(逐行循环)的最大差 = 0.00e+00  → 一致 ✅

本章练习 ​

练习 1:写出你项目里"离线 vs 在线"的特征实现差异

选你项目里的 3 个特征,分别写出:

  1. 离线怎么算(pandas / SQL);
  2. 在线怎么算(伪代码);
  3. 两者可能在什么情况下不一致。
参考答案

示例(设备健康度预测):

特征离线在线可能不一致的地方
res_win_mean(过去 20 窗口内阻均值)groupby.rolling(20).mean(),按 cycle 排序维护 20 长队列,按到达顺序排序键不同:迟到数据会打乱到达顺序
soh_lag1(上一窗口实测 SOH)groupby.shift(1)查历史标签表的最新一条标签表有 15 分钟延迟 → 在线可能查到旧值或查不到
temp_win_range20(温升极差)rolling(20).max() - rolling(20).min()同样维护队列窗口边界:离线含"当前",在线若用"到达时"则含/不含不一致

★ 一个具体的不一致场景

离线:
   df.sort_values(["cell_id","cycle"]) 后 rolling(20)
   → "过去 20 个" = cycle 最小的 20 个

在线:
   事件按【到达顺序】进队列
   → 如果第 100 圈的记录比第 98 圈【晚到】
   → 队列顺序变成 …98, 100, 96…
   → "过去 20 个"的含义变了
       ↓
   【同一个特征,值不同】

★ 怎么防

python
# 在线实现要按 event time 排序,而不是到达顺序
class WindowFeature:
    def __init__(self, w=20):
        self.buf = []                      # [(event_time, value)]
        self.w = w

    def update(self, event_time, value):
        self.buf.append((event_time, value))
        self.buf.sort(key=lambda x: x[0])  # ★ 按事件时间排,不是到达顺序
        self.buf = self.buf[-self.w:]

    def value(self):
        vals = [v for _, v in self.buf]
        return sum(vals) / len(vals) if vals else None

★ 这里有个重要的取舍:

按 event time 排序 → 更准,但需要"等迟到的数据"(引入延迟)
按到达顺序       → 无延迟,但结果与离线不一致
    ↓
【没有一个免费的选择 —— 必须显式决定,并把这个决定写进模型卡】
练习 2:设计一个"影子比对"方案

为了让训练-服务偏差可见,你需要同时拿到"离线算的值"和"在线算的值"。

请设计一个方案,包含:记录什么、怎么比对、发现不一致后怎么办。

参考答案

方案:影子模式(shadow mode)

python
# 在线服务里:每次预测时,同时记录"在线算的特征"和"原始输入"
def predict_with_shadow(raw_event, online_features, model):
    # ① 正常预测
    pred = model.predict(online_features)

    # ② 影子记录:把原始事件 + 在线特征值都落盘
    shadow_log.write({
        "request_id": raw_event["request_id"],
        "event_time": raw_event["event_time"],
        "online_features": online_features,        # 在线算的
        "raw_inputs": raw_event["raw"],            # 原始输入(供离线重算)
        "prediction": float(pred),
    })
    return pred
python
# 离线(每天跑一次):用同一份原始输入重算特征,与在线值比对
def compare_shadow(shadow_log_path, offline_pipeline, tolerance=1e-3):
    log = pd.read_parquet(shadow_log_path)

    # 用原始输入重算离线特征
    offline_feats = offline_pipeline.transform(log["raw_inputs"])

    report = []
    for col in offline_feats.columns:
        diff = (offline_feats[col] - log["online_features"].apply(lambda d: d[col])).abs()
        report.append({
            "feature": col,
            "max_diff": diff.max(),
            "p99_diff": diff.quantile(0.99),
            "mismatch_rate": (diff > tolerance).mean(),
        })
    return pd.DataFrame(report).sort_values("mismatch_rate", ascending=False)

期望输出形态:

text
           feature  max_diff  p99_diff  mismatch_rate
      res_win_mean    12.340     0.0021          0.187   ← ⚠️ 有问题
        soh_lag1      0.0001    0.0000          0.000   ← ✅
   temp_win_range20    0.450     0.0014          0.021   ← 轻微

★ 发现不一致后怎么办(分三种)

mismatch_rate判断动作
≈ 0一致正常,继续监控
1% ~ 10%边界情形(迟到数据、冷启动)逐条查,多半是窗口边界或延迟问题
> 10%实现逻辑不同紧急:先下线那个特征,再查代码差异

★ 这个方案的关键设计点

① 记录【原始输入】而不只是特征值
   → 否则无法用离线代码重算(这是最重要的一点)

② 记录 event_time
   → 才能区分"迟到数据"与"逻辑错误"

③ 用 mismatch_rate 而不是 max_diff 排序
   → max_diff 会被一两个极端值主导

④ 逐特征报告
   → 才能定位到具体是哪个特征的问题

★★ 最重要的一句话

影子比对的成本是"存一份日志 + 每天跑一次脚本", 而它换来的是"训练-服务偏差变得可见"。 没有它,你只会在业务指标掉下来时才知道出了问题。

练习 3:这本书没讲什么

列出至少三件这本书不会讲的事,并说明该去哪里学。

参考答案
没讲的为什么不讲去哪学
在线特征平台(Feature Store)它是工程架构话题,不是"特征工程方法"特征平台/MLOps 专门资料
流式计算(Flink / Kafka Streams)需要另一套工具链与运维知识流处理专门书籍
训练-服务偏差的完整工程方案需要真实服务环境,本书是离线教程需要结合具体生产系统
自动特征工程(Featuretools / 自动交叉)本书讲"手工判断",自动方法的前提是"会判断"各工具的官方文档
深度学习自动特征提取那是表示学习,不是表格特征工程深度学习专门教材
因果推断预测不问因果因果推断专门教材
序列模型的构造(RNN/Transformer)本书的模型都是表格模型序列建模资料

★ 其中第一、二、三条合起来就是"下一本书"的轮廓

《特征工程实战》假设:
   数据在一张静态表里,特征是 pandas 算出来的
      ↓
下一本的落差:
   数据是流式的,特征要实时算,离线和在线必须一致
      ↓
【这个落差值得单独一本书 —— 因为它引入的是全新的一类问题】

★ 但有一个重要的提醒

不要因为"在线很时髦"就跳过离线纪律
    ↓
在线系统里 90% 的严重问题,根因都是离线纪律没做好:
   · 划分泄漏 → 评估虚高 → 上线才发现
   · 特征泄漏 → 用了未来信息 → 在线根本取不到
   · 没有账本 → 不知道哪个特征依赖什么
      ↓
【把这本书的纪律做扎实,是上线前的必要条件】

面向工程实践:把这本书变成你项目里的东西 ​

① 你现在就可以拿走的四样东西

交付物在哪怎么用
防泄漏检查清单第 22 章接进 CI(第 31 章给了配置)
特征账本第 16 章每列一行,随代码提交
精度报告模板第 30 章一节一条命令生成
判据:"预测这一刻拿得到吗"第 10、17 章加任何特征前问一遍

★ 建议的落地顺序

第 1 步:把"判据"贴在工位上(0 成本,立刻生效)
第 2 步:建一个特征账本(半天)
第 3 步:把 C1~C5 写成 pytest(1 天)
第 4 步:接进 CI(2 小时)
第 5 步:加精度报告(1 天)
    ↓
两周内,你的特征工程从"靠经验"变成"有纪律"

② 一个自检:你现在能回答这五个问题吗?

markdown
- [ ] 我的预测时点是什么?(as-of date)
- [ ] 我按什么划分?训练/测试的实体交集是 0 吗?
- [ ] 每一列特征在预测时点都拿得到吗?
- [ ] 哪一组特征真的值钱?(消融数字是多少)
- [ ] 我凭什么相信这个分数?(做过哪些结构检查)

★ 五个都能回答,说明这本书你真的读进去了。

★ ③ 最后:这本书真正想教的东西

它教的不是"20 个特征公式"
    ↓
而是三个判断:
   ① 什么时候该做特征(而不是换模型/调参)
   ② 一个特征值不值得做(消融 + 可得性)
   ③ 我怎么知道自己骗了自己(结构检查)
      ↓
【三个判断,在任何数据、任何行业都成立】

★ 回到最开始那句话

特征决定上限,模型决定你离上限多远。 而"知道自己在哪个位置",才是这一切的前提。

祝你学得顺利 —— 也祝你在下一次看到 R² = 1.000000 时,第一反应是"查一下"。


全书回顾 ​

恭喜你走到这里。回头看看这 32 章。

点开:全书能力清单(自测一下)

已勾选 0 / 0 项

如果打勾超过 80%,你已经具备"独立做特征工程并守住纪律"的能力。

剩下的部分需要在真实项目里反复遇到、反复踩坑才能内化 —— 那本书帮不了你,只有实践能。

点开:下一步该学什么

这本书的终点,是下一本书的起点。

① 在线特征与 MLOps(本书明确的交棒方向)

为什么是下一步:本书全程假设静态表,而生产是流式的
要学什么:
  · 特征平台(Feature Store)与特征注册表
  · 流式计算(Flink / Kafka Streams)
  · 训练-服务偏差的检测与治理(影子模式、特征回放)
  · 模型卡与漂移监控

② 时序专门方法

本书把"同一实体的多行"当分组结构处理(按实体划分 + 滚动窗口)。
严格的时间序列还有:趋势、季节性、平稳性、状态空间模型。

③ 因果推断

这本书反复强调"相关 ≠ 因果"。
如果你要回答"改变 X 能不能影响 Y",
需要实验设计 / DID / 工具变量,不是预测模型。

④ 自动特征工程

Featuretools、自动化交叉、AutoML 的特征搜索
    ↓
但它们的前提是"你判断得出哪些特征好" —— 那正是这本书教的

★ 一个诚实的建议

不要急着学在线/流式。
    ↓
先把这本书的纪律用到你手上的离线项目上:
   按实体划分、写特征账本、做消融、接 CI
    ↓
你会发现:光是这四条,就能让你的结论质量提升一个档次
    ↓
然后再决定往哪个方向深入

★ 最后一句话

这本书教的不是"怎么把分数做高",而是"怎么知道自己有没有骗自己"。

后者才是能带走的能力 —— 因为分数会变,数据会变,模型会过时, 只有判断力会一直跟着你。