高效地对不等大小的组进行递归随机抽样

3
这个问题是我之前有关递归随机抽样的问题的后续问题。在Efficient recursive random sampling中,该线程中的解决方案仅在组大小相同时或需要每组固定数量的样本时有效。然而,现在假设数据集如下;
   ID1 ID2
1    A   1
2    A   6
3    B   1
4    B   2
5    B   3
6    C   4
7    C   5
8    C   6
9    D   6
10   D   7
11   D   8
12   D   9

我们希望对每个ID1随机采样最多n个ID2,并进行递归操作。这里的递归意味着我们从第一个ID1移到最后一个ID1,如果对于一个ID1已经抽取了一个ID2,则不应将其用于后续ID1。 假设n=2,则预期结果如下:

ID1 ID2
1    A   1
2    A   6
4    B   2
5    B   3
6    C   4
7    C   5
11   D   8
12   D   9
  • 对于ID1 =“A”,恰好有两个潜在的ID2可供选择,因此都被选中。
  • 对于ID1 =“B”,还剩下两个潜在的ID2可供选择,所以都被选中。
  • 对于ID1 =“C”,还剩下两个潜在的ID2可供选择,所以都被选中。
  • 对于ID =“D”,还剩下三个潜在的ID2可供抽样,因此从中随机选择两个。

除了示例中显示的情况外,还可能发生什么:

  • 每个ID1总是有一定数量的可用ID2,但可能所有这些ID2都已经被使用。在这种情况下,应该简单地略过ID1。
  • 可能没有任何ID1将具有指定数量的ID2。在这种情况下,应检索最接近指定n的n。
  • ID不必是seq(ID1)
  • ID2也可以是类似于ID1的字符向量。

示例df;

df <- structure(list(ID1 = c("A", "A", "B", "B", "B", "C", "C", "C", 
"D", "D", "D", "D"), ID2 = c(1, 6, 1, 2, 3, 4, 5, 6, 6, 7, 8, 
9)), class = "data.frame", row.names = c(NA, -12L))

1
ID2是否总是seq(ID1),还是可以只有1, 6这样的值? - Onyambu
@Onyambu 它可以是任何数字。此外,理论上ID2甚至可以是一个字符串。 - tmfmnk
1
仅仅是说一句,你给出的例子可能会引导错误方向。如果可能,请更改一些ID2,使其没有序列。例如,我的第一个想法是删除所有大小为2的组,因为它们不能作为A已经被采样并且大小为2的样本。你知道我是什么意思吗?例如,如果E大小为2,我们就无法从中进行采样。这个想法是错误的,因为E可以有ID2=10、20。所以请尝试使数据更有趣。希望这能帮到你。 - Onyambu
它必须是一个 data.frame 吗? - Donald Seinen
@Onyambu感谢您的评论,我已经相应地进行了更新。我也同意,这实际上是随机抽样和选择所有可用值的混合,具体取决于组大小。 - tmfmnk
@Donald Seinen,该输入始终为数据框,输出也可以是任何可以合理地转换回数据框的东西。 - tmfmnk
3个回答

1
以下函数似乎可以得到您想要的结果。基本上,它循环遍历每个 ID1 组并选择相应的 ID2 尚未被抽样的行。然后选择不同的行(在某些 ID1 组具有重复的 ID2 值的情况下)。样本大小将是 n 或该组行数中较小的值。
sample <- function(df, n) {
  `%notin%` <- Negate(`%in%`)
  groups <- unique(df$ID1)
  out <- data.frame(ID1 = character(), ID2 = character())
  
  for (group in groups) {
    options <- df %>%
      filter(ID1 == group,
             ID2 %notin% out$ID2)
    
    chosen <- sample_n(options,
                       size = min(n, nrow(options))) %>%
      distinct()
    
    out <- rbind(out, chosen)
  }
  
  out
}

set.seed(123)
sample(df, 2)

  ID1 ID2
1   A   1
2   A   6
3   B   2
4   B   3
5   C   4
6   C   5
7   D   8
8   D   9

一组ID1已经用完了ID2的情况:

输入:

# A tibble: 10 × 2
   ID1     ID2
   <chr> <dbl>
 1 A         1
 2 A         3
 3 B         1
 4 B         3
 5 C         5
 6 C         6
 7 C         7
 8 C         7
 9 D        10
10 D        20

输出:

sample(df2, 2)
# A tibble: 6 × 2
  ID1     ID2
  <chr> <dbl>
1 A         3
2 A         1
3 C         6
4 C         7
5 D        20
6 D        10

这看起来非常有前途,非常感谢 :) 我会保持开放状态,以便我也可以授予赏金。 - tmfmnk

0

我不知道是否过于简单化了这个问题。请看以下内容,看看它是否适用于你的情况:

library(tidyverse)


df %>%
  group_split(ID1)%>%
  reduce(~ bind_rows(.x, .y) %>%
           filter(!duplicated(ID2))%>%
           group_by(ID1)%>%
           slice_sample(n=2) %>%
           ungroup, 
         .init = slice_sample(.[[1]], n=2))

# A tibble: 8 x 2
  ID1     ID2
  <chr> <dbl>
1 A         1
2 A         6
3 B         2
4 B         3
5 C         4
6 C         5
7 D         9
8 D         8

免责声明:未矢量化,因此效率低下


0

这里是一个使用动态规划(DP)的基本R选项

d <- table(df)
nms <- dimnames(d)
res <- list()
for (i in nms$ID1) {
  idx <- which(d[i, ] > 0)
  if (length(idx) >= 2) {
    j <- sample(idx, 2)
    res[[i]] <- nms$ID2[j]
    d[, j] <- 0
  }
}
dfout <- type.convert(
  setNames(rev(stack(res)), names(df)),
  as.is = TRUE
)

这提供了

  ID1 ID2
1   A   6
2   A   1
3   B   2
4   B   3
5   C   4
6   C   5
7   D   7
8   D   8

对于已经使用了ID2的情况,例如:

> (df <- structure(list(ID1 = c(
+   "A", "A", "B", "B", "B", "C", "C", "C",
+   "D", "D", "D", "D"
+ ), ID2 = c(
+   1, 3, 1, 2, 3, 3, 4, 5, 4, 5, 6, .... [TRUNCATED]
   ID1 ID2
1    A   1
2    A   3
3    B   1
4    B   2
5    B   3
6    C   3
7    C   4
8    C   5
9    D   4
10   D   5
11   D   6
12   D   1

我们将获得

  ID1 ID2
1   A   1
2   A   3
3   C   5
4   C   4

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接