Skip to content

第 26 章 · 模型选型:没有银弹 ​

这一章要解决什么:前面所有章节都在比较"模型好不好"。 这一章要回答一个更根本的问题:"对我的场景来说,什么叫做'好'?"

最容易踩的坑:把选型当成"挑 R² 最高的"。 更隐蔽的坑是:用场景标签代替量化 —— 听说"实时场景要选线性模型"就选线性模型,却从没测过延迟到底是多少。

本章你需要完成的代码:demos/ch26-model-selection.py

26.1 四个维度的量化 ​

选型不是"排名",是"在约束下求最优"。 所以第一步是把约束量化。

真实输出:

text
  模型                     R²       训练       单行预测      可解释    ±σ
  线性回归              0.95053    0.01s    0.0004ms        高     无
  随机森林(200)         0.96503    4.70s    0.0220ms        中     无
  HistGB(300)       0.96429   35.75s    0.0148ms       中低     无
  高斯过程(800行)        0.82693    0.47s    0.0204ms        低     有
  (GPR 只喂 800 行、只用 cycle 一个特征,R² 不参与横向比较)
  (它的 ±2σ 覆盖率 = 91.8%,区间可信)

★ 这张表就是选型的原材料。 逐列读:

维度数据解读
精度0.95053 ~ 0.96503(三个主流模型)极差只有 0.0145
训练速度0.01s ~ 35.75s差 3500 倍
单行预测全部 < 0.03 ms都不构成约束
可解释性高 / 中 / 中低 / 低线性最好
不确定性只有 GPR 有且覆盖率 91.8% 可信
点开:这张表里有两个反直觉的地方

反直觉一:训练耗时的差异,比精度差异大 3500 倍

精度差异:0.0145(相对差异 1.5%)
训练耗时:0.01s vs 35.75s(相对差异 357500%)

但大多数人的注意力 95% 花在精度上。

为什么这是错的:

精度差异 0.0145 的实际含义:
  MAE 从 0.00968 降到 0.00790
  → 平均误差从 0.97 个百分点降到 0.79 个百分点
  → 差异 0.18 个百分点

训练耗时差异的实际含义:
  离线:无所谓(一周跑一次,35 秒不算什么)
  频繁重训(比如每天):35 秒也无所谓
  超参搜索(要跑几百次):0.01s × 500 = 5 秒  vs  35.75s × 500 = 5 小时
        ↑ 这里差异就变成"能做完"和"做不完"的区别了

★ 所以"耗时重要不重要"取决于你的工作流,不是一个绝对值。

反直觉二:单行预测耗时全部 < 0.03 ms —— 速度根本不构成约束

很多人会说:"实时场景要用线性模型,因为树模型慢。"
   ↓
实测:线性 0.0004ms,森林 0.0220ms
   ↓
100 ms 的延迟预算下,两者都完全没问题
   ↓
【所以"实时"这个标签,在这里不构成"必须选线性"的理由】

★ 这是一个很重要的思维修正:

约束只有在"真的构成约束"时才能用来做取舍。先量化,再取舍 —— 不要凭场景标签做决定。

那什么时候速度会成为约束?

场景延迟预算会不会成为约束
离线批处理分钟~小时❌ 不会
Web API100~500 ms⚠️ 看模型大小和批量
高频交易微秒级✅ 会
边缘设备(单片机)内存/算力都受限✅ 会(模型大小是关键)
本数据的所有模型—❌ 都不会

结论:在这份数据上,"速度"不是决定性维度。而"不确定性"和"精度"才是。

⚠️ 但关于"训练耗时"有一个必须说明的细节

同一个 HistGB(300 棵)在本章不同位置跑出了:
    15.22 s   (第 23 章四路横评)
    35.75 s   (本章第 26.1 节的表)
    28.44 s   (本章练习 1)
        ↑
   相差 2 倍以上

