Tidy data

Comme indiqué dans l’introduction au tidyverse, 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 :

── Attaching packages ─────────────────────────────── tidyverse 1.2.1 ──
✔ ggplot2 3.2.1     ✔ purrr   0.3.2
✔ tibble  2.1.3     ✔ dplyr   0.8.3
✔ tidyr   0.8.3     ✔ stringr 1.4.0
✔ readr   1.3.1     ✔ forcats 0.4.0
── Conflicts ────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag()    masks stats::lag()
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 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

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

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

# A tibble: 3 x 2
  country  pop_min
  <fct>      <int>
1 Belgium 10045622
2 France  57374179
3 Germany 80597764

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 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.

Les verbes de tidyr

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 (%>%), les pipelines, permettant d’enchaîner les opérations.

gather : rassembler des colonnes

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

country 2002 2007
Belgium 10311970 10392226
France 59925035 61083916
Germany 82350671 82400996
Italy 57926999 58147733
Spain 40152517 40448191
Switzerland 7361757 7554661

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 gather (“rassembler”) :

# A tibble: 12 x 3
   country     annee population
   <fct>       <chr>      <int>
 1 Belgium     2002    10311970
 2 France      2002    59925035
 3 Germany     2002    82350671
 4 Italy       2002    57926999
 5 Spain       2002    40152517
 6 Switzerland 2002     7361757
 7 Belgium     2007    10392226
 8 France      2007    61083916
 9 Germany     2007    82400996
10 Italy       2007    58147733
11 Spain       2007    40448191
12 Switzerland 2007     7554661

La fonction gather prend comme arguments la liste des colonnes à rassembler (ici on a mis 2002 et 2007 entre backticks (`2002`) pour indiquer à gather qu’il s’agit d’un nom de colonne et pas d’un nombre), ainsi que deux arguments key et value :

  • key est le nom de la colonne qui va contenir les “clés”, c’est-à-dire les identifiants des différentes observations
  • value est le nom de la colonne qui va contenir la valeur des observations

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

# A tibble: 12 x 3
   country     annee population
   <fct>       <chr>      <int>
 1 Belgium     2002    10311970
 2 France      2002    59925035
 3 Germany     2002    82350671
 4 Italy       2002    57926999
 5 Spain       2002    40152517
 6 Switzerland 2002     7361757
 7 Belgium     2007    10392226
 8 France      2007    61083916
 9 Germany     2007    82400996
10 Italy       2007    58147733
11 Spain       2007    40448191
12 Switzerland 2007     7554661

spread : disperser des lignes

La fonction spread est l’inverse de gather.

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 spread pour disperser ces lignes dans deux colonnes différentes :

# A tibble: 6 x 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

spread prend deux arguments principaux :

  • key indique la colonne contenant les noms des nouvelles variables à créer
  • value 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 fill permet de spécifier la valeur à utiliser pour ces données manquantes (par défaut fill vaut, logiquement, 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
# A tibble: 6 x 6
  country continent  year density lifeExp      pop
  <chr>   <chr>     <dbl>   <dbl>   <dbl>    <dbl>
1 Belgium Europe     2002      NA    78.3 10311970
2 Belgium Europe     2007      NA    79.4 10392226
3 France  Europe     2002      94    79.6 59925035
4 France  Europe     2007      NA    80.7 61083916
5 Germany Europe     2002      NA    78.7 82350671
6 Germany Europe     2007      NA    79.4 82400996
# A tibble: 6 x 6
  country continent  year density lifeExp pop     
  <chr>   <chr>     <dbl> <chr>   <chr>   <chr>   
1 Belgium Europe     2002 -       78.32   10311970
2 Belgium Europe     2007 -       79.441  10392226
3 France  Europe     2002 94      79.59   59925035
4 France  Europe     2007 -       80.657  61083916
5 Germany Europe     2002 -       78.67   82350671
6 Germany Europe     2007 -       79.406  82400996

separate : séparer une colonne en plusieurs

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 :

eleve note
Félicien Machin 5/20
Raymonde Bidule 6/10
Martial Truc 87/100

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

# A tibble: 3 x 3
  eleve           note  note_sur
  <chr>           <chr> <chr>   
1 Félicien Machin 5     20      
2 Raymonde Bidule 6     10      
3 Martial Truc    87    100     

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 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 :

# A tibble: 3 x 3
  prenom   nom    note  
  <chr>    <chr>  <chr> 
1 Félicien Machin 5/20  
2 Raymonde Bidule 6/10  
3 Martial  Truc   87/100

unite : regrouper plusieurs colonnes en une seule

unite est l’opération inverse de 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 unite pour cela :

# A tibble: 6 x 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 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 :

# A tibble: 6 x 5
  code_insee code_departement code_commune commune   pop_tot
  <chr>      <chr>            <chr>        <chr>       <int>
1 01004      01               004          Ambérieu…   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-…    3130
6 01027      01               027          Balan        2785

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

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
Félicien Machin 5/20
Raymonde Bidule 6/10
Martial Truc 87/100

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

On passe donc à 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 :

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 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 :

On voit que les combinaisons manquante “Barnabé - Français” et “Chantal - Maths” ont bien été ajoutées par 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 :

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 complete comme précédemment, le résultat n’est pas bon car il contient toutes les combinaisons de id, eleve et matiere.

Dans ce cas, pour signifier à 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 nesting :

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, 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.