卷 IV · 上机CH 17工位 17/24

Step 7(15%):测试设计——划分、k 折、不平衡

工位 STEP 7.1 / 7.2 / 7.3 测试设计 · 执行 · 搜索模式 15% 评分员在找:7.1 要「create and justify」——论证,不是描述。以及 7.2 那句强制要求:models must execute

Step 7 的 15% 里,大部分人只做了 7.2(跑模型)。而 7.1「建立并论证测试设计」是这一步最容易拿也最容易丢的分,因为它考的是一件很多人从没想过的事:你报出来的那个准确率,本身有多少不确定性?这一章会给出答案——在这份数据上,只换随机种子,它能跳 6.3 个百分点。

分层划分k 折方差类不平衡准确率悖论

一个准确率,六个百分点的抖动

下面这台做了两件事:一是用十个不同的随机种子各做一次 70/30 分层划分,二是做 3/5/10 折交叉验证。同一份数据、同一个算法:

测试设计均值标准差范围说明
单次划分 × 10 个种子0.8760.0220.847 – 0.910极差 0.063。你可以「挑」一个好看的种子
5 折交叉验证0.8870.0310.844 – 0.927每折用 80% 训练、20% 测试
10 折(决策树)0.8400.0530.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.9220000 / 7
逻辑回归(阈值 0.5)0.9220.50.5710.5334 / 3

两行的准确率一模一样,都是 0.922。但第一行一个高风险地区都没找到,第二行找到了 4 个(共 7 个)。这就是准确率悖论:在不平衡数据上,准确率主要反映的是多数类的占比,不是模型的能力。

三种应对,Demo 里都能切:

做法准确率精确率召回率代价
原样、阈值 0.50.9220.5000.571漏掉 3 个(43%)
把阈值挪到 0.10.8330.3181.000误报 15 个
过采样到 1:1(阈值仍 0.5)0.8220.2860.857误报 15 个;AUC 0.929,与不重采样几乎相同

最值得注意的是最后一行的 AUC:过采样前后 AUC 几乎没变。这说明重采样没有创造任何新信息——它做的事情在效果上等价于「悄悄把阈值往下挪」。

两种做法都合法,但阈值法更透明。因为阈值是一个你可以拿去和业务方讨论的数字(「我们把警报线放宽到 10%,代价是走访量增加两倍」),而「我做了 SMOTE」不是一句业务方能参与的话。第 18 章会把阈值的选择变成一道成本计算题。

⚠ 重采样的三条纪律
  1. 只对训练集做。对测试集重采样是严重错误——测试集必须保持真实分布,否则你算出的精确率完全失真。
  2. 在划分之后做,而且要在交叉验证的每一折里单独做。先重采样再划分,复制出的样本会同时出现在两边,这是第 13 章讲的分组泄漏。(sklearn 的 Pipeline 不支持重采样,要用 imbalanced-learnPipeline。)
  3. 优先考虑「加权」而不是「复制」。大多数算法支持类别权重:sklearn 的 class_weight='balanced'、SPSS 的 misclassification costs 矩阵。它们在数学上更干净,也不增加训练数据量。

7.1 到 7.3:三小节各写什么

小节原文要求你要交
7.1create and justify test designs划分方式 + 比例 + 是否分层 + 随机种子 + 为什么这样划分对应你的部署场景 + 泄漏防护声明(第 13 章那段模板)
7.2conduct data mining(models must execute可重跑的流程文件/Notebook + 执行截图(含运行时间)+ 关键输出。提交前清空状态跑一次
7.3search 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%。它的第一课是——那条阈值线你本来就可以挪,而挪它比换算法值钱得多。