使用data.table按组进行无重复抽样

4
我将用一个假设情景来说明这个问题。下面是一个音乐家及其演奏乐器的表格,以及一个乐队作品的表格。
musicians <- data.table(
  instrument = rep(c('bass','drums','guitar'), each = 4),
  musician = c('Chas','John','Paul','Stuart','Andy','Paul','Peter','Ringo','George','John','Paul','Ringo')
)

band.comp <- data.table(
  instrument = c('bass','drums','guitar'),
  n = c(2,1,2)
)

为避免关于谁最擅长哪种乐器的争论,乐队将通过抽签方式组成。以下是我进行的方式:
musicians[band.comp, on = 'instrument'][, sample(musician, n), by = instrument]

   instrument     V1
1:       bass   Paul
2:       bass   Chas
3:      drums   Andy
4:     guitar   Paul
5:     guitar George

问题是:由于有演奏多个乐器的音乐家,可能会出现一个人被抽中多次的情况。
可以建立一个 for 循环,在每个后续的乐器子集中抽取音乐家,然后将其从表的其他部分中删除。但我希望能够借助 data.table 来解决这个问题,并且更好地理解 data.table 语法。主要是因为我需要用这种逻辑解决实际生活中涉及数十万行数据的数据库问题。此外,我尝试了 Andrew Brooks 博客中的一些 技巧,但没有找到解决方案。
3个回答

5

这可以是一个解决方案,首先您选择一位音乐家的乐器,然后选择样本中的音乐家。但是,如果按音乐家选择乐器,导致样本大小大于总体,则会出现错误(但在您的实际数据中可能不是问题)。

musicians[, .(instrument = sample(instrument, 1)), by = musician][band.comp, on = 'instrument'][, sample(musician, n), by = instrument]

4
感到困惑为何会被踩。这看起来是一个好的解决方案。 - Hugh
很聪明,我没有想到要沿着这条路走。对于大数据集,样本大小不应该是问题,除非有很多类别和很多重复。例如,当Netflix在不同的类别中不断建议观看同一部电影时。 - Carlos Eduardo Lagosta
对不起,当时我太匆忙地进行了投票,因为有时它无法正常工作。回想起来,这是一个好的解决方案。一直想要撤销,但由于没有编辑权限,所以被锁定了。现在已经通过添加空格并反转投票来进行了编辑。我真诚地道歉。 - chinsoon12

3
您可以将band comp扩展为sum(band.comp$n)个不同的位置,并持续采样,直到找到可行的组合:
roles = musicians[, 
  CJ(posn = 1:band.comp[.BY, on=.(instrument), x.n], musician = musician)
, by=instrument]

set.seed(1)
while (TRUE){
  roles[sample(1:.N), keep := !duplicated(.SD, by="musician") & !duplicated(.SD, by=c("instrument", "posn"))][]
  if (sum(roles$keep) == sum(band.comp$n)) break
}

setorder(roles[keep == TRUE, !"keep"])[]

   instrument posn musician
1:       bass    1   Stuart
2:       bass    2     John
3:      drums    1     Andy
4:     guitar    1   George
5:     guitar    2     Paul

也许可以通过线性规划或二分图来回答是否存在可行的计算机组合,但就“抽样”在可行组合分配方面的含义而言,目前并不清楚。


这非常好。它避免了通过从整个数据集中排除个体的方法中存在偏差的问题,并且比使用子集循环要快得多。但是,本意是完全避免循环,仅使用data.table语法,因为我正在处理从SQL数据库获取的大型数据集。 - Carlos Eduardo Lagosta
1
只要roles(R中的一个表,因为data.table不支持与DB连接的查询)可以适应内存,我想这不是问题。在循环内部,它只是暂时混淆表的行(在R中),而不是重新创建它。但是,当构建roles时,如果band.comp$n的值足够大,这种方法可能会失败。 - Frank
1
@Frank 你说得对,我走了错误的推理路线。我想避免数据的乘法,所以我在思考一些涉及排除行的解决方案。在某些情况下 roles 表可能确实太大而无法在内存中存储,但我只需要读取 id 和 category 列,筛选后,然后使用得到的 id 向量获取完整的数据。感谢你的建议。 - Carlos Eduardo Lagosta

1
我发现了一篇相关的文章:基于唯一值和列值从数据框中随机抽取行,eddi的答案非常适合这个问题的提出者。
#keep number of musicians per instrument in 1 data.table
musicians[band.comp, n:=n, on=.(instrument)]

#for storing the musician that has been sampled so far
m <- c()

musicians[, {
    #exclude sampled musician before sampling
    res <- .SD[!musician %chin% m][sample(.N, n[1L])]
    m <- c(m, res$musician)
    res
}, by=.(instrument)]

样例输出:
   instrument musician n
1:       bass   Stuart 2
2:       bass     Chas 2
3:      drums     Paul 1
4:     guitar     John 2
5:     guitar    Ringo 2

或者更简洁地说,还带有错误处理:

m <- c()
musicians[
    band.comp, 
    on=.(instrument), 
    j={
        s <- setdiff(musician, m)
        if (length(s) < n) stop(paste("Not enough musicians playing", .BY))
        res <- sample(s, n)    
        m <- c(m, res)
        res
    }, 
    by=.EACHI]

老实说,Eddi 值得获得荣誉。请在其他 OP 上投票支持他的回答,而不是这个。 - chinsoon12

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