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 étendus 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()/sselect()

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

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

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

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

Ces outils provenant du package {dplyr} et de {collapse} et {svBase} pour les versions dont le nom commence par un “s” supplémentaire sont décrits en détails dans le chapitre 4 de “R for Data Science (2e)”. Nous allons nous familiariser avec ces outils en adoptant une approche pratique sur base de résolution d’exemples concrets.

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

5.4.1 select()/sselect()

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() et sselect() effectuent cette opération21 :

La fonction select() est une fonction du “tidyverse” (nous l’appellerons désormais une fonction tidy) qui a un comportement particulier dans R. Une fonction relativement équivalente, mais “non-tidy” est sselect(). Le préfixe “s” est là pour indiquer que c’est une fonction “speedy”. Ces fonctions sont similaire à leur équivalents tidy, mais généralement plus rapides. Les fonctions tidy, ont cependant d’autres qualités. Notamment, elles fonctionneront aussi en grande partie sur des bases de données. Ces deux fonctions sont souvent interchangeables, mais pas toujours. Dans SciViews::R, il est très important de faire la distinction entre les deux types de fonctions et leurs particularités.

5.4.1.1 Fonctions “speedy”

Pour lister les fonctions speedy disponibles, vous pouvez faire :

list_speedy_functions()
#  [1] "sadd_count"         "sadd_tally"         "sarrange"          
#  [4] "sbind_cols"         "sbind_rows"         "scount"            
#  [7] "sdistinct"          "sdrop_na"           "sextract"          
# [10] "sfill"              "sfilter"            "sfilter_ungroup"   
# [13] "sfull_join"         "sgroup_by"          "sinner_join"       
# [16] "sleft_join"         "smutate"            "smutate_ungroup"   
# [19] "spivot_longer"      "spivot_wider"       "spull"             
# [22] "srename"            "srename_with"       "sreplace_na"       
# [25] "sright_join"        "sselect"            "sseparate"         
# [28] "sseparate_rows"     "ssummarise"         "stally"            
# [31] "stransmute"         "stransmute_ungroup" "suncount"          
# [34] "sungroup"           "sunite"

Vous pouvez constater que le nom de toutes ces fonctions est préfixé d’un “s”. En fait, elles sont l’équivalent de fonctions du même nom, mais sans le préfixe “s” qui sont définies dans le tidyverse, voir les fonctions “tidy” ci-dessous. Ces fonctions speedy sont rendues disponibles dans SciViews::R afin d’accélérer le calcul sur de gros jeux de données. Vous ne devez pas prendre de précautions particulières pour les utiliser, si ce n’est de vous rappeler de ne pas mélanger les fonctions speedy et tidy dans une même instruction R.

Par exemple, si vous voulez utiliser sselect pour extraire du tableau urchin un sous-tableau qui ne reprend que les variables (= colonnes) origin, height et skeleton, vous utiliserez :

urchin2 <- sselect(urchin, origin, height, skeleton)
head(urchin2)
#     origin height skeleton
# 1: Fishery    5.0   0.1793
# 2: Fishery    5.7   0.1880
# 3: Fishery    5.2   0.2354
# 4: Fishery    4.6   0.0630
# 5: Fishery    4.8       NA
# 6: Fishery    5.0       NA

Vous voyez que vous obtenez ici un data.table qui ne contient plus que trois colonnes.

