36  Réorganisation avec tidyr

36.1 Tidy data

Comme indiqué dans le chapitre sur les tibbles (cf. Chapitre 5), les extensions du tidyverse comme dplyr ou ggplot2 partent du principe que les données sont “bien rangées” sous forme de tidy data.

Prenons un exemple avec les données suivantes, qui indique la population de trois pays pour quatre années différentes :

country 1992 1997 2002 2007
Belgium 10045622 10199787 10311970 10392226
France 57374179 58623428 59925035 61083916
Germany 80597764 82011073 82350671 82400996

Imaginons qu’on souhaite représenter avec ggplot2 l’évolution de la population pour chaque pays sous forme de lignes : c’est impossible avec les données sous ce format. On a besoin d’arranger le tableau de la manière suivante :

country year population
Belgium 1992 10045622
Belgium 1997 10199787
Belgium 2002 10311970
Belgium 2007 10392226
France 1992 57374179
France 1997 58623428
France 2002 59925035
France 2007 61083916
Germany 1992 80597764
Germany 1997 82011073
Germany 2002 82350671
Germany 2007 82400996

C’est seulement avec les données dans ce format qu’on peut réaliser le graphique :

library(tidyverse)
ggplot(d) + 
  aes(x = year, y = population, color = country) +
  geom_line() +
  scale_x_continuous(breaks = unique(d$year)) +
  scale_y_continuous(
    labels = scales::label_number(
      scale = 10^-6,
      suffix = " millions"
    )
  )

C’est la même chose pour dplyr, par exemple si on voulait calculer la population minimale pour chaque pays avec dplyr::summarise() :

d |> 
  group_by(country) |>  
  summarise(pop_min = min(population))
# A tibble: 3 × 2
  country  pop_min
  <fct>      <int>
1 Belgium 10045622
2 France  57374179
3 Germany 80597764

36.2 Trois règles pour des données bien rangées

Le concept de tidy data repose sur trois règles interdépendantes. Des données sont considérées comme tidy si :

  1. chaque ligne correspond à une observation
  2. chaque colonne correspond à une variable
  3. chaque valeur est présente dans une unique case de la table ou, de manière équivalente, si des unités d’observations différentes sont présentes dans des tables différentes

Ces règles ne sont pas forcément très intuitives. De plus, il y a une infinité de manières pour un tableau de données de ne pas être tidy.

Prenons par exemple les règles 1 et 2 et le tableau de notre premier exemple :

country 1992 1997 2002 2007
Belgium 10045622 10199787 10311970 10392226
France 57374179 58623428 59925035 61083916
Germany 80597764 82011073 82350671 82400996

Pourquoi ce tableau n’est pas tidy ? Parce que si l’on essaie d’identifier les variables mesurées dans le tableau, il y en a trois : le pays, l’année et la population. Or elles ne correspondent pas aux colonnes de la table. C’est le cas par contre pour la table transformée :

country annee population
Belgium 1992 10045622
France 1992 57374179
Germany 1992 80597764
Belgium 1997 10199787
France 1997 58623428
Germany 1997 82011073
Belgium 2002 10311970
France 2002 59925035
Germany 2002 82350671
Belgium 2007 10392226
France 2007 61083916
Germany 2007 82400996

On peut remarquer qu’en modifiant notre table pour satisfaire à la deuxième règle, on a aussi réglé la première : chaque ligne correspond désormais à une observation, en l’occurrence l’observation de trois pays à plusieurs moments dans le temps. Dans notre table d’origine, chaque ligne comportait en réalité quatre observations différentes.

Ce point permet d’illustrer le fait que les règles sont interdépendantes.

Autre exemple, généré depuis le jeu de données nycflights13, permettant cette fois d’illustrer la troisième règle :

year month day dep_time carrier name flights_per_year
2013 1 1 517 UA United Air Lines Inc. 58665
2013 1 1 533 UA United Air Lines Inc. 58665
2013 1 1 542 AA American Airlines Inc. 32729
2013 1 1 554 UA United Air Lines Inc. 58665
2013 1 1 558 AA American Airlines Inc. 32729
2013 1 1 558 UA United Air Lines Inc. 58665
2013 1 1 558 UA United Air Lines Inc. 58665
2013 1 1 559 AA American Airlines Inc. 32729

