Utilice nombres de variables dinámicas en `dplyr`

168

Quiero usar dplyr::mutate()para crear múltiples columnas nuevas en un marco de datos. Los nombres de columna y sus contenidos deben generarse dinámicamente.

Datos de ejemplo de iris:

library(dplyr)
iris <- tbl_df(iris)

He creado una función para mutar mis nuevas columnas de la Petal.Widthvariable:

multipetal <- function(df, n) {
    varname <- paste("petal", n , sep=".")
    df <- mutate(df, varname = Petal.Width * n)  ## problem arises here
    df
}

Ahora creo un bucle para construir mis columnas:

for(i in 2:5) {
    iris <- multipetal(df=iris, n=i)
}

Sin embargo, dado que mutate piensa que varname es un nombre de variable literal, el ciclo solo crea una nueva variable (llamada varname) en lugar de cuatro (llamada petal.2 - petal.5).

¿Cómo puedo mutate()usar mi nombre dinámico como nombre de variable?

Timm S.
fuente
1
No insisto en mutar, pregunto si es posible. Tal vez es solo un pequeño truco que no sé. Si hay otra forma, escuchemos.
Timm S.
Creo que hay un espacio para el reloj en el paquete de lazyeval
Baptiste
1
En este punto, dplyrtiene una viñeta completa sobre evaluación no estándar
Gregor Thomas
16
La viñeta ni siquiera menciona mutate_, y realmente no es obvio por las otras funciones cómo usarla.
nacnudus

Respuestas:

191

Dado que está construyendo dinámicamente un nombre de variable como un valor de carácter, tiene más sentido hacer la asignación utilizando la indexación de data.frame estándar que permite valores de caracteres para los nombres de columna. Por ejemplo:

multipetal <- function(df, n) {
    varname <- paste("petal", n , sep=".")
    df[[varname]] <- with(df, Petal.Width * n)
    df
}

La mutatefunción hace que sea muy fácil nombrar nuevas columnas a través de parámetros con nombre. Pero eso supone que conoce el nombre cuando escribe el comando. Si desea especificar dinámicamente el nombre de la columna, también debe crear el argumento con nombre.


versión dplyr> = 0.7

La última versión de dplyr(0.7) hace esto usando :=para asignar dinámicamente nombres de parámetros. Puedes escribir tu función como:

# --- dplyr version 0.7+---
multipetal <- function(df, n) {
    varname <- paste("petal", n , sep=".")
    mutate(df, !!varname := Petal.Width * n)
}

Para obtener más información, consulte el formulario de documentación disponible vignette("programming", "dplyr").


dplyr (> = 0.3 y <0.7)

La versión ligeramente anterior de dplyr(> = 0.3 <0.7), fomentó el uso de alternativas de "evaluación estándar" para muchas de las funciones. Consulte la viñeta de evaluación no estándar para obtener más información ( vignette("nse")).

Entonces, la respuesta es usar en mutate_()lugar de mutate()y hacer:

# --- dplyr version 0.3-0.5---
multipetal <- function(df, n) {
    varname <- paste("petal", n , sep=".")
    varval <- lazyeval::interp(~Petal.Width * n, n=n)
    mutate_(df, .dots= setNames(list(varval), varname))
}

dplyr <0.3

Tenga en cuenta que esto también es posible en versiones anteriores de las dplyrque existían cuando la pregunta se planteó originalmente. Requiere un uso cuidadoso de quotey setName:

# --- dplyr versions < 0.3 ---
multipetal <- function(df, n) {
    varname <- paste("petal", n , sep=".")
    pp <- c(quote(df), setNames(list(quote(Petal.Width * n)), varname))
    do.call("mutate", pp)
}
MrFlick
fuente
24
Gracias, eso es útil. por cierto, siempre creo variables realmente dramáticas.
Timm S.
27
Jeje. Ese es probablemente uno de mis errores tipográficos favoritos que he hecho en mucho tiempo. Creo que lo dejaré.
MrFlick
1
do.call()probablemente no hace lo que crees que hace: rpubs.com/hadley/do-call2 . Vea también la viñeta nse en la versión de desarrollo de dplyr.
hadley
44
Entonces, si entiendo su punto @hadley, he actualizado lo do.callanterior para usar do.call("mutate")y citar dfen la lista. ¿Es eso lo que estabas sugiriendo? Y cuando la lazyevalversión de dplyres la versión lanzada, mutate_(df, .dots= setNames(list(~Petal.Width * n), varname))¿sería una mejor solución?
MrFlick
1
¿Qué sucede si necesito el encabezado de columna variable no solo en el lado izquierdo de la tarea sino también en el derecho? por ejemplo mutate(df, !!newVar := (!!var1 + !!var2) / 2), no funciona :(
Mario Reutter
55

En la nueva versión de dplyr( 0.6.0en espera de abril de 2017), también podemos hacer una asignación ( :=) y pasar variables como nombres de columna quitando las comillas ( !!) para no evaluarlo

 library(dplyr)
 multipetalN <- function(df, n){
      varname <- paste0("petal.", n)
      df %>%
         mutate(!!varname := Petal.Width * n)
 }

 data(iris)
 iris1 <- tbl_df(iris)
 iris2 <- tbl_df(iris)
 for(i in 2:5) {
     iris2 <- multipetalN(df=iris2, n=i)
 }   

Comprobación de la salida basada en @ MrFlick's multipetalaplicado en 'iris1'

identical(iris1, iris2)
#[1] TRUE
akrun
fuente
26

Después de muchas pruebas y errores, el patrón me pareció UQ(rlang::sym("some string here")))realmente útil para trabajar con cadenas y verbos dplyr. Parece funcionar en muchas situaciones sorprendentes.

