
本文介绍一种高效、安全的分组下采样方法:按month分组后,在每个组内对target=0和target=1的样本分别随机抽样,使二者数量相等(取每组中较小类别的原始频数),从而实现月度层面的类别平衡。
本文介绍一种高效、安全的分组下采样方法:按month分组后,在每个组内对target=0和target=1的样本分别随机抽样,使二者数量相等(取每组中较小类别的原始频数),从而实现月度层面的类别平衡。
在构建时间序列分类模型(如逾期预测、用户流失预警)时,常需对不平衡数据进行下采样以缓解类别偏差,同时保留时间维度结构。本文解决的核心问题是:在保证每月(MONTH)独立性前提下,使每个自然月内 TARGET=0 与 TARGET=1 的样本数严格相等,且不破坏原始索引与其余特征列(如 other_column1、other_column2)的完整性。
关键挑战在于避免 SettingWithCopyWarning —— 原始代码中直接对 df[df['MONTH']==m] 切片调用 .drop(),本质是对视图(view)操作,而 Pandas 无法安全推断是否应修改原 DataFrame,故抛出警告且实际未生效。正确做法是避免链式赋值与就地修改切片,转而采用分组 + 向量化采样策略。
以下是推荐的三步稳健实现:
✅ 步骤 1:计算每组最小类别样本量
对每个 MONTH,统计 TARGET=0 和 TARGET=1 的数量,并取二者最小值作为该月每类应保留的样本数:
# 构建交叉表,按 MONTH 行聚合,取每行最小值(即 min(#0, #1)) min_counts = pd.crosstab(df['MONTH'], df['TARGET']).min(axis=1).reset_index(name='n_sample')
对示例数据,输出为:
MONTH n_sample 0 202207 1 1 202208 1 2 202209 2
✅ 步骤 2:关联最小样本量至原始数据
通过 merge 将 n_sample 映射到每行记录,确保后续分组时可访问该月配额:
df_with_quota = pd.merge(df, min_counts, on='MONTH', how='left')
✅ 步骤 3:按 (MONTH, TARGET) 分组并精确采样
使用 groupby(['MONTH', 'TARGET']) 确保每类独立抽样,apply 中调用 sample(n=x['n_sample'].iloc[0]) 实现“每组抽 n_sample 行”:
balanced_df = (df_with_quota
.groupby(['MONTH', 'TARGET'])
.apply(lambda x: x.sample(n=x['n_sample'].iloc[0], random_state=42))
.reset_index(drop=True))? 注意:random_state 参数保证结果可复现;若无需固定随机性,可省略。
✅ 验证结果
运行后检查交叉表:
pd.crosstab(balanced_df['MONTH'], balanced_df['TARGET'])
输出将严格满足目标结构:
TARGET 0 1 MONTH 202207 1 1 202208 1 1 202209 2 2
所有原始列(如 other_column1, other_column2)均完整保留,索引自动重置为连续整数(如需保留原始索引,可在 reset_index(drop=True) 中改为 reset_index() 并删除新索引列)。
⚠️ 注意事项
- 性能优化:对超大数据集(千万级行),可改用 sample(frac=...) 或 numpy.random.Generator.choice 加速;
- 重复索引处理:本方案天然兼容重复索引,因 merge 和 groupby 均基于值而非索引匹配;
- 边界情况:若某月 TARGET 全为 0 或全为 1,则 crosstab 对应行为 0,min() 返回 0 → 该月无样本被保留,建议前置校验 if (grp.sum(axis=1) == 0).any(): ...;
- 扩展性:该模式可轻松推广至多分类(TARGET ∈ {0,1,2})场景,只需将 min(1) 替换为 min(axis=1) 并调整分组逻辑。
此方法兼具可读性、健壮性与工程实用性,是时间分层类别平衡下采样的标准实践。

















