将data.frame列名传递给函数

plupiseo  于 2022-12-06  发布在  其他
关注(0)|答案(8)|浏览(134)

我试图编写一个函数来接受一个data.frame(x)和来自它的一个column。该函数对x执行一些计算,然后返回另一个data.frame。我被最佳实践方法卡住了,无法将列名传递给函数。
下面的两个最小的例子fun1fun2产生了想要的结果,能够在x$column上执行操作,以max()为例。
1.调用substitute(),也可能调用eval()
1.需要将列名作为字符向量传递。

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()。理想情况下,函数可以接受任何一个。
  • with(x, get(column)),但是,即使它工作,我认为这仍然需要substitute
  • 利用formula()match.call(),这两个我都没有太多的经验。
  • 子问题 *:do.call()是否优于eval()
dxpyg8gm

dxpyg8gm1#

您可以直接使用列名:

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

mdfafbf12#

这个答案将涵盖许多与现有答案相同的元素,但这个问题(将列名传递给函数)经常出现,我希望有一个答案,涵盖的东西更全面一点。
假设我们有一个非常简单的 Dataframe :

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书中的This部分是解决这些问题的很好的参考。
如果您 * 真的 * 想让用户不必键入所有这些引号,一个选项可能是使用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等。

68bkxrlz

68bkxrlz3#

我个人认为将列作为字符串传递是相当丑陋的。我喜欢这样做:

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

请注意,数据框架的规格说明是可选的。您甚至可以使用列的函数:

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

agyaoht74#

另一种方法是使用tidy evaluation方法。将 Dataframe 的列作为字符串或空列名传递是非常简单的。请参阅有关tidyevalhere的更多信息。

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 package(v0.2.1.9000)于2019年3月1日创建

3ks5zfa0

3ks5zfa05#

使用dplyr,现在还可以访问 Dataframe 的特定列,只需在函数体中使用双花括号{{...}}将所需的列名括起来,例如col_name

library(tidyverse)

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

jmp7cifd6#

作为一个额外的想法,如果需要将不带引号的列名传递给自定义函数,那么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 package(v0.2.1)于2019年1月11日创建
我不认为我会使用这种方法,因为除了传递上面答案中指出的带引号的列名之外,还有额外的类型输入和复杂性,但这是一种方法。

wfveoks0

wfveoks07#

Tung's answermgrund's answer提供了tidy evaluation。在此答案中,我将说明如何使用这些概念来执行类似于joran's answer的操作(特别是他的函数new_column3)。这样做的目的是更容易看出基本求值和整洁求值之间的区别,并查看可用于整洁计算的不同语法。为此,您将需要rlangdplyr
使用基地评估工具(约兰的回答):

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将表达式/符号转换为字符串。您也可以使用as.characterrlang::as_string
在第二行中,substituteexpr转换为“完整”表达式(而不是符号),因此ensym不再是一个选项。
同样在第二行中,我们现在可以将eval改为rlang::eval_tidy。Eval仍然可以使用enexpr,但不能使用引号。当你有一个引号时,你不需要将环境传递给求值函数(就像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 !!运算符对它进行焦虑求值(与lazy-evaluate相反,默认值为R)。因为我们对左手进行了运算,所以不能使用'normal' =,而必须使用新的sintax :=
将列名转换为符号,然后使用bang-bang对其进行焦虑评估的常见操作有一个快捷方式: curl - curl {{运算符:

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

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

pgccezyw

pgccezyw8#

如果您尝试在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 mode”(根据CRAN documentation)。如果提供的列名在data.frame中,则if语句是一种快速捕获的方法。此处还可以使用tryCatch错误处理。

相关问题