【问题标题】:Getting the top values by group按组获取最高值
【发布时间】:2020-03-18 00:57:24
【问题描述】:

这是一个示例数据框:

d <- data.frame(
  x   = runif(90),
  grp = gl(3, 30)
) 

我想要d 的子集,其中包含x 的前5 个值对于grp 的每个值的行。

使用 base-R,我的方法类似于:

ordered <- d[order(d$x, decreasing = TRUE), ]    
splits <- split(ordered, ordered$grp)
heads <- lapply(splits, head)
do.call(rbind, heads)
##              x grp
## 1.19 0.8879631   1
## 1.4  0.8844818   1
## 1.12 0.8596197   1
## 1.26 0.8481809   1
## 1.18 0.8461516   1
## 1.29 0.8317092   1
## 2.31 0.9751049   2
## 2.34 0.9269764   2
## 2.57 0.8964114   2
## 2.58 0.8896466   2
## 2.45 0.8888834   2
## 2.35 0.8706823   2
## 3.74 0.9884852   3
## 3.73 0.9837653   3
## 3.83 0.9375398   3
## 3.64 0.9229036   3
## 3.69 0.8021373   3
## 3.86 0.7418946   3

使用dplyr,我希望这可以工作:

d %>%
  arrange_(~ desc(x)) %>%
  group_by_(~ grp) %>%
  head(n = 5)

但它只返回整体前 5 行。

head 替换为top_n 会返回整个d

d %>%
  arrange_(~ desc(x)) %>%
  group_by_(~ grp) %>%
  top_n(n = 5)

如何获得正确的子集?

