【问题标题】:How to determine whether two sets of variables have a shared value in R?如何确定两组变量在R中是否具有共享值?
【发布时间】:2022-01-23 20:32:14
【问题描述】:

我有一个包含两组变量的数据,我想比较这两组变量是否具有相同的值。在两组变量的每一行中,只要任意一对值相等,就标记为1,否则标记为0。如果数据中包含缺失值,我希望缺失值不参与比较。如果数据中包含字符变量,只要其实际值与数值变量的值相同,它们仍然被认为是相等的。

为了说明问题,我生成数据a1。我想确定第一组变量(z1x1)和第二组变量(z2中是否有任何值>x2) 相同并生成变量result

a1=data.table(z1=c(1,NA,3:5),x1=c("3",4:7),z2=c(2,NA,4:6),x2=c(3,5,4,7,5))
a1$result=c(1,0,0,0,1)

实际数据接近2000万行,每组变量很多。我想找到最有效的方法。非常感谢!

【问题讨论】:

    标签: r data.table


    【解决方案1】:

    这绝对是一个扩展性较差的问题。在对(比如)as.matrixapplyasplitdata.table::transpose 等进行了一些基准测试之后,我还没有找到一个可以合理扩展超过 50K 行的。

    最直接的(对我来说,是可口的,性能方面的)路径是最直接的:

    a1[, result := +(z1 == z2 | z1 == x2 | x1 == z2 | x1 == x2)]
    

    但是,NA 值会失败,因此我们需要更加小心。玩了一会儿,我觉得这个辅助函数是最直接的,因为它完全符合我们需要的逻辑,并且是完全向量化的:

    `%=%` <- function(a, b) !is.na(a) & !is.na(b) & a == b
    a1[, +(z1 %=% z2 | x1 %=% z2 | z1 %=% x2 | x1 %=% x2)]
    # [1] 1 0 0 0 1
    

    (我故意避免使用`%==%`,因为我在其他包中看到它允许NA %==% NA 为真。如果您更喜欢使用`%==%`,请随意,或使用其他一些中缀运算符由您选择。它甚至不需要是中缀,这主要是为了美观。)

    问题是当我们在每个中有更多变量时如何自动执行此操作(由变量名称中的尾随数字定义)。为此,我建议我们手动创建表达式,然后对其进行评估/解析。

    g1 = grep("1", names(a1), value = TRUE)
    g2 = grep("2", names(a1), value = TRUE)
    expr <- paste0(
      "+(",
      paste(outer(g1, g2, function(a, b) sprintf("%s %%=%% %s", a, b)), collapse = " | "),
      ")")
    expr
    # [1] "+(z1 %=% z2 | x1 %=% z2 | z1 %=% x2 | x1 %=% x2)"
    

    这会产生预期的结果:

    a1[, result2 := eval(parse(text = expr))]
    #       z1     x1    z2    x2 result result2
    #    <num> <char> <num> <num>  <num>   <int>
    # 1:     1      3     2     3      1       1
    # 2:    NA      4    NA     5      0       0
    # 3:     3      5     4     4      0       0
    # 4:     4      6     5     7      0       0
    # 5:     5      7     6     5      1       1
    

    这可以很好地垂直缩放。如果 a1 是 5 行,那么复制它 1e4 次会产生 50K 行,等等。

    a1e4 <- rbindlist(replicate(1e4, a1, simplify=FALSE))    # 50K rows
    system.time(a1e4[, result2 := eval(parse(text = expr))])
    #    user  system elapsed 
    #    0.06    0.00    0.06 
    
    a1e5 <- rbindlist(replicate(1e5, a1, simplify=FALSE))    # 500K
    system.time(a1e5[, result2 := eval(parse(text = expr))])
    #    user  system elapsed 
    #     0.7     0.0     0.7 
    
    a1e6 <- rbindlist(replicate(1e6, a1, simplify=FALSE))    # 5M
    system.time(a1e6[, result2 := eval(parse(text = expr))])
    #    user  system elapsed 
    #    7.16    0.06    7.22 
    

    它似乎是线性缩放的,这意味着另外 4 倍的行应该在大约 30 秒内解决。

    如果每个组有更多变量呢? (即,水平缩放

    set.seed(42)
    b1 <- copy(a1[,1:4])[, c("s1","t1","u1","v1","w1","y1", "s2","t2","u2","v2","w2","y2") :=
                             replicate(12, sample(9, .N, replace = TRUE), simplify = FALSE)]
    b1
    #       z1     x1    z2    x2    s1    t1    u1    v1    w1    y1    s2    t2    u2    v2    w2    y2
    #    <num> <char> <num> <num> <int> <int> <int> <int> <int> <int> <int> <int> <int> <int> <int> <int>
    # 1:     1      3     2     3     1     2     9     9     4     8     6     8     1     2     2     1
    # 2:    NA      4    NA     5     5     1     5     9     2     6     2     2     5     4     7     1
    # 3:     3      5     4     4     1     8     4     4     8     8     5     3     2     3     6     7
    # 4:     4      6     5     7     9     7     2     5     3     4     4     8     6     6     8     4
    # 5:     5      7     6     5     4     4     3     5     1     4     2     7     6     5     5     9
    bg1 = grep("1", names(b1), value = TRUE)
    bg2 = grep("2", names(b1), value = TRUE)
    bexpr <- paste0(
      "+(",
      paste(outer(bg1, bg2, function(a, b) sprintf("%s %%=%% %s", a, b)), collapse = " | "),
      ")")
    bexpr
    # [1] "+(z1 %=% z2 | x1 %=% z2 | s1 %=% z2 | t1 %=% z2 | u1 %=% z2 | v1 %=% z2 | w1 %=% z2 | y1 %=% z2 | z1 %=% x2 | x1 %=% x2 | s1 %=% x2 | t1 %=% x2 | u1 %=% x2 | v1 %=% x2 | w1 %=% x2 | y1 %=% x2 | z1 %=% s2 | x1 %=% s2 | s1 %=% s2 | t1 %=% s2 | u1 %=% s2 | v1 %=% s2 | w1 %=% s2 | y1 %=% s2 | z1 %=% t2 | x1 %=% t2 | s1 %=% t2 | t1 %=% t2 | u1 %=% t2 | v1 %=% t2 | w1 %=% t2 | y1 %=% t2 | z1 %=% u2 | x1 %=% u2 | s1 %=% u2 | t1 %=% u2 | u1 %=% u2 | v1 %=% u2 | w1 %=% u2 | y1 %=% u2 | z1 %=% v2 | x1 %=% v2 | s1 %=% v2 | t1 %=% v2 | u1 %=% v2 | v1 %=% v2 | w1 %=% v2 | y1 %=% v2 | z1 %=% w2 | x1 %=% w2 | s1 %=% w2 | t1 %=% w2 | u1 %=% w2 | v1 %=% w2 | w1 %=% w2 | y1 %=% w2 | z1 %=% y2 | x1 %=% y2 | s1 %=% y2 | t1 %=% y2 | u1 %=% y2 | v1 %=% y2 | w1 %=% y2 | y1 %=% y2)"
    

    呃,这看起来很糟糕,但性能非常好,每组 8 个变量:

    b1e4 <- rbindlist(replicate(1e4, b1, simplify=FALSE))
    system.time(b1e4[, result2 := eval(parse(text = bexpr))])
    #    user  system elapsed 
    #    0.11    0.00    0.10 
    
    b1e5 <- rbindlist(replicate(1e5, b1, simplify=FALSE))
    system.time(b1e5[, result2 := eval(parse(text = bexpr))])
    #    user  system elapsed 
    #    1.03    0.00    1.03 
    
    b1e6 <- rbindlist(replicate(1e6, b1, simplify=FALSE))
    system.time(b1e6[, result2 := eval(parse(text = bexpr))])
    #    user  system elapsed 
    #   11.72    0.51   12.25 
    

    【讨论】:

    • 感谢您提供可扩展的解决方案。相信很多类似的问题都可以通过你的回答解决!
    【解决方案2】:

    实际数据接近2000万行,每组变量很多。我想找到最有效的方法

    您可以转换为长格式并加入以查看是否有任何匹配项。估计还是比较快的。

    # this code should work for the original question (without character vectors or NAs)
    # create a row id
    a1[, row_id := .I]
    
    # specify column groups
    cols1 = c("x1", "z1")
    cols2 = c("x2", "z2")
    
    # transform to long form, drop colnames, drop dupes
    longDT1 = unique(melt(a1[, c("row_id", ..cols1)], id.vars="row_id")[, !"variable"])
    longDT2 = unique(melt(a1[, c("row_id", ..cols2)], id.vars="row_id")[, !"variable"])
    
    # find any matches
    w = longDT1[longDT2, on=.(row_id, value), which=TRUE, nomatch=0]
    
    # find associated row_ids
    match_row_ids = longDT1[w, unique(row_id)]
    
    # flag rows
    a1[, res := FALSE][match_row_ids, res := TRUE]
    

    注意:如果您在某些列中混合了字符值:

    • 您可以使用type.convert 作为数据清理的一部分,从一开始就获得正确的类型。
    • 如果必须有字符串,那么 longDT1 和 longDT2 中的 value 列都必须转换为字符串。

    【讨论】:

    • 虽然加入会非常快,但我认为melt 步骤可能会减慢整个过程……看看基准会很有趣。
    • @GregorThomas 是的,我同意。我的猜测是,由于any/intersect 需要每行调用一次,当有足够的行时,此处使用 melt x2 + join 的方法应该更快。融化的另一个问题可能是您遇到了一个有 20 亿行的表(超过了向量的最大长度)。因此,对于 20m 行的 OP 示例,如果每个组中有 100 列,则会出现问题。
    • 谢谢你,弗兰克,你的回答给了我另一个想法。我刚刚修改了问题,使a1 包含更多样化的数据情况。
    • @Vesper 很高兴知道——我已经进行了编辑。如果字符串存在错误(即它们实际上是数字但格式错误),那么在此步骤之前将它们转换为正确的类型可能会更好。如果它们需要是字符串(例如,有些是“3”,有些是“foo”),那么我认为您仍然可以使用这里的任何解决方案。
    • 感谢您的耐心回复。我的问题可以解决了。
    【解决方案3】:

    这是另一种依赖于每个组的列名的通用方法:

    g1 = grep("1", names(a1), value = TRUE)
    g2 = grep("2", names(a1), value = TRUE)
    
    a1[, result := as.integer(
      apply(.SD, MARGIN = 1, FUN = function(x) any(x[g1] %in% x[g2]))
    ), .SDcols = c(g1, g2)]
    a1
    #    z1 x1 z2 x2 result
    # 1:  1  3  2  3      1
    # 2:  2  4  3  5      0
    # 3:  3  5  4  4      0
    # 4:  4  6  5  7      0
    # 5:  5  7  6  5      1
    

    【讨论】:

    • 我建议添加 .SDcols = c(g1,g2) 以提高速度和课堂安全性。
    • 感谢改进!
    • 为了记录(我想你明白了,GregorThomas),“速度”(使用.SDcols)在列数超过这里提到的四个时最能感受到,并且“类-safety" 是因为apply(.SD, 1, ...) 可以将所有列强制转换为character,如果它们中的任何一个是非数字的。在此示例数据中,两者都不是一个因素,但在将解决方案调整到其他数据集时,这些小事情可能会让您感到困惑。
    • 感谢您的回答。为了使问题更普遍,我修改了a1 以包含缺失值和字符变量。当前的解决方案会产生奇怪的result=c(1,1,0,0,0)。如果第2行是缺失值的影响,我不明白为什么第5行会被误判。
    • 混合字符类会搞砸。 apply 转换为矩阵,将所有内容转换为字符。我不明白为什么,但是在整数转换中引入了空格。我们可以使用a1[, test := apply(.SD, MARGIN = 1, FUN = function(x) list(g1 = x[g1], g2 = x[g2])), .SDcols = c(g1, g2)] 进行检查,这里a1$test[[5]] 表明z1 5 变为" 5" 并带有前导空格,而x2 5 变为"5"。很奇怪....
    【解决方案4】:

    我们可能会遍历行,在对之间找到intersectlength 并转换为逻辑

    library(data.table)
    a1[, result := +(apply(.SD, 1, FUN = function(x) 
         length(intersect(x[1:2], x[3:4]))) > 0)]
    

    -输出

    > a1
       z1 x1 z2 x2 result
    1:  1  3  2  3      1
    2:  2  4  3  5      0
    3:  3  5  4  4      0
    4:  4  6  5  7      0
    5:  5  7  6  5      1
    

    在效率方面,dapply(来自collapse)可能比apply更快

    library(collapse)
    a1[, result := dapply(.SD, MARGIN = 1, FUN = function(x)
              length(intersect(x[1:2], x[3:4])))]
    

    或者使用带有str_detect的矢量化选项

    library(stringr)
    a1[, result := +(str_detect(paste(z1, x1), paste0(z2, "|", x2)))]
    

    【讨论】:

    • 谢谢阿克伦。为了让问题更笼统,我更改了a1的变量。现在第一个解决方案生成result=c(1,1,0,0,0),后两个解决方案生成result=c(1,1,0,0,1),这两种解决方案似乎都有一些问题。
    • @Vesper 没有提到。 NA。如果有则使用na.omita1[, +(apply(.SD, 1, FUN = function(x) length(intersect(na.omit(x[1:2]), na.omit(x[3:4])))) &gt; 0)]# [1] 1 0 0 0 0
    • 是的,我明白了。感谢您的补充回复。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-07-15
    • 2017-12-04
    • 1970-01-01
    • 2021-06-01
    • 2016-01-04
    • 1970-01-01
    相关资源
    最近更新 更多