使用 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
对我来说比 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
我们可以应用通用加速策略:
- 少做
- 选择合适的后端
- 使用适当的数据结构
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
我们可以看到 collapse
与 dplyr
相比,内存效率更高,速度明显更快。 dtplyr
方法包含在这里,因为它的 时间复杂度 不同于 dplyr
并且它的重写方便。
根据@www 的要求,包含纯 data.table
方法,为简洁起见重写了包装函数。输入/输出分别是 collapse
的 data.frame
和 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
.
我有一个看起来像这样的数据框
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
对我来说比 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
我们可以应用通用加速策略:
- 少做
- 选择合适的后端
- 使用适当的数据结构
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
我们可以看到 collapse
与 dplyr
相比,内存效率更高,速度明显更快。 dtplyr
方法包含在这里,因为它的 时间复杂度 不同于 dplyr
并且它的重写方便。
根据@www 的要求,包含纯 data.table
方法,为简洁起见重写了包装函数。输入/输出分别是 collapse
的 data.frame
和 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
.