Skip to content

第 03 章 · 为什么横截面特征到顶了 ​

这一章要解决什么:上一本最后得出结论"卡点不在模型,在特征"。 这一章把它量化:三个方向各自的收益到底是多少、差多少倍。

最容易踩的坑:看到"分数到 0.965 就上不去了", 第一反应是"换个更强的模型"或"再多采点数据"。这两个方向都到顶了。

本章你需要完成的代码:demos/ch03-why-features.py

3.1 三张表:越往下越"窄",但信息越"厚" ​

真实输出:

text
=== ① 三张表的规模(越往下越「窄」,但信息越「厚」)===
  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,2399 → 9降采样(把曲线压成几个数)
窗口 → 时序21,239 → 21,2399 → 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 收益一:换模型(上一本的结论,重新量一遍) ​

真实输出:

text
=== ② 收益一:换模型(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 收益二:加数据 ​

真实输出:

text
=== ④ 加数据(学习曲线,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 ★ 收益三:加"对的特征" ​

真实输出:

text
=== ③ 加特征(同一份 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 ★ 三个收益放在一起 ​

真实输出:

text
汇总(统一 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 列

★ 但比"倍数"更有说服力的是下面这一对数字

text
换模型(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 常见坑 ​

text
① 分数上不去就先换模型 → 换模型的极差只有 0.0145,而且已经到顶
② 分数上不去就先加数据 → 验证线早就走平,加数据收益 0.0108
③ 不看"上限"只看"差距" → 换模型是"更接近上限",不是"抬高上限"
④ 把三个收益当成可加的 → 它们互相重叠,都在同一条可预测性的池子里
⑤ 以为"加特征"就是"多凑列" → 加错的特征会让分数【下降】(第 05 章会实测)

★ ⑤ 提前说明:

本书第 05 章会看到一个反直觉的结果:
   加了 10 列窗口/差分特征 → 测试 R² 从 0.96503 【降到】0.95906

所以"特征工程"不是"加列",而是"加对的信息"。这一章只证明了"特征方向有空间",还没说明"该怎么加"。

3.7 完整示例与输出 ​

bash
cd code
python demos/ch03-why-features.py

真实输出(节选):

text
★ 统一口径: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 复现本章的三个收益:

  1. 换模型:线性 / 随机森林 / 梯度提升,各自的测试 R² 与极差;
  2. 加数据:用 learning_curve 扫 5 个训练规模,看验证 R² 的涨幅;
  3. 加特征:基础 7 列 vs 基础 + 轨迹 4 列;
  4. 算"加 4 列的收益"是"换模型收益"的几倍。
参考答案
python
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} 倍")

期望输出:

text
  线性       测试 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 对【特征】的偏导远大于对【模型】的偏导(在模型已经足够强时)
   ↓
而且两者【不是可分离的】:
   同一个模型提升幅度,在贫矿和富矿上完全不同
      ↓
   所以 "收益_模型 + 收益_特征" 没有意义

★ 一个具体的验证(可以自己跑):

python
# 在 7 列上:线性 vs 提升,各多少?
# 在 21 列上:同一对模型,各多少?

实测(同一份 19,979 行、同一划分):

text
  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 列。

  1. 跑一次,报告测试 R²;
  2. 和第 3.2 节的"基础 7 列"、"基础+轨迹 21 列"对比;
  3. 说明"拿不到历史"这个约束带来了多大损失。
参考答案
python
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 行可比样本、同一划分):

text
  基础 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(因为"听起来更硬核")

如果先花半天量收益量级:

python
# 半天能做完的三件事
# ① 换模型:线性 -> 提升,看极差
# ② 学习曲线:看验证线是否还在涨
# ③ 加一组"猜测的特征",看收益量级

结果可能是:

换模型(已试 4 种):+0.015   → 方向 A 上限低
加数据(学习曲线走平):+0.011 → 方向 B 上限低
加一组候选特征:     +0.035   → 方向 C 最有希望
   ↓
【半天验证省下 4 人月】

★ 这就是"用实验代替猜测"在资源分配上的价值。

② 一个可复用的"收益量级探测"模板

python
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

用法:

python
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}")

输出直接告诉你该往哪投:

text
  加特征[轨迹]        +0.03998   ← 值得做
  换模型              +0.01587
  加数据              +0.01277
  加特征[窗口统计]    +0.00006   ← 几乎为零,不值得做
  加特征[差分斜率]    +0.00001   ← 同上

★ 注意那两行负值 —— 它们同样重要: 它们帮你"排除"了两个方向,避免白做两周。

③ 这条方法论的适用范围

它不只用于特征工程,任何"效果不够好"的场景都适用:

  · 推荐系统点击率不达标 → 是模型问题、数据问题、还是特征问题?
  · 搜索相关性不好     → 是排序模型、标注数据、还是查询理解?
  · 风控模型漏放       → 是特征不足、样本偏差、还是阈值设错?
     ↓
统一做法:把候选方向各自的收益【量到一个量级上】,
         再决定把资源投给哪个

★ 一句话总结

在"效果不够好"的时候,最贵的错误不是选错方法, 而是【没有先量收益量级就开始投入】。

这一章给你的就是那个"先量一下"的工具和习惯。


下一章:第 04 章 · 信息结构:模型只看到一行