17  Manipuler des données avec le tidyverse

17.1 Tâches concernées et recommandations

L’utilisateur souhaite manipuler des données structurées sous forme de data.frame (sélectionner des variables, sélectionner des observations, créer des variables, joindre des tables, résumer l’information).

Tâche concernée et recommandation
  • Pour des tables de données de taille petite et moyenne (inférieure à 1 Go ou moins d’un million d’observations), il est recommandé d’utiliser les packages tibble, dplyr et tidyr qui font l’objet de la présente fiche ;
  • Pour des tables de données de grande taille (plus de 1 Go ou plus d’un million d’observations), il est recommandé d’utiliser soit le package data.table présenté dans la fiche Manipuler des données avec data.table, soit les packages arrow et duckdb présentés dans les fiches Manipuler des données avec arrow et Manipuler des données avec duckdb.
Note

Certains exemples de cette fiche utilisent les données disponibles dans le package doremifasolData ; vous ne pourrez reproduire ces exemples que si ce package est installé sur la machine sur laquelle vous travaillez. Si vous ne savez pas si ce package est déjà installé, consultez la fiche Comment utiliser la documentation utilitR.

17.2 Présentation des packages dplyr, tidyr et tibble

17.2.1 Introduction

Les packages dplyr, tidyr et tibble font partie du tidyverse, une suite de packages dont l’objectif est de faciliter la manipulation de tables de données. De façon schématique, on peut dire que :

  • le package tibble propose une nouvelle structure de données qui est une formalisation plus poussée du data.frame (le tibble), et des fonctions pour manipuler les tibbles ;
  • le package dplyr propose des fonctions pour manipuler des données qui sont déjà organisées sous forme de table (sélectionner des lignes et des colonnes, calculer des statistiques descriptives) ;
  • le package tidyr propose des fonctions pour mettre en ordre des données et pour les organiser sous forme de tables (créer des tables à partir de données brutes ou de listes, décomposer une colonne ou une ligne en plusieurs, restructurer les données en format long ou wide).

