卷 III · 备料CH 11工位 11/24

Step 3.3–3.5:构造、整合、格式化

工位 STEP 3.3 / 3.4 / 3.5 构造数据 · 整合来源 · 格式化 15% 的后半 评分员在找:你构造的每一列有没有理由和验证(构造完 r 变成多少);join 之后行数为什么变了;以及哪些算法需要缩放、哪些不需要,你知不知道。

这三小节是 Step 3 里最有创造性的部分,也是最容易糊过去的部分。糊过去的代价不是这 15% 里的几分,而是第 20 章那个问题:当你要向卫生部解释「人均 GDP 每多一美元,死亡率降低 0.00018‰」时,你会发现没人听得懂——而这本来是可以在 3.3 花五分钟避免的。

特征构造join 的坑编码缩放列规格要冻住

3.3 构造:七种常见的造法

类型例子什么时候用
数学变换log_gdp = log₁₀(gdp_pc)右偏、跨数量级、边际效应递减
比率/归一医生数 / 人口支出 / GDP消除规模影响,让不同大小的单元可比
交互项疫苗覆盖 × 医生密度你有业务理由相信「两个条件同时具备才起作用」
时间派生从日期抽出年/月/季度;年份 − 起始年几乎总是要做。原始日期字符串对模型毫无用处
滑窗/滞后上一年的 u5mr三年移动平均面板或时序数据。威力最大,也最容易造出泄漏(见下方警告)
分组聚合该地区历年均值该年全国均值提供「相对位置」信息(这个地区比全国平均差多少)
分箱人均 GDP 分成四档需要向业务方解释「低收入组 vs 高收入组」时;会损失信息,别默认做

取一次对数,值多少?

第 8 章发现了「Pearson 与 Spearman 差 0.2」,指向一个动作:构造 log_gdp。下面这台真的构造了这一列,并重训了模型:

把两个开关都打开再关上,你会看到这组数字:

指标gdp_pclog_gdp变化
u5mr 的 Pearson r−0.674−0.864大幅改善
偏度3.390.02几乎完美对称
回归 R²(测试集)0.8150.820+0.005
分类准确率0.8750.882+0.007

相关系数和偏度大幅改善,模型指标几乎没动。这个反差是真实的,而且很常见——原因是本来信号就足够强,线性模型已经从其他六列里拿到了大部分信息。

那为什么还要做?三个理由,其中第三个才是重点:

  1. 它让关系变成线性的,所以线性模型的假设成立了。虽然这次收益小,但在信号弱的数据上收益会很大。
  2. 它压低了极值的杠杆作用。清洗后 gdp_pc 还有 18 个 IQR 异常(第 10 章决定保留),取对数后它们不再拉着回归线跑。
  3. 它让系数变得可以说出口。这是决定性的理由——
◆ 「每多一美元」vs「每翻十倍」

用原始 gdp_pc 时,线性回归给出的系数是 −0.00018:人均 GDP 每增加 1 美元,5 岁以下死亡率降低 0.00018‰。这句话在会议室里等于没说。

log₁₀(gdp_pc) 时,系数变成「人均 GDP 每翻十倍,死亡率降低 X‰」——这句话决策者能用,因为它对应一个可以想象的变化幅度。

Step 3.3 的真正客户是 Step 8.3。你在这里构造的每一列,最后都要用人话说出来。构造之前先想一遍「我要怎么解释它」,能省掉后面很多返工。

Demo 里还有第三行:一个比率列 doc_per_urban = doctors / urban,相关系数只有 −0.373——比两个原始列都弱。这是诚实的结果,也是一条纪律:构造出来的特征不一定更好,要用数字验一遍再决定留不留。报告里写「我们尝试构造了三个特征,其中两个保留、一个因相关性显著低于原始列而弃用」,比只写成功的那两个更有说服力。

⚠ 滑窗特征:最容易造出的时间泄漏

「用上一年的死亡率预测今年」听起来很合理,而且效果会好得惊人。但如果你的划分方式是随机的,这就是泄漏:某个地区 2019 年的记录进了训练集,2020 年的记录进了测试集,而 2020 年那行的特征里含有 2019 年的目标值——模型在测试集上「看见」了训练集的答案。

两条规则:① 只要用了滞后特征或滑窗特征,划分必须按时间切(训练用 2015–2020,测试用 2021–2023),不能随机划分;② 造滑窗时确认窗口只往回看——rolling(3).mean() 默认是往回的,但 rolling(3, center=True) 会偷看未来。

这一条在 Step 7.1「create and justify test designs」里必须交代。第 13 章会把三种泄漏一起摊开。

3.4 整合:join 之后先看行数

如果你只用一份数据源,这一节写三行就够:「本项目使用单一数据源,无需整合。若后续加入 X 数据,将按(地区码、年)连接。」写这三行,不要留空。

如果要合并多个来源,join 之后的第一件事永远是数行数

