使用group by 数据变慢_【R语言新书】2.6 其它数据操作

b883647680cc9707e1fd83550d9e9b7b.png
张敬信:《R语言编程—基于tidyverse》新书信息汇总​zhuanlan.zhihu.com
f8dc02f947258fbd62bff17da40eca5f.png

2.6.1 按行汇总

通常的数据操作逻辑都是 按列方式colwise) ,这使得按行汇总很困难。

dplyr 包提供了 rowwise() 函数为数据框创建 按行方式rowwise) ,使用 rowwise() 后并不是真的改变数据框,只是创建了按行元信息,改变了数据框的操作逻辑:

rf = df %>%
  rowwise()
rf %>%
  mutate(total = sum(c(chinese, math, english)))

25c7094f5a21756dd42c627536e83e46.png

函数 c_across() 是为 按行方式rowwise )在选定的列范围汇总数据而设计的,它没

有提供 .fns 参数,只能选择列。

rf %>%
  mutate(total = sum(c_across(where(is.numeric))))

a806cb239fa3554f7e5fd6cea37b1d9b.png

6a9d565bf576e93d8c52efb13fbbb725.png

若只是做按行求和或均值,直接用 rowSums() / rowMeans() 速度更快(不需要分割 - 汇总 - 合并),这里的 rowwise 行化后提供可以做更多的按行汇总的可能。

df %>%
  mutate(total = rowSums(across(where(is.numeric))))

c7ec7284b8e9520fc757091a98b3358d.png

按行方式(rowwise) 可以理解为一种特殊的分组:每一行作为一组。为 rowwise()

供行 ID ,用 summarise() 做汇总更能体会这一点:

df %>%
  rowwise(name) %>%
  summarise(total = sum(c_across(where(is.numeric))))

4383fe791df6e099cbcd46378ed10128.png

c4ebbe7166ea991bc4afc639b24c4dc1.png

rowwise 行化更让人惊喜的是:它的逐行处理的逻辑 + 嵌套数据框可以更好地实现批量建模,在 rowwise 行化模式下,批量建模就像计算新列一样自然。批量建模 [见下章] 可以用 " 嵌套数据框 + purrr::map_*() ”实现,但这种 rowwise 技术,具有异曲同工之妙。

2.6.2 窗口函数

汇总函数如 sum()mean() 接受 n个输入,返回 1 个值。而窗口函数是汇总函数的变体:接受 n 个输入,返回 n 个值。

例如, cumsum()cummean()rank()lead()lag() 等。

1. 排名和排序函数

共有 6 个排名函数,只介绍最常用的 min_rank() :从小到大排名( ties.method="min" ) ,若要从大到小排名需要套一个 desc()

df %>%
  mutate(ranks = min_rank(desc(math))) %>%
  arrange(ranks)

c086ea8cbe435c0870638d5cb2e1ba4d.png

2. 移位函数

lag() : 取前一个值,数据整体右移一位,相当于将时间轴滞后一个单位

lead() : 取后一个值,数据整体左移一位,相当于将时间轴超前一个单位

library(lubridate)
dt = tibble(
  day = as_date("2019-08-30") + c(0,4:6),
  wday = weekdays(day),
  sales = c(2,6,2,3),
  balance = c(30, 25, -40, 30)
)
dt %>%
  mutate(sales_lag = lag(sales), sales_delta = sales - lag(sales))

fa1902dfd5de00a72e5c899ff2f9f77e.png

注 :默认是根据行序移位,可用参数 order_by 设置根据某变量值大小顺序做移位。

3. 累计汇总

R base 已经提供了 cumsum()cummin()cummax()cumprod()

dplyr 包又提供了 cummean()cumany()cumall() ,后两者可与 filter() 连用

选择行:

  • cumany(x) : 用来选择遇到第一个满足条件之后的所有行
  • cumany(!x) : 用来选择遇到第一个不满足条件之后的所有行
  • cumall(x) : 用来选择所有行直到遇到第一个不满足条件的行
  • cumall(!x) : 用来选择所有行直到遇到第一个满足条件的行
dt %>%
  filter(cumany(balance < 0))         # 选择第一次透支之后的所有行

20adf53ede7f93a300ec0784328f5e3e.png
dt %>%
  filter(cumall(!(balance < 0)))           # 选择所有行直到第一次透支

4c0ae6a20d716359fa00834da35672cc.png

cec0dbf221cec520f922e9ca7d5f1d0c.png

2.6.3 滑窗迭代

"窗口函数"术语来自 SQL,意味着逐窗口浏览数据,将某函数重复应用于数据的每个 "窗口 " 。窗口函数的典型应用包括滑动平均、累计和以及更复杂如滑动回归。

