Comment créer des fonctions dans le tidyverse avec la tidyeval et le {{stash stash}}

Tags : Autour de R, Actualités, Ressources
Date :

Rappels sur la tidyeval

L’évaluation tidy, c’est une évaluation non-standard. C’est-à-dire que contrairement à l’évaluation standard qui consiste en l’interprétation d’un nom présent dans le .GlobalEnv voire retrouvé dans la hiérarchie du searchpaths, l’évaluation non-standard permet d’évaluer des symboles/noms qui ne sont pas dans notre environnement global.

Un exemple simple, que vous faites tous les jours : le chargement d’un package. Vous avez 2 façons de faire, qui reviennent à la même chose : library(dplyr) et library('dplyr'). library('dplyr') correspond à de l’évaluation standard, mais quand vous faites library(dplyr), il n’existe pas de symbole dplyr dans l’environnement global, ni d’ailleurs dans le searchpaths(). Le symbole dplyr est capturé par la fonction library() qui va aller chercher le package correspondant dans votre libraire.

La tidyeval, c’est donc un mécanisme permettant l’évaluation non standard dans les packages du tidyverse. Par exemple, lorsque vous faites :

iris %>% filter(Species == "setosa") %>% head()
##   Sepal.Length Sepal.Width Petal.Length Petal.Width Species
## 1          5.1         3.5          1.4         0.2  setosa
## 2          4.9         3.0          1.4         0.2  setosa
## 3          4.7         3.2          1.3         0.2  setosa
## 4          4.6         3.1          1.5         0.2  setosa
## 5          5.0         3.6          1.4         0.2  setosa
## 6          5.4         3.9          1.7         0.4  setosa

Ici, Species n’est pas connu dans l’environnement global. Il est interprété dans un contexte particulier : le dataframe iris.

Les fonctions tidy

Jusque là tout va bien, mais les choses se complexifient un peu quand on essaye d’écrire des fonctions au sein desquelles les opérations nécessitent une évaluation tidy. Imaginons par exemple une fonction qui prend en paramètre un dataframe, et renvoie ce même dataframe avec une colonne supplémentaire qui sera le carré d’une certaine colonne. Dans un premier temps, on voudrait l’écrire comme suit :

square_col <- function(df, colonne, newcol) {
  df %>%
    mutate(newcol = colonne^2)
}

Si on teste cette fonction en l’état, on tombe rapidement sur une erreur :

square_col(iris, Sepal.Length, "SL_squared")

Bon, ce n’est pas une surprise, on se doutait bien que l’on allait rencontrer une erreur d’évaluation, en effet, l’objet Sepal.Length n’existe pas. On pourrait alors être tenté d’appeler notre fonction en ajoutant des guillemets au deuxième paramètre :

square_col(iris, "Sepal.Length", "SL_squared")


… mais cela reviendrait à essayer de calculer la chaine de caractère "Sepal.Length" au carré, ce qui n’a aucun sens.

Alors, comment fait-on ? On passe à la tidyeval bien sûr ! Comme Vincent nous le disait dans son article, “cette évaluation repose sur une nouvelle classe d’objets : les « quosures », qui font référence aux symboles capturés dans le contexte de certaines fonctions du tidyverse.” C’est-à-dire que lorsque l’on fait :

iris %>% mutate(SL_squared = Sepal.Length^2)

Sepal.Length est transformé en quosure qui est évaluée dans le contexte de iris.

enquo(), bang bang et stash stash

Dans des temps plus anciens, pour que notre paramètre colonne soit évalué dans le contexte de df, il fallait transformer cet élément en quosure, et y accéder ensuite grâce au “bang bang” que l’on écrira !! :

square_col <- function(df, colonne, newcol) {
  colonne <- enquo(colonne)
  df %>%
    mutate(newcol = (!! colonne)^2)
}
square_col(iris, Sepal.Length, "SL_squared") %>% head()
##   Sepal.Length Sepal.Width Petal.Length Petal.Width Species newcol
## 1          5.1         3.5          1.4         0.2  setosa  26.01
## 2          4.9         3.0          1.4         0.2  setosa  24.01
## 3          4.7         3.2          1.3         0.2  setosa  22.09
## 4          4.6         3.1          1.5         0.2  setosa  21.16
## 5          5.0         3.6          1.4         0.2  setosa  25.00
## 6          5.4         3.9          1.7         0.4  setosa  29.16

Oui ça fonctionne bien, mais depuis peu, les doubles moustaches {{...}} sont arrivées pour nous faciliter la vie : plus besoin de transformer notre élément en quosure, et adieu le bang bang ! On peut maintenant écrire :