Dans ce tableau on a bien une observation par ligne (un vol), et une variable par colonne. Mais on a une “infraction” à la troisième règle, qui est que chaque valeur doit être présente dans une unique case : si on regarde la colonne name, on a en effet une duplication de l’information concernant le nom des compagnies aériennes. Notre tableau mêle en fait deux types d’observations différents : des observations sur les vols, et des observations sur les compagnies aériennes.

Pour “arranger” ce tableau, il faut séparer les deux types d’observations en deux tables différentes :

year month day dep_time carrier
2013 1 1 517 UA
2013 1 1 533 UA
2013 1 1 542 AA
2013 1 1 554 UA
2013 1 1 558 AA
2013 1 1 558 UA
2013 1 1 558 UA
2013 1 1 559 AA
carrier name flights_per_year
UA United Air Lines Inc. 58665
AA American Airlines Inc. 32729

On a désormais deux tables distinctes, l’information n’est pas dupliquée, et on peut facilement faire une jointure si on a besoin de récupérer l’information d’une table dans une autre.

L’objectif de tidyr est de fournir des fonctions pour arranger ses données et les convertir dans un format tidy. Ces fonctions prennent la forme de verbes qui viennent compléter ceux de dplyr et s’intègrent parfaitement dans les séries de pipes (|>, cf. Chapitre 7), les pipelines, permettant d’enchaîner les opérations.

36.3 pivot_longer() : rassembler des colonnes

Prenons le tableau d suivant, qui liste la population de 4 pays en 2002 et 2007 :

country 2002 2007
Belgium 10311970 10392226
France 59925035 61083916
Germany 82350671 82400996
Spain 40152517 40448191

Dans ce tableau, une même variable (la population) est répartie sur plusieurs colonnes, chacune représentant une observation à un moment différent. On souhaite que la variable ne représente plus qu’une seule colonne, et que les observations soient réparties sur plusieurs lignes.

Pour cela on va utiliser la fonction tidyr::pivot_longer() :

d |>
  pivot_longer(
    cols = c(`2002`,`2007`),
    names_to = "annee",
    values_to = "population"
  )
# A tibble: 8 × 3
  country annee population
  <fct>   <chr>      <int>
1 Belgium 2002    10311970
2 Belgium 2007    10392226
3 France  2002    59925035
4 France  2007    61083916
5 Germany 2002    82350671
6 Germany 2007    82400996
7 Spain   2002    40152517
8 Spain   2007    40448191

La fonction tidyr::pivot_longer() prend comme arguments la liste des colonnes à rassembler (on peut également y utiliser les différentes fonctions de sélection de variables utilisables avec dplyr::select()), ainsi que deux arguments names_to et values_to :

  • names_to est le nom de la colonne qui va contenir les “noms” des colonnes originelles, c’est-à-dire les identifiants des différentes observations
  • values_to est le nom de la colonne qui va contenir la valeur des observations

Parfois il est plus rapide d’indiquer à tidyr::pivot_longer() les colonnes qu’on ne souhaite pas rassembler. On peut le faire avec la syntaxe suivante :

d |>
  pivot_longer(
    -country,
    names_to = "annee",
    values_to = "population"
  )
# A tibble: 8 × 3
  country annee population
  <fct>   <chr>      <int>
1 Belgium 2002    10311970
2 Belgium 2007    10392226
3 France  2002    59925035
4 France  2007    61083916
5 Germany 2002    82350671
6 Germany 2007    82400996
7 Spain   2002    40152517
8 Spain   2007    40448191

36.4 pivot_wider() : disperser des lignes

La fonction tidyr::pivot_wider() est l’inverse de tidyr::pivot_longer().

Soit le tableau d suivant :

country continent year variable value
Belgium Europe 2002 lifeExp 78.320
Belgium Europe 2007 lifeExp 79.441
France Europe 2002 lifeExp 79.590
France Europe 2007 lifeExp 80.657
Germany Europe 2002 lifeExp 78.670
Germany Europe 2007 lifeExp 79.406
Belgium Europe 2002 pop 10311970.000
Belgium Europe 2007 pop 10392226.000
France Europe 2002 pop 59925035.000
France Europe 2007 pop 61083916.000
Germany Europe 2002 pop 82350671.000
Germany Europe 2007 pop 82400996.000

