分层抽样需先明确分层变量和层权重,按比例从各层随机抽取;必须用groupby().apply()控制每层样本量,避免直接sample()或groupby().sample();注意处理空值、离散化连续变量、小层保底及权重与频次区别。

分层抽样前必须明确分层变量和层权重
分层抽样的核心不是“随机选”,而是“按比例从各层里随机选”。如果你直接对整个 DataFrame 调用 sample(),哪怕设了 frac,也得不到分层效果。必须先用 groupby() 拆出层,再控制每层抽取数量——这个数量要么按层内原始占比计算,要么按你指定的权重分配。
常见错误是把分类变量当字符串处理却忽略空值或不一致编码(比如 "Unknown" 和 NaN 混用),导致某层被跳过。建议先跑一遍 df[<code>stratum_col].value_counts(dropna=False) 看真实分布。
- 层变量必须是离散型:类别型(
category)、字符串或整数枚举,不能是连续数值列(如年龄)直接作层——需先pd.cut()或pd.qcut()分箱 - 若要求“每层至少 1 个样本”,别用
frac,改用n并配合min(实际层大小, 目标数)防止ValueError: Cannot take a larger sample than population when 'replace=False' - 权重不等于频次:若业务要求“城市层占 60%、农村占 40%”,而原始数据中城市只占 30%,就得显式计算目标样本量:
target_n_per_layer = (total_sample_size * np.array([0.6, 0.4])).astype(int)
用 groupby().apply() 实现可控分层抽样
这是最灵活、最不易出错的方式。它绕过了 sample(n=...) 在 groupby 后无法跨层统一分配的限制,允许你为每组单独指定抽样逻辑。
import pandas as pd
import numpy as np
<h1>假设 df 有列 'region'(取值为 'urban', 'rural')和 'income'</h1><p>strata = ['urban', 'rural']
target_sizes = [120, 80] # 显式指定每层要抽多少</p><p>sampled = (df.groupby('region', group_keys=False)
.apply(lambda g: g.sample(n=target_sizes[strata.index(g.name)],
replace=False,
random_state=42)))
</p>
注意 group_keys=False,否则返回结果多一列分组名索引;replace=False 是默认值,但显式写出更安全——尤其当某层样本量小于目标时,不写会静默失败或报错。
- 如果某层原始行数不足目标数,
sample(..., replace=False)会直接抛ValueError;加try/except或提前过滤小层,或改用replace=True(但会引入重复记录,需评估偏差) - 避免在
apply里调用np.random.choice或手动索引,pandas 的sample()内置了更稳定的随机引擎和 NaN 处理 - 不要用
df.groupby(...).sample(...)(pandas ≥1.1 新接口)——它只支持统一n或frac,无法为每层设不同数量
处理不均衡层时的平滑策略
当某些层极小(比如只有 3 条记录),而你又必须包含它们时,硬性要求“每层抽 50 个”显然不可行。这时需要降级策略:要么接受该层全量入选(即 oversampling),要么按最小层大小缩放所有层目标量(即 proportional scaling)。
推荐用“最小层保底 + 其余层按比例”:
# 计算各层原始大小
layer_sizes = df.groupby('region').size()
min_size = layer_sizes.min()
<h1>设定最小采样数(如 5),超过则按比例放大</h1><p>base_n = 5
scaled_n = (base_n / min_size * layer_sizes).round().astype(int)</p><p>sampled = df.groupby('region', group_keys=False).apply(
lambda g: g.sample(n=min(scaled_n[g.name], len(g)),
replace=False,
random_state=42)
)
</p>
- 替换抽样(
replace=True)仅在业务允许重复观测时使用;金融风控或用户行为分析中通常禁止,因会扭曲事件概率 - 若层变量含高基数类别(如 500 个省市),抽样后可能仍存在稀疏层(某省只有 1 行被抽中),此时应考虑合并小层(如“其他省市”)或改用分层聚类抽样
-
random_state必须全局一致,否则不同层抽样独立,整体不可复现
验证抽样结果是否真满足分层约束
抽完立刻检查,别等到建模才发现分布偏了。重点核对两件事:各层样本数是否匹配预期、层内分布是否与原始数据一致(比如某层内性别比突变,说明该层内部还有隐藏结构没分层)。
一行命令快速比对:
# 原始层分布
print("Original:", df['region'].value_counts(normalize=True).round(3))
# 抽样后层分布
print("Sampled: ", sampled['region'].value_counts(normalize=True).round(3))
- 如果用了
frac而非n,务必确认normalize=True下的结果是否接近你设定的权重——浮点舍入可能导致某层少 1 行,属正常现象 - 抽样后若要做模型训练,记得检查特征在各层内的缺失率是否一致;曾有案例显示:某层中
income缺失率达 90%,而其他层仅 5%,这说明分层变量与缺失机制强相关,需补充分层或插补策略 - 时间序列数据慎用纯随机分层抽样——同一用户的多条记录可能被拆到训练/测试集,造成数据泄露;此时需按用户 ID 分层,而非按单条记录的某个字段
分层抽样真正的难点不在代码实现,而在于分层变量的选择是否捕捉到了影响目标变量的关键异质性。写十行代码能跑通,但选错层变量会让后续所有分析失去意义。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