Aquí hay un ejemplo con mutate. Queremos crear una función que agregue dos columnas, donde pase la función de ambos nombres de columna como cadenas. Podemos usar este patrón, junto con el operador de asignación :=, para hacer esto.

## Take column `name1`, add it to column `name2`, and call the result `new_name`
mutate_values <- function(new_name, name1, name2){
  mtcars %>% 
    mutate(UQ(rlang::sym(new_name)) :=  UQ(rlang::sym(name1)) +  UQ(rlang::sym(name2)))
}
mutate_values('test', 'mpg', 'cyl')

El patrón también funciona con otras dplyrfunciones. Aquí está filter:

## filter a column by a value 
filter_values <- function(name, value){
  mtcars %>% 
    filter(UQ(rlang::sym(name)) != value)
}
filter_values('gear', 4)

O arrange:

## transform a variable and then sort by it 
arrange_values <- function(name, transform){
  mtcars %>% 
    arrange(UQ(rlang::sym(name)) %>%  UQ(rlang::sym(transform)))
}
arrange_values('mpg', 'sin')

Para select, no necesita usar el patrón. En su lugar, puede usar !!:

## select a column 
select_name <- function(name){
  mtcars %>% 
    select(!!name)
}
select_name('mpg')
Tom Roth
fuente
Tus consejos funcionan muy bien, pero tengo un pequeño problema. Cambio una columna inicial myCola una url (por ejemplo) y copio la columna anterior myColInitialValueal final del marco de datos dfcon un nuevo nombre. Pero un which(colnames(df)=='myCol')envío de vuelta el col # de myColInitialValue. Todavía no escribí un problema porque no encontré una reprex. Mi objetivo es para el escapeparámetro de DT::datatable(). Yo uso escape=FALSEen esperar eso. Con las constantes no funciona también, pero el paquete DT también parece tener la columna # incorrecta. :)
phili_b
Parece que las variables dinámicas no son la causa. (btw reprex agregado)
phili_b
Gracias por esta respuesta! Este es un ejemplo muy sencillo de cómo lo utilicé:varname = sym("Petal.Width"); ggplot(iris, aes(x=!!varname)) + geom_histogram()
bdemarest
Esto funcionó para mí dentro de una fórmula donde !! varname no estaba funcionando.
daknowles
12

Aquí hay otra versión, y podría decirse que es un poco más simple.

multipetal <- function(df, n) {
    varname <- paste("petal", n, sep=".")
    df<-mutate_(df, .dots=setNames(paste0("Petal.Width*",n), varname))
    df
}

for(i in 2:5) {
    iris <- multipetal(df=iris, n=i)
}

> head(iris)
Sepal.Length Sepal.Width Petal.Length Petal.Width Species petal.2 petal.3 petal.4 petal.5
1          5.1         3.5          1.4         0.2  setosa     0.4     0.6     0.8       1
2          4.9         3.0          1.4         0.2  setosa     0.4     0.6     0.8       1
3          4.7         3.2          1.3         0.2  setosa     0.4     0.6     0.8       1
4          4.6         3.1          1.5         0.2  setosa     0.4     0.6     0.8       1
5          5.0         3.6          1.4         0.2  setosa     0.4     0.6     0.8       1
6          5.4         3.9          1.7         0.4  setosa     0.8     1.2     1.6       2
usuario2946432
fuente
8

Con rlang 0.4.0tenemos operadores rizado-rizado ( {{}}) lo que hace que esto sea muy fácil.

library(dplyr)
library(rlang)

iris1 <- tbl_df(iris)

multipetal <- function(df, n) {
   varname <- paste("petal", n , sep=".")
   mutate(df, {{varname}} := Petal.Width * n)
}

multipetal(iris1, 4)

# A tibble: 150 x 6
#   Sepal.Length Sepal.Width Petal.Length Petal.Width Species petal.4
#          <dbl>       <dbl>        <dbl>       <dbl> <fct>     <dbl>
# 1          5.1         3.5          1.4         0.2 setosa      0.8
# 2          4.9         3            1.4         0.2 setosa      0.8
# 3          4.7         3.2          1.3         0.2 setosa      0.8
# 4          4.6         3.1          1.5         0.2 setosa      0.8
# 5          5           3.6          1.4         0.2 setosa      0.8
# 6          5.4         3.9          1.7         0.4 setosa      1.6
# 7          4.6         3.4          1.4         0.3 setosa      1.2
# 8          5           3.4          1.5         0.2 setosa      0.8
# 9          4.4         2.9          1.4         0.2 setosa      0.8
#10          4.9         3.1          1.5         0.1 setosa      0.4
# … with 140 more rows