为什么:树模型的训练耗时受并行调度、当前机器负载、内存带宽影响很大, 不是一个稳定指标(n_jobs=-1 时会和系统里其他进程抢 CPU)。

★ 所以对"耗时"这一维度,正确的用法是:

✅ 看数量级:线性是 0.0x 秒级,森林是秒级,提升是十秒级 —— 这个关系是稳定的
❌ 拿具体数值做精确比较:"森林比提升快 7.6 倍" ← 这个倍数每次跑都不一样

★ 汇报时的写法:

"线性回归在毫秒级,树模型在秒到十秒级"        ← ✅ 稳定的表述
"随机森林 4.70s,比 HistGB 的 35.75s 快 7.6 倍"  ← ❌ 换个环境就不成立

这条纪律对所有"性能数字"都适用(延迟、吞吐、内存): 报数量级和量测条件,不要报看起来精确的倍数。

26.2 精度维度:换模型的天花板很低 ​

真实输出:

text
=== ② 精度维度的结论:换模型的天花板很低 ===
  三个主流模型 R² 极差 = 0.01449
  → 换模型最多只能再挤出 0.0145;而第 13 章显示【加特征】能涨 0.12

★ 把两个数字放在一起,方向就清楚了

单特征 → 七特征    :  +0.12183   (第 13 章)
线性   → 随机森林  :  +0.01449   (本章)

加特征的收益是换模型的 8.4 倍

★ 这不是"随机森林不好",而是"问题的瓶颈不在模型"

数据里的可预测部分(R² 上限)≈ 0.965
   ↓
线性回归已经拿到 0.95053(占 98.5%)
   ↓
剩下的 1.5% 需要更复杂的模型去挤
   ↓
而那 3.5% 的误差(1 - 0.965)是测量噪声,任何模型都消不掉

★ 所以选型的结论不是"哪个最好",而是:

在这份数据上,选模型对精度的边际收益已经很小。 选型的意义在于"找到满足约束的最便宜方案",而不是"榨出最后 0.015"。

26.3 不确定性维度:只有 GPR 给区间 ​

真实输出:

text
=== ④ 不确定性维度:只有 GPR 给区间 ===
  用一个具体决策看差别(退役线 0.80):
    GPR:0.815 ± 0.055 → 区间 [0.705, 0.925] 跨过 0.80 → 需要人工复检
    森林:0.815(单个数字)        → 无法判断,只能凭感觉

★ 同一个预测值,两种读法给出相反的结论

随机森林说:"0.815"
   ↓
0.815 > 0.80 → 高于退役线 → "可以继续用"
   ↓
但模型的平均误差是 0.008 —— 0.815 离 0.80 只差 0.015
   ↓
【这个数字的精度不足以支撑"换不换"的决定,而你不知道这一点】

GPR 说:"0.815 ± 0.055"
   ↓
区间 [0.705, 0.925] 跨过了 0.80
   ↓
正确的动作不是"按 0.815 决定",而是【安排人工复检】

★ 这就是"精度不是唯一指标"的具体含义

森林GPR
R²0.965030.82693
能不能说"我不确定"❌✅
在"决定是否退役"这个动作上给出一个不可靠的确定值主动指出需要复检

★ 一个关键的工程认知

点预测的真实问题是:它【隐藏了不确定性】。
   ↓
"0.815" 和 "0.815 ± 0.055" 看起来都是"0.815"
   ↓
但后者告诉你"这个数可能偏 0.11",前者让你以为"就是 0.815"
   ↓
在【决策代价不对称】的场景里,这个差别是决定性的

注意:GPR 的 R² 更低(0.82693 vs 0.96503),但它在这个场景更合适。

★ 这里有一个前提必须检查(第 24 章的教训):

python
# GPR 的区间是可信的,因为覆盖率合格
print(f"±2σ 覆盖率 = {cover:.1%}")   # 91.8%,接近理论 95%

