训练完一个分类模型,验证集准确率95%,满怀期待地上线,结果实际效果只有70%多——这种落差很多工程师都遇到过。排查半天,模型、超参数都没问题,最后发现根源竟然是最不起眼的一步:数据划分。如果训练集和测试集的类别比例不一致,或者某个关键特征的分布在两个集合中差异悬殊,评估结果就天然带着偏差。本文围绕两个核心手段展开:用分层抽样保证划分时类别比例不失真,用分布一致性检查验证划分后的数据集是否真正可比。

一、数据划分为什么会引入评估偏差
先从原理上说清楚问题的来源。机器学习有一个基本假设:训练数据和测试数据来自同一个分布,也就是所谓的独立同分布假设。一旦这个假设被打破,模型在训练集上学到的规律就不能顺利迁移到测试集上,评估指标自然失真。
最常见的失真场景是类别不平衡问题。假设一个二分类数据集里正样本只占3%,你随机切分数据,测试集里正样本可能只占1%,也可能占6%。如果测试集正样本偏多,模型只要倾向于预测正类,准确率就会虚高;反之如果正样本几乎没被分进测试集,召回率的估计会严重偏低。小样本类别在这种随机划分下波动尤其剧烈,一次划分的结果可能完全是运气。
另一个容易被忽视的场景是时间序列数据。如果不做任何处理直接随机打乱划分,训练集里会混入时间上晚于测试集的样本,相当于用未来预测过去,评估结果会异常漂亮,但这在真实业务里根本不可能发生。此外,按患者、按用户分组的数据如果被随机拆开,同一个人的多条记录分别落入训练集和测试集,会造成信息泄漏,评估指标同样偏高。所以在讨论具体方法之前,先想清楚数据本身的性质,比套用任何划分技巧都重要。
二、分层抽样:让类别比例在划分后保持一致
分层抽样的思路很直接:划分时不做纯随机,而是按类别标签分层,在每个类别内部按比例抽取样本。比如正样本占3%,那么训练集和测试集中的正样本比例都保持3%左右,从根源上消除类别比例波动带来的评估方差。
在sklearn中,最常用的入口是train_test_split的stratify参数,直接传入标签即可:
from sklearn.model_selection import train_test_split
import numpy as np
X = np.random.rand(1000, 10)
y = np.array([1] * 30 + [0] * 970) # 正样本仅占3%
# stratify=y 表示按标签分层抽样
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, stratify=y, random_state=42
)
print(np.mean(y_train)) # 约0.03
print(np.mean(y_test)) # 约0.03,与整体比例一致
交叉验证场景下应该使用StratifiedKFold替代普通的KFold。它保证每一折中各类别的比例与整体数据一致,特别适合不平衡数据。如果还涉及按组划分的需求,可以关注StratifiedGroupKFold,它同时兼顾分层和组不可拆分两个约束:
from sklearn.model_selection import StratifiedKFold
import numpy as np
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
for train_idx, val_idx in skf.split(X, y):
X_tr, X_val = X[train_idx], X[val_idx]
y_tr, y_val = y[train_idx], y[val_idx]
print("验证集正样本比例:", y_val.mean())
需要提醒的是,分层抽样解决的是标签维度的比例问题,它并不保证特征维度的分布一致。也就是说,用了stratify之后类别比例没问题了,但某个连续特征在训练集和测试集里可能依然分布不同。这时候就需要下一节的分布一致性检查登场。另外,当某个类别的样本数少于折数时,StratifiedKFold会直接报错,这本身也是一个信号:某些极端稀有类需要专门的处理策略,比如合并类别或采用重复采样。
三、分布一致性检查:验证划分是否真的可靠
分层抽样做完之后,还应该主动验证一下划分结果。检查可以分为标签层面和特征层面两个部分。标签层面很简单,直接对比训练集和测试集的类别计数即可:
import pandas as pd from collections import Counter train_dist = pd.Series(Counter(y_train), name="train").sort_index() test_dist = pd.Series(Counter(y_test), name="test").sort_index() compare = pd.concat([train_dist, test_dist], axis=1) compare["train_ratio"] = compare["train"] / compare["train"].sum() compare["test_ratio"] = compare["test"] / compare["test"].sum() print(compare)
特征层面则可以借助统计检验。对于连续特征,KS检验(Kolmogorov-Smirnov)是比较两个样本分布是否相同的经典方法,零假设是两者来自同一分布,p值大于0.05通常可以认为没有显著差异:
from scipy.stats import ks_2samp
feature_train = X_train[:, 0]
feature_test = X_test[:, 0]
stat, p_value = ks_2samp(feature_train, feature_test)
print(f"KS统计量: {stat:.4f}, p值: {p_value:.4f}")
if p_value < 0.05:
print("该特征在训练集与测试集上的分布存在显著差异,需警惕")
对于类别特征,可以用卡方检验或者直接对比频率表。除了统计检验,可视化也是最直观的手段:把连续特征在两个集合上的直方图、KDE曲线叠在一起画,肉眼就能发现明显的偏移。需要注意的是,当特征数量很多时,即使两个集合完全同分布,也会有少量特征偶然通过显著性检验,这属于多重检验问题。工程上不必对所有特征逐一较真,重点关注重要性靠前的特征即可,或者用Bonferroni校正控制整体错误率。
四、工程实践建议与常见陷阱
把上面的方法落到日常流程里,可以总结为几条经验。第一,划分代码固定随机种子,保证实验可复现,同时换几个种子跑划分,观察指标波动,波动大说明评估本身就不可靠。第二,凡是类别不平衡场景,默认开启stratify,这几乎没有任何副作用。第三,划分完成后跑一次分布一致性检查脚本,把它当作流水线中的常规校验步骤,而不是等出了问题再回头查。
还有一些常见陷阱值得单独列出:时间序列数据不要用随机划分,应该用TimeSeriesSplit或按时间点切分;带分组的数据要用GroupKFold防止泄漏;分层抽样只能保证标签比例,特征漂移要靠分布检查兜底;如果训练集和测试集来自不同时间或不同渠道,分布不一致可能不是划分造成的,而是数据本身的协变量偏移,这时需要考虑样本加权或领域自适应方法,单纯重新划分解决不了问题。
最后强调一点,评估偏差往往不是单一环节造成的,数据划分只是其中最容易被忽视的一环。把分层抽样和分布一致性检查纳入标准流程,配合交叉验证和多种子实验,才能得到一个真正可信的模型评估结论,避免被虚高的离线指标误导。
分层抽样数据划分训练集与测试集分布一致性修改时间:2026-09-04 16:54:46