Uso de evaluación no estándar basada en tidyeval en recodificación en el lado derecho de la mutación

13

Considere un tibble donde cada columna es un vector de caracteres que puede tomar muchos valores, digamos "A" a "F".

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

Deseo crear una función que tome el nombre de una columna como argumento, y recodifique esa columna para que cualquier respuesta "A" se convierta en un NA y el df se devuelva como está. La razón para diseñarlo de esta manera es encajar en una tubería más amplia que realiza una serie de operaciones utilizando una columna dada.

Hay muchas maneras de hacer esto. Pero estoy interesado en comprender cuál sería el mejor enfoque idiomático tidy_eval / tidyverse. Primero, el nombre de la pregunta debe estar en el lado izquierdo de un verbo mutado, por lo que usamos los operadores !!y :=adecuadamente. Pero entonces, ¿qué poner en el lado derecho?

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")

Mi pensamiento inicial fue que esto funcionaría:

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

Pero, por supuesto, el bang-bang dentro de la función solo devuelve la cadena de caracteres literal (por ejemplo, "q1"). Terminé tomando lo que parece una ruta hacky para hacer referencia a los datos en el lado derecho, usando el [[operador base R y confiando en la .construcción de dplyr, y funciona, por lo que en cierto sentido he resuelto mi problema subyacente:

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

Estoy interesado en recibir comentarios de personas que son muy buenas en tidyeval en cuanto a si hay una forma más idiomática de hacer esto, con la esperanza de que ver un ejemplo trabajado mejoraría mi comprensión del conjunto de funciones tidyeval en general. ¿Alguna idea?

aaron
fuente
Gracias, este es un enfoque inteligente: uso el enfoque funcional en otras partes de mi código y podría haber pensado en hacerlo también aquí. Sé que algunas personas fruncen el ceño al hablar de estilo de código en SO, pero ver algunos estilos diferentes de respuesta tan rápidamente me ha resultado muy fructífero.
aaron
1
Combinando varias ideas en esta pregunta, creo que esta es la versión más sucinta que funciona con q1(símbolo) y "q1"(cadena):df %>% mutate_at( vars(!!ensym(question)), recode, A = NA_character_)
Artem Sokolov

Respuestas:

6

Aquí, en el lado derecho de :=, podemos especificar symconvertir a símbolo y luego evaluar ( !!)

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    

Un mejor enfoque que funcionaría para las entradas citadas y no citadas es 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
fuente
2
Intenté dar vueltas con algunas de las funciones de conversión de argot, pero obviamente no elegí la correcta, pero su enfoque funciona; creo que realmente solo necesito hacer un flujo de trabajo de las conversiones de tipos en mi cabeza. Mi pregunta no funciona porque evalúa una cadena de caracteres literalmente. El suyo funciona porque primero convierte la cadena de caracteres en un símbolo y luego evalúa el símbolo, devolviendo el vector. Simplemente no podía entender que ese era el orden de las operaciones. Gracias de nuevo.
aaron
8

Puede usar el método "rizado rizado" ahora si tiene argot> = 0.4.0 .

Explicación gracias a @ eipi10:

Esto combina el proceso de dos pasos de citar y luego citar en un solo paso, por lo que {{question}}es equivalente a!!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    

Tenga en cuenta que, a diferencia del ensymenfoque, esto no funciona con los nombres de los personajes. Peor aún, hace lo incorrecto en lugar de simplemente dar un error.

fix_question(sample_df, 'q1')

# # A tibble: 3 x 2
#   q1    q2   
#   <chr> <chr>
# 1 q1    B    
# 2 q1    B    
# 3 q1    A    
IceCreamToucan
fuente
2
Todavía no he adquirido el hábito "rizado rizado". ¿Sabes por qué esto funciona, mientras que la versión aparentemente idéntica "bang bang" del OP no lo hizo?
camille
Gracias por mencionar rizado-rizado, que había escuchado que se acercaba. La respuesta no funciona para cualquier versión de rlang / dplyr que haya instalado; Me sale un error con el LHS. Si reemplazo el LHS con mi LHS y cito q1, obtengo el mismo problema que tuve anteriormente; Si no cito q1, aparece un error. Esto es posiblemente una cosa de la versión.
aaron
1
Yeah rlang 0.4.0 se acaba de lanzar a finales de junio, así que si no lo has actualizado desde entonces, esto no funcionará para ti
IceCreamToucan
2
Creo que el bang-bang no funcionó porque questionprimero debe convertirse en un quosure ( question = enquo(question)) antes de usarse en la tubería dplyr. {{question}}es equivalente a !!enquo(question).
eipi10
2
Necesita enquo para la primera instancia de pregunta también para que sea equivalente.
IceCreamToucan
7

Puede hacer que la función sea un poco más flexible permitiendo que también se ingrese un vector de valores recodificados como argumento. Por ejemplo:

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

Tenga en cuenta que recode.vecestá "entre comillas" !!!. Puede ver lo que está haciendo con este ejemplo, adaptado de la viñeta Programación con dplyr (busque "empalme" para ver los ejemplos relevantes). Observe cómo !!!"empalma" los pares de valores de recodificación en la recodefunción para que se utilicen como ...argumento en 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

Si desea ejecutar potencialmente la función de recodificación en varias columnas, puede convertirla en una función que solo tome el nombre de una columna y un vector de recodificación. Parece que este enfoque sería más amigable con las tuberías.

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>

O para recodificar una sola columna:

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