Ce tableau a le problème inverse du précédent : on a deux variables, lifeExp et pop qui, plutôt que d’être réparties en deux colonnes, sont réparties entre plusieurs lignes.

On va donc utiliser tidyr::pivot_wider() pour disperser ces lignes dans deux colonnes différentes :

d |>
  pivot_wider(
    names_from = variable,
    values_from = value
  )
# A tibble: 6 × 5
  country continent  year lifeExp      pop
  <fct>   <fct>     <int>   <dbl>    <dbl>
1 Belgium Europe     2002    78.3 10311970
2 Belgium Europe     2007    79.4 10392226
3 France  Europe     2002    79.6 59925035
4 France  Europe     2007    80.7 61083916
5 Germany Europe     2002    78.7 82350671
6 Germany Europe     2007    79.4 82400996

tidyr::pivot_wider() prend deux arguments principaux :

  • names_from indique la colonne contenant les noms des nouvelles variables à créer
  • values_from indique la colonne contenant les valeurs de ces variables

Il peut arriver que certaines variables soient absentes pour certaines observations. Dans ce cas l’argument values_fill permet de spécifier la valeur à utiliser pour ces données manquantes (par défaut, les valeurs manquantes sont indiquées avec NA).

Exemple avec le tableau d suivant :

country continent year variable value
Belgium Europe 2002 lifeExp 78.320
Belgium Europe 2007 lifeExp 79.441
France Europe 2002 lifeExp 79.590
France Europe 2007 lifeExp 80.657
Germany Europe 2002 lifeExp 78.670
Germany Europe 2007 lifeExp 79.406
Belgium Europe 2002 pop 10311970.000
Belgium Europe 2007 pop 10392226.000
France Europe 2002 pop 59925035.000
France Europe 2007 pop 61083916.000
Germany Europe 2002 pop 82350671.000
Germany Europe 2007 pop 82400996.000
France Europe 2002 density 94.000
d |> 
  pivot_wider(
    names_from =  variable,
    values_from = value
  )
# A tibble: 6 × 6
  country continent  year lifeExp      pop density
  <chr>   <chr>     <dbl>   <dbl>    <dbl>   <dbl>
1 Belgium Europe     2002    78.3 10311970      NA
2 Belgium Europe     2007    79.4 10392226      NA
3 France  Europe     2002    79.6 59925035      94
4 France  Europe     2007    80.7 61083916      NA
5 Germany Europe     2002    78.7 82350671      NA
6 Germany Europe     2007    79.4 82400996      NA
d |> 
  pivot_wider(
    names_from =  variable,
    values_from = value,
    values_fill = list(value = 0)
  )
# A tibble: 6 × 6
  country continent  year lifeExp      pop density
  <chr>   <chr>     <dbl>   <dbl>    <dbl>   <dbl>
1 Belgium Europe     2002    78.3 10311970       0
2 Belgium Europe     2007    79.4 10392226       0
3 France  Europe     2002    79.6 59925035      94
4 France  Europe     2007    80.7 61083916       0
5 Germany Europe     2002    78.7 82350671       0
6 Germany Europe     2007    79.4 82400996       0

36.5 separate() : séparer une colonne en plusieurs colonnes

Parfois on a plusieurs informations réunies en une seule colonne et on souhaite les séparer. Soit le tableau d’exemple caricatural suivant, nommé df :

df <- tibble(
  eleve = c("Alex Petit", "Bertrand Dupont", "Corinne Durand"),
  note = c("5/20", "6/10", "87/100")
)
df
# A tibble: 3 × 2
  eleve           note  
  <chr>           <chr> 
1 Alex Petit      5/20  
2 Bertrand Dupont 6/10  
3 Corinne Durand  87/100

tidyr::separate() permet de séparer la colonne note en deux nouvelles colonnes note et note_sur :

df |>
  separate(note, c("note", "note_sur"))
# A tibble: 3 × 3
  eleve           note  note_sur
  <chr>           <chr> <chr>   
1 Alex Petit      5     20      
2 Bertrand Dupont 6     10      
3 Corinne Durand  87    100     

