使用 group_by > mutate > slice 的更有效方式

More efficient way of using group_by > mutate > slice

我有一个看起来像这样的数据框

df <- data.frame("Month" = c("April","April","May","May","June","June","June"),
"ID" = c(11, 11, 12, 10, 11, 11, 11),
"Region" = c("East", "West", "North", "East", "North" ,"East", "West"),
"Qty" = c(120, 110, 110, 110, 100, 90, 70),
"Sales" = c(1000, 1100, 900, 1000, 1000, 800, 650),
"Leads" = c(10, 12, 9, 8, 6, 5, 4))

Month   ID     Region    Qty    Sales   Leads
April   11     East      120    1000    10
April   11     West      110    1100    12
May     12     North     110    900     9
May     10     East      110    1000    8
June    11     North     100    1000    6
June    11     East      90     800     5
June    11     West      70     650     4

我想要一个像这样的数据框

Month   ID     Qty     Sales   Leads   Region
April   11     230     2100    22      East
May     12     110     900     9       North
May     10     110     1000    8       East
June    11     260     2450    15      North

我正在使用以下代码

result <- df %>% group_by(Month, ID) %>% mutate(across(.cols = Qty:Leads, ~sum(.x, na.rm = T))) %>% slice(n = 1) 

result$Region <- NULL

我有超过 200 万个这样的行,计算总和需要很长时间。

我正在使用 mutate 和 slice 而不是 summarize,因为 df 是以某种方式排列的,我想保留第一行中的区域。

不过我认为可以有更有效的方法。请帮助两者。我这辈子都想不通。

summarize 对我来说比 mutateslice 更有意义。这应该可以节省您一些时间。

library(dplyr)
result <- df %>%
  group_by(Month, ID) %>%
  summarize(across(.cols = Qty:Leads, ~sum(.x, na.rm = T)),
            Region = first(Region))
result
# # A tibble: 4 x 6
# # Groups:   Month [3]
#   Month    ID   Qty Sales Leads Region
#   <chr> <dbl> <dbl> <dbl> <dbl> <chr> 
# 1 April    11   230  2100    22 East  
# 2 June     11   260  2450    15 North 
# 3 May      10   110  1000     8 East  
# 4 May      12   110   900     9 North 

这是一个data.table解决方案。

library(data.table)

setDT(df)

cols <- c("Qty", "Sales", "Leads")

df[, c(lapply(.SD, sum, na.rm = TRUE),
       Region = first(Region)), .SDcols = cols, 
   by = .(Month, ID)][]
#    Month ID Qty Sales Leads Region
# 1: April 11 230  2100    22   East
# 2:   May 12 110   900     9  North
# 3:   May 10 110  1000     8   East
# 4:  June 11 260  2450    15  North

我们可以应用通用加速策略:

  1. 少做
  2. 选择合适的后端
  3. 使用适当的数据结构

dplyr 为数据操作提供语法糖,但在处理大型数据集时可能不是最有效的。

解决方案 1

我们可以使用 collapse 包稍微重写代码以提高效率,它为 dplyr 函数提供了 C++ 接口。它在 dplyr 函数前加上 f,但有一个例外 fsubset,它类似于 dplyr::filter(或基础 R subset)。

library(collapse)
df |>
    fgroup_by(Month, ID) |>
    fsummarise(Qty = fsum(Qty),
               Sales = fsum(Sales),
               Leads = fsum(Leads),
               Region = fsubset(Region, 1L),
               keep.group_vars = T) |>
    as_tibble() # optional
#> # A tibble: 4 x 6
#>   Month    ID   Qty Sales Leads Region
#>   <chr> <dbl> <dbl> <dbl> <dbl> <chr> 
#> 1 April    11   230  2100    22 East  
#> 2 June     11   260  2450    15 North 
#> 3 May      10   110  1000     8 East  
#> 4 May      12   110   900     9 North 

其中 |>(要求 R 版本 > 3.5)是比 %>% 稍快的管道。其结果是 ungrouped.

解决方案 2

data.table 经常因其 speed, memory use and utility 而受到称赞。从现有 dplyr 代码转换为使用 data.table 的最简单方法是使用 dtplyr 包,它随 tidyverse 一起提供。我们可以通过添加两行代码来转换它。

library(dtplyr)
df1 <- lazy_dt(df)
df1 %>%
      group_by(Month, ID) %>%
      summarize(across(.cols = Qty:Leads, ~sum(.x, na.rm = T)),
                Region = first(Region)) %>%
      as_tibble() # or data.table()

请注意,此结果最后是 未分组 data.frame。

基准

方法放在包装函数中。 dplyr 这是 www 的做法。所有输出的方法都是 tibble.

bench::mark(collapse = collapse(df), dplyr = dplyr(df), dtplyr = dtplyr(df),
            time_unit = "ms", iterations = 200)[c(1, 3,5,7)]
# A tibble: 3 x 4
  expression median mem_alloc n_itr
  <bch:expr>  <dbl> <bch:byt> <int>
1 collapse    0.316        0B   200
2 dplyr       5.42     8.73KB   195
3 dtplyr      6.67   120.21KB   196

我们可以看到 collapsedplyr 相比,内存效率更高,速度明显更快。 dtplyr 方法包含在这里,因为它的 时间复杂度 不同于 dplyr 并且它的重写方便。

根据@www 的要求,包含纯 data.table 方法,为简洁起见重写了包装函数。输入/输出分别是 collapsedata.framedata.tabledata.table

data.table = \(x){setDT(x); cols = c("Qty", "Sales", "Leads");x[, c(lapply(.SD, sum, na.rm = T), Region = first(Region)), .SDcols = cols, by = .(Month, ID)][]}
# retainig the `|>` pipes for readability, impact is ~4us. 
collapse = \(x) x|>fgroup_by(Month, ID)|>fsummarise(Qty = fsum(Qty),Sales = fsum(Sales),Leads = fsum(Leads),Region = fsubset(Region, 1L),keep.group_vars = T)
dt <- as.data.table(df)
bench::mark(collapse(df), iterations = 10e3)[c(1,3,5,7)] ; bench::mark(data.table(dt), iterations = 10e3)[c(1,3,5,7)]
  expression     median mem_alloc n_itr
  <bch:expr>   <bch:tm> <bch:byt> <int>
1 collapse(df)    150us        0B  9988
2 data.table(dt)  796us     146KB  9939

对于如此小的数据集,collapse 和纯 data.table 之间的差异可以忽略不计。速度提高的原因可能是使用 fsum 而不是 base R sum.