将 data.frame 列名传递给函数

我尝试编写一个函数来接受一个 data.frame (x)和一个来自它的 column。该函数对 x 执行一些计算,然后返回另一个 data.frame。我被最佳实践方法困住了,无法将列名传递给函数。

下面的两个最小示例 fun1fun2产生了预期的结果,能够以 max()为例在 x$column上执行操作。然而,两者都依赖于表面上(至少对我来说)的不雅

  1. 呼叫 substitute()可能还有 eval()
  2. 需要将列名作为字符向量传递。

fun1 <- function(x, column){
do.call("max", list(substitute(x[a], list(a = column))))
}


fun2 <- function(x, column){
max(eval((substitute(x[a], list(a = column)))))
}


df <- data.frame(B = rnorm(10))
fun1(df, "B")
fun2(df, "B")

例如,我希望能够调用 fun(df, B)函数。我考虑过但没有尝试过的其他选择:

  • column作为列号的整数传递。我认为这将避免 substitute()。理想情况下,该函数可以接受。
  • 但是,即使它工作,我认为这仍然需要 substitute
  • 利用 formula()match.call(),这两个我都没有多少经验。

子问题 : do.call()优于 eval()吗?

175233 次浏览

您可以直接使用列名:

df <- data.frame(A=1:10, B=2:11, C=3:12)
fun1 <- function(x, column){
max(x[,column])
}
fun1(df, "B")
fun1(df, c("B","A"))

没有必要使用替代品,eval 等等。

您甚至可以将所需的函数作为参数传递:

fun1 <- function(x, column, fn) {
fn(x[,column])
}
fun1(df, "B", max)

