你准备在第 6 天训练一个模型,预测之后订单的退款风险。测试目标是:未来日期里,模型在训练时没见过的用户。 如果只按用户分组,训练数据可能来自测试日期之后;如果只按日期切开,老用户仍可能出现在两侧。两条规则解决的是不同问题。
已有数据划分与泄漏入门讲过随机、分组与时间切分。本实验把它们放到同一份数据里,逐项检查哪些条件已经满足,并补上容易遗漏的“当时是否拿得到特征和标签”。
先写评估目标,再挑切分方法
| 将来要回答的问题 | 本轮至少应检查什么 |
|---|---|
| 老用户未来的记录 | 时间先后、信息可用时间;用户出现在两侧可以符合任务目标 |
| 训练中没见过的用户 | 用户组隔离;还要确认采样与真实使用场景相符 |
| 未来日期里训练中没见过的用户 | 同时检查用户隔离、时间先后和信息可用时间 |
GroupKFold 的契约是各折训练与测试的组不重叠;文档说明组顺序可以任意。因此它不会替你保证训练记录早于测试记录。GroupKFold API
本实验选择第三个目标。“新用户”在这里指模型训练中未见过,不自动等于业务上刚注册的人。若你的目标是给老用户预测明天的订单,不要为了得到零用户交集而把评估目标换掉。
八条记录,能暴露四种不同的问题
以下全部是自行设计的教学数据。日期用同一个月里的整数日表示;第 6 天固定模型,第 15 天离线评分。x 是一项数值特征。标签要等退款观察结束后才确定,不能把最终拿到的结果误当成第 6 天已经知道的结果。
| 记录 | 用户 | 预测日 | 标签可用日 | x | 特征可用日 |
|---|---|---|---|---|---|
| a1 | A | 1 | 3 | 10 | 1 |
| b1 | B | 2 | 4 | 20 | 2 |
| a2 | A | 4 | 8 | 30 | 4 |
| c1 | C | 6 | 8 | 100 | 6 |
| b2 | B | 7 | 9 | 40 | 7 |
| d1 | D | 8 | 10 | 120 | 8 |
| c2 | C | 9 | 11 | 140 | 10 |
| a3 | A | 10 | 12 | 50 | 10 |
先试两种不完整的方案:
- 把 A、B 全给训练,把 C、D 全给测试。 用户确实没有交集,但训练集包含第 10 天的 a3,测试却从第 6 天开始。它没有模拟“第 6 天固定模型”的条件。
- 第 6 天之前给训练,之后给测试。 时间顺序正确,但 A、B 在两侧重现,不能直接当作“未见用户”的测试。此外,a2 的标签第 8 天才确定;c2 的特征第 10 天才出现,却声称用于第 9 天预测。
后两项属于本实验依据业务时间定义的检查。只存一列“记录日期”无法判断它们;需要分别保留预测时间、特征可用时间和标签可用时间。晚到数据、事后回填的字段也需要按其当时可用状态重建。
固定一个可检查的划分
在这个教学协议中,训练记录必须同时满足:预测日早于 6、标签在第 6 天已确定、特征在各自预测时已可用。因此只留下 a1、b1。测试再选第 6 天起、训练中未见过的用户,并检查特征当时可用、标签在第 15 天评分时已确定,留下 c1、d1。
train = [r for r in rows
if r.day < 6 and r.label_known_day <= 6
and r.feature_known_day <= r.day]
seen = {r.user for r in train}
test = [r for r in rows
if r.day >= 6 and r.user not in seen
and r.feature_known_day <= r.day
and r.label_known_day <= 15]
这是一种明确的人工保留集规则,不是 GroupKFold 与 TimeSeriesSplit 的组合调用,也不声称适合所有数据。本例没有 c2 在第 9 天的历史特征值,故将其排除;实际项目应优先重建当时可得的特征,再决定能否评估。过滤后只有两条测试记录,不能据此评价模型泛化。还要记录排除原因、人数、类别比例和时间覆盖;不能悄悄删掉难样本,只汇报剩下的漂亮分数。若评估对象被排光,应报告无法完成该评估,而不是输出零错误。
gap 为 2,不等于隔开两天
TimeSeriesSplit 面向已按时间排序的数据,gap 排除的是训练末尾的样本数;它不读取业务日历,也忽略传入的 groups。官方提到等时间间隔,是为了让各折覆盖相同时间长度、指标容易比较,并不表示不规则时间戳会被 API 自动拒绝。TimeSeriesSplit API
假设一折的行号是:训练 [0,1,2],跳过 [3,4],测试 [5]。同样跳过两行:
| 六行的日期 | 最后一条训练 → 第一条测试 | 相差 |
|---|---|---|
| 1、2、3、4、5、6 | 3 → 6 | 3 天 |
| 1、2、20、21、22、30 | 20 → 30 | 10 天 |
如果订单量每天不同,用固定行数不能表达固定天数。若标签要观察 30 天才能确定,应按模型固定时刻检查标签是否已形成;不要只设置 gap=30 就声称问题解决。这个表仅演示行数与日期的区别,不是在运行 scikit-learn。
预处理也不能偷看测试值
训练特征是 [10,20],测试特征是 [100,120]。只从训练集学习“减去均值”的规则,均值是 15,测试变成 [85,105]。如果先把两侧合在一起算,均值变为 62.5,测试得到 [37.5,57.5]。即使没有读取测试标签,测试输入已经改变了训练流程。
scikit-learn 的建议是先切分,再只对训练数据 fit,对测试数据使用已学到的转换;交叉验证时也要在每折训练部分内拟合预处理。Pipeline 有助于守住这一步,但不能替你选择正确的分组或时间规则。常见陷阱:数据泄漏
这个均值反例只证明变换受到了污染,并没有证明某个模型的准确率一定升高。不要给未训练的模型编造分数。
运行后检查什么
下载完整 Python 实验,保存后运行:
python group-time-split.py
脚本已在 Python 3.14.0 运行,关键结果如下:
group_only: group_overlap=[], train_before_test=false
time_only: group_overlap=["A", "B"], train_before_test=true
training_labels_unavailable=["a2"]
features_unavailable_at_prediction=["c2"]
joint: train_ids=["a1", "b1"], test_ids=["c1", "d1"]
centering: train_mean=15, leaky_mean=62.5
calendar_distance_days=[3, 10]
PASS: split, availability, centering and row-gap invariants
它会断言错误方案被发现、最终规则通过、修改测试值不会影响训练均值,并拒绝空划分。运行范围是 Python 标准库的集合、时间条件与算术检查;没有调用 scikit-learn,也没有训练模型。
留给你的三项练习
- 将目标改成“老用户未来的记录”。明确哪些组交集应被允许,哪些时间检查仍须保留;写出理由和新测试。
- 给某用户添加第二个账号 ID。仅检查字符串交集还能发现同一实体跨集合吗?设计一个来源于业务事实的实体映射,并记录无法确定的记录。
- 把第 6 天以前的一条记录改成“标签第 20 天才能确定”,再把评分日改成第 9 天。分别记录训练与评分阶段应排除哪些记录,以及测试样本还剩多少。
来源与复核
2026 年 10 月 30 日复核以上三份 scikit-learn 官方文档,页面显示版本 1.9.1;独立发布日期未标明。本文的八条数据、日期协议、均值对照和断言为原创教学设计。继续学习时,可把检查接到概率校准的数据隔离之前,先确认基础预测来自合适的数据划分,再比较概率质量。