5db06c7320c9b744c7463b4490281d30.png

slider::slide_*() 系列函数与 purrr::map_*() 是类似的,只是将 "逐元素迭代" 换成了"逐窗口迭代"。

金融时间序列数据经常需要计算滑动平均,比如计算 sales 的 3 日滑动平均:

library(slider)
dt %>%
  mutate(avg_3 = slide_dbl(sales, mean, .before = 1, .after = 1))

a006e68d846c5e163245eecccbcf3b49.png

输出每个滑动窗口更便于该 3 日滑动平均理解是如何计算的:

slide(dt$sales, ~ .x, .before = 1, .after = 1)

867bad8f7dc165ecb2dcb68aa9a3d238.png

0a8574bcc0703ee834cd94013e8402de.png

细心的读者可能发现了:上面计算的并不是真正的 3 日滑动平均,而是连续 3 个值的滑动平均。这是因为 slide() 函数默认是以行索引来滑动,如果日期也是连续日期这是没有问题的。但是若日期有跳跃,则结果可能不是你想要的。

那么,怎么计算真正的 3 日滑动平均呢?需要改用 slide_index() 函数,并提供日期索引,其基本格式为:

slide_index(.x, .i, .f, ...)

其中参数 .i 用来传递索引向量,实现根据 “.i 的当前元 + 其前 / 后若干元 ”创建相应的 .x 的滑动窗口。

来看一下的连续 3 日滑动窗口与连续 3 值滑动窗口的区别:

slide(dt$day, ~ .x, .before = 1, .after = 1)

3f40b751edc9a5290809af5fd289c0a2.png
slide_index(dt$day, dt$day, ~ .x, .before = 1, .after = 1)

b28f304cc5b03336c7ce6b5563d67683.png

fdcb3efeff98509056a987a7638a1dfd.png

最后,计算 sales 真正的 3 日滑动平均:

dt %>%
  mutate(avg_3 = slide_index_dbl(sales, day, mean, .before = 1, .after = 1))

cc75a467ea12d7b4f0b40d1486700cb0.png

2.6.4 整洁计算

tidyverse 代码之所以这么“整洁、优雅”,访问列只需要提供列名,不需要加引号,不需要加数据框环境 df$, 这是因为它内部采用了一套 整洁计算tidy evaluation )框架。

如果我们也想自定义这样的 "整洁、优雅" 函数,即在自定义函数中也这样 "整洁、优雅地传递参数,就需要掌握一点 整洁计算 的技术。

1. 数据屏蔽与整洁选择

整洁计算 的两种基本形式是:

  • 数据屏蔽:使得可以不用带数据框(环境变量)名字,就能使用数据框内的变量(数据变量),便于在数据集内计算值
  • 整洁选择:即各种选择列语法,便于使用数据集中的列

数据屏蔽为直接使用带来了代码简洁,但作为函数参数时的间接使用,正常是环境变量,要想作为数据变量使用,则需要用两个大括号括起来 {{var}}

var_summary = function(data, var) {
  data %>%
  summarise(n = n(), mean = mean({{var}}))
}
mtcars %>%
  group_by(cyl) %>%
  var_summary(mpg)

8470ebadef630a1b98470041d7383403.png

若是字符向量形式,想作为数据变量,则需要在函数体中使用 .data[[var]] ,这里 .data 是代替数据集的代词:

var_summary = function(data, var) {
  data %>%
  summarise(n = n(), mean = mean(.data[[var]]))
}
mtcars %>%
  group_by(cyl) %>%
  var_summary("mpg")

b1e7ccdf086fa7be11f6f3e7e16f23d5.png

还可用于对列名向量的循环机制,比如对因子型各列计算各水平值频数:

mtcars[,9:10] %>%
  names() %>%
  map(~ count(mtcars, .data[[.x]]))

e901d32ef3953893f33b947e8189e370.png

daf84cd95fe11c9f1db81a5961218e51.png

同样地,整洁选择作为函数参数时的间接使用,也需要用两个大括号括起来 {{vars}}

summarise_mean = function(data, vars) {
  data %>% 
    summarise(n = n(), across({{vars}}, mean))
}
mtcars %>%
  group_by(cyl) %>%
  summarise_mean(where(is.numeric))

30a2c1b1447602a8541272e1e5937044.png

若是字符向量形式,则需要借助函数 all_of()any_of() ,取决于你的选择:

vars = c("mpg", "vs")
mtcars %>% select(all_of(vars))
mtcars %>% select(!all_of(vars))

最后,再来看使用数据屏蔽或整洁选择同时修改列名的用法:

