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

Posted

技术标签:

【中文标题】使用 group_by > mutate > slice 的更有效方式【英文标题】:More efficient way of using group_by > mutate > slice 【发布时间】:2021-12-25 19:59:31 【问题描述】:

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

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 而不是 summarise,因为 df 以某种方式排列,我想在第一行保留 Region。

但是我认为可能有更有效的方法。请在这两个方面提供帮助。我这辈子都想不通。

【问题讨论】:

您的代码无法创建您预期的输出。我可以看到您想保留某个顺序,但我不明白为什么您不能通过在汇总后排列数据框来做到这一点。 你说你使用了这个代码df %&gt;% group_by(month, ID) %&gt;% mutate(across(.cols = Qty:Leads, ~sum(.x, na.rm = T))) %&gt;% slice(n = 1) (注意month应该是Month)来产生预期的结果。但是使用summarize 的结果是相同的。使用mutatesummarize 在这两种方法中更改顺序。所以我不明白为什么你不能使用summarize。无论如何,您必须安排数据框。 此外,您使用result$Region &lt;- NULL 在代码中故意删除了Region 列,但您的预期结果包含该列。我只是不明白你做了什么,你的期望是什么。 【参考方案1】:

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

    少做事 选择合适的后端 使用适当的数据结构

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 

|&gt;(需要 R 版本 > 3.5)是比%&gt;% 稍快的管道。它的结果是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()

注意,这个结果是一个 ungrouped 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

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

根据@www 的要求,包含纯data.table 方法,为简洁起见重写了包装函数。输入/输出分别是data.frame 对应collapsedata.table 对应data.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

【讨论】:

感谢您提供这个很好的答案并比较了几种方法。如果您不介意并且有时间,您可以将我的data.table 解决方案添加到您的基准比较中吗?只是好奇纯data.table 解决方案是否更有效。 @www pure data.table 可能是一个非常有效的 OP 解决方案,根据我的经验,collapsedata.table 在性能上非常接近。但是很难进行公平的比较,因为setDT 覆盖了data.frame,因此它不会在基准测试的每次迭代中产生相同的数据结构转换成本,并且输出也略有不同。此外,小样本数据不会给出全貌(我认为 DT 对于 2M 行更快)。您认为什么是公平的,包括 as_tibble 或为所有方法提供其原生 I/O 格式?【参考方案2】:

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

【讨论】:

我的错。我编辑了我的问题。我已经给出了使用 mutate 和 slice 的原因,我将不得不保留“Region”列 @FinRC 我已更新我的答案以保留Region 列。我仍然认为summarize 是正确的方法。如果需要保持一定的顺序,在summary之后排列数据框。 谢谢。看起来不错。有 data.table 解决方案吗?作为 data.frame,它又需要很长时间。 请注意,结果是一个分组的 tibble - 您可能需要在末尾添加 ungroup() @FinRC 我已经用data.table 解决方案更新了我的答案。

以上是关于使用 group_by > mutate > slice 的更有效方式的主要内容,如果未能解决你的问题,请参考以下文章

R语言dplyr包使用dplyr函数使用group_by函数summarise函数和mutate函数计算分组占比实战

当尝试在 group_by 和 mutate 中使用 get() 调用对象时,它会调出整个对象而不是分组对象。我该如何解决?

使用 dplyr、group_by 与 mutate() 或 summarise() & str_c() 或 paste() & 折叠连接字符串/行,但保持 NA & 所有字符

了解 dplyr 和 group_by

R语言dplyr包使用arrange函数group_by函数mutate函数生成分组数据的排名(rank)实战(Rank Variable by Group):升序排名降序排名以及相同排名的处理

R语言使用dplyr包使用group_by函数summarise函数和mutate函数计算分组下的均值标准差样本个数以及分组均值的95%执行区间对应的下限值和上限值(Calculate CI)