Step 3.3–3.5:构造、整合、格式化
这三小节是 Step 3 里最有创造性的部分,也是最容易糊过去的部分。糊过去的代价不是这 15% 里的几分,而是第 20 章那个问题:当你要向卫生部解释「人均 GDP 每多一美元,死亡率降低 0.00018‰」时,你会发现没人听得懂——而这本来是可以在 3.3 花五分钟避免的。
3.3 构造:七种常见的造法
| 类型 | 例子 | 什么时候用 |
|---|---|---|
| 数学变换 | log_gdp = log₁₀(gdp_pc) | 右偏、跨数量级、边际效应递减 |
| 比率/归一 | 医生数 / 人口、支出 / GDP | 消除规模影响,让不同大小的单元可比 |
| 交互项 | 疫苗覆盖 × 医生密度 | 你有业务理由相信「两个条件同时具备才起作用」 |
| 时间派生 | 从日期抽出年/月/季度;年份 − 起始年 | 几乎总是要做。原始日期字符串对模型毫无用处 |
| 滑窗/滞后 | 上一年的 u5mr、三年移动平均 | 面板或时序数据。威力最大,也最容易造出泄漏(见下方警告) |
| 分组聚合 | 该地区历年均值、该年全国均值 | 提供「相对位置」信息(这个地区比全国平均差多少) |
| 分箱 | 人均 GDP 分成四档 | 需要向业务方解释「低收入组 vs 高收入组」时;会损失信息,别默认做 |
取一次对数,值多少?
第 8 章发现了「Pearson 与 Spearman 差 0.2」,指向一个动作:构造 log_gdp。下面这台真的构造了这一列,并重训了模型:
把两个开关都打开再关上,你会看到这组数字:
| 指标 | 用 gdp_pc | 用 log_gdp | 变化 |
|---|---|---|---|
与 u5mr 的 Pearson r | −0.674 | −0.864 | 大幅改善 |
| 偏度 | 3.39 | 0.02 | 几乎完美对称 |
| 回归 R²(测试集) | 0.815 | 0.820 | +0.005 |
| 分类准确率 | 0.875 | 0.882 | +0.007 |
相关系数和偏度大幅改善,模型指标几乎没动。这个反差是真实的,而且很常见——原因是本来信号就足够强,线性模型已经从其他六列里拿到了大部分信息。
那为什么还要做?三个理由,其中第三个才是重点:
- 它让关系变成线性的,所以线性模型的假设成立了。虽然这次收益小,但在信号弱的数据上收益会很大。
- 它压低了极值的杠杆作用。清洗后
gdp_pc还有 18 个 IQR 异常(第 10 章决定保留),取对数后它们不再拉着回归线跑。 - 它让系数变得可以说出口。这是决定性的理由——
用原始 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 |
这本书的引擎第一版就犯了这个错,而且它的表现极具欺骗性: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_pc 与 log_gdp 高度共线,二者不同时进入线性模型(见 4.1)。」
这一章的一句话
构造特征的收益经常不在精度上(取对数只把 R² 从 0.815 挪到 0.820),而在「你能不能把系数说成人话」上;join 之后第一件事是数行数;缩放与否取决于你在 Step 6 选什么算法——而独热编码的列规格必须在训练集上冻住一次,否则会出现 AUC 1.000、F1 0 这种最令人困惑的 bug。
下一章是 Step 4,全课权重最小的一步(5%)。它只值一页纸——但那一页里有一个陷阱:PCA 会给一列纯噪声单开一个主成分,解释 14.2% 的方差。