簡體   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