[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

上一篇:mysql-用于分组的mutate操作的dbplyr窗口函数


下一篇:R删除数据列基于dplyr包