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进行后续操作,这个先求值的操作可以通过!!
运算符来完成。如下:
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的,因此需要将=
替换为:=
。其他细节和上述例子都是类似的。
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执行语句即可:
expr(mtcars %>% ggstatsplot::ggbetweenstats(x = !!sym(x), y = !!sym(y))) %>% eval_tidy
PS:对于ggplot2而言也是一样的,它的aes也是不能直接使用变量传入列名,如果想要使用赋值了字符串的变量来传值的话,可以如上述操作。
但是也有更简单的的办法,它是?
参考资料
Advanced R:https://adv-r.hadley.nz/