外婆煮了一大锅八宝粥。锅里漂着红豆、绿豆、花生、红枣……食材们没有均匀地沉在锅底,而是散落在各个角落。分粥的时候,如果拿起勺子随手舀,第一碗可能红豆扎堆,第二碗可能全是花生,第三碗里的红枣屈指可数。孩子们撅起嘴:“不公平!”外婆有她的规矩:先看清锅里每种食材大概占几成,再按这个比例,从每种食材里各舀一些,保证每碗粥的“配方”都和整锅相近。这,就是今天要讲的主角——分层抽样(Stratified Sampling)。在机器学习里,我们把数据集当成一锅粥,把不同类型的样本当成不同的食材。划分训练集和测试集时若随手一舀,稀有类别可能在测试集里“缺席”,模型评估的结果就会失真。分层抽样按类别比例抽取样本,让训练集和测试集都保持和全量数据一致的分布,评估结论才可靠。
学会分层抽样,你会在数据划分的第一步就避开“评估失真”的坑,让交给决策者的每个模型指标都经得起推敲——这是机器学习工程师的基本功,也是面试高频考点。
目录
可视化演示
展示分层抽样在各类别内保持比例的划分原理、随机抽样与分层抽样在不平衡数据上的分布差异、以及分层K折交叉验证的折划分过程。
本节配有交互动效(进入视口后自动加载,约 3KB)。
分层抽样核心原理 在每个类别层内独立抽样,保持原始类别比例在训练集和测试集中一致 P(y|D_tr)=P(y|D) 避免分布偏移导致评估失真
抽样困境:随手舀一勺有多危险
外婆煮了一大锅八宝粥,红豆最多,绿豆次之,红枣最少。分粥时,外婆让小明先舀。小明兴冲冲拿起勺子,随手一舀——碗里的红豆占了七成,好不容易熬好的红枣一颗都没捞着。小红在边上不乐意了:“红枣呢?这碗不公平!”其实小明没有偏心,只是“随机”的结果天然会偏心。
机器学习里的数据划分也一样。设想一个信用卡欺诈检测数据集,99% 是正常交易,1% 是欺诈。你用 train_test_split 随手一划,看似公平的随机抽样,完全可能让 1000 条测试样本里一条欺诈样本都没有。模型在那样的测试集上“准确率 99%”,实际上它根本没见过欺诈长什么样,评估自然失真。
随机抽样为什么会有这种风险?因为每个类别在样本中出现的次数是一个随机变量。假设总共有 N 个样本,其中 r 个是稀有类别,要抽取 n 个样本进测试集,稀有类全体缺席的概率可以用组合数算出来:
1. 组合数符号 C:从 N 个东西里选 n 个的选法总数,写作组合数。
2. 分子 C(N-r, n):所有被选中的样本都来自非稀有类(共有 N−r 个)时,能选出多少种测试集。
3. 分母 C(N, n):从全部 N 个样本里随便选 n 个,总共有多少种选法。
4. 两者相除:分子除以分母,就是“稀有类整类缺席”在随机抽样中发生的机会。
外婆后来立了条规矩:谁敢随手舀,谁就负责吃掉碗里那颗硌牙的花生。道理很简单——评估一个模型可不可靠,前提是测试集必须“见多识广”。随手一勺,总有概率让某个类别缺席,而一旦缺席,模型的成绩单就像缺了参考文献的论文,看似完整,实则不可信。
分层原理:先看清锅里有什么
外婆的解决之道并不复杂。她先拿大勺子把锅搅了几圈,然后眯着眼睛数了数:红豆大概六成,绿豆三成,红枣一成。于是她宣布:每碗粥都按六三一的配方来——红豆舀六勺的量,绿豆三勺,红枣一勺。这样分出来的每一碗,配方和整锅完全一致,谁也没话说。
分层抽样(Stratified Sampling)就是这个思路。它把“抽样”拆成两步:第一步,按类别把全部样本分组,每一组叫一个“层”(stratum);第二步,对每个层分别进行随机抽样,抽多少由该层在全量数据中的占比决定。层内是随机的,层间是定量的——既保留了随机性,又锁住了比例。
用数学语言说,假设全量数据有 N 个样本、共 K 个类别,第 j 类有 N_j 个样本;我们希望总共抽取 n 个样本进入测试集,那么第 j 类应抽取的数量就是:
1. 总体 N 与各类 N_j:总样本量等于各类样本量之和。
2. 类别占比 p_j:第 j 类在整体中所占的比例。
3. 按占比分配名额:抽取总量 n 乘以占比 p_j,就是第 j 类分到的样本数。
4. 名额守恒:所有类别分到的样本数加起来,恰好等于要抽取的总数。
外婆的“六三一配方”写在纸上贴在厨房,成了家规;分层抽样的“配方”则写进了划分代码。有了这层保护,每一碗粥端出去都是同样的配方,孩子们的抱怨自然就少了——训练集和测试集,也从此站在了同一条起跑线上。
实现方法:把配方写进代码
外婆的配方用嘴说就够,但机器学习工程师得把配方写进代码。不要慌,这不是要你手写一个复杂算法,scikit-learn 早就把“外婆的分粥秘方”封装好了。
train_test_split 函数里有一个 stratify 参数。把标签数组 y 传给它,函数就会先按 y 的取值把数据分组,再在各组内部“随手舀”出比例相同的训练集和测试集。一行代码,外婆的智慧就生效了。
如果非要看清这行代码背后的算术,其实也就三个数:设测试集占比为 t,第 j 类共有 N_j 个样本,则该类进入测试集的数量是 N_j × t 向下取整,剩下全部进训练集:
1. 测试集占比 t:t 是划分时指定的测试集比例,比如 0.2 表示 20%。
2. 第 j 类的测试集样本数:取该类样本数的 t 倍并向下取整。
3. 第 j 类的训练集样本数:该类剩下的样本全部进入训练集。
4. 完整性校验:两类样本数量相加等于该类的总数量,没有样本被浪费或重复。
外婆的配方是“六三一”,你的配方是“二八开”。一字之差,道理相通:分层不是玄学,而是把“公平”固化成一个可执行的规则。代码写好,数据划分就再也不会“出锅不匀”了。
适用场景:什么时候必须分层
外婆也不是每锅粥都这么较真。如果煮的是白米粥,每碗都一个样,随手舀完全没问题。但凡食材丰富、种类悬殊的八宝粥,分粥就必须按配方来。机器学习里也一样,分不分层,得看数据长什么样。
最典型的场景是类别不均衡的分类任务:欺诈检测、罕见病预测、垃圾邮件识别,正类可能只占 1%。此时随机划分就像“白米粥里挑红豆”,稀有类在测试集里朝不保夕。小样本数据集同样要分层——总共 100 条数据,测试集 20 条,某个只占 5% 的类别平均只能进测试集 1 条,随手一舀就没了。甚至回归任务也有分层变体:把连续目标值切成几个区间(分箱),再按区间分层。
用期望来衡量:设第 j 类占比为 p_j,测试集大小为 n,那么该类在测试集中的期望数量是 n·p_j,波动幅度由方差决定:
1. 期望数量:类别 j 在测试集中出现的平均次数,等于测试集大小乘以占比。
2. 波动方差:随机抽样下该数量不会乖乖等于期望,而是围绕期望上下波动。
3. 可靠性判据:当期望不足 1,说明类别 j 很可能整个从测试集消失。
外婆的直觉是“八宝粥必分,白粥不用分”——对应到工程上就是:类别越不均衡、样本量越小、评估结论越重要,分层就越不能省。省下这一步,省的是代码量,赔的是评估可靠性。
效果评估:分层让成绩单更可信
外婆分层分粥之后,孩子们不再抱怨了。但有人问:外婆你怎么知道这样分就真的公平?外婆说:你随便去检查每一碗,配方都对得上整锅,这就是证据。
机器学习评估也一样,分层带来的可验证的好处是:类别占比的“估计误差”被清零。随机抽样下,测试集里每个类别的占比都是一个随机数,忽高忽低;分层抽样直接把占比锁死在目标值上,不给你波动的机会。
数学上,随机抽样时类别占比估计值的方差为:
分层之后,这个方差在类别比例这一维度上变成 0——因为比例是“设计”出来的,不是“抽”出来的。
1. 单样本不确定度:p(1−p) 来自“一次抽样是否命中该类”的伯努利波动。
2. 有限总体校正:无放回抽样时样本量越大,波动越小,乘上这个小于 1 的校正因子。
3. 分层后的方差:类别比例被设计锁定,这一项化为零,评估指标不再因抽样而抖动。
当然,外婆也明白:配方一致不等于每一颗红枣一样甜——分层消除的是“类别比例”的偏差,特征层面的噪声依然存在。但至少,模型成绩单上的每个数字,都建立在“见多识广”的基础上,给决策者看的时候,腰杆硬了。
分层K折:轮流当评委也要守规矩
光分一次碗,外婆还是不放心。她想:谁说这一锅粥的配方一定好?得让每个孩子都来当一次评委。于是她定下规矩:把一锅粥分成 5 份,轮流让 1 份当“待审样品”,另外 4 份当“标准对照”,5 轮下来每个孩子都评过一次。这就是交叉验证的思路。
交叉验证里,数据被分成 K 份(fold),每份轮流做验证集,其余 K−1 份做训练集。但如果分 K 份时随手切,某些份里可能缺了红枣,轮到它当验证集时就又不公平了。于是外婆升级了配方:分 K 份时,每一份都按“六三一”来配。这就是分层K折(Stratified K-Fold)。
形式化地说,普通 K 折只保证份与份不相交、合起来是全集:
分层 K 折还额外要求每一折保持类别比例:
1. K 折划分:全集被拆成 K 个子集,拼起来正好是整锅数据。
2. 互不相交:任意两份没有重叠样本,保证每轮评委和对照都不“串味”。
3. 逐折保比例:每一折中第 j 类样本占比都等于它在全量中的占比。
外婆把“分粥一次”升级成“轮流评比”,从单次分层走向了分层 K 折。单次分层抽样是“一锤定音”,分层 K 折是“多方会审”——两者共用同一套配方,后者把可靠性再乘以 K 次。
实操陷阱:别让外婆的配方翻车
配方是好配方,可外婆也有失手的时候。比如,若锅里的红枣一共就两颗,她还想给五个孩子每人都分到红枣,那必然有人分不到。这不是配方的问题,是原料太少的问题。
分层抽样的第一个陷阱就是“层太小”。某个类别只有四五个样本,测试集又想占 20%,按比例算下来一层可能分到 0 个或全被划走,类别就在训练集或测试集中消失了。第二个陷阱是“标签不可信”:分层依赖的类别标签本身就是错的,那分层等于给错误分类建了档案,越分越歪。第三个陷阱是“泄露”:在划分训练测试集之前,就用全量数据做了归一化、填补缺失值等预处理,测试集的信息悄悄溜进了训练过程,分层再准也救不回来。
一个稳妥的检查标准是:每一个层,在训练集和测试集里都必须“至少留下一个样本”:
凡是打破这个条件的划分,都是在用一口缺料的锅硬熬八宝粥。
1. 下界:第 j 类在测试集中至少分到 1 个样本,否则该类在测试集中消失。
2. 上界:第 j 类在测试集中最多分到 N_j−1 个,否则训练集里就没有该类。
3. 逐类检查:K 个类别必须全部满足,一个都不能例外。
外婆后来在厨房贴了张便条:“红枣不够,就厚着脸皮跟隔壁再借两颗。”工程师们也有自己的借法:对极稀有的类别,用更小的验证集占比、分层K折,或者干脆把稀有类做上采样。总之,分层不是万能药,它只保证“能分到的食材按比例分”,食材本身就缺时,先补食材(数据),再谈公平(评估)。
AI 应用场景
场景 1:风控模型的“分粥师”
阶段:数据准备与模型评估
解决的问题:金融风控中欺诈样本占比常低于 1%。传统随机划分下,测试集中的欺诈样本数量随随机种子剧烈波动,模型召回率在 0.70~0.93 之间来回跳,业务方无法判断模型真实水平。用分层抽样把测试集的欺诈比例锚定在 1%,评估指标稳定,决策才能落地。
具体案例:某支付平台有 500 万条交易记录,其中欺诈 4.2 万条。划 20% 测试集时使用 stratify=y,测试集精确包含约 8400 条欺诈样本(与期望量一致),召回率从“每次重跑都不一样”变成“多次重跑几乎一致”,风控阈值终于敢拍板了。
场景 2:罕见病影像的小样本建模
阶段:模型调参与交叉验证
解决的问题:医院仅有 600 张眼底图像,其中罕见病 60 张(占 10%)。训练/验证/测试若随机划分,验证集可能恰好没有罕见病,模型调参就成了“盲人摸象”。用 StratifiedKFold 做 5 折交叉验证,每一折验证集都保留 10% 的罕见病比例。
具体案例:研究团队用 StratifiedKFold(5) 对 600 张影像调参,每折罕见病验证样本恰好 12 张,5 轮评估的 AUC 标准差从随机划分的 0.09 降到 0.02,最终向科室提交的报告有底气写明“95% 置信区间”。
Python 演示
import numpy as npfrom collections import Counter rng = np.random.default_rng(42) # 模拟 1000 个样本, 3 个类别, 比例 70:20:10 (类别不平衡)y = np.array([0] * 700 + [1] * 200 + [2] * 100)rng.shuffle(y) def random_split(y, test_ratio=0.3, seed=1): """随机抽样: 从头到尾都没有考虑类别比例""" rng2 = np.random.default_rng(seed) idx = rng2.permutation(len(y)) cut = int(len(y) * test_ratio) return idx[:cut], idx[cut:] def stratified_split(y, test_ratio=0.3, seed=1): """分层抽样: 按类别分组, 组内随机抽取同样比例""" rng2 = np.random.default_rng(seed) test_idx, train_idx = [], [] for cls in np.unique(y): cls_idx = np.where(y == cls)[0] rng2.shuffle(cls_idx) cut = int(len(cls_idx) * test_ratio) test_idx.extend(cls_idx[:cut]) train_idx.extend(cls_idx[cut:]) return np.array(test_idx), np.array(train_idx) def show(tag, idx, y): counts = dict(Counter(y[idx])) print("%s: %s" % (tag, {k: counts[k] for k in sorted(counts)})) print("全量数据类别分布:", dict(Counter(y)))print("--- 随机抽样 ---")for seed in [0, 1, 2]: test_idx, _ = random_split(y, seed=seed) show("随机测试集(seed=%d)" % seed, test_idx, y)print("--- 分层抽样 ---")for seed in [0, 1, 2]: test_idx, _ = stratified_split(y, seed=seed) show("分层测试集(seed=%d)" % seed, test_idx, y)
y = np.array([0] * 700 + [1] * 200 + [2] * 100)构造一个 7:2:1 的不平衡标签序列,模拟八宝粥里三种食材的份额。idx = rng2.permutation(len(y))随机打乱所有样本的序号,这就是“随手舀一勺”的随机动作。cls_idx = np.where(y == cls)[0]找出第 cls 类样本的下标,相当于外婆先把食材按种类分好堆。rng2.shuffle(cls_idx)在每一类内部再洗一次牌,保证组内随机,不引入顺序偏差。cut = int(len(cls_idx) * test_ratio)每类都切出同样的 test_ratio 比例,这是“六三一配方”的代码化身。train_idx.extend(cls_idx[cut:])该类剩下的样本进训练集,保证每类在两侧都留有代表。counts = dict(Counter(y[idx]))统计测试集里的实际类别数量,用来验证分层后比例是否精确锁定。
常见误区
误区stratify=y 不就是把数据先 shuffle 再切分吗?搞不懂它和随机抽样有什么区别,反正最后都混在一起。
正解打乱是 shuffle 做的事,stratify 做的是另一件事:先按类别分组,再按比例从各组抽取。随机划分只打乱全局顺序,cut 一刀之后各类占比完全看运气;分层则保证每组都切出相同比例。两者可以同时用,但作用完全不同——一个是“搅匀”,一个是“按配方分配”。
为何常见:因为 train_test_split 默认会 shuffle,很多人把“打乱”和“分层”两个动作混为一谈。
误区反正样本有百万条,随机抽样自然趋近真实比例,分不分层无所谓,还省得写参数。
正解大样本下随机抽样的比例确实会“趋近”真实比例,但趋近不等于精确,而且对占比极小的类别(如 0.01%),百万级数据里该类也只有几百条,随机划分的绝对数量依然可能显著偏离。分层成本极低(只多写一个参数),收益是“精确保证”,没有理由放弃。
为何常见:大数定律告诉人们大样本下随机趋于稳定,却忽略了稀有类在很小绝对量下的脆弱性。
误区stratify 只能传分类标签,回归的目标是连续数值,没法分组,所以回归任务不能用分层。
正解连续值确实不能直接分组,但可以先把目标变量按分位数切成若干区间(桶),再把“桶号”当标签传给 stratify。这叫分箱分层,能保证不同取值区间的样本在训练集和测试集中比例一致,让回归评估同样受益。
为何常见:官方文档和教程的示例几乎都是分类问题,容易让人误以为分层只适用于分类。
误区StratifiedKFold 每一折都是在全量数据里随机抽出来的,所以它就是一种随机 K 折。
正解StratifiedKFold 与 KFold 的区别恰恰在于“不随机”——它先按类别分组,再在每组内轮流分配出 K 份,逐折保证类别比例一致。KFold 是纯随机切分,StratifiedKFold 是对每类分别做 K 折后再合并,两套逻辑完全不同。
为何常见:两者 API 用法相似、名字又接近,初学者极容易把“每折比例一致”误当成“随机抽的”。
误区只要用了分层抽样,模型评估就一定准确,不可能出现过拟合或评估虚高的问题。
正解分层只负责“类别比例的公平”,不负责特征分布的公平,也不过问模型是否过拟合。如果划分前用全量数据做过归一化、填补缺失值等预处理,信息泄露照样让评估虚高;层内特征也仍存在随机噪声。分层是必要不充分条件,不是万金油。
为何常见:把“按类别比例抽样”误当成“统计严谨性”的充分条件,忽视了数据预处理阶段的泄露风险。
面试题
Q1. 分层抽样的核心目标是什么?
- A.让抽取的样本总量尽可能大
- B.保持样本与总体的类别比例一致← 正确答案
- C.让抽样过程的随机性更强
- D.让样本的特征维度变得更低
答案:B。分层抽样按类别分组并按总体比例分配名额,锁住的是类别比例,不是样本量、随机性或者维度。A/C/D 都是抽样的其他属性,只有 B 是分层抽样的设计目标。
Q2. 下列哪种场景最应该使用分层抽样?
- A.类别极不均衡的分类任务← 正确答案
- B.特征全为数值型的回归任务
- C.数据量充裕且类别分布均衡
- D.样本之间完全相互独立
答案:A。类别极不均衡时随机划分容易让稀有类在测试集中缺席,分层抽样能精确保住比例;B 虽然可以通过分箱分层,但不是“最应该”的典型场景;均衡大数据下分层收益小;样本独立性与分层无直接因果。
Q3. train_test_split 中启用分层抽样应传入哪个参数?
- A.固定随机种子的参数
- B.按类别比例划分的参数← 正确答案
- C.事先打乱数据顺序的参数
- D.规定测试集大小的参数
答案:B。“按类别比例划分的参数”指的就是 stratify,把标签数组传给它即按类别分层。random_state 是固定随机种子,shuffle 控制打乱,test_size 规定测试集比例,三者都不是分层开关。
Q4. 分层抽样中第 j 类应抽取的样本量公式是?
- A.n_j = N_j(把该类全部抽取)
- B.n_j = n·N_j/N(按总体占比抽取)← 正确答案
- C.n_j = n·N/N_j(按占比倒数抽取)
- D.n_j = N/n(各类平均分配)
答案:B。分层抽样的名额按类别在总体中的占比分配:n·(N_j/N)。A 把整个类都抽走,D 把各种类均匀化,C 的倒数关系刻意放大稀有类,都与“保持原始比例”相悖。
Q5. 某二分类数据中正类占 99%,负类占 1%,测试集 1000 条。随机抽样时,负类样本数量的期望最接近?
- A.大约 0 条,很可能完全缺席
- B.大约 10 条,但会随机波动← 正确答案
- C.大约 1 条,数量严重不足
- D.大约 99 条,基本符合比例
答案:B。期望 = 1000×1% = 10 条。A 夸大了缺失风险,C 少算了一个数量级,D 把正类比例安到了负类头上。随机抽样的期望是 10,但方差会带来波动,这正是分层抽样要压平的抖动。
Q6. 分层K折与普通K折的主要区别是什么?
- A.每一折都保留原始类别比例← 正确答案
- B.把交叉验证的折数加倍
- C.抽样时全部采用有放回
- D.只适用于回归类任务
答案:A。分层K折在切分每一折时都先按类别分组并等比分配,使每折类别占比与全量一致;折数由 K 决定不会翻倍;交叉验证一般是无放回;分层K折适用于分类任务,D 也错。
Q7. 关于“分层抽样消除测试集分布偏移”的说法,正确的理解是?
- A.它同时消除了训练集里的所有随机噪声
- B.它只锁定类别比例,特征偏差仍需注意← 正确答案
- C.它保证模型在测试集上一定过拟合
- D.它只对回归这一种任务有效果
答案:B。分层只控制“类别比例”这一维度,层内特征仍是随机抽取的,可能还有其他偏差;A 把分层的作用夸大到全部噪声,C 与事实相反,D 忽略了分类任务中的广泛应用。
Q8. 以下哪些情况适合使用分层抽样?(多选)
- A.小样本分类任务的数据划分← 正确答案
- B.类别占比极不均衡的欺诈检测← 正确答案
- C.在线学习中的实时数据流抽样
- D.需要测试集分布与总体一致的评估← 正确答案
答案:ABD。A 小样本下随机划分风险大,分层能保底;B 是典型的不均衡场景;D 是分层抽样的定义性目标。C 在线实时流无法预先知道全部类别分布,分层不适用。
Q9. 关于分层抽样与分层K折的关系,下列说法正确的有?(多选)
- A.分层抽样用于单次训练测试集划分← 正确答案
- B.分层K折是多次划分并轮流做验证集← 正确答案
- C.两者都要求保持类别比例← 正确答案
- D.分层K折不需要知道类别标签
答案:ABC。分层抽样是“一锤定音”的划分,分层K折把同样原则重复 K 次形成交叉验证,二者都以类别比例为约束;D 错误——两者都必须知道类别标签才能分组分层。
Q10. 下列哪些情况可能导致分层抽样失效?(多选)
- A.某类只有一两个样本且全被划进同一侧← 正确答案
- B.特征归一化时使用了全量数据的统计量← 正确答案
- C.类别标签存在大量标注错误← 正确答案
- D.数据量很大且类别分布均衡
答案:ABC。A 违反了“每层在训练和测试两侧都至少留一个”的边界条件;B 属于数据泄露,分层管不了泄露;C 标签错了,分层等于按错误档案分组。D 是分层表现良好的场景,不是失效条件。