Manipuler ses données avec tidyr ou comment éviter la surchauffe cérébrale

tidyr pivot wider pivot longer
Tags : Actualités, Ressources
Date :

Tourner, retourner, étirer, pivoter, transposer … Mais quel casse-tête ces données ! Le nom d’une partie de mes colonnes constitue en fait les valeurs d’une certaine variable, cette autre colonne représente en réalité plusieurs variables, et puis chaque ligne contient l’information de deux individus statistiques distincts, et pffff ! J’ai comme envie de retourner sous la couette. Mais je viens de relire la tip top cheat sheet sur l’import des données avec {tidyr} (que vous pouvez retrouver ici : https://readr.tidyverse.org/) qui me redonne de la motivation. Allez au boulot ! Voyons quels genres de puzzles nous attendent !

Note : A l’heure où j’écris cet article, la cheat sheet n’est pas encore mise à jour. Les fonctions qui nous intéressent ici sont gather() et spread() : elles n’ont plus vocation à être utilisées et ont été remplacées par les fonctions pivot_longer() et pivot_wider(), beaucoup plus intuitives, s’inspirant des fonctions melt(), dcast()/ cast() des packages {data.table} et {reshape2}.

Installation et chargement du package :

install.packages("tidyr")
library(tidyr)

La notion d’individu statistique

Avant toute chose, il est ici primordial de déterminer en amont nos individus statistiques, c’est-à-dire les observations pour lesquelles on a des mesures qui seront nos variables.

Allonger sa table avec pivot_longer()


Comme son nom l’indique, la fonction pivot_longer() permet d’allonger ses données en augmentant le nombre de lignes et en diminuant le nombre de colonnes. Ci-dessous, un certain nombre d’exemple de cas que nous pouvons rencontrer.

Des données dans les noms de colonnes

Prenons cet exemple, tiré de la vignette de {data.table} (https://cran.r-project.org/web/packages/data.table/vignettes/datatable-reshape.html) :

family <- data.frame(
  family_id = 1:5, 
  age_mother = c(30, 27, 26, 32, 29), 
  birth_child1 = c("1998-11-26", "1996-06-22", "2002-07-11", "2004-10-10", "2000-12-05"),
  birth_child2 = c("2000-01-29", NA, "2004-04-05", "2009-08-27", "2005-02-28"),
  birth_child3 = c(NA, NA, "2007-09-02", "2012-07-21", NA)
)
family
##   family_id age_mother birth_child1 birth_child2 birth_child3
## 1         1         30   1998-11-26   2000-01-29         <NA>
## 2         2         27   1996-06-22         <NA>         <NA>
## 3         3         26   2002-07-11   2004-04-05   2007-09-02
## 4         4         32   2004-10-10   2009-08-27   2012-07-21
## 5         5         29   2000-12-05   2005-02-28         <NA>
family_long <- family %>%
  pivot_longer(
    cols = paste0("birth_child", 1:3), names_to = "child_number",
    names_prefix = "birth_child", values_to = "date_of_birth",
    values_drop_na = TRUE) 
family_long
## # A tibble: 11 x 4
##    family_id age_mother child_number date_of_birth
##        <int>      <dbl> <chr>        <fct>        
##  1         1         30 1            1998-11-26   
##  2         1         30 2            2000-01-29   
##  3         2         27 1            1996-06-22   
##  4         3         26 1            2002-07-11   
##  5         3         26 2            2004-04-05   
##  6         3         26 3            2007-09-02   
##  7         4         32 1            2004-10-10   
##  8         4         32 2            2009-08-27   
##  9         4         32 3            2012-07-21   
## 10         5         29 1            2000-12-05   
## 11         5         29 2            2005-02-28

Le paramètre cols permet de spécifier les colonnes concernées, c’est-à-dire celles qui contiennent des données. Les paramètres names_to et value_to indiquent les noms des nouvelles colonnes. Le paramètre names_prefix permet de supprimer un préfixe potentiellement présent et inutile dans nos valeurs. Enfin, le paramètre values_drop_na mis à TRUE supprime les lignes pour lesquelles la valeur vaut NA.

Plusieurs variables dans les noms de colonnes

Considérons les données iris :

head(iris)
##   Sepal.Length Sepal.Width Petal.Length Petal.Width Species
## 1          5.1         3.5          1.4         0.2  setosa
## 2          4.9         3.0          1.4         0.2  setosa
## 3          4.7         3.2          1.3         0.2  setosa
## 4          4.6         3.1          1.5         0.2  setosa
## 5          5.0         3.6          1.4         0.2  setosa
## 6          5.4         3.9          1.7         0.4  setosa

On a ici en réalité 4 variables : Species, Type qui prendra les valeurs Sepal et Petal, Measure qui prendra les valeurs Length et Width et Value la valeur mesurée. Notre observation devient la longueur (en cm) d’une pièce florale (le sépale ou le pétale) d’une variété d’iris donnée.

On peut alors transformer les données de la manière qui suit :

iris_long <- iris %>%
  pivot_longer(
    cols = -Species, names_to = c("Type", "Measure"),
    names_sep = "[^[:alnum:]]+", values_to = "Value")
iris_long
## # A tibble: 600 x 4
##    Species Type  Measure Value
##    <fct>   <chr> <chr>   <dbl>
##  1 setosa  Sepal Length    5.1
##  2 setosa  Sepal Width     3.5
##  3 setosa  Petal Length    1.4
##  4 setosa  Petal Width     0.2
##  5 setosa  Sepal Length    4.9
##  6 setosa  Sepal Width     3  
##  7 setosa  Petal Length    1.4
##  8 setosa  Petal Width     0.2
##  9 setosa  Sepal Length    4.7
## 10 setosa  Sepal Width     3.2
## # … with 590 more rows

On crée 2 variables, le paramètre names_to prend donc comme valeur un vecteur contenant le nom de chacune des deux variables. Il est aussi nécessaire de spécifier le pattern du nom des variables au sein du nom des colonnes, ou encore comme ici ce qui sépare nos deux noms de variables, grâce au paramètre names_sep.

Pour que ce jeu de données soit sans implicites, car c’est bien l’objectif des pivots que d’expliciter et de révéler des variables, il faudrait expliciter l’unité de mesure de nos observations : comment procéderiez-vous ? 😉 Indice : la réponse se trouve sûrement dans notre article « Utiliser la grammaire {dplyr} pour triturer ses données ».

Plusieurs observations par ligne

La difficulté du puzzle augmente d’un cran ! On reconnaît ce type de problématique lorsque l’on observe que les noms de variables se retrouvent dans plusieurs noms de colonnes. Reprenons le premier exemple, en ajoutant une information supplémentaire :

family2 <- data.frame(
    family_id = 1:5, age_mother = c(30, 27, 26, 32, 29), 
    birth_child1 = c("1998-11-26", "1996-06-22", "2002-07-11", "2004-10-10", "2000-12-05"),
    birth_child2 = c("2000-01-29", NA, "2004-04-05", "2009-08-27", "2005-02-28"),
    gender_child1 = c("M", "M", "F", "M", "F"),
    gender_child2 = c("F", NA, "M", "F", "F"))
family2
##   family_id age_mother birth_child1 birth_child2 gender_child1 gender_child2
## 1         1         30   1998-11-26   2000-01-29             M             F
## 2         2         27   1996-06-22         <NA>             M          <NA>
## 3         3         26   2002-07-11   2004-04-05             F             M
## 4         4         32   2004-10-10   2009-08-27             M             F
## 5         5         29   2000-12-05   2005-02-28             F             F

Ici, on souhaite donc obtenir les trois variables child_number, birth et gender. Une observation devient l’âge lors de la naissance, la parité ET le sexe d’un enfant pour une mère donnée. Pour ce faire :

family2_long <- family2 %>%
  pivot_longer(
    cols = -c("family_id", "age_mother"),
    names_to = c(".value", "child_number"),
    names_sep = "_", 
    values_drop_na = TRUE)
family2_long
## # A tibble: 9 x 5
##   family_id age_mother child_number birth      gender
##       <int>      <dbl> <chr>        <fct>      <fct> 
## 1         1         30 child1       1998-11-26 M     
## 2         1         30 child2       2000-01-29 F     
## 3         2         27 child1       1996-06-22 M     
## 4         3         26 child1       2002-07-11 F     
## 5         3         26 child2       2004-04-05 M     
## 6         4         32 child1       2004-10-10 M     
## 7         4         32 child2       2009-08-27 F     
## 8         5         29 child1       2000-12-05 F     
## 9         5         29 child2       2005-02-28 F

Ici aussi, on remarque que puisque nous créons plusieurs variables, le paramètre names_to prend donc comme valeur un vecteur, et on spécifie le séparateur de noms de variables grâce au paramètre names_sep. Par ailleurs, .value indique ici que cette partie du nom de la colonne spécifie le nom de la nouvelle variable créée.

Elargir sa table avec pivot_wider()


Bon et maintenant, comment je fais si je suis dans le cas opposé ? Mon tableau initial décrit bien toutes mes observations mais je souhaite rapatrier des modalités dans les colonnes ou agréger ? La fonction pivot_wider() permet d’élargir ses données en augmentant le nombre de colonnes et en diminuant le nombre de lignes. Il faut bien comprendre que chaque fois qu’on élargit un tableau, de l’information (portée par un nom de variable ou un calcul) devient implicite. Ci-dessous, un certain nombre d’exemples de cas que nous pouvons rencontrer.

Les valeurs d’une variable sont en fait des noms de variables

Prenons l’exemple suivant : je souhaite créer une table de contingence des variables Label et Soil du jeu de données wine du package {FactoMineR}. À l’intersection d’une ligne et d’une colonne : une somme.

library(FactoMineR)
data(wine)
head(wine[,c("Label", "Soil")])
##           Label      Soil
## 2EL      Saumur      Env1
## 1CHA     Saumur      Env1
## 1FON Bourgueuil      Env1
## 1VAU     Chinon      Env2
## 1DAM     Saumur Reference
## 2BOU Bourgueuil Reference
# ce que je souhaite obtenir, sous forme de data.frame : 
table(wine$Label, wine$Soil)
##             
##              Reference Env1 Env2 Env4
##   Saumur             3    3    3    2
##   Bourgueuil         3    3    0    0
##   Chinon             1    1    2    0
# ce que j'obtiens : 
df_table <- as.data.frame(table(wine$Label, wine$Soil)) %>%
  setNames(c("Label", "Soil", "Freq"))
df_table
##         Label      Soil Freq
## 1      Saumur Reference    3
## 2  Bourgueuil Reference    3
## 3      Chinon Reference    1
## 4      Saumur      Env1    3
## 5  Bourgueuil      Env1    3
## 6      Chinon      Env1    1
## 7      Saumur      Env2    3
## 8  Bourgueuil      Env2    0
## 9      Chinon      Env2    2
## 10     Saumur      Env4    2
## 11 Bourgueuil      Env4    0
## 12     Chinon      Env4    0
# mise en forme grâce à pivot_wider() :
df_table_wider <- df_table %>%
  pivot_wider(names_from = Soil, values_from = Freq)
df_table_wider
## # A tibble: 3 x 5
##   Label      Reference  Env1  Env2  Env4
##   <fct>          <int> <int> <int> <int>
## 1 Saumur             3     3     3     2
## 2 Bourgueuil         3     3     0     0
## 3 Chinon             1     1     2     0

Bon, vous me direz que j’aurais très bien pu faire ça à la place : as.data.frame.matrix(table(wine$Label, wine$Soil)), mais c’était pour illustrer mon propos et économiser de la frappe de parenthèse alors vous ne m’en voudrez pas 😉

Le paramètre names_from indique la variable pour laquelle les valeurs représentent des variables, et values_from la variable contenant les valeurs prises par ces variables.

Agrégation

Continuons avec nos données de vin. Je souhaite maintenant connaître la valeur moyenne de la variable Overall.quality par Label et par Soil. La fonction pivot_wider() nous permet cela :

wine_means <- wine %>% 
  as_tibble() %>% 
  dplyr::select(Label, Soil, Overall.quality) %>%
  pivot_wider(
    names_from = Soil,
    values_from = Overall.quality,
    values_fn = list(Overall.quality = mean))
wine_means
## # A tibble: 3 x 5
##   Label       Env1  Env2 Reference  Env4
##   <fct>      <dbl> <dbl>     <dbl> <dbl>
## 1 Saumur      3.33  3.30      3.75  2.75
## 2 Bourgueuil  3.18 NA         3.70 NA   
## 3 Chinon      3.2   2.96      3.54 NA

Le paramètre values_fn permet d’indiquer quelle fonction appliquer à la variable que l’on souhaite agréger (ici, la moyenne).

Générer des noms de colonne à partir de plusieurs variables

Imaginons le jeu de données suivant : nous avons pour 3 cépages différents et provenant de 2 types d’environnements l’évolution de leur note globale entre 2010 et 2015 :

data_vin <- expand.grid(
    cepage = c("Malbec", "Cabernet", "Syrah"), 
    env = c("env1", "env2"), 
    annee = 2010:2015
  ) %>%
  dplyr::mutate(note = runif(nrow(.), 0, 5))
data_vin
## # A tibble: 36 x 4
##    cepage env   annee  note
##    <chr>  <chr> <int> <dbl>
##  1 Malbec env1   2010 2.96 
##  2 Malbec env1   2011 1.86 
##  3 Malbec env1   2012 0.123
##  4 Malbec env1   2013 4.38 
##  5 Malbec env1   2014 3.96 
##  6 Malbec env1   2015 3.35 
##  7 Malbec env2   2010 4.84 
##  8 Malbec env2   2011 1.65 
##  9 Malbec env2   2012 4.26 
## 10 Malbec env2   2013 2.37 
## # … with 26 more rows

On souhaite maintenant obtenir un jeu de données ayant pour colonne chaque combinaison cépage/environnement. Il suffit d’attribuer au paramètre names_from un vecteur contenant le nom des deux variables :

data_vin %>% 
  pivot_wider(
    names_from = c(cepage, env), 
    values_from = note
  )
## # A tibble: 6 x 7
##   annee Malbec_env1 Malbec_env2 Cabernet_env1 Cabernet_env2 Syrah_env1 Syrah_env2
##   <int>       <dbl>       <dbl>         <dbl>         <dbl>      <dbl>      <dbl>
## 1  2010       2.96         4.84         4.75           1.90      1.78       3.78 
## 2  2011       1.86         1.65         2.61           2.04      1.84       3.52 
## 3  2012       0.123        4.26         4.93           3.40      4.50       0.687
## 4  2013       4.38         2.37         3.50           4.22      3.41       2.75 
## 5  2014       3.96         3.56         0.533          1.26      0.628      0.912
## 6  2015       3.35         2.93         1.93           3.84      3.60       2.78

Pour conclure… et aller plus loin !

On l’a vu, les nouvelles fonctions pivot_longer() et pivot_wider() du package {tidyr} ont été refondues pour faciliter leur utilisation et les noms des paramètres sont plus parlants, intuitifs que leurs ancêtres gather() and spread(). C’est l’esprit du {tidyverse}. Ces fonctions permettent de pivoter un large spectre de jeux de données, mais il est toutefois possible que pour des puzzles plus complexes elles ne soient pas suffisantes. Pour les problèmes de pivot plus élaborés, vous pourrez faire appel aux fonctions build_longer_spec()/pivot_longer_spec() et build_wider_spec()/pivot_wider_spec().

Pour plus d’exemples et plus d’explications sur les fonctions *_spec(), je vous invite à consulter cet article dédié au pivot (en anglais): https://tidyr.tidyverse.org/articles/pivot.html


À propos de l'auteur


Commentaires


À lire également