À noter que la façon de sélectionner des colonnes dans un data.frame en R de base se fait différemment. R de base utilise l’opérateur [ dans lequel vous spécifiez les lignes à conserver d’abord, puis, séparé par une virgule, les colonnes à conserver. Si vous n’indiquez rien devant la virgule vous conservez toutes les lignes, et si vous n’indiquez rien derrière la virgule vous conservez toutes les colonnes. Donc urchin[ , ] conservera le tableau entier. La sélection se fail à l’aide de nombre entries qui indiquent l’index des lignes ou des colonnes à conserver. Si ce sont des nombres négatives, ces lignes ou colonnes sont enlevées. Par exemple pour conserver les colonnes 1 et 3 de urchin, on écrira en R de base :

urchin2 <- urchin[, c(1, 3)]
head(urchin2)
#     origin diameter2
# 1: Fishery      10.2
# 2: Fishery      10.6
# 3: Fishery      10.8
# 4: Fishery       9.3
# 5: Fishery      10.7
# 6: Fishery      11.1

Au contraire, pour conserver tout sauf les colonnes 1 et 3, on écrira :

urchin2 <- urchin[, c(-1,-3)] # ou -c(1, 3)
head(urchin2)
#    diameter1 height buoyant_weight weight solid_parts integuments
# 1:       9.9    5.0             NA 0.5215      0.4777      0.3658
# 2:      10.5    5.7             NA 0.6418      0.5891      0.4447
# 3:      10.8    5.2             NA 0.7336      0.6770      0.5326
# 4:       9.6    4.6             NA 0.3697      0.3438      0.2661
# 5:      10.4    4.8             NA 0.6097      0.5587      0.4058
# 6:      10.5    5.0             NA 0.6096      0.5509      0.4269
#    dry_integuments digestive_tract dry_digestive_tract gonads dry_gonads
# 1:              NA          0.0525              0.0079      0          0
# 2:              NA          0.0482              0.0090      0          0
# 3:              NA          0.0758              0.0134      0          0
# 4:              NA          0.0442              0.0064      0          0
# 5:              NA          0.0743              0.0117      0          0
# 6:              NA          0.0492              0.0097      0          0
#    skeleton lantern   test spines maturity  sex
# 1:   0.1793  0.0211 0.0587 0.0995        0 <NA>
# 2:   0.1880  0.0205 0.0622 0.1053        0 <NA>
# 3:   0.2354  0.0254 0.0836 0.1263        0 <NA>
# 4:   0.0630  0.0167 0.0180 0.0283        0 <NA>
# 5:       NA      NA     NA     NA        0 <NA>
# 6:       NA      NA     NA     NA        0 <NA>

Enfin, pour être complet, vous pouvez aussi utiliser un vecteur de valeurs logiques pour sélectionner les lignes ou les colonnes à conserver.

5.4.1.2 Fonctions “tidy”

Pour lister les fonctions tidy disponibles, vous pouvez faire :

list_tidy_functions()
#  [1] "add_count"         "add_tally"         "arrange"          
#  [4] "bind_cols"         "bind_rows"         "count"            
#  [7] "distinct"          "drop_na"           "extract"          
# [10] "fill"              "filter"            "filter_ungroup"   
# [13] "full_join"         "group_by"          "inner_join"       
# [16] "left_join"         "mutate"            "mutate_ungroup"   
# [19] "pivot_longer"      "pivot_wider"       "pull"             
# [22] "rename"            "rename_with"       "replace_na"       
# [25] "right_join"        "select"            "separate"         
# [28] "separate_rows"     "summarise"         "tally"            
# [31] "transmute"         "transmute_ungroup" "uncount"          
# [34] "ungroup"           "unite"

Ces fonctions ont toutes un comportement commun et sont conçues pour travailler sur un tableau de données. Dans R, un tel tableau de données peut être encodé dans trois types d’objets :

  • le data.frame est l’objet de R de base,
  • le tibble est un data.frame particulier implémenté dans le tidyverse, et
  • le data.table est implémenté dans le package {data.table} et permet de réaliser des remaniements de tableaux plus rapidement que les deux précédents.

Dans SciViews::R, l’objet data.table est utilisé par défaut. Les fonctions speedy sont écrites pour utiliser cet objet de manière native. Il existe une surcouche des fonctions tidy dans le package {dtplyr} qui fonctionne aussi sur des objets de classe data.table.

Les fonctions select() et sselect() permettent aussi de sélectionner des colonnes par leur position dans le tableau :

urchin2 <- sselect(urchin, c(1, 4, 14))
head(urchin2)
#     origin height skeleton
# 1: Fishery    5.0   0.1793
# 2: Fishery    5.7   0.1880
# 3: Fishery    5.2   0.2354
# 4: Fishery    4.6   0.0630
# 5: Fishery    4.8       NA
# 6: Fishery    5.0       NA

Attention : la fonction sselect() n’est pas toujours utilisable pour remplacer la fonction select(). Par exemple, l’instruction contains() est utile pour sélectionner les variables dont le nom contient une chaîne de caractères. Mais cette forme n’est pas comprise par sselect().

urchin3 %<-% select(urchin, origin, contains("weight"))
head(urchin3)
#     origin buoyant_weight weight
# 1: Fishery             NA 0.5215
# 2: Fishery             NA 0.6418
# 3: Fishery             NA 0.7336
# 4: Fishery             NA 0.3697
# 5: Fishery             NA 0.6097
# 6: Fishery             NA 0.6096

Idem pour ends_with() et d’autres opérateurs du genre rassemblés dans la page d’aide ?select_helpers :

urchin4 %<-% select(urchin, ends_with("ht"))
head(urchin4)
#    height buoyant_weight weight
# 1:    5.0             NA 0.5215
# 2:    5.7             NA 0.6418
# 3:    5.2             NA 0.7336
# 4:    4.6             NA 0.3697
# 5:    4.8             NA 0.6097
# 6:    5.0             NA 0.6096

5.4.2 filter()/sfilter()

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. filter() est une fonction tidy qui effectue ce travail. L’équivalent speedy est sfilter(). Repartons du jeu de données urchin simplifié à trois variables (urchin2).

urchin2 <- sselect(urchin, origin, height, skeleton)
head(urchin2)
#     origin height skeleton
# 1: Fishery    5.0   0.1793
# 2: Fishery    5.7   0.1880
# 3: Fishery    5.2   0.2354
# 4: Fishery    4.6   0.0630
# 5: Fishery    4.8       NA
# 6: Fishery    5.0       NA

Si vous voulez sélectionner uniquement un niveau "lvl" d’une variable facteur fact, vous pouvez utiliser une instruction de comparaison “é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érateurs “plus petit que” (<) ou “plus grand que” (>) fonctionnent sur les chaines de caractères selon une logique d’ordre alphabétique, donc, "a" < "b"22.

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"

Bien entendu, les comparaisons sont aussi possibles entre données numériques. Dans ce cas, vous n’utilisez pas de guillemets. Par exemple pour indiquer une condition telle qu’une variable numérique x doit être supérieure à 15, vous indiquerez x > 15 et surtout pas x > "15" dans ce cas.

En version speedy, vous utiliserez sfilter() pour indiquer les lignes de votre jeu de données à conserver, la plupart du temps grâce à un test de condition. Ainsi pour conserver tous les oursins qui ne sont pas issus de la pêche, vous indiquerez :

# Tous les oursins sauf ceux issus de la pêche
urchin_sub1 <- sfilter(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) et comme nous l’avons déjà expliqué, vous ne l’indiquez pas entre guillemets.

# Oursins plus hauts que 20mm
urchin_sub2 <- sfilter(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 <- sfilter(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 <- sfilter(zooplankton,
  class %in% c("Calanoïde", "Cyclopoïde",  "Harpacticoïde", "Poecilostomatoïde"))
rmarkdown::paged_table(sselect(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. Il peut être utilisé à l’intérieur de filter() ou sfilter(). Par exemple pour conserver toutes les lignes n’ayant pas de valeurs manquantes pour skeleton, vous écrirez sfilter(urchin2, !is.na(skeleton)). Vous noterez tout de même que ce n’est pas des plus lisibles. Il existe une fonction spécialement prévue pour débarrasser les tableaux des lignes contenant des valeurs manquantes : drop_na() ou sdrop_na(). 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 (mais faites très attention à ne pas utiliser inconsidérément cette fonction sans spécifier les colonnes : éliminer des individus sur base de valeurs manquantes dans des colonnes que vous n’utilisez pas ensuite est idiot).

urchin_sub4 <- sdrop_na(urchin)
rmarkdown::paged_table(urchin_sub4)

Pour terminer le filtrage des lignes de vos tableaux, il est utile d’être aussi capable de le faire en R de base. Nous avons déjà étudié l’opérateur [ qui extrait un sous-ensemble d’un data.frame et nous avons dit que son premier argument sélectionne les lignes. Nous pouvons y indiquer aussi un vecteur de valeurs logiques issues d’une comparaison. Ainsi, l’équivalent en R de base de urchin_sub2 <- sfilter(urchin2, height > 20) est :

urchin_sub2 <- urchin2[urchin2$height > 20, ]
head(urchin_sub2)
#     origin height skeleton
# 1: Fishery   21.6     9.31
# 2: Fishery   22.8    10.36
# 3: Fishery   21.2    10.30
# 4: Fishery   23.1    10.63
# 5: Fishery   23.3    11.63
# 6: Fishery   20.3     9.54

Notez la différence importante : en R de base, les variables du jeu de données urchin2 ne sont pas accessibles juste avec leur nom. Il faut spécifier de manière complète “la variable height du jeu de données urchin2” qui s’écrit urchin2$height, sans quoi R vous dira qu’il ne connait pas l’objet height. Avec filter() ou sfilter(), vous pouvez utiliser directement le nom de la variable, sans spécifier le nom du jeu de données qui est connu car c’est le premier argument de la fonction.

Enfin, l’opérateur [ permet de sélectionner des lignes et des colonnes en une seule opération, là ou les fonctions tidy ou speedy nécessitent un appel de filter()/sfilter() suivi d’un second appel de select()/sselect(). Donc, à partir du tableau complet urchin, pour sélectionner les six première lignes et les trois premières colonnes vous ferez en R de base :

urchin_sub5 <- urchin[1:6, 1:3]
urchin_sub5
#     origin diameter1 diameter2
# 1: Fishery       9.9      10.2
# 2: Fishery      10.5      10.6
# 3: Fishery      10.8      10.8
# 4: Fishery       9.6       9.3
# 5: Fishery      10.4      10.7
# 6: Fishery      10.5      11.1

Dans ce cas-ci, R de base est bien plus concis que les fonctions tidy ou speedy. Pour obtenir la même chose, vous devez écrire :

sfilter(urchin, 1:6) %>.%
  sselect(., 1:3) ->
  urchin_sub5
urchin_sub5
#     origin diameter1 diameter2
# 1: Fishery       9.9      10.2
# 2: Fishery      10.5      10.6
# 3: Fishery      10.8      10.8
# 4: Fishery       9.6       9.3
# 5: Fishery      10.4      10.7
# 6: Fishery      10.5      11.1
À vous de jouer !
h5p

5.4.3 mutate()/smutate()

La fonction tidy 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. La fonction speedy équivalente est smutate(). 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 <- smutate(urchin, 
  sum_skel  =  lantern + spines + test, 
  ratio     = sum_skel / skeleton,
  skeleton2 = skeleton^2)
rmarkdown::paged_table(sselect(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 <- smutate(urchin,
  skeleton_log  = log(skeleton), 
  skeleton_sqrt = sqrt(skeleton))
rmarkdown::paged_table(sselect(urchin, skeleton, skeleton_log, skeleton_sqrt))

La fonction tidy transmute() ou stransmute() 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.

En R de base, le calcul d’une nouvelle variable se fait comme ceci :

urchin$log_skeleton <- log(urchin$skeleton)
rmarkdown::paged_table(sselect(urchin, skeleton, log_skeleton))

Remarquez encore une fois que vous devez qualifier complètement le nom de la variable à calculer, en indiquant le nom du jeu de données et le nom de la variable comme urchin$skeleton. Si vous voulez éliminer une variable du jeu de données en R de base, vous lui assignez NULL :

urchin$log_skeleton <- NULL # Efface la variable log_skeleton dans urchin

Selon le contexte, la version tidy/speedy ou la version R de base sera la plus pratique.

À vous de jouer !
h5p

5.4.4 group_by()/sgroup_by()

La fonction tidy group_by() ou la fonction speedy sgroup_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()/ssummarise() (voir ci-dessous), mais aussi avec mutate()/smutate() pour faire des transformations par groupes. Pour annuler le regroupement, il suffit d’utiliser ungroup()/sungroup().

À noter que toutes les fonctions qui collectent les résultats, à savoir as_dtx(), collect_dtx() et les assignations alternatives %<-% et %->% dégroupent également automatiquement les données. Ceci est voulu afin d’éviter que l’on oublie plus tard que l’objet a un regroupement enregistré et que vous ne réalisiez par la suite des calculs non voulus (par groupes alors que vous pensez travailler individu). Il faut toujours préciser les groupes aussi proche que possible de leur utilisation, et dégrouper éventuellement à la fin lorsqu’on n’en a plus besoin.

urchin_by_orig <- sgroup_by(urchin, origin)
urchin_by_orig
#       origin diameter1 diameter2 height buoyant_weight weight solid_parts
#   1: Fishery       9.9      10.2    5.0             NA 0.5215      0.4777
#   2: Fishery      10.5      10.6    5.7             NA 0.6418      0.5891
#   3: Fishery      10.8      10.8    5.2             NA 0.7336      0.6770
#   4: Fishery       9.6       9.3    4.6             NA 0.3697      0.3438
#   5: Fishery      10.4      10.7    4.8             NA 0.6097      0.5587
#  ---                                                                     
# 417:    Farm      16.7      17.2    8.5         0.5674 2.4300      2.2900
# 418:    Farm      16.5      16.5    7.9         0.5472 2.3200      2.1800
# 419:    Farm      16.8      16.7    8.2         0.4864 2.2200      2.1300
# 420:    Farm      17.3      17.2    8.5         0.4864 2.5200      2.3400
# 421:    Farm      17.0      16.6    7.9         0.4357 2.0500      1.9800
#      integuments dry_integuments digestive_tract dry_digestive_tract gonads
#   1:      0.3658              NA          0.0525              0.0079 0.0000
#   2:      0.4447              NA          0.0482              0.0090 0.0000
#   3:      0.5326              NA          0.0758              0.0134 0.0000
#   4:      0.2661              NA          0.0442              0.0064 0.0000
#   5:      0.4058              NA          0.0743              0.0117 0.0000
#  ---                                                                       
# 417:      1.8400            1.02          0.1661              0.0229 0.0215
# 418:      1.8000            1.01          0.0977              0.0147 0.0253
# 419:      1.6300            0.88          0.1704              0.0208 0.0154
# 420:      1.7200            0.89          0.1444              0.0167 0.0237
# 421:      1.4300            0.83          0.1462              0.0212 0.0266
#      dry_gonads skeleton lantern   test spines maturity  sex sum_skel     ratio
#   1:     0.0000   0.1793  0.0211 0.0587 0.0995        0 <NA>   0.1793 1.0000000
#   2:     0.0000   0.1880  0.0205 0.0622 0.1053        0 <NA>   0.1880 1.0000000
#   3:     0.0000   0.2354  0.0254 0.0836 0.1263        0 <NA>   0.2353 0.9995752
#   4:     0.0000   0.0630  0.0167 0.0180 0.0283        0 <NA>   0.0630 1.0000000
#   5:     0.0000       NA      NA     NA     NA        0 <NA>       NA        NA
#  ---                                                                           
# 417:     0.0034   0.9046  0.0750 0.3399 0.4896        0 <NA>   0.9045 0.9998895
# 418:     0.0051   0.8965  0.0908 0.3189 0.4868        0 <NA>   0.8965 1.0000000
# 419:     0.0020   0.7714  0.0877 0.2961 0.3876        0 <NA>   0.7714 1.0000000
# 420:     0.0032   0.7938  0.0772 0.3077 0.4090        0 <NA>   0.7939 1.0001260
# 421:     0.0051   0.7421  0.0723 0.2689 0.4009        0 <NA>   0.7421 1.0000000
#       skeleton2 skeleton_log skeleton_sqrt
#   1: 0.03214849   -1.7186949     0.4234383
#   2: 0.03534400   -1.6713133     0.4335897
#   3: 0.05541316   -1.4464691     0.4851804
#   4: 0.00396900   -2.7646206     0.2509980
#   5:         NA           NA            NA
#  ---                                      
# 417: 0.81830116   -0.1002624     0.9511046
# 418: 0.80371225   -0.1092570     0.9468368
# 419: 0.59505796   -0.2595482     0.8782938
# 420: 0.63011844   -0.2309237     0.8909545
# 421: 0.55071241   -0.2982713     0.8614523
# 
# Grouped by:  origin  [2 | 210 (10.6) 203-218]

Noter la ligne ci-dessus : Grouped by: origin qui indique quels regroupements sont actifs dans le tableau.

# collect_dtx() ou %<-% / %->% dégroupent automatiquement
urchin_collected %<-% urchin_by_orig
urchin_collected
#       origin diameter1 diameter2 height buoyant_weight weight solid_parts
#   1: Fishery       9.9      10.2    5.0             NA 0.5215      0.4777
#   2: Fishery      10.5      10.6    5.7             NA 0.6418      0.5891
#   3: Fishery      10.8      10.8    5.2             NA 0.7336      0.6770
#   4: Fishery       9.6       9.3    4.6             NA 0.3697      0.3438
#   5: Fishery      10.4      10.7    4.8             NA 0.6097      0.5587
#  ---                                                                     
# 417:    Farm      16.7      17.2    8.5         0.5674 2.4300      2.2900
# 418:    Farm      16.5      16.5    7.9         0.5472 2.3200      2.1800
# 419:    Farm      16.8      16.7    8.2         0.4864 2.2200      2.1300
# 420:    Farm      17.3      17.2    8.5         0.4864 2.5200      2.3400
# 421:    Farm      17.0      16.6    7.9         0.4357 2.0500      1.9800
#      integuments dry_integuments digestive_tract dry_digestive_tract gonads
#   1:      0.3658              NA          0.0525              0.0079 0.0000
#   2:      0.4447              NA          0.0482              0.0090 0.0000
#   3:      0.5326              NA          0.0758              0.0134 0.0000
#   4:      0.2661              NA          0.0442              0.0064 0.0000
#   5:      0.4058              NA          0.0743              0.0117 0.0000
#  ---                                                                       
# 417:      1.8400            1.02          0.1661              0.0229 0.0215
# 418:      1.8000            1.01          0.0977              0.0147 0.0253
# 419:      1.6300            0.88          0.1704              0.0208 0.0154
# 420:      1.7200            0.89          0.1444              0.0167 0.0237
# 421:      1.4300            0.83          0.1462              0.0212 0.0266
#      dry_gonads skeleton lantern   test spines maturity  sex sum_skel     ratio
#   1:     0.0000   0.1793  0.0211 0.0587 0.0995        0 <NA>   0.1793 1.0000000
#   2:     0.0000   0.1880  0.0205 0.0622 0.1053        0 <NA>   0.1880 1.0000000
#   3:     0.0000   0.2354  0.0254 0.0836 0.1263        0 <NA>   0.2353 0.9995752
#   4:     0.0000   0.0630  0.0167 0.0180 0.0283        0 <NA>   0.0630 1.0000000
#   5:     0.0000       NA      NA     NA     NA        0 <NA>       NA        NA
#  ---                                                                           
# 417:     0.0034   0.9046  0.0750 0.3399 0.4896        0 <NA>   0.9045 0.9998895
# 418:     0.0051   0.8965  0.0908 0.3189 0.4868        0 <NA>   0.8965 1.0000000
# 419:     0.0020   0.7714  0.0877 0.2961 0.3876        0 <NA>   0.7714 1.0000000
# 420:     0.0032   0.7938  0.0772 0.3077 0.4090        0 <NA>   0.7939 1.0001260
# 421:     0.0051   0.7421  0.0723 0.2689 0.4009        0 <NA>   0.7421 1.0000000
#       skeleton2 skeleton_log skeleton_sqrt
#   1: 0.03214849   -1.7186949     0.4234383
#   2: 0.03534400   -1.6713133     0.4335897
#   3: 0.05541316   -1.4464691     0.4851804
#   4: 0.00396900   -2.7646206     0.2509980
#   5:         NA           NA            NA
#  ---                                      
# 417: 0.81830116   -0.1002624     0.9511046
# 418: 0.80371225   -0.1092570     0.9468368
# 419: 0.59505796   -0.2595482     0.8782938
# 420: 0.63011844   -0.2309237     0.8909545
# 421: 0.55071241   -0.2982713     0.8614523
# Excepté pour les commentaires, ces deux objets sont identiques
comment(urchin_collected) <- comment(urchin)
identical(urchin_collected, urchin)
# [1] TRUE

5.4.5 summarise()/ssummarise()

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 tidy summarise(), ou son équivalent speedy ssummarise() reporte automatiquement ces calculs, en tenant compte des regroupements proposés via group_by()/sgroup_by().

Si vous utilisez les fonctions speedy (commençant par un “s”), il est conseillé d’utiliser les fonction “fstat” (commençant par un “f”) pour les calculs des moyennes, médianes, écarts types… Donc fmean(), fmedian(), fsd()… La combinaison des deux permet des accélérations substantielles des calculs. Cela ne vous apparaîtra pas avec les petits jeux de données utilisés au cours, mais lorsque vous traiterez des gros tableaux, le gain de vitesse peut être de dix fois, voire plus encore.

tooth <- read("ToothGrowth", package = "datasets", lang = "fr")
tooth_summary <- ssummarise(tooth,
  "moyenne" = fmean(len), 
  "minimum" = fmin(len), 
  "médiane" = fmedian(len), 
  "maximum" = fmax(len))
# Utiliser soit kable(), soit tabularise() pour afficher un tableau
#knitr::kable(tooth_summary, digits = 2,
#  caption = "Allongement des dents chez des cochons d'Inde recevant de l'acide ascorbique.")
# Avec tabularise(), indiquer auto.labs = FALSE pour éviter une erreur !
tabularise(tooth_summary, auto.labs = FALSE)

moyenne

minimum

médiane

maximum

18.81333

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. Pour se faire, nous allons combiner deux étapes en une : un (s)group_by() suivi d’un (s)summarise() (en fait, imbriqué dans …).

tooth_summary2 <- ssummarise(sgroup_by(tooth, supp),
  "moyenne" = fmean(len), 
  "minimum" = fmin(len), 
  "médiane" = fmedian(len), 
  "maximum" = fmax(len))
# Avec tabularise(), indiquer auto.labs = FALSE pour éviter une erreur !
tabularise(tooth_summary2, auto.labs = FALSE)

supp

moyenne

minimum

médiane

maximum

OJ

20.66333

8.2

22.7

30.9

VC

16.96333

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’observations par sous-groupe grâce à la fonction fn().
tooth_summary2 <- ssummarise(sgroup_by(tooth, supp),
  "moyenne" = fmean(len), 
  "minimum" = fmin(len), 
  "médiane" = fmedian(len), 
  "maximum" = fmax(len),
  "n"       = fn(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).")
tabularise(tooth_summary2, auto.labs = FALSE)

supp

moyenne

minimum

médiane

maximum

n

OJ

20.66333

8.2

22.7

30.9

30

VC

16.96333

4.2

16.5

33.9

30

Si vous souhaitez connaitre le nombre d’observations non manquantes dans votre jeu de données, utilisez fnobs() au lieu de fn(). Vous pouvez naturellement utiliser les deux simultanément.

urchin_skeleton_summary <- ssummarise(sgroup_by(urchin, origin),
  "moyenne" = fmean(skeleton, na.rm = TRUE),
  "n"       = fn(skeleton),    # Nombre total de cas
  "n obs"   = fnobs(skeleton)) # Nombre de cas hors valeurs manquantes
#knitr::kable(urchin_skeleton_summary, digits = 2,
#  caption = "Moyenne, nombre de cas et nombre d'observations hors valeurs manquantes pour la masse du squelette (en g) d'oursins d'élevage et de pêcheries.")
tabularise(urchin_skeleton_summary, auto.labs = FALSE)

origin

moyenne

n

n obs

Farm

6.882698

218

122

Fishery

7.206843

203

136

Dans le cas présent, le nombre d’observations utilisable est nettement inférieur au nombre total de cas dans le tableau. Cette distinction entre fn() et fnobs() est donc très importante. Ne la perdez pas de vue. De plus, si vous n’utilisez pas na.rm = TRUE dans les fonctions comme mean()/fmean(), median()/fmedian()… vous aurez des NA partout dans votre tableau résumé. D’un autre côté, soyez bien conscient qu’avec na.rm = TRUE, c’est le nombre d’observations hors valeurs manquantes qui est utilisé pour ces calculs !

Les fonctions fn() et fnobs() ne sont pas définies dans le tidyverse. Ce sont des fonctions d’une autre famille appelée dans SciViews::R des fonctions “fstat”. Dans tidyverse, on vous fera utiliser la fonction n() sans argument qui est l’équivalent de fn(). Il n’y a pas d’équivalent direct de fnobs() et il faut ruser avec du code comme sum(!is.na(skeleton)) pour obtenir ce résultat, ce qui n’est pas pratique. La fonction n() n”est pas utilisable avec les fonctions speedy, et d’une manière générale, préférez-lui fn() qui est plus logique et qui fonctionne partout.

  • 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 seraient écrasées par le résultat du calcul. Avec un data.table, ceci n’est pas permis (le code fonctionnera pourtant dans un autre contexte avec un data.frame, c’est pourquoi nous convertissons d’abord tooth avec as_dtf()). Voici un exemple concret :
tooth_summary3 <- summarise(as_dtf(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.6: Exemple de résumé des données erroné à cause de l’écrasement de la variable len.
len len_sd
18.81 NA
#tabularise(tooth_summary3, auto.labs = FALSE)

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 len s’agit-il ici ? Et bien la dernière calculée, 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 espère !

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

Toutefois, ssummarise() fonctionne différemment et n’a pas de problèmes dans ce cas-là :

tooth_summary4 <- ssummarise(tooth,
  "len" = fmean(len), # Notez le même nom à gauche et à droite du = (len)
  "len_sd" = fsd(len))
knitr::kable(tooth_summary4, digits = 2,
  caption = "Exemple de résumé des données correct avec `fsummarise()`.")
Tableau 5.7: Exemple de résumé des données correct avec fsummarise().
len len_sd
18.81 7.65
#tabularise(tooth_summary4, auto.labs = FALSE)

Faites bien attention : les couples de fonctions avec et sans “s” comme select()/sselect(), mutate()/smutate() ou summarise()/ssummarise() se ressemblent très fort au niveau de leur usage. On pourrait donc être tenté de croire qu’elles sont parfaitement interchangeables. La plupart du temps, c’est le cas. Cependant, elles fonctionnement radicalement différemment en interne et des différences existent, sont connues, et ne sont pas des bugs. Vérifiez toujours votre code !

5.4.5.1 Résumé avec les fonctions “fstat”

R propose de base une série de fonctions qui calculent des descripteurs statistiques classiques tels que la moyenne mean(), la médiane median(), la variance var(), etc. Toutes ces fonctions ont un argument na.rm qui prend la valeur FALSE par défaut. S’il y a une valeur manquante ou plus, le calcul renverra alors NA.

vec <- c(5, 3, 8, NA, 4)
mean(vec)
# [1] NA

Vous devez indiquer na.rm = TRUE si vous voulez quand même calculer la moyenne sur ce vecteur vec en utilisant uniquement les observations (non manquantes) :

mean(vec, na.rm = TRUE)
# [1] 5

Ces fonctions ne sont pas prévues pour travailler sur des tableaux entiers.

mean(iris)
# Warning in mean.default(iris): argument is not numeric or logical: returning NA
# [1] NA

La fonction mean() (et les autres fonctions équivalentes) ne sont pas prévues pour travailler sur ce genre d’objet qui contient plusieurs colonnes. C’est pour cela que vous devez utiliser une construction plus complexe avec summarise() ou ssummarise(). Si vous voulez résumer les quatre variables numériques par la moyenne en fonction de species, vous devez faire (rappelez-vous que vous devez aussi utiliser d’autres noms que les noms de variables dans le tableau de départ) :

ssummarise(sgroup_by(iris, species),
  mean_sepal_length = mean(sepal_length, na.rm = TRUE),
  mean_sepal_width  = mean(sepal_width, na.rm = TRUE),
  mean_petal_length = mean(petal_length, na.rm = TRUE),
  mean_petal_width  = mean(petal_width, na.rm = TRUE))
#       species mean_sepal_length mean_sepal_width mean_petal_length
# 1:     setosa             5.006            3.428             1.462
# 2: versicolor             5.936            2.770             4.260
# 3:  virginica             6.588            2.974             5.552
#    mean_petal_width
# 1:            0.246
# 2:            1.326
# 3:            2.026

Tidyverse offre une astuce pour éviter de se répéter. Si vous devez appliquer la même fonction sur plusieurs variables, vous pouvez l’indiquer avec across() (aussi avec les fonctions speedy) comme ceci :

ssummarise(sgroup_by(iris, species),
  across(sepal_length:petal_width, fmean, na.rm = TRUE))
#       species sepal_length sepal_width petal_length petal_width
# 1:     setosa        5.006       3.428        1.462       0.246
# 2: versicolor        5.936       2.770        4.260       1.326
# 3:  virginica        6.588       2.974        5.552       2.026

C’est déjà mieux, même si c’est moins lisible quant à ce que l’on veut obtenir comme tableau final. Dans le package {collapse}, et dans SciViews::R qui utilise ce package, il y a des fonctions de remplacement de mean(), median(), … qui offrent plusieurs avantages. Ce sont les fonctions “fstat” que nous avons déjà utilisées. Nous allons maintenant détailler un peu plus ces fonctions.

Souvent, vous pourrez utiliser fmean() à la place de mean(), mais son comportement et ses possibilités sont très différentes :

  • elle est plus rapide (appréciable pour les gros jeux de données), en fait “f”, c’est pour fast !
  • la valeur par défaut pour son argument na.rm est TRUE. Vous ne devez donc pas préciser na.rm = TRUE à tout bout de champ (mais restez bien attentif aux valeurs manquantes dans vos données !)
  • elle fonctionne aussi sur des tableaux de données ne contenant que des variables numériques
  • elle peut utiliser les regroupements effectués à l’aide de sgroup_by() (mais pas group_by()) et en tient compte à condition que toutes les autres variables non concernées par le regroupement soient numériques

Illustrons tout ceci :

fmean(vec) # Pas besoin de préciser na.rm = TRUE, c'est la valeur pas défaut
# [1] 5

Application directe sur un tableau, même avec regroupement :

fmean(sgroup_by(iris, species))
#       species sepal_length sepal_width petal_length petal_width
# 1:     setosa        5.006       3.428        1.462       0.246
# 2: versicolor        5.936       2.770        4.260       1.326
# 3:  virginica        6.588       2.974        5.552       2.026

Le tableau obtenu est très similaire à celui que nous avions calculé à l’aide de ssummarise() plus haut (à part que le nom des variables est conservé) et identique à celui obtenu en utilisant across(). Le calcul est toutefois bien plus rapide (vous ne devez pas comprendre le code ci-dessous, juste considérer que l’on détermine les performances de trois versions du même calcul : “tidy”, “speedy” et “fstat”) :

iris2 <- datasets::iris
names(iris2) <- c("sepal_length", "sepal_width", "petal_length", "petal_width", "species")
bench::mark(
  tidy   = collect_dtf(summarise(group_by(iris2, species),
    across(sepal_length:petal_width, mean, na.rm = TRUE))),
  speedy = ssummarise(sgroup_by(iris2, species),
    across(sepal_length:petal_width, fmean, na.rm = TRUE)),
  fstat  = fmean(sgroup_by(iris2, species)))
# Warning: There was 1 warning in `summarise()`.
# ℹ In argument: `across(sepal_length:petal_width, mean, na.rm = TRUE)`.
# ℹ In group 1: `species = setosa`.
# Caused by warning:
# ! The `...` argument of `across()` is deprecated as of dplyr 1.1.0.
# Supply arguments directly to `.fns` through an anonymous function instead.
# 
#   # Previously
#   across(a:b, mean, na.rm = TRUE)
# 
#   # Now
#   across(a:b, \(x) mean(x, na.rm = TRUE))
# # A tibble: 3 × 6
#   expression      min   median `itr/sec` mem_alloc `gc/sec`
#   <bch:expr> <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl>
# 1 tidy         11.2ms   11.4ms      86.6    1.79MB     17.3
# 2 speedy         36µs     40µs   24177.     1.27KB     16.9
# 3 fstat        19.8µs   22.1µs   43410.     1.27KB     17.4

Examinez les colonnes median qui est le temps median et mem_alloc qui est la mémoire vive nécessaires pour faire ces calculs.

  • Il a fallu 500 fois moins de temps à fmean() qu’à la version tidy avec summarise() et presque trois fois moins de temps que la version speedy avec ssummarise(). Vous comprenez maintenant pourquoi ces fonctions sont appelées “speedy” et “fast”.

  • Concernant l’utilisation de la mémoire vive, il a fallu neuf cent fois moins de mémoire vive à fmean() qu’à summarise et un petit peu moins qu’à ssummarise().

Par rapport aux petits jeux de données que nous utilisons dans le cadre de ce cours, la différence n’est pas très visible. Mais si vous travaillez plus tard avec des bien plus gros jeux de données, pensez à utiliser les fonctions speedy, ou mieux les fonctions fstat directement si vous le pouvez.

Enfin, vous pouvez lister toutes les fonctions fstat disponibles comme ceci :

list_fstat_functions()
#  [1] "ffirst"     "flast"      "fmax"       "fmean"      "fmedian"   
#  [6] "fmin"       "fmode"      "fndistinct" "fnobs"      "fn"        
# [11] "fna"        "fnth"       "fprod"      "fsd"        "fsum"      
# [16] "fvar"

Notez bien aussi les fonction fn() et fnobs() que nous avons vues pour énumérer les cas et les observations effectivement réalisées hors valeurs manquantes. fna compte les valeurs manquantes et est donc complémentaire à fnobs() (fna(x) + fnobs(x) == fn(x)) fndistinct() peut aussi être utile pour énumérer le nombre de valeurs différentes rencontrées (par exemple pour décider si cela est raisonnable de convertir directement une variable numeric ou character en factor). Vous devriez pouvoir déduire le rôle des autres fonctions via leur nom. fvar() calcule la variance alors que fsd() calcule l’écart type (standard deviation en anglais, d’où l’abréviation “sd”) qui est la racine carrée de la variance.

Pour en savoir plus
  • Le chapitre consacré à la transformation des données de R for Data Science seconde edition présente le remaniement d’un tableau de données à l’aide des fonctions tidy 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 ou https://phind.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”. Ne cherchez pas les fonctions speedy ou fstat, vous ne trouverez pas grand chose par contre. Adaptez les exemples trouvés avec les fonctions tidy, si nécessaire.

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


  1. Voyez ?tidyselect::select_helpers pour une panoplie de fonctions supplémentaires qui permettent une sélection “intelligente” des variables utilisables avec select().↩︎

  2. L’ordre alphabétique qui fait également intervenir les caractères accentués diffère en fonction de la configuration du système (langue). L’état du système tel que vu par R pour le tri alphabétique est obtenu par Sys.getlocale("LC_COLLATE"). Dans la SciViews Box, ceci est toujours "en_US.UTF-8", ceci afin de rendre le traitement reproductible d’un PC à l’autre, qu’il soit en anglais, français, espagnol, chinois, ou n’importe quelle autre langue.↩︎