高效递归随机抽样

24
想象一个以以下格式的df:
   ID1 ID2
1    A   1
2    A   2
3    A   3
4    A   4
5    A   5
6    B   1
7    B   2
8    B   3
9    B   4
10   B   5
11   C   1
12   C   2
13   C   3
14   C   4
15   C   5

问题是随机选择一行(理想情况下可调整为n行),以获取ID1中的第一个唯一值,从数据集中删除相应的ID2值,从剩余的ID2值池中随机选择一个值作为第二个ID1值(即递归进行),依此类推。
例如,对于第一个ID1值,它会执行sample(1:5, 1),结果为2。对于第二个ID1值,它会执行sample(c(1, 3:5), 1),结果为3。对于第三个ID1值,它会执行sample(c(1, 4:5), 1),结果为5。不会出现没有至少一个唯一的ID2值可供分配给特定ID1的情况。然而,如果有多个ID2值可供选择(例如三个),可能会出现它们的数量不足的情况;在这种情况下,尽可能选择尽可能多的值。最终,结果应具有类似的格式:
  ID1 ID2
1   A   2
2   B   3
3   C   5

它应该足够高效地处理相当大的数据集(ID1中数万个唯一值和每个ID2中数十万个唯一值)。

我尝试了多种方法来解决这个问题,但老实说,它们都没有意义,只会增加混乱,所以我不在这里分享。

示例数据:

df <- data.frame(ID1 = rep(LETTERS[1:3], each = 5),
                 ID2 = rep(1:5, 3))

1
每个 ID1 组是否都包含相同的值?或者 ID1 == "B" 是否可能有不同于 1:5ID2 值,例如 2:6?如果它们都是相同的,我建议对不重复的值进行采样,然后添加 ID1 - Martin Gal
在ID1中第一个唯一值中随机抽取一行”是什么意思? - s_baldur
@sindri_baldur,你在ID1中拥有第一个唯一值(即A)。对于这个值,你从ID2中随机选择一个值(即1:5)。假设你选择了2。因此,对于第二个唯一的ID1值(即B),你只从c(1, 4:5)中进行抽样,因为2已经被选择并因此被移除。 - tmfmnk
1
为什么要顺序抽样?一次性抽取3个数字不会在可能性上产生相同的结果吗?一次性无放回地抽取3个数字与逐个无放回地抽取3个数字有何区别? - s_baldur
@sindri_baldur 如果从ID2中随机抽取3个数字且不重复,那么有可能某个给定的ID1不会被选中。必须确保所有的ID1值都有对应的ID2值。 - tmfmnk
显示剩余5条评论
9个回答

16

可能的解决方案

以下是一些方法:

  • 使用Reduce + subset进行基本的R递归
  • 使用igraph进行最大二分匹配
  • 使用for循环进行基本的R动态规划

1. 递归

您可以尝试下面的代码(Reduce应用于递归添加未访问的ID2值)

lst <- split(df, ~ID1)
lst[[1]] <- lst[[1]][sample(1:nrow(lst[[1]]), 1), ]
Reduce(
  function(x, y) {
    y <- subset(y, !ID2 %in% x$ID2)
    rbind(x, y[sample(nrow(y), 1), ])
  },
  lst
)

它提供

   ID1 ID2
4    A   4
7    B   2
11   C   1

2. 二分图匹配

可以看出,这个问题可以解释为图论中的一个匹配问题。

library(igraph)
library(dplyr)

g <- df %>%
  arrange(sample(n())) %>%
  graph_from_data_frame() %>%
  set_vertex_attr(
    name = "type",
    value = names(V(.)) %in% df$ID1
  )

type.convert(
  setNames(
    rev(
      stack(
        max_bipartite_match(g)$matching[unique(df$ID1)]
      )
    ), names(df)
  ),
  as.is = TRUE
)

我们能够获得

  ID1 ID2
1   A   2
2   B   5
3   C   1