如果覆盖率只有 29%(第 24 章那个 bug),那 GPR 的区间就是错的, 它在"退役决策"场景里反而不如森林 —— 因为一个错误的"我很确定"会误导决策。

26.4 三种场景,三个答案 ​

真实输出:

text
=== ⑤ 结论:没有银弹,只有约束下的最优 ===
  离线周报(一周一次)             → 随机森林 / 提升        理由:精度优先,慢一点无所谓
  实时看板(100ms 内)          → 线性回归             理由:延迟预算内,精度只差 0.014
  退役决策(要风险区间)          → 高斯过程             理由:宁可不那么准,也要给 ±σ

★ 同一个数据、同一个问题,三个场景三个答案。

这就是"没有银弹"的意思:

选型取决于【约束】,不取决于【排行榜】。

点开:逐个场景的推理链(这才是选型的正确姿势)

场景 1:离线周报(一周跑一次,给质量部门看分布)

第 1 步:列出约束
  · 延迟:无要求(一周一次)
  · 成本:可以接受几分钟
  · 输出:一批设备的健康度分布

第 2 步:确定主导维度
  → 精度优先(其他维度都不构成约束)

第 3 步:选择
  → 随机森林(0.96503)或提升(0.96429)
  → 两者精度几乎相同,随机森林训练更快(4.70s vs 35.75s)→ 选随机森林

第 4 步:说明不选其他的理由
  → 线性(0.95053):精度低 0.0145,而这里精度是唯一约束
  → GPR(0.82693):精度明显低,且这里不需要区间

场景 2:实时看板(每来一组数据要 100 ms 内出结果)

第 1 步:列出约束
  · 延迟:100 ms
  · 输出:单条记录一个数

第 2 步:量化约束是否真的构成约束
  → 实测单行预测:线性 0.0004ms、森林 0.0220ms
  → 【都远低于 100 ms】

第 3 步:约束不成立 → 退回到精度优先
  → 那还是选随机森林?

★ 但这里有一个现实考量:
  实时系统通常要求模型"小、快、可解释、易维护"
  线性回归的系数能写成一个公式,部署和排查都简单得多
  而精度只差 0.0145(MAE 差 0.0018)
      ↓
  如果业务上 0.0018 的 MAE 差异可以接受 → 选线性更划算

★ 场景 2 是最有意思的:它的正确答案不是由技术指标唯一决定的, 而是"技术指标的差异能否被业务接受"这个判断。

场景 3:退役决策(要决定设备换不换)

第 1 步:列出约束
  · 决策代价不对称:
      误判"该换"(其实不用换)→ 浪费设备
      误判"不用换"(其实该换)→ 设备故障风险
  · 需要:知道"这个预测有多可信"

第 2 步:确定主导维度
  → 不确定性优先

第 3 步:选择
  → GPR(能给 ±σ,且覆盖率 91.8% 合格)

第 4 步:说明代价
  → R² 从 0.965 降到 0.827 —— 精度放弃了 0.14
  → 换来的是"哪些预测需要人工复检"这个能力

★ 三个场景的推理链有一个共同结构

① 列出约束(延迟/成本/输出形式/决策代价)
② 量化每个约束是否真的构成约束
③ 找出主导维度
④ 选择 + 说明代价
⑤ 说明不选其他的理由

★ 第 ② 步是最容易被跳过的,也是最重要的。

它防止你"为了满足一个不存在的约束"而牺牲精度。

26.5 常见坑 ​

text
① 只比精度 → 忽略了"能不能给不确定性"这类根本差异
② 用场景标签代替量化("实时就要用线性模型")→ 从没测过延迟
③ 横评时四路条件不同(划分/特征/预处理)→ 数字不可比
④ 把训练耗时当成"速度" → 部署关心的是单行预测耗时和模型大小
⑤ 只报一个维度 → 别人无法判断你的取舍是否合理
⑥ 忘了说明"不选其他模型的理由" → 结论像是拍的
⑦ 引用了不可信的区间 → GPR 的 σ 必须先验覆盖率(第 24 章)

