Как исправить проблему с {{}} и all_off(), если я хочу, чтобы dplyr-подобная функция могла работать как с NSE, так и с SE?

Я хочу написать функцию, которая будет принимать как символические имена столбцов, так и имена, переданные как переменная (строка).

Позвольте мне показать вам пример:

Данные:

> ( d <- data.frame(A=1:3, B=3:1) )
  A B
1 1 3
2 2 2
3 3 1

Теперь моя функция:

fn <- function(data, cols) {
  return(data %>% mutate(across({{cols}}, ~. * 2)))
}

Он хорошо работает для:

А) символические имена

> d %>% fn(cols = A)
  A B
1 2 3
2 4 2
3 6 1

> d %>% fn(cols = B)
  A B
1 1 6
2 2 4
3 3 2

> d %>% fn(cols = c(A, B))
  A B
1 2 6
2 4 4
3 6 2

Б) имена передаются как строки

> column <- "A"
> d %>% fn(cols = column)
  A B
1 2 3
2 4 2
3 6 1

> d %>% fn(cols = c("A", "B"))
  A B
1 2 6
2 4 4
3 6 2

Все идет нормально!

Теперь, когда я предоставляю внешний вектор › 1 столбец, он выдает предупреждение.

> d %>% fn(cols = columns)
Note: Using an external vector in selections is ambiguous.
i Use `all_of(columns)` instead of `columns` to silence this message.
i See <https://tidyselect.r-lib.org/reference/faq-external-vector.html>.
This message is displayed once per session.
  A B
1 2 6
2 4 4
3 6 2

Поэтому я добавил функцию all_of, которая хорошо работает со строками:

fn <- function(data, cols) {
  return(data %>% mutate(across(all_of({{cols}}), ~. * 2)))
}

> d %>% fn(cols = columns)
  A B
1 2 6
2 4 4
3 6 2

но выдает ошибку, когда я передаю символическое имя:

> d %>% fn(cols = A)

 Error: Problem with `mutate()` input `..1`.
x object 'A' not found
i Input `..1` is `across(all_of(A), ~. * 2)`.
Run `rlang::last_error()` to see where the error occurred. > d %>% fn(cols = B)

> d %>% fn(cols = c(A, B))

 Error: Problem with `mutate()` input `..1`.
x object 'A' not found
i Input `..1` is `across(all_of(c(A, B)), ~. * 2)`.
Run `rlang::last_error()` to see where the error occurred. 

Как это исправить, чтобы включить оба подхода и избежать предупреждения?


person Bastian    schedule 21.12.2020    source источник
comment
Итак, вы хотите, чтобы функция работала как для символов, так и для строк?   -  person Ronak Shah    schedule 21.12.2020
comment
Да, точно. И это работает хорошо, если имена столбцов указаны явно: fn(A, B) или fn(A, B). Когда я предоставляю внешний вектор, он печатает примечание о неоднозначности выбора. В дальнейшем это примечание превратится в предупреждение, а затем - в ошибку. Теоретически я мог бы предоставить 2 функции, например fn() для NSE и fn_() для SE, но мне бы очень хотелось этого избежать. Может быть, какая-то условная проверка параметров?   -  person Bastian    schedule 21.12.2020


Ответы (1)


Мое предложение было бы сохранить вашу первоначальную реализацию и предупреждение, потому что ситуация действительно неоднозначна. Рассмотреть возможность:

d <- data.frame(A=1:3, B=3:1, columns=4:6)  # Note the new column named columns
columns <- c("A","B")
d %>% fn(cols = columns)                    # Which `columns` should this use?

Затем пользователи вашей функции могут устранить неоднозначность, используя all_of() самостоятельно, и вы можете задокументировать это на странице справки функции.

d %>% fn(cols = all_of(columns))     # works without a warning

РЕДАКТИРОВАТЬ: Хотя я рекомендую описанный выше подход, другим способом является проверка существования переменной в вызывающей среде. Если переменная существует, предположите, что она содержит имена столбцов, и используйте ее в all_of(); в противном случае предположим, что имена столбцов указаны как есть:

fn <- function(data, cols) {
  varExist <- rlang::enexpr(cols) %>% 
    rlang::expr_deparse() %>%
    exists(envir=rlang::caller_env())
  
  if(varExist)
    data %>% mutate( across(all_of(cols), ~. *2) )
  else
    data %>% mutate( across({{cols}}, ~. * 2) )
}

rm(A)              # Ensure there is no variable called A
d %>% fn(cols=A)   # Mutate will operate on column A only

A <- c("A","B")    # A now contains column names
d %>% fn(cols=A)   # Mutate will operate on A and B
person Artem Sokolov    schedule 21.12.2020
comment
А, понял, не туда положил! К сожалению, это будет использоваться в основном в динамическом скрипте, где имя столбца будет получено от веб-сервиса, но эта функция будет доступна и конечным пользователям, не знакомым с dplyr и NSE. Они автоматически предоставят вектор строк. Я не могу рассчитывать на то, что они прочитают руководство (теоретически должен, на практике - меня убил менеджер). Столбцы с такими именами вряд ли появятся во фрейме данных в производственной среде, с которой я работаю. Я должен найти другой способ... Или уйти из dplyr ради этой задачи. - person Bastian; 21.12.2020
comment
Тем более, что сейчас это заметка, а в дальнейшем будет выдавать просто ошибку, которая сломает весь код. - person Bastian; 21.12.2020
comment
@Bastian Похоже, ваша функция будет в основном использоваться со стандартной оценкой в ​​​​производстве. Мое предложение состояло бы в том, чтобы взять на себя обязательство и отказаться от поддержки NSE. Однако, пожалуйста, смотрите мое редактирование, если вам абсолютно необходимо поддерживать оба. - person Artem Sokolov; 21.12.2020
comment
Это красивая работа. rlang такой мощный, что я должен изучить его, наконец, так как он открывает невероятные возможности с R. Большое спасибо, Артем. Это абсолютно соответствует моим потребностям. - person Bastian; 21.12.2020