4.4 Remaniement des données
Dans le module 3, 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.
4.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.
4.4.1.1 Fonctions “speedy”
Pour lister les fonctions speedy disponibles, vous pouvez faire :
# [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 :
# origin height skeleton
# <fctr> <num> <num>
# 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 une 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 fait à l’aide de nombre entiers qui indiquent l’index des lignes ou des colonnes à conserver. Si ce sont des nombres négatifs, ces lignes ou colonnes sont enlevées. Par exemple pour conserver les colonnes 1 et 3 de urchin
, on écrira en R de base :
# origin diameter2
# <fctr> <num>
# 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 :
# diameter1 height buoyant_weight weight solid_parts integuments
# <num> <num> <num> <num> <num> <num>
# 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
# <num> <num> <num> <num> <num>
# 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
# <num> <num> <num> <num> <int> <fctr>
# 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.
4.4.1.2 Fonctions “tidy”
Pour lister les fonctions tidy disponibles, vous pouvez faire :
# [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
- la data.table est implémentée 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 :
# origin height skeleton
# <fctr> <num> <num>
# 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()
.
# origin buoyant_weight weight
# <fctr> <num> <num>
# 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
:
# height buoyant_weight weight
# <num> <num> <num>
# 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
4.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
).
# origin height skeleton
# <fctr> <num> <num>
# 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. 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).
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 :
# origin height skeleton
# <fctr> <num> <num>
# 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 :
# origin diameter1 diameter2
# <fctr> <num> <num>
# 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 :
# origin diameter1 diameter2
# <fctr> <num> <num>
# 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
4.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 :
%/%
- addition :
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()
oulog()
(logarithme népérien),lg()
oulog10()
(logarithme en base 10)ln1p()
oulog1p()
(logarithme népérien de x + 1), oulg1p()
(logarithme en base 10 de x + 1)exp()
(exponentielle, ex) etexpm1()
(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
:
Selon le contexte, la version tidy/speedy ou la version R de base sera la plus pratique.
4.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.
# origin diameter1 diameter2 height buoyant_weight weight solid_parts
# <fctr> <num> <num> <num> <num> <num> <num>
# 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
# <num> <num> <num> <num> <num>
# 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
# <num> <num> <num> <num> <num> <int> <fctr> <num>
# 1: 0.0000 0.1793 0.0211 0.0587 0.0995 0 <NA> 0.1793
# 2: 0.0000 0.1880 0.0205 0.0622 0.1053 0 <NA> 0.1880
# 3: 0.0000 0.2354 0.0254 0.0836 0.1263 0 <NA> 0.2353
# 4: 0.0000 0.0630 0.0167 0.0180 0.0283 0 <NA> 0.0630
# 5: 0.0000 NA NA NA NA 0 <NA> NA
# ---
# 417: 0.0034 0.9046 0.0750 0.3399 0.4896 0 <NA> 0.9045
# 418: 0.0051 0.8965 0.0908 0.3189 0.4868 0 <NA> 0.8965
# 419: 0.0020 0.7714 0.0877 0.2961 0.3876 0 <NA> 0.7714
# 420: 0.0032 0.7938 0.0772 0.3077 0.4090 0 <NA> 0.7939
# 421: 0.0051 0.7421 0.0723 0.2689 0.4009 0 <NA> 0.7421
# ratio skeleton2 skeleton_log skeleton_sqrt
# <num> <num> <num> <num>
# 1: 1.0000000 0.03214849 -1.7186949 0.4234383
# 2: 1.0000000 0.03534400 -1.6713133 0.4335897
# 3: 0.9995752 0.05541316 -1.4464691 0.4851804
# 4: 1.0000000 0.00396900 -2.7646206 0.2509980
# 5: NA NA NA NA
# ---
# 417: 0.9998895 0.81830116 -0.1002624 0.9511046
# 418: 1.0000000 0.80371225 -0.1092570 0.9468368
# 419: 1.0000000 0.59505796 -0.2595482 0.8782938
# 420: 1.0001260 0.63011844 -0.2309237 0.8909545
# 421: 1.0000000 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
# <fctr> <num> <num> <num> <num> <num> <num>
# 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
# <num> <num> <num> <num> <num>
# 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
# <num> <num> <num> <num> <num> <int> <fctr> <num>
# 1: 0.0000 0.1793 0.0211 0.0587 0.0995 0 <NA> 0.1793
# 2: 0.0000 0.1880 0.0205 0.0622 0.1053 0 <NA> 0.1880
# 3: 0.0000 0.2354 0.0254 0.0836 0.1263 0 <NA> 0.2353
# 4: 0.0000 0.0630 0.0167 0.0180 0.0283 0 <NA> 0.0630
# 5: 0.0000 NA NA NA NA 0 <NA> NA
# ---
# 417: 0.0034 0.9046 0.0750 0.3399 0.4896 0 <NA> 0.9045
# 418: 0.0051 0.8965 0.0908 0.3189 0.4868 0 <NA> 0.8965
# 419: 0.0020 0.7714 0.0877 0.2961 0.3876 0 <NA> 0.7714
# 420: 0.0032 0.7938 0.0772 0.3077 0.4090 0 <NA> 0.7939
# 421: 0.0051 0.7421 0.0723 0.2689 0.4009 0 <NA> 0.7421
# ratio skeleton2 skeleton_log skeleton_sqrt
# <num> <num> <num> <num>
# 1: 1.0000000 0.03214849 -1.7186949 0.4234383
# 2: 1.0000000 0.03534400 -1.6713133 0.4335897
# 3: 0.9995752 0.05541316 -1.4464691 0.4851804
# 4: 1.0000000 0.00396900 -2.7646206 0.2509980
# 5: NA NA NA NA
# ---
# 417: 0.9998895 0.81830116 -0.1002624 0.9511046
# 418: 1.0000000 0.80371225 -0.1092570 0.9468368
# 419: 1.0000000 0.59505796 -0.2595482 0.8782938
# 420: 1.0001260 0.63011844 -0.2309237 0.8909545
# 421: 1.0000000 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
4.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.8 | 4.2 | 19.2 | 33.9 |
Voici les mêmes calculs, mais effectués séparément pour les deux types de supplémentations alimentaires. Pour ce 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.7 | 8.2 | 22.7 | 30.9 |
VC | 17.0 | 4.2 | 16.5 | 33.9 |
Pièges et astuces
- Tout comme lors de réalisation d’une boite à moustaches, 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.7 | 8.2 | 22.7 | 30.9 | 30 |
VC | 17.0 | 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.88 | 218 | 122 |
Fishery | 7.21 | 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 une 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’abordtooth
avecas_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`.")
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 quellen
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()`.")
len | len_sd |
---|---|
18.81 | 7.65 |
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 !
4.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
.
# [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) :
# [1] 5
Ces fonctions ne sont pas prévues pour travailler sur des tableaux entiers.
# 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
# <fctr> <num> <num> <num>
# 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
# <num>
# 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 :
# species sepal_length sepal_width petal_length petal_width
# <fctr> <num> <num> <num> <num>
# 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
estTRUE
. Vous ne devez donc pas préciserna.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 pasgroup_by()
) et en tient compte à condition que toutes les autres variables non concernées par le regroupement soient numériques
Illustrons tout ceci :
# [1] 5
Application directe sur un tableau, même avec regroupement :
# species sepal_length sepal_width petal_length petal_width
# <fctr> <num> <num> <num> <num>
# 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 2.33ms 2.6ms 373. 1.78MB 15.3
# 2 speedy 36.33µs 38µs 25564. 1.27KB 15.3
# 3 fstat 19.8µs 20.9µs 45606. 1.27KB 13.7
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 avecsummarise()
et presque trois fois moins de temps que la version speedy avecssummarise()
. 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 :
# [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
.
Voyez
?tidyselect::select_helpers
pour une panoplie de fonctions supplémentaires qui permettent une sélection “intelligente” des variables utilisables avecselect()
.↩︎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.↩︎