繁体   English   中英

将 data.frame 列名传递给 function

[英]Pass a data.frame column name to a function

我正在尝试编写一个 function 来接受一个 data.frame ( x ) 和它的一column function 对 x 执行一些计算,然后返回另一个 data.frame。 我坚持使用最佳实践方法将列名传递给 function。

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

  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")

例如,我希望能够将 function 称为fun(df, B) 我考虑过但没有尝试过的其他选项:

  • column作为列号的 integer 传递。 我认为这可以避免substitute() 理想情况下,function 可以接受其中任何一个。
  • with(x, get(column)) ,但是,即使它有效,我认为这仍然需要substitute
  • 使用formula()match.call() ,我都没有太多经验。

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

您可以直接使用列名:

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"))

没有必要使用替代、评估等。

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

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")

这个答案将涵盖许多与现有答案相同的元素,但是这个问题(将列名传递给函数)经常出现,我希望有一个更全面地涵盖事物的答案。

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

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

这通常被认为是“最佳实践”,因为它是最难搞砸的方法。 将列名作为字符串传递是尽可能明确的。

以下两个选项更高级。 许多流行软件的使用这类技术,但使用起来需要更多的谨慎态度和技能,因为他们可以引入微妙的复杂性和失败的意料之外点。 Hadley 的 Advanced 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等,如果你真的知道自己在做什么。

我个人认为将列作为字符串传递非常难看。 我喜欢做这样的事情:

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

另一种方法是使用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 包(v0.2.1.9000) 于 2019 年 3 月 1 日创建

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

library(tidyverse)

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

作为一个额外的想法,如果需要将不带引号的列名传递给自定义函数,也许match.call()在这种情况下也很有用,作为deparse(substitute())的替代方法:

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 包(v0.2.1) 于 2019 年 1 月 11 日创建

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

Tung 的回答mgrund 的回答给出了整洁的评价 在这个答案中,我将展示我们如何使用这些概念来做类似于joran 的答案(特别是他的 function 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计算为一个表达式,更具体地说是一个符号(有时也称为名称),而不是 object。rlang 的替代品可以是:

  • ensym - 把它变成一个符号;
  • enexpr - 把它变成一个表达式;
  • enquo - 把它变成一个 quosure,一个表达式,它也指向 R 应该寻找变量来评估它的环境。

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

同样在第一行, deparse将表达式/符号转换为字符串。 您也可以使用as.characterrlang::as_string

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

同样在第二行,我们现在可以将eval更改为rlang::eval_tidy Eval 仍然可以与enexpr一起使用,但不能与 quosure 一起使用。 当你有一个 quosure 时,你不需要将环境传递给评估 function (就像 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 !! 操作员。 因为我们对左侧进行了操作,所以我们不能使用'normal' = ,而必须使用新语法:=

将列名转换为符号,然后使用 bang-bang 对其进行焦虑求值的常见操作有一个快捷方式:花哨的{{运算符:

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

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

如果您尝试在 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. 也可以在这里使用 tryCatch 错误处理。

这很好,但由于某种原因不适用于日期时间列。 它给了我这个错误 ..Error in Ops.POSIXt(dataset[[col_name_x]], z) :
没有为“POSIXt”对象定义“*”有什么建议吗?

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM