Utilisez des noms de variables dynamiques dans `dplyr`

168

Je souhaite utiliser dplyr::mutate()pour créer plusieurs nouvelles colonnes dans un bloc de données. Les noms de colonnes et leur contenu doivent être générés dynamiquement.

Exemple de données d'iris:

library(dplyr)
iris <- tbl_df(iris)

J'ai créé une fonction pour muter mes nouvelles colonnes à partir de la Petal.Widthvariable:

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

Maintenant, je crée une boucle pour construire mes colonnes:

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

Cependant, comme mutate pense que varname est un nom de variable littéral, la boucle ne crée qu'une nouvelle variable (appelée varname) au lieu de quatre (appelée petal.2 - petal.5).

Comment puis-je mutate()utiliser mon nom dynamique comme nom de variable?

Timm S.
la source
1
Je n'insiste pas pour muter, je demande si c'est possible. C'est peut-être juste un petit truc que je ne connais pas. S'il y a un autre moyen, écoutons-le.
Timm S.
Je crois qu'il y a un espace à regarder dans le paquet paresseux
baptiste
1
À ce stade, dplyra une vignette entière sur l'évaluation non standard
Gregor Thomas
16
La vignette ne mentionne même pas mutate_, et les autres fonctions ne savent pas vraiment comment l'utiliser.
nacnudus

Réponses:

191

Puisque vous construisez dynamiquement un nom de variable en tant que valeur de caractère, il est plus judicieux de faire une affectation en utilisant l'indexation data.frame standard qui permet des valeurs de caractère pour les noms de colonne. Par exemple:

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

La mutatefonction permet de nommer très facilement de nouvelles colonnes via des paramètres nommés. Mais cela suppose que vous connaissez le nom lorsque vous tapez la commande. Si vous souhaitez spécifier dynamiquement le nom de la colonne, vous devez également créer l'argument nommé.


version de dplyr> = 0.7

La dernière version de dplyr(0.7) le fait en utilisant :=pour attribuer dynamiquement des noms de paramètres. Vous pouvez écrire votre fonction comme suit:

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

Pour plus d'informations, consultez le formulaire de documentation disponible vignette("programming", "dplyr").


dplyr (> = 0,3 et <0,7)

Une version légèrement antérieure de dplyr(> = 0,3 <0,7), encourageait l'utilisation d'alternatives «d'évaluation standard» à de nombreuses fonctions. Consultez la vignette d'évaluation non standard pour plus d'informations ( vignette("nse")).

Alors ici, la réponse est d'utiliser mutate_()plutôt que de mutate()faire:

# --- 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

Notez que cela est également possible dans les anciennes versions de celles dplyrqui existaient lorsque la question a été initialement posée. Cela nécessite une utilisation prudente de quoteet 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
la source
24
Merci, c'est utile. btw, je crée toujours des variables vraiment dramatiques.
Timm S.
27
Hehe. c'est probablement l'une de mes fautes de frappe préférées que j'ai faites depuis un moment. Je pense que je vais le laisser.
MrFlick
1
do.call()ne fait probablement pas ce que vous pensez qu'il fait: rpubs.com/hadley/do-call2 . Voir aussi la vignette nse dans la version dev de dplyr.
hadley
4
Donc, si je comprends votre point @hadley, j'ai mis à jour ce qui do.callprécède pour utiliser do.call("mutate")et citer dfdans la liste. Est-ce ce que vous proposiez? Et quand la lazyevalversion de dplyrest la version publiée, alors mutate_(df, .dots= setNames(list(~Petal.Width * n), varname))serait une meilleure solution?
MrFlick du
1
Que faire si j'ai besoin de l'en-tête de colonne variable non seulement sur le côté gauche de l'affectation, mais également sur la droite? par exemple mutate(df, !!newVar := (!!var1 + !!var2) / 2)ne fonctionne pas :(
Mario Reutter
55