3. for 循环动态规划

  lst <- with(df, split(ID2, ID1))
  v <- c()
  for (k in seq_along(lst)) {
    u <- lst[[k]][!lst[[k]] %in% v]
    v <- c(v, u[sample(length(u), 1)])
  }
  type.convert(
    data.frame(ID1 = names(lst), ID2 = v),
    as.is = TRUE
  )

提供了

  ID1 ID2
1   A   4
2   B   5
3   C   3

我认为这是一个很好的答案。我添加了你提供的解决方案的基准测试,希望你没问题。如果有问题,请随时恢复到之前的状态。 - missuse
我刚刚看到有一个基准答案,所以我会更新它。我已经删除了编辑。 - missuse

10

我认为这个算法可以实现你想要的功能,但它并不是非常高效。它可能会为他人提供更快解决方案的起点。

all_ID1 <- unique(df$ID1)
available <- unique(df$ID2)
new_ID2 <-  numeric(length(all_ID1))

for(i in seq_along(all_ID1))
{
  ID2_group <- df$ID2[df$ID1 == all_ID1[i]]
  sample_space <- ID2_group[ID2_group %in% available]
  new_ID2[i]<- sample(sample_space, 1)
  available <- available[available != new_ID2[i]]
}

data.frame(ID1 = all_ID1, ID2 = new_ID2)
#>   ID1 ID2
#> 1   A   5
#> 2   B   1
#> 3   C   2
请注意,如果您用尽了唯一的ID2值,则此方法将无法使用。例如,如果在ID1列中有字母A:F,每个字母的ID2值为1:5,则当您为ID1值“F”选择ID2值时,由于数字1到5已分配给字母A:E,因此没有剩余的唯一ID2值可用。您在问题中未说明当某个ID1没有唯一的ID2值可用时应该发生什么 - 它们应该是NA,还是允许重复?

编辑

以下修改允许选择任意的n。如果所有可用数字都用完,则样本空间将得到补充:

AC_function <- function(ID1, ID2, n = 1)
{
  all_ID1   <- rep(unique(ID1), each = n)
  available <- unique(ID2)
  new_ID2   <- numeric(length(all_ID1))

   for(i in seq_along(all_ID1))
   {
     ID2_group    <- ID2[ID1 == all_ID1[i]]
     sample_space <- ID2_group[ID2_group %in% available]
     
     if(length(sample_space) < 1) {
        available    <- unique(ID2)
        sample_space <- ID2_group[ID2_group %in% available]
     }
     if(length(sample_space) == 1) {
        new_ID2[i] <- sample_space
        available <- available[available != new_ID2[i]]
     }
     else {
        new_ID2[i]   <- sample(sample_space, 1)
        available    <- available[available != new_ID2[i]]
     }
   }

  data.frame(ID1 = all_ID1, ID2 = new_ID2)
}
例如:
AC_function(df$ID1, df$ID2)
#>   ID1 ID2
#> 1   A   2
#> 2   B   4
#> 3   C   5

AC_function(df$ID1, df$ID2, n = 2)
#>   ID1 ID2
#> 1   A   1
#> 2   A   2
#> 3   B   5
#> 4   B   4
#> 5   C   3
#> 6   C   2

2021-11-03由 reprex软件包 (v2.0.0)创建


谢谢这个解决方案,看起来很有前途!只是为了澄清一下,某个ID1没有剩余的唯一ID2值可分配,这种情况不可能发生。 - tmfmnk
你的解决方案对于n = 1非常有效。它能够扩展到处理n > 1,同时反映出对于某些ID1而言,可能没有足够的ID2值可供分配(在这种情况下尽可能选择)吗? - tmfmnk
1
@tmfmnk 看起来它的运行速度只有这里最快解决方案的四分之一。如果它能满足你的需求并且速度非常重要,我可以将其翻译为Rcpp以大幅提高速度。 - Allan Cameron

4
selected <- c()

for(i in unique(df[,1])) {

    x <- df[df[,"ID1"]==i,"ID2"]

    y <- setdiff(x,selected)
    selected <- unique(c(sample(y,1),selected))
    

}

