训练集与测试集划分:避开数据泄漏与评估失真的实战指南

发布时间:2026/10/27 22:08:00
训练集与测试集划分:避开数据泄漏与评估失真的实战指南 做机器学习项目这些年我见过太多人把八成精力砸在调模型、调超参上结果模型在本地跑出漂亮指标一上线就拉胯。回头排查半天问题往往出在最不起眼的一步——训练集和测试集怎么划分。这个环节看起来就是调个库、切一刀的事但它直接决定了你评估出来的指标到底可不可信。划分得不对你后面所有的模型选择、阈值调整、特征取舍全都建立在错误的反馈上越努力偏得越远。这篇内容我想把“训练集和测试集划分”这件事彻底讲透。它适合刚入门、还在用默认参数切数据的同学也适合做了几个项目、但没系统想过划分策略背后逻辑的从业者。我会从划分的整体设计思路讲起拆解几种主流划分方式的适用场景和取舍理由再落到可直接复现的实操步骤和参数选择最后把我自己踩过的坑和排查经验整理出来。核心关键词训练集、测试集、数据划分、数据泄漏、分层抽样、交叉验证会自然贯穿全文你跟着读下来基本能把这一刀切得明明白白。1. 数据划分的整体设计与思路拆解1.1 为什么“切一刀”值得单独拿出来讲很多人对数据划分的理解停留在“把数据按 8:2 分一下”这个层面。这个理解不能算错但它只覆盖了最简单的一种情形。真实项目里数据划分要回答的问题远比这复杂你的数据有没有时间顺序类别是不是严重不平衡同一个用户/同一台设备是否产生了多条样本测试集要模拟的是“未来数据”还是“同分布随机数据”这些问题的答案不同划分方式就完全不同。我打个生活化的比方。假设你要考核一个学生的真实水平你手里有一本习题册。如果你把习题册里的题随机抽 20% 留作考试剩下的 80% 给他练习那这个考核是合理的——练习题和考题来自同一个题库难度分布一致。但如果你把习题册前 80 页给他练后 20 页拿来考而习题册恰好是按难度递增编排的那这个考核就失真了因为考题系统性偏难。数据划分也是同样的道理划分方式必须让测试集的分布尽可能贴近模型上线后真正会遇到的数据分布否则评估结果就是自欺欺人。这就是划分环节的核心设计原则我把它总结成一句话测试集不是用来“陪跑”的它是模型上线前的最后一道模拟考模拟得越真你上线时越有底。围绕这个原则后面所有的方案选型、参数设置才有依据。1.2 三种划分思路的取舍逻辑从大方向上看数据划分可以归为三类思路它们对应不同的数据特性和业务目标。第一类是随机划分也就是最常被提到的按比例随机切分。它的前提假设是样本之间相互独立、分布一致测试集和训练集来自同一个总体。绝大多数入门教程默认用这种方式因为实现简单、适用面广。但它的隐患在于一旦样本之间存在隐藏的关联比如同一用户的多条记录随机切分会让关联样本同时出现在训练集和测试集里造成数据泄漏指标虚高。第二类是分层划分在随机的基础上保证各个子集的类别比例与整体一致。分类任务里类别不平衡是常态比如正负样本 1:99 的风控场景纯随机切分很可能让测试集里正样本少得可怜评估结果波动极大。分层抽样能稳住这个比例让评估更可靠。第三类是按时间或按组划分适用于数据本身带有时序性或分组结构的情形。时序数据必须用“过去训练、未来测试”的方式切绝不能随机打乱否则等于用未来信息预测过去属于典型的数据泄漏。分组数据同一实体多条样本则要保证同一组的所有样本只出现在一个子集里避免组内信息串味。这三种思路没有绝对优劣关键看你的数据长什么样、业务要模拟什么场景。我在实际项目里的判断顺序通常是先看有没有时间字段和分组字段有就优先考虑时序或分组划分没有的话再看类别是否平衡不平衡就上分层都还好就用随机划分。这个判断链条后面我会展开讲具体怎么落地。1.3 训练集、验证集、测试集三者的分工严格来说一个规范的划分不止训练集和测试集两份中间还应该有一个验证集。三者的分工是这样的训练集用来拟合模型参数验证集用来做模型选择和超参调整测试集只在最后用一次给出对泛化能力的无偏估计。为什么要把验证集单独拎出来因为如果你反复用测试集来调超参测试集的信息就会一点点泄漏进模型选择过程你最终报告的那个指标就不再是无偏的了它会偏乐观。这就像考试前你偷偷看了考卷并针对性复习考出来的分数不能代表你的真实水平。验证集的存在就是为了给调参过程提供一个“可以反复看”的集合把测试集保护起来只在最终验收时用一次。实践中常见的比例是训练:验证:测试 6:2:2 或 7:1.5:1.5数据量小的时候验证集比例可以适当放大因为小数据下验证指标的方差很大需要更多样本才能稳住。数据量特别大的时候比如百万级以上测试集占比可以压到 5% 甚至更低因为绝对数量已经足够支撑稳定的评估。这个比例不是死规定核心是保证每个子集都有足够的样本量来支撑其承担的职责。2. 核心细节解析与实操要点2.1 随机划分的实现与随机种子的重要性随机划分是最基础的起点用现成的库一行代码就能完成。以常见的 Python 生态为例train_test_split是最常用的入口核心参数就几个测试集比例、随机种子、是否分层。看起来简单但每个参数背后都有讲究。随机种子random_state这个参数新手最容易忽略老手最不敢忽略。它的作用是固定随机数生成器的初始状态保证每次运行切出来的子集完全一致。为什么重要因为如果你不固定种子每次跑实验切出来的训练集和测试集都不一样模型指标一波动你根本分不清是模型改动带来的提升还是数据切分带来的噪声。我见过有人调了一下午超参指标忽高忽低最后发现是没固定种子白忙活。固定种子是可复现实验的最低要求这一点怎么强调都不过分。测试集比例的选择也有讲究。常见的 0.2 到 0.3 是经验值但真正该看的是测试集的绝对样本量。如果你的数据集只有几百条20% 的测试集只有几十条评估结果的置信区间会非常宽这时候要么扩大测试集比例要么改用交叉验证来充分利用数据。反过来数据量上到十万级10% 的测试集就有一万条足够稳定了没必要留太多把数据尽量留给训练。from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, # 测试集占比 20% random_state42, # 固定随机种子保证可复现 shuffleTrue # 切分前是否打乱时序数据必须设为 False )这段代码几乎是所有项目的起点但请注意shuffle参数。默认是 True也就是切分前先打乱顺序。对于独立同分布的数据这没问题但如果你的数据是按时间采集的打乱就等于把未来样本混进了训练集这是数据泄漏的重灾区。后面讲时序划分时会专门说这个坑。2.2 分层抽样类别不平衡场景的救命稻草分类任务里类别不平衡几乎是常态。假设你做一个欺诈检测正样本占比 2%总共一万条数据。如果纯随机切 20% 做测试集测试集里正样本的期望数量是 40 条但实际可能多几条少几条波动很大。更糟的是如果某次切分恰好正样本特别少你算出来的召回率、F1 会剧烈跳动根本没法用来做决策。分层抽样stratified sampling解决的就是这个问题。它的做法是在切分时保证训练集和测试集里各类别的比例与原始数据一致。上面那个例子里分层之后测试集的正样本比例会稳定在 2% 左右评估指标的方差大幅下降。实现上train_test_split提供了stratify参数传入标签列即可。X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy # 按标签 y 的分布分层 )这里有个细节值得说分层不仅对二分类有用多分类同样适用甚至回归任务在把目标值分箱之后也可以做分层保证目标值分布在各子集里一致。我做过一个房价预测的项目目标值长尾分布很严重直接随机切分导致测试集里高价房比例偏高模型评估偏差明显后来把价格分箱后做分层评估才稳定下来。注意分层的前提是每个类别都有足够的样本。如果某个类别只有一两个样本分层会直接报错因为无法在训练集和测试集里同时保证比例。这种情况下要么合并稀有类别要么改用其他策略。2.3 时序数据划分绝不能打乱顺序时序数据是数据划分里最容易翻车的地方没有之一。所谓时序数据就是样本之间有时间先后关系比如每天的销量、每分钟的传感器读数、按时间排列的交易记录。这类数据的核心特性是未来的数据依赖于过去但过去不依赖未来。如果你对时序数据用随机划分会发生什么训练集里混进了“未来”的样本模型在训练时已经见过未来的信息测试时自然表现很好但这个好是假的。上线之后模型只能看到历史数据去预测未来性能立刻打回原形。这就是时序场景下最典型的数据泄漏。正确的做法是按时间切分用较早的一段做训练较晚的一段做测试中间可以再切一段做验证。比如你有三年的日度数据可以前两年训练、第三年上半年验证、下半年测试。这样测试集模拟的正是“用历史预测未来”的真实场景。# 假设 df 已按时间升序排列 n len(df) train_end int(n * 0.7) val_end int(n * 0.85) train df.iloc[:train_end] val df.iloc[train_end:val_end] test df.iloc[val_end:]这段代码的关键在于数据必须先按时间排序然后按位置切片绝不能 shuffle。我踩过的一个坑是数据从数据库导出来的时候顺序是乱的我没检查就直接切结果训练集里混了后期的数据模型指标好得离谱上线后一塌糊涂。后来养成了习惯切分前第一件事就是按时间字段排序并打印首尾时间戳核对。还有一个进阶技巧叫滚动划分walk-forward适合数据量较大、需要多次评估的场景。做法是设定一个固定的训练窗口每次向后滑动一步用窗口内的数据训练、紧接着的一段测试多次滚动后把各次的评估结果平均。这种方式能更充分地利用数据也能观察模型在不同时间段的稳定性比单次切分更可靠。2.4 分组划分同一实体不能跨子集有些数据天然带有分组结构比如同一个用户的多条行为记录、同一台设备的多条日志、同一个病人的多次检查。这类数据如果随机切分同一个实体的样本会同时出现在训练集和测试集里模型可能记住了这个实体的特征测试时自然表现好但这不代表它能泛化到新实体上。举个具体例子。假设你做一个用户流失预测每个用户有多条行为记录。随机切分后用户 A 的三条记录可能两条在训练集、一条在测试集。模型在训练时已经见过用户 A 的行为模式测试时对用户 A 的那条记录预测得很准但上线后面对的是全新用户性能会明显下降。这就是分组泄漏。正确的做法是按实体分组划分保证同一个实体的所有样本只出现在一个子集里。实现上可以用GroupShuffleSplit传入分组标识列。from sklearn.model_selection import GroupShuffleSplit gss GroupShuffleSplit(n_splits1, test_size0.2, random_state42) train_idx, test_idx next(gss.split(X, y, groupsuser_ids)) X_train, X_test X.iloc[train_idx], X.iloc[test_idx] y_train, y_test y.iloc[train_idx], y.iloc[test_idx]判断要不要用分组划分有个简单的自检方法问自己“测试集里的样本其所属实体在训练集里出现过吗如果出现过这合理吗”如果答案是不合理那就必须分组。这个判断在推荐系统、风控、医疗等场景里尤其关键。2.5 交叉验证小数据时代的评估利器当数据量不大时单次划分的评估结果方差很大切得好指标高切得差指标低很难判断模型的真实水平。交叉验证cross-validation就是为这种场景设计的。最常用的是 K 折交叉验证把数据分成 K 份每次拿其中一份做验证、其余 K-1 份做训练重复 K 次最后把 K 次的评估结果平均。K 的取值通常是 5 或 10。5 折计算量适中偏差和方差的平衡较好10 折更充分但计算量翻倍。数据量特别小的时候可以用留一法LOOCV每次只留一个样本做验证但计算量随样本数线性增长样本上千就不实用了。from sklearn.model_selection import cross_val_score, StratifiedKFold cv StratifiedKFold(n_splits5, shuffleTrue, random_state42) scores cross_val_score(model, X, y, cvcv, scoringf1) print(fF1 均值: {scores.mean():.4f}, 标准差: {scores.std():.4f})注意这里用的是StratifiedKFold而不是普通的KFold原因和前面讲分层抽样一样——分类任务要保证每折的类别比例一致。另外交叉验证的结果要同时看均值和标准差均值反映平均水平标准差反映稳定性。如果标准差很大说明模型对数据切分敏感这时候光看均值会误导决策。交叉验证还有一个容易忽略的点它评估的是“模型配置”的好坏不是最终模型的好坏。你通过交叉验证选定了超参之后还应该用全部训练数据重新训练一个最终模型再用独立的测试集做最终验收。测试集在整个调参过程中都不能碰这一点前面已经强调过这里再提醒一次。3. 实操过程与核心环节实现3.1 从原始数据到可用子集的完整流程光讲理论不够我把一个完整的划分流程从头到尾走一遍你可以直接对照自己的项目复现。假设我们有一个二分类数据集带有时间字段、用户分组字段和类别标签这个设定覆盖了前面讲的多种情形便于演示判断逻辑。第一步是数据体检。切分之前先摸清数据的基本情况总样本量、类别分布、时间跨度、分组数量、缺失值情况。这一步很多人跳过但它决定了后面所有策略的选择。我通常会写一小段探查代码把关键统计量打印出来。import pandas as pd print(f总样本数: {len(df)}) print(f类别分布:\n{df[label].value_counts(normalizeTrue)}) print(f时间范围: {df[ts].min()} ~ {df[ts].max()}) print(f分组数量: {df[user_id].nunique()}) print(f每组平均样本数: {len(df) / df[user_id].nunique():.1f})第二步是确定划分策略。根据体检结果做判断有时间字段且业务需要模拟未来预测走时序划分有分组字段且同一实体多条样本走分组划分类别不平衡叠加分层以上都不涉及用随机划分。注意这些策略可以组合比如时序划分的同时也可以在每个时间段内做分层。第三步是执行切分并核对。切完之后一定要核对子集的统计特征确认分布符合预期。核对内容包括各子集的样本量、类别比例、时间范围是否重叠、分组是否交叉。这一步是防止低级错误的关键我后面会专门讲怎么核对。第四步是固化切分结果。把切分好的索引或子集保存下来后续所有实验都用同一份切分保证可比性。可以用 pickle 保存索引也可以直接把子集落成文件。这一步在团队协作里尤其重要避免每个人各切各的导致结果无法对比。3.2 参数选择背后的计算过程划分过程中涉及几个需要拍板的参数我把选择逻辑和计算过程摊开讲。测试集比例。前面说过要看绝对样本量。一个实用的经验是测试集至少要有几百条样本评估指标才相对稳定。假设你的数据有一万条测试集比例取 0.2 就是两千条足够如果只有一千条0.2 只有两百条偏少可以考虑 0.3 或者改用交叉验证。更严谨的做法是用置信区间来估算分类任务里评估指标的标准误大致与 1/√n 成正比n 是测试集样本量。测试集一千条时标准误约 3%三百条时约 6%你可以根据对精度的要求反推需要的样本量。K 折的 K 值。K 越大每次训练用的数据越多评估偏差越小但计算量越大且各折之间的相关性越强。5 折是通用默认值数据量小几百条可以用 10 折榨取更多信息数据量大十万级以上用 3 折甚至单次划分就够了因为大样本下单次评估已经足够稳定。随机种子的选取。种子本身取什么值无所谓42、0、2024 都行关键是整个项目统一。我习惯在项目配置里定义一个全局种子常量所有涉及随机的地方都引用它避免不同模块用了不同种子导致结果对不上。分层字段的选择。分类任务用标签分层是标准做法。但如果类别特别多比如上百类每类样本又少分层可能报错这时候可以退而求其次只对主要类别分层或者改用分组划分。回归任务可以把目标值分箱后分层箱数取 5 到 10 比较合适箱太多会导致每箱样本不足。3.3 切分结果的核对清单切分执行完别急着训练模型先花两分钟核对。我整理了一份核对清单每次切分后逐项过一遍能挡掉大部分低级错误。核对项检查方法异常表现样本量打印各子集长度某子集为空或远小于预期类别比例对比各子集与整体的标签分布比例偏差超过几个百分点时间范围打印各子集时间字段的最小最大值训练集时间晚于测试集分组交叉检查分组 ID 是否跨子集同一 ID 出现在多个子集特征分布对比各子集关键特征的均值方差分布明显偏移缺失值统计各子集缺失比例某子集缺失异常集中这张表里时间范围和分组交叉两项是时序和分组场景的必查项也是最容易出问题的地方。特征分布对比则是一个通用的健康检查如果训练集和测试集在某个关键特征上分布差异很大说明切分可能有问题或者数据本身存在漂移。3.4 把划分固化进项目流程划分不是一次性动作它应该固化进项目流程。我的做法是写一个独立的划分脚本输入原始数据输出切分好的子集文件和一份划分元信息记录种子、比例、策略、各子集样本量等。后续所有训练、评估脚本都读取这份固化结果不再各自切分。这样做的好处有三个一是可复现任何人拿到这份划分结果都能复现实验二是可对比不同模型、不同特征工程都在同一份切分上评估结果可比三是可追溯划分元信息记录了当时的决策依据过几个月回头看也知道为什么这么切。import json import pickle # 保存切分索引 with open(split_indices.pkl, wb) as f: pickle.dump({train: train_idx, test: test_idx}, f) # 保存划分元信息 meta { strategy: stratified_random, test_size: 0.2, random_state: 42, n_train: len(train_idx), n_test: len(test_idx), label_dist_train: y.iloc[train_idx].value_counts(normalizeTrue).to_dict(), label_dist_test: y.iloc[test_idx].value_counts(normalizeTrue).to_dict(), } with open(split_meta.json, w) as f: json.dump(meta, f, indent2, ensure_asciiFalse)这份元信息在团队协作和项目交接时价值极高尤其是当项目运行了几个月、原始数据又更新过之后你能清楚知道当初的划分是怎么做的避免重复踩坑。4. 常见问题与排查技巧实录4.1 指标虚高数据泄漏的典型症状与定位数据泄漏是划分环节最隐蔽也最致命的问题。它的典型症状是本地评估指标好得反常比如准确率 99%、AUC 0.99但上线后性能断崖式下跌。遇到这种情况第一反应就该怀疑数据泄漏。定位泄漏有一套系统的排查思路。先查时间泄漏训练集里有没有晚于测试集时间戳的样本如果有说明切分时没按时间排序或者 shuffle 了。再查分组泄漏同一个用户/设备/实体的样本是否跨了子集用分组字段做一次交集检查就能发现。然后查特征泄漏有没有特征是在目标发生之后才产生的比如预测用户是否流失却用了“流失后 30 天的登录次数”这种特征这种特征在训练时存在、上线时拿不到属于典型的未来信息泄漏。我踩过最隐蔽的一次泄漏是特征工程阶段引入的。当时做了一个滑动窗口统计特征窗口计算时不小心把当前样本之后的数据也算了进去导致每个样本的特征里都混入了未来信息。这种泄漏在划分环节查不出来得回到特征工程代码里逐行核对时间边界。所以排查泄漏不能只盯着划分要顺着数据流从原始数据一路查到模型输入。提示一个快速验证泄漏的方法是把模型换成一个极简单的基线比如逻辑回归甚至随机猜测如果复杂模型和简单模型的指标差距大得离谱往往说明有泄漏在起作用因为真实任务里复杂模型的提升通常是渐进的。4.2 类别不平衡导致的评估失真类别不平衡场景下即使划分正确评估指标本身也可能误导人。假设正样本占比 1%一个把所有样本都预测为负的“傻瓜模型”准确率高达 99%但这个模型毫无价值。这时候看准确率就是自欺欺人。解决办法有两个层面。划分层面用分层抽样稳住各子集的类别比例减少评估方差。评估层面换用对不平衡不敏感的指标比如 F1、AUC-ROC、AUC-PR、召回率等。特别是 AUC-PR在极度不平衡的场景下比 AUC-ROC 更能反映模型对少数类的识别能力因为 ROC 曲线在负样本极多时会被“稀释”看起来很美但实际没用。还有一个实操细节分层抽样时如果某个类别样本极少比如只有个位数分层会报错。这时候的处理方式是先把稀有类别合并成“其他类”或者干脆从数据里剔除如果业务上允许保证每个类别在训练集和测试集里都有足够样本。硬要分层只会让代码崩掉。4.3 小数据集的划分困境与应对数据量小是很多实际项目的常态几百条甚至几十条样本的情况并不少见。这种场景下单次划分的评估结果极不稳定切分方式稍微一变指标就大幅波动。应对策略有这么几条。首选是交叉验证把每一份数据都轮流当一次验证集充分利用有限样本。K 取 5 到 10配合分层保证每折类别比例一致。如果数据量实在太小比如几十条可以用留一法虽然计算量大但能榨干每一条数据的信息。其次是重复划分。单次随机划分做多次比如重复 10 次不同种子把各次结果平均也能降低方差。这种做法实现简单适合快速评估。最后是谨慎解读结果。小数据下的评估指标置信区间很宽报告结果时应该给出区间而不是单点值。比如“F1 为 0.7595% 置信区间 [0.68, 0.82]”比单纯说“F1 为 0.75”诚实得多也能让决策者理解不确定性。4.4 划分策略速查表把前面讲的判断逻辑整理成一张速查表遇到新项目时对照着走一遍基本不会选错策略。数据特征推荐策略关键参数主要风险独立同分布、类别平衡随机划分test_size, random_state样本量不足导致评估不稳类别不平衡分层随机划分stratify标签稀有类别样本过少报错带时间顺序按时间切分切分点位置未排序导致时间泄漏同一实体多条样本分组划分groups实体ID分组字段选错导致泄漏数据量小交叉验证n_splits, shuffle计算量大、方差仍偏高时序不平衡时序切分段内分层时间切分点分层字段实现复杂度上升这张表不是穷举实际项目里往往是多种特征的组合需要灵活叠加策略。比如时序数据里如果类别也不平衡可以在每个时间段内再做分层保证各时间段的类别比例一致。组合策略的实现会复杂一些但核心逻辑不变让测试集尽可能贴近上线后的真实分布。4.5 几个容易被忽略的实操心得最后分享几个我在实际项目里攒下来的心得都是文档里不会写、但踩过坑才明白的细节。第一个心得切分前先做数据去重。重复样本如果跨了训练集和测试集等于变相泄漏。我遇到过一次数据源合并时产生了大量重复记录随机切分后训练集和测试集里都有相同的样本模型指标虚高。后来在划分脚本里加了一步去重问题才解决。去重不一定要删但至少要保证重复样本只落在一个子集里。第二个心得测试集要“锁起来”。物理上把测试集单独存一个文件权限上限制访问流程上规定只有最终验收才能碰。人都是有惰性的测试集放在手边调参调不动的时候难免想“就看一眼”看一眼就收不住了。锁起来是最有效的自律手段。第三个心得划分策略要写进项目文档。别觉得这是小事项目交接时如果没写清楚当初怎么切的接手的人很可能用错误的方式重新切一遍之前所有实验结论都得推翻。我现在的习惯是在项目 README 里专门留一节讲数据划分把策略、参数、种子、各子集样本量都写清楚。第四个心得定期复查划分是否仍然合理。数据是会变的几个月前合理的划分策略数据分布漂移之后可能就不合理了。比如业务扩张后新增了大量新用户原来的分组划分可能让测试集里全是新用户评估结果不能代表整体。定期比如每个迭代周期重新体检数据、复查划分策略是保证评估持续可信的必要动作。这些心得听起来琐碎但每一条背后都是一次真实的翻车。数据划分这件事做对了不显眼做错了处处是坑。把上面这些逻辑和细节吃透你切出来的数据才真正撑得起后续的模型评估指标也才敢拿去汇报和上线。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询