Переменные в качестве аргументов по умолчанию для функции, используя dplyr

Цель

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

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

Пример игрушки

Рассмотрим следующий пример, в котором я использую dplyr для вычисления того, изменилась ли переменная или нет (в данном случае это довольно бессмысленно):

library(dplyr)
mtcars  %>%
  mutate(cyl_change = cyl != lag(cyl))

Теперь lag также поддерживает альтернативное упорядочение следующим образом:

mtcars  %>%
  mutate(cyl_change = cyl != lag(cyl, order_by = gear))

Но что, если я хотел бы создать свою собственную версию lag, которая всегда заказывает gear?

Неудачные попытки

Наивный подход заключается в следующем:

lag2 <- function(x, n = 1L, order_by = gear) lag(x, n = n, order_by = order_by)

mtcars %>%
  mutate(cyl_change = cyl != lag2(cyl))

Но это, очевидно, вызывает ошибку:

не найден объект с именем 'gear

Более реалистичные варианты были бы такими, но они также не работают:

lag2 <- function(x, n = 1L) lag(x, n = n, order_by = ~gear)
lag2 <- function(x, n = 1L) lag(x, n = n, order_by = get(gear))
lag2 <- function(x, n = 1L) lag(x, n = n, order_by = getAnywhere(gear))
lag2 <- function(x, n = 1L) lag(x, n = n, order_by = lazyeval::lazy(gear))

Вопрос

Есть ли способ получить lag2, чтобы правильно найти gear в файле data.frame, который dplyr работает на?

  • Вы должны иметь возможность вызывать lag2 без предоставления gear.
  • Можно использовать lag2 в наборах данных, которые не называются mtcars (но имеют gear как переменные).
  • Предпочтительно gear будет аргументом по умолчанию для функции, поэтому его можно по-прежнему изменять, если это необходимо, но это не имеет решающего значения.

Ответы

Ответ 1

Вот мой возможный ответ, который я фактически использовал. Он в основном полагается на функцию, которая явно вводит любые значения функции по умолчанию в выражения объекта ленивых точек.

Полная функция (с комментариями) находится в конце этого ответа.

Ограничения:

  • Вам нужно по крайней мере некоторые дополнительные трюки, чтобы сделать эту работу красивой (см. ниже).
  • Он игнорирует примитивные функции, но я не думаю, что у них есть аргументы функции по умолчанию.
  • Для генерических чисел S3 вместо этого следует использовать фактический метод. Например, seq.default вместо seq. Если целью является введение значений по умолчанию в ваши собственные функции, тогда это обычно не будет проблемой.

Например, эту функцию можно использовать следующим образом:

dots <- lazyeval::all_dots(a = ~x, b = ~lm(y ~ x, data = d))
add_defaults_to_dots(dots)
$a
<lazy>
  expr: x
  env:  <environment: R_GlobalEnv>