tidyr::separate() prend deux arguments principaux, le nom de la colonne à séparer et un vecteur indiquant les noms des nouvelles variables à créer. Par défaut tidyr::separate() sépare au niveau des caractères non-alphanumérique (espace, symbole, etc.). On peut lui indiquer explicitement le caractère sur lequel séparer avec l’argument sep :

df |>
  tidyr::separate(
    eleve,
    c("prenom", "nom"),
    sep = " "
  )
# A tibble: 3 × 3
  prenom   nom    note  
  <chr>    <chr>  <chr> 
1 Alex     Petit  5/20  
2 Bertrand Dupont 6/10  
3 Corinne  Durand 87/100

36.6 separate_rows() : séparer une colonne en plusieurs lignes

La fonction tidyr::separate_rows() est utile lorsque plusieurs valeurs sont contenues dans la même variable. Mais, alors que tidyr::separate() permet de répartir ces différentes valeurs dans plusieurs colonnes, tidyr::separate_rows() va créé une ligne pour chaque valeur. Prenons cet exemple trivial où les différentes notes de chaque élève sont contenues dans la colonne notes.

df <- tibble(
  eleve = c("Alex Petit", "Bertrand Dupont", "Corinne Durand"),
  notes = c("10,15,16", "18,12,14", "16,17")
)
df
# A tibble: 3 × 2
  eleve           notes   
  <chr>           <chr>   
1 Alex Petit      10,15,16
2 Bertrand Dupont 18,12,14
3 Corinne Durand  16,17   

Appliquons tidyr::separate_rows().

df |> 
  separate_rows(notes) |> 
  rename(note = notes)
# A tibble: 8 × 2
  eleve           note 
  <chr>           <chr>
1 Alex Petit      10   
2 Alex Petit      15   
3 Alex Petit      16   
4 Bertrand Dupont 18   
5 Bertrand Dupont 12   
6 Bertrand Dupont 14   
7 Corinne Durand  16   
8 Corinne Durand  17   

Par défaut tidyr::separate_rows() sépare les valeurs dès qu’elle trouve un caractère qui ne soit ni un chiffre ni une lettre, mais on peut spécifier le séparateur à l’aide de l’argument sep (qui accepte une chaîne de caractère ou même une expression régulière) :

df |> 
  separate_rows(notes, sep = ",") |> 
  rename(note = notes)
# A tibble: 8 × 2
  eleve           note 
  <chr>           <chr>
1 Alex Petit      10   
2 Alex Petit      15   
3 Alex Petit      16   
4 Bertrand Dupont 18   
5 Bertrand Dupont 12   
6 Bertrand Dupont 14   
7 Corinne Durand  16   
8 Corinne Durand  17   

36.7 unite() : regrouper plusieurs colonnes en une seule

tidyr::unite() est l’opération inverse de tidyr::separate(). Elle permet de regrouper plusieurs colonnes en une seule. Imaginons qu’on obtient le tableau d suivant :

code_departement code_commune commune pop_tot
01 004 Ambérieu-en-Bugey 14233
01 007 Ambronay 2437
01 014 Arbent 3440
01 024 Attignat 3110
01 025 Bâgé-la-Ville 3130
01 027 Balan 2785

On souhaite reconstruire une colonne code_insee qui indique le code INSEE de la commune, et qui s’obtient en concaténant le code du département et celui de la commune. On peut utiliser tidyr::unite() pour cela on indique d’abord le nom de la nouvelle variable puis la liste des variables à concaténer :

d |>
  unite(code_insee, code_departement, code_commune)
# A tibble: 6 × 3
  code_insee commune           pop_tot
  <chr>      <chr>               <int>
1 01_004     Ambérieu-en-Bugey   14233
2 01_007     Ambronay             2437
3 01_014     Arbent               3440
4 01_024     Attignat             3110
5 01_025     Bâgé-la-Ville        3130
6 01_027     Balan                2785

Le résultat n’est pas idéal : par défaut tidyr::unite() ajoute un caractère _ entre les deux valeurs concaténées, alors qu’on ne veut aucun séparateur. De plus, on souhaite conserver nos deux colonnes d’origine, qui peuvent nous être utiles. On peut résoudre ces deux problèmes à l’aide des arguments sep et remove :