my_summarise = function(data, mean_var, sd_var) {
data %>%
  summarise("mean_{{mean_var}}" := mean({{mean_var}}),
            "sd_{{sd_var}}" := mean({{sd_var}}))
}
mtcars %>%
  group_by(cyl) %>%
  my_summarise(mpg, disp)

eca1bbf6320325a3900ac4d5c3535623.png

d7763273a270fe36b478dc2be9dc7927.png
my_summarise = function(data, group_var, summarise_var) {
  data %>%
    group_by(across({{group_var}})) %>%
    summarise(across({{summarise_var}}, mean, .names = "mean_{.col}"))
}
mtcars %>%
  my_summarise(c(am, cyl), where(is.numeric))

acf441bd3dba66859cf3b4de9a45dfa9.png

2. 引用与反引用

创建 tidyverse 风格的整洁函数,另一种做法是使用引用与反引用机制。这需要额外的两个步骤:

  • enquo() 让函数自动引用其参数
  • !! 反引用该参数

以自定义 计算分组均值 函数为例:

grouped_mean = function(data, summary_var, group_var) {
  summary_var = enquo(summary_var)
  group_var = enquo(group_var)
  data %>%
    group_by(!!group_var) %>%
    summarise(mean = mean(!!summary_var))
}
grouped_mean(mtcars, mpg, cyl)

a74be3b322baabaf2f9c7400c7a04dbc.png

要想修改结果列名,可借助 as_label() 函数从引用中提取名字:

grouped_mean = function(data, summary_var, group_var) {
  summary_var = enquo(summary_var)
  group_var = enquo(group_var)
  summary_nm = str_c("mean_", as_label(summary_var))
  group_nm = str_c("group_", as_label(group_var))
data %>%
  group_by(!!group_nm := !!group_var) %>%
  summarise(!!summary_nm := mean(!!summary_var))
}
grouped_mean(mtcars, mpg, cyl)

0074fb383e2819a80c7199dfd78045b8.png

要传递多个参数可以用 ...参数 。比如,我们还想让用于计算分组均值的 group_var

可以是任意多个,这就需要改用 ... 参数,为了更好地应付这种传递特意将该参数放在最后一个位置。另外,将其它函数参数都增加 . 前缀是一个好的做法,因为可以降低与 ... 参数的冲突风险。

grouped_mean = function(.data, .summary_var, ...) {
  summary_var = enquo(.summary_var)
  .data %>%
    group_by(...) %>%
    summarise(mean = mean(!!summary_var))
}
grouped_mean(mtcars, disp, cyl, am)

d74b75a5a673b2298888731ade4a3a12.png

... 参数不需要做引用和反引用就能正确工作,但若要修改结果列名就不行了,仍需要借助引用和反引用,但是要改用 enques()!!!

grouped_mean = function(.data, .summary_var, ...) {
  summary_var = enquo(.summary_var)
  group_vars = enquos(..., .named = TRUE)
  summary_nm = str_c("avg_", as_label(summary_var))
  names(group_vars) = str_c("groups_", names(group_vars))
  .data %>%
    group_by(!!!group_vars) %>%
    summarise(!!summary_nm := mean(!!summary_var))
}
grouped_mean(mtcars, disp, cyl, am)

3da1425b7d273a79e13f02e3ddb089fc.png

001d112c0ddba20787450bc3175f28f7.png

另外,参数 ... 也可以传递表达式:

filter_fun = function(df, ...) {
  df %>%
    filter(...)
}
mtcars %>% filter_fun(mpg > 25 & disp > 90)

d1348ca3632d601af53dd0629542be55.png

最后,再来看一个自定义绘制散点图的模板函数:

scatter_plot = function(df, x_var,y_var) {
  x_var = enquo(x_var)
  y_var = enquo(y_var)
  ggplot(data = df, aes(x = !!x_var, y = !!y_var)) +
    geom_point() +
    theme_bw() +
    theme(plot.title = element_text(lineheight = 1, face = "bold", hjust = 0.5)) +
    geom_smooth() +
    ggtitle(str_c(as_label(y_var), " vs. ", as_label(x_var)))
}
scatter_plot(mtcars, disp, hp)

88f5a9d4034f76e9162e8b95a50a1eee.png

2afa4aeac9931fa37ae1f757fe870f8f.png

参考文献

  1. Vignettes of dplyr
  2. Vignettes of slider
  3. Lionel Henry, Hadley Wickham. Tidy evaluation, https://tidyeval.tidyverse.org/
  4. Jesse Cambon, R-Bloggers:Practical Tidy Evaluation.

版权声明:原创作品,版权所有,禁止用于任何出版发行。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值