También podemos pasar nombres de variables entre comillas / sin comillas que se asignarán como nombres de columna.

multipetal <- function(df, name, n) {
   mutate(df, {{name}} := Petal.Width * n)
}

multipetal(iris1, temp, 3)

# A tibble: 150 x 6
#   Sepal.Length Sepal.Width Petal.Length Petal.Width Species  temp
#          <dbl>       <dbl>        <dbl>       <dbl> <fct>   <dbl>
# 1          5.1         3.5          1.4         0.2 setosa  0.6  
# 2          4.9         3            1.4         0.2 setosa  0.6  
# 3          4.7         3.2          1.3         0.2 setosa  0.6  
# 4          4.6         3.1          1.5         0.2 setosa  0.6  
# 5          5           3.6          1.4         0.2 setosa  0.6  
# 6          5.4         3.9          1.7         0.4 setosa  1.2  
# 7          4.6         3.4          1.4         0.3 setosa  0.900
# 8          5           3.4          1.5         0.2 setosa  0.6  
# 9          4.4         2.9          1.4         0.2 setosa  0.6  
#10          4.9         3.1          1.5         0.1 setosa  0.3  
# … with 140 more rows

Funciona igual con

multipetal(iris1, "temp", 3)
Ronak Shah
fuente
4

También estoy agregando una respuesta que aumenta esto un poco porque llegué a esta entrada cuando buscaba una respuesta, y esto tenía casi lo que necesitaba, pero necesitaba un poco más, que obtuve a través de la respuesta de @MrFlik y el R lazyeval viñetas.

Quería hacer una función que pudiera tomar un marco de datos y un vector de nombres de columna (como cadenas) que quiero convertir de una cadena a un objeto Date. No pude encontrar la forma de as.Date()tomar un argumento que es una cadena y convertirlo en una columna, así que lo hice como se muestra a continuación.

A continuación se muestra cómo hice esto a través de SE mutate ( mutate_()) y el .dotsargumento. Las críticas que lo mejoran son bienvenidas.

library(dplyr)

dat <- data.frame(a="leave alone",
                  dt="2015-08-03 00:00:00",
                  dt2="2015-01-20 00:00:00")

# This function takes a dataframe and list of column names
# that have strings that need to be
# converted to dates in the data frame
convertSelectDates <- function(df, dtnames=character(0)) {
    for (col in dtnames) {
        varval <- sprintf("as.Date(%s)", col)
        df <- df %>% mutate_(.dots= setNames(list(varval), col))
    }
    return(df)
}

dat <- convertSelectDates(dat, c("dt", "dt2"))
dat %>% str
mpettis
fuente
3

Si bien disfruto usando dplyr para uso interactivo, me resulta extraordinariamente complicado hacerlo usando dplyr porque tienes que pasar por aros para usar lazyeval :: interp (), setNames, etc.

Aquí hay una versión más simple que usa la base R, en la que me parece más intuitivo, al menos para mí, poner el bucle dentro de la función, y que extiende la solución de @ MrFlicks.

multipetal <- function(df, n) {
   for (i in 1:n){
      varname <- paste("petal", i , sep=".")
      df[[varname]] <- with(df, Petal.Width * i)
   }
   df
}
multipetal(iris, 3) 
hackR
fuente
2
+1, aunque todavía uso dplyrmucho en configuraciones no interactivas, usarlo con entrada variable dentro de una función usa una sintaxis muy torpe.
Paul Hiemstra
3

Puede disfrutar de un paquete friendlyevalque presenta una API de evaluación ordenada simplificada y documentación para nuevos / casualesdplyr usuarios .

Está creando cadenas que desea mutatetratar como nombres de columna. Entonces, usando friendlyevalpodría escribir:

multipetal <- function(df, n) {
  varname <- paste("petal", n , sep=".")
  df <- mutate(df, !!treat_string_as_col(varname) := Petal.Width * n)
  df
}

for(i in 2:5) {
  iris <- multipetal(df=iris, n=i)
}

Que bajo el capó llama rlangfunciones que compruebanvarname es legal como nombre de columna.

friendlyeval el código se puede convertir a un código de evaluación simple y ordenado equivalente en cualquier momento con un complemento RStudio.

MilesMcBain
fuente
0

Otra alternativa: use {}comillas internas para crear fácilmente nombres dinámicos. Esto es similar a otras soluciones, pero no exactamente lo mismo, y me resulta más fácil.

library(dplyr)
library(tibble)

iris <- as_tibble(iris)

multipetal <- function(df, n) {
  df <- mutate(df, "petal.{n}" := Petal.Width * n)  ## problem arises here
  df
}

for(i in 2:5) {
  iris <- multipetal(df=iris, n=i)
}
iris

Creo que esto proviene, dplyr 1.0.0pero no estoy seguro (también tengo rlang 4.7.0si importa).

bretauv
fuente