【问题讨论】:

    标签: r data.table dplyr


    【解决方案1】:

    dplyr 1.0.0、“slice_min()slice_max() 中选择具有变量最小值或最大值的行,从令人困惑的top_n(). 中接管”

    d %>% group_by(grp) %>% slice_max(order_by = x, n = 5)
    # # A tibble: 15 x 2
    # # Groups:   grp [3]
    #     x grp  
    # <dbl> <fct>
    #  1 0.994 1    
    #  2 0.957 1    
    #  3 0.955 1    
    #  4 0.940 1    
    #  5 0.900 1    
    #  6 0.963 2    
    #  7 0.902 2    
    #  8 0.895 2    
    #  9 0.858 2    
    # 10 0.799 2    
    # 11 0.985 3    
    # 12 0.893 3    
    # 13 0.886 3    
    # 14 0.815 3    
    # 15 0.812 3
    

    dplyr 1.0.0 使用top_n

    来自?top_n,关于wt 参数:

    用于排序的变量 [...] 默认为 tbl" 中的最后一个变量

    您的数据集中的最后一个变量是“grp”,这不是您希望排名的变量,这就是您的top_n 尝试“返回整个 d”的原因。因此,如果您希望在数据集中按“x”排名,则需要指定wt = x

    d %>%
      group_by(grp) %>%
      top_n(n = 5, wt = x)
    

    数据:

    set.seed(123)
    d <- data.frame(
      x = runif(90),
      grp = gl(3, 30))
    

    【讨论】:

    【解决方案2】:

    data.table 也很容易...

    library(data.table)
    setorder(setDT(d), -x)[, head(.SD, 5), keyby = grp]
    

    或者

    setorder(setDT(d), grp, -x)[, head(.SD, 5), by = grp]
    

    或者(对于大数据集应该更快,因为避免为每个组调用.SD

    setorder(setDT(d), grp, -x)[, indx := seq_len(.N), by = grp][indx <= 5]
    

    编辑:以下是dplyrdata.table 的比较(如果有人感兴趣的话)

    set.seed(123)
    d <- data.frame(
      x   = runif(1e6),
      grp = sample(1e4, 1e6, TRUE))
    
    library(dplyr)
    library(microbenchmark)
    library(data.table)
    dd <- copy(d)
    
    microbenchmark(
      top_n = {d %>%
                 group_by(grp) %>%
                 top_n(n = 5, wt = x)},
      dohead = {d %>%
                  arrange_(~ desc(x)) %>%
                  group_by_(~ grp) %>%
                  do(head(., n = 5))},
      slice = {d %>%
                 arrange_(~ desc(x)) %>%
                 group_by_(~ grp) %>%
                 slice(1:5)},
      filter = {d %>% 
                  arrange(desc(x)) %>%
                  group_by(grp) %>%
                  filter(row_number() <= 5L)},
      data.table1 = setorder(setDT(dd), -x)[, head(.SD, 5L), keyby = grp],
      data.table2 = setorder(setDT(dd), grp, -x)[, head(.SD, 5L), grp],
      data.table3 = setorder(setDT(dd), grp, -x)[, indx := seq_len(.N), grp][indx <= 5L],
      times = 10,
      unit = "relative"
    )
    
    
    #        expr        min         lq      mean     median        uq       max neval
    #       top_n  24.246401  24.492972 16.300391  24.441351 11.749050  7.644748    10
    #      dohead 122.891381 120.329722 77.763843 115.621635 54.996588 34.114738    10
    #       slice  27.365711  26.839443 17.714303  26.433924 12.628934  7.899619    10
    #      filter  27.755171  27.225461 17.936295  26.363739 12.935709  7.969806    10
    # data.table1  13.753046  16.631143 10.775278  16.330942  8.359951  5.077140    10
    # data.table2  12.047111  11.944557  7.862302  11.653385  5.509432  3.642733    10
    # data.table3   1.000000   1.000000  1.000000   1.000000  1.000000  1.000000    10
    

    添加一个稍微快一点的data.table 解决方案:

    set.seed(123L)
    d <- data.frame(
        x   = runif(1e8),
        grp = sample(1e4, 1e8, TRUE))
    setDT(d)
    setorder(d, grp, -x)
    dd <- copy(d)
    
    library(microbenchmark)
    microbenchmark(
        data.table3 = d[, indx := seq_len(.N), grp][indx <= 5L],
        data.table4 = dd[dd[, .I[seq_len(.N) <= 5L], grp]$V1],
        times = 10L
    )
    

    定时输出:

    Unit: milliseconds
            expr      min       lq     mean   median        uq      max neval
     data.table3 826.2148 865.6334 950.1380 902.1689 1006.1237 1260.129    10
     data.table4 729.3229 783.7000 859.2084 823.1635  966.8239 1014.397    10
    

    【讨论】:

    • 添加另一个 data.table 方法应该会稍微快一点:dt &lt;- setorder(setDT(dd), grp, -x); dt[dt[, .I[seq_len(.N) &lt;= 5L], grp]$V1]
    • @chinsoon12 做我的客人。我没有时间再次对这些解决方案进行基准测试。
    • 添加另一个 data.table 方法更容易:setDT(d)[order(-x),x[1:5],keyby = .(grp)]
    • @TaoHu 这很像前两个解决方案。我不认为: 会击败head
    • @DavidArenburg 是的,我同意你的观点,我认为最大的区别是setorderorder
    【解决方案3】:

    您需要将head 包装在对do 的调用中。在以下代码中,. 代表当前组(参见do 帮助页面中... 的描述)。

    d %>%
      arrange_(~ desc(x)) %>%
      group_by_(~ grp) %>%
      do(head(., n = 5))
    

    正如 akrun 所说,slice 是一个替代方案。

    d %>%
      arrange_(~ desc(x)) %>%
      group_by_(~ grp) %>%
      slice(1:5)
    

    虽然我没有问这个,但为了完整起见,可能的data.table 版本是(感谢@Arun 的修复):

    setDT(d)[order(-x), head(.SD, 5), by = grp]
    

    【讨论】:

    • @akrun 谢谢。我不知道那个功能。
    • @DavidArenburg 谢谢。这就是匆忙发布答案的原因。我已经删除了废话。
    • Richie,FWIW,你只需要一个小补充:setDT(d)[order(-x), head(.SD, 5L), by=grp]
    • 这个答案有点过时了,但如果你放弃~ 并使用arrangegroup_by 而不是arrange_group_by_,第二部分是惯用的方式
    【解决方案4】:

    我在基础 R 中的方法是:

    ordered <- d[order(d$x, decreasing = TRUE), ]
    ordered[ave(d$x, d$grp, FUN = seq_along) <= 5L,]
    

    使用 dplyr,slice 的方法可能最快,但您也可以使用 filter,这可能比使用 do(head(., 5)) 更快:

    d %>% 
      arrange(desc(x)) %>%
      group_by(grp) %>%
      filter(row_number() <= 5L)
    

    dplyr 基准测试

    set.seed(123)
    d <- data.frame(
      x   = runif(1e6),
      grp = sample(1e4, 1e6, TRUE))
    
    library(microbenchmark)
    
    microbenchmark(
      top_n = {d %>%
                 group_by(grp) %>%
                 top_n(n = 5, wt = x)},
      dohead = {d %>%
                  arrange_(~ desc(x)) %>%
                  group_by_(~ grp) %>%
                  do(head(., n = 5))},
      slice = {d %>%
                 arrange_(~ desc(x)) %>%
                 group_by_(~ grp) %>%
                 slice(1:5)},
      filter = {d %>% 
                  arrange(desc(x)) %>%
                  group_by(grp) %>%
                  filter(row_number() <= 5L)},
      times = 10,
      unit = "relative"
    )
    
    Unit: relative
       expr       min        lq    median        uq       max neval
      top_n  1.042735  1.075366  1.082113  1.085072  1.000846    10
     dohead 18.663825 19.342854 19.511495 19.840377 17.433518    10
      slice  1.000000  1.000000  1.000000  1.000000  1.000000    10
     filter  1.048556  1.044113  1.042184  1.180474  1.053378    10
    

    【讨论】:

    • @akrun filter 需要额外的功能,而您的slice 版本不需要...
    • 你知道你为什么不在这里添加data.table ;)
    • 我知道,我可以告诉你:因为问题是专门针对 dplyr 解决方案提出的。
    • 我只是在开玩笑...不像你从来没有did the same(正好相反)。
    • @DavidArenburg,我并不是说提供 data.table 答案是“非法的”或类似的东西。当然你可以这样做并提供你喜欢的任何基准:) 顺便说一句,您链接到的问题是一个很好的例子,其中 dplyr 语法比 data.table 更方便(我知道,主观!)。
    【解决方案5】:
    如果 ordering 变量在每个组中不是唯一的,

    top_n(n = 1) 仍将为每个组返回多行。为了为每个组精确地选择一个出现,为每一行添加一个唯一的变量:

    set.seed(123)
    d <- data.frame(
      x   = runif(90),
      grp = gl(3, 30))
    
    d %>%
      mutate(rn = row_number()) %>% 
      group_by(grp) %>%
      top_n(n = 1, wt = rn)
    

    【讨论】:

      【解决方案6】:

      还有一个data.table 解决方案来突出其简洁的语法:

      setDT(d)
      d[order(-x), .SD[1:5], grp]
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-12-27
        • 1970-01-01
        • 2013-01-29
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多