d |> 
  unite(
    code_insee,
    code_departement,
    code_commune, 
    sep = "",
    remove = FALSE
  )
# A tibble: 6 × 5
  code_insee code_departement code_commune commune           pop_tot
  <chr>      <chr>            <chr>        <chr>               <int>
1 01004      01               004          Ambérieu-en-Bugey   14233
2 01007      01               007          Ambronay             2437
3 01014      01               014          Arbent               3440
4 01024      01               024          Attignat             3110
5 01025      01               025          Bâgé-la-Ville        3130
6 01027      01               027          Balan                2785

36.8 extract() : créer de nouvelles colonnes à partir d’une colonne de texte

tidyr::extract() permet de créer de nouvelles colonnes à partir de sous-chaînes d’une colonne de texte existante, identifiées par des groupes dans une expression régulière.

Par exemple, à partir du tableau suivant :

eleve note
Alex Petit 5/20
Bertrand Dupont 6/10
Corinne Durand 87/100

On peut extraire les noms et prénoms dans deux nouvelles colonnes avec :

df |>
  extract(
    eleve,
    c("prenom", "nom"),
    "^(.*) (.*)$"
  )
# A tibble: 3 × 3
  prenom   nom    note  
  <chr>    <chr>  <chr> 
1 Alex     Petit  5/20  
2 Bertrand Dupont 6/10  
3 Corinne  Durand 87/100

On passe donc à tidyr::extract() trois arguments :

  • la colonne d’où on doit extraire les valeurs,
  • un vecteur avec les noms des nouvelles colonnes à créer,
  • et une expression régulière comportant autant de groupes (identifiés par des parenthèses) que de nouvelles colonnes.

Par défaut la colonne d’origine n’est pas conservée dans la table résultat. On peut modifier ce comportement avec l’argument remove = FALSE. Ainsi, le code suivant extrait les initiales du prénom et du nom mais conserve la colonne d’origine :

df |>
  tidyr::extract(
    eleve, 
    c("initiale_prenom", "initiale_nom"), 
    "^(.).* (.).*$", 
    remove = FALSE
  )
# A tibble: 3 × 4
  eleve           initiale_prenom initiale_nom note  
  <chr>           <chr>           <chr>        <chr> 
1 Alex Petit      A               P            5/20  
2 Bertrand Dupont B               D            6/10  
3 Corinne Durand  C               D            87/100

36.9 complete() : compléter des combinaisons de variables manquantes

Imaginons qu’on ait le tableau de résultats suivants :

eleve matiere note
Alain Maths 16
Alain Français 9
Barnabé Maths 17
Chantal Français 11

Les élèves Barnabé et Chantal n’ont pas de notes dans toutes les matières. Supposons que c’est parce qu’ils étaient absents et que leur note est en fait un 0. Si on veut calculer les moyennes des élèves, on doit compléter ces notes manquantes.

La fonction tidyr::complete() est prévue pour ce cas de figure : elle permet de compléter des combinaisons manquantes de valeurs de plusieurs colonnes.

On peut l’utiliser de cette manière :

df |>
  complete(eleve, matiere)
# A tibble: 6 × 3
  eleve   matiere   note
  <chr>   <chr>    <dbl>
1 Alain   Français     9
2 Alain   Maths       16
3 Barnabé Français    NA
4 Barnabé Maths       17
5 Chantal Français    11
6 Chantal Maths       NA

On voit que les combinaisons manquante “Barnabé - Français” et “Chantal - Maths” ont bien été ajoutées par tidyr::complete().

Par défaut les lignes insérées récupèrent 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 :

df |>
  complete(
    eleve,
    matiere,
    fill = list(note = 0)
  )
# A tibble: 6 × 3
  eleve   matiere   note
  <chr>   <chr>    <dbl>
1 Alain   Français     9
2 Alain   Maths       16
3 Barnabé Français     0
4 Barnabé Maths       17
5 Chantal Français    11
6 Chantal Maths        0

Parfois on ne souhaite pas inclure toutes les colonnes dans le calcul des combinaisons de valeurs. Par exemple, supposons qu’on rajoute dans notre tableau une colonne avec les identifiants de chaque élève :

