5.4 Remaniement des données

Dans le module 4, vous avez réalisé vos premiers remaniements de données dans le cadre des graphiques en barres. Nous ne nous sommes pas étendu sur les fonctions utilisées à cette occasion. Le remaniement des données est une étape cruciale en analyse des données et il faut en maîtriser au moins les principaux outils. Heureusement, il est déjà possible d’aller loin en combinant une petite dizaine d’outils simples. Les cinq principaux (les plus utilisés) dans l’approche Tidyverse utilisée ici sont :

  • sélectionner des colonnes au sein d’un jeu de données avec select()

  • filtrer des lignes dans un jeu de données avec filter()

  • calculer de nouvelles variables dans un jeu de données avec mutate()

  • regrouper les données au sein d’un tableau avec group_by()

  • résumer les variables d’un jeu de données avec summarise()

Ces outils provenant du package {dplyr} sont décrits en détails dans le chapitre 5 de “R for Data Science”. Nous allons nous familiariser avec eux via une approche pratique sur base d’exemples concrets.

urchin <- read("urchin_bio", package = "data.io", lang = "fr")
rmarkdown::paged_table(urchin)

5.4.1 select()

Lors de l’utilisation de vos jeux de données, vous serez amené à réduire vos données en sous-tableau ne reprenant qu’un sous-ensemble des variables initiales. select() effectue cette opération20 :

urchin1 <- select(urchin, origin, solid_parts, test)
rmarkdown::paged_table(urchin1)
urchin2 <- select(urchin, c(1, 4, 14))
rmarkdown::paged_table(urchin2)
urchin3 <-  select(urchin, origin, contains("weight"))
rmarkdown::paged_table(urchin3)
urchin4 <- select(urchin, ends_with("ht"))
rmarkdown::paged_table(urchin4)

5.4.2 filter()

De même que toutes les colonnes d’un tableau ne sont pas forcément utiles, il est souvent nécessaire de sélectionner les lignes en fonction de critères particuliers pour restreindre l’analyse à une sous-population données, ou pour éliminer les cas qui ne correspondent pas à ce que vous voulez. La fonction filter() effectue ce travail. Repartons du jeu de données urchin_bio simplifié à trois variables (urchin2).

rmarkdown::paged_table(urchin2)

Si vous voulez sélectionner uniquement un niveau lvl d’une variable facteur fact, vous pouvez utiliser un test de condition “égal à” (==) : fact == "lvl". Notez bien le double signe égal ici, et n’oubliez pas d’indiquer le niveau entre guillemets. De même, vous pouvez sélectionner tout sauf ce niveau avec l’opérateur “différent de” (!=). Les opérateur “plus petit que” (<) ou “plus grand que” (>) fonctionnent sur les chaines de caractère selon une logique d’ordre alphabétique, donc, "a" < "b"21.

Comparaison Opérateur Exemple
Égal à == fact == "lvl"
Différent de != fact != "lvl"
Plus grand que > fact > "lvl"
Plus grand ou égal à >= fact >= "lvl"
Plus petit que < fact < "lvl"
Plus petit ou égale à <= fact <= "lvl"
# Tous les oursins sauf ceux issus de la pêche
urchin_sub1 <- filter(urchin2, origin != "Fishery")
rmarkdown::paged_table(urchin_sub1)

Vous pouvez aussi utiliser une variable numérique pour filtrer les données. Les comparaisons précédentes sont toujours applicables, sauf que cette fois vous faites porter la comparaison par rapport à une constante (ou par rapport à une autre variable numérique).

# Oursins plus hauts que 20mm
urchin_sub2 <- filter(urchin2, height > 20)
rmarkdown::paged_table(urchin_sub2)

Vous pouvez combiner différentes comparaisons avec les opérateurs “et” (&) et “ou” (|) :

# Oursins plus hauts que 20 mm ET issus d'élevage ("Farm")
urchin_sub3 <- filter(urchin2, height > 20 & origin == "Farm") 
rmarkdown::paged_table(urchin_sub3)

Avec des variables facteurs composées de nombreux niveaux comme on peut en retrouver dans le jeu de données zooplankton du package {data.io}, vous pouvez être amené à sélectionner plusieurs niveaux au sein de cette variable. L’opérateur %in% permet d’indiquer que nous souhaitons garder tous les niveaux qui sont dans une liste. Il n’existe pas d’opérateur %not_in%, mais il suffit d’inverser le résultat en précédent l’instruction de ! pour obtenir cet effet. Par exemple, !letters %in% c("a", "d", "f") conserve toutes les lettres sauf a, d et f. L’opérateur ! est d’ailleurs utilisable avec toutes les comparaisons pour en inverser les effets. Ainsi, !x == 1 est équivalent à x != 1.

zooplankton <- read("zooplankton", package = "data.io", lang  = "FR")
# Garde uniquement les copépodes (correspondant à 4 groupes distincts)
copepoda <- filter(zooplankton,
  class %in% c("Calanoïde", "Cyclopoïde",  "Harpacticoïde", "Poecilostomatoïde"))
