使用 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 %>% group_by(month, ID) %>% mutate(across(.cols = Qty:Leads, ~sum(.x, na.rm = T))) %>% slice(n = 1)
(注意month
应该是Month
)来产生预期的结果。但是使用summarize
的结果是相同的。使用mutate
或summarize
在这两种方法中更改顺序。所以我不明白为什么你不能使用summarize
。无论如何,您必须安排数据框。
此外,您使用result$Region <- 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
|>
(需要 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()
注意,这个结果是一个 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
对应collapse
和data.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 解决方案,根据我的经验,collapse
和 data.table
在性能上非常接近。但是很难进行公平的比较,因为setDT
覆盖了data.frame
,因此它不会在基准测试的每次迭代中产生相同的数据结构转换成本,并且输出也略有不同。此外,小样本数据不会给出全貌(我认为 DT 对于 2M 行更快)。您认为什么是公平的,包括 as_tibble
或为所有方法提供其原生 I/O 格式?【参考方案2】:
summarize
对我来说比 mutate
和 slice
更有意义。这应该可以为您节省一些时间。
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 & 所有字符
R语言dplyr包使用arrange函数group_by函数mutate函数生成分组数据的排名(rank)实战(Rank Variable by Group):升序排名降序排名以及相同排名的处理
R语言使用dplyr包使用group_by函数summarise函数和mutate函数计算分组下的均值标准差样本个数以及分组均值的95%执行区间对应的下限值和上限值(Calculate CI)