Tidy evaluation


dplyr是数据处理,特别是操作data.frame最常用的工具,提供了一系列函数来实现常见的数据框的处理,如mutate(),select(),为了方便我们在交互环境下处理数据,编写更简单的代码,这些函数利用了非标准求值方法实现(NSE, non-standard evaluation).”

background,

NSE

NSE, 仅当参数是一个表达式(不能直接eval 的参数,比如 quote 的参数,或者在函数内部使用 substitute 使参数为 quoted). 当函数调用的时候,参数会被存储为 promise:参数的表达式及其在环境中的值,直到在函数内部实际使用到参数就会eval.substitute()捕捉表达式,变量值被替换,而没有用到参数的值,所以不会 eval.

data environment is evaluated before the enclosed environment, we say the data environment does overscope.

formula

expression的定义和 expression 类的区别

rlang

  • expr(), 等价于 bquote()
  • enexpr(), 等价于substitute()
  • quosure: quote and closure, formula 的子类,单边公式

dplyr package

dplyr是数据处理,特别是操作 data.frame 最常用的工具,提供了一系列函数来实现常见的数据框 df 的处理,如filter(),select(),为了方便我们在交互环境下处理数据,编写更简单的代码,这些函数利用了非标准求值方法实现(NSE, non-standard evaluation)1.

library(dplyr)
df <- data.frame(x = 1:3, y = 3:1)

# 等价于 df[df$x == 1 & df$y == 3, ]
filter(df, x == 1, y == 3) 
##   x y
## 1 1 3

但是 NSE 方法构建的函数,在交互式环境下可以正常使用,在函数内部调用用变量传递参数可能得到奇怪的结果或者错误.

# 用 var 传递其参数,结果异常
var <- "x"
filter(df, var == 1)
## [1] x y
## <0 rows> (or 0-length row.names)
my_filter <- function(df, var) {
  filter(df, var == 1)
}
my_filter(df = df, var = x)
## Error in filter_impl(.data, quo): Evaluation error: object 'x' not found.

因此,dplyr中对于这些函数提供了相应的标准求值版本,形如*_(),以便在自定义函数或者开发包的时候使用这类函数.

filter_(df, "x == 1")
##   x y
## 1 1 3

0.7版本后,dplyr 采用了一种新的 NSE方法 tidyeval 的策略,上述函数的参数不仅可以是

tidy eval

quote

  • quote(), substitute():quote(),literal quote,not quote the value ;而substitute(arg, env)quote the value of the arg, substituting any variables bound in env.

  • quo(), enquo()
  • quo_name(), :=

unquote

  • !!,UQ, unquote
# quo,  捕捉环境极其参数表达式, R general quasure, quosureish
# enquo, 作用于函数的参数,返回一个 quosure,成为 tidy eval quosure
q <- quo(quote)

# 输入 general quasure,返回 quasure
UQ(q)

# unquote, evaluate
"!!"(q)
  • !!!, UQS, unquoting splicing
  • UQE

创建函数

第一个参数是数据 .data (tbldata.frame),后续参数是表达式

数据作为参数

dplyr 中基本的数据操作函数对第一个参数.data者采用SE,所以直接构建函数即可:

mutate(df1, y = a + x)
mutate(df2, y = a + x)
mutate(df2, y = a + x)
my_mutate <- function(df) {
  mutate(df, y = a + x)
}

但是,针对 .data 不存在而全局环境中存在的变量,函数可能返回错误的结果,并且错误很难发现.

df1 <- tibble(x = 1:3)
## Warning: `list_len()` is soft-deprecated as of rlang 0.2.0.
## Please use `new_list()` instead
## This warning is displayed once per session.
a <- 10
my_mutate(df1)
## # A tibble: 3 x 2
##       x     y
##   <int> <dbl>
## 1     1    11
## 2     2    12
## 3     3    13

为此, dplyr定义了.data变量,用于表示输入的数据,也就是第一个参数,当对数据内不存在的变量操作时不会再向父环境中寻值,直接抛出错误2.

my_mutate <- function(df) {
  mutate(df, y = .data$a + .data$x)
}
my_mutate(df1)
## Error in mutate_impl(.data, dots): Evaluation error: Column `a` not found in `.data`.

建议在构建自己的函数的时候,使用.data$访问数据的变量,以免返回错误的结果.

表达式作为参数

后续的参数是非引用(unquoted)的表达式,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     3
## 2     2     3
df %>%
  group_by(g2) %>%
  summarise(a = mean(a))
## # A tibble: 2 x 2
##      g2     a
##   <dbl> <dbl>
## 1     1  3.33
## 2     2  2.5

构建由表达式作为参数的函数时会抛出错误.

