前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >R tips:使用!!来增加dplyr的可操作性

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

作者头像
生信菜鸟团
发布2020-06-19 11:37:48
2.3K0
发布2020-06-19 11:37:48
举报
文章被收录于专栏:生信菜鸟团

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/

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-06-16,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 生信菜鸟团 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • R中代码的运行过程
  • 如何使用!!辅助dplyr完成编程工作
  • !!也不局限于dplyr,它是R MetaProgram的一部分
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档