rmarkdown::paged_table(select(copepoda, ecd:perimeter, class))

Enfin, la détection et l’élimination de lignes contenant des valeurs manquantes (encodées comme NA) est spéciale. En effet, vous ne pouvez pas écrire quelque chose comme x == NA car ceci se lit comme “x est égale à … je ne sais pas quoi”, ce qui renvoie à son tour NA pour toutes les comparaisons quelles qu’elles soient. Vous pouvez utiliser la fonction spécialement prévue pour ce test is.na(). Ainsi, is.na(x) effectue en réalité ce que vous voulez et peut être utilisée à l’intérieur de filter(). Cependant, il existe une fonction spécialement prévue pour débarrasser les tableaux des lignes contenant des valeurs manquantes : drop_na() du package {tidyr}. Si vous spécifier des noms de colonnes (facultatifs), la fonction ira rechercher les valeurs manquantes uniquement dans ces colonnes-là, sinon, elle scrutera tout le tableau.

urchin_sub4 <- drop_na(urchin) 
rmarkdown::paged_table(urchin_sub4)
À vous de jouer !
h5p

5.4.3 mutate()

La fonction mutate() permet de calculer de nouvelles variables (si le nom fourni n’existe pas encore dans le jeu de donnée) ou écrase les variables existantes de même nom. Repartons du jeu de données urchin. Pour calculer de nouvelles variables, vous pouvez employer :

  • les opérateurs arithmétiques :
    • addition : +
    • soustraction : -
    • multiplication : *
    • division : /
    • exposant : ^
    • modulo (reste lors d’une division entière) : %%
    • division entière : %/%
urchin <- mutate(urchin, 
  sum_skel  =  lantern + spines + test, 
  ratio     = sum_skel / skeleton,
  skeleton2 = skeleton^2)
rmarkdown::paged_table(select(urchin, skeleton:spines, sum_skel:skeleton2))
  • les fonctions mathématiques :
    • ln() ou log() (logarithme népérien), lg() ou log10() (logarithme en base 10)
    • ln1p() ou log1p() (logarithme népérien de x + 1), ou lg1p() (logarithme en base 10 de x + 1)
    • exp() (exponentielle, ex) et expm1() (ex - 1)
    • sqrt() (racine carrée)
    • sin(), cos(), tan()
urchin <- mutate(urchin,
  skeleton_log  = log(skeleton), 
  skeleton_sqrt = sqrt(skeleton))
rmarkdown::paged_table(select(urchin, skeleton, skeleton_log, skeleton_sqrt))

La fonction transmute() effectue la même opération, mais en plus, elle laisse tomber les variables d’origine pour ne garder que les nouvelles variables calculées.

À vous de jouer !
h5p

5.4.4 group_by()

La fonction group_by() ne change rien dans le tableau lui-même, mais ajoute une annotation qui indique que les calculs ultérieurs devront être effectués sur des sous-ensembles du tableau en parallèle. Ceci est surtout utile avec summarise() (voir ci-dessous). Pour annuler le regroupement, il suffit d’utiliser ungroup().

urchin_by_orig <- group_by(urchin, origin)
head(urchin_by_orig)
# # A tibble: 6 x 24
# # Groups:   origin [1]
#   origin  diameter1 diameter2 height buoyant_weight weight solid_parts
#   <fct>       <dbl>     <dbl>  <dbl>          <dbl>  <dbl>       <dbl>
# 1 Fishery       9.9      10.2    5               NA  0.522       0.478
# 2 Fishery      10.5      10.6    5.7             NA  0.642       0.589
# 3 Fishery      10.8      10.8    5.2             NA  0.734       0.677
# 4 Fishery       9.6       9.3    4.6             NA  0.370       0.344
# 5 Fishery      10.4      10.7    4.8             NA  0.610       0.559
# 6 Fishery      10.5      11.1    5               NA  0.610       0.551
# # … with 17 more variables: integuments <dbl>, dry_integuments <dbl>,
# #   digestive_tract <dbl>, dry_digestive_tract <dbl>, gonads <dbl>,
# #   dry_gonads <dbl>, skeleton <dbl>, lantern <dbl>, test <dbl>, spines <dbl>,
# #   maturity <int>, sex <fct>, sum_skel <dbl>, ratio <dbl>, skeleton2 <dbl>,
# #   skeleton_log <dbl>, skeleton_sqrt <dbl>
identical(ungroup(urchin_by_orig), urchin)
# [1] FALSE

5.4.5 summarise()

Si vous voulez résumer vos données (calcul de la moyenne, médiane, etc.), vous pouvez réaliser ceci sur une variable en particulier avec les fonctions dédiées. Par exemple mean(urchin$skeleton) renvoie la masse moyenne de squelette pour tous les oursins (ce calcul donne NA dès qu’il y a des valeurs manquantes, mais l’argument na.rm = TRUE permet d’obtenir un résultat en ne tenant pas compte de ces données manquantes : mean(urchin$skeleton, na.rm = TRUE)). Cela devient vite laborieux s’il faut réitérer ce genre de calcul sur plusieurs variables du jeu de données, et assembler ensuite les résultats dans un petit tableau synthétique. D’autant plus, s’il faut séparer d’abord le jeu de données en sous-groupes pour faire ces calculs. La fonction summarise() reporte automatiquement ces calculs, en tenant compte automatiquement des regroupements proposés via group_by().

