从 R 中数据帧的不同大小的子层中绘制大小相同的样本

Drawing equally-sized samples from differently-sized substrata of a dataframe in R

我有一个包含多个列的数据框,其中包含单词及其在句子中的位置。对于某些职位,行数比其他职位多。这是一个模拟示例:

df <- data.frame(
  word = sample(LETTERS, 100, replace = T),
  position = sample(1:5, 100, replace = T)
)
head(df)
  word position
1    K        1
2    R        5
3    J        2
4    Y        5
5    Z        5
6    U        4

显然,'position' 的各个部分大小不同:

table(df$position)
 1  2  3  4  5 
15 15 17 28 25

为了使不同的部分更容易比较,我想在一个数据帧内的变量 'position' 上绘制相同大小的样本。这在理论上可以分步完成,例如:

df_pos1 <- df[df$position==1,]
df_pos1_sample <- df_pos1[sample(1:nrow(df_pos1), 3),]

df_pos2 <- df[df$position==2,]
df_pos2_sample <- df_pos2[sample(1:nrow(df_pos2), 3),]

df_pos3 <- df[df$position==3,]
df_pos3_sample <- df_pos3[sample(1:nrow(df_pos3), 3),]

df_pos4 <- df[df$position==4,]
df_pos4_sample <- df_pos4[sample(1:nrow(df_pos4), 3),]

df_pos5 <- df[df$position==5,]
df_pos5_sample <- df_pos5[sample(1:nrow(df_pos5), 3),]

依此类推,最终将各个样本合并到一个数据帧中:

df_samples <- rbind(df_pos1_sample, df_pos2_sample, df_pos3_sample, df_pos4_sample, df_pos5_sample)

但是这个过程很麻烦而且容易出错。更经济的解决方案可能是 for 循环。到目前为止,我已经尝试过这段代码,但是,returns 不是每个位置值的单个样本的组合,而是从 'position':

的所有值中提取的单个样本
df_samples <-c()
for(i in unique(df$position)){
   df_samples <- rbind(df[sample(1:nrow(df[df$position==i,]), 3),])
}
df_samples
   word position
13    D        2
2     R        5
12    G        3
4     Y        5
16    Z        3
11    S        3
6     U        4
14    J        3
9     O        5
1     K        1

这段代码有什么问题,如何改进?

我们可以将 data.table 与行索引 .Isample 分组一起使用,并使用它来对数据集进行子集化。这会非常有效

i1 <- setDT(df)[, sample(.I, 3), position]$V1
df[i1]

或使用 sample_n 来自 tidyverse

library(tidyverse)
df %>% 
   group_by(position) %>% 
   sample_n(3)

或作为函数

f1 <- function(data) {
     data as.data.table(data)
     i1 <- data[, sample(.I, 3), by = position]$V1
     data[i1]
    }

考虑 by 将数据帧按 位置 拆分,并根据需要进行采样。然后 rbind 所有 dfs 在循环外与 do.call().

df_list <- by(df, df$position, function(sub) sub[sample(1:nrow(sub), 3),])

final_df <- do.call(rbind, df_list)

目前您在每次迭代中索引整个(非子集)数据框。此外,您在 for 循环中使用 rbind,这是内存密集型的,不建议这样做。

具体来说,

  • bytapply 的面向对象包装器,本质上是按因子将数据帧拆分为子集,并将每个子集传递给定义的函数。这里 sub 只是子集变量的名称(可以命名为任何名称)。这里的结果是一个数据框列表。
  • do.call 本质上运行跨多个元素的扩展调用的紧凑版本,其中 rbind(df1, df2, df3) 等同于 do.call(rbind, list(df1, df2, df3))。这里要注意的关键是 rbind 不是在循环内调用(避免在迭代内增加复杂对象(如数据框)的危险),而是在循环外调用 once

每次 运行 循环都会覆盖最后一个条目。尝试:

df_samples <- data.frame()
df_samples <- rbind(df_samples, df[sample(1:nrow(df[df$position==i,]), 3),])