You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在R中按组欠采样解决分层数据的类别与特征不平衡问题

分层不平衡数据集的处理优化方案

背景与现有流程

我手头有一份分层数据,目标变量Season和核心分类预测变量Location的观测值存在严重不平衡。当前使用XGBoost算法,结合数值变量与Location预测Season,后续还要用SHapley Additive exPlanations(SHAP)解释模型输出。

由于温度、气压、日照的范围和波动高度依赖Location,此前仅平衡Season的欠采样无法解决Location嵌套带来的偏差问题。因此我先按Location对数值变量做标准化,再手动针对Location-Season分组执行欠采样,但手动方法重复运行过于繁琐,希望找到更高效的替代方案。

数据集构建代码

# 加载合适的包
library(tidyverse)

# 设置随机种子保证可复现性
set.seed(999)

# 创建嵌套且不平衡的数据集
df <- data.frame(
  Season = sample(as.factor(c("Dry", "Rainy")), replace = TRUE), times=c(90, 10),
  Location = rep(as.factor(c('A', 'B','C', 'D')), times=c(60, 10, 20, 10)), 
  Temperatures = round(rnorm(10000), digits = 2)*10,
  Pressure = round(rnorm(10000), digits = 2)*1000,
  Sunshine = round(rnorm(10000), digits = 2))

# 查看分组观测数
df %>% 
  group_by(Season, Location) %>% 
  tally()

按Location标准化数值变量的代码

# 提取Location A的观测值
df_A <- df %>%
  dplyr::filter(Location == "A") %>%
  dplyr::select_if(is.factor) 

# 标准化Location A的观测值
df_A_scaled <- df %>%
  dplyr::filter(Location == "A") %>%
  ungroup() %>%
  dplyr::select_if(is.numeric) %>%
  scale(center = TRUE, scale = TRUE) %>%
  bind_cols(df_A) %>%
  relocate(where(is.factor))

# 提取Location B的观测值
df_B <- df %>%
  dplyr::filter(Location == "B") %>%
  dplyr::select_if(is.factor) 

# 标准化Location B的观测值
df_B_scaled <- df %>%
  dplyr::filter(Location == "B") %>%
  ungroup() %>%
  dplyr::select_if(is.numeric) %>%
  scale(center = TRUE, scale = TRUE) %>%
  bind_cols(df_B) %>%
  relocate(where(is.factor))

# 提取Location C的观测值
df_C <- df %>%
  dplyr::filter(Location == "C") %>%
  dplyr::select_if(is.factor) 

# 标准化Location C的观测值
df_C_scaled <- df %>%
  dplyr::filter(Location == "C") %>%
  ungroup() %>%
  dplyr::select_if(is.numeric) %>%
  scale(center = TRUE, scale = TRUE) %>%
  bind_cols(df_C) %>%
  relocate(where(is.factor))

# 提取Location D的观测值
df_D <- df %>%
  dplyr::filter(Location == "D") %>%
  dplyr::select_if(is.factor) 

# 标准化Location D的观测值
df_D_scaled <- df %>%
  dplyr::filter(Location == "D") %>%
  ungroup() %>%
  dplyr::select_if(is.numeric) %>%
  scale(center = TRUE, scale = TRUE) %>%
  bind_cols(df_D) %>%
  relocate(where(is.factor))

# 合并之前创建的不同数据集的行
df <- bind_rows(df_A_scaled, df_B_scaled, df_C_scaled, df_D_scaled)

# 打印结果
print(df)

手动欠采样代码

# 提取按Location和Season分组后的最小观测数
minimum_obs_per_class <- df %>% 
  group_by(Location, Season) %>% 
  tally() %>%
  ungroup() %>%
  select(n) %>%
  summarise(min(n)) %>%
  as.numeric()

# 执行手动欠采样以重新平衡数据
df <- df %>% 
    group_by(Location, Season) %>%
    slice_sample(n = minimum_obs_per_class, replace = F)

# 检查结果
df %>% 
  group_by(Location, Season) %>%
  tally() %>%
  arrange(n)

替代方案推荐

1. 简化分组标准化代码

无需逐个编写每个Location的标准化逻辑,用group_by+mutate批量处理,一行代码完成所有Location的数值变量标准化:

df_scaled <- df %>%
  group_by(Location) %>%
  mutate(across(where(is.numeric), ~scale(.) %>% as.vector())) %>%
  ungroup()

2. 使用专业重采样工具包

(1) themis包(tidyverse友好)

themis是tidymodels生态下的重采样工具,支持分层/分组重采样,直接针对Location-Season分组做欠采样:

library(themis)

# 按Location分组,对每组内的Season类别做欠采样,统一到最小组规模
df_balanced <- df_scaled %>%
  group_by(Location) %>%
  step_downsample(Season) %>%
  ungroup()

如果需要更精细控制样本量,也可结合step_upsample(过采样)使用。

(2) caret包的分层重采样

caret支持创建分层抽样索引,适配嵌套不平衡场景:

library(caret)

# 创建分层抽样索引,按Location-Season组合分层
set.seed(999)
group_labels <- paste(df_scaled$Location, df_scaled$Season, sep = "_")
sample_ratio <- minimum_obs_per_class / max(table(group_labels))
index <- createDataPartition(group_labels, p = sample_ratio, list = FALSE)
df_balanced <- df_scaled[index, ]

3. 模型层面直接处理不平衡(无需提前重采样)

XGBoost本身支持通过参数调整应对不平衡数据,无需提前重采样,还能保留全部数据信息:

# 计算每个Location-Season组的样本权重,权重为总样本数/(分组数*组内样本数)
df_scaled <- df_scaled %>%
  group_by(Location, Season) %>%
  mutate(weight = nrow(df_scaled) / (n_distinct(paste(Location, Season)) * n())) %>%
  ungroup()

# 训练XGBoost时传入权重
library(xgboost)
xgb_matrix <- xgb.DMatrix(
  data = as.matrix(df_scaled %>% select(-Season, -Location, -weight)),
  label = as.integer(df_scaled$Season) - 1,
  weight = df_scaled$weight
)

xgb_model <- xgboost(
  data = xgb_matrix,
  objective = "binary:logistic",
  nrounds = 100,
  verbose = 0
)

这种方法适合后续SHAP解释,因为SHAP能直接反映样本权重对模型输出的影响。


内容的提问来源于stack exchange,提问作者Philippe Duteil

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.14 19:31:09