square_col <- function(df, colonne, newcol) {
  df %>%
    mutate(newcol = {{colonne}}^2)
}
square_col(iris, Sepal.Length, "SL_squared") %>% head()
##   Sepal.Length Sepal.Width Petal.Length Petal.Width Species newcol
## 1          5.1         3.5          1.4         0.2  setosa  26.01
## 2          4.9         3.0          1.4         0.2  setosa  24.01
## 3          4.7         3.2          1.3         0.2  setosa  22.09
## 4          4.6         3.1          1.5         0.2  setosa  21.16
## 5          5.0         3.6          1.4         0.2  setosa  25.00
## 6          5.4         3.9          1.7         0.4  setosa  29.16

Elle est pas belle la vie ?

L’indispensable :=

On se rapproche de ce que l’on souhaitait faire au départ, mais il nous manque encore un petit quelque chose : la nouvelle colonne que nous voulions créer n’a pas le nom que nous lui avions donné en paramètre, soit “SL_squared” et non “newcol”. C’est ici que nous aurons besoin de := : cet opérateur permet de placer un symbole à gauche de l’assignation.

Avant la révolution de la double moustache, il fallait utiliser la fonction quo_name() pour permettre l’évaluation de ce qu’on écrivait à gauche. Dans le cadre de notre fonction, cela donnerait :

square_col <- function(df, colonne, newcol) {
  colonne <- enquo(colonne)
  newcol <- quo_name(newcol)
  df %>%
    mutate(!! newcol := (!! colonne)^2)
}
square_col(iris, Sepal.Length, "SL_squared") %>% head()
##   Sepal.Length Sepal.Width Petal.Length Petal.Width Species SL_squared
## 1          5.1         3.5          1.4         0.2  setosa      26.01
## 2          4.9         3.0          1.4         0.2  setosa      24.01
## 3          4.7         3.2          1.3         0.2  setosa      22.09
## 4          4.6         3.1          1.5         0.2  setosa      21.16
## 5          5.0         3.6          1.4         0.2  setosa      25.00
## 6          5.4         3.9          1.7         0.4  setosa      29.16

Sounds perfect, mais aujourd’hui, en plus de se substituer à l’utilisation de enquo()/bang bang, la double moustache nous permet également d’alléger notre code et de dire au revoir à quo_name()/bang bang. On écrira finalement notre fonction d’une façon bien plus élégante :

square_col <- function(df, colonne, newcol) {
  df %>%
    mutate({{newcol}} := {{colonne}}^2)
}
square_col(iris, Sepal.Length, "SL_squared") %>% head()
##   Sepal.Length Sepal.Width Petal.Length Petal.Width Species SL_squared
## 1          5.1         3.5          1.4         0.2  setosa      26.01
## 2          4.9         3.0          1.4         0.2  setosa      24.01
## 3          4.7         3.2          1.3         0.2  setosa      22.09
## 4          4.6         3.1          1.5         0.2  setosa      21.16
## 5          5.0         3.6          1.4         0.2  setosa      25.00
## 6          5.4         3.9          1.7         0.4  setosa      29.16

Joli non ?

“Oui mais si je souhaite que mon second paramètre, colonne, soit une chaine de caractères, je fais comment ?” Dans ce cas, deux possibilités :

  • en utilisant le bang bang !! qu’on aimait bien quand même :
square_col <- function(df, colonne, newcol) {
  df %>%
    mutate({{newcol}} := (!! sym(colonne))^2)
}
square_col(iris, "Sepal.Length", "SL_squared") %>% head()
##   Sepal.Length Sepal.Width Petal.Length Petal.Width Species SL_squared
## 1          5.1         3.5          1.4         0.2  setosa      26.01
## 2          4.9         3.0          1.4         0.2  setosa      24.01
## 3          4.7         3.2          1.3         0.2  setosa      22.09
## 4          4.6         3.1          1.5         0.2  setosa      21.16
## 5          5.0         3.6          1.4         0.2  setosa      25.00
## 6          5.4         3.9          1.7         0.4  setosa      29.16
  • en utilisant .[data] :
square_col <- function(df, colonne, newcol) {
  df %>%
    mutate({{newcol}} := .data[[colonne]]^2)
}
square_col(iris, "Sepal.Length", "SL_squared") %>% head()
##   Sepal.Length Sepal.Width Petal.Length Petal.Width Species SL_squared
## 1          5.1         3.5          1.4         0.2  setosa      26.01
## 2          4.9         3.0          1.4         0.2  setosa      24.01
## 3          4.7         3.2          1.3         0.2  setosa      22.09
## 4          4.6         3.1          1.5         0.2  setosa      21.16
## 5          5.0         3.6          1.4         0.2  setosa      25.00
## 6          5.4         3.9          1.7         0.4  setosa      29.16

Alors, pas si compliqué que ça la tidyeval non ?


À propos de l'auteur


Commentaires

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *


À lire également

Nos formations Certifiantes à R sont finançables à 100% via le CPF

Nos formations disponibles via moncompteformation.gouv.fr permettent de délivrer des Certificats de Qualifications Professionnelles (CQP) reconnus par l’état. 3 niveaux de certifications existent :

Contactez-nous pour en savoir plus.

Calendrier des formations