【问题标题】:Select first and last row from grouped data从分组数据中选择第一行和最后一行
【发布时间】:2015-10-10 07:30:11
【问题描述】:

问题

使用dplyr,如何在一个语句中选择顶部和底部的观察值/分组数据行?

数据和示例

给定一个数据框

df <- data.frame(id=c(1,1,1,2,2,2,3,3,3), 
                 stopId=c("a","b","c","a","b","c","a","b","c"), 
                 stopSequence=c(1,2,3,3,1,4,3,1,2))

我可以使用slice 获得每个组的顶部和底部观察值,但使用两个单独的语句:

firstStop <- df %>%
  group_by(id) %>%
  arrange(stopSequence) %>%
  slice(1) %>%
  ungroup

lastStop <- df %>%
  group_by(id) %>%
  arrange(stopSequence) %>%
  slice(n()) %>%
  ungroup

我可以将这两个 statmenets 合并为一个同时选择 顶部和底部观察值吗?

【问题讨论】:

标签: r dplyr


【解决方案1】:

使用which.minwhich.max

library(dplyr, warn.conflicts = F)
df %>% 
  group_by(id) %>% 
  slice(c(which.min(stopSequence), which.max(stopSequence)))

#> # A tibble: 6 x 3
#> # Groups:   id [3]
#>      id stopId stopSequence
#>   <dbl> <fct>         <dbl>
#> 1     1 a                 1
#> 2     1 c                 3
#> 3     2 b                 1
#> 4     2 c                 4
#> 5     3 b                 1
#> 6     3 a                 3

基准测试

它也比当前接受的答案快得多,因为我们按组查找最小值和最大值,而不是对整个 stopSequence 列进行排序。

# create a 100k times longer data frame
df2 <- bind_rows(replicate(1e5, df, F)) 
bench::mark(
  mm =df2 %>% 
    group_by(id) %>% 
    slice(c(which.min(stopSequence), which.max(stopSequence))),
  jeremy = df2 %>%
    group_by(id) %>%
    arrange(stopSequence) %>%
    filter(row_number()==1 | row_number()==n()))
#> Warning: Some expressions had a GC in every iteration; so filtering is disabled.
#> # A tibble: 2 x 6
#>   expression      min   median `itr/sec` mem_alloc `gc/sec`
#>   <bch:expr> <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl>
#> 1 mm           22.6ms     27ms     34.9     14.2MB     21.3
#> 2 jeremy      254.3ms    273ms      3.66    58.4MB     11.0