26.6 完整示例与输出 ​

bash
cd code
python demos/ch26-model-selection.py

真实输出:

text
=== ① 四个维度的量化:精度 / 速度 / 可解释性 / 不确定性 ===
  模型                     R²       训练       单行预测      可解释    ±σ
  线性回归              0.95053    0.01s    0.0004ms        高     无
  随机森林(200)         0.96503    4.70s    0.0220ms        中     无
  HistGB(300)       0.96429   35.75s    0.0148ms       中低     无
  高斯过程(800行)        0.82693    0.47s    0.0204ms        低     有

=== ② 精度维度的结论:换模型的天花板很低 ===
  三个主流模型 R² 极差 = 0.01449

=== ⑤ 结论:没有银弹,只有约束下的最优 ===
  离线周报(一周一次)             → 随机森林 / 提升
  实时看板(100ms 内)          → 线性回归
  退役决策(要风险区间)          → 高斯过程

本章练习 ​

练习 1:给四个模型做一次完整的四维量化

用四个模型(线性、随机森林 100 棵、HistGB、GPR)在同一份按设备划分的数据上, 测出四个维度的数字,填进一张表。

要求:

  1. 精度用 R²;
  2. 速度分"训练耗时"和"单行预测耗时"两列;
  3. 可解释性用"高/中/低"标注并说明依据;
  4. 不确定性用"有/无"标注;对有的那个,报出 ±2σ 覆盖率。
参考答案
python
import time

import numpy as np
import pandas as pd
from sklearn.ensemble import HistGradientBoostingRegressor, RandomForestRegressor
from sklearn.gaussian_process import GaussianProcessRegressor
from sklearn.gaussian_process.kernels import RBF, WhiteKernel
from sklearn.linear_model import LinearRegression
from sklearn.metrics import r2_score
from sklearn.model_selection import GroupShuffleSplit
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler

df = pd.read_csv("data/battery/soh_features.csv")
FEATS = ["cycle", "mean_charge_voltage_v", "mean_temp_rise_c", "resistance_mohm",
         "coulombic_eff", "cap_fade_rate_ah_per_100", "temp_std_c"]
X, y, groups = df[FEATS], df["soh"], df["cell_id"]
tr, te = next(GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42)
              .split(X, y, groups=groups))
Xtr, Xte, ytr, yte = X.iloc[tr], X.iloc[te], y.iloc[tr], y.iloc[te]

rows = []


def bench(name, model, Xf, yf, Xt, interp):
    t0 = time.time(); model.fit(Xf, yf); fs = time.time() - t0
    t0 = time.time(); pred = model.predict(Xt); pr = (time.time() - t0) / len(Xt) * 1000
    rows.append((name, r2_score(yte, pred), fs, pr, interp, "无"))


bench("线性回归", Pipeline([("s", StandardScaler()), ("m", LinearRegression())]),
      Xtr, ytr, Xte, "高")
bench("随机森林(100)", RandomForestRegressor(n_estimators=100, random_state=42, n_jobs=-1),
      Xtr, ytr, Xte, "中")
bench("HistGB", HistGradientBoostingRegressor(max_iter=300, learning_rate=0.05, max_depth=4,
                                              random_state=42, early_stopping=False),
      Xtr, ytr, Xte, "低")

# GPR:单特征 + 抽子集
rng = np.random.default_rng(42)
gi = rng.choice(tr, size=800, replace=False)
k = (RBF(30.0, length_scale_bounds="fixed")
     + WhiteKernel(1e-3, noise_level_bounds=(1e-8, 1e1)))