data.frame(ID1 = unique(df[,1]), ID2 =selected)

提供,

  ID1 ID2
1   A   4
2   B   2
3   C   3

这很好,但是遭受了我在答案脚注中讨论的同样问题。尽管如此,我认为如果没有来自OP的更多信息,那是不可避免的,所以+1。 - Allan Cameron
@AllanCameron 是的,你说得对。这很不清楚。 - maydin
这是一个不错的答案,但在我的实际数据中,生成的ID1-ID2对与初始对不对应。也就是说,ID1的ID2值并不是实际存在于它们身上的ID2值集合(例如,当集合只有1:5时,ID1的ID2值为6)。 - tmfmnk
@tmfmnk 在我的回答中,集合并没有被限制在1:5之间。代码是根据所选ID1对应的ID2进行子集操作。它可以是任何集合、任何范围等等。无论如何,请您提供一个更真实的数据集,以代表您实际遇到的问题和有问题的情况。通过阅读评论很难理解您的观点。 - maydin

4
您可以在split数据帧上使用Reduce中的sample函数。
df <- data.frame(ID1 = rep(LETTERS[1:3], each = 5),
                 ID2 = rep(1:5, 3))
set.seed(42)

. <- split(df$ID2, df$ID1)
data.frame(ID1 = `storage.mode<-`(names(.), typeof(df$ID1)),
           ID2 = Reduce(function(x, y) {
             y <- y[!y %in% x]
             c(x, y[sample.int(length(y),1)])}, c(list(NULL), .)))
#  ID1 ID2
#1   A   1
#2   B   2
#3   C   3

或者使用 for 循环:

. <- split(df$ID2, df$ID1)
x <- df$ID2[0]
for(y in .) {
  y <- y[!y %in% x]
  x <- c(x, y[sample.int(length(y),1)])
}
data.frame(ID1 = `storage.mode<-`(names(.), typeof(df$ID1)), ID2 = x)
#  ID1 ID2
#1   A   1
#2   B   2
#3   C   3

或者使用fastmatchdqrng代替base

library(fastmatch)
library(dqrng)
. <- split(df$ID2, df$ID1)
x <- df$ID2[0]
for(y in .) {
  y <- y[!y %fin% x]
  x <- c(x, y[dqsample.int(length(y),1)])
}
data.frame(ID1 = `storage.mode<-`(names(.), typeof(df$ID1)), ID2 = x)
#  ID1 ID2
#1   A   2
#2   B   1
#3   C   5

创建结果向量,其大小为最终大小:

library(fastmatch)
library(dqrng)
. <- split(df$ID2, df$ID1)
x <- vector(typeof(df$ID2), length(.))
for(i in seq_along(.)) {
  y <- .[[i]]
  y <- y[!y %fin% x[seq_len(i-1)]]
  x[i] <- y[dqsample.int(length(y),1)]
}
data.frame(ID1 = `storage.mode<-`(names(.), typeof(df$ID1)), ID2 = x)
#  ID1 ID2
#1   A   3
#2   B   1
#3   C   2

性能改进的好发现!干杯!点赞了! - ThomasIsCoding
1
好的,我的意思是,如果我们只有5个未访问的元素,那么sample(5,1)会从1到5中随机选择一个整数,但实际上我们需要的输出应该是5。在这种情况下,5[sample(length(5),1)]应该是一个稳定的表达式(尽管看起来很“愚蠢”)。当然,如果我们有多个未访问的值,它可以正常工作,没有任何问题。 - ThomasIsCoding

4

欢迎更新基准测试!

基准测试图片

df <- data.frame(
  ID1 = rep(LETTERS, each = 10000),
  ID2 = sample(1000, length(LETTERS) * 10000, replace = TRUE)
)

f_TIC1 <- function() {
  lst <- split(df, ~ID1)
  lst[[1]] <- lst[[1]][sample(1:nrow(lst[[1]]), 1), ]
  Reduce(
    function(x, y) {
      y <- subset(y, !ID2 %in% x$ID2)
      rbind(x, y[sample(nrow(y), 1), ])
    },
    lst
  )
}

