第 03 章 · 为什么横截面特征到顶了
这一章要解决什么:上一本最后得出结论"卡点不在模型,在特征"。 这一章把它量化:三个方向各自的收益到底是多少、差多少倍。
最容易踩的坑:看到"分数到 0.965 就上不去了", 第一反应是"换个更强的模型"或"再多采点数据"。这两个方向都到顶了。
本章你需要完成的代码:demos/ch03-why-features.py
3.1 三张表:越往下越"窄",但信息越"厚"
真实输出:
=== ① 三张表的规模(越往下越「窄」,但信息越「厚」)===
cycle_raw.csv(逐循环) 69,591 行 × 3 列
soh_features.csv(窗口) 21,239 行 × 9 列
soh_features_v2.csv(窗口+时序) 21,239 行 × 27 列★ 这三行是整本书的地图
第 1 层:逐循环 69,591 行 × 9 列 ← 最原始,一行 = 一圈
↓ 按窗口汇总(100 圈 → 1 行)
第 2 层:窗口表 21,239 行 × 9 列 ← 上一本用的表,一行 = 一段
↓ 补上"带历史看"的列
第 3 层:时序特征表 21,239 行 × 27 列 ← 本书用的表,一行 = 一段 + 它的过去关键观察:第 2 层和第 3 层行数完全相同(都是 21,239)。
第 2 层 → 第 3 层:行数没变,列数从 9 涨到 27
↓
所以这一层的升级【不是"加样本",而是"给同样的样本补上历史信息"】★ 这个区分很重要:
| 升级方式 | 行数 | 列数 | 本质 |
|---|---|---|---|
| 逐循环 → 窗口 | 69,591 → 21,239 | 9 → 9 | 降采样(把曲线压成几个数) |
| 窗口 → 时序 | 21,239 → 21,239 | 9 → 27 | 补信息(同一行,多看一段历史) |
本书做的全是第二件事。
点开:为什么"降采样"能压缩 3.28 倍还不怎么损失信息?
逐循环 69,591 行 → 窗口 21,239 行
压缩比 = 69,591 / 21,239 = 3.28为什么压得掉:
设备退化是一个【慢变量】
↓
相邻两圈的 soh 只差 0.0001~0.0002(第 08 章会实测)
↓
而测量噪声和它同量级
↓
也就是说:相邻两圈的信息【几乎是重复的】
↓
所以"每 3 圈取一个窗口"几乎不丢信息,反而把噪声平均掉了★ 这是"窗口"这个动作的本质:
用一个统计量(均值/标准差)代表一段时间。 它成立的前提是"这段时间内变化很慢"。
反过来,如果信号变化很快(比如故障瞬间的电流尖峰), 窗口汇总就会把关键信息抹掉 —— 那时应该用更细的窗口,或者干脆不汇总。
判断标准:
窗口长度 << 信号变化的时间尺度 → 汇总安全(本例成立)
窗口长度 ≈ 信号变化的时间尺度 → 汇总会抹掉信息本数据:窗口 100 圈,而退化在 800~1500 圈的尺度上发生 —— 相差一个数量级,安全。
3.2 收益一:换模型(上一本的结论,重新量一遍)
真实输出:
=== ② 收益一:换模型(7 列基础特征)===
线性回归(+标准化) R² = 0.94313
随机森林(200) R² = 0.95900
梯度提升(300) R² = 0.95862
→ 极差 = 0.01587从最朴素的线性回归,到 300 棵提升树,总共只涨 0.0159。
口径说明:本章所有对比都在同一份 19,979 行(四组时序特征齐全的行)上做, 划分是
GroupShuffleSplit(test_size=0.2, random_state=42)→ 训练 16,236 / 测试 3,743 行。 所以这里的 0.95900 与上一本第 23 章的 0.96503 不是同一个数 —— 那边用的是全部 21,239 行。这正是第 07 章要讲的纪律:对照实验必须口径一致。
这个数字你应该在上本书里见过(第 23、27 章的四路横评)。 在这里重新量一遍,是为了让三个收益放在同一把尺子上比较。
3.3 收益二:加数据
真实输出:
=== ④ 加数据(学习曲线,7 列)===
2664 -> 0.9392
5328 -> 0.9450
7992 -> 0.9512
10656 -> 0.9524
13320 -> 0.9519
→ 加数据收益 = +0.01277样本量从 2,664 涨到 13,320(5 倍),验证 R² 只涨 0.0128,而且在 7,992 行之后就停了。
这构成了第二重否定结论:
换模型:收益 0.0159
加数据:收益 0.0128(且已经走平)
↓
两个方向都到顶了3.4 ★ 收益三:加"对的特征"
真实输出:
=== ③ 加特征(同一份 sub,随机森林)===
基础 7 列 特征 7 R² = 0.95900
+窗口/差分/极差(17 列) 特征 17 R² = 0.95906
+轨迹(21 列) 特征 21 R² = 0.99904
→ 17 列比 7 列: +0.00006
→ 21 列比 17 列: +0.03998★ 加 10 列窗口/差分,只涨 0.00006;再加 4 列轨迹,涨 0.03998。
这就是本书的性格:加错的特征几乎没用,加对的特征一下跳 0.04。
这 4 列是什么?
soh_lag1 上一个窗口的实测 SOH
soh_lag5 5 个窗口前的实测 SOH
soh_win_mean5 过去 5 个窗口实测 SOH 的均值
soh_win_std5 过去 5 个窗口实测 SOH 的标准差它们的共同点:都是"标签的历史值"。
★ 这一节要建立的核心认知:
不是"特征越多越好",而是"特征里要包含能定位当前状态的信息"。
为什么这 4 列这么有用?
基础 7 列描述的是"这个窗口内的瞬时读数"
↓
但同一个读数可以对应不同的历史轨迹:
· 一支设备从 1.00 慢慢降到这里(健康地老化)
· 一支设备昨天还是 0.98、今天突然掉到这里(异常)
↓
光看当前读数,模型分不出这两种情况
↓
而"历史 SOH"直接告诉它:这支设备之前在哪、降得多快
↓
【它把"横截面回归"变成了"轨迹外推"】★ "横截面回归"和"轨迹外推"的差别
| 横截面 | 轨迹 | |
|---|---|---|
| 模型看到的信息 | 一个点的 7 个读数 | 一个点 + 它过去在哪 |
| 它回答的问题 | "这个读数对应多少 SOH" | "从这条轨迹看,现在应该在哪" |
| 类比 | 看一张照片判断年龄 | 看一段视频判断年龄 |
后者显然更准 —— 因为退化是有惯性的。
3.5 ★ 三个收益放在一起
真实输出:
汇总(统一 19,979 行口径)
换模型(7 列上的极差) +0.01587
换模型(21 列上的极差) +0.00007 ← 特征到位后,模型几乎没差
加数据(样本 ×5) +0.01277
加特征(7 → 21 列) +0.04003
其中 10 列窗口/差分 +0.00006
其中 4 列轨迹 +0.03998★ 这张表是本书存在的理由。 逐行读:
| 方向 | 收益 | 说明 |
|---|---|---|
| 换模型(7 列上) | +0.01587 | 线性 → 300 棵提升树 |
| 加数据(样本 ×5) | +0.01277 | 且已经走平 |
| 加特征(7 → 21 列) | +0.04003 | 是换模型的 2.5 倍、加数据的 3.1 倍 |
| 其中 10 列窗口/差分 | +0.00006 | 几乎为零 |
| 其中 4 列轨迹 | +0.03998 | 全部收益来自这 4 列 |
★ 但比"倍数"更有说服力的是下面这一对数字
换模型(7 列上)的极差 = 0.01587
换模型(21 列上)的极差 = 0.00007
↑
缩小了 221 倍含义:
只有 7 列时,选哪个模型很重要(差 0.0159)
↓
有了 21 列(含历史)后,选哪个模型几乎不重要(差 0.00007)
↓
【因为信息上限抬高了,所有模型都能贴近它】★ 这才是"卡点在特征"的最强证据 —— 不是"加特征比换模型收益大 2.5 倍",而是:
特征到位之后,换模型这件事直接变得无关紧要了。
★ 一句话总结
特征决定上限,模型决定你离上限多远。 上限抬高之后,"离上限多远"这个问题就不存在了。
点开:为什么"加特征"和"换模型"不能简单相加?
一个直观的解释:
模型的作用是"从特征里榨出可预测性"
特征的作用是"决定有多少可预测性可以被榨"
↓
所以:
特征 = 上限(有多少信息)
模型 = 你离上限多近
↓
当特征里只有 7 列瞬时读数时:
线性榨到 0.95053,提升榨到 0.96503
→ 上限就在 0.965 附近,两个模型差了 0.0145(都是"离上限的距离")
当特征里有 21 列(含历史)时:
上限提到了 0.999 附近
→ 线性也能到 0.999 左右(本数据线性逼近能力够用)★ 所以"换模型"和"加特征"的关系是:
加特征把上限抬高,换模型让你更接近上限。上限低的时候,换模型有点用;上限抬上去之后,换模型就没必要了。
这直接解释了第 27 章会看到的一个现象:
21 列特征下,随机森林 0.99904、梯度提升 0.99907、线性 0.99910
↓
三个模型差 0.00006 —— 几乎完全相同
↓
【因为上限已经很高,谁都能贴近它】这也是"信息上限由特征决定,不由超参决定"的完整含义。
3.6 常见坑
① 分数上不去就先换模型 → 换模型的极差只有 0.0145,而且已经到顶
② 分数上不去就先加数据 → 验证线早就走平,加数据收益 0.0108
③ 不看"上限"只看"差距" → 换模型是"更接近上限",不是"抬高上限"
④ 把三个收益当成可加的 → 它们互相重叠,都在同一条可预测性的池子里
⑤ 以为"加特征"就是"多凑列" → 加错的特征会让分数【下降】(第 05 章会实测)★ ⑤ 提前说明:
本书第 05 章会看到一个反直觉的结果:
加了 10 列窗口/差分特征 → 测试 R² 从 0.96503 【降到】0.95906所以"特征工程"不是"加列",而是"加对的信息"。这一章只证明了"特征方向有空间",还没说明"该怎么加"。
3.7 完整示例与输出
cd code
python demos/ch03-why-features.py真实输出(节选):
★ 统一口径:19,979 行(四组时序特征齐全),训练 16,236 / 测试 3,743
=== ② 收益一:换模型(7 列基础特征)===
线性回归(+标准化) R² = 0.94313
随机森林(200) R² = 0.95900
梯度提升(300) R² = 0.95862
→ 极差 = 0.01587
→ 21 列上的极差 = 0.00007 ← 特征到位后,模型几乎没差
=== ③ 收益二:加数据(学习曲线,7 列)===
2664 -> 0.9392
13320 -> 0.9519
→ 加数据收益 = +0.01277
=== ④ 收益三:加【对的特征】===
基础 7 列 特征 7 R² = 0.95900
+窗口/差分/极差(17 列) 特征 17 R² = 0.95906
+轨迹(21 列) 特征 21 R² = 0.99904
→ 加 10 列窗口/差分: +0.00006 ← 几乎为零
→ 加 4 列轨迹 : +0.03998 ← 全部收益来自这 4 列本章练习
练习 1:把三个收益量到同一把尺子上
用 soh_features_v2.csv 复现本章的三个收益:
- 换模型:线性 / 随机森林 / 梯度提升,各自的测试 R² 与极差;
- 加数据:用
learning_curve扫 5 个训练规模,看验证 R² 的涨幅; - 加特征:基础 7 列 vs 基础 + 轨迹 4 列;
- 算"加 4 列的收益"是"换模型收益"的几倍。
参考答案
import numpy as np
import pandas as pd
from sklearn.ensemble import HistGradientBoostingRegressor, RandomForestRegressor
from sklearn.linear_model import LinearRegression
from sklearn.metrics import r2_score
from sklearn.model_selection import GroupKFold, GroupShuffleSplit, learning_curve
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
v2 = pd.read_csv("data/battery/soh_features_v2.csv")
BASE = ["cycle", "mean_charge_voltage_v", "mean_temp_rise_c", "resistance_mohm",
"coulombic_eff", "cap_fade_rate_ah_per_100", "temp_std_c"]
TRAJ = ["soh_lag1", "soh_lag5", "soh_win_mean5", "soh_win_std5"]
gss = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42)
# ① 换模型
tr, te = next(gss.split(v2[BASE], v2["soh"], groups=v2["cell_id"]))
r2s = {}
for name, mk in [
("线性", lambda: Pipeline([("s", StandardScaler()), ("m", LinearRegression())])),
("随机森林", lambda: RandomForestRegressor(n_estimators=200, random_state=42, n_jobs=-1)),
("梯度提升", lambda: HistGradientBoostingRegressor(max_iter=300, learning_rate=0.05,
max_depth=4, random_state=42,
early_stopping=False)),
]:
m = mk().fit(v2[BASE].iloc[tr], v2["soh"].iloc[tr])
r2s[name] = r2_score(v2["soh"].iloc[te], m.predict(v2[BASE].iloc[te]))
print(f" {name:<8} 测试 R² = {r2s[name]:.5f}")
spread_model = max(r2s.values()) - min(r2s.values())
print(f" 换模型极差 = {spread_model:.5f}\n")
# ② 加数据
sizes, _, va = learning_curve(
RandomForestRegressor(n_estimators=60, random_state=42, n_jobs=1),
v2[BASE], v2["soh"], groups=v2["cell_id"], cv=GroupKFold(3),
train_sizes=np.linspace(0.2, 1.0, 5), scoring="r2", n_jobs=1)
for n, v in zip(sizes, va.mean(1)):
print(f" {int(n):>6} -> {v:.4f}")
gain_data = va.mean(1)[-1] - va.mean(1)[0]
print(f" 加数据收益 = {gain_data:+.5f}\n")
# ③ 加特征(同一份可比样本)
usable = v2[BASE + TRAJ].notna().all(axis=1)
sub = v2[usable].reset_index(drop=True)
tr2, te2 = next(gss.split(sub[BASE], sub["soh"], groups=sub["cell_id"]))
res = {}
for name, feats in [("基础 7 列", BASE), ("基础+轨迹", BASE + TRAJ)]:
m = RandomForestRegressor(n_estimators=200, random_state=42, n_jobs=-1)
m.fit(sub[feats].iloc[tr2], sub["soh"].iloc[tr2])
res[name] = r2_score(sub["soh"].iloc[te2], m.predict(sub[feats].iloc[te2]))
print(f" {name:<10} 测试 R² = {res[name]:.5f}")
gain_feat = res["基础+轨迹"] - res["基础 7 列"]
print(f" 加特征收益 = {gain_feat:+.5f}\n")
print(f"④ 加特征 / 换模型 = {gain_feat / spread_model:.1f} 倍")
print(f" 加特征 / 加数据 = {gain_feat / gain_data:.1f} 倍")期望输出:
线性 测试 R² = 0.94313
随机森林 测试 R² = 0.95900
梯度提升 测试 R² = 0.95862
换模型极差 = 0.01587
2664 -> 0.9392
5328 -> 0.9450
7992 -> 0.9512
10656 -> 0.9524
13320 -> 0.9519
加数据收益 = +0.01277
基础 7 列 测试 R² = 0.95900
基础+轨迹 测试 R² = 0.99904
加特征收益 = +0.04003
④ 加特征 / 换模型 = 2.5 倍
加特征 / 加数据 = 3.1 倍★ 这里有一个必须注意的口径问题
本章统一用【19,979 行】(四组时序特征齐全的行),
所以基础 7 列的分数是 0.95900,而不是上一本第 23 章的 0.96503(那是全部 21,239 行)。
↓
两个数字都对,差别来自【样本集不同】,不是【代码不同】。
↓
这正是第 07 章要讲的纪律:做对照实验时,两边的行数必须一致。所以第 ③ 步必须用同一份 sub 来对比 —— 否则 gain_feat 里就混进了"Δ 样本"而不只是"Δ 特征"。
练习 2:为什么"换模型"和"加特征"不能简单相加?
请用你自己的话解释:为什么不能把"换模型收益 0.0145"和"加特征收益 0.0352"直接加起来 得到 0.0497?
参考答案
因为它们是两种不同性质的收益。
一个类比:
把"预测 SOH"想成"从矿里挖金子":
特征 = 矿的品位(有多少金子)
模型 = 你的开采技术(能挖出多少比例)
↓
"加特征" = 换了一个更富的矿
"换模型" = 改进了开采技术为什么不能相加:
在贫矿(7 列基础特征)上:
开采技术从"线性"换到"300 棵提升树",多挖出 0.0145
但这已经是贫矿的极限了 —— 矿里就那么多金子
在富矿(21 列含历史)上:
矿的品位提高了,可挖的量多了 0.0352
但此时"提升树"和"线性回归"挖出来的几乎一样(差 0.00006)
↓
因为在富矿上,技术不再是瓶颈★ 数学上的说法:
最终分数 ≈ f(特征, 模型)
其中 f 对【特征】的偏导远大于对【模型】的偏导(在模型已经足够强时)
↓
而且两者【不是可分离的】:
同一个模型提升幅度,在贫矿和富矿上完全不同
↓
所以 "收益_模型 + 收益_特征" 没有意义★ 一个具体的验证(可以自己跑):
# 在 7 列上:线性 vs 提升,各多少?
# 在 21 列上:同一对模型,各多少?实测(同一份 19,979 行、同一划分):
7 列 :线性 0.94313 提升 0.95862 差 0.01549
21 列:线性 0.99911 提升 0.99910 差 0.00001同一对模型,在两组特征上的差距差了 1500 倍以上 —— 这就说明"模型收益"依赖于"特征",不可分离。
★ 那正确的表述是什么?
❌ "换模型收益 0.0145 + 加特征收益 0.0352 = 总共 0.0497"
✅ "在 7 列特征下,换模型值 0.0145;
换成 21 列特征后,分数从 0.965 提到 0.999(+0.034),
而此时换模型已经不值钱了(差 0.00003)。"★ 一句话总结
特征决定上限,模型决定你离上限多远。 上限低时模型有用;上限抬高后,模型就没那么重要了。
这两句话不能相加,因为它们是"天花板"和"离天花板的高度"。
练习 3:如果没有"历史 SOH",还剩多少空间?
假设出于某种原因(比如上线时拿不到历史实测 SOH), 你只能用 21 列里除轨迹 4 列之外的 17 列。
- 跑一次,报告测试 R²;
- 和第 3.2 节的"基础 7 列"、"基础+轨迹 21 列"对比;
- 说明"拿不到历史"这个约束带来了多大损失。
参考答案
import pandas as pd
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import r2_score
from sklearn.model_selection import GroupShuffleSplit
v2 = pd.read_csv("data/battery/soh_features_v2.csv")
BASE = ["cycle", "mean_charge_voltage_v", "mean_temp_rise_c", "resistance_mohm",
"coulombic_eff", "cap_fade_rate_ah_per_100", "temp_std_c"]
WIN = ["volt_win_mean", "volt_win_std", "res_win_mean", "res_win_std", "temp_win_mean"]
DIFF = ["volt_slope20", "res_slope20", "temp_slope20", "res_diff1"]
RANGE = ["temp_win_range20"]
TRAJ = ["soh_lag1", "soh_lag5", "soh_win_mean5", "soh_win_std5"]
NO_TRAJ = BASE + WIN + DIFF + RANGE # 17 列
usable = v2[NO_TRAJ + TRAJ].notna().all(axis=1)
sub = v2[usable].reset_index(drop=True)
tr, te = next(GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42)
.split(sub[BASE], sub["soh"], groups=sub["cell_id"]))
for name, feats in [("基础 7 列", BASE),
("无轨迹 17 列", NO_TRAJ),
("全 21 列(含轨迹)", NO_TRAJ + TRAJ)]:
m = RandomForestRegressor(n_estimators=200, random_state=42, n_jobs=-1)
m.fit(sub[feats].iloc[tr], sub["soh"].iloc[tr])
r2 = r2_score(sub["soh"].iloc[te], m.predict(sub[feats].iloc[te]))
print(f" {name:<20} 特征 {len(feats):>2} 测试 R² = {r2:.5f}")期望输出(同一份 19,979 行可比样本、同一划分):
基础 7 列 特征 7 测试 R² = 0.95900
无轨迹 17 列 特征 17 测试 R² = 0.95906
全 21 列(含轨迹) 特征 21 测试 R² = 0.99904★ ② 对比与解读
基础 7 列 0.95900
无轨迹 17 列 0.95906 ← 加了 10 列,只涨 0.00006(等于没涨)
全 21 列 0.99904 ← 加上 4 列轨迹,跳了 0.03998注意第一行到第二行:加了 10 列,几乎没有任何变化。 (在全部 21,239 行的口径下,这 10 列甚至会让分数下降 —— 因为它带来了 1,260 行 NaN,dropna 后样本变少。这正是第 07 章的坑。)
为什么这 10 列几乎不涨分? 三个原因叠加:
① 信息重复:窗口统计那 5 列与基础列的相关性很高
(res_win_mean ~ resistance_mohm = 0.9951,volt_win_mean ~ mean_charge_voltage_v = 0.9950)
→ 没带来新信息
② 丢了样本:时序特征有 NaN,dropna 后从 21,239 行降到 19,979 行
→ 训练数据变少(这里为了让三行可比已统一用同一份 sub)
③ 多列噪声:几列与标签相关性很低的特征(res_diff1 只有 -0.0171)
→ 给树增加了"偶尔切错"的机会★ ③ "拿不到历史"这个约束的代价
有历史(21 列):0.99904
无历史(17 列):0.95906
↓
损失 0.03998这个数字就是"信息结构不对"的代价:
不是"少了几列",而是"少了能定位轨迹的那类信息"。
★ 由此得到一条重要的工程判断
"我能不能拿到历史实测标签?"
↓
能 → 特征工程的主线是"构造历史统计量"(本书第 05~10 章)
不能 → 只能做横截面建模,此时要接受 0.96 左右的上限,
并转向"其他能表达个体差异的信息"(比如工况、批次、外部环境)所以"预测时刻拿得到吗"这个问题,不只用于排查泄漏(第 17 章), 也用于提前判断"哪些特征值得做"。
面向工程实践:先量"收益量级",再决定投入方向
这一章最重要的不是那三个数字,而是"量收益量级"这个动作。
① 一个真实的资源分配场景
模型效果:R² = 0.87,业务要求 0.95
↓
三个可选方向:
A. 换更复杂的模型(深度学习?) 预算:2 人月
B. 采集更多数据 预算:3 人月 + 设备成本
C. 做特征工程(需要领域专家配合) 预算:1 人月
↓
如果没有量化,团队往往选 A 或 B(因为"听起来更硬核")如果先花半天量收益量级:
# 半天能做完的三件事
# ① 换模型:线性 -> 提升,看极差
# ② 学习曲线:看验证线是否还在涨
# ③ 加一组"猜测的特征",看收益量级结果可能是:
换模型(已试 4 种):+0.015 → 方向 A 上限低
加数据(学习曲线走平):+0.011 → 方向 B 上限低
加一组候选特征: +0.035 → 方向 C 最有希望
↓
【半天验证省下 4 人月】★ 这就是"用实验代替猜测"在资源分配上的价值。
② 一个可复用的"收益量级探测"模板
def probe_gains(df, base_feats, target, groups, candidate_feats_groups):
"""花少量时间探明三个方向的收益量级。"""
results = {}
# ① 换模型的极差(用最便宜的两个模型)
results["换模型"] = spread_over_models(df, base_feats, target, groups)
# ② 加数据的收益(学习曲线的前后差)
results["加数据"] = gain_from_more_data(df, base_feats, target, groups)
# ③ 加特征组的收益(逐个候选组)
for gname, gfeats in candidate_feats_groups.items():
results[f"加特征[{gname}]"] = gain_from_feats(
df, base_feats, base_feats + gfeats, target, groups)
return results用法:
gains = probe_gains(v2, BASE, "soh", "cell_id", {
"窗口统计": WIN, "差分斜率": DIFF, "轨迹": TRAJ,
})
for k, v in sorted(gains.items(), key=lambda x: -abs(x[1])):
print(f" {k:<20} {v:+.5f}")输出直接告诉你该往哪投:
加特征[轨迹] +0.03998 ← 值得做
换模型 +0.01587
加数据 +0.01277
加特征[窗口统计] +0.00006 ← 几乎为零,不值得做
加特征[差分斜率] +0.00001 ← 同上★ 注意那两行负值 —— 它们同样重要: 它们帮你"排除"了两个方向,避免白做两周。
③ 这条方法论的适用范围
它不只用于特征工程,任何"效果不够好"的场景都适用:
· 推荐系统点击率不达标 → 是模型问题、数据问题、还是特征问题?
· 搜索相关性不好 → 是排序模型、标注数据、还是查询理解?
· 风控模型漏放 → 是特征不足、样本偏差、还是阈值设错?
↓
统一做法:把候选方向各自的收益【量到一个量级上】,
再决定把资源投给哪个★ 一句话总结
在"效果不够好"的时候,最贵的错误不是选错方法, 而是【没有先量收益量级就开始投入】。
这一章给你的就是那个"先量一下"的工具和习惯。