gpr = GaussianProcessRegressor(kernel=k, normalize_y=True, random_state=42)
t0 = time.time(); gpr.fit(df[["cycle"]].iloc[gi], y.iloc[gi]); fs = time.time() - t0
t0 = time.time(); mu, sd = gpr.predict(df[["cycle"]].iloc[te], return_std=True)
pr = (time.time() - t0) / len(te) * 1000
rows.append(("高斯过程(800行)", r2_score(yte, mu), fs, pr, "低", "有"))
cover = np.mean(np.abs(yte.to_numpy() - mu) <= 2 * sd)
print(f"GPR ±2σ 覆盖率 = {cover:.1%}\n")

print(f"{'模型':<18}{'R²':>9}{'训练':>9}{'单行':>11}{'可解释':>8}{'±σ':>5}")
for n, r2, fs, pr, it, sg in rows:
    print(f"{n:<18}{r2:>9.5f}{fs:>8.2f}s{pr:>10.4f}ms{it:>8}{sg:>5}")

期望输出:

text
GPR ±2σ 覆盖率 = 91.8%

模型                      R²       训练         单行     可解释   ±σ
线性回归              0.95053    0.02s    0.0005ms      高    无
随机森林(100)         0.96487    2.71s    0.0124ms      中    无
HistGB             0.96429   28.44s    0.0192ms      低    无
高斯过程(800行)        0.82693    0.47s    0.0204ms      低    有

★ 四个维度的标注说明

维度怎么看
精度R²(也可以补 MAE,给业务看)
训练耗时影响"能不能频繁重训 / 做超参搜索"
单行预测影响"能不能上实时服务"
可解释性线性"高"(系数就是一个公式);森林"中"(有重要性但看不到方向);HistGB"低"(几百棵树的组合,说不清);GPR"低"(核函数不直观)
±σ只有 GPR 有,且必须报覆盖率证明它可信

★ 这张表最该注意的两点

① 单行预测耗时四者都在 0.03 ms 以内

说明"实时场景"在这份数据上【不构成约束】
   ↓
如果你因为"实时"而选了线性模型,那是基于误解做的决定

② GPR 的 R² 低是"条件不同"造成的

GPR:800 行 + 1 个特征
其他:17,244 行 + 7 个特征
   ↓
这个比较【不公平】—— 报数时必须说明

★ 一个完整的"四维表"应该再补一列"适用场景":

python
extra = {
    "线性回归": "基线;可解释性要求高",
    "随机森林(100)": "表格数据稳健起点",
    "HistGB": "精度优先;有缺失值也扛得住",
    "高斯过程(800行)": "样本少 + 需要风险区间",
}
练习 2:为一个真实场景写选型建议

场景:你要给一家工厂做一个"设备健康度预警系统",约束如下:

  • 每小时对全部在役设备(约 5000 台)算一次健康度;
  • 结果推送给 3 个工程师,他们每天最多能人工复检 20 台;
  • 误判"该检修"会浪费人力,漏判"该检修"可能导致停机事故;
  • 系统跑在一台普通服务器上,没有 GPU。

写出你的选型建议(不超过 6 句话),必须包含:选什么、为什么、代价是什么。

参考答案

参考建议:

推荐用梯度提升(HistGB)做主体预测 + 高斯过程做风险分诊的组合。 理由:每小时 5000 台、单行预测在 0.01 ms 量级,速度完全不构成约束, 所以精度优先(HistGB 的 R² 0.96429 最高);但"漏判会导致停机事故"意味着 决策代价不对称,必须知道哪些预测不可信 —— 这需要一个能给区间的高斯过程。 用它把预测值附近的区间算出来,凡是区间跨过预警线的设备就进入"人工复检"队列, 每天正好排满 20 台。代价是 GPR 是 O(n³),只能对"高风险子集"跑(几百台), 不能对全量 5000 台跑 —— 这也是为什么用两个模型分工,而不是只用一个。

★ 这段建议的结构拆解

要素在文中的位置
选什么第 1 句(GBDT + GPR 组合)
为什么(速度不构成约束)第 2 句(量化了 5000 台/小时的可行性)
为什么(需要不确定性)第 2-3 句(决策代价不对称)
怎么用(落成流程)第 4 句(区间跨线 → 复检队列,对齐 20 台/天)
代价是什么第 5 句(O(n³),只能对子集跑)

