【问题标题】:Group-specific calculations involving both row-specific and whole-group elements涉及特定行和整组元素的特定组计算
【发布时间】:2019-10-01 22:40:51
【问题描述】:

我在将这个问题的逻辑与dplyr 的逻辑匹配时遇到了一点问题。通常,如果您想将一个组减少到每组一个数字,则使用summarise,而如果您想为每行计算一个单独的数字,则使用mutate。但是如果你想对每一行的组进行计算呢?

在下面的示例中,mloc 包含一个指向 pnum 的指针,目标是添加一个新列 nm_child,该列对于每一行计算指向的组中 mloc 值的数量到(即具有相同的值)pnum 中的行中组索引。如果我知道如何迭代 1) 每个组,& 2) 每个元素,& 3) 将映射输出作为组中的列返回,这将很容易使用嵌套循环或 map 来实现。

library(tidyverse)

ser    <- c(1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2)
pnum   <- c(1:5, 1:6)
mloc   <- c(0, 2, 2, 0, 3, 1, 1, 0, 0, 3, 4)

tb1 <- tibble(ser,pnum,  mloc)
tb2 <- tb1 %>%
group_by(ser) %>%
mutate(nm_child = sum(pnum == mloc))

上面的 nm_child 总是 = 1。我明白它为什么不起作用,但我不明白它为什么会这样做。

我也试过

mutate(nm_child = count(pnum == mloc))

(返回

no applicable method for 'groups' applied to an object of class "logical")

和其他各种东西。我确实通过为中间值添加几列并使用一堆嵌套的 ifelse() 来完成一件事,但是在我的 900 万行上运行需要超过 20 分钟 - 相比之下,例如回归,以及最简单的 dplyr 操作,这些操作在几秒钟之间变化,很快就无法注意到。

期望的输出:

tb2$nm_child = c(0, 2, 1, 0, 0, 2, 0, 1, 1, 0, 0)

