Step 7(15%):测试设计——划分、k 折、不平衡
Step 7 的 15% 里,大部分人只做了 7.2(跑模型)。而 7.1「建立并论证测试设计」是这一步最容易拿也最容易丢的分,因为它考的是一件很多人从没想过的事:你报出来的那个准确率,本身有多少不确定性?这一章会给出答案——在这份数据上,只换随机种子,它能跳 6.3 个百分点。
一个准确率,六个百分点的抖动
下面这台做了两件事:一是用十个不同的随机种子各做一次 70/30 分层划分,二是做 3/5/10 折交叉验证。同一份数据、同一个算法:
| 测试设计 | 均值 | 标准差 | 范围 | 说明 |
|---|---|---|---|---|
| 单次划分 × 10 个种子 | 0.876 | 0.022 | 0.847 – 0.910 | 极差 0.063。你可以「挑」一个好看的种子 |
| 5 折交叉验证 | 0.887 | 0.031 | 0.844 – 0.927 | 每折用 80% 训练、20% 测试 |
| 10 折(决策树) | 0.840 | 0.053 | 0.771 – 0.917 | 折数越多,单折测试集越小,抖动越大 |
第一行是这一章的核心事实:如果你只跑一次划分就报一个数,你有能力让它好看 3 个百分点。而评分员无法分辨你是运气好还是挑过种子——所以整个数字的可信度都被削弱了。
解法很便宜:报「均值 ± 标准差」。写成「5 折交叉验证准确率 0.887 ± 0.031」,比写「准确率 0.91」更可信,也更接近事实。
| 设计 | 怎么做 | 什么时候用 |
|---|---|---|
| 单次留出(holdout) | 70/30 或 80/20,分层,固定种子 | 数据量大(> 几万行)时够用;Spark 上最省算力。必须报种子 |
| k 折交叉验证 | k=5 最常用,分层k 折 | 本课这种几百到几万行的规模,默认选它。三条产线都支持 |
| 按时间划分 | 训练 = 早期,测试 = 晚期,不打乱 | 有滞后/滑窗特征,或部署场景是「预测下一期」(第 13 章、第 21 章) |
「分层」(stratified)是本课几乎总该打开的开关:它保证每一折里正负例的比例和整体一致。不分层的后果在小样本或不平衡数据上很直接——某一折可能几乎没有正例,那一折的召回率会毫无意义地暴跌。
为什么 10 折的标准差反而更大:折数越多,每折的测试集越小(480 行分 10 折,每折只有 48 行),单折的估计就越不稳。5 折是本课规模最合适的默认值。
类不平衡:准确率 92.2% 的模型,一个都没抓到
本书示例数据的正例占比是 42.3%,相当平衡。但真实的 SDG 数据经常不是——「未达标地区」可能只占 5%,「设备故障」可能只占 0.3%。下面这台把正例压到 8% 再跑一遍:
第一个标签页(正例 8%、原样、阈值 0.5)的结果:
| 模型 | 准确率 | 精确率 | 召回率 | F1 | 抓到 / 漏掉 |
|---|---|---|---|---|---|
| 全猜「达标」 | 0.922 | 0 | 0 | 0 | 0 / 7 |
| 逻辑回归(阈值 0.5) | 0.922 | 0.5 | 0.571 | 0.533 | 4 / 3 |
两行的准确率一模一样,都是 0.922。但第一行一个高风险地区都没找到,第二行找到了 4 个(共 7 个)。这就是准确率悖论:在不平衡数据上,准确率主要反映的是多数类的占比,不是模型的能力。
三种应对,Demo 里都能切:
| 做法 | 准确率 | 精确率 | 召回率 | 代价 |
|---|---|---|---|---|
| 原样、阈值 0.5 | 0.922 | 0.500 | 0.571 | 漏掉 3 个(43%) |
| 把阈值挪到 0.1 | 0.833 | 0.318 | 1.000 | 误报 15 个 |
| 过采样到 1:1(阈值仍 0.5) | 0.822 | 0.286 | 0.857 | 误报 15 个;AUC 0.929,与不重采样几乎相同 |
最值得注意的是最后一行的 AUC:过采样前后 AUC 几乎没变。这说明重采样没有创造任何新信息——它做的事情在效果上等价于「悄悄把阈值往下挪」。
两种做法都合法,但阈值法更透明。因为阈值是一个你可以拿去和业务方讨论的数字(「我们把警报线放宽到 10%,代价是走访量增加两倍」),而「我做了 SMOTE」不是一句业务方能参与的话。第 18 章会把阈值的选择变成一道成本计算题。
- 只对训练集做。对测试集重采样是严重错误——测试集必须保持真实分布,否则你算出的精确率完全失真。
- 在划分之后做,而且要在交叉验证的每一折里单独做。先重采样再划分,复制出的样本会同时出现在两边,这是第 13 章讲的分组泄漏。(sklearn 的
Pipeline不支持重采样,要用imbalanced-learn的Pipeline。) - 优先考虑「加权」而不是「复制」。大多数算法支持类别权重:sklearn 的
class_weight='balanced'、SPSS 的 misclassification costs 矩阵。它们在数学上更干净,也不增加训练数据量。
7.1 到 7.3:三小节各写什么
| 小节 | 原文要求 | 你要交 |
|---|---|---|
| 7.1 | create and justify test designs | 划分方式 + 比例 + 是否分层 + 随机种子 + 为什么这样划分对应你的部署场景 + 泄漏防护声明(第 13 章那段模板) |
| 7.2 | conduct data mining(models must execute) | 可重跑的流程文件/Notebook + 执行截图(含运行时间)+ 关键输出。提交前清空状态跑一次 |
| 7.3 | search for patterns | 不只是指标:具体的模式——规则、系数方向、簇画像、Lift 表。这一节是 Step 8 的原料 |
7.3 是最容易被跳过的一节,因为它和 Step 8 看起来重复。区别是:7.3 是「找到了什么」,8.3 是「它意味着什么」。7.3 写「树的第一分裂点在人均 GDP 1 182 美元,该分支下 148 个地区中有 82% 未达标」;8.3 写「这说明存在一条收入门槛,低于它时单靠卫生服务投入不足以达标」。
# OSAS · 分层 k 折 + 网格搜索(参数选择只用训练集)
from sklearn.model_selection import StratifiedKFold, cross_val_score, GridSearchCV
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
scores = cross_val_score(pipe, X_train, y_train, cv=cv, scoring='f1')
print(f'5 折 F1 = {scores.mean():.3f} ± {scores.std():.3f}')
grid = GridSearchCV(pipe, {'clf__max_depth': [1, 2, 3, 4, 6, 8]},
cv=cv, scoring='f1') # 只在训练集上选参
grid.fit(X_train, y_train)
print(grid.best_params_, grid.best_score_.round(3))
print('测试集(只碰一次):', grid.score(X_test, y_test).round(3))
# BDAS · PySpark 的交叉验证
from pyspark.ml.tuning import CrossValidator, ParamGridBuilder
from pyspark.ml.evaluation import BinaryClassificationEvaluator
grid = ParamGridBuilder().addGrid(dt.maxDepth, [1, 2, 3, 4, 6]).build()
cv = CrossValidator(estimator=pipeline, estimatorParamMaps=grid,
evaluator=BinaryClassificationEvaluator(metricName='areaUnderROC'),
numFolds=5, seed=42, parallelism=2)
model = cv.fit(train)
print(model.avgMetrics) # 每个参数组合的平均 AUC
# 注意:Spark 的 randomSplit 不做分层。不平衡数据要自己分层:
# pos.randomSplit([0.7,0.3], 42) 与 neg.randomSplit([0.7,0.3], 42) 后再 union
ISAS · SPSS Modeler:Partition 节点负责划分(可设 70/30 的比例与随机种子)。它做的是随机划分——想要分层,就用 Sample 节点的 Stratified 模式(按目标字段分层抽样)来构造,或者退一步:划分之后用一个 Distribution/Matrix 节点核对两侧的正负例比例并把它写进报告。后者更省事,而且「我检查过划分后的类别比例」本身就是 7.1 想看到的严谨。
交叉验证在 Modeler 里没有单独节点,常见做法是用不同种子多跑几次 Partition 并记录指标区间,或用 Auto Classifier 的内置验证。报告里说明「因 Modeler 无内置 k 折,采用 5 个不同随机种子的留出法并报告指标区间」,这是完全可接受的处理,而且展示了你知道两者的差异。
一个跨线陷阱:Spark 的 randomSplit 不分层,也不保证和 sklearn 的 train_test_split(stratify=) 划出同样的集合。所以三条线的指标本来就不该完全相同。把这件事写清楚,比试图让三个数字对齐要好——后者会诱使你去调种子。
1. Step 7.1(半页):第 13 章那段测试设计模板 + 一段方差说明:「为评估估计的稳定性,我们以 5 个不同随机种子重复划分,准确率区间为 0.847–0.910(极差 0.063),故后续报告采用 5 折交叉验证的均值 ± 标准差。」
2. 不平衡处理声明(若适用):正例占比、选用的应对(阈值/权重/重采样)、为什么不用另外两种、以及「测试集未做任何重采样」这一句。
3. Step 7.2:执行证据——流程截图或 Notebook 输出,含运行时间。Spark 那次还要记分区数与 shuffle 次数(第 23 章)。
4. Step 7.3:3–5 个具体模式(规则、系数、簇画像、Lift 分箱),只陈述事实,解释留给 8.3。
这一章的一句话
同一份数据只换随机种子,准确率能在 0.847 到 0.910 之间跳——所以要报「5 折均值 ± 标准差」而不是一个数;而在不平衡数据上,准确率 0.922 可能对应召回率 0(一个都没抓到),此时该报的是召回、F1 和 AUC,并把阈值当成一个可以和业务方讨论的旋钮。
卷 IV 结束,模型跑完了。下一卷是全书主星:Step 8,单步 20%。它的第一课是——那条阈值线你本来就可以挪,而挪它比换算法值钱得多。