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

13

Рассмотрим тиббл, где каждый столбец является символьным вектором, который может принимать много значений - скажем, от «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_))

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

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

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

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

Ответы:

6

Здесь, с правой стороны :=, мы можем указать 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    
akrun
источник
2
Я попытался использовать несколько функций преобразования rlang, но, очевидно, не выбрал правильную, но ваш подход работает - я думаю, что мне действительно нужно просто обработать преобразования типов в моей голове. Мой вопрос !! не работает, потому что он буквально оценивает строку символов. Ваш работает, потому что сначала преобразует строку символов в символ, а затем вычисляет символ, возвращая вектор. Я просто не мог обернуть голову, что это был порядок операций. Еще раз спасибо.
Арон
8

Вы можете использовать метод " 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    
IceCreamToucan
источник
2
Я еще не вошел в "кудрявую" привычку. Знаете ли вы, почему это работает, в то время как внешне идентичная версия «взрыва» ОП не сработала?
Камилла
Спасибо за упоминание кудрявый, который я слышал, предстояло. Ответ не работает для любой установленной версии rlang / dplyr; Я получаю ошибку с LHS. Если я заменим LHS своим LHS и укажу q1, я получу ту же проблему, что и выше; если я не цитирую q1, я получаю ошибку. Это возможно версия вещь.
Арон
1
Да rlang 0.4.0 была выпущена только в конце июня , так что если вы не обновляли его с тех пор это не будет работать для вас
IceCreamToucan
2
Я думаю, что bang-bang не сработал, потому что questionсначала нужно превратить в quosure ( question = enquo(question)) перед использованием в трубе dplyr. {{question}}эквивалентно !!enquo(question).
eipi10
2
Вам нужен enquo для первого вопроса вопроса, чтобы это было эквивалентно.
IceCreamToucan
7

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

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 vignette» (ищите «сращивание», чтобы увидеть соответствующие примеры). Обратите внимание, как !!!"склеивает" пары значений перекодировки в 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")))
eipi10
источник