library(igraph)
library(dplyr)
f_TIC2 <- function() {
  g <- df %>%
    arrange(sample(n())) %>%
    graph_from_data_frame() %>%
    set_vertex_attr(
      name = "type",
      value = names(V(.)) %in% df$ID1
    )

  type.convert(
    setNames(
      rev(
        stack(
          max_bipartite_match(g)$matching[unique(df$ID1)]
        )
      ), names(df)
    ),
    as.is = TRUE
  )
}

f_TIC3 <- function() {
  lst <- with(df, split(ID2, ID1))
  v <- c()
  for (k in seq_along(lst)) {
    u <- lst[[k]][!lst[[k]] %in% v]
    v <- c(v, u[sample(length(u), 1)])
  }
  type.convert(
    data.frame(ID1 = names(lst), ID2 = v),
    as.is = TRUE
  )
}

f_GKi1 <- function() {
  . <- split(df$ID2, df$ID1)
  data.frame(ID1 = type.convert(names(.), as.is=TRUE),
    ID2 = Reduce(function(x, y) {c(x, sample(y[!y %in% x], 1))}, c(list(NULL), .)))
}

f_GKi2 <- function() {
  . <- split(df$ID2, df$ID1)
  x <- df$ID2[0]
  for(y in .) {
    y <- y[!y %in% x]
    x <- c(x, y[sample.int(length(y),1)])
  }
  data.frame(ID1 = type.convert(names(.), as.is=TRUE), ID2 = x)
}

library(fastmatch)
library(dqrng)
f_GKi3 <- function() {
  . <- split(df$ID2, df$ID1)
  x <- df$ID2[0]
  for(y in .) {
    y <- y[!y %fin% x]
    x <- c(x, y[dqsample.int(length(y),1)])
  }
  data.frame(ID1 = type.convert(names(.), as.is=TRUE), ID2 = x)
}

f_GKi4 <- function() {
  . <- split(df$ID2, df$ID1)
  x <- vector(typeof(df$ID2), length(.))
  for(i in seq_along(.)) {
    y <- .[[i]]
    y <- y[!y %fin% x[seq_len(i-1)]]
    x[i] <- y[dqsample.int(length(y),1)]
  }
  data.frame(ID1 = type.convert(names(.), as.is=TRUE), ID2 = x)
}

f_Onyambu <- function() {
  data <- df[order(df$ID1, df$ID2),] #Just in case it is not sorted
  n <- 1
  st <- table(data[[1]])
  s <- min(st)
  m <- length(st) 
  size <- min(m*n, s) 
  samples <- sample(s, size)
  index <- rep(seq(s), each = n, length = size) * s - s + samples
  data[index, ]
}

bm <- microbenchmark::microbenchmark(
  f_TIC1(),
  f_TIC2(),
  f_TIC3(),
  f_GKi1(),
  f_GKi2(),
  f_GKi3(),
  f_GKi4(),
  f_Onyambu()
)
ggplot2::autoplot(bm)
bm
#Unit: milliseconds
#        expr       min        lq      mean    median        uq       max neval
#    f_TIC1()  43.85147  46.00637  48.77332  46.53265  48.06150  86.60333   100
#    f_TIC2() 138.12085 143.15468 154.59155 146.49701 169.47343 191.70579   100
#    f_TIC3()  13.30333  13.89822  15.16400  14.49575  15.57266  52.16352   100
#    f_GKi1()  13.42718  13.88382  16.22395  14.31689  15.69188  52.70818   100
#    f_GKi2()  13.34032  13.80074  14.70703  14.52709  15.46372  17.80398   100
#    f_GKi3()  11.86203  12.09923  14.73456  12.26890  13.84257  50.41542   100
#    f_GKi4()  11.86614  12.08120  13.19142  12.20973  13.74152  50.82025   100
# f_Onyambu() 201.06478 203.11184 206.04584 204.10129 205.60191 242.28008   100

