Как передать аргумент '...' в формулу interp() внутри lazyeval
Я пытаюсь сделать некоторые параметризованные dplyr
Манипуляции. Простейший воспроизводимый пример для выражения корня проблемы заключается в следующем:
# Data
test <- data.frame(group = rep(1:5, each = 2),
value = as.integer(c(NA, NA, 2, 3, 3, 5, 7, 8, 9, 0)))
> test
group value
1 1 NA
2 1 NA
3 2 2
4 2 3
5 3 3
6 3 5
7 4 7
8 4 8
9 5 9
10 5 0
# Summarisation example, this is what I'd like to parametrise
# so that I can pass in functions and grouping variables dynamically
test.summary <- test %>%
group_by(group) %>%
summarise(group.mean = mean(value, na.rm = TRUE))
> test.summary
Source: local data frame [5 x 2]
group group.mean
<int> <dbl>
1 1 NaN
2 2 2.5
3 3 4.0 # Correct results
4 4 7.5
5 5 4.5
Это как далеко я остался один
# This works fine, but notice there's no 'na.rm = TRUE' passed in
doSummary <- function(d_in = data, func = 'mean', by = 'group') {
# d_in: data in
# func: required function for summarising
# by: the variable to group by
# NOTE: the summary is always for the 'value' column in any given dataframe
# Operations for summarise_
ops <- interp(~f(value),
.values = list(f = as.name(func),
value = as.name('value')))
d_out <- d_in %>%
group_by_(by) %>%
summarise_(.dots = setNames(ops, func))
}
> doSummary(test)
Source: local data frame [5 x 2]
group mean(value)
<int> <dbl>
1 1 NA
2 2 2.5
3 3 4.0
4 4 7.5
5 5 4.5
Попытка с параметром "na.rm"
# When I try passing in the 'na.rm = T' parameter it breaks
doSummary.na <- function(d_in = data, func = 'mean', by = 'group') {
# Doesn't work
ops <- interp(~do.call(f, args),
.values = list(f = func,
args = list(as.name('value'), na.rm = TRUE)))
d_out <- d_in %>%
group_by_(by) %>%
summarise_(.dots = setNames(ops, func))
}
> doSummary.na(test)
Error: object 'value' not found
Большое спасибо за вашу помощь!
1 ответ
Ваш заголовок упоминает ...
но твой вопрос не Если нам не нужно иметь дело с ...
, ответ становится намного проще, потому что нам не нужно do.call
вообще, мы можем вызвать функцию напрямую; просто замени ops
определение с:
ops = interp(~f(value, na.rm = TRUE),
f = match.fun(func), value = as.name('value'))
Обратите внимание, что я использовал match.fun
здесь вместо as.name
, Как правило, это лучшая идея, так как она работает "точно так же, как R" для поиска функций. Как следствие, вы не можете просто передать символ имени функции в качестве аргумента, но также имя функции или анонимную функцию:
doSummary.na(test, function (x, ...) mean(x, ...) / sd(x, ...)) # x̂/s?! Whatever.
Говоря об этом, ваша попытка установить имена столбцов также не удалась; вам нужно положить ops
в список, чтобы исправить это:
d_in %>%
group_by_(by) %>%
summarise_(.dots = setNames(list(ops), func))
… так как .dots
ожидает список операций (и setNames
также ожидает вектор / список). Тем не менее, этот код еще раз не будет работать, если вы передаете func
объект в функцию, которая не является символьным вектором. Чтобы сделать это более надежным, используйте что-то вроде этого:
fname = if (is.character(func)) {
func
} else if (is.name(substitute(func))) {
as.character(substitute(func))
} else {
'func'
}
d_in %>%
group_by_(by) %>%
summarise_(.dots = setNames(list(ops), fname))
Все становится сложнее, если вы действительно хотите разрешить прохождение ...
вместо известных аргументов, потому что (насколько я знаю) просто нет прямого пути прохождения ...
с помощью interp
и, как и вы, я не могу получить do.call
подход к работе.
Пакет ‹lazyeval› обеспечивает очень хорошую функцию make_call
, который помогает нам на пути к решению. Выше также может быть написано как
# Not good. :-(
ops = make_call(as.name(func), list(as.name('value'), na.rm = TRUE))
Это работает. НО только когда func
передается как символьный вектор. Как объяснено выше, это просто не гибко.
Тем не мение, make_call
просто оборачивает базу R as.call
и мы можем использовать это напрямую:
ops = as.call(list(match.fun(func), as.name('value'), na.rm = TRUE))
И теперь мы можем просто пройти ...
на:
doSummary = function (d_in = data, func = 'mean', by = 'group', ...) {
ops = as.call(list(match.fun(func), as.name('value'), ...))
fname = if (is.character(func)) {
func
} else if (is.name(substitute(func))) {
as.character(substitute(func))
} else {
'func'
}
d_in %>%
group_by_(by) %>%
summarize_(.dots = setNames(list(ops), fname))
}
Чтобы было ясно: то же самое может быть достигнуто с помощью interp
но я думаю, что для этого потребуется вручную построить formula
объект из списка, что составляет почти то же самое, что и в моем решении, а затем (избыточно) вызывает interp
на результат.
Я обычно нахожу, что, хотя lazyeval невероятно элегантен, в некоторых ситуациях base R предлагает более простые решения. Особенно, interp
это мощный substitute
замена, но bquote
довольно недоиспользуемая базовая R-функция уже обеспечивает многие из тех же синтаксических преимуществ. Большим преимуществом ‹lazyeval› объектов является то, что они несут вокруг себя среду оценки, в отличие от базовых выражений R. Однако это просто не всегда необходимо.