id eleve matiere note
1001001 Alain Maths 16
1001001 Alain Français 9
1001002 Barnabé Maths 17
1001003 Chantal Français 11

Si on applique tidyr::complete() comme précédemment, le résultat n’est pas bon car il génère des valeurs manquantes pour id.

df |>
  complete(eleve, matiere)
# A tibble: 6 × 4
  eleve   matiere       id  note
  <chr>   <chr>      <dbl> <dbl>
1 Alain   Français 1001001     9
2 Alain   Maths    1001001    16
3 Barnabé Français      NA    NA
4 Barnabé Maths    1001002    17
5 Chantal Français 1001003    11
6 Chantal Maths         NA    NA

Et si nous ajoutons id dans l’appel de la fonction, nous obtenons toutes les combinaisons de id, eleve et matiere.

df |>
  complete(id, eleve, matiere)
# A tibble: 18 × 4
        id eleve   matiere   note
     <dbl> <chr>   <chr>    <dbl>
 1 1001001 Alain   Français     9
 2 1001001 Alain   Maths       16
 3 1001001 Barnabé Français    NA
 4 1001001 Barnabé Maths       NA
 5 1001001 Chantal Français    NA
 6 1001001 Chantal Maths       NA
 7 1001002 Alain   Français    NA
 8 1001002 Alain   Maths       NA
 9 1001002 Barnabé Français    NA
10 1001002 Barnabé Maths       17
11 1001002 Chantal Français    NA
12 1001002 Chantal Maths       NA
13 1001003 Alain   Français    NA
14 1001003 Alain   Maths       NA
15 1001003 Barnabé Français    NA
16 1001003 Barnabé Maths       NA
17 1001003 Chantal Français    11
18 1001003 Chantal Maths       NA

Dans ce cas, pour signifier à tidyr::complete() que id et eleve sont deux attributs d’un même individu et ne doivent pas être combinés entre eux, on doit les placer dans une fonction tidyr::nesting() :

df |>
  complete(
    nesting(id, eleve),
    matiere
  )
# A tibble: 6 × 4
       id eleve   matiere   note
    <dbl> <chr>   <chr>    <dbl>
1 1001001 Alain   Français     9
2 1001001 Alain   Maths       16
3 1001002 Barnabé Français    NA
4 1001002 Barnabé Maths       17
5 1001003 Chantal Français    11
6 1001003 Chantal Maths       NA

36.10 Ressources

Chaque jeu de données est différent, et le travail de remise en forme est souvent long et plus ou moins compliqué. On n’a donné ici que les exemples les plus simples, et c’est souvent en combinant différentes opérations qu’on finit par obtenir le résultat souhaité.

Le livre R for data science, librement accessible en ligne, contient un chapitre complet sur la remise en forme des données.

L’article Tidy data, publié en 2014 dans le Journal of Statistical Software (doi: 10.18637/jss.v059.i10), présente de manière détaillée le concept éponyme (mais il utilise des extensions désormais obsolètes qui ont depuis été remplacées par dplyr ettidyr).

Le site de l’extension est accessible à l’adresse : http://tidyr.tidyverse.org/ et contient une liste des fonctions et les pages d’aide associées.

En particulier, on pourra se référer à la vignette dédiée à tidyr::pivot_wider() et tidyr::pivot_longer() pour des exemples avancés de réorganisation des données.

Pour des usages avancés, il est possible avec tidyr de gérer des données nichées (nested data), c’est-à-dire des tableaux de données dans des tableaux de données. Ces fonctionnalités, réservées aux utilisateurs avancés, sont décrites dans une vignette spécifique.

36.11 Fichiers volumineux

Si l’on a des tableaux de données particulièrement volumineux (plusieurs Go), les fonctions de tidyr ne sont pas les plus performantes.

On aura alors intérêt à regarder du côté des fonctions data.table::melt() et data.table::dcast() de l’extension data.table développées pour optimiser la performance sur les grands tableaux de données.

Pour plus de détails, voir la vignette dédiée : https://rdatatable.gitlab.io/data.table/articles/datatable-reshape.html

36.12 webin-R

Le package tidyr est évoqué sur YouTube dans le webin-R #13 (exemples de graphiques avancés) et le le webin-R #17 (trajectoires de soins : un exemple de données longitudinales).