现象说明什么怎么办
行数变多右表的键不唯一(一对多),行被复制了先在右表上去重或聚合,再 join。这是最常见的 join 事故
行数变少用了内连接,键匹配不上的行被丢掉改左连接,然后看哪些行的右侧全空——那批行本身就是发现
行数没变但缺失暴增左连接成功,但匹配率低查键的格式(前后空格?大小写?编码不同?年份是字符串还是整数?)
before = len(df)
merged = df.merge(pop, on=['region', 'year'], how='left', validate='one_to_one')
print(f'{before} → {len(merged)} 行,匹配率 '
      f'{merged["population"].notna().mean():.1%}')

validate='one_to_one' 是那个能救你的参数——如果键不唯一,pandas 会当场抛错,而不是默默把行数翻倍。(Spark 没有这个参数,要自己先 groupBy(key).count() 检查。)

报告里要写:连接键、连接类型、连接前后行数、匹配率、未匹配行的处理方式。五个数字,一句话。

3.5 格式化:编码与缩放

类别列怎么编码

方法怎么做什么时候用
独热(One-Hot)6 个地区 → 5 个 0/1 列(丢一个做基准)默认选择。线性模型必须用它;类别数少(< 15)时最安全
序数(Ordinal)低/中/高 → 0/1/2类别本身有顺序时。用在无序类别上是错误——模型会以为「西区 = 4 > 北区 = 1」
目标编码用该类别下目标的均值替代类别数极多(几百个)时。极易泄漏,必须只用训练集算,且要做折内编码
不编码直接喂给算法SPSS 的 C5.0、LightGBM 等能原生处理类别的算法。Spark MLlib 需要 StringIndexer + OneHotEncoder
⚠ 一个真实的 bug:独热的列规格必须在训练集上冻住

这本书的引擎第一版就犯了这个错,而且它的表现极具欺骗性:AUC = 1.000,准确率 0.283,F1 = 0。模型的排序能力完美,但所有预测都落在阈值下方。

原因:预测时对单行重新推导独热列,那一行只有一个地区,于是生成的列数与训练时不同,权重和特征对不上位。

正确做法:把列规格(有哪些类别、顺序如何)在训练集上确定一次并保存下来,预测时复用同一份规格。这正是 sklearn 的 OneHotEncoder(handle_unknown='ignore') 和 Spark 的 StringIndexer(handleInvalid='keep') 存在的理由——它们要处理的是「测试集里出现了训练集没见过的类别」这件事。

报告里值得写一句:「类别编码器在训练集上 fit 后复用于测试集,未见类别按 handle_unknown='ignore' 处理,以保证特征维度一致。」这一句会让人知道你真的做过这件事,而不是抄的教程。

要不要缩放:一张表说完

算法需要缩放?为什么
KNN、K-Means、PCA、SVM必须它们算距离。人均 GDP 的量级(几千)会完全压掉医生密度(0–1.25)
逻辑回归、线性回归(带正则)梯度下降收敛更快;正则项对系数大小敏感,不缩放等于对不同列施加不同惩罚
线性回归(无正则,解析解)不必数学上等价。但系数没法互相比大小(第 20 章会展示怎么补救)
决策树、随机森林、GBDT不需要它们只比较「大于还是小于某个阈值」,对单调变换完全免疫
朴素贝叶斯不需要逐列独立建分布

最后一列的一个推论值得记住:树模型不需要缩放,也不需要取对数——第 10 章保留的那 18 个极值对树完全无害。所以「要不要做 Step 4 的变换」这个问题的答案取决于你在 Step 6 选什么算法。这条依赖关系写进报告,就是证据链。

▣ 本章交付物 —— 报告里放什么

1. Step 3.3:构造列清单表(列名 / 公式 / 构造理由 / 构造后与目标的相关系数 / 是否保留)。把弃用的那一列也留在表里。

2. Step 3.4:单源就写三行说明;多源写连接键、类型、前后行数、匹配率、未匹配处理。

3. Step 3.5:编码方式与理由(含「列规格在训练集冻住」那一句)、缩放方式与「哪些算法需要/不需要」的对照、最终特征清单(几列进了模型)。

4. 一句给 Step 4 的过渡:「经 3.3–3.5 处理后,特征矩阵为 480 × N;因 gdp_pclog_gdp 高度共线,二者不同时进入线性模型(见 4.1)。」

这一章的一句话

构造特征的收益经常不在精度上(取对数只把 R² 从 0.815 挪到 0.820),而在「你能不能把系数说成人话」上;join 之后第一件事是数行数;缩放与否取决于你在 Step 6 选什么算法——而独热编码的列规格必须在训练集上冻住一次,否则会出现 AUC 1.000、F1 0 这种最令人困惑的 bug。

下一章是 Step 4,全课权重最小的一步(5%)。它只值一页纸——但那一页里有一个陷阱:PCA 会给一列纯噪声单开一个主成分,解释 14.2% 的方差。