Передайте имя столбца data.frame функции

Я пытаюсь написать функцию для принятия 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()?

8 ответов

Решение

Вы можете просто использовать имя столбца напрямую:

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

Этот ответ будет охватывать многие из тех же элементов, что и существующие ответы, но эта проблема (передача имен столбцов функциям) возникает достаточно часто, поэтому я хотел, чтобы был ответ, который охватывал бы вещи немного более всесторонне.

Предположим, у нас есть очень простой фрейм данных:

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

Это часто считается "лучшей практикой", так как это метод, который сложнее всего испортить. Передача имен столбцов в виде строк примерно так же однозначно, как вы можете получить.

Следующие два варианта более продвинуты. Во многих популярных пакетах используются такие методы, но их правильное использование требует большей осторожности и навыков, поскольку они могут привести к незначительным сложностям и непредвиденным ошибкам. Этот раздел книги Хадли "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 как строки и использовать [[ выбрать отдельные столбцы. Только начать копаться в eval, substituteи т.д., если вы действительно знаете, что делаете.

Лично я считаю, что передача столбца в виде строки довольно уродлива. Мне нравится делать что-то вроде:

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

С участием dplyr теперь также можно получить доступ к определенному столбцу фрейма данных, просто используя двойные фигурные скобки {{...}} вокруг желаемого имени столбца в теле функции, например, для col_name:

library(tidyverse)

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

Другой способ заключается в использовании 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
#>

Создано в 2019-03-01 пакетом представлением (v0.2.1.9000)

Ответ Танга и ответ Мгрунда представили аккуратную оценку . В этом ответе я покажу, как мы можем использовать эти концепции, чтобы сделать что-то похожее на ответ Джорана (в частности, его функциюnew_column3). Цель этого состоит в том, чтобы облегчить понимание различий между базовой и аккуратной оценкой, а также увидеть различные синтаксисы, которые можно использовать в аккуратной оценке. Тебе понадобитсяrlangи для этого.

Использование инструментов базовой оценки (ответ Джорана):

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

В первой строке заставляет нас оценивать как выражение, точнее символ (также иногда называемый именем), а не объект. заменителями rlang могут быть:

  • - превращает его в символ;
  • - превращает его в выражение;
  • - превращает его в quosure, выражение, которое также указывает среду, в которой R должен искать переменные для его оценки.

В большинстве случаев вы хотите иметь этот указатель на среду. Когда он вам конкретно не нужен, его наличие редко вызывает проблемы. Таким образом, большую часть времени вы можете использоватьenquo. В этом случае вы можете использовать для облегчения чтения кода, так как он делает более понятным, чтоcol_nameявляется.

Также в первой строкеdeparseпревращает выражение/символ в строку. Вы также можете использоватьas.characterилиrlang::as_string.

Во второй строке указанsubstituteпревращаетсяexprв «полное» выражение (не символ), поэтомуensymэто уже не вариант.

Также во второй строке теперь мы можем изменитьevalкrlang::eval_tidy. Eval по-прежнему будет работать сenexpr, но не с квазурой. Когда у вас есть quosure, вам не нужно передавать среду в функцию оценки (как это сделал Джоран с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операторы, которые позволяют маскировать данные (оценивая столбец во фрейме данных как переменную, вызывая его по имени). Мы можем изменить метод преобразования символа в символ + подмножествоdfс использованием[[сmutate:

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

Чтобы новый столбец не назывался «имя_столбца», мы выполняем его тревожную оценку (в отличие от ленивой оценки, используемой по умолчанию в R) с помощью бах-бах!!оператор. Поскольку мы сделали операцию с левой стороны, мы не можем использовать «нормальный»=, и должен использовать новый синтаксис:=.

Обычная операция превращения имени столбца в символ, а затем тревожная оценка его с помощью бах-бах имеет короткий путь: кудрявый-кудрявый{{оператор:

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

Я не эксперт по оценке в R и, возможно, сделал чрезмерное упрощение или использовал неправильный термин, поэтому, пожалуйста, поправьте меня в комментариях. Я надеюсь, что помог в сравнении различных инструментов, используемых в ответах на этот вопрос.

В качестве дополнительной мысли, если необходимо передать имя столбца без кавычек в пользовательскую функцию, возможно, 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

Создано в 2019-01-11 пакетом представлением (v0.2.1)

Я не думаю, что я бы использовал этот подход, поскольку существует дополнительная типизация и сложность, чем просто передача имени столбца в кавычках, как указано в приведенных выше ответах, но это подход.

Если вы пытаетесь создать эту функцию в пакете 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). Оператор if - это быстрый способ определить, находится ли указанное имя столбца в пределах data.frame. Здесь также можно использовать обработку ошибок tryCatch.

Другие вопросы по тегам