目前最快的是 GKi3GKi4,其次是 TIC3GKi1GKi2,它们几乎相等,因为它们使用了与 TIC1 相同的逻辑,该逻辑在 GKi1 中进行了优化,并在 TIC3 和 GKi2 中重新使用。


排序部分不应该是解决方案的一部分。那是额外负担。看起来给定的数据很可能已经排序了。只有在未排序的情况下才需要排序。但是上面使用的代码假设它没有排序。你应该使用if else或在函数外部进行排序。给定的代码使用基本R并且是最快的。还要注意,问题需要对n>1进行采样。所有其他答案仅对n=1进行采样。 - Onyambu

1

免责声明:此解决方案假定数据已经排列/排序。如果数据未排序,请先按照ID1列排序,然后使用以下函数:

还有一种方法可以在不使用for-loop/递归或更高级别的函数的情况下完成这个任务。我们需要注意,在R中,sample函数是向量化的。因此,如果数据框中的所有组大小相同,或者增加的大小,则可以利用向量化的样本。

n <- 1 # to be sampled from each group
s <- 5 # size of each group - Note that you have to give the minimum size. 
m <- length(unique(df[[1]])) # number of groups.
size <- min(m*n, s) #Total number of sampled data from the dataframe
samples <- sample(s, size)
index <- rep(seq(s), each = n, length = size) * s - s + samples
df[index, ]

这可以写成一个函数:
sub_sample <- function(data, n){
  st <- table(data[[1]])
  s <- min(st)
  m <- length(st) 
  size <- min(m*n, s) 
  samples <- sample(s, size)
  st1 <- rep(c(0, cumsum(head(st,-1))), each = n, length = size)
  index <- st1 + samples
  data[index, ]
}

sub_sample(df, 1)
   ID1 ID2
1    A   1
7    B   2
13   C   3

sub_sample(df, 2)
   ID1 ID2
1    A   1
5    A   5
8    B   3
7    B   2
14   C   4

请注意,当我们将n=2作为子集时,我们只有1个组C行。为什么?因为组C有5行。但是我们已经使用了4个样本来分组A和B。我们只剩下1个样本用于组C。
速度测试:
n = 1时:
Unit: milliseconds
              expr        min         lq      mean     median        uq       max neval
          f_TIC1()  35.682390  41.610310  53.68912  45.618234  49.88343 227.73160   100
          f_TIC2() 151.643959 166.402584 196.51770 179.098992 192.16335 401.36526   100
          f_TIC3()  11.059033  12.268831  14.53906  13.278606  15.38623  23.32695   100
          f_GKi1()  10.725358  11.879908  14.70369  13.108852  17.86946  26.71074   100
          f_GKi2()  10.816891  11.941173  16.55455  12.989787  17.92708 198.44482   100
          f_GKi3()   8.942479   9.950978  14.94726  10.857187  13.35428 171.08643   100
          f_GKi4()   9.085794   9.834465  13.98820  10.666282  13.20658 191.47267   100
 sub_sample(df, 1)   7.878367   8.737534  11.22173   9.508084  14.22219  19.82063   100

n>1时,这段代码很容易处理。其他的需要稍微调整一下,但它们的速度会急剧下降。即使n=组大小,这种方法也非常有效。大多数其他方法耗时太长,甚至会失败。


当我使用 df <- data.frame(ID1 = rep(LETTERS[1:3], each = 7),ID2 = rep(1:7, 3))[-6:-7,]; set.seed(2); sub_sample(df, 1) 时,我得到了两个 B 的样本和零个 C 的样本。 - GKi
@GKi editted accordingly - Onyambu
谢谢!但现在我得到了 df <- data.frame(ID1 = rep(LETTERS[1:3], each = 7),ID2 = rep(1:7, 3))[-6:-7,]; set.seed(2); sub_sample(df, 1),对于 A 是一个,对于 B5 - GKi
@Gki。谢谢你的帮助。我的起点是错误的。我从8开始计数,而不是6(因为第一组只有5个)。我已经考虑到这一点并进行了编辑。 - Onyambu
再次感谢更新! 但是现在它只对ABC的范围1:5进行采样,并且从不对BC进行67的采样。 - GKi
在这种情况下,我会说该解决方案适用于具有相同组大小的数据。将不得不研究如何处理不同组大小的情况。我确定的一件事是,该解决方案考虑了n>1和相等的组大小。 - Onyambu