$b
<lazy>
  expr: lm(formula = y ~ x, data = d, subset = , weights = , na.action = ,  ...
  env:  <environment: R_GlobalEnv>

Мы можем решить проблему игрушек из вопроса несколькими способами. Помните новую функцию и идеальный вариант использования:

lag2 <- function(x, n = 1L, order_by = gear) lag(x, n = n, order_by = order_by)

mtcars %>%
  mutate(cyl_change = cyl != lag2(cyl))
  • Используйте mutate_ с dots напрямую:

    dots <- lazyeval::all_dots(cyl_change = ~cyl != lag2(cyl), all_named = TRUE)
    dots <- add_defaults_to_dots(dots)
    mtcars %>% mutate_(.dots = dots)
    
  • Переопределить mutate, чтобы включить добавление значений по умолчанию.

    mutate2 <- function(.data, ...) {
      dots <- lazyeval::lazy_dots(...)
      dots <- add_defaults_to_dots(dots)
      dplyr::mutate_(.data, .dots = dots)
    }
    
    mtcars %>% mutate2(cyl_change = cyl != lag2(cyl))
    
  • Используйте S3-рассылку, чтобы сделать это по умолчанию для любого настраиваемого класса:

    mtcars2 <- mtcars
    class(mtcars2) <- c('test', 'data.frame')
    
    mutate_.test <- function(.data, ..., .dots) {
      dots <- lazyeval::all_dots(.dots, ..., all_named = TRUE)
      dots <- add_defaults_to_dots(dots)
      dplyr::mutate_(tibble::as_tibble(.data), .dots = dots)
    }
    mtcars2 %>% mutate(cyl_change = cyl != lag2(cyl))
    

В зависимости от варианта использования варианты 2 и 3 являются лучшими способами для этого. Вариант 3 фактически имеет полный предлагаемый вариант использования, но полагается на дополнительный класс S3.

Функции:

add_defaults_to_dots <- function(dots) {
  # A recursive function that continues to add defaults to lower and lower levels.
  add_defaults_to_expr <- function(expr) {
    # First, if a call is a symbol or vector, there is nothing left to do but
    # return the value (since it is not a function call).
    if (is.symbol(expr) | is.vector(expr) | class(expr) == "formula") {
      return(expr)
    }
    # If it is a function however, we need to extract it.
    fun <- expr[[1]]
    # If it is a primitive function (like `+`) there are no defaults, and we
    # should not manipulate that call, but we do need to use recursion for cases
    # like a + f(b).
    if (is.primitive(match.fun(fun))) {
      new_expr <- expr
    } else {
      # If we have an actual non-primitive function call, we formally match the
      # call, so abbreviated arguments and order reliance work.
      matched_expr <- match.call(match.fun(fun), expr, expand.dots = TRUE)
      expr_list <- as.list(matched_expr)
      # Then we find the default arguments:
      arguments <- formals(eval(fun))
      # And overwrite the defaults for which other values were supplied:
      given <- expr_list[-1]
      arguments[names(given)] <- given
      # And finally build the new call:
      new_expr <- as.call(c(fun, arguments))
    }
    # Then, for all function arguments we run the function recursively.
    new_arguments <- as.list(new_expr)[-1]
    null <- sapply(new_arguments, is.null)
    new_arguments[!null] <- lapply(new_arguments[!null], add_defaults_to_expr)
    new_expr <- as.call(c(fun, new_arguments))
    return(new_expr)
  }
  # For lazy dots supplied, separate the expression and environments.
  exprs <- lapply(dots, `[[`, 'expr')
  envrs <- lapply(dots, `[[`, 'env')
  # Add the defaults to the expressions.
  new_exprs <- lapply(exprs, add_defaults_to_expr)
  # Add back the correct environments.
  new_calls <- Map(function(x, y) {
    lazyeval::as.lazy(x, y)
  }, new_exprs, envrs)
  return(new_calls)
}

Ответ 2

Вот два подхода в data.table, однако я не считаю, что любой из них будет работать в dplyr в настоящее время.

В data.table все, что находится внутри j-expression (так же как второй аргумент [.data.table), сначала анализируется на пакет data.table, а не обычный R-парсер. В некотором смысле вы можете думать об этом как о отдельном парсере, живущем внутри обычного парсера языка, который равен R. Что делает этот парсер, он ищет, какие переменные вы использовали, которые являются фактически столбцами data.table, которые вы используете on, и все, что он находит, помещает его в среду j-expression.

Это означает, что вы должны позволить этому парсеру как-то знать, что gear будет использоваться или просто не будет частью среды. Ниже приведены две идеи для достижения этого.

"Простой" способ сделать это состоит в том, чтобы фактически использовать имя столбца в j-expression, где вы вызываете lag2 (в дополнение к некоторым обезьяньям внутри lag2):

dt = as.data.table(mtcars)

lag2 = function(x) lag(x, order_by = get('gear', sys.frame(4)))

dt[, newvar := {gear; lag2(cyl)}]
# or
dt[, newvar := {.SD; lag2(cyl)}]

Это решение имеет 2 нежелательных свойства: во-первых, я не уверен, насколько хрупким является sys.frame(4) - вы помещаете это в функцию или пакет, и я не знаю, что произойдет. Вы, вероятно, можете обойти его и выяснить правильную рамку, но это какая-то боль. Во-вторых - вам нужно указать конкретную переменную, в которой вы заинтересованы, в любом месте в выражении или сбросить все из них в среде с помощью .SD, снова в любом месте.

Второй вариант, который мне больше нравится, заключается в том, чтобы воспользоваться тем фактом, что парсер data.table оценивает выражения eval на месте до поиска переменных, поэтому, если вы используете переменную внутри какое-то выражение, которое вы eval, это сработало бы:

lag3 = quote(function(x) lag(x, order_by = gear))

dt[, newvar := eval(lag3)(cyl)]

Это не страдает от проблем другого решения с очевидным недостатком в том, что нужно вводить дополнительные eval.

Ответ 3

Это решение приближается:

Рассмотрим немного более легкий пример с игрушкой:

mtcars %>%
  mutate(carb2 = lag(carb, order_by = gear))

Мы по-прежнему используем аргумент lag и order_by, но не делаем никаких дальнейших вычислений с ним. Вместо того, чтобы придерживаться SE mutate, мы переключаемся на NSE mutate_ и делаем lag2 строят вызов функции как вектор символа.

lag2 <- function(x, n = 1, order_by = gear) {
  x <- deparse(substitute(x))
  order_by <- deparse(substitute(order_by))
  paste0('dplyr::lag(x = ', x, ', n = ', n, ', order_by = ', order_by, ')')
}

mtcars %>%
  mutate_(carb2 = lag2(carb))

Это дает нам идентичный результат выше.

Пример orginial toy может быть достигнут с помощью:

mtcars %>%
  mutate_(cyl_change = paste('cyl !=', lag2(cyl)))

Downsides:

  • Мы должны использовать SE mutate_.
  • Для расширенного использования, как в исходном примере, нам также нужно использовать paste.
  • Это не особенно безопасно, т.е. не сразу видно, откуда должен прибыть gear. Назначение значений gear или carb в глобальной среде, похоже, в порядке, но я предполагаю, что в некоторых случаях могут возникать неожиданные ошибки. Использование формулы вместо символьного вектора было бы более безопасным, но для этого требуется, чтобы для нее была назначена правильная среда, и это все еще большой знак вопроса для меня.

Ответ 4

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

lag2 <- function(x, df, n = 1L, order_by = df[['gear']], ...) {
  lag(x, n = n, order_by = order_by, ...)
}

hack <- mtcars  %>%  mutate(cyl_change = cyl != lag2(cyl, .))
ans <- mtcars  %>%  mutate(cyl_change = cyl != lag(cyl, order_by = gear))
all.equal(hack, ans)
# [1] TRUE
  • Нужно иметь возможность называть lag2 без необходимости передавать передачу.

Да, но вам нужно пройти ..

  1. Нужно иметь возможность использовать lag2 в наборах данных, которые не называются mtcars (но у них есть механизм, как один из них).

Это работает.

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

Это также работает:

hack_nondefault <- mtcars %>%  mutate(cyl_change = cyl != lag2(cyl, order_by = cyl))
ans_nondefault <- mtcars %>%  mutate(cyl_change = cyl != lag(cyl, order_by = cyl))
all.equal(hack_nondefault, ans_nondefault)
# [1] TRUE

Обратите внимание, что если вы вручную указали order_by, указание df на . больше не требуется, а использование становится идентичным оригиналу lag (что очень приятно).

Добавление

Кажется, трудно избежать использования SE mutate_, как в ответе OP, сделать какой-то простой хакер, как в моем ответе здесь, или сделать что-то более продвинутое, связанное с реверсивной техникой lazyeval::lazy_dots.

Доказательства:

1) dplyr::lag сам не использует никакое волшебство NSE

2) mutate просто вызывает mutate_(.data, .dots = lazyeval::lazy_dots(...))