[R]在dplyr函数的基础上编写函数-(3)tidyeval
dplyr的优点很明显,数据框操作简洁,如filter(df, x == 1, y == 2, z == 3)
等于df[df$x == 1 & df$y ==2 & df$z == 3, ]
。然而优点也是缺点,因为它的的参数不是透明的,这意味着你不能用一个看似等价的对象代替一个在别处定义的值。
df <- tibble(x = 1:3, y = 3:1)
filter(df, x == 1)
#错误
my_var <- x
filter(df, my_var == 1)
#同样错误
my_var <- "x"
filter(df, my_var == 1)
从上我们可以看出在针对dplyr编写函数时,传参并非我们想象的那么容易。
dplyr代码不明确,取决于在什么地方定义了什么变量。
filter(df, x == y)
#等价于以下任意代码:
df[df$x == df$y, ]
df[df$x == y, ]
df[x == df$y, ]
df[x == y, ]
预热一下
greet <- function(name) {
"How do you do, name?"
}
greet("Hadley")
[1] "How do you do, name?"
传参失败,因为引号把参数括起来,没有对输入的东西进行解释,它仅仅将输入作为一个字符串进行存储。一种解决的办法是使用paste函数将字符串粘连起来。
greet <- function(name) {
paste0("How do you do, ", name, "?")
}
greet("Hadley")
## [1] "How do you do, Hadley?"
另一个方法是使用glue包:“unquote”一个字符串内容(就是取消引号),用R表达式的值替换字符串。这就优雅地实现我们的函数,因为{name}被替换为name参数的值。
greet <- function(name) {
glue::glue("How do you do, {name}?")
}
greet("Hadley")
## How do you do, Hadley?
开始编程
1.对不同数据集编写函数
dplyr的第一个参数data是透明的,这个参数没有做任何特殊的处理。
mutate(df1, y = a + x)
mutate(df2, y = a + x)
mutate(df3, y = a + x)
mutate(df4, y = a + x)
我们想对以上数据编写一个函数来避免重复。
mutate_y <- function(df) {
mutate(df, y = a + x)
}
但这个函数存在一个缺点:如果其中一个变量不存在于数据框中,但存在于全局环境时,则有可能出错。
df1 <- tibble(x = 1:3)
a <- 10 #来自全局环境的变量
mutate_y(df1)
我们可以通过使用.data代词(pronoun)更明确地指定,来修正这种不确定性。这时如果变量不存在,这会抛出一个信息错误。
mutate_y <- function(df) {
mutate(df, y = .data$a + .data$x)
}
mutate_y(df1)
## Error in mutate_impl(.data, dots): Evaluation error: Column `a`: not found in data.
2.对不同表达式编写函数
如果我们想要函数的一个参数是变量名(如x)或者一个表达式(如x + y)是非常困难的,因此dplyr自动将输入括起来了(“quote”),因此它们都不是透明的。
比如我们想要创建一个可变分组用于数据汇总。
df <- tibble(
g1 = c(1, 1, 2, 2, 2),
g2 = c(1, 2, 1, 2, 1),
a = sample(5),
b = sample(5)
)
df %>%
group_by(g1) %>%
summarise(a = mean(a))
## # A tibble: 2 x 2
## g1 a
## <dbl> <dbl>
## 1 1. 2.50
## 2 2. 3.33
df %>%
group_by(g2) %>%
summarise(a = mean(a))
## # A tibble: 2 x 2
## g2 a
## <dbl> <dbl>
## 1 1. 2.00
## 2 2. 4.50
自然想到编写类似下面的函数:
my_summarise <- function(df, group_var) {
df %>%
group_by(group_var) %>%
summarise(a = mean(a))
}
my_summarise(df, g1)
## Error in grouped_df_impl(data, unname(vars), drop): Column `group_var` is unknown
报错了。将变量名换成字符串:
my_summarise(df, "g2")
## Error in grouped_df_impl(data, unname(vars), drop): Column `group_var` is unknown
仍然报错。
我们看到这两次报错是一样的。group_by()函数似乎自带引号功能:它不会评估输入,不管是啥,它都先将其括起来。
因此想要以上函数工作,我们需要做两件事:一是自己手动把输入括起来(这样上面编写的my_summarise()函数像group_by()一样可以输入一个裸的变量名);二是告诉group_by()不要再quote它的输入(因为我们已经做过了)。
那么,要怎样才能quote输入呢?我们不能使用""
,因为它返回一个字符串。我们需要的是一个能够捕捉表达式及其环境的函数。base R中的函数quote()以及操作符~貌似可以做,但它们都不是我们真正想要的。这里,引入一个新的函数:quo()
,它将输入括起来但不执行。
quo(g1)
## <quosure>
## expr: ^g1
## env: global
quo(a + b + c)
## <quosure>
## expr: ^a + b + c
## env: global
quo("a")
## <quosure>
## expr: ^"a"
## env: empty
quo() 返回的是一个quosure,这是一种特殊类型的公式。后续会讲。
现在我们已经捕捉到了这个表达式,怎么在group_by中使用它呢?如果直接使用这个函数的结果作为我们创建函数的输入不会起作用:
my_summarise(df, quo(g1))
## Error in grouped_df_impl(data, unname(vars), drop): Column `group_var` is unknown
错误还是一样。因为我们还没有告诉group_by()已经处理过quote的问题,因此这里需要unquote
(去掉括起)group_var变量。
在dplyr(和通用的tidyeval)中,可以使用!!
告诉动词函数你想要unquote输入从而让它执行,而不是括起来。
联合上面操作:
my_summarise <- function(df, group_var) {
df %>%
group_by(!! group_var) %>%
summarise(a = mean(a))
}
my_summarise(df, quo(g1))
## # A tibble: 2 x 2
## g1 a
## <dbl> <dbl>
## 1 1. 2.50
## 2 2. 3.33
虽然功能是实现了,但还是不够优雅,我们想要实现像group_by(df,g1)
一样方便使用。因此可以将括起改到函数中:
my_summarise <- function(df, group_var) {
quo_group_var <- quo(group_var)
print(quo_group_var) #为查看错误
df %>%
group_by(!! quo_group_var) %>%
summarise(a = mean(a))
}
my_summarise(df, g1)
## <quosure>
## expr: ^group_var
## env: 000000001DF8CAC8
## Error in grouped_df_impl(data, unname(vars), drop): Column `group_var` is unknown
但是又报错了。这里的问题是:quo(group_var)
总是返回~group_var
,而我们想将它替换为~g1
。
类似于字符串我们不用""
,而是用一些可以将参数变成字符串的函数,enquo()
就是这样的函数,它通过查看用户键入值,然后将该值返回为quosure(技术上来说,这是可以实现的,因为函数的参数都使用一种特殊的数据结构promise进行执行)。
my_summarise <- function(df, group_var) {
group_var <- enquo(group_var)
print(group_var)
df %>%
group_by(!! group_var) %>%
summarise(a = mean(a))
}
my_summarise(df, g1)
## <quosure>
## expr: ^g1
## env: global
## # A tibble: 2 x 2
## g1 a
## <dbl> <dbl>
## 1 1. 2.50
## 2 2. 3.33
对应于我们第二节讲到的base R中的quote()和substitute()函数,quo()等价于quote(),而enquo()等价于substitute()。
如果是处理多个分组变量呢?这种情况我们也更常见,接着往下看。
3.对不同的输入变量编写函数
summarise(df, mean = mean(a), sum = sum(a), n = n())
## # A tibble: 1 x 3
## mean sum n
## <dbl> <int> <int>
## 1 3. 15 5
summarise(df, mean = mean(a * b), sum = sum(a * b), n = n())
## # A tibble: 1 x 3
## mean sum n
## <dbl> <int> <int>
## 1 9.60 48 5
我们要对以上两项处理自定义编写一个函数,汇总三个变量。先试写一下:
my_var <- quo(a)
summarise(df, mean = mean(!! my_var), sum = sum(!! my_var), n = n())
## # A tibble: 1 x 3
## mean sum n
## <dbl> <int> <int>
## 1 3. 15 5
我们可以直接用quo作用于dplyr函数,这是调试很好的方法:
quo(summarise(df,
mean = mean(!! my_var),
sum = sum(!! my_var),
n = n()
))
## <quosure>
## expr: ^summarise(df, mean = mean(^a), sum = sum(^a), n = n())
## env: global
enquo --> !!
的方法我们已经了解了。下面正式编写函数:
my_summarise2 <- function(df, expr) {
expr <- enquo(expr)
summarise(df,
mean = mean(!! expr),
sum = sum(!! expr),
n = n()
)
}
my_summarise2(df, a)
## # A tibble: 1 x 3
## mean sum n
## <dbl> <int> <int>
## 1 3. 15 5
my_summarise2(df, a * b)
## # A tibble: 1 x 3
## mean sum n
## <dbl> <int> <int>
## 1 9.60 48 5
发现对不同的变量/表达式也是可以的。
4.对不同输入和输出变量编写函数
mutate(df, mean_a = mean(a), sum_a = sum(a))
## # A tibble: 5 x 6
## g1 g2 a b mean_a sum_a
## <dbl> <dbl> <int> <int> <dbl> <int>
## 1 1. 1. 1 3 3. 15
## 2 1. 2. 4 2 3. 15
## 3 2. 1. 2 1 3. 15
## 4 2. 2. 5 4 3. 15
## # ... with 1 more row
mutate(df, mean_b = mean(b), sum_b = sum(b))
## # A tibble: 5 x 6
## g1 g2 a b mean_b sum_b
## <dbl> <dbl> <int> <int> <dbl> <int>
## 1 1. 1. 1 3 3. 15
## 2 1. 2. 4 2 3. 15
## 3 2. 1. 2 1 3. 15
## 4 2. 2. 5 4 3. 15
## # ... with 1 more row
要对以上处理编写函数,看起来和前面的例子比较相似,但是有两个新的问题:
一是要将字符串连在一起创建新的变量名。因此我们需要quo_name()
将输入表达式转换为字符串。
二是!! mean_name = mean(!! expr)
不是合法的R代码。我们要使用由rlang
提供的:=
帮助函数。
my_mutate <- function(df, expr) {
expr <- enquo(expr)
mean_name <- paste0("mean_", quo_name(expr))
sum_name <- paste0("sum_", quo_name(expr))
mutate(df,
!! mean_name := mean(!! expr),
!! sum_name := sum(!! expr)
)
}
my_mutate(df, a)
## # A tibble: 5 x 6
## g1 g2 a b mean_a sum_a
## <dbl> <dbl> <int> <int> <dbl> <int>
## 1 1. 1. 1 3 3. 15
## 2 1. 2. 4 2 3. 15
## 3 2. 1. 2 1 3. 15
## 4 2. 2. 5 4 3. 15
## # ... with 1 more row
5.捕获多个变量
这里我们要将my_summarise()
扩展到可以接收任何数目的分组变量。需要3个改变:
- 一是在函数定义中使用
...
以便于我们的函数能够接收任意数目的参数。 - 二是使用
quos()
去捕获所有的...
作为公式列表。 - 三是使用
!!!
替换!!
将参数一个个切进group_by()
。
my_summarise <- function(df, ...) {
group_var <- quos(...)
df %>%
group_by(!!! group_var) %>%
summarise(a = mean(a))
}
my_summarise(df, g1, g2)
## # A tibble: 4 x 3
## # Groups: g1 [?]
## g1 g2 a
## <dbl> <dbl> <dbl>
## 1 1. 1. 1.00
## 2 1. 2. 4.00
## 3 2. 1. 2.50
## 4 2. 2. 5.00
!!!
将元素列表作为参数并把它们切开放入当前的函数调用。
args <- list(na.rm = TRUE, trim = 0.25)
quo(mean(x, !!! args))
## <quosure>
## expr: ^mean(x, na.rm = TRUE, trim = 0.25)
## env: global
args <- list(quo(x), na.rm = TRUE, trim = 0.25)
quo(mean(!!! args))
## <quosure>
## expr: ^mean(^x, na.rm = TRUE, trim = 0.25)
## env: global
以上是tidyeval的一些基础,下一节继续深入理论,以应对编写函数新的情况。
Ref: https://github.com/tidyverse/dplyr/blob/master/vignettes/programming.Rmd
https://www.jianshu.com/p/5eca388205d4
[R]在dplyr函数的基础上编写函数-(3)tidyeval的更多相关文章
- [R]在dplyr基础上编写函数-(1)eval
tidyverse系列的R包虽然解放了大家的双手,但同时也束缚了我们重新编写函数的能力.在这一套语法中,要实现作为函数参数的字符串和变量之间的相互转换困难重重,但只要掌握了其中原理后,也就能够游刃有余 ...
- [R]在dplyr基础上编写函数-(2)substitute和quote
关于这两个函数,官方是这么定义的: substitute returns the parse tree for the (unevaluated) expression expr, substitut ...
- C语言中的函数与数学上的函数很类似
函数,是C语言编程中一个很重要的概念,重要到个人认为可以与指针并驾齐驱.好多教材.老师.学习资源都会专门挑出一章来讲函数.我今天也来说说函数,只不过我是从数学课上的函数来引申到C语言中的函数. 先来说 ...
- C++标准库里面没有字符分割函数split,自己编写函数实现字符串分割功能
#include <vector> #include <string> #include <iostream> using namespace std; vecto ...
- 初学者入门web前端:C#基础知识:函数
入行前端对函数的掌握程度有可能直接影响以后工作的效率,使用函数可以高效的编写编码,节省时间,所以我整理了C#中最基础的函数知识点,虽然我在学习中 遇到很多问题,但是只要能够解决这些问题,都是好的. 一 ...
- python函数之基础
一: 函数的定义与调用 1.1 :函数的定义 def 关键字必需写 函数名必需是字母,数字,下划线组合,并且不能以数字开头 函数名后面要加括号然后“:” 为函数写注释是一个好习惯 # 函数的定义 de ...
- python基础之 初识函数&函数进阶
函数基础部分 1.什么是函数? 函数是组织好的,可重复使用的,用来实现单一,或相关联功能的代码段.函数能提高应用的模块性,和代码的重复利用率. 2.定义函数 定义:def 关键词开头,空格之后接函数名 ...
- Python基础知识:函数
1.定义函数和调用函数 #定义函数def def greet_user(username): '''简单的问候语''' print('Hello,%s!'%username) greet_user(' ...
- python 基础篇 11 函数进阶----装饰器
11. 前⽅⾼能-装饰器初识本节主要内容:1. 函数名的运⽤, 第⼀类对象2. 闭包3. 装饰器初识 一:函数名的运用: 函数名是一个变量,但他是一个特殊变量,加上括号可以执行函数. ⼆. 闭包什么是 ...
随机推荐
- 对比7种分布式事务方案,还是偏爱阿里开源的Seata,真香!(原理+实战)
前言 这是<Spring Cloud 进阶>专栏的第六篇文章,往期文章如下: 五十五张图告诉你微服务的灵魂摆渡者Nacos究竟有多强? openFeign夺命连环9问,这谁受得了? 阿里面 ...
- Convolutional Neural Network-week1编程题(TensorFlow实现手势数字识别)
1. TensorFlow model import math import numpy as np import h5py import matplotlib.pyplot as plt impor ...
- the Agiles Scrum Meeting 6
会议时间:2020.4.14 20:00 1.每个人的工作 今天已完成的工作 增量组:开发广播正文展开收起功能 issues:增量组:广播正文展开收起功能实现 完善组:修复冲刺部分的bug issue ...
- objdump--反汇编查看
转载:objdump命令_Linux objdump 命令用法详解:显示二进制文件信息 (linuxde.net) objdump命令 编程开发 objdump命令是用查看目标文件或者可执行的目标文件 ...
- linux shell 提示符
当我们打开或者登陆到一个终端的时候都会显示一长串提示符 void@void-ThinkPad-E450:~$ 提示符一般包含当前登陆的用户名 ,主机名,以及当前工作路径路径,最后都是以 $ 或者 # ...
- linux 内核源代码情景分析——linux 内核源码中的汇编语言代码
1. 用汇编语言编写部分核心代码的原因: ① 操作系统内核中的底层程序直接与硬件打交道,需要用到一些专用的指令,而这些指令在C语言中并无对应的语言成分: ② CPU中的一些特殊指令也没有对应的C语言成 ...
- 为什么IDEA不推荐你使用@Autowired ?
@Autowired注解相信每个Spring开发者都不陌生了!在DD的Spring Boot基础教程和Spring Cloud基础教程中也都经常会出现. 但是当我们使用IDEA写代码的时候,经常会发现 ...
- Linux下的 sniff-andthen-spoof程序编写
Linux下的 sniff-andthen-spoof程序编写 一.任务描述 在本任务中,您将结合嗅探和欺骗技术来实现以下嗅探然后欺骗程序.你需要两台机器在同一个局域网.从机器A ping IP_X, ...
- 西邮Linux兴趣小组第一次技术分享会
2016年10月30日晚,西邮Linux兴趣小组技术分享会在西安邮电大学长安校区东区逸夫教学楼FF305室成功举办.200多名来自全校不同专业的15,16级同学参加了此次分享会. 分享会于20:00正 ...
- Python MySSH 实现剧本执行器
通过封装Paramiko这个SSH模块,我们可以实现远程批量管理Linux主机,在上一篇文章中我们封装过一个MySSH类,这个类可以执行命令上传下载文件等,我们在这个类的基础上,实现一个简单的任务执行 ...