Использование нестандартной оценки на основе tidyeval в recode в правой части mutate

Рассмотрим таблицу, в которой каждый столбец представляет собой вектор символов, который может принимать множество значений - скажем, от «A» до «F».

library(tidyverse)
sample_df <- tibble(q1 = c("A", "B", "C"), q2 = c("B", "B", "A"))

Я хочу создать функцию, которая принимает имя столбца в качестве аргумента и перекодирует этот столбец так, чтобы любой ответ «A» становился NA, а df в противном случае возвращался как есть. Причина создания этого способа состоит в том, чтобы вписаться в более широкий конвейер, который выполняет серию операций с использованием данного столбца.

Есть много способов сделать это. Но мне интересно понять, какой будет лучший идиоматический подход tidy_eval / tidyverse. Во-первых, имя вопроса должно быть слева от глагола изменения, поэтому мы используем операторы !! и := соответственно. Но что тогда надеть на правую сторону?

fix_question <- function(df, question) {
    df %>% mutate(!!question := recode(... something goes here...))
}

fix_question(sample_df, "q1") # should produce a tibble whose first column is (NA, "B", "C")

Моя первоначальная мысль заключалась в том, что это сработает:

df %>% mutate(!!question := recode(!!question, "A" = NA_character_))

Но, конечно, внутри функции просто возвращается буквальная строка символов (например, «q1»). В итоге я выбрал хакерский путь для ссылки на данные с правой стороны, используя базовый оператор R [[ и полагаясь на конструкцию . из dplyr, и это работает, так что в некотором смысле я решил свою основную проблему:

df %>% mutate(!!question := recode(.[[question]], "A" = NA_character_))

Мне интересно получить отзывы от людей, которые очень хорошо разбираются в tidyeval, о том, есть ли более идиоматический способ сделать это, в надежде, что просмотр рабочего примера улучшит мое понимание набора функций tidyeval в более общем плане. Есть предположения?


person aaron    schedule 11.10.2019    source источник
comment
Спасибо, это умный подход - я использую функциональный подход в других частях своего кода и мог бы подумать о том, чтобы сделать это и здесь. Я знаю, что некоторые люди недовольны разговорами о стиле кода на SO, но столь быстрое наблюдение за несколькими разными стилями ответов было для меня очень плодотворным.   -  person aaron    schedule 11.10.2019
comment
Объединив несколько идей в этом вопросе, я считаю, что это наиболее емкая версия, которая работает как с q1 (символ), так и с "q1" (строка): df %>% mutate_at( vars(!!ensym(question)), recode, A = NA_character_)   -  person Artem Sokolov    schedule 11.10.2019


Ответы (3)


Здесь, справа от :=, мы можем указать sym для преобразования в символ, а затем оценить (!!)

fix_question <- function(df, question) {
    df %>%
       mutate(!!question := recode(!! rlang::sym(question), "A" = NA_character_))
  }

fix_question(sample_df, "q1") 
# A tibble: 3 x 2
#  q1    q2   
#  <chr> <chr>
#1 <NA>  B    
#2 B     B    
#3 C     A    

Лучшим подходом, который будет работать как для цитируемого, так и для некотируемого ввода, является ensym

fix_question <- function(df, question) {
    question <- ensym(question)
    df %>%
       mutate(!!question := recode(!! question, "A" = NA_character_))
  }


fix_question(sample_df, q1)
# A tibble: 3 x 2
#  q1    q2   
#  <chr> <chr>
#1 <NA>  B    
#2 B     B    
#3 C     A    

fix_question(sample_df, "q1")
# A tibble: 3 x 2
#  q1    q2   
#  <chr> <chr>
#1 <NA>  B    
#2 B     B    
#3 C     A    
person akrun    schedule 11.10.2019
comment
Я пытался возиться с несколькими функциями преобразования rlang, но, очевидно, не выбрал правильную, но ваш подход работает - я думаю, что на самом деле мне просто нужно обработать преобразование типов в моей голове. Мой вопрос !! не работает, потому что он буквально оценивает символьную строку. Ваш работает, потому что он сначала преобразует строку символов в символ, а затем оценивает символ, возвращая вектор. Я просто не мог понять, что это порядок действий. Спасибо еще раз. - person aaron; 11.10.2019

Теперь вы можете использовать метод "curly curly", если у вас есть rlang> = 0.4.0.

Объяснение благодаря @ eipi10:

Это объединяет двухэтапный процесс цитирования и отмены цитирования в один шаг, поэтому {{question}} эквивалентно !!enquo(question)

fix_question <- function(df, question){
  df %>% mutate({{question}} := recode({{question}}, A = NA_character_))
}

fix_question(sample_df, q1)
# # A tibble: 3 x 2
#   q1    q2   
#   <chr> <chr>
# 1 NA    B    
# 2 B     B    
# 3 C     A    

Обратите внимание, что в отличие от подхода ensym, это не работает с именами персонажей. Хуже того, вместо того, чтобы просто выдать ошибку, он делает неправильные вещи.

fix_question(sample_df, 'q1')

# # A tibble: 3 x 2
#   q1    q2   
#   <chr> <chr>
# 1 q1    B    
# 2 q1    B    
# 3 q1    A    
person IceCreamToucan    schedule 11.10.2019
comment
Я еще не привыкла к вьющимся кудряшкам. Вы знаете, почему это работает, в то время как кажущаяся идентичная версия OP - нет? - person camille; 11.10.2019
comment
Спасибо за упоминание curly-curly, которое, как я слышал, скоро появится. Ответ не работает для любой установленной мной версии rlang / dplyr; Я получаю ошибку с LHS. Если я заменю LHS на мою LHS и процитирую q1, я получу ту же проблему, что и выше; если я не процитирую q1, я получу сообщение об ошибке. Возможно, это версия. - person aaron; 11.10.2019
comment
Да, rlang 0.4.0 был выпущен только в конце июня, поэтому, если вы не обновляли его с тех пор, это не сработает для вас - person IceCreamToucan; 11.10.2019
comment
Я думаю, что это не сработало, потому что question сначала нужно преобразовать в запрос (question = enquo(question)), прежде чем использовать его в канале dplyr. {{question}} эквивалентно !!enquo(question). - person eipi10; 11.10.2019
comment
Возможно, но я попробовал df %>% mutate(!!question := recode(!! enquo(question), A = NA_character_)) и получил ту же проблему, что и !!question. Опять же, это может быть потому, что я использую rlang 0.3.4. - person aaron; 11.10.2019
comment
Вам также нужно enquo для первого экземпляра вопроса, чтобы он был эквивалентен. - person IceCreamToucan; 11.10.2019

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

library(tidyverse)
sample_df <- tibble(q1 = c("A", "B", "C"), q2 = c("B", "B", "A"))

fix_question <- function(df, question, recode.vec) {

  df %>% mutate({{question}} := recode({{question}}, !!!recode.vec))

}

fix_question(sample_df, q1, c(A=NA_character_, B="Was B"))
  q1    q2   
1 <NA>  B    
2 Was B B    
3 C     A

Обратите внимание, что recode.vec "без кавычек" с !!!. Вы можете увидеть, что это происходит в этом примере, адаптированном из Программирование с помощью виньетки dplyr (выполните поиск по запросу "splice", чтобы увидеть соответствующие примеры). Обратите внимание, как !!! «соединяет» пары значений перекодирования в функцию recode, чтобы они использовались в качестве аргумента ... в recode.

x = c("A", "B", "C")
args = c(A=NA_character_, B="Was B")

quo(recode(x, !!!args))

<quosure>
expr: ^recode(x, A = <chr: NA>, B = "Was B")
env:  global

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

fix_question <- function(question, recode.vec) {

  recode({{question}}, !!!recode.vec)

}

sample_df %>% 
  mutate_at(vars(matches("q")), list(~fix_question(., c(A=NA_character_, B="Was B"))))
  q1    q2   
1 <NA>  Was B
2 Was B Was B
3 C     <NA>

Или перекодировать один столбец:

sample_df %>% 
  mutate(q1 = fix_question(q1, c(A=NA_character_, B="Was B")))
person eipi10    schedule 11.10.2019