★ 这个答案的关键洞察:两个模型分工

约束之间是矛盾的:
  · "全量 5000 台"    →  需要快模型
  · "必须知道风险"    →  需要 GPR(慢)

一个模型解决不了 → 拆成两个阶段:
  第 1 阶段:HistGB 对全量跑(快)→ 得到预测值
  第 2 阶段:GPR 只对"预测值靠近预警线"的设备跑(几百台)→ 得到区间
  第 3 阶段:区间跨线的进复检队列

★ 这个模式在工程里非常常见,叫"two-stage / cascade":

便宜模型筛一遍 → 贵的模型只处理"需要精细判断"的那部分
   ↓
总成本可控,且关键部分享有最好的精度

★ 一个可以运行出来的简化版

python
# 第 1 阶段:快模型算全量
mu_all = gbdt.predict(X_all)

# 第 2 阶段:只对"靠近预警线"的算区间
near = np.abs(mu_all - 0.80) < 0.10          # 落在 0.70~0.90 之间的
idx_near = np.where(near)[0]
mu_g, sd_g = gpr.predict(X_subset.iloc[idx_near], return_std=True)

# 第 3 阶段:区间跨线的进复检队列
lo, hi = mu_g - 2 * sd_g, mu_g + 2 * sd_g
need_check = (lo < 0.80) & (hi > 0.80)
print(f"全量 {len(mu_all)} 台 → 需精算 {len(idx_near)} 台 → 进复检 {need_check.sum()} 台")

★ 这个练习最重要的一点

"选哪个模型"这个问题,答案常常是【组合】,而不是"某一个"。
   ↓
而且组合的依据来自【约束的量化】(5000 台、20 台/天、代价不对称)
   ↓
不是来自"哪个 R² 高"
练习 3:为什么"用场景标签代替量化"是错的?

有人这样说:"实时看板场景,肯定要选最快的模型,所以用线性回归。"

请指出这个推理的问题,并给出正确的推理方式。

参考答案

这个推理有三个问题。

问题一:没有量化"实时"到底是多少

"实时"可能是:
  · 100 ms 内(Web 服务)
  · 1 秒内(看板刷新)
  · 1 分钟内(准实时报表)
  · 1 小时内(批处理)
      ↓
每个量级的约束强度完全不同
      ↓
而【没有量化,就无法判断它是否构成约束】

问题二:把"最快的模型"当成了目标

"选最快的"这个目标本身是错的 ——
   ↓
如果所有模型都满足延迟要求,那"最快"就没有意义了
   ↓
正确的目标应该是:"在满足约束的前提下,最大化你真正在意的那个维度"

问题三:没有验证"树模型真的慢"这个假设

本数据的实测:

text
单行预测耗时:
  线性回归    0.0004 ms
  随机森林    0.0220 ms     ← 慢 55 倍
  HistGB      0.0148 ms

但看绝对值:

100 ms 的预算下:
  线性   用了预算的 0.0004%
  森林   用了预算的 0.022%
      ↓
【两者都绰绰有余】

★ 正确的推理方式

第 1 步:量化约束
  "实时" = ? ms(去问业务方,或者去查系统需求文档)

第 2 步:测出候选模型在这个约束下的表现
  单行预测耗时、批量预测吞吐、模型加载时间、内存占用

第 3 步:判断约束是否成立
  如果所有候选都在预算内 → 这个约束不构成约束 → 排除它
  如果只有部分候选在预算内 → 它构成约束 → 用它筛掉不合格的

第 4 步:在剩下的候选里,按真正的目标维度选
  如果精度是目标 → 选精度最高的
  如果需要不确定性 → 选能给的

★ 一个真实的教训

某团队认为"实时场景必须用线性模型",于是放弃了 GBDT
   ↓
后来做性能测试发现:GBDT 单次预测 8 ms,而延迟预算是 500 ms
   ↓