my_summarise <- function(df, group_var) {
  df %>%
    group_by(group_var) %>%
    summarise(a = mean(a))
}

# 不会用g1来替换 group_var,只是 quote(group_var)
my_summarise(df, g1) 
## Error in grouped_df_impl(data, unname(vars), drop): Column `group_var` is unknown

解决办法是首先 quote参数(函数 quo(),确保参数 group_var可以是变量),然后调用group_by()时unquote (!!)该参数求值.

my_summarise2 <- function(df, group_var) {
  df %>% 
    group_by(!!group_var) %>% 
    summarise(a = mean(a))
}

my_summarise2(df, quo(g1))
## # A tibble: 2 x 2
##      g1     a
##   <dbl> <dbl>
## 1     1     3
## 2     2     3

利用 enquo()3, 构建 dplyr 类似的函数,`my_summarise(df, group_var)

my_summarise3 <- function(df, group_var) {
  group_var <- enquo(group_var)
  df %>% 
    group_by(!!group_var) %>% 
    summarise(a = mean(a))
}

my_summarise3(df, g1)
## # A tibble: 2 x 2
##      g1     a
##   <dbl> <dbl>
## 1     1     3
## 2     2     3

多个输入变量

diff_input_summarise <- function(df, expr) {
  expr <- enquo(expr)
  
  summarise(df,
    mean = mean(!!expr),
    sum = sum(!!expr),
    n = n()
  )
}

diff_input_summarise(df, g1)
## # A tibble: 1 x 3
##    mean   sum     n
##   <dbl> <dbl> <int>
## 1   1.6     8     5
diff_input_summarise(df, a * b)
## # A tibble: 1 x 3
##    mean   sum     n
##   <dbl> <int> <int>
## 1  10.4    52     5

多个输入和输出变量

形如mutate(df, mean_a = mean(a), sum_a = sum(a)),根据输入变量创建输出变量,解决方法是利用quo_name()创建输出变量的名称.4

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     5     5      3    15
## 2     1     2     1     2      3    15
## 3     2     1     3     1      3    15
## 4     2     2     4     4      3    15
## 5     2     1     2     3      3    15

任意多参数

  • 多个参数用...表示
  • 使用quos()来quote ...,构成一个 quosure 列表
  • !!!代替!!分割 quos的参数,把列表分割为多参数
args <- list(na.rm = TRUE, trim = 0.25)
quo(mean(x, !!! args))
## <quosure>
## expr: ^mean(x, na.rm = TRUE, trim = 0.25)
## env:  global
multiple_arg_summarise <- function(df, ...) {
  group_by <- quos(...)

  df %>%
    group_by(!!!group_by) %>%
    summarise(a = mean(a))
}

multiple_arg_summarise(df, g1, g2)
## # A tibble: 4 x 3
## # Groups:   g1 [?]
##      g1    g2     a
##   <dbl> <dbl> <dbl>
## 1     1     1   5  
## 2     1     2   1  
## 3     2     1   2.5
## 4     2     2   4

引用 Quoting

引用就是不计算其值而直接捕捉表达式,函数中表达式组成的参数调用时首先都是引用这些参数. 最常见的是公式~

y ~ a + b
## y ~ a + b

另外,quote(expr)直接返回其参数表达式

# 计算表达式
toupper(letters[1:5])
## [1] "A" "B" "C" "D" "E"
# 捕捉表达式
quote(toupper(letters[1:5]))
## toupper(letters[1:5])

其他

  • sym: character to name,可以构建函数输入参数为字符,而非表达式, name can also be quoted

  1. NSE 和 SE 的区别:NSE,函数的参数传递代码,SE,函数的参数直接传递其值,具体可参考 https://adv-r.hadley.nz/nse,后面准备专门写一篇详细介绍 NSE 的文章.

  2. 在开发包时,使用 dplyr 中函数,时使用.data访问变量值可以避在R CMD check产生 undefined global variables 的NOTE (@importFrom rlang .data).

  3. quo(expression)返回一个quosure,相当于单边公式,表达式及其环境(https://adv-r.hadley.nz/nse),可以在任何表达式中使用.enquo(arg),捕捉函数的调用环境,并对 arg 中的变量进行求值,返回最终的表达式, 只适用于函数内部. quo()enquo()类似于 R 中的quote()substitute().

  4. !!mean_name = mean(!!expr)是非法的 R 表达式,需要使用 rlang 中:=的来创建.


一路嘿嘿

Bioinformatics, R enthusiast. Thoughts on reasarch, personal experience and other distractions.

Tags

blogdown font ggplot git github github pages Homebrew html hugo icon liner algebra linux machine learning R scholar sublime text 3 tidyverse