或者,使用 [[也可以一次选择一个列:

df <- data.frame(A=1:10, B=2:11, C=3:12)
fun1 <- function(x, column){
max(x[[column]])
}
fun1(df, "B")

就个人而言,我认为以字符串的形式传递列是相当丑陋的。我喜欢这样做:

get.max <- function(column,data=NULL){
column<-eval(substitute(column),data, parent.frame())
max(column)
}

它将产生:

> get.max(mpg,mtcars)
[1] 33.9
> get.max(c(1,2,3,4,5))
[1] 5

注意,data.frame 的规范是可选的,你甚至可以使用列的函数:

> get.max(1/mpg,mtcars)
[1] 0.09615385

这个答案将包含许多与现有答案相同的元素,但是这个问题(将列名传递给函数)出现得太频繁了,以至于我希望这个答案能够更全面地涵盖所有问题。

假设我们有一个非常简单的数据框架:

dat <- data.frame(x = 1:4,
y = 5:8)

我们希望编写一个函数来创建一个新的列 z,它是列 xy的总和。

这里一个非常常见的绊脚石是,一个自然的(但不正确的)尝试通常看起来是这样的:

foo <- function(df,col_name,col1,col2){
df$col_name <- df$col1 + df$col2
df
}


#Call foo() like this:
foo(dat,z,x,y)

这里的问题是 df$col1不计算表达式 col1。它只是在 df中查找字面上称为 col1的列。?Extract在“递归(类列表)对象”一节中描述了这种行为。

最简单、也是最受推荐的解决方案是简单地从 $切换到 [[,并将函数参数作为字符串传递:

new_column1 <- function(df,col_name,col1,col2){
#Create new column col_name as sum of col1 and col2
df[[col_name]] <- df[[col1]] + df[[col2]]
df
}


> new_column1(dat,"z","x","y")
x y  z
1 1 5  6
2 2 6  8
3 3 7 10
4 4 8 12

这通常被认为是“最佳实践”,因为这是最难搞砸的方法。将列名作为字符串传递大约是您所能得到的最明确的方式。

下面两个选项更高级。许多流行的软件包使用这些技术,但是使用它们 好吧需要更多的注意和技巧,因为它们可能会引入微妙的复杂性和意想不到的失败点。哈德利的高级 R 书的 这个部分是这些问题的一个极好的参考。

如果 真的希望避免用户键入所有这些引号,一个选项可能是使用 deparse(substitute())将无引号的空列名转换为字符串:

new_column2 <- function(df,col_name,col1,col2){
col_name <- deparse(substitute(col_name))
col1 <- deparse(substitute(col1))
col2 <- deparse(substitute(col2))


df[[col_name]] <- df[[col1]] + df[[col2]]
df
}


> new_column2(dat,z,x,y)
x y  z
1 1 5  6
2 2 6  8
3 3 7 10
4 4 8 12

坦率地说,这可能有点傻,因为我们实际上在做与 new_column1相同的事情,只是需要一大堆额外的工作来将名称转换为字符串。

最后,如果我们想要得到 真的,我们可能会决定,而不是传递两个列的名称添加,我们希望更灵活,并允许其他两个变量的组合。在这种情况下,我们可能会对包含两列的表达式使用 eval():

new_column3 <- function(df,col_name,expr){
col_name <- deparse(substitute(col_name))
df[[col_name]] <- eval(substitute(expr),df,parent.frame())
df
}

只是为了好玩,我仍然使用 deparse(substitute())作为新列的名称。在这里,以下所有措施都将奏效:

> new_column3(dat,z,x+y)
x y  z
1 1 5  6
2 2 6  8
3 3 7 10
4 4 8 12
> new_column3(dat,z,x-y)
x y  z
1 1 5 -4
2 2 6 -4
3 3 7 -4
4 4 8 -4
> new_column3(dat,z,x*y)
x y  z
1 1 5  5
2 2 6 12
3 3 7 21
4 4 8 32

因此,简短的答案基本上是: 将 data.frame 列名作为字符串传递,并使用 [[选择单个列。只有当你真正知道自己在做什么的时候才开始钻研 evalsubstitute等等。

作为额外的考虑,如果需要将未引号的列名传递给定制函数,那么在这种情况下,作为 deparse(substitute())的替代品,match.call()可能也很有用:

df <- data.frame(A = 1:10, B = 2:11)


fun <- function(x, column){
arg <- match.call()
max(x[[arg$column]])
}


fun(df, A)
#> [1] 10


fun(df, B)
#> [1] 11

如果列名中有一个输入错误,那么停止输入错误会更安全:

fun <- function(x, column) max(x[[match.call()$column]])
fun(df, typo)
#> Warning in max(x[[match.call()$column]]): no non-missing arguments to max;
#> returning -Inf
#> [1] -Inf


# Stop with error in case of typo
fun <- function(x, column){
arg <- match.call()
if (is.null(x[[arg$column]])) stop("Wrong column name")
max(x[[arg$column]])
}


fun(df, typo)
#> Error in fun(df, typo): Wrong column name
fun(df, A)
#> [1] 10

Reprex 软件包于2019-01-11创作(0.2.1)

我不认为我会使用这种方法,因为除了传递上述答案中指出的引用列名之外,还有额外的类型和复杂性,但是,这是一种方法。

另一种方法是使用 tidy evaluation方法。将数据框架的列作为字符串或空列名传递非常简单。更多关于 tidyeval 给你的信息。

library(rlang)
library(tidyverse)


set.seed(123)
df <- data.frame(B = rnorm(10), D = rnorm(10))

使用列名作为字符串

fun3 <- function(x, ...) {
# capture strings and create variables
dots <- ensyms(...)
# unquote to evaluate inside dplyr verbs
summarise_at(x, vars(!!!dots), list(~ max(., na.rm = TRUE)))
}


fun3(df, "B")
#>          B
#> 1 1.715065


fun3(df, "B", "D")
#>          B        D
#> 1 1.715065 1.786913

使用空列名

fun4 <- function(x, ...) {
# capture expressions and create quosures
dots <- enquos(...)
# unquote to evaluate inside dplyr verbs
summarise_at(x, vars(!!!dots), list(~ max(., na.rm = TRUE)))
}


fun4(df, B)
#>          B
#> 1 1.715065


fun4(df, B, D)
#>          B        D
#> 1 1.715065 1.786913
#>

Reprex 软件包于2019-03-01创作(0.2.1.9000)

如果您试图在 R 包中构建这个函数,或者只是想降低复杂性,您可以执行以下操作:

test_func <- function(df, column) {
if (column %in% colnames(df)) {
return(max(df[, column, with=FALSE]))
} else {
stop(cat(column, "not in data.frame columns."))
}
}

参数 with=FALSE“禁用将列当作变量来引用的能力,从而恢复了“ data.frame 模式”(每个 CRAN 文档)。如果提供的列名在 data.frame 中,if 语句是一种快速捕获方法。这里还可以使用 tryCatch 错误处理。

使用 dplyr,现在也可以通过在函数体中所需的列名周围使用双花括号 \{\{...}}来访问数据帧的特定列,例如,对于 col_name:

library(tidyverse)


fun <- function(df, col_name){
df %>%
filter(\{\{col_name}} == "test_string")
}

董建华的回答 Mgrund 的回答呈现了 整齐的评估。在这个答案中,我将展示我们如何使用这些概念来做类似于 Jordan 的回答的事情(特别是他的函数 new_column3)。这样做的目的是为了更容易看到基本评估和整洁评估之间的差异,以及可以在整洁评估中使用的不同语法。您将需要 rlangdplyr为此。

使用基础评估工具(joran 的回答) :

new_column3 <- function(df,col_name,expr){
col_name <- deparse(substitute(col_name))
df[[col_name]] <- eval(substitute(expr),df,parent.frame())
df
}

在第一行,substitute让我们将 col_name作为一个表达式计算,更确切地说,是一个符号(有时也称为名称) ,而不是一个对象。Rlang 的替代品可以是:

  • ensym-把它变成一个符号;
  • enexpr-将其转换为一个表达式;
  • enquo-将其转换为一个半确定式,这个表达式也指向 R 应该查找变量来计算它的环境。

大多数情况下,您希望拥有指向环境的指针。当你不是特别需要它的时候,拥有它很少会引起问题。因此,大多数情况下可以使用 enquo。在这种情况下,您可以使用 ensym来使代码更容易阅读,因为它可以使代码更清楚地显示 col_name是什么。

同样在第一行中,deparse将表达式/符号转换为字符串。

在第二行中,substituteexpr转换为“完整”表达式(不是符号) ,因此 ensym不再是一个选项。

同样在第二行中,我们现在可以将 eval改为 rlang::eval_tidy。Eval 仍然可以使用 enexpr,但不能使用一个法定伴侣。当您有一个 quosure 时,您不需要将环境传递给计算函数(就像 joran 对 parent.frame()所做的那样)。

以上建议的替代办法的一种组合可能是:

new_column3 <- function(df,col_name,expr){
col_name <- as_string(ensym(col_name))
df[[col_name]] <- eval_tidy(enquo(expr), df)
df
}

我们还可以使用 dplyr操作符,它允许数据屏蔽(将数据框架中的列作为变量进行计算,并按其名称调用它)。我们可以用 [[mutate改变将符号转换为字符 + 子集 df的方法:

new_column3 <- function(df,col_name,expr){
col_name <- ensym(col_name)
df %>% mutate(!!col_name := eval_tidy(enquo(expr), df))
}

为了避免将新列命名为“ col_ name”,我们使用 bang-bang !!操作符对其进行焦虑评估(而不是惰性评估,即 R 的缺省值)。因为我们在左边做了一个操作,所以我们不能使用“正常的”=,而必须使用新的语法 :=

将列名转换为符号,然后使用 bang-bang 对其进行焦虑评估的常见操作有一个捷径: 卷曲的 \{\{操作符:

new_column3 <- function(df,col_name,expr){
df %>% mutate(\{\{col_name}} := eval_tidy(enquo(expr), df))
}

我不是 R 语言评估的专家,可能做了过度简化,或者使用了错误的术语,所以请在评论中纠正我。我希望在比较这个问题的答案中使用的不同工具方面有所帮助。