Dans la nouvelle version de dplyr(en 0.6.0attente d'avril 2017), nous pouvons également faire une affectation ( :=) et passer des variables en tant que noms de colonnes en unquoting ( !!) pour ne pas l'évaluer

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

Vérification de la sortie basée sur @ MrFlick multipetalappliqué sur 'iris1'

identical(iris1, iris2)
#[1] TRUE
Akrun
la source
26

Après de nombreux essais et erreurs, j'ai trouvé le modèle UQ(rlang::sym("some string here")))vraiment utile pour travailler avec des chaînes et des verbes dplyr. Cela semble fonctionner dans de nombreuses situations surprenantes.

Voici un exemple avec mutate. Nous voulons créer une fonction qui ajoute deux colonnes, où vous passez la fonction les deux noms de colonne sous forme de chaînes. Nous pouvons utiliser ce modèle, avec l'opérateur d'affectation :=, pour ce faire.

## 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')

Le modèle fonctionne également avec d'autres dplyrfonctions. Voici filter:

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

Ou 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')

Pour select, vous n'avez pas besoin d'utiliser le modèle. Au lieu de cela, vous pouvez utiliser !!:

## select a column 
select_name <- function(name){
  mtcars %>% 
    select(!!name)
}
select_name('mpg')
Tom Roth
la source
Vos conseils fonctionnent très bien, mais j'ai un petit problème. Je change une colonne initiale myColen une URL (par exemple), et copie l'ancienne colonne myColInitialValueà la fin du dataframe dfavec un nouveau nom. Mais which(colnames(df)=='myCol')renvoyer le col # de myColInitialValue. Je n'ai pas encore écrit de numéro car je n'ai pas trouvé de reprex. Mon objectif est pour le escapeparamètre de DT::datatable(). J'utilise escape=FALSEen attendant ça. Avec les constantes, cela ne fonctionne pas non plus, mais le package DT semble également avoir la mauvaise colonne #. :)
phili_b
Il semble que les variables dynamiques ne soient pas la cause. (btw reprex ajouté)
phili_b
Merci pour cette réponse! Voici un exemple très simple de la façon dont je l'ai utilisé:varname = sym("Petal.Width"); ggplot(iris, aes(x=!!varname)) + geom_histogram()
bdemarest
Cela a fonctionné pour moi dans une formule où !! varname ne fonctionnait pas.
daknowles
12

Voici une autre version, et c'est sans doute un peu plus 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
user2946432
la source
8

Avec rlang 0.4.0nous avons des opérateurs bouclés ( {{}}) qui rendent cela très facile.

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

Nous pouvons également passer des noms de variables entre guillemets / non guillemets à attribuer comme noms de colonne.

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

Cela fonctionne de la même manière avec

multipetal(iris1, "temp", 3)
Ronak Shah
la source
4

J'ajoute également une réponse qui augmente un peu cela parce que je suis venu à cette entrée lors de la recherche d'une réponse, et cela avait presque ce dont j'avais besoin, mais j'avais besoin d'un peu plus, ce que j'ai obtenu via la réponse de @MrFlik et le R vignettes paresseux.

Je voulais créer une fonction qui pourrait prendre un dataframe et un vecteur de noms de colonnes (sous forme de chaînes) que je souhaite convertir d'une chaîne en objet Date. Je ne pouvais pas comprendre comment as.Date()prendre un argument qui est une chaîne et le convertir en colonne, alors je l'ai fait comme indiqué ci-dessous.

Voici comment j'ai fait cela via SE mutate ( mutate_()) et l' .dotsargument. Les critiques qui améliorent la situation sont les bienvenues.

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
la source
3

Bien que j'aime utiliser dplyr pour une utilisation interactive, je trouve extrêmement difficile de le faire avec dplyr car vous devez passer par des obstacles pour utiliser lazyeval :: interp (), setNames, etc.

Voici une version plus simple utilisant la base R, dans laquelle il me semble plus intuitif, du moins pour moi, de mettre la boucle à l'intérieur de la fonction, et qui étend la solution 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
la source
2
+1, bien que j'en utilise encore dplyrbeaucoup dans des paramètres non interactifs, son utilisation avec une entrée variabel dans une fonction utilise une syntaxe très maladroite.
Paul Hiemstra le
3

Vous pouvez profiter du package friendlyevalqui présente une API eval simplifiée et une documentation pour les dplyrutilisateurs débutants / occasionnels .

Vous créez des chaînes que vous souhaitez mutatetraiter comme des noms de colonnes. Donc, en utilisant, friendlyevalvous pourriez écrire:

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

Ce qui sous le capot appelle des rlangfonctions qui vérifient varnameest légal comme nom de colonne.

friendlyeval Le code peut être converti à tout moment en code eval simple équivalent avec un addin RStudio.

MilesMcBain
la source
0

Autre alternative: utilisez des {}guillemets pour créer facilement des noms dynamiques. C'est similaire à d'autres solutions mais pas exactement les mêmes, et je trouve cela plus facile.

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

Je pense que cela vient dplyr 1.0.0mais pas sûr (je l'ai aussi rlang 4.7.0si cela compte).

bretauv
la source