【讨论】:

    【解决方案2】:

    使用data.table

    # convert to data.table
    setDT(df) 
    # order, group, filter
    df[order(stopSequence)][, .SD[c(1, .N)], by = id]
    
       id stopId stopSequence
    1:  1      a            1
    2:  1      c            3
    3:  2      b            1
    4:  2      c            4
    5:  3      b            1
    6:  3      a            3
    

    【讨论】:

      【解决方案3】:

      不同的基本 R 替代方案是 idstopSequence 的第一个 ordersplit 它们基于 id 并且对于每个 id,我们只选择第一个和最后一个索引并子集使用这些索引的数据框。

      df[sapply(with(df, split(order(id, stopSequence), id)), function(x) 
                         c(x[1], x[length(x)])), ]
      
      
      #  id stopId stopSequence
      #1  1      a            1
      #3  1      c            3
      #5  2      b            1
      #6  2      c            4
      #8  3      b            1
      #7  3      a            3
      

      或类似使用by

      df[unlist(with(df, by(order(id, stopSequence), id, function(x) 
                         c(x[1], x[length(x)])))), ]
      

      【讨论】:

        【解决方案4】:

        使用 lapply 和 dplyr 语句的另一种方法。我们可以将任意数量的汇总函数应用于同一个语句:

        lapply(c(first, last), 
               function(x) df %>% group_by(id) %>% summarize_all(funs(x))) %>% 
        bind_rows()
        

        例如,您可能也对具有最大 stopSequence 值的行感兴趣并执行以下操作:

        lapply(c(first, last, max("stopSequence")), 
               function(x) df %>% group_by(id) %>% summarize_all(funs(x))) %>%
        bind_rows()
        

        【讨论】:

          【解决方案5】:

          不是dplyr,而是使用data.table更直接:

          library(data.table)
          setDT(df)
          df[ df[order(id, stopSequence), .I[c(1L,.N)], by=id]$V1 ]
          #    id stopId stopSequence
          # 1:  1      a            1
          # 2:  1      c            3
          # 3:  2      b            1
          # 4:  2      c            4
          # 5:  3      b            1
          # 6:  3      a            3
          

          更详细的解释:

          # 1) get row numbers of first/last observations from each group
          #    * basically, we sort the table by id/stopSequence, then,
          #      grouping by id, name the row numbers of the first/last
          #      observations for each id; since this operation produces
          #      a data.table
          #    * .I is data.table shorthand for the row number
          #    * here, to be maximally explicit, I've named the variable V1
          #      as row_num to give other readers of my code a clearer
          #      understanding of what operation is producing what variable
          first_last = df[order(id, stopSequence), .(row_num = .I[c(1L,.N)]), by=id]
          idx = first_last$row_num
          
          # 2) extract rows by number
          df[idx]
          

          请务必查看Getting Started wiki,了解data.table 的基本知识

          【讨论】:

          • df[ df[order(stopSequence), .I[c(1,.N)], keyby=id]$V1 ]。看到id 出现两次对我来说很奇怪。
          • 您可以在setDT调用中设置密钥。所以这里不需要order 电话。
          • @ArtemKlevtsov - 不过,您可能并不总是想设置密钥。
          • df[order(stopSequence), .SD[c(1L,.N)], by = id]。见here
          • @JWilliman 不一定完全相同相同,因为它不会在 id 上重新排序。我认为df[order(stopSequence), .SD[c(1L, .N)], keyby = id] 应该可以解决问题(与上面的解决方案略有不同,结果将是keyed
          【解决方案6】:

          我知道dplyr 指定的问题。但是,由于其他人已经发布了使用其他包的解决方案,我决定也尝试使用其他包:

          基础包:

          df <- df[with(df, order(id, stopSequence, stopId)), ]
          merge(df[!duplicated(df$id), ], 
                df[!duplicated(df$id, fromLast = TRUE), ], 
                all = TRUE)
          

          数据表:

          df <-  setDT(df)
          df[order(id, stopSequence)][, .SD[c(1,.N)], by=id]
          

          sqldf:

          library(sqldf)
          min <- sqldf("SELECT id, stopId, min(stopSequence) AS StopSequence
                FROM df GROUP BY id 
                ORDER BY id, StopSequence, stopId")
          max <- sqldf("SELECT id, stopId, max(stopSequence) AS StopSequence
                FROM df GROUP BY id 
                ORDER BY id, StopSequence, stopId")
          sqldf("SELECT * FROM min
                UNION
                SELECT * FROM max")
          

          在一个查询中:

          sqldf("SELECT * 
                  FROM (SELECT id, stopId, min(stopSequence) AS StopSequence
                        FROM df GROUP BY id 
                        ORDER BY id, StopSequence, stopId)
                  UNION
                  SELECT *
                  FROM (SELECT id, stopId, max(stopSequence) AS StopSequence
                        FROM df GROUP BY id 
                        ORDER BY id, StopSequence, stopId)")
          

          输出:

            id stopId StopSequence
          1  1      a            1
          2  1      c            3
          3  2      b            1
          4  2      c            4
          5  3      a            3
          6  3      b            1
          

          【讨论】:

            【解决方案7】:

            为了完整起见:您可以传递 slice 一个索引向量:

            df %>% arrange(stopSequence) %>% group_by(id) %>% slice(c(1,n()))
            

            给了

              id stopId stopSequence
            1  1      a            1
            2  1      c            3
            3  2      b            1
            4  2      c            4
            5  3      b            1
            6  3      a            3
            

            【讨论】:

            • 甚至可能比 filter 更快 - 尚未对此进行测试,但请参阅 here
            • @Tjebo 与过滤器不同,切片可以多次返回同一行,例如mtcars[1, ] %&gt;% slice(c(1, n())),因此从这个意义上说,它们之间的选择取决于您想要返回的内容。我预计时间会很接近,除非n 非常大(切片可能会受到青睐),但也没有测试过。
            【解决方案8】:

            类似:

            library(dplyr)
            
            df <- data.frame(id=c(1,1,1,2,2,2,3,3,3),
                             stopId=c("a","b","c","a","b","c","a","b","c"),
                             stopSequence=c(1,2,3,3,1,4,3,1,2))
            
            first_last <- function(x) {
              bind_rows(slice(x, 1), slice(x, n()))
            }
            
            df %>%
              group_by(id) %>%
              arrange(stopSequence) %>%
              do(first_last(.)) %>%
              ungroup
            
            ## Source: local data frame [6 x 3]
            ## 
            ##   id stopId stopSequence
            ## 1  1      a            1
            ## 2  1      c            3
            ## 3  2      b            1
            ## 4  2      c            4
            ## 5  3      b            1
            ## 6  3      a            3
            

            使用do,您几乎可以对组执行任意数量的操作,但@jeremycg 的答案更适合此任务。

            【讨论】:

            • 没有考虑过编写函数——当然是做更复杂的事情的好方法。
            • 与仅使用slice 相比,这似乎过于复杂,例如df %&gt;% arrange(stopSequence) %&gt;% group_by(id) %&gt;% slice(c(1,n()))
            • 不同意(我指出 jeremycg 是一个更好的答案 in 帖子)但是当 slice 不起作用时,这里有一个 do 示例可能会对其他人有所帮助(即对组进行更复杂的操作)。而且,您应该发布您的评论作为答案(这是最好的)。
            【解决方案9】:

            可能有更快的方法:

            df %>%
              group_by(id) %>%
              arrange(stopSequence) %>%
              filter(row_number()==1 | row_number()==n())
            

            【讨论】:

            • rownumber() %in% c(1, n()) 将无需运行两次矢量扫描
            • @MichaelChirico 我怀疑你省略了_?即filter(row_number() %in% c(1, n()))
            猜你喜欢
            • 2012-06-01
            • 1970-01-01
            • 2021-08-17
            • 1970-01-01
            • 2014-11-10
            • 2016-04-08
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多