tooth <- read("ToothGrowth", package = "datasets", lang = "fr")
tooth_summary <- summarise(tooth,
  "moyenne" = mean(len), 
  "minimum" = min(len), 
  "médiane" = median(len), 
  "maximum" = max(len))
knitr::kable(tooth_summary, digits = 2,
  caption = "Allongement des dents chez des cochons d'Inde recevant de l'acide ascorbique.")
Tableau 5.6: Allongement des dents chez des cochons d’Inde recevant de l’acide ascorbique.
moyenne minimum médiane maximum
18.81 4.2 19.25 33.9

Voici les mêmes calculs, mais effectués séparément pour les deux types de supplémentations alimentaires :

tooth_by_supp <- group_by(tooth, supp)
tooth_summary2 <- summarise(tooth_by_supp,
  "moyenne" = mean(len), 
  "minimum" = min(len), 
  "médiane" = median(len), 
  "maximum" = max(len))
knitr::kable(tooth_summary2, digits = 2,
  caption = "Allongement des dents chez des cochons d'Inde en fonction du supplément jus d'orange (OJ) ou vitamine C (VC).")
Tableau 5.7: Allongement des dents chez des cochons d’Inde en fonction du supplément jus d’orange (OJ) ou vitamine C (VC).
supp moyenne minimum médiane maximum
OJ 20.66 8.2 22.7 30.9
VC 16.96 4.2 16.5 33.9
Pièges et astuces
  • Tout comme lors de réalisation d’une boite de dispersion, vous devez être particulièrement vigilant au nombre d’observation par sous-groupe. Pensez toujours à ajoutez à chaque tableau de résumé des données, le nombre d’observation par sous-groupe grâce à la fonction n().
tooth_summary2 <- summarise(tooth_by_supp,
  "moyenne" = mean(len), 
  "minimum" = min(len), 
  "médiane" = median(len), 
  "maximum" = max(len),
  "n"       = n())
knitr::kable(tooth_summary2, digits = 2,
  caption = "Allongement des dents chez des cochons d'Inde en fonction du supplément jus d'orange (OJ) ou vitamine C (VC).")
Tableau 5.8: Allongement des dents chez des cochons d’Inde en fonction du supplément jus d’orange (OJ) ou vitamine C (VC).
supp moyenne minimum médiane maximum n
OJ 20.66 8.2 22.7 30.9 30
VC 16.96 4.2 16.5 33.9 30
  • summarise() calcule ses variables dans le même environnement que le tableau de départ. Donc, si vous utiliser des noms de colonnes qui existent déjà, elles seront écrasées par le résultat du calcul. Voici un exemple concret :
tooth_summary3 <- summarise(tooth,
  "len" = mean(len), # Notez le même nom à gauche et à droite du = (len)
  "len_sd" = sd(len))
knitr::kable(tooth_summary3, digits = 2,
  caption = "Exemple de résumé des données erroné à cause de l'écrasement de la variable `len`.")
Tableau 5.9: Exemple de résumé des données erroné à cause de l’écrasement de la variable len.
len len_sd
18.81 NA

L’écart type est… NA ??? Pour comprendre ce qui s’est passé, il faut lire la transformation réalisée par summarise() ligne après ligne :

  • la moyenne de la variable len est placée dans … len. Donc ici, nous écrasons la variable initiale de 60 observations par un nombre unique : la moyenne,
  • l’écart type de len est ensuite calculé. Attendez une minute, de quel lens’agit-il ici ? Et bien la dernière en date, soit celle qui contient une seule valeur, la moyenne. Or, sd() nécessite au moins deux valeurs pour que l’écart type puisse être calculé, sinon, NA est renvoyé. C’est encore heureux ici, car nous aurions pu faire un calcul qui renvoie un résultat, … mais qui n’est pas celui qu’on croit !

Conclusion : ne nommez jamais vos variables créées avec summarise() exactement comme les variables de votre tableau en entrée.

Pour en savoir plus
  • Le chapitre consacré à la transformation des données de R for Data Science présente le remaniement d’un tableau de données différemment et propose des exemples et exercices complémentaires très utiles.

  • La meilleure façon de se familiariser avec les “verbes” du tidyverse est de réaliser des transformations de données par soi-même. En cas de blocage, le site https://stackoverflow.com permet de chercher des solutions. Pour une recherche ciblée sur le langage R, précédez vos mots clés par “[R]” (R entre crochets). Par exemple, pour explorer diverses utilisations de la fonction mutate(), vous entrerez le texte de recherche suivant: “[R] mutate”.

  • N’oubliez pas les aide-mémoires de {dplyr} et de {tidyr} qui forment aussi une source d’inspiration utile pour vous guider vers les fonction (les “verbes”) adéquats. Ensuite, allez voir l’aide en ligne de la fonction avec ?ma_fonction.