Передайте имя столбца data.frame функции
Я пытаюсь написать функцию для принятия data.frame (x
) и column
от него. Функция выполняет некоторые вычисления для x и позже возвращает еще один data.frame. Я застрял на методе передовой практики, чтобы передать имя столбца в функцию.
Два минимальных примера fun1
а также fun2
ниже производят желаемый результат, имея возможность выполнять операции на x$column
, с помощью max()
В качестве примера. Тем не менее, оба полагаются на, по-видимому (по крайней мере для меня) неэлеганс
- позвонить
substitute()
и, возможно,eval()
- необходимость передавать имя столбца как символьный вектор.
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.