R tips:使用!!来增加dplyr的可操作性

2020-06-19 11:37:48 浏览数 (1)

dplyr包在数据变换方面非常的好用,它有很多易用性的体现:比如书写数据内的变量名时不需要引号包裹,也不需要绝对引用,而这在多数baseR函数中都不是这样的,比如:

代码语言:javascript复制
library(tidyverse)
library(rlang)
library(magrittr)

data(mtcars)
### gear不需要引号包裹: "gear"
### 也不需要绝对引用: mtcars$gear
mtcars %>% group_by(gear) %>% summarise(mean_mpg=mean(mpg))
# # A tibble: 3 x 2
# gear mean_mpg
# <dbl>    <dbl>
# 1     3     16.1
# 2     4     24.5
# 3     5     21.4

### 然而很多的baseR函数都需要绝对引用
### 或者需要引号包裹
which(mtcars$mpg > median(mtcars$mpg))
# [1]  1  2  3  4  8  9 18 19 20 21 26 27 28 30 32
which(mtcars[, "mpg"] > median(mtcars[, "mpg"]))
# [1]  1  2  3  4  8  9 18 19 20 21 26 27 28 30 32

然而dplyr的这种易用性是有代价的,假如想要对分析工作稍微增加一些编程属性时,就会发现dplyr的异常情况,比如将分组变量赋值给一个变量,使用变量来进行分组:

代码语言:javascript复制
### 分组变量group_var无法完成工作
group_var <- "gear"
mtcars %>% group_by(group_var) %>% summarise(mean_mpg=mean(mpg))
#Error: Column `group_var` is unknown

### 所以下面的代码肯定是无法运行的
group_v <- c("vs", "am", "gear", "carb")
for (group in  group_v) {
    mtcars %>% group_by(group) %>% summarise(mean_mpg=mean(mpg))
}
#Error: Column `group` is unknown

上述困境可以使用!!来解决。

R中代码的运行过程

在介绍!!运算符之前,有必要先了解一下R中的代码是如何运行的。

在R console中输入一个代码,R就会返回代码的结果。

这个瞬间的过程其实需要两个步骤和三个阶段:

代码 --解析-> 语句 --执行-> 结果

输入的是文本代码(code),R会首先解析成语句(R称之为expression),expression在R中是一个树状结构,叫做abstract syntax tree(AST,抽象语义树),AST也是很多其他编程语言中的语句结构。

AST中的元素要么是Symbol,要么是常量,Symbol包括函数和变量。

比如对于语句:f(x, "y", 1),它的AST如下图所示,其中f、x是Symbol,”y"、1是常量。

执行expression(语句)即可获得结果,执行在R中叫做evaluation。

上述过程中,baseR中的函数parse可以进行解析工作,函数eval可以进行执行工作。

一个代码在R console中是直接运行到结束的,如果想要获得其中间态:语句,可以使用expr函数来捕获它。

这些函数在rlang包中有更加系统的相同角色的存在:parse的对应函数是parse_expr(语句还重新变为字符串,使用expr_text)。

expression的对应函数为expr,substitute的对应函数为enexpr。

eval的对应函数为eval_tidy。

转换为Symbol的函数as.name的对应函数为sym。

下面完成的上述操作的所需的函数都是rlang包中相应函数。

如何使用!!辅助dplyr完成编程工作

上面的例子中,之所以group_var不起作用,是因为dplyr直接将group_var当做变量名,然后去mtcars中寻找名字叫做group_var的列,这肯定是会报错的。

为了可以让它执行,我们可以需要告诉dplyr,先对group_var求值,获得真正的分组名:gear,使用gear进行后续操作,这个先求值的操作可以通过!!运算符来完成。如下:

代码语言:javascript复制
mtcars %>% group_by(!!sym(group_var)) %>% summarise(mean_mpg = mean(mpg))
# # A tibble: 3 x 2
# gear mean_mpg
# <dbl>    <dbl>
# 1     3     16.1
# 2     4     24.5
# 3     5     21.4

上述代码的实现逻辑是!!会告诉group_by函数,先对group_var进行求值,获得其值为gear,然后在进行后续操作。

为什么group_var需要先使用sym函数包裹?

sym是指的将group_var变为Symbol,这是由于上面code的所有操作层面都是上面提到的R代码运行阶段中的语句阶段,对于变量而言,其需要变为Symbol才可以操作。

使用循环完成多个分组汇总操作

代码语言:javascript复制
### 四个分组变量
group_v <- c("vs", "am", "gear", "carb")
### 构建一个函数
mean_manuel <- function(.data, .grp_v){
  group_v <- ensym(.grp_v)
  .data %>% group_by(!!group_v) %>% summarise(mean_mpg = mean(mpg))
}
### 调用函数,进行分组汇总的操作
map(group_v, ~mean_manuel(mtcars[1:6, ], !!sym(.)))
# [[1]]
# # A tibble: 2 x 2
# vs mean_mpg
# <dbl>    <dbl>
# 1     0     20.2
# 2     1     20.8
# 
# [[2]]
# # A tibble: 2 x 2
# am mean_mpg
# <dbl>    <dbl>
# 1     0     19.4
# 2     1     21.6
# 
# [[3]]
# # A tibble: 2 x 2
# gear mean_mpg
# <dbl>    <dbl>
# 1     3     19.4
# 2     4     21.6
# 
# [[4]]
# # A tibble: 3 x 2
# carb mean_mpg
# <dbl>    <dbl>
# 1     1     20.8
# 2     2     18.7
# 3     4     21

上述过程的实现过程是,首先map逐一将分组变量group_v的元素传递给mean_manual函数,传入mean_manual时,先使用!!对此元素进行求值,然后在传给mean_manual。

