【问题标题】:Efficient recursive random sampling高效的递归随机抽样
【发布时间】:2021-12-17 18:52:19
【问题描述】:

想象一个格式如下的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

问题是为 ID1 中的第一个唯一值随机选择一行(理想情况下可调整为 n 行),从数据集中删除相应的 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))

【问题讨论】:

  • 每个ID1 组是否包含相同的值?或者ID1 == "B" 是否有可能具有ID2 值,例如2:6 而不是1:5?如果它们都相同,我建议对(唯一)值进行抽样而不进行替换,然后添加ID1
  • @Martin Gal 实际上,在我的真实数据中,ID1 之间的 ID2 值并不完全重叠。可能是c(1:5)c(1:7) 等。
  • 为 ID1 中的第一个唯一值随机采样一行”是什么意思?
  • @sindri_baldur 您在 ID1 中拥有第一个唯一值(即 A)。对于此值,您从 ID2 中随机选择一个值(即 1:5)。假设您选择 2。因此,对于第二个唯一 ID1 值(即 B),您仅从 c(1, 4:5) 进行抽样,因为 2 已被选择并因此被删除。
  • 为什么是顺序的?就可能性而言,一次采样 3 个数字不会产生相同的结果。一次抽 3 个不换人还是抽 3 个不换人?

标签: r performance recursion


【解决方案1】:

可能的解决方案

以下是一些方法:

  • 使用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

【讨论】:

  • 我认为这是一个很好的答案。我添加了您提供的解决方案的基准,希望您可以接受。如果不是,请随时恢复到以前的状态。
  • 我刚刚看到有一个基准答案,所以我会更新它。我删除了编辑。
【解决方案2】:

我认为该算法可以满足您的需求,但效率不高。它可以为其他人提供更快的解决方案的起点。

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 值时,就没有唯一的 ID2 值了,因为数字 1 到 5 都已分配给字母 A:E。您没有在问题中说明当没有唯一的 ID2 值可分配给特定 ID1 时会发生什么 - 它们应该是 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

reprex package (v2.0.0) 于 2021-11-03 创建

【讨论】:

  • 感谢这个解决方案,看起来很有希望!澄清一下,不可能没有唯一的 ID2 值可以分配给特定的 ID1。
  • 您的解决方案适用于 n = 1。是否可以将其扩展为处理 n > 1,同时也反映了这样一个事实,即对于某些 ID1,没有足够的 ID2 值可供分配(即尽可能选择案例)?
  • @tmfmnk 是的 - 查看我的更新。它可能很慢,但我认为它可以满足您的需求。
  • @tmfmnk 它的运行速度似乎是这里最快解决方案的四分之一。如果它可以满足您的需求并且速度非常重要,我可以将其转换为 Rcpp 以大幅提升速度。
【解决方案3】:
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
  • @AllanCameron 是的。你说的对。目前还不清楚。
  • 这是一个很好的答案,但是根据我的实际数据,生成的 ID1-ID2 对与初始对不对应。也就是说,ID1 的 ID2 值并不是它们实际存在的 ID2 值集合(即,当集合仅为 1:5 时为 6)。
  • @tmfmnk 在我的回答中,该集合不受 1:5 的限制。这些代码是对所选 ID1 的对应 ID2 的子集。它可以是任何集合、任何范围等。无论如何,您能否提供一个更真实的数据集来代表您的实际问题和有问题的案例?通过阅读 cmets 很难明白你的意思。
【解决方案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 em> 它们或多或少相等,因为它们使用来自 TIC1 的相同逻辑,该逻辑在 GKi1 中进行了优化,并在 TIC3 和 GKi2 中重用。

【讨论】:

  • 排序部分不应该是解决方案的一部分。那是开销。似乎给出的数据很可能是排序的。仅当它未排序时才排序。但是上面使用的代码假定它没有排序。您应该使用 if else 或在函数之外对其进行排序。给出的代码使用 base R 并且是最快的。另请注意,该问题需要对 n>1 进行抽样。所有其他答案仅样本 n=1
【解决方案5】:

您可以在split df 上的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

【讨论】:

  • 性能改进的好发现!干杯!点赞!
  • 好吧,我的意思是,如果我们只有 5 未被访问,那么 sample(5,1) 会给出一个从 1 到 5 的随机整数,但它应该正好是 5 作为所需的输出。在这种情况下,5[sample(length(5),1)] 应该是一个稳定的表达式(尽管它看起来很“愚蠢”)。当然,如果我们有多个未访问的值,它可以正常工作。
【解决方案6】:

免责声明:此解决方案假定数据已排列/有序。如果数据没有排序。请先按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 行。但是我们已经为 A 组和 B 组使用了 4 个样本。我们只为 C 组保留了 1 个样本。

速度测试:

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&gt;1 时,这段代码很容易解决这个问题。其他的需要稍微调整一下,但它们的速度会急剧下降。即使在n = group size 时,这种方法也很有效。大多数其他人花费的时间太长甚至失败

【讨论】:

  • 当我将它与df &lt;- 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 相应编辑
  • 谢谢!但现在我得到了df &lt;- data.frame(ID1 = rep(LETTERS[1:3], each = 7),ID2 = rep(1:7, 3))[-6:-7,]; set.seed(2); sub_sample(df, 1)AB 一个5
  • @Gki。感谢那。我的出发点不正确。我开始从 8 而不是 6 开始数(第二组,因为第一组只有 5 个)。我已考虑到这一点并进行了相应的编辑。
  • 再次感谢您的更新!但是现在它仅对ABC 的范围1:5 进行采样,而从不对67 采样BC
【解决方案7】:

一种可能的方法

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)
})

【讨论】:

    【解决方案8】:

    如果我对帖子的理解正确,ID2 的样本应该是单调递增的。

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

    请注意,它假定每个ID1ID2 在 1 处重新启动并递增 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)
    

    【讨论】:

      【解决方案9】:

      这是另一个使用基础 R 的选项,我认为可以满足您的要求。我确实想指出,如果 ID2 中没有选项,它将悄悄地排除 ID1 值(例如,如果您将 n = 5 与示例数据一起放入函数中,您会看到 ID1 == B 被排除在外。

      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)
        )
        
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2012-01-26
        • 1970-01-01
        • 1970-01-01
        • 2016-09-06
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多