web-dev-qa-db-fra.com

Utiliser des noms de variables dynamiques dans `dplyr`

Je souhaite utiliser dplyr::mutate() pour créer plusieurs nouvelles colonnes dans un bloc de données. Les noms de colonne 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 variable Petal.Width:

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, étant donné que mutate pense que nomvar est un nom de variable littéral, la boucle ne crée qu'une nouvelle variable (appelée nomvar) au lieu de quatre (appelée petal.2 - petal.5).

Comment puis-je obtenir que mutate() utilise mon nom dynamique comme nom de variable?

124
Timm S.

Etant donné que vous créez de manière spectaculaire un nom de variable en tant que valeur de caractère, il est plus logique de procéder à une affectation à l'aide de l'indexation standard data.frame, qui permet d'utiliser 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 fonction mutate permet de nommer facilement de nouvelles colonnes via des paramètres nommés. Mais cela suppose que vous connaissiez le nom lorsque vous tapez la commande. Si vous souhaitez spécifier dynamiquement le nom de la colonne, vous devez également générer l'argument nommé.


version dplyr> = 0.7

La dernière version de dplyr (0.7) utilise pour cela l’utilisation de := pour attribuer dynamiquement des noms de paramètres. Vous pouvez écrire votre fonction en tant que:

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

Pour plus d'informations, voir la documentation disponible sous la forme vignette("programming", "dplyr").


dplyr (> = 0.3 & <0.7)

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

Donc, ici, la réponse est d'utiliser mutate_() plutôt que mutate() et de 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 dplyr qui existaient lorsque la question a été posée à l'origine. Cela nécessite une utilisation prudente de quote et 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)
}
140
MrFlick

Dans la nouvelle version de dplyr (0.6.0 attendue en avril 2017), nous pouvons également effectuer une affectation (:=) et transmettre des variables comme noms de colonne en décochant (!!) 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 en fonction de multipetal de @ MrFlick appliqué à 'iris1'

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

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

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

## 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 motif fonctionne avec les autres fonctions dplyr. 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 motif. Au lieu de cela, vous pouvez utiliser !!:

## select a column 
select_name <- function(name){
  mtcars %>% 
    select(!!name)
}
select_name('mpg')
16
Tom Roth

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
12
user2946432

J'ajoute également une réponse qui augmente un peu ce point car je suis arrivé à cette entrée lorsque je cherchais une réponse, ce qui me donnait presque ce dont j'avais besoin, mais il me fallait un peu plus, ce que j'ai obtenu via la réponse de @MrFlik et le R vignettes lazyeval.

Je voulais créer une fonction pouvant prendre une structure de données et un vecteur de noms de colonnes (en tant que chaînes) que je souhaite convertir en une chaîne en objet Date. Je n'arrivais pas à comprendre comment faire que as.Date() prenne un argument qui soit une chaîne et le convertisse en colonne, je l'ai donc fait comme indiqué ci-dessous.

Voici comment je l’ai fait via SE mutate (mutate_()) et l’argument .dots. Les critiques qui améliorent cette 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
4
mpettis

Vous pouvez profiter du paquet friendlyeval qui présente une API simplifiée eval tidy eval et une documentation pour les utilisateurs plus récents/occasionnels dplyr.

Vous créez des chaînes que vous souhaitez que mutate traite comme des noms de colonne. Donc, en utilisant friendlyeval, vous 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)
}

Qui sous le capot appelle des fonctions rlang qui vérifient varname est légal en tant que nom de colonne.

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

2
MilesMcBain

Bien que j'apprécie d'utiliser dplyr pour une utilisation interactive, je trouve qu'il est extrêmement difficile de le faire à l'aide de dplyr, car il faut parcourir des étapes pour utiliser les solutions de contournement lazyeval :: interp (), setNames, etc.

Voici une version plus simple utilisant la base R, dans laquelle il me semble plus intuitif, du moins, 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) 
2
hackR