mean_manual获得此分组元素需要使用ensym,也就是ensym(.grp_v),因为此时的.grp_v是形参,如果要获取实参的值并转换为Symbol,需要使用ensym,而不是sym。

在mutate中完成新变量名的编程

假如想要在mutate中使用变量对新变量进行设置,其结果并不会如愿,比如,将新变量名var_name赋值为“gear_new",使用var_name进行mutate操作,结果却发现新变量为var_name,而不是我们想要的gear_new。

代码语言:javascript复制
var_name <- "gear_new"
mutate(mtcars[1:6, group_v], var_name = gear 1)
# vs am gear carb var_name
# 1  0  1    4    4        5
# 2  0  1    4    4        5
# 3  1  1    4    1        5
# 4  1  0    3    1        4
# 5  0  0    3    2        4
# 6  1  0    3    1        4

此时同样可以使用!!告诉mutate,先对var_name求值,然后再赋值。这里有一个小改动,由于var_name求值后是一个Symbol,在baseR是中无法将数据赋值给Symbol的,因此需要将=替换为:=。其他细节和上述例子都是类似的。

代码语言:javascript复制
var_name <- "gear_new"
mutate(mtcars[1:6, group_v], !!var_name := gear 1)
# vs am gear carb gear_new
# 1  0  1    4    4        5
# 2  0  1    4    4        5
# 3  1  1    4    1        5
# 4  1  0    3    1        4
# 5  0  0    3    2        4
# 6  1  0    3    1        4

也可以使用迭代,完成多个增添变量的操作,下述例子代表对vs am gear carb四列数据,各自加1后生成为新列,新列名字为原始名 “_new"。

代码语言:javascript复制
### 增添变量的函数
mutate_new <- function(.data, .var){
  var <- ensym(.var)
  var_name <- expr(!!paste0(var, "_new"))
  .data %>% mutate(!!var_name := !!var 1)
}
### 调用函数
map(group_v, ~mutate_new(mtcars[1:6, group_v], !!sym(.x)))
# [[1]]
# vs am gear carb vs_new
# 1  0  1    4    4      1
# 2  0  1    4    4      1
# 3  1  1    4    1      2
# 4  1  0    3    1      2
# 5  0  0    3    2      1
# 6  1  0    3    1      2
# 
# [[2]]
# vs am gear carb am_new
# 1  0  1    4    4      2
# 2  0  1    4    4      2
# 3  1  1    4    1      2
# 4  1  0    3    1      1
# 5  0  0    3    2      1
# 6  1  0    3    1      1
# 
# [[3]]
# vs am gear carb gear_new
# 1  0  1    4    4        5
# 2  0  1    4    4        5
# 3  1  1    4    1        5
# 4  1  0    3    1        4
# 5  0  0    3    2        4
# 6  1  0    3    1        4
# 
# [[4]]
# vs am gear carb carb_new
# 1  0  1    4    4        5
# 2  0  1    4    4        5
# 3  1  1    4    1        2
# 4  1  0    3    1        2
# 5  0  0    3    2        3
# 6  1  0    3    1        2

上述虽然可以完成,但是实际使用时,可能更倾向于将四个新变量放置到同一个数据框中,可以如下操作:

代码语言:javascript复制
### 添加新列的函数
mutate_news <- function(.data, .vars) {
  data <- enexpr(.data) #使用enexpr而不是ensym,因为后边调用时传入的实参是mtcars[1:6, group_v],它是一个语句,而不是symbol

  for (i in seq_along(.vars)) {
    var <- .vars[i]
    data <- expr(mutate(!!data, !!sym(paste0(var,"_new")) := !!sym(var)   1)) %>% eval_tidy() # 此处!!sym(paste0(var,"_new"))使用sym,而不是expr,是由于有!!的存在,paste0的运行结果是字符,需要转换为Symbol
    data <- enexpr(data) #上一步的data已经变为一个数据框,此处需要再将其转换为expr,使得循环可以持续进行
  }
  eval_tidy(data) #在对data求值即是结果
}

### 调用函数
mutate_news(mtcars[1:6, group_v], group_v)
# vs am gear carb vs_new am_new gear_new carb_new
# 1  0  1    4    4      1      2        5        5
# 2  0  1    4    4      1      2        5        5
# 3  1  1    4    1      2      2        5        2
# 4  1  0    3    1      2      1        4        2
# 5  0  0    3    2      1      1        4        3
# 6  1  0    3    1      2      1        4        2

!!也不局限于dplyr,它是R MetaProgram的一部分

比如对于ggstatplot包而言,它是一个统计及绘图的包,常规使用如下:

代码语言:javascript复制
### 两种写法都可以
mtcars %>% ggstatsplot::ggbetweenstats(x=gear, y=mpg)
mtcars %>% ggstatsplot::ggbetweenstats(x="gear", y="mpg")

但是如果想使用变量传入列名的话,就会报错:

代码语言:javascript复制
x="gear" y="mpg"
mtcars %>% ggstatsplot::ggbetweenstats(x=x, y=y)
#Error: Can't subset columns that don't exist.
#x The column `x` doesn't exist.
#Run `rlang::last_error()` to see where the error occurred.

此时同样的可以使用expr捕获上述过程的中间态,然后使用!!将x与y先求值,最后eval_tidy执行语句即可:

代码语言:javascript复制
expr(mtcars %>% ggstatsplot::ggbetweenstats(x = !!sym(x), y = !!sym(y))) %>% eval_tidy

PS:对于ggplot2而言也是一样的,它的aes也是不能直接使用变量传入列名,如果想要使用赋值了字符串的变量来传值的话,可以如上述操作。

但是也有更简单的的办法,它是?

参考资料

Advanced R:https://adv-r.hadley.nz/

0 人点赞