« tidyeval », vous avez dit « tidyeval » ?

« tidyeval », « tidyeval »… depuis quelques mois, toute la communauté R n’a que ce mot à la bouche (Vincent est d’ailleurs venu en parler au dernier Meetup Paris R-Addicts). Les slides sont ici

Pourtant, vous avez beau lire, relire et rerelire la vignette Programming with dplyr, ça ne fait toujours pas tilt. Rassurez-vous, lecteurs, nous sommes là pour vous faire le point sur tout ça.

 

La « tidyeval » est une forme d’évaluation non standard. Ce qui pose déjà une première question :

C’est quoi l’évaluation standard ?

L’évaluation, lecteur, c’est l’action qu’effectue votre machine à chaque fois qu’elle doit interpréter un symbole. En clair, quand vous tapez a dans votre console, R va chercher à interpréter le symbole a, pour vous retourner une valeur. R va d’abord aller chercher dans l’environnement global, puis descendre la hiérarchie de votre searchpaths(), jusqu’à trouver une valeur associée à votre symbole. S’il n’en trouve pas, il renvoie une erreur.

L’évaluation standard, en R, c’est donc ça : on entre un symbole (autrement dit un nom), et R va aller chercher sa valeur en partant de .GlobalEnv.

Et donc, l’évaluation non standard ?

R est un langage flexible en ce sens qu’il permet de s’affranchir des règles d’évaluation qu’on vient de voir. On parle d’évaluation non standard lorsque l’on définit des règles spécifiques contournant le processus décrit juste au-dessus.

En fait, de l’évaluation non standard, vous en faites tous les jours :

library(dplyr)

Ici, le symbole n’est pas évalué de manière standard : il n’existe pas de symbole dplyr dans l’environnement global, ni d’ailleurs dans le searchpaths(). Ce que library() fait, c’est capturer le symbole dplyr, pour aller chercher le package correspondant dans votre libraire.

Welcome to the tidyverse

La tidyeval est donc un mécanisme permettant l’évaluation non standard dans les packages du tidyverse, notamment {dplyr}. Parce que par exemple, lorsque vous faites :

iris %>% filter(Species == "setosa")

Le symbole Species ne réfère à rien si on l’envisage via l’évaluation standard : il n’existe pas de symbole Species dans l’environnement global pointant vers une valeur. Ici, le symbole Species doit être évalué dans le contexte du data.frame iris.

C’est d’ailleurs pourquoi, en base (et donc en évaluation standard), il faudra écrire iris$Species:

iris[iris$Species == "setosa", ]

Ce qui devient vite verbeux si on a besoin de répéter de nombreuses opérations sur une table.

Let’s tidyeval

Si vous arrivez sur cette page, c’est que vous avez sûrement déjà essayé de coder cette fonction (ou du moins une qui ressemble à) :

mon_select <- function(df, colonne, n){
  df %>% 
    select(colonne) %>% 
    head(n)
}

Et que vous êtes tombé sur :

mon_select(iris, Sepal.Length, 2)
Error in FUN(X[[i]], ...) : objet 'Sepal.Length' introuvable 

Si vous relisez le message d’erreur, vous voyez qu’il y a eu une erreur d’évaluation : l’objet Sepal.Length n’a pas été trouvé. Alors, pour faire face, une solution : la tidyeval, l’évaluation non standard du tidyverse (vous l’aviez deviné, n’est-ce pas 😉 ).

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. Donc, quand vous tapez :

filter(iris, Species == "setosa")

Species est transformé en quosure, et cette quosure est évaluée dans le contexte de iris.

enquo et bang bang

Première étape donc : transformer votre nom de colonne en quosure. Pour ça, on appellera la fonction enquo, un opérateur de mise en quosure.

mon_select <- function(df, colonne, n){
  colonne <- enquo(colonne)
  df %>% 
    select(colonne) %>% 
    head(n)
}

Et ça devrait marcher, non ?

mon_select(iris, Sepal.Length, 2)
 Erreur : `colonne` must resolve to integer column positions, not formula 

Eh non, toujours pas ! Et pour cause : select pense toujours qu’il doit prendre en charge la « quosurisation ». Pour spécifier à la fonction que nous avons déjà pris en charge la mise en quosure, on va faire appel à !! (à prononcer « bang bang »), que l’on mettra devant la quosure :

mon_select <- function(df, colonne, n){
  colonne <- enquo(colonne)
  df %>% 
    select(!! colonne) %>% 
    summarise(moyenne = mean(!!colonne))
}
mon_select(iris, Sepal.Length, 2)
  Sepal.Length
1          5.1
2          4.9

Eh voilà !

quos et bang bang bang

« On voudrait passer plusieurs colonnes », dites-vous ? Décidément, vous êtes inarrêtable ! Pas de soucis, ça c’est un job pour quos, qui va créer une liste de quosures, et !!!, qui unquote une liste de quosures :

mon_select <- function(df, n, ...){
  colonnes <- quos(...)
  df %>% 
    select(!!! colonnes) %>% 
    head(n)
}
mon_select(iris, 2, Species, Sepal.Length)

Well done !

À propos de :=

Enfin, dernier opérateur indispensable, :=, permettant de placer un symbole à gauche de l’assignation, dans une fonction comme summarise(iris, nom = f(colonne)).

Attention, à gauche de l’opérateur :=, on ne peut passer qu’une chaîne de caractères ou un symbole (et donc pas une quosure). On va donc utiliser quo_name() :

mon_mean <- function(df, colonne, nom){
  colonne <- enquo(colonne)
  nom <- quo_name(enquo(nom))
  df %>% 
    summarise(!! nom := mean(!! colonne))
}
mon_mean(iris, Sepal.Length, moyenne)
   moyenne
1 5.843333

C’était facile, non 😉 ?


Pour en savoir plus :


À propos de l'auteur

Colin Fay

Colin Fay

Data scientist & R Hacker


Commentaires


À lire également