country | 1992 | 1997 | 2002 | 2007 |
---|---|---|---|---|
Belgium | 10045622 | 10199787 | 10311970 | 10392226 |
France | 57374179 | 58623428 | 59925035 | 61083916 |
Germany | 80597764 | 82011073 | 82350671 | 82400996 |
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 :
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()
:
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 :
- chaque ligne correspond à une observation
- chaque colonne correspond à une variable
- 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()
:
# 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 :
# 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 :
# 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 |
# 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
# 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
:
# 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
:
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()
.
# 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) :
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 :
# 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
:
# 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 :
# 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 :
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 :
# 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 :
# 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.
# 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
.
# 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()
:
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).