【他们为一个不存在的约束,牺牲了 0.03 的 R² 和大量的实际效果】

★ 一条通用原则

约束必须被量化,才能被用来做取舍。 没有量化的"约束"只是一个猜测,而基于猜测做的技术决策, 很可能是在优化一个不存在的瓶颈。

★ 这个原则的推广

它不只适用于延迟。所有"性能约束"都一样:

约束类型常见误解正确做法
延迟"实时必须用简单模型"测 P50/P99 延迟,和预算对比
内存"树模型太占内存"测模型文件大小和运行时峰值
训练时间"深度模型训不起"测一次完整训练要多久、多久重训一次
数据量"样本不够不能上树模型"看学习曲线,而不是看经验值

★ 一句话总结

先测量,再取舍。 这条纪律在整本书里出现了很多次 (划分前先看数据结构、调参前先看学习曲线、选型前先量化约束), 它是同一个思维习惯的不同应用。

面向工程实践:选型是一次"被记录的决策" ​

① 选型结论必须留下"为什么不选别的"

❌ 只写结论:
   "选用随机森林。"

✅ 完整的决策记录:
   "选用随机森林(n_estimators=200)。
    理由:三个主流模型 R² 极差仅 0.0145,精度维度上差异很小;
          随机森林训练 4.70s,比 HistGB 快 7.6 倍,精度相当(0.96503 vs 0.96429);
          不选线性:精度低 0.0145,而本场景精度是唯一约束;
          不选 GPR:R² 低 0.14,且本场景(离线周报)不需要区间。"

★ 后面那句话的价值:

六个月后有人问:"为什么不用 XGBoost?"
   ↓
翻到这份记录 → 有数字、有理由 → 不用重新做一遍实验

② 一个可复用的选型决策记录模板

markdown
## 模型选型决策(2026-10-10)

### 场景与约束
| 约束 | 量化值 | 是否构成约束 |
|---|---|---|
| 延迟预算 | 100 ms | ❌ 否(最快 0.0004ms,最慢 0.022ms,都远低于预算) |
| 训练频率 | 每天 1 次 | ❌ 否(最慢 35.75s,可接受) |
| 需要风险区间 | 是 | ✅ 是 |
| 可解释性要求 | 中(要给工程师看趋势) | ⚠️ 弱约束 |

### 候选与量化结果
| 模型 | R² | 训练 | 单行 | 可解释 | ±σ |
|---|---|---|---|---|---|
| ... |

### 决策
**选定:<模型>**
- 主导维度:<哪个>
- 理由:<数字支撑>
- 代价:<放弃了什么>

### 未选说明
- <模型 A>:<为什么不选>
- <模型 B>:<为什么不选>

### 局限
- <数据范围 / 覆盖率 / 合成数据 / 未覆盖的子群>

③ 为什么"局限"这一节不能省

没有局限说明的结论 = 无法判断它什么时候会失效
   ↓
而模型一定会失效(数据漂移、场景变化、上游改动)
   ↓
那时你需要知道:"原来的结论建立在什么假设上"

本项目的三条局限(第 27 章的报告会写进去):

① 数据是合成的:单调退化、无随机跳变、无工况切换
② 覆盖面有限:只有 60 支设备、4 个批次
③ GPR 的区间只在训练分布内可信(超出 cycle 1440 属于外推)

★ 最后:这一章想留给你的思维

"用哪个模型?"
   ↓
这个问题本身是不完整的。
   ↓
完整的问法是:"在 <这些约束> 下,为了 <这个目标>,用哪个模型最划算?"
   ↓
把约束量化、把目标说清、把取舍记录下来
   ↓
【这就是"选型"和"挑一个"的区别】

下一章(阶段目标)会把这一切拼起来: 一条命令产出一张对比表、两张图、一份带结论的报告。


下一章:第 27 章 · ❖ 阶段目标:四路横评与选型报告