【问题标题】:Pass a data.frame column name to a function将 data.frame 列名传递给函数
【发布时间】:2011-02-08 03:47:18
【问题描述】:

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

下面的两个最小示例fun1 和fun2 产生了所需的结果,能够对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")

例如,我希望能够将该函数称为fun(df, B)。我考虑过但未尝试过的其他选项:

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

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

【问题讨论】:

    标签: r dataframe r-faq


    【解决方案1】:

    你可以直接使用列名:

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

    不需要使用substitute、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")
    

    【讨论】:

    • 有什么办法可以不将列名作为字符串传递吗?
    • 您需要传递作为字符引用的列名或列的整数索引。只需传递 B 就会假定 B 本身就是一个对象。
    • 我明白了。我不确定我是如何得到令人费解的替代品、eval 等的。
    • 谢谢!我发现 [[ 解决方案是唯一对我有用的解决方案。
    • 嗨@Luis,查看this answer
    【解决方案2】:

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

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

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

    我们想编写一个函数来创建一个新列 z,它是列 x 和 y 的总和。

    这里的一个非常常见的绊脚石是自然(但不正确)的尝试通常如下所示:

    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 列名作为字符串传递并使用[[ 选择单个列。如果您真的知道自己在做什么,请仅开始深入研究eval、substitute 等。

    【讨论】:

    • 不知道为什么这不是选定的最佳答案。
    • 我也是!很好的解释!
    • 如果我想通过列进行整齐选择怎么办?我想创建一个使用 pivot_longer 的函数。我的函数看起来像这样lineplots &lt;- function(df, colname){ ggplot(data = df %&gt;% pivot_longer(-colname), aes(x = colname, y = value)) + geom_point() + facet_grid(rows = vars(name), scales = "free_y") }',但它没有像我预期的那样工作
    • 绝对是最好的答案。干杯
    【解决方案3】:

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

    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
    

    【讨论】:

    • 你需要改掉认为使用引号是丑陋的习惯。不使用它们是丑陋的!为什么?因为您创建了一个只能以交互方式使用的函数 - 使用它进行编程非常困难。
    • 我很高兴看到更好的方法,但我看不出这与 qplot(x=mpg,data=mtcars) 之间的区别。 ggplot2 从不将列作为字符串传递,我认为这样做更好。为什么说这个只能交互使用?在什么情况下会导致不良后果?编程难度如何?在帖子的正文中,我展示了它是如何更加灵活的。
    • 5 年后 -) .. 为什么我们需要:parent.frame() ?
    • 7 年后:不使用引号仍然丑陋吗?
    【解决方案4】:

    另一种方法是使用tidy evaluation 方法。将数据框的列作为字符串或裸列名称传递是非常简单的。查看更多关于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 日创建

    【讨论】:

    【解决方案5】:

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

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

    【讨论】:

      【解决方案6】:

      另外考虑一下,如果需要将不带引号的列名传递给自定义函数,也许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 日创建

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

      【讨论】:

        【解决方案7】:

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

        【讨论】:

          猜你喜欢
          • 2023-02-05
          • 1970-01-01
          • 2017-04-15
          相关资源
          最近更新 更多