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的更多相关文章

  1. [R]在dplyr基础上编写函数-(1)eval

    tidyverse系列的R包虽然解放了大家的双手,但同时也束缚了我们重新编写函数的能力.在这一套语法中,要实现作为函数参数的字符串和变量之间的相互转换困难重重,但只要掌握了其中原理后,也就能够游刃有余 ...

  2. [R]在dplyr基础上编写函数-(2)substitute和quote

    关于这两个函数,官方是这么定义的: substitute returns the parse tree for the (unevaluated) expression expr, substitut ...

  3. C语言中的函数与数学上的函数很类似

    函数,是C语言编程中一个很重要的概念,重要到个人认为可以与指针并驾齐驱.好多教材.老师.学习资源都会专门挑出一章来讲函数.我今天也来说说函数,只不过我是从数学课上的函数来引申到C语言中的函数. 先来说 ...

  4. C++标准库里面没有字符分割函数split,自己编写函数实现字符串分割功能

    #include <vector> #include <string> #include <iostream> using namespace std; vecto ...

  5. 初学者入门web前端:C#基础知识:函数

    入行前端对函数的掌握程度有可能直接影响以后工作的效率,使用函数可以高效的编写编码,节省时间,所以我整理了C#中最基础的函数知识点,虽然我在学习中 遇到很多问题,但是只要能够解决这些问题,都是好的. 一 ...

  6. python函数之基础

    一: 函数的定义与调用 1.1 :函数的定义 def 关键字必需写 函数名必需是字母,数字,下划线组合,并且不能以数字开头 函数名后面要加括号然后“:” 为函数写注释是一个好习惯 # 函数的定义 de ...

  7. python基础之 初识函数&函数进阶

    函数基础部分 1.什么是函数? 函数是组织好的,可重复使用的,用来实现单一,或相关联功能的代码段.函数能提高应用的模块性,和代码的重复利用率. 2.定义函数 定义:def 关键词开头,空格之后接函数名 ...

  8. Python基础知识:函数

    1.定义函数和调用函数 #定义函数def def greet_user(username): '''简单的问候语''' print('Hello,%s!'%username) greet_user(' ...

  9. python 基础篇 11 函数进阶----装饰器

    11. 前⽅⾼能-装饰器初识本节主要内容:1. 函数名的运⽤, 第⼀类对象2. 闭包3. 装饰器初识 一:函数名的运用: 函数名是一个变量,但他是一个特殊变量,加上括号可以执行函数. ⼆. 闭包什么是 ...

随机推荐

  1. 基于JWT的Token身份验证

    ​ 身份验证,是指通过一定的手段,完成对用户身份的确认.为了及时的识别发送请求的用户身份,我们调研了常见的几种认证方式,cookie.session和token. 1.Cookie ​ cookie是 ...

  2. [no code][scrum meeting] Beta 4

    例会时间:5月16日11:30,主持者:伦泽标 下次例会时间:5月18日11:30,主持者:叶开辉 一.工作汇报 人员 昨日完成任务 明日要完成的任务 乔玺华 完成整体框架设计与登录逻辑 与后端对接 ...

  3. Noip模拟84 2021.10.27

    以后估计都是用\(markdown\)来写了,可能风格会有变化 T1 宝藏 这两天老是会的题打不对,还是要细心... 考场上打的是维护\(set\)的做法,但是是最后才想出来的,没有维护对于是没有交. ...

  4. [HNOI2009]双递增序列(洛谷P4728)+小烈送菜(内部训练题)——奇妙的dp

    博主学习本题的经过嘤嘤嘤: 7.22 : 听学长讲(一知半解)--自己推(推不出来)--网上看题解--以为自己会了(网上题解是错的)--发现错误以后又自己推(没推出来)--给学长发邮件--得到正确解法 ...

  5. 嵌入式单片机之STM32F103C8T6最小系统板电路设计参考

    STM32F103C8T6最小系统板电路设计 一.电源部分 设计了一个XH插座,以便使用3.7V锂电池供电,接入电压不允许超过6V. 二.指示灯部分 电源指示灯可以通过一个短路帽控制亮灭,以达到节电的 ...

  6. $time $stime $realtime

    1,$time The $time system function returns an integer that is a 64-bit time, scaled to the timescale ...

  7. 关于linux的fork的一点学习总结

    最近操作系统的实验要用到fork,于是去搜索了一下资料,很幸运地在博客中找到一篇深度好文: http://blog.csdn.net/jason314/article/details/5640969 ...

  8. 51nod_1006 最长公共子序列,输出路径【DP】

    题意: 给出两个字符串A B,求A与B的最长公共子序列(子序列不要求是连续的). 比如两个串为: abcicba abdkscab ab是两个串的子序列,abc也是,abca也是,其中abca是这两个 ...

  9. 让 AI 为你写代码 - 体验 Github Copilot

    前几天在群里看到有大神分享 Copoilot AI 写代码,看了几个截图有点不敢相信自己的眼睛.今天赶紧自己也来体验一下 Copoilot AI 写代码到底有多神奇. 申请 现在 Copoilot 还 ...

  10. .NET 生态系统的蜕变之 .NET 6云原生

    云原生的英文名是cloud native,native 就是土著的意思,也就是土著对当地的环境是非常适应的,在云的环境和传统的数据中心是非常不同的,云原生就是要用的云的技术来构建应用, 利用云的技术来 ...