Ces packages présentent plusieurs avantages :

  • La syntaxe est structurée autour d’opérations élémentaires (les verbes), ce qui la rend très lisible ;
  • La grammaire du tidyverse s’inspire du langage SQL pour fournir des commandes dont l’enchaînement est facile à comprendre, même sans faire tourner le code. Par exemple, la ligne matable %>% select(mavariable) %>% summarise() est facilement compréhensible ;
  • Les opérations par groupe sont facilitées par l’instruction group_by() ;
  • Les noms des colonnes peuvent être appelés directement (pas besoin d’adopter la syntaxe nomtable$nomvariable comme dans la grammaire de base). Là aussi, la lecture du code en est facilitée ;
  • Plusieurs verbes peuvent facilement être combinés en utilisant l’opérateur %>% (pipe) qui permet d’enchaîner les instructions (voir le paragraphe Enchaîner les manipulations avec l’opérateur pipe) ;
  • La suite de packages tidyverse couvre la plupart des opérations qu’un statisticien est amené à réaliser (mais pas toutes.

Pour se servir de ces packages, il ne faut pas oublier de les charger avec la fonction library.

Les exemples de cette fiche s’appuient sur les données disponibles dans le package doremifasolData. On utilise en premier lieu la base permanente des équipements 2018.

Les objets récupérés en sortie de doremifasolData sont tous des data.frame, bien que le package s’appuie sur des fonctions issues de tidyverse. Il est donc nécessaire de les convertir en tibble avec la fonction as_tibble du package tibble.

17.2.2 Le tibble : un data.frame amélioré

Le tidyverse propose une version améliorée du data.frame de base : le tibble. Il s’agit d’un data.frame standard, avec quelques propriétés supplémentaires qui rendent son utilisation plus facile. Le package tibble contient par ailleurs des fonctions utiles pour manipuler les tibbles. Sauf exception, toutes les fonctions de R qui manipulent un data.frame fonctionneront sans problème avec un tibble.

Pour convertir un data.frame en tibble on utilise la fonction tibble::as_tibble(). Dans l’exemple suivant, on charge la table de la base permanente des équipements puis on la convertit en tibble.

# Charger la base permanente des équipements
bpe_ens_2018 <- doremifasolData::bpe_ens_2018
# Convertir ce data.frame en tibble
bpe_ens_2018_tbl <- as_tibble(bpe_ens_2018)

Il y a deux différences principales entre un tibble et un data.frame :

  • La sélection (subsetting) fonctionne différemment : sélectionner une colonne dans un data.frame renvoie un vecteur, alors que sélectionner une colonne dans un tibble renvoie un tibble à une seule colonne. Si on veut vraiment récupérer un vecteur à partir d’un tibble à une colonne, on utilise la fonction pull.

    # Si on sélectionne une colonne dans un data.frame,
    # on obtient un vecteur.
    bpe_ens_2018[ , 1]
     [1] "84" "84" "84" "84" "84" "84" "84" "84" "84" "84"
     [ reached getOption("max.print") -- omitted 1035554 entries ]
    # Si on sélectionne une colonne dans un tibble,
    # on obtient un tibble avec une seule colonne.
    bpe_ens_2018_tbl[ , 1]
    # A tibble: 1,035,564 × 1
      REG  
      <chr>
    1 84   
    2 84   
    3 84   
    4 84   
    5 84   
    6 84   
    # ℹ 1,035,558 more rows
    pull(bpe_ens_2018_tbl[ , 1])
     [1] "84" "84" "84" "84" "84" "84" "84" "84" "84" "84"
     [ reached getOption("max.print") -- omitted 1035554 entries ]
  • L’affichage des tibbles est meilleur que celui des data.frames. Vous pouvez par exemple remarquer que même sans la fonction head(), l’affichage d’un tibble affiche toujours les dimensions de celui-ci (nombres de lignes et de colonnes), ainsi que le type des variables (en-dessous des noms des colonnes).

# Afficher les premières lignes d'un dataframe
head(bpe_ens_2018)
  REG DEP DEPCOM DCIRIS   AN TYPEQU NB_EQUIP
1  84  01  01001  01001 2018   A401        2
 [ reached 'max' / getOption("max.print") -- omitted 5 rows ]
# Afficher les premières lignes d'un tibble
bpe_ens_2018_tbl
# A tibble: 1,035,564 × 7
  REG   DEP   DEPCOM DCIRIS    AN TYPEQU NB_EQUIP
  <chr> <chr> <chr>  <chr>  <dbl> <chr>     <dbl>
1 84    01    01001  01001   2018 A401          2
2 84    01    01001  01001   2018 A404          4
3 84    01    01001  01001   2018 A504          1
4 84    01    01001  01001   2018 A507          1
5 84    01    01001  01001   2018 B203          1
6 84    01    01001  01001   2018 C104          1
# ℹ 1,035,558 more rows

17.2.3 Comment utiliser les fonctions du tidyverse

Les fonctions (ou verbes) du tidyverse partagent quatre caractéristiques :

  • Elles prennent toujours un tibble ou un data.frame en entrée et renvoient toujours un tibble ou un data.frame en sortie ;
  • Elles ont toujours la table de données pour premier argument : verbe(tibble ou data.frame, ...) ;
  • Elles renvoient toujours une copie de la table de données fournie en entrée et ne modifient jamais les données d’entrée, sauf si on le demande explicitement avec l’opérateur d’assignation <- (voir remarque) ;
  • Les noms de variables peuvent être cités sans guillemets (sauf dans le cas des jointures).
Note

Par défaut, les fonctions du tidyverse renvoient une copie des données manipulées. Cela signifie que vous devez utiliser l’opérateur d’assignation <- si vous voulez modifier une table. Voici deux exemples simples pour bien comprendre la différence.

Le premier code sélectionne la variable TYPEEQ dans la table bpe_ens_2018_tbl et renvoie un data.frame contenant uniquement cette variable. En revanche, la table bpe_ens_2018_tbl n’est pas modifiée.

select(bpe_ens_2018_tbl, TYPEQU)

Le second code modifie la table bpe_ens_2018_tbl en utilisant l’opérateur <- :

bpe_ens_2018_tbl <- select(bpe_ens_2018_tbl, TYPEQU)

Ce fonctionnement des fonctions du tidyverse a pour avantage qu’il est difficile d’écraser ses données par mégarde. Il a pour inconvénient d’être gourmand en mémoire vive (RAM) car les données sont temporairement dupliquées. C’est pour cette raison que le tidyverse n’est pas adapté à la manipulation de données volumineuses.

17.2.4 Enchaîner les manipulations avec l’opérateur pipe

17.2.4.1 Présentation de l’opérateur pipe

Lorsqu’on enchaîne les manipulations sur une table de données, un problème est que le code devient peu lisible car il y a beaucoup d’opérations imbriquées les unes dans les autres, avec un grand nombre de parenthèses ou de crochets. L’opérateur pipe (noté %>%) du package magrittr permet de résoudre ce problème en réécrivant les opérations de façon plus lisible. Le principe de l’opérateur pipe est très simple :

  • le terme qui précède l’opérateur est utilisé comme premier argument de la fonction qui suit l’opérateur ;
  • les opérations peuvent être enchaînées en enchaînant les opérateurs pipe ;
  • l’opérateur pipe fonctionne quelle que soit la nature de l’argument ;
  • l’opérateur pipe fonctionne également à l’intérieur de parenthèses.

Voici un petit tableau qui vous donne des exemples :

Ce code est équivalent à… … ce code
fonction(x) x %>% fonction()
fonction3(fonction2(fonction1(x))) x %>% fonction1() %>% fonction2() %>% fonction3()
mutate(tibble, y = log(x)) tibble %>% mutate(y = x %>% log())

17.2.4.2 Comment utiliser l’opérateur pipe avec le tidyverse

Un traitement statistique avec les packages du tidyverse prend généralement la forme d’une succession de verbes séparés par l’opérateur pipe (%>%). Il est possible d’aller à la ligne en mettant le pipe en bout de ligne (mais pas en début de ligne).

Voici un exemple détaillé pour comprendre l’utilisation du pipe. Toutes les fonctions utilisées sont présentées ailleurs dans cette fiche. Ce code se lit comme ceci : on part de la base permanente des équipements 2018, puis on la transforme en tibble, puis on conserve uniquement les stations services TYPEQU == "B316", puis on groupe les observations par département group_by(DEP), puis on calcule la somme du nombre de stations-services par département summarise(nb_equip_total = sum(NB_EQUIP, na.rm = TRUE)).

nombre <- bpe_ens_2018 %>%
  as_tibble() %>%
  filter(TYPEQU == "B316") %>% 
  group_by(DEP) %>% 
  summarise(nombre_station_serv = sum(NB_EQUIP, na.rm = TRUE)) 
nombre
# A tibble: 96 × 2
  DEP   nombre_station_serv
  <chr>               <dbl>
1 01                    110
2 02                     91
3 03                     78
4 04                     48
5 05                     43
6 06                    148
# ℹ 90 more rows

17.3 Manipuler des tables de données avec dplyr

Le package dplyr permet de manipuler facilement des données organisées sous forme de table, c’est-à-dire comprenant une colonne par variable et une ligne par observation. Si ce n’est pas le cas, vous pouvez utiliser le package tidyr pour organiser vos données (voir la section Mettre en ordre des données avec tidyr).

17.3.1 Manipuler une seule table avec dplyr

Dans dplyr, les manipulations simples de données sont résumées en quelques verbes :

  • select() : sélectionner des variables par leur nom ;
  • rename() : renommer des variables ;
  • filter() : sélectionner des observations selon une ou plusieurs conditions ;
  • arrange() : trier la table selon une ou plusieurs variables ;
  • mutate() : ajouter des variables qui sont fonction d’autres variables ;
  • summarise() : calculer une statistique à partir de données ;
  • group_by() : faire des opérations par groupe.

17.3.1.1 Sélectionner des variables : select()

La fonction select() permet de sélectionner des variables par leur nom, ou par une condition sur leur nom. Cette fonction est principalement utilisée de deux façons :

  • Avec une liste de noms de variables. Le code suivant sélectionne le code commune, le type d’équipement et le nombre d’équipement dans la base permanente des équipements :

    bpe_ens_2018_tbl %>% 
      select(DEPCOM, TYPEQU, NB_EQUIP)
    # A tibble: 1,035,564 × 3
      DEPCOM TYPEQU NB_EQUIP
      <chr>  <chr>     <dbl>
    1 01001  A401          2
    2 01001  A404          4
    3 01001  A504          1
    4 01001  A507          1
    5 01001  B203          1
    6 01001  C104          1
    # ℹ 1,035,558 more rows
  • Avec la position des colonnes. Le code suivant sélectionne les cinq premières colonnes :

    bpe_ens_2018_tbl %>% 
      select(1:5)
    # A tibble: 1,035,564 × 5
      REG   DEP   DEPCOM DCIRIS    AN
      <chr> <chr> <chr>  <chr>  <dbl>
    1 84    01    01001  01001   2018
    2 84    01    01001  01001   2018
    3 84    01    01001  01001   2018
    4 84    01    01001  01001   2018
    5 84    01    01001  01001   2018
    6 84    01    01001  01001   2018
    # ℹ 1,035,558 more rows
  • Avec une condition logique. Par exemple, la fonction starts_with("DEP") permet de sélectionner toutes les variables dont le nom commence par “DEP”.

    bpe_ens_2018_tbl %>% 
      select(starts_with("DEP"))
    # A tibble: 1,035,564 × 2
      DEP   DEPCOM
      <chr> <chr> 
    1 01    01001 
    2 01    01001 
    3 01    01001 
    4 01    01001 
    5 01    01001 
    6 01    01001 
    # ℹ 1,035,558 more rows

    Le tableau suivant donne la liste des conditions utilisables avec select() :

Fonction Signification
select(starts_with("...") dont le nom commence par “…”
select(ends_with("...") dont le nom se termine par “…”
select(contains("...") contient “…”
select(matches("...") vérifie une expression régulière (cf. fiche Manipuler des données textuelles
select(all_of(...)) sélectionne les colonnes listées dans un vecteur en paramètre
select(any_of(...)) identique à all_of(), mais sans erreur si la colonne n’existe pas
select(everything()) toutes les colonnes (utile pour mettre une nouvelle colonne devant les autres)
  • On peut aussi renommer des colonnes avec la syntaxe select(data, nouveau_nom = selection_colonne) :
  bpe_ens_2018_tbl %>% 
    select(dept = DEP, depcom = DEPCOM)
# A tibble: 1,035,564 × 2
  dept  depcom
  <chr> <chr> 
1 01    01001 
2 01    01001 
3 01    01001 
4 01    01001 
5 01    01001 
6 01    01001 
# ℹ 1,035,558 more rows

17.3.1.2 Renommer des variables : rename()

La fonction rename() permet de renommer des variables. La syntaxe est la suivante : rename(data, nouveau_nom = ancien nom). Voici un exemple :

bpe_ens_2018_tbl %>% 
  rename(code_commune = DEPCOM)
# A tibble: 1,035,564 × 7
  REG   DEP   code_commune DCIRIS    AN TYPEQU NB_EQUIP
  <chr> <chr> <chr>        <chr>  <dbl> <chr>     <dbl>
1 84    01    01001        01001   2018 A401          2
2 84    01    01001        01001   2018 A404          4
3 84    01    01001        01001   2018 A504          1
4 84    01    01001        01001   2018 A507          1
5 84    01    01001        01001   2018 B203          1
6 84    01    01001        01001   2018 C104          1
# ℹ 1,035,558 more rows

La fonction rename_with() permet de renommer un groupe de colonnes avec une fonction. La syntaxe est la suivante : rename_with(data, nom_fonction, selection_colonnes). Voici un exemple qui met en minuscules tous les noms de colonnes :

bpe_ens_2018_tbl %>% 
  rename_with(tolower)
# A tibble: 1,035,564 × 7
  reg   dep   depcom dciris    an typequ nb_equip
  <chr> <chr> <chr>  <chr>  <dbl> <chr>     <dbl>
1 84    01    01001  01001   2018 A401          2
2 84    01    01001  01001   2018 A404          4
3 84    01    01001  01001   2018 A504          1
4 84    01    01001  01001   2018 A507          1
5 84    01    01001  01001   2018 B203          1
6 84    01    01001  01001   2018 C104          1
# ℹ 1,035,558 more rows

17.3.1.3 Sélectionner des observations : filter()

Le verbe filter() permet de sélectionner des observations selon une ou plusieurs conditions logiques. Voici un exemple de code qui sélectionne les magasins de chaussures (TYPEQU == "B304") dans le premier arrondissement de Paris (DEPCOM == "75101") dans la BPE.

bpe_ens_2018_tbl %>% 
  filter(DEPCOM == "75101" & TYPEQU == "B304")
# A tibble: 12 × 7
  REG   DEP   DEPCOM DCIRIS        AN TYPEQU NB_EQUIP
  <chr> <chr> <chr>  <chr>      <dbl> <chr>     <dbl>
1 11    75    75101  75101_0101  2018 B304          2
2 11    75    75101  75101_0103  2018 B304          1
3 11    75    75101  75101_0201  2018 B304         26
4 11    75    75101  75101_0202  2018 B304          6
5 11    75    75101  75101_0203  2018 B304         17
6 11    75    75101  75101_0204  2018 B304         13
# ℹ 6 more rows

Voici quelques utilisations fréquentes de filter() :

Action Code
Filtrer sur les modalités qualitatives d’une colonne filter(DEP %in% c("75", "92"))
Filtrer sur les modalités quantitatives d’une colonne filter(NB_EQUIP == 1)
Filtrer sur une variable caractère (voir la fiche données textuelles) filter(str_detect(TYPEQU, "^A"))
Filtrer sur deux conditions (et) filter(DEP == "75" & NB_EQUIP == 1)
Filtrer sur une alternative (ou) filter(DEP == "75" | NB_EQUIP == 1)
Conserver les observations pour lesquelles la variable est manquante filter(is.na(pop_2016))
Conserver les observations pour lesquelles la variable est renseignée filter( !is.na(pop_2016))

17.3.1.4 Trier la table : arrange()

Le verbe arrange() permet de trier les observations de la table selon une ou plusieurs colonnes. Par défaut, arrange trie par ordre croissant. Il faut utiliser desc(nom_de_variable) pour trier par ordre décroissant. Le code suivant trie la BPE selon le code commune et le type d’équipement.

bpe_ens_2018_tbl %>% 
  arrange(DEPCOM, TYPEQU)
# A tibble: 1,035,564 × 7
  REG   DEP   DEPCOM DCIRIS    AN TYPEQU NB_EQUIP
  <chr> <chr> <chr>  <chr>  <dbl> <chr>     <dbl>
1 84    01    01001  01001   2018 A401          2
2 84    01    01001  01001   2018 A404          4
3 84    01    01001  01001   2018 A504          1
4 84    01    01001  01001   2018 A507          1
5 84    01    01001  01001   2018 B203          1
6 84    01    01001  01001   2018 C104          1
# ℹ 1,035,558 more rows

Voici quelques utilisations fréquentes de arrange() :

Action Code
Trier sur une colonne en ordre croissant arrange(NB_EQUIP)
Trier sur plusieurs colonnes en ordre croissant arrange(DEPCOM, NB_EQUIP)
Trier sur une colonne en ordre décroissant arrange(desc(NB_EQUIP))

Comme il n’est pas nécessaire dans R de trier les tables avant de faire une jointure (merge), le tri est utile surtout pour afficher à l’écran un tableau synthétique de données.

17.3.1.5 Ajouter et modifier des colonnes : mutate()

La fonction mutate() permet de créer de nouvelles colonnes ou de modifier des colonnes existantes. Il est possible d’utiliser toutes sortes de fonctions à l’intérieur d’une étape mutate(). Le code suivant crée une variable NB_EQUIP_3PLUS qui vaut TRUE si le nombre d’équipement est supérieur ou égal à 3, et FALSE sinon.

bpe_ens_2018_tbl %>% 
  mutate(NB_EQUIP_3PLUS = (NB_EQUIP >= 3))
# A tibble: 1,035,564 × 8
  REG   DEP   DEPCOM DCIRIS    AN TYPEQU NB_EQUIP NB_EQUIP_3PLUS
  <chr> <chr> <chr>  <chr>  <dbl> <chr>     <dbl> <lgl>         
1 84    01    01001  01001   2018 A401          2 FALSE         
2 84    01    01001  01001   2018 A404          4 TRUE          
3 84    01    01001  01001   2018 A504          1 FALSE         
4 84    01    01001  01001   2018 A507          1 FALSE         
5 84    01    01001  01001   2018 B203          1 FALSE         
6 84    01    01001  01001   2018 C104          1 FALSE         
# ℹ 1,035,558 more rows

Pour créer une nouvelle variable, on utilise un nom de variable qui n’existe pas encore dans la table. Pour modifier une variable qui existe déjà, on utilise directement le nom de cette variable.

Voici quelques utilisations fréquentes de mutate() :

Action Code
Calculer une somme cumulée mutate(NB_EQUIP_CUM = cumsum(NB_EQUIP, na.rm = TRUE))
Calculer un total mutate(NB_EQUIP_TOT = sum(NB_EQUIP, na.rm = TRUE))
Sommer deux variables mutate(NB_EQUIP_DOUBLE = NB_EQUIP + NB_EQUIP)
Extraire une sous-chaine de caractères (voir la fiche données textuelles) mutate(CATEGORIE_EQ = str_sub(TYPEQU, 1L, 1L))

17.3.1.6 Calculer des statistiques : summarise()

La fonction summarise() permet de calculer une ou plusieurs statistiques à partir de la table de données. Cette fonction est souvent utilisée après la fonction group_by() pour calculer des statistiques par groupe, et elle conduit à une agrégation de la table en fonction des groupes définis par la fonction group_by (par défaut une agrégation sur l’ensemble de la table). Le code suivant calcule le nombre total d’équipements dans la BPE sum(NB_EQUIP, na.rm = TRUE), et le nombre total de boulangeries sum(NB_EQUIP * (TYPEQU == "B203"), na.rm = TRUE).

bpe_ens_2018_tbl %>% 
  summarise(
    NB_EQUIP_TOT = sum(NB_EQUIP, na.rm = TRUE),
    NB_BOULANGERIES_TOT = sum(NB_EQUIP * (TYPEQU == "B203"), na.rm = TRUE)
  )
# A tibble: 1 × 2
  NB_EQUIP_TOT NB_BOULANGERIES_TOT
         <dbl>               <dbl>
1      2504782               48568

Il est possible d’utiliser un grand nombre de fonctions différentes avec summarise(). Ces fonctions peuvent être combinées entre elles, et il est possible d’en définir de nouvelles. Voici quelques fonctions courantes :

Fonction Code
Moyenne mean()
Médiane median()
Ecart-type sd()
Minimum min()
Maximum max()
Valeur de la première valeur first()
Valeur de la première valeur last()
Nombre de lignes n()
Nombre de valeurs distinctes n_distinct()
Somme sum()
Somme cumulée cumsum()
Note

Les fonctions mutate() et summarise() calculent toutes les deux de nouvelles variables. Il arrive donc fréquemment qu’on les confonde, ou qu’on ne sache pas laquelle il faut utiliser. Comme indiqué précédemment, l’une, mutate(), conduit à l’ajout d’une variable supplémentaire, l’autre, summarise(), définit une procédure d’agrégation de la donnée. Voici une règle simple pour savoir quelle fonction utiliser :

  • Si vous voulez résumer une information contenue dans une table, il faut utiliser summarise(). Exemple : calculer le nombre total d’équipements pour chaque commune.

    bpe_ens_2018_tbl %>%  
      group_by(DEPCOM) %>% 
      summarise(NB_EQUIP_TOT = sum(NB_EQUIP, na.rm = TRUE))
  • Si vous voulez ajouter une information dans une table (en conservant toutes les autres variables), il faut utiliser mutate(). Exemple : ajouter dans la table bpe_ens_2018_tbl une colonne donnant le nombre total d’équipements pour chaque commune.

    bpe_ens_2018_tbl %>%  
    group_by(DEPCOM) %>% 
    mutate(NB_EQUIP_TOT = sum(NB_EQUIP, na.rm = TRUE))

17.3.1.7 Faire des opérations par groupe : group_by()

La fonction group_by() permet de définir des groupes dans la table de données pour faire des opérations par groupe. L’utilisation de group_by() rend très utiles les opérations avec summarise(). Le code suivant groupe les données de la BPE par département group_by(DEP) puis calcule le nombre total d’équipements sum(NB_EQUIP, na.rm = TRUE) et le nombre total de boulangeries sum(NB_EQUIP * (TYPEQU == "B203"), na.rm = TRUE).

bpe_ens_2018_tbl %>% 
  group_by(DEP) %>%
  summarise(NB_EQUIP_TOT = sum(NB_EQUIP, na.rm = TRUE),
            NB_BOULANGERIES_TOT = sum(NB_EQUIP * (TYPEQU == "B203"), na.rm = TRUE))
# A tibble: 101 × 3
  DEP   NB_EQUIP_TOT NB_BOULANGERIES_TOT
  <chr>        <dbl>               <dbl>
1 01           21394                 401
2 02           15534                 339
3 03           12216                 299
4 04            8901                 185
5 05            8785                 175
6 06           66766                 966
# ℹ 95 more rows

La fonction group_by peut également modifier le comportement des fonctions filter ou mutate, comme pour summarise. L’instruction suivante permet ainsi d’ajouter à la BPE une variable égale au nombre total de boulangeries dans le département.

bpe_ens_2018_tbl %>% 
  group_by(DEP) %>%
  mutate(NB_BOULANGERIES_DEP = sum(NB_EQUIP * (TYPEQU == "B203"), na.rm = TRUE))
# A tibble: 1,035,564 × 8
# Groups:   DEP [101]
  REG   DEP   DEPCOM DCIRIS    AN TYPEQU NB_EQUIP NB_BOULANGERIES_DEP
  <chr> <chr> <chr>  <chr>  <dbl> <chr>     <dbl>               <dbl>
1 84    01    01001  01001   2018 A401          2                 401
2 84    01    01001  01001   2018 A404          4                 401
3 84    01    01001  01001   2018 A504          1                 401
4 84    01    01001  01001   2018 A507          1                 401
5 84    01    01001  01001   2018 B203          1                 401
6 84    01    01001  01001   2018 C104          1                 401
# ℹ 1,035,558 more rows

L’instruction suivante permet de ne conserver que les communes pour lesquelles le nombre d’équipements est le plus important de leur région.

bpe_ens_2018_tbl %>% 
  group_by(REG) %>%
  filter(NB_EQUIP == max(NB_EQUIP))
# A tibble: 18 × 7
# Groups:   REG [18]
  REG   DEP   DEPCOM DCIRIS        AN TYPEQU NB_EQUIP
  <chr> <chr> <chr>  <chr>      <dbl> <chr>     <dbl>
1 27    25    25056  25056_0102  2018 B302        104
2 94    2A    2A041  2A041       2018 A504        110
3 75    33    33063  33063_0505  2018 A504        172
4 76    34    34003  34003_0201  2018 A504        287
5 53    35    35288  35288_0101  2018 A504        132
6 52    44    44109  44109_0106  2018 A504        236
# ℹ 12 more rows

Ainsi, les opérations à la suite d’un group_by(), comme ici sum et max, sont calculées par groupe de lignes (ici suivant la colonne DEP).

Note

Trois remarques sur l’utilisation de group_by() :

  • si vous ne savez pas si une table comporte des groupes, vous pouvez afficher la liste des variables de groupe avec la fonction group_vars() ;
  • si vous appliquez une instruction group_by() à une table qui comporte déjà des groupes, alors les groupes sont redéfinis ;
  • il est prudent d’appliquer la fonction ungroup() à vos données une fois que les opérations par groupe ont été réalisées, afin que les opérations suivantes ne soient pas effectuées par groupe par mégarde.

17.3.1.8 Appliquer une fonction dplyr sur un groupe de colonnes : across()

La fonction across() permet de sélectionner un groupe de colonnes sur lequel on veut appliquer une fonction dplyr. Ainsi on peut appliquer une fonction sur des colonnes en les sélectionnant comme le fait select(). Si on veut sélectionner des colonnes en fonction de leur type, on utilise en outre where(). Le code suivant groupe les données de la BPE avec les deux colonnes dont le nom commence par “DEP”, puis effectue la somme de toutes les colonnes de type numérique.

bpe_ens_2018_tbl %>% 
  group_by(across(starts_with('DEP'))) %>%
  summarise(across(where(is.numeric), sum, na.rm = TRUE)) 
# A tibble: 34,065 × 4
# Groups:   DEP [101]
  DEP   DEPCOM     AN NB_EQUIP
  <chr> <chr>   <dbl>    <dbl>
1 01    01001   22198       15
2 01    01002    4036        2
3 01    01004  472212      655
4 01    01005   64576       68
5 01    01006    6054        3
6 01    01007   58522       60
# ℹ 34,059 more rows

17.3.2 Manipuler plusieurs tables avec dplyr

17.3.2.1 Concaténer deux tables

Le package dplyr propose la fonction bind_rows() pour superposer deux ou plusieurs tables (en empilant des observations). Deux remarques sur l’utilisation de cette fonction :

  • bind_rows() combine les tables en fonction du nom des colonnes, l’ordre des colonnes n’a pas donc d’importance ;
  • si une colonne est manquante dans une des tables, alors des valeurs manquantes sont générées dans la table de sortie.
Note

Le package dplyr contient également la fonction bind_cols() qui permet de juxtaposer des tables (qui doivent avoir le même nombre d’observations). Il est conseillé de ne pas se servir de cette fonction. En effet, cette fonction juxtapose les colonnes par position (la première ligne d’une table est juxtaposée à la première de l’autre table), sans aucun contrôle. Si les différentes tables ne sont pas triées de la même façon, la table de sortie sera incohérente. Pour rapprocher deux tables, il est fortement conseillé d’utiliser les fonctions de jointures : inner_join, left_join, full_join… Ces fonctions sont présentées dans le paragraphe Joindre des tables.

17.3.2.2 Joindre des tables

Avec dplyr, les jointures se réalisent grâce aux fonctions left_join, right_join, inner_join, full_join et anti_join. Ces fonctions prennent les arguments suivants :

  • le nom des deux data.frame à joindre ;
  • les variables de jointure, défini par l’argument by. Lorsque la variable de jointure ne porte pas le même nom dans les deux tables, on utilise le paramètre by = c("var_x" = "var_y"). S’il y a plusieurs variables de jointures, on écrit by = c("var_x1" = "var_y1", "var_x2" = "var_y2").

Il est préférable d’utiliser ces fonctions sur des objets tibble plutôt que data.frame. On va donc convertir les deux tables avant de présenter un exemple :

library(dplyr)
filosofi_com_2016_tbl <- as_tibble(doremifasolData::filosofi_com_2016)
cog_com_2019_tbl <- as_tibble(doremifasolData::cog_com_2019)

Voici un exemple dans lequel on utilise la fonction left_join pour réaliser une jointure à gauche entre la table des données Filosofi et la table des communes du COG.

table_jointe_tbl <- filosofi_com_2016_tbl %>% 
left_join(y = cog_com_2019_tbl, 
by = c("CODGEO" = "com"))
head(table_jointe_tbl)
# A tibble: 6 × 39
  CODGEO LIBGEO      NBMENFISC16 NBPERSMENFISC16  MED16 PIMP16 TP6016 TP60AGE116
  <chr>  <chr>             <dbl>           <dbl>  <dbl>  <dbl>  <dbl>      <dbl>
1 01001  L'Abergeme…         313            796. 22679      NA     NA         NA
2 01002  L'Abergeme…         101            248  24382.     NA     NA         NA
3 01004  Ambérieu-e…        6363          14228  19721      49     17         19
4 01005  Ambérieux-…         633           1662. 23378      NA     NA         NA
5 01006  Ambléon              NA             NA     NA      NA     NA         NA
6 01007  Ambronay           1087           2684  22146.     57     NA         NA
# ℹ 31 more variables: TP60AGE216 <dbl>, TP60AGE316 <dbl>, TP60AGE416 <dbl>,
#   TP60AGE516 <dbl>, TP60AGE616 <dbl>, TP60TOL116 <dbl>, TP60TOL216 <dbl>,
#   PACT16 <dbl>, PTSA16 <dbl>, PCHO16 <dbl>, PBEN16 <dbl>, PPEN16 <dbl>,
#   PPAT16 <dbl>, PPSOC16 <dbl>, PPFAM16 <dbl>, PPMINI16 <dbl>, PPLOGT16 <dbl>,
#   PIMPOT16 <dbl>, D116 <dbl>, D916 <dbl>, RD16 <dbl>, typecom <chr>,
#   reg <chr>, dep <chr>, arr <chr>, tncc <chr>, ncc <chr>, nccenr <chr>,
#   libelle <chr>, can <chr>, comparent <chr>

La syntaxe pour réaliser les autres types de jointure est très similaire :

Type de jointure Syntaxe dplyr
Jointure à gauche left_join(x = filosofi_com_2016_tbl, y = cog_com_2019_tbl, by = c("CODGEO" = "com"))
Jointure à droite right_join(x = filosofi_com_2016_tbl, y = cog_com_2019_tbl, by = c("CODGEO" = "com"))
Jointure externe full_join(x = filosofi_com_2016_tbl, y = cog_com_2019_tbl, by = c("CODGEO" = "com"))
Anti-jointure anti_join(x = filosofi_com_2016_tbl, y = cog_com_2019_tbl, by = c("CODGEO" = "com"))

La jointure de deux ou plusieurs tables est une opération lourde qui doit être soigneusement préparée. Il est recommandé de consulter la fiche [Joindre des tables de données] qui présente un certain nombre de règles et de bonnes pratiques sur les jointures.

17.4 Mettre en ordre des données avec tidyr

Le package tidyr fournit de multiples fonctions pour retraiter et restructurer des données afin de les organiser sous forme de table (on parle de tidy data). Ces fonctions permettent de résoudre un grand nombre de problèmes : retraiter des colonnes et des lignes, restructurer des tables, convertir des listes imbriquées en tables de données… Elles prennent la forme de verbes qui complètent ceux de dplyr et s’intègrent parfaitement dans les séries de pipes (%>%), les pipelines, permettant d’enchaîner les opérations.

17.4.1 Retraiter des colonnes avec tidyr

17.4.1.1 separate : scinder une colonne en plusieurs

Il arrive que plusieurs informations réunies en une seule colonne et qu’on souhaite les séparer. La fonction separate permet d’effectuer cette opération. Elle prend trois arguments principaux :

  • le nom de la colonne à scinder ;
  • un vecteur indiquant les noms des nouvelles variables à créer ;
  • le séparateur sep indique à quel endroit la variable doit être scindée. Par défaut separate scinde au niveau des caractères non-alphanumérique (espace, symbole, etc.). Si l’on indique un nombre entier n, alors la colonne est scindée après le n-ième caractère.

Voici un exemple qui utilise la table des communes du Code Officiel Géographique. Dans cette table, la colonne com (code commune Insee) contient deux informations : le numéro du département et le numéro de la commune.

cog_com_2019_tbl <- doremifasolData::cog_com_2019 %>% as_tibble()
cog_com_2019_tbl
# A tibble: 37,930 × 11
  typecom com   reg   dep   arr   tncc  ncc       nccenr libelle can   comparent
  <chr>   <chr> <chr> <chr> <chr> <chr> <chr>     <chr>  <chr>   <chr> <chr>    
1 COM     01001 84    01    012   5     ABERGEME… Aberg… L'Aber… 0108  <NA>     
2 COM     01002 84    01    011   5     ABERGEME… Aberg… L'Aber… 0101  <NA>     
3 COM     01004 84    01    011   1     AMBERIEU… Ambér… Ambéri… 0101  <NA>     
4 COM     01005 84    01    012   1     AMBERIEU… Ambér… Ambéri… 0122  <NA>     
5 COM     01006 84    01    011   1     AMBLEON   Amblé… Ambléon 0104  <NA>     
6 COM     01007 84    01    011   1     AMBRONAY  Ambro… Ambron… 0101  <NA>     
# ℹ 37,924 more rows

Voici comment on peut utiliser separate pour scinder com en deux nouvelles colonnes code_dep et code_com. Vous pouvez noter que la colonne com a disparu, car par défaut separate supprime la colonne scindée. Si on veut la conserver, il faut ajouter l’option remove = FALSE.

cog_com_2019_tbl %>% 
  separate(com, c("code_dep", "code_com"), sep = 2)
# A tibble: 37,930 × 12
  typecom code_dep code_com reg   dep   arr   tncc  ncc     nccenr libelle can  
  <chr>   <chr>    <chr>    <chr> <chr> <chr> <chr> <chr>   <chr>  <chr>   <chr>
1 COM     01       001      84    01    012   5     ABERGE… Aberg… L'Aber… 0108 
2 COM     01       002      84    01    011   5     ABERGE… Aberg… L'Aber… 0101 
3 COM     01       004      84    01    011   1     AMBERI… Ambér… Ambéri… 0101 
4 COM     01       005      84    01    012   1     AMBERI… Ambér… Ambéri… 0122 
5 COM     01       006      84    01    011   1     AMBLEON Amblé… Ambléon 0104 
6 COM     01       007      84    01    011   1     AMBRON… Ambro… Ambron… 0101 
# ℹ 37,924 more rows
# ℹ 1 more variable: comparent <chr>

17.4.1.2 unite : regrouper plusieurs colonnes en une seule

La fonction unite permet est de réaliser l’opération inverse de separate : regrouper plusieurs colonnes en une seule. Elle prend trois arguments principaux :

  • le nom de la colonne à créer ;
  • un vecteur indiquant les noms des variables à regrouper ;
  • le séparateur sep qui indique quel séparateur doit être introduit entre les variables regroupées (par défaut, unite utilise le caractère _).

Voici un exemple où l’on regroupe le code commune Insee et le nom officiel de la commune, avec ” - ” comme séparateur. Vous pouvez noter que les colonnes com et ncc ont disparu, car par défaut unite supprime les colonnes regroupées. Si on veut les conserver, il faut ajouter l’option remove = FALSE.

cog_com_2019_tbl %>% 
  unite(code_et_nom, c("com", "ncc"), sep = " - ") 
# A tibble: 37,930 × 10
  typecom code_et_nom     reg   dep   arr   tncc  nccenr libelle can   comparent
  <chr>   <chr>           <chr> <chr> <chr> <chr> <chr>  <chr>   <chr> <chr>    
1 COM     01001 - ABERGE… 84    01    012   5     Aberg… L'Aber… 0108  <NA>     
2 COM     01002 - ABERGE… 84    01    011   5     Aberg… L'Aber… 0101  <NA>     
3 COM     01004 - AMBERI… 84    01    011   1     Ambér… Ambéri… 0101  <NA>     
4 COM     01005 - AMBERI… 84    01    012   1     Ambér… Ambéri… 0122  <NA>     
5 COM     01006 - AMBLEON 84    01    011   1     Amblé… Ambléon 0104  <NA>     
6 COM     01007 - AMBRON… 84    01    011   1     Ambro… Ambron… 0101  <NA>     
# ℹ 37,924 more rows

17.4.1.3 replace_na : remplacer des valeurs manquantes

La fonction replace_na permet de remplacer des valeurs manquantes (NA). Cette fonction peut être utilisée de deux façons, que l’on va illustrer avec les données suivantes :

df <- tibble(x = c(1, 2, 3, NA), y = c("a", NA, "b", NA))
df
# A tibble: 4 × 2
      x y    
  <dbl> <chr>
1     1 a    
2     2 <NA> 
3     3 b    
4    NA <NA> 

Premier usage : on remplace les valeurs manquantes dans une colonne d’un data.frame. Dans ce cas, la fonction prend deux arguments : le nom de la variable et la valeur utilisée pour remplacer les valeurs manquantes. Voici un exemple :

df %>% 
  mutate(x = replace_na(x, 888))
# A tibble: 4 × 2
      x y    
  <dbl> <chr>
1     1 a    
2     2 <NA> 
3     3 b    
4   888 <NA> 

Second usage : on remplace les valeurs manquantes dans toutes les colonnes d’un data.frame. Dans ce cas, la fonction prend deux arguments : le nom du data.frame, et une liste donnant pour chaque variable la valeur à utiliser pour remplacer les valeurs manquantes. Voici un exemple :

df %>% 
  replace_na(list(x = 888, y = "zzz"))
# A tibble: 4 × 2
      x y    
  <dbl> <chr>
1     1 a    
2     2 zzz  
3     3 b    
4   888 zzz  

17.4.1.4 complete : compléter des combinaisons de variables manquantes

La fonction complete permet de compléter des combinaisons manquantes de valeurs de plusieurs colonnes (autrement dit, de compléter un produit cartésien incomplet). Dans le tableau de donnée suivant, seuls les pays ayant remporté la Coupe du Monde de football sont renseignés, avec l’année de leur victoire. Supposons qu’on veuille compléter ce tableau, en indiquant 0 lorsqu’un pays n’a pas remporté la Coupe du Monde.

df <- tibble(pays = c("France", "France", "Espagne", "Angleterre"),
             annee = c(1998, 2018, 2010, 1966),
             victoire = c(1, 1, 1, 1))
df
# A tibble: 4 × 3
  pays       annee victoire
  <chr>      <dbl>    <dbl>
1 France      1998        1
2 France      2018        1
3 Espagne     2010        1
4 Angleterre  1966        1

On peut l’utiliser de cette manière :

df %>% complete(pays, annee)
# A tibble: 12 × 3
  pays       annee victoire
  <chr>      <dbl>    <dbl>
1 Angleterre  1966        1
2 Angleterre  1998       NA
3 Angleterre  2010       NA
4 Angleterre  2018       NA
5 Espagne     1966       NA
6 Espagne     1998       NA
# ℹ 6 more rows

On voit que les combinaisons manquantes ont bien été ajoutées par complete. Par défaut les lignes insérées comprennent des valeurs manquantes NA pour les colonnes restantes. On peut néanmoins choisir une autre valeur avec l’argument fill, qui prend la forme d’une liste nommée. Dans le cas présent, la liste ne comprend qu’un seul élément car il n’y a qu’une seule variable à compléter :

df %>% complete(pays, annee, fill = list(victoire = 0))
# A tibble: 12 × 3
  pays       annee victoire
  <chr>      <dbl>    <dbl>
1 Angleterre  1966        1
2 Angleterre  1998        0
3 Angleterre  2010        0
4 Angleterre  2018        0
5 Espagne     1966        0
6 Espagne     1998        0
# ℹ 6 more rows

Si l’on ne souhaite pas inclure toutes les colonnes dans le calcul des combinaisons de valeurs, on peut utiliser l’option nesting (taper ?tidyr::complete pour les détails).

17.4.2 Restructurer des données avec tidyr

17.4.2.1 Principe de la restructuration des données

Une table de données stocke des informations sous forme de lignes et de colonnes. Au cours d’un traitement statistique, il est souvent nécessaire de restructurer les données, en transformant en colonnes certaines informations qui figuraient en ligne (ou inversement). Les deux principales opérations de restructuration des données peuvent être illustrées par les deux transformations suivantes :

  • Transformation wide to long :

Transformation wide to long
  • Transformation long to wide :

Transformation long to wide

Pour illustrer ces transformations, nous allons utiliser les données du répertoire Filosofi 2016 agrégées au niveau des EPCI (table filosofi_epci_2016), et disponibles dans le package doremifasolData. On convertit cette table en tibble et on conserve uniquement certaines variables grâce à la fonction select.

filosofi_epci_2016_tbl <- as_tibble(filosofi_epci_2016) %>% 
  select(CODGEO, TP6016, TP60AGE116, TP60AGE216,
         TP60AGE316, TP60AGE416, TP60AGE516, TP60AGE616)

17.4.2.2 pivot_longer : transformer des colonnes en lignes

La fonction pivot_longer permet de restructurer des données en transformant des colonnes en lignes. Elle sert fréquemment au début d’un traitement, pour transformer des données mal structurées en une table facile à traiter. Cette fonction prend quatre arguments principaux :

  • le data.frame (ou le tibble) auquel elle est appliquée ;
  • cols : un vecteur contenant le nom des colonnes dont les valeurs vont être transposées ;
  • names_to : le nom de la nouvelle colonne qui va contenir les noms des colonnes transposées ;
  • values_to : le nom de la nouvelle colonne qui va contenir les valeurs des colonnes transposées.

Pour illustrer l’usage de cette fonction, nous allons utiliser les données du répertoire Filosofi 2016 agrégées au niveau des EPCI (table filosofi_epci_2016), et disponibles dans le package doremifasol. On convertit cette table en tibble et on conserve uniquement certaines variables grâce à la fonction select.

filosofi_epci_2016_tbl <- as_tibble(filosofi_epci_2016) %>% 
  select(CODGEO, TP6016, TP60AGE116, TP60AGE216,
         TP60AGE316, TP60AGE416, TP60AGE516, TP60AGE616)
filosofi_epci_2016_tbl
# A tibble: 1,244 × 8
  CODGEO    TP6016 TP60AGE116 TP60AGE216 TP60AGE316 TP60AGE416 TP60AGE516
  <chr>      <dbl>      <dbl>      <dbl>      <dbl>      <dbl>      <dbl>
1 200000172    8.8       12.5        9.4        9.7        7.9        6.6
2 200000438    8          9.5        6.5        9.1        9          6  
3 200000545   23.7       38.2       30.8       32         20.5       14.9
4 200000628   20.1       28.1       22.4       25.5       18.2       17.1
5 200000800   11.4       NA         NA         15.9       12         NA  
6 200000925    6.5       NA          6          6.7        7.6        5.5
# ℹ 1,238 more rows
# ℹ 1 more variable: TP60AGE616 <dbl>

Nous allons restructurer cette table pour obtenir une nouvelle table, avec une observation par EPCI et par tranche d’âge. Voici le code qui permet d’obtenir cette table : on transpose les valeurs des colonnes dont le nom commence par “TP” (cols = starts_with("TP")), le nom des colonnes transposées sera indiquée dans la nouvelle colonne “tranche_age” (names_to = "tranche_age") et les valeurs des colonnes transposées seront indiquées dans la colonne “taux_pauvrete” (values_to = "taux_pauvrete").

donnees_pauvrete_long <- filosofi_epci_2016_tbl %>% 
  pivot_longer(cols = starts_with("TP"), 
               names_to = "tranche_age", 
               values_to = "taux_pauvrete")
donnees_pauvrete_long
# A tibble: 8,708 × 3
   CODGEO    tranche_age taux_pauvrete
   <chr>     <chr>               <dbl>
 1 200000172 TP6016                8.8
 2 200000172 TP60AGE116           12.5
 3 200000172 TP60AGE216            9.4
 4 200000172 TP60AGE316            9.7
 5 200000172 TP60AGE416            7.9
 6 200000172 TP60AGE516            6.6
 7 200000172 TP60AGE616           NA  
 8 200000438 TP6016                8  
 9 200000438 TP60AGE116            9.5
10 200000438 TP60AGE216            6.5
# ℹ 8,698 more rows
Tip

Il est recommandé de travailler avec des données en format long plutôt qu’en format wide, notamment lorsque vous voulez faire des graphiques. En effet, le package de visualisation graphique ggplot2 est optimisé pour manipuler des données en format long (voir la fiche [Faire des graphiques avec ggplot2]). Ce conseil est particulièrement important si vous voulez représenter un graphique avec des groupes : il est préférable que les groupes soient empilés (format long) plutôt que juxtaposés (format wide), car le code est plus rapide et surtout plus facile à écrire.

17.4.2.3 pivot_wider : transformer des lignes en colonnes

La fonction pivot_wider permet de restructurer des données en transformant des lignes en colonnes. Cette fonction prend quatre arguments principaux :

  • le data.frame (ou le tibble) auquel elle est appliquée ;
  • id_cols : un vecteur contenant le nom des colonnes qui définissent les observations de la table transposée ;
  • names_from : un vecteur contenant le nom de la (ou des) colonne(s) qui donne(nt) les noms des nouvelles colonnes ;
  • values_from : un vecteur contenant le nom de la (ou des) colonne(s) dont les valeurs vont être transposées.

Par ailleurs, l’option names_prefix permet de définir le préfixe du nom des nouvelles colonnes, ce qui est utile pour avoir des noms explicites.

bpe_ens_2018_tbl %>%
    group_by(REG, TYPEQU) %>%
    summarise(NB_EQUIP_TOT = sum(NB_EQUIP)) %>%
    pivot_wider(id_cols =TYPEQU,  
                names_from = REG, 
                values_from = NB_EQUIP_TOT, 
                names_prefix = "nb_equip_reg")
# A tibble: 186 × 19
  TYPEQU nb_equip_reg01 nb_equip_reg02 nb_equip_reg03 nb_equip_reg04
  <chr>           <dbl>          <dbl>          <dbl>          <dbl>
1 A101                2              2              1              7
2 A104               20             21             16             28
3 A105                1              1              1              1
4 A106                2              1              2              2
5 A107                2              1              1              4
6 A108                2              1              1              2
# ℹ 180 more rows
# ℹ 14 more variables: nb_equip_reg06 <dbl>, nb_equip_reg11 <dbl>,
#   nb_equip_reg24 <dbl>, nb_equip_reg27 <dbl>, nb_equip_reg28 <dbl>,
#   nb_equip_reg32 <dbl>, nb_equip_reg44 <dbl>, nb_equip_reg52 <dbl>,
#   nb_equip_reg53 <dbl>, nb_equip_reg75 <dbl>, nb_equip_reg76 <dbl>,
#   nb_equip_reg84 <dbl>, nb_equip_reg93 <dbl>, nb_equip_reg94 <dbl>
Tip

Il est conseillé de bien réfléchir avant de restructurer en format wide, et de ne le faire que lorsque cela paraît indispensable. En effet, s’il est tentant de restructurer les données sous format wide car ce format peut paraître plus intuitif, il est généralement plus simple et plus rigoureux de traiter les données en format long. Ceci dit, il existe des situations dans lesquelles il est indiqué de restructurer les données en format wide. Voici deux exemples :

  • produire un tableau synthétique de résultats, prêt à être diffusé, avec quelques colonnes donnant des indicateurs par catégorie (exemple : la table filosofi_epci_2016 du package doremifasolData, qui contient plusieurs colonnes contenant le taux de pauvreté pour différentes tranches d’âge) ;
  • produire une table avec une colonne par année, de façon à calculer facilement un taux d’évolution entre deux dates.

17.5 Programmer des fonctions avec le tidyverse

Lorsqu’une suite d’opérations est amenée à être effectuée plusieurs fois, une bonne pratique consiste à factoriser son code, notamment grâce à des fonctions définies par l’utilisateur. Le site internet pratiques.utilitr.org présente notamment quelques recommandations de cet ordre.

17.5.1 Présentation de la difficulté à fonctionnaliser

La fonctionnalisation n’est pas simple lorsqu’on travaille avec le tidyverse, en raison de l’évaluation non standard que celui-ci exploite. En effet, le tidyverse est un paradigme de traitement des données qui cherche à faciliter au maximum son utilisation. Pour ce faire, comme vu précédemment, il n’est pas besoin d’écrire le nom des variables entre guillemets, ou de répéter le nom de la base que l’on exploite, ni même d’utiliser le $. Toutefois, cette facilité génère des difficultés lorsqu’on souhaite créer une fonction. Le code suivant montre deux traitements qui, en toute logique, devrait donner des résultats similaires.

head(bpe_ens_2018) %>% select(REG) # fonctionne

selectionner_colonne <- function(donnees,colonne) {
  retour <- donnees %>%
    select(colonne)
  
  return(retour)
}

# Ce code ne fonctionne pas
selectionner_colonne(
  donnees=head(bpe_ens_2018),
  colonne = REG)

Alors que R parvient parfaitement à comprendre que le terme REG fait référence à un nom de colonne de la base dans le premier cas, il ne le comprend pas dans le second cas ! Ceci est dû aux fonctions du tidyverse qui permettent cette flexibilité, tandis que la fonction selectionner_colonne cherche un objet qui s’intitulerait REG, sans comprendre qu’il s’agit du nom de la colonne du jeu de données.

Note

L’évaluation non-standard est un concept complexe et très riche dans R. La tidy-evaluation en est une utilisation spécifique.

De nombreuses fonctionnalités élémentaires de R utilisent de telles évaluations. Par exemple, les commandes library(dplyr) ou library("dplyr") fonctionnent toutes les deux indifféremment. Pour autant, l’objet dplyr de la première expression n’existe pas. R comprend implicitement que nous voulions parler du nom d’un package et charge le package "dplyr".

L’évaluation, standard ou non-standard, repose donc sur l’idée que R doit savoir quand nous faisons référence au nom d’un objet ou à l’objet lui-même. Par exemple, quand on écrit head(bpe_ens_2018), R comprend que l’appel à bpe_ens_2018 fait référence à une table de données nommée “bpe_ens_2018”, pas au nom en question. La tidy-evaluation permet de faire des appels un peu plus complexes. Ainsi, lorsque nous écrivons select(REG), R comprend que quelques opérations préalables sont nécessaires :

  1. patienter : il ne faut pas chercher à comprendre immédiatement le sens de REG car aucun objet dans notre environnement ne s’appelle REG ;
  2. transporter : R va reconstituer toute la chaine de traitement select(head(bpe_ens_2018), REG) avant de chercher à interpréter ;
  3. réinterpréter : R peut désormais chercher à identifier si une colonne de head(bpe_ens_2018) se nomme REG.

17.5.2 Présentation de la solution

Pour réussir à créer des fonctions facilement compatibles avec le tidyverse, il faut faire appel à un opérateur spécifique, appelé curly-curly ou doublestache : { }. Il s’agit de l’équivalent tidyverse de la fonction get présentée dans le chapitre Manipuler ses données avec data.table. Son utilisation est particulièrement simple, car il suffit d’entourer la variable par ces accolades.

selectionner_colonne <- function(donnees,colonne) {
  retour <- donnees %>%
    select({{colonne}})
  
  return(retour)
}

# Cette fois, le code fonctionne!
selectionner_colonne(donnees=head(bpe_ens_2018),
               colonne = REG)
  REG
1  84
2  84
3  84
4  84
5  84
6  84

Le doublestache est une nouvelle notation du package rlang(version 0.4.0), équivalent à l’opération !!enquo(). L’idée générale étant que enquo() permet de transformer un input en symbole à interpréter, tandis que l’opérateur bang-bang !! détermine quand il faut l’interpréter. Parfois, certaines opérations un peu complexes, qui dépassent le cadre de cette fiche nécessitent le recours à ces opérateurs particuliers.

Note

Il peut arriver que vous trouviez sur internet une page recommandant l’usage de l’expression eval(parse()). Il est fortement conseillé de ne jamais l’utiliser pour les raisons suivantes :

  • elle est susceptible de créer des problèmes de sécurité. Tout le contenu à l’intérieur du parse est interprété par R comme du code à lancer, quel que soit le contenu. Il est donc particulièrement sensible aux problèmes d’injection de code ;
  • sa performance est très mauvaise ;
  • l’identification des bug est particulièrement délicate, avec des messages d’erreurs peu opportuns et compliqués à comprendre.

Lorsqu’on souhaite définir une nouvelle colonne dans le tidyverse, une légère modification est nécessaire, avec l’utilisation de l’opérateur :=, comme dans l’exemple suivant.

selectionner_et_renommer <- function(donnees,colonne, nouveau_nom = "nouveau_nom") {
  retour <- donnees %>%
    select({{colonne}}) %>%
    rename({{nouveau_nom}}:={{colonne}})
  
  return(retour)
}

# Ce code fonctionne!
selectionner_et_renommer(
  donnees=head(bpe_ens_2018),
  colonne = REG,
  nouveau_nom = "region")
  region
1     84
2     84
3     84
4     84
5     84
6     84

Notez bien la présence des := dans l’opération de renommage. Il est également prudent de proposer une valeur par défault à la variable dans la fonction. Ainsi, si l’utilisateur oublie d’indiquer un nouveau nom, la colonne sera renommée en nouveau_nom.

Note

L’opérateur := du tidyverse ne remplit pas les mêmes fonctions que l’opérateur := présenté dans la fiche Manipuler ses données avec data.table:

  • Dans data.table, l’opérateur sert à créer une nouvelle variable en limitant le coût en mémoire de cette opération ;
  • Dans le tidyverse, l’opérateur sert à faire comprendre à dplyr que la partie à gauche est un nom de colonne à ré-interpréter sans les guillemets.
Tip

Pour fonctionnaliser un code utilisant le tidyverse, il est recommandé :

  • d’utiliser au maximum l’opérateur doublestache {{ qui présente l’intérêt d’être très lisible et très simple d’utilisation ;
  • lorsque l’évaluation doit avoir lieu “à gauche” d’un opérateur =, il faut habituellement le remplacer par := ;
  • d’éviter l’utilisation de eval(parse()).

Si les exemples précédents se basent tous sur dplyr, il est à remarquer que les solutions présentées s’appliquent à tous les packages du tidyverse, y compris ggplot2 (c.f. Faire des graphiques avec ggplot2).

17.6 Pour en savoir plus