Как передать аргумент '...' в формулу 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. Однако это просто не всегда необходимо.

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