【问题讨论】:

    标签: r dictionary dplyr iteration tidyverse


    【解决方案1】:

    这是一种使用sapply的方法-

    tb1 %>% 
      group_by(ser) %>% 
      mutate(
        nm_child = sapply(pnum, function(x) sum(x == mloc))
      )
    
    # A tibble: 11 x 4
    # Groups:   ser [2]
         ser  pnum  mloc nm_child
       <dbl> <int> <dbl>    <int>
     1  1.00     1  0           0
     2  1.00     2  2.00        2
     3  1.00     3  2.00        1
     4  1.00     4  0           0
     5  1.00     5  3.00        0
     6  2.00     1  1.00        2
     7  2.00     2  1.00        0
     8  2.00     3  0           1
     9  2.00     4  0           1
    10  2.00     5  3.00        0
    11  2.00     6  4.00        0
    

    感谢@RonakShah,这是另一种方式-

    tb1 %>% 
      group_by(ser) %>% 
      mutate(
        nm_child = map_int(pnum, ~sum(. == mloc))
      )
    

    更新:查看其他答案中的基准,@thelatemail 的答案肯定是最好的。

    【讨论】:

    • 男人啊男人!我讨厌不得不在如此出色的答案之间做出选择!这些答案中的每一个都教会了我一些有价值的东西。在这里,我没有意识到您可以在 bog dplyr 函数中使用 apply 系列函数。我的意思是,它是有道理的——apply 函数毕竟只是 R 函数——但我的想法是 dplyr 和 purrr 取代了传统的 apply 系列函数,因此从未想过将它们一起使用。
    • 您可以将sapply 替换为map_inttb1 %&gt;% group_by(ser) %&gt;% mutate(nm_child = map_int(pnum, ~sum(. == mloc))),这在逻辑上仍然相同,但只是做同样事情的另一种方式。
    【解决方案2】:

    您可以使用outerrowSums

    tb1 %>% 
      group_by(ser) %>% 
      mutate(nm_child = rowSums(outer(pnum, mloc, `==`)))
    
    # # A tibble: 11 x 4
    # # Groups:   ser [2]
    #      ser  pnum  mloc nm_child
    #    <dbl> <int> <dbl>    <dbl>
    #  1     1     1     0        0
    #  2     1     2     2        2
    #  3     1     3     2        1
    #  4     1     4     0        0
    #  5     1     5     3        0
    #  6     2     1     1        2
    #  7     2     2     1        0
    #  8     2     3     0        1
    #  9     2     4     0        1
    # 10     2     5     3        0
    # 11     2     6     4        0
    

    使用 thelatemail 的示例数据进行基准测试

    tb1 <- tb1[rep(1:11,5e4),]
    tb1$ser <- rep(1:1e5, rep(5:6,5e4))
    
    tb2 <- as.data.table(tb1)
    
    library(microbenchmark)
    
    microbenchmark(
      sapply = {
        tb1 %>% 
          group_by(ser) %>% 
          mutate(
            nm_child = sapply(pnum, function(x) sum(x == mloc))
          )
      },
      join = {
        tb1 %>%
          group_by(ser, mloc) %>%
          summarise(nm_child=n()) %>%
          left_join(tb1, ., by=c("ser"="ser","pnum"="mloc"))
      },
      outer1 = {
        tb1 %>% 
          group_by(ser) %>% 
          mutate(nm_child = rowSums(outer(pnum, mloc, `==`)))
      },
      outer2 = {
        tb1 %>% 
          group_by(ser) %>% 
          mutate(nm_child = colSums(outer(mloc, pnum, `==`)))
      },
      data.table = {
        tb2[tb2[, .N, by=.(ser,mloc)], on=c("ser","pnum"="mloc"), nm_child := N][]
        },
      times = 10)
    

    基准输出

    # Unit: milliseconds
    #        expr       min        lq      mean    median        uq        max neval
    #      sapply 8233.5740 8297.7331 8939.9369 8647.5935 8956.3364 10706.3362    10
    #        join  889.6682  899.0483  935.7493  908.1441  932.2827  1135.8424    10
    #      outer1 4551.0428 4631.1605 5184.9359 4986.7327 5160.0109  7563.4190    10
    #      outer2 4495.9134 4552.1169 4763.5954 4723.7783 4893.2190  5198.4556    10
    #  data.table  108.7449  115.7866  124.4453  120.6742  125.7591   171.8111    10
    

    【讨论】:

      【解决方案3】:

      这是ser + mloc 的聚合,然后是返回原始数据的左连接。应该不需要遍历每个值:

      tb1 %>%
        group_by(ser, mloc) %>%
        summarise(nm_child=n()) %>%
        left_join(tb1, ., by=c("ser"="ser","pnum"="mloc"))
      
      ## A tibble: 11 x 4
      #     ser  pnum  mloc nm_child
      #   <dbl> <dbl> <dbl>    <int>
      # 1  1.00  1.00  0          NA
      # 2  1.00  2.00  2.00        2
      # 3  1.00  3.00  2.00        1
      # 4  1.00  4.00  0          NA
      # 5  1.00  5.00  3.00       NA
      # 6  2.00  1.00  1.00        2
      # 7  2.00  2.00  1.00       NA
      # 8  2.00  3.00  0           1
      # 9  2.00  4.00  0           1
      #10  2.00  5.00  3.00       NA
      #11  2.00  6.00  4.00       NA
      

      这样会更有效率:

      # big example
      tb1 <- tb1[rep(1:11,5e4),]
      tb1$ser <- rep(1:1e5, rep(5:6,5e4))
      
      system.time({
      tb1 %>% 
        group_by(ser) %>% 
        mutate(
          nm_child = sapply(pnum, function(x) sum(x == mloc))
        )
      })
      #   user  system elapsed 
      #   8.83    0.06    8.97     
      
      system.time({
      tb1 %>%
        group_by(ser, mloc) %>%
        summarise(nm_child=n()) %>%
        left_join(tb1, ., by=c("ser"="ser","pnum"="mloc"))
      })
      #   user  system elapsed 
      #   0.67    0.02    0.69 
      

      在基本 R 逻辑中,这将类似于:

      tabu <- aggregate(cbind(nm_child=mloc) ~ ser + mloc, tb1, FUN=length)
      merge(tb1, tabu, by.x=c("ser","pnum"), by.y=c("ser","mloc"), all.x=TRUE)
      

      然后在data.table 中进行四舍五入,这将再次快一个数量级:

      tb1[tb1[, .N, by=.(ser,mloc)], on=c("ser","pnum"="mloc"), nm_child := N]
      

      【讨论】:

      • 我不得不接受这个答案,尽管我非常喜欢其他答案。首先,当然,因为它快 10 倍。那是因为 left_join 对于整个向量是完全向量化的,而其他解决方案对每个组都有几个函数调用?但也提醒我,对大小合理的数据进行基准测试是多么容易。每次我尝试时,我的完整数据集都会使基准测试崩溃(实际上是 pandoc 崩溃了),所以我几乎已经停止尝试复制你的示例告诉我,我只需使用 1% 的样本就可以解决这个问题。
      • @andrewH - 干杯。我不是基准测试专家,但我相信合并/连接通常会比 R 中手动循环值更快,因为它们通常是类似任务的优化版本。快速创建大量可重现的数据是 R imho 的一大优势。很高兴它有帮助。
      猜你喜欢
      • 1970-01-01
      • 2018-04-16
      • 1970-01-01
      • 2017-12-13
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多