0

一种可能的方法

library(data.table)
setDT(df)
exclude.values <- as.numeric()
L <- split(df, by = "ID1")
ans <- lapply(L, function(x) {
  sample.value <- sample(setdiff(x$ID2, exclude.values), 1)
  exclude.values <<- c(exclude.values, sample.value)
  return(sample.value)
})

0

如果我理解这篇文章正确的话,ID2 的样本应该是单调递增的。

这个方法似乎有效。方法是确定每个 ID1 存在多少“松弛度”,然后随机分配。

请注意,它假设 ID2 在每个 ID1 处重新开始为 1,并逐个递增。

dt <- data.table(ID1 = LETTERS[rep.int(1:10, sample(10:20, 10, replace = TRUE))])[, ID2 := 1:.N, by = ID1]

stepSample <- function(dt) {
  dt2 <- dt[, .(n = max(ID2)), by = ID1][, `:=`(slack = rev(cummin(cummin(rev(n)) - rev(.I))), inc = 0L)]
  dtSlack <- data.table(idx = 1:nrow(dt2), slack = dt2$slack)
  
  while (nrow(dtSlack)) {
    if (nrow(dtSlack) == 1L) {
      dt2[dtSlack$idx, inc := inc + sample(0:dtSlack$slack, 1L)]
      break
    } else {
      dt2[sample(dtSlack$idx, 1L), inc := inc + 1L]
      dtSlack <- dtSlack[, slack := slack - 1L][slack != 0L]
    }
  }
  
  return(dt2[, ID2 := .I + cumsum(inc)][, c("ID1", "ID2")])
}

dtSample <- stepSample(dt)

0

这里有另一种选项,它使用基本的R语言,并且我认为符合您的要求。但是需要注意的是,如果ID2中没有选项(例如,如果您在函数中使用示例数据并设置n = 5,则会发现ID1 == B被排除在外),它将会悄悄地排除掉一个ID1值。

df <- data.frame(ID1 = rep(LETTERS[1:3], each = 5),
                 ID2 = rep(1:5, 3))

set.seed(1)
andrew_fun(df$ID1, df$ID2, n = 1)
#>   ID1 ID2
#> 1   A   1
#> 2   B   5
#> 3   C   3
andrew_fun(df$ID1, df$ID2, n = 2)
#>   ID1 ID2
#> 1   A   1
#> 2   A   2
#> 3   B   3
#> 4   B   5
#> 5   C   4
#> 6   C   2
andrew_fun(df$ID1, df$ID2, n = 3)
#>   ID1 ID2
#> 1   A   2
#> 2   A   3
#> 3   A   4
#> 4   B   1
#> 5   B   5
#> 6   C   2
#> 7   C   3
#> 8   C   4

功能:

andrew_fun = function(ID1, ID2, n = 1) {
  l = split.default(ID2, ID1)
  l_len = length(l)
  l_vals = vector("list", l_len)

  for(i in seq_along(l)) {
    vec = l[[i]]
    if(n < length(vec)) {
      val = vec[sample.int(length(vec), n)] # sample if there are enough values
    } else {
      val = vec # grab everything if not
    }
    
    l_vals[[i]] = val

    # remove values from next level of ID1
    if(i < l_len) {
      idx = i + 1L
      l[[idx]] = l[[idx]][!l[[idx]] %in% val] 
    }
  }
  data.frame(
    ID1 = rep(names(l), lengths(l_vals)),
    ID2 = unlist(l_vals, use.names = FALSE)
  )
  
}

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