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