From 2f49a0d2445e7f455045f2511949da8fd1b8b2be Mon Sep 17 00:00:00 2001 From: Philippe Grosjean Date: Mon, 11 Nov 2024 17:33:04 +0100 Subject: [PATCH] French translation (vignettes) (#6455) Co-authored-by: Michael Chirico Co-authored-by: Toby Dylan Hocking Co-authored-by: Toby Dylan Hocking Co-authored-by: Toby Dylan Hocking --- .Rbuildignore | 1 + DESCRIPTION | 7 +- vignettes/.check.translations.R | 27 + vignettes/datatable-benchmarking.Rmd | 4 + vignettes/datatable-faq.Rmd | 4 + vignettes/datatable-importing.Rmd | 4 + vignettes/datatable-intro.Rmd | 4 + vignettes/datatable-keys-fast-subset.Rmd | 4 + vignettes/datatable-programming.Rmd | 4 + vignettes/datatable-reference-semantics.Rmd | 4 + vignettes/datatable-reshape.Rmd | 4 + vignettes/datatable-sd-usage.Rmd | 4 + ...le-secondary-indices-and-auto-indexing.Rmd | 4 + vignettes/fr/datatable-benchmarking.Rmd | 130 ++++ vignettes/fr/datatable-faq.Rmd | 630 ++++++++++++++++ vignettes/fr/datatable-importing.Rmd | 283 +++++++ vignettes/fr/datatable-intro.Rmd | 698 ++++++++++++++++++ vignettes/fr/datatable-keys-fast-subset.Rmd | 501 +++++++++++++ vignettes/fr/datatable-programming.Rmd | 420 +++++++++++ .../fr/datatable-reference-semantics.Rmd | 386 ++++++++++ vignettes/fr/datatable-reshape.Rmd | 297 ++++++++ vignettes/fr/datatable-sd-usage.Rmd | 256 +++++++ ...le-secondary-indices-and-auto-indexing.Rmd | 332 +++++++++ 23 files changed, 4007 insertions(+), 1 deletion(-) create mode 100644 vignettes/.check.translations.R create mode 100644 vignettes/fr/datatable-benchmarking.Rmd create mode 100644 vignettes/fr/datatable-faq.Rmd create mode 100644 vignettes/fr/datatable-importing.Rmd create mode 100644 vignettes/fr/datatable-intro.Rmd create mode 100644 vignettes/fr/datatable-keys-fast-subset.Rmd create mode 100644 vignettes/fr/datatable-programming.Rmd create mode 100644 vignettes/fr/datatable-reference-semantics.Rmd create mode 100644 vignettes/fr/datatable-reshape.Rmd create mode 100644 vignettes/fr/datatable-sd-usage.Rmd create mode 100644 vignettes/fr/datatable-secondary-indices-and-auto-indexing.Rmd diff --git a/.Rbuildignore b/.Rbuildignore index 07c95a0fd3..f3ee0dd7e9 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -1,4 +1,5 @@ .dir-locals.el +.check.translations.R ^\.Rprofile$ ^data\.table_.*\.tar\.gz$ ^config\.log$ diff --git a/DESCRIPTION b/DESCRIPTION index d17f2ea75c..1fab0614c5 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -92,5 +92,10 @@ Authors@R: c( person("Ivan", "Krylov", role="ctb"), person("Angel", "Feliz", role="ctb"), person("Michael","Young", role="ctb"), - person("Mark", "Seeto", role="ctb") + person("Mark", "Seeto", role="ctb"), + person("Philippe", "Grosjean", role="ctb"), + person("Vincent", "Runge", role="ctb"), + person("Christian", "Wia", role="ctb"), + person("Elise", "Maigné", role="ctb"), + person("Vincent", "Rocher", role="ctb") ) diff --git a/vignettes/.check.translations.R b/vignettes/.check.translations.R new file mode 100644 index 0000000000..ae7173f9ae --- /dev/null +++ b/vignettes/.check.translations.R @@ -0,0 +1,27 @@ +library(data.table) +translated.Rmd <- Sys.glob("*/*.Rmd") +rm_suffix <- function(x)sub("[.][^.]+$", "", x) +trans.dt <- data.table( + lang=dirname(translated.Rmd), + trans_file=rm_suffix(basename(translated.Rmd))) +en.dt <- data.table(en.Rmd=Sys.glob("*.Rmd"))[ +, en_file := rm_suffix(en.Rmd) +][] +link.dt <- en.dt[, nc::capture_all_str( + en.Rmd, + "https://rdatatable.gitlab.io/data.table/articles/", + lang=".*?", + "/", + trans_file=".*?html", rm_suffix +), by=en_file] +maybe_err <- function(dt,msg){ + if(nrow(dt)){ + cat(msg,":\n") + print(dt) + } +} +maybe_err(link.dt[en_file != trans_file],"typos in links") +files.join <- trans.dt[en.dt, .( + langs=paste(lang[!is.na(lang)],collapse=",") +), on=.(trans_file=en_file), by=.EACHI] +maybe_err(files.join[nchar(langs) +Translations of this document are available in + +* [French](https://rdatatable.gitlab.io/data.table/articles/fr/datatable-benchmarking.html) + This document is meant to guide on measuring performance of `data.table`. Single place to document best practices and traps to avoid. # fread: clear caches diff --git a/vignettes/datatable-faq.Rmd b/vignettes/datatable-faq.Rmd index edad2a0c05..4c6f4c6331 100644 --- a/vignettes/datatable-faq.Rmd +++ b/vignettes/datatable-faq.Rmd @@ -26,6 +26,10 @@ h2 { } +Translations of this document are available in + +* [French](https://rdatatable.gitlab.io/data.table/articles/fr/datatable-faq.html) + ```{r, echo = FALSE, message = FALSE} library(data.table) knitr::opts_chunk$set( diff --git a/vignettes/datatable-importing.Rmd b/vignettes/datatable-importing.Rmd index 2d08ffcf9e..21e42b67c6 100644 --- a/vignettes/datatable-importing.Rmd +++ b/vignettes/datatable-importing.Rmd @@ -15,6 +15,10 @@ h2 { } +Translations of this document are available in + +* [French](https://rdatatable.gitlab.io/data.table/articles/fr/datatable-importing.html) + This document is focused on using `data.table` as a dependency in other R packages. If you are interested in using `data.table` C code from a non-R application, or in calling its C functions directly, jump to the [last section](#non-r-api) of this vignette. Importing `data.table` is no different from importing other R packages. This vignette is meant to answer the most common questions arising around that subject; the lessons presented here can be applied to other R packages. diff --git a/vignettes/datatable-intro.Rmd b/vignettes/datatable-intro.Rmd index 72eabbd8e7..a0ce8bf047 100644 --- a/vignettes/datatable-intro.Rmd +++ b/vignettes/datatable-intro.Rmd @@ -9,6 +9,10 @@ vignette: > \usepackage[utf8]{inputenc} --- +Translations of this document are available in + +* [French](https://rdatatable.gitlab.io/data.table/articles/fr/datatable-intro.html) + ```{r, echo = FALSE, message = FALSE} require(data.table) knitr::opts_chunk$set( diff --git a/vignettes/datatable-keys-fast-subset.Rmd b/vignettes/datatable-keys-fast-subset.Rmd index 5602c32051..d60552ea8f 100644 --- a/vignettes/datatable-keys-fast-subset.Rmd +++ b/vignettes/datatable-keys-fast-subset.Rmd @@ -9,6 +9,10 @@ vignette: > \usepackage[utf8]{inputenc} --- +Translations of this document are available in + +* [French](https://rdatatable.gitlab.io/data.table/articles/fr/datatable-keys-fast-subset.html) + ```{r, echo = FALSE, message = FALSE} require(data.table) knitr::opts_chunk$set( diff --git a/vignettes/datatable-programming.Rmd b/vignettes/datatable-programming.Rmd index eedb5e9928..93c6dc5568 100644 --- a/vignettes/datatable-programming.Rmd +++ b/vignettes/datatable-programming.Rmd @@ -9,6 +9,10 @@ vignette: > \usepackage[utf8]{inputenc} --- +Translations of this document are available in + +* [French](https://rdatatable.gitlab.io/data.table/articles/fr/datatable-programming.html) + ```{r init, include = FALSE} require(data.table) knitr::opts_chunk$set( diff --git a/vignettes/datatable-reference-semantics.Rmd b/vignettes/datatable-reference-semantics.Rmd index 3f9fd7328b..0c55fc4a1d 100644 --- a/vignettes/datatable-reference-semantics.Rmd +++ b/vignettes/datatable-reference-semantics.Rmd @@ -9,6 +9,10 @@ vignette: > \usepackage[utf8]{inputenc} --- +Translations of this document are available in + +* [French](https://rdatatable.gitlab.io/data.table/articles/fr/datatable-reference-semantics.html) + ```{r, echo = FALSE, message = FALSE} require(data.table) knitr::opts_chunk$set( diff --git a/vignettes/datatable-reshape.Rmd b/vignettes/datatable-reshape.Rmd index e5efb660f0..51c7fe2f96 100644 --- a/vignettes/datatable-reshape.Rmd +++ b/vignettes/datatable-reshape.Rmd @@ -9,6 +9,10 @@ vignette: > \usepackage[utf8]{inputenc} --- +Translations of this document are available in + +* [French](https://rdatatable.gitlab.io/data.table/articles/fr/datatable-reshape.html) + ```{r, echo = FALSE, message = FALSE} require(data.table) knitr::opts_chunk$set( diff --git a/vignettes/datatable-sd-usage.Rmd b/vignettes/datatable-sd-usage.Rmd index b3c6b58d08..f005b1594e 100644 --- a/vignettes/datatable-sd-usage.Rmd +++ b/vignettes/datatable-sd-usage.Rmd @@ -21,6 +21,10 @@ vignette: > } +Translations of this document are available in + +* [French](https://rdatatable.gitlab.io/data.table/articles/fr/datatable-sd-usage.html) + ```{r, echo = FALSE, message = FALSE} require(data.table) knitr::opts_chunk$set( diff --git a/vignettes/datatable-secondary-indices-and-auto-indexing.Rmd b/vignettes/datatable-secondary-indices-and-auto-indexing.Rmd index ba3ec267e8..85ce0d67c3 100644 --- a/vignettes/datatable-secondary-indices-and-auto-indexing.Rmd +++ b/vignettes/datatable-secondary-indices-and-auto-indexing.Rmd @@ -9,6 +9,10 @@ vignette: > \usepackage[utf8]{inputenc} --- +Translations of this document are available in + +* [French](https://rdatatable.gitlab.io/data.table/articles/fr/datatable-secondary-indices-and-auto-indexing.html) + ```{r, echo = FALSE, message = FALSE} require(data.table) knitr::opts_chunk$set( diff --git a/vignettes/fr/datatable-benchmarking.Rmd b/vignettes/fr/datatable-benchmarking.Rmd new file mode 100644 index 0000000000..0539b155a3 --- /dev/null +++ b/vignettes/fr/datatable-benchmarking.Rmd @@ -0,0 +1,130 @@ +--- +title: "Analyse comparative (benchmark) de data.table" +date: "`r Sys.Date()`" +output: + markdown::html_format: + options: + toc: true + number_sections: true +vignette: > + %\VignetteIndexEntry{Analyse comparative (benchmark) de data.table} + %\VignetteEngine{knitr::knitr} + \usepackage[utf8]{inputenc} +--- + + + +Ce document a pour but de guider la mesure de la performance de `data.table`. Il centralise la documentation des meilleures pratiques et des pièges à éviter. + +# fread : effacer les caches + +Idéalement, chaque appel à `fread` devrait être exécuté dans une nouvelle session avec les commandes suivantes précédant l'exécution de R. Cela permet d'effacer le fichier cache du système d'exploitation en RAM et le cache du disque dur. + +```sh +free -g +sudo sh -c 'echo 3 >/proc/sys/vm/drop_caches' +sudo lshw -class disk +sudo hdparm -t /dev/sda +``` + +Lorsque l'on compare `fread` à des solutions non-R, il faut savoir que R exige que les valeurs des colonnes de caractères soient ajoutées au *cache global de chaînes de caractères de R*. Cela prend du temps lors de la lecture des données, mais les opérations ultérieures en bénéficient puisque les chaînes de caractères ont déjà été mises en cache. Par conséquent, en plus de chronométrer des tâches isolées (comme `fread` seul), c'est une bonne idée d'évaluer le temps total d'un pipeline de traitement de données de bout en bout contenant des tâches telles que la lecture de données, leur manipulation et la production de la sortie finale. + +# sous-ensemble : seuil d'optimisation de l'index pour les requêtes composées + +L'optimisation de l'index pour les requêtes de filtres composés ne sera pas utilisée lorsque le produit croisé des éléments fournis au filtre dépasse 1e4 éléments. + +```r +DT = data.table(V1=1:10, V2=1:10, V3=1:10, V4=1:10) +setindex(DT) +v = c(1L, rep(11L, 9)) +length(v)^4 # produit en croix des éléments du filtre +#[1] 10000 # <= 10000 +DT[V1 %in% v & V2 %in% v & V3 %in% v & V4 %in% v, verbose=TRUE] +#Optimisation du sous-ensemble avec l'index 'V1__V2__V3__V4' +#on= correspond à l'index existant, utilise l'index +#Démarrage de bmerge ...terminé en 0.000sec +#... +v = c(1L, rep(11L, 10)) +length(v)^4 # produit croisé des éléments du filtre +#[1] 14641 # > 10000 +DT[V1 %in% v & V2 %in% v & V3 %in% v & V4 %in% v, verbose=TRUE] +#Optimisation de la substitution désactivée car le produit croisé des valeurs du membre droit dépasse 1e4, ce qui cause des problèmes de mémoire. +#... +``` + +# sous-ensemble : analyse comparative basée sur l'index + +Pour des raisons de commodité, `data.table` construit automatiquement un index sur les champs que vous utilisez pour sous-répertorier les données. Cela ajoutera une certaine surcharge au premier sous-ensemble sur des champs particuliers, mais réduira considérablement le temps d'interrogation de ces colonnes dans les exécutions suivantes. La meilleure façon de mesurer la vitesse est de mesurer séparément la création d'un index et les requêtes utilisant un index. Avec de tels temps, il est facile de décider quelle est la stratégie optimale pour votre cas d'utilisation. Pour contrôler l'utilisation de l'index, employez les options suivantes : + +```r +options(datatable.auto.index=TRUE) +options(datatable.use.index=TRUE) +``` + +- `use.index=FALSE` forcera la requête à ne pas utiliser les index même s'ils existent, mais les clés existantes sont toujours utilisées pour l'optimisation. +- `auto.index=FALSE` désactive la construction automatique d'index lors d'un sous-ensemble sur des données non indexées, mais si les index ont été créés avant que cette option ne soit définie, ou explicitement en appelant `setindex`, ils seront toujours utilisés pour l'optimisation. + +Deux autres options permettent de contrôler l'optimisation de manière globale, y compris l'utilisation d'index : + +```r +options(datatable.optimize=2L) +options(datatable.optimize=3L) +``` + +`options(datatable.optimize=2L)` désactivera complètement l'optimisation des sous-ensembles, tandis que `options(datatable.optimize=3L)` la réactivera. Ces options affectent beaucoup plus d'optimisations et ne devraient donc pas être utilisées lorsque seul le contrôle des index est nécessaire. Plus d'informations dans `?datatable.optimize`. + +# opérations *par référence* + +Lors de l'évaluation des fonctions `set*`, il n'est utile de mesurer que la première exécution. Ces fonctions mettent à jour leur entrée par référence, donc les exécutions suivantes utiliseront le fichier `data.table` déjà traité, ce qui faussera les résultats. + +Protéger votre `data.table` d'une mise à jour par des opérations de référence peut être réalisé en utilisant les fonctions `copy` ou `data.table:::shallow`. Soyez conscient que `copy` peut être très coûteux car il doit dupliquer l'objet entier. Il est peu probable que nous voulions inclure le temps de duplication dans le temps de la tâche réelle que nous benchmarkons. + +# tenter d'étalonner les processus atomiques + +Si votre analyse comparative est destinée à être publiée, elle sera beaucoup plus utile si vous la divisez pour mesurer la durée des processus atomiques. De cette manière, vos lecteurs pourront voir combien de temps a été consacré à la lecture des données à partir de la source, au nettoyage, à la transformation proprement dite et à l'exportation des résultats. Bien sûr, si votre benchmark est destiné à présenter un *flux de travail de bout en bout*, il est tout à fait logique de présenter le temps global. Néanmoins, la séparation des temps des étapes individuelles est utile pour comprendre quelles étapes sont les principaux goulots d'étranglement d'un flux de travail. Il existe d'autres cas où le benchmarking atomique n'est pas souhaitable, par exemple lors de la *lecture d'un csv*, suivie d'un *regroupement*. R nécessite de remplir le *cache global de chaînes de caractères de R*, ce qui ajoute une surcharge supplémentaire lors de l'importation de données de caractères dans une session R. D'un autre côté, le *cache global de chaînes de caractères* peut accélérer des processus tels que le *regroupement*. Dans de tels cas, lorsque l'on compare R à d'autres langages, il peut être utile d'inclure le temps total. + +# éviter la coercition de classe + +Si ce n'est pas ce que vous voulez vraiment mesurer, vous devez préparer des objets d'entrée de la classe attendue pour chaque outil que vous comparez. + +# éviter `microbenchmark(..., times=100)` + +Répéter un benchmark plusieurs fois ne donne généralement pas l'image la plus claire des outils de traitement des données. Bien sûr, c'est parfaitement logique pour les calculs plus atomiques, mais ce n'est pas une bonne représentation de la manière la plus courante dont ces outils seront utilisés, à savoir pour les tâches de traitement des données, qui consistent en des lots de transformations fournies de manière séquentielle, chacune exécutée une fois. Matt a dit un jour : + +> Je me méfie beaucoup des benchmarks qui prennent moins d'une seconde. Je préfère de loin 10 secondes ou plus pour une seule exécution, obtenues en augmentant la taille des données. Un nombre de répétitions de 500 tire la sonnette d'alarme. 3 à 5 exécutions devraient suffire à convaincre sur des données plus importantes. Le coût des appels de fonctions et le temps nécessaire au GC affectent les calculs à une si petite échelle. + +Ceci est tout à fait vrai. Plus la mesure du temps est petite, plus le bruit est important, de manière relative. Le bruit est généré par le dispatching des méthodes, l'initialisation de packages/classes, etc. Le benchmark devrait se concentrer sur des scénarios d'utilisation réelle. + +# traitement multithread + +L'un des principaux facteurs susceptibles d'influer les délais d’exécution est le nombre de threads disponibles dans votre session R. Dans les versions récentes de `data.table`, certaines fonctions sont parallélisées. Vous pouvez contrôler le nombre de threads que vous voulez utiliser avec `setDTthreads`. + +```r +setDTthreads(0) # utilise tous les cœurs disponibles (par défaut) +getDTthreads() # vérifie combien de cœurs sont actuellement utilisés +``` + +# à l'intérieur d'une boucle, préférez `set` au lieu de `:=` + +À moins que vous n'utilisiez l'index en faisant un *sous-affectation par référence*, vous devriez préférer la fonction `set` qui n'impose pas la surcharge de l'appel à la méthode `[.data.table`. + +```r +DT = data.table(a=3:1, b=lettres[1:3]) +setindex(DT, a) + +# for (...) { # imaginez une boucle ici + + DT[a==2L, b := "z"] # sous-affectation par référence, utilise l'index + DT[, d := "z"] # pas de sous-affectation par référence, n'utilise pas l'index et ajoute la surcharge de `[.data.table` + set(DT, j="d", value="z") # pas de surcharge `[.data.table`, mais pas encore d'index, jusqu'à #1196 + +# } +``` + +# à l'intérieur d'une boucle, préférez `setDT` au lieu de `data.table()` + +Pour l'instant, `data.table()` a un surcoût, donc à l'intérieur des boucles, il est préférable d'utiliser `as.data.table()` ou `setDT()` sur une liste valide. diff --git a/vignettes/fr/datatable-faq.Rmd b/vignettes/fr/datatable-faq.Rmd new file mode 100644 index 0000000000..dc82003c64 --- /dev/null +++ b/vignettes/fr/datatable-faq.Rmd @@ -0,0 +1,630 @@ +--- +title: "Foire aux questions de data.table" +date: "`r Sys.Date()`" +output: + markdown::html_format: + options: + toc: true + number_sections: true +vignette: > + %\VignetteIndexEntry{Foire aux questions de data.table} + %\VignetteEngine{knitr::knitr} + \usepackage[utf8]{inputenc} +--- + + + +```{r, echo = FALSE, message = FALSE} +library(data.table) +knitr::opts_chunk$set( + comment = "#", + error = FALSE, + tidy = FALSE, + cache = FALSE, + collapse = TRUE) +.old.th = setDTthreads(1) +``` + +La première section, FAQ pour débutants, est destinée à être lue dans l'ordre, du début à la fin. Elle est simplement rédigée dans le style d'une FAQ afin d'être plus facile à assimiler. Il ne s'agit pas vraiment des questions les plus fréquemment posées. Une meilleure mesure pour cela est de regarder sur Stack Overflow. + +Cette FAQ est une lecture obligatoire et est considérée comme une documentation de base. Ne posez pas de questions sur Stack Overflow ou ne soulevez pas de problèmes (issues) sur GitHub avant de l'avoir lue. Nous savons tous que vous n'avez pas lu la FAQ lorsque vous posez une question. Si vous posez une question sans l'avoir lue, n'utilisez pas votre vrai nom. + +Ce document a été rapidement révisé en fonction des changements apportés à la version 1.9.8 publiée en novembre 2016. N'hésitez pas à soumettre des pull requests pour corriger des erreurs ou faire des améliorations. Si quelqu'un sait pourquoi la table des matières est si étroite et écrasée lorsqu'elle est affichée par le CRAN, merci de nous le faire savoir. Ce document était auparavant un PDF et nous l'avons récemment changé en HTML. + +# FAQ pour les débutants + +## Pourquoi `DT[ , 5]` et `DT[2, 5]` renvoient-ils un data.table à une colonne plutôt que des vecteurs comme `data.frame` ? {#j-num} + +Pour des raisons de cohérence, lorsque vous utilisez data.table dans des fonctions qui acceptent des entrées variables, vous pouvez compter sur `DT[...]` qui renvoie un data.table. Vous n'avez pas à vous souvenir d'inclure `drop=FALSE` comme vous le faites dans data.frame. data.table a été publié pour la première fois en 2006 et cette différence avec data.frame a été une caractéristique depuis le tout début. + +Vous avez peut-être entendu dire qu'il n'est généralement pas judicieux de désigner les colonnes par leur numéro plutôt que par leur nom. Si votre collègue vient à lire votre code plus tard, il devra peut-être chercher à savoir quelle colonne porte le numéro 5. Si vous ou lui changez l'ordre des colonnes plus haut dans votre programme R, vous risquez de produire des résultats erronés sans avertissement ni erreur si vous oubliez de modifier tous les endroits de votre code qui font référence à la colonne numéro 5. C'est votre faute, pas celle de R ou de data.table. C'est vraiment très mauvais. S'il vous plaît, ne le faites pas. C'est le même mantra que celui des développeurs SQL professionnels : ne jamais utiliser `select *`, toujours sélectionner explicitement par le nom de la colonne pour au moins essayer d'être robuste aux changements futurs. + +Disons que la colonne 5 s'appelle "region" et que vous devez vraiment extraire cette colonne en tant que vecteur et non en tant que data.table. Il est plus robuste d'utiliser le nom de la colonne et d'écrire `DT$region` ou `DT["region"]]` ; c'est à dire, la même chose que R de base. Pas lorsqu'ils sont combinés avec `<-` pour assigner (utilisez `:=` à la place pour cela) mais juste pour sélectionner une seule colonne par son nom, ils sont encouragés. + +Il y a des circonstances où se référer à une colonne par un numéro semble être la seule façon, comme une séquence de colonnes. Dans ces situations, tout comme data.frame, vous pouvez écrire `DT[, 5:10]` et `DT[,c(1,4,10)]`. Cependant, encore une fois, il est plus robuste (face à de futurs changements dans le nombre et l'ordre des colonnes de vos données) d'utiliser une plage nommée comme `DT[,columnRed:columnViolet]` ou de nommer chacune `DT[,c("columnRed", "columnOrange", "columnYellow")]`. C'est un travail plus difficile au départ, mais vous vous en féliciterez probablement et vos collègues vous en remercieront peut-être à l'avenir. Au moins, vous pourrez dire que vous avez fait de votre mieux pour écrire un code robuste en cas de problème. + +Cependant, ce que nous voulons vraiment que vous fassiez est `DT[,.(colonneRouge,colonneOrange,colonneJaune)]` ; c'est-à-dire, utiliser les noms de colonnes comme s'ils étaient des variables directement à l'intérieur de `DT[...]`. Vous n'avez pas besoin de préfixer chaque colonne avec `DT$` comme vous le faites dans data.frame. La partie `.()` est juste un alias pour `list()` et vous pouvez utiliser `list()` à la place si vous préférez. Vous pouvez placer n'importe quelle expression R de noms de colonnes, en utilisant n'importe quel package R, retournant différents types de longueurs différentes, juste ici. Nous voulions tellement vous encourager à le faire dans le passé que nous avons délibérément fait en sorte que `DT[,5]` ne fonctionne pas du tout. Avant la version 1.9.8 publiée en novembre 2016, `DT[,5]` retournait simplement `5`. L'idée était d'enseigner plus simplement que les parties à l'intérieur de `DT[...]` sont toujours évaluées dans le cadre de DT (ils voient les noms de colonnes comme s'il s'agissait de variables). Et `5` est évalué à `5`, de sorte que ce comportement est cohérent avec la règle unique. Nous vous avons demandé de passer par un obstacle supplémentaire délibéré `DT[,5,with=FALSE]` si vous vouliez vraiment sélectionner une colonne par nom ou par nombre. A partir de Nov 2016, vous n'aurez plus besoin d'utiliser `with=FALSE` et nous verrons comment une plus grande cohérence avec data.frame à cet égard aidera ou gênera les nouveaux utilisateurs et les utilisateurs de longue date. Les nouveaux utilisateurs qui ne lisent pas cette FAQ, pas même cette toute première entrée, ne trébucheront pas aussi vite avec data.table qu'ils l'ont fait auparavant s'ils s'attendaient à ce qu'il fonctionne comme data.frame. Nous espérons qu'ils ne manqueront pas de comprendre notre intention et notre recommandation de placer les expressions de colonnes à l'intérieur de `DT[i, j, by]`. S'ils utilisent data.table comme data.frame, ils n'en tireront aucun bénéfice. Si vous connaissez quelqu'un dans ce cas, donnez-lui un coup de pouce amical pour qu'il lise ce document comme vous le faites. + +Rappel : vous pouvez placer *n'importe quelle* expression R à l'intérieur de `DT[...]` en utilisant les noms de colonnes comme s'il s'agissait de variables ; par exemple, essayez `DT[, colA*colB/2]`. Cela renvoie un vecteur parce que vous avez utilisé les noms de colonnes comme s'il s'agissait de variables. Enveloppez avec `.()` pour retourner un data.table ; i.e. `DT[,.(colA*colB/2)]`. Nommez-le : `DT[,.(myResult = colA*colB/2)]`. Et nous vous laissons deviner comment retourner deux choses à partir de cette requête. Il est aussi assez courant de faire un tas de choses à l'intérieur d'un corps anonyme : `DT[, { x<-colA+10 ; x*x/2 }]` ou d'appeler une fonction d'un autre package : `DT[ , fitdistr(columnA, "normal")]`. + +## Pourquoi `DT[, "region"]` renvoie-t-il un data.table à une colonne plutôt qu'un vecteur ? + +Voir la [réponse ci-dessus](#j-num). Essayez `DT$region` à la place. Ou `DT[["region"]]`. + +## Pourquoi `DT[, region]` retourne un vecteur pour la colonne "region" ? Je voudrais un data.table à 1 colonne. + +Essayez plutôt `DT[ , .(region)]`. `.()` est un alias de `list()` et assure qu'un data.table est retournée. + +Poursuivez également votre lecture et consultez la FAQ qui suit. Parcourez des documents entiers avant de rester bloqué sur une partie. + +## Pourquoi `DT[ , x, y, z]` ne fonctionne pas ? Je voulais les 3 colonnes `x`,`y` et `z`. + +L'expression `j` est le 2ème argument. Essayez `DT[ , c("x", "y", "z")]` ou `DT[ , .(x,y,z)]`. + +## J'ai assigné une variable `mycol="x"` mais `DT[, mycol]` renvoie une erreur. Comment faire pour qu'il recherche le nom de la colonne contenue dans la variable `mycol` ? + +L'erreur est que la colonne nommée `"mycol"` ne peut pas être trouvée, et cette erreur est correcte. la portée de `data.table` est différente de celle de `data.frame` dans la mesure où vous pouvez utiliser les noms de colonnes comme s'il s'agissait de variables directement à l'intérieur de `DT[...]` sans préfixer chaque nom de colonne par `DT$` ; voir la FAQ 1.1 ci-dessus. + +Pour utiliser `mycol` afin de sélectionner la colonne `x` de `DT`, il y a quelques options : + +```r +DT[, ..mycol] # ... préfixe indique qu'il faut rechercher le mycol un niveau plus haut dans l'appel +DT[, mycol, with=FALSE] # revient au comportement data.frame +DT[[mycol]] # traiter DT comme une liste et utiliser [[ de la base R +``` + +Voir `?data.table` pour plus de détails sur le préfixe `..`. + +L'argument `with` tire son nom de la fonction `base` `with()`. Lorsque `with=TRUE` (par défaut), `data.table` fonctionne de manière similaire à `with()`, c'est-à-dire que `DT[, mycol]` se comporte comme `with(DT, mycol)`. Lorsque `with=FALSE`, les règles d'évaluation standard de `data.frame` s'appliquent à toutes les variables de `j` et vous ne pouvez plus utiliser les noms de colonnes directement. + +## Quels sont les avantages de pouvoir utiliser les noms de colonnes comme s'il s'agissait de variables à l'intérieur de `DT[...]` ? + +`j` n'a pas besoin d'être uniquement un nom de colonne. Vous pouvez écrire n'importe quelle *expression* R de noms de colonnes directement dans `j`, *e.g.*, `DT[ , mean(x*y/z)]`. La même chose s'applique à `i`, *e.g.*, `DT[x>1000, sum(y*z)]`. + +Ceci exécute l'expression `j` sur l'ensemble des lignes où l'expression `i` est vraie. Vous n'avez même pas besoin de renvoyer des données, *e.g.*, `DT[x>1000, plot(y, z)]`. Vous pouvez faire `j` par groupe en ajoutant simplement `by = ` ; par exemple, `DT[x>1000, sum(y*z), by = w]`. Ceci exécute `j` pour chaque groupe dans la colonne `w` mais seulement sur les lignes où `x>1000`. En plaçant les 3 parties de la requête (i=where, j=select et by=group by) à l'intérieur des crochets, data.table voit cette requête comme un tout avant qu'aucune partie ne soit évaluée. Il peut ainsi optimiser les performances de la requête combinée. Il peut le faire parce que le langage R dispose uniquement d'une évaluation paresseuse (ce qui n'est pas le cas de Python et de Julia). data.table voit les expressions à l'intérieur de `DT[...]` avant qu'elles ne soient évaluées et les optimise avant l'évaluation. Par exemple, si data.table voit que vous n'utilisez que 2 colonnes sur 100, il ne s'embêtera pas à sous-sélectionner les 98 qui ne sont pas nécessaires à votre expression j. + +## OK, je commence à comprendre ce qu'est data.table, mais pourquoi n'avez-vous pas simplement amélioré `data.frame` dans R ? Pourquoi faut-il que ce soit un nouveau package ? + +Comme [souligné ci-dessus] (#j-num), `j` dans `[.data.table` est fondamentalement différent de `j` dans `[.data.frame`. Même si quelque chose d'aussi simple que `DF[ , 1]` était modifié dans la base R pour retourner un data.frame plutôt qu'un vecteur, cela casserait le code existant dans des milliers de package CRAN et dans le code utilisateur. Dès que nous avons pris la décision de créer une nouvelle classe héritant de data.frame, nous avons eu l'opportunité de changer certaines choses et nous l'avons fait. Nous voulons que data.table soit légèrement différent et qu'il fonctionne de cette façon pour que la syntaxe plus compliquée fonctionne. Il existe également d'autres différences (voir [ci-dessous](#PetitesDifférences) ). + +De plus, data.table *hérite* de `data.frame`. C'est aussi un `data.frame`. Un data.table peut être passé à n'importe quel package qui n'accepte que `data.frame` et ce package peut utiliser la syntaxe `[.data.frame` sur le data.table. Voir [cette réponse] (https://stackoverflow.com/a/10529888/403310) pour savoir comment procéder. + +Nous avons également proposé des améliorations à R chaque fois que cela était possible. L'une d'entre elles a été acceptée comme nouvelle fonctionnalité dans R 2.12.0 : + +> `unique()` et `match()` sont maintenant plus rapides sur les vecteurs de caractères où tous les éléments sont dans le cache global CHARSXP et ont un encodage non marqué (ASCII). Merci à Matt Dowle pour avoir suggéré des améliorations dans la façon dont le code de hachage est généré dans unique.c. + +Une deuxième proposition était d'utiliser `memcpy` dans duplicate.c, qui est beaucoup plus rapide qu'une boucle for en C. Cela améliorerait la *manière* dont R copie les données en interne (sur certaines mesures, de 13 fois). Le fil de discussion sur r-devel est [ici] (https://stat.ethz.ch/pipermail/r-devel/2010-April/057249.html). + +Une troisième proposition plus significative qui a été acceptée est que R utilise maintenant le code de tri par base (radix sort) de data.table à partir de R 3.3.0 : + +> L'algorithme de tri par base (radix sort) et l'implémentation de data.table (forder) remplace l'ancien tri par base (comptage) et ajoute une nouvelle méthode pour order(). Proposé par Matt Dowle et Arun Srinivasan, le nouvel algorithme supporte les vecteurs de logiques, d’entiers (même avec de grandes valeurs), de réels et de caractères. Il est plus performant que toutes les autres méthodes, mais il y a quelques mises en garde (voir ?sort). + +C'était un grand événement pour nous et nous l'avons fêté jusqu'à ce que les vaches rentrent à la maison. (Pas vraiment.) + +## Pourquoi les valeurs par défaut sont-elles telles qu'elles sont ? Pourquoi le système fonctionne-t-il comme il le fait ? + +La réponse est simple : l'auteur principal l'a conçu à l'origine pour son propre usage. C'est ce qu'il voulait. Il trouve que c'est une façon plus naturelle et plus rapide d'écrire du code, qui s'exécute également plus rapidement. + +## N'est-ce pas déjà fait par `with()` et `subset()` dans `base` ? + +Certaines des caractéristiques discutées jusqu'à présent sont, oui. Le package s'appuie sur la fonctionnalité de base. Il fait le même genre de choses, mais avec moins de code et s'exécute beaucoup plus rapidement s'il est utilisé correctement. + +## Pourquoi `X[Y]` retourne-t-il aussi toutes les colonnes de `Y` ? Ne devrait-elle pas retourner un sous-ensemble de `X` ? + +Cela a été modifié dans la version 1.5.3 (février 2011). Depuis lors, `X[Y]` inclut les colonnes non-jointes de `Y`. Nous nous référons à cette fonctionnalité comme *join inherited scope* parce que non seulement les colonnes `X` sont disponibles pour l'expression `j`, mais les colonnes `Y` le sont aussi. L'inconvénient est que `X[Y]` est moins efficace puisque chaque élément des colonnes non-jointes de `Y` est dupliqué pour correspondre au nombre (probablement grand) de lignes dans `X` qui correspondent. Nous encourageons donc fortement l'utilisation de `X[Y, j]` au lieu de `X[Y]`. Voir [FAQ suivante](#MergeDiff). + +## Quelle est la différence entre `X[Y]` et `merge(X, Y)` ? {#MergeDiff} + +`X[Y]` est une jointure, qui recherche les lignes de `X` en utilisant `Y` (ou la clé de `Y` si elle en a une) comme index. + +`Y[X]` est une jointure, qui recherche les lignes de `Y` en utilisant `X` (ou la clé de `X` si elle en a une) comme index. + +`merge(X,Y)`[^1] fait les deux en même temps. Le nombre de lignes de `X[Y]` et de `Y[X]` est généralement différent, alors que le nombre de lignes retournées par `merge(X, Y)` et `merge(Y, X)` est le même. + +*MAIS* cela ne tient pas compte de l'essentiel. La plupart des tâches exigent que l'on fasse quelque chose sur les données après une jointure ou une fusion. Pourquoi fusionner toutes les colonnes de données pour n'en utiliser qu'un petit sous-ensemble par la suite ? Vous pouvez suggérer `merge(X[ , ColsNeeded1], Y[ , ColsNeed2])`, mais cela demande au programmeur de déterminer quelles colonnes sont nécessaires. `X[Y, j]` dans data.table fait tout cela en une seule étape pour vous. Quand vous écrivez `X[Y, sum(foo*bar)]`, data.table inspecte automatiquement l'expression `j` pour voir quelles colonnes elle utilise. Il ne prend qu’un sous-ensemble des colonnes ; les autres sont ignorées. La mémoire n'est créée que pour les colonnes que `j` utilise et les colonnes `Y` bénéficient des règles de recyclage standard de R dans le contexte de chaque groupe. Disons que `foo` est dans `X` et `bar` est dans `Y` (avec 20 autres colonnes dans `Y`). Est-ce que `X[Y, sum(foo*bar)]` n'est pas plus rapide à programmer et à exécuter qu'une `fusion` de tout ce qui est suivi par un `subset` ? + +[^1]: Il s'agit ici de la méthode `merge` *method* pour data.table ou de la méthode `merge` pour `data.frame` puisque les deux méthodes fonctionnent de la même manière à cet égard. Voir `?merge.data.table` et [below](#r-dispatch) pour plus d'informations sur la répartition des méthodes. + +## Autre chose à propos de `X[Y, sum(foo*bar)]` ? + +Ce comportement a changé dans la version 1.9.4 (septembre 2014). Il fait maintenant la jointure `X[Y]` et exécute ensuite `sum(foo*bar)` sur toutes les lignes ; c'est à dire, `X[Y][ , sum(foo*bar)]`. Il avait l'habitude d'exécuter `j` pour chaque *groupe* de `X` auquel correspondait chaque ligne de `Y`. Cela peut toujours être fait et c'est très utile, mais vous devez maintenant être explicite et spécifier `by = .EACHI`, *c'est-à-dire `X[Y, sum(foo*bar), by = .EACHI]`. C'est ce que nous appelons le *regroupement par chaque `i`*. + +Par exemple, (en le compliquant encore en utilisant *join inherited scope*, aussi) : + +```{r} +X = data.table(grp = c("a", "a", "b", + "b", "b", "c", "c"), foo = 1:7) +setkey(X, grp) +Y = data.table(c("b", "c"), bar = c(4, 2)) +X +Y +X[Y, sum(foo*bar)] +X[Y, sum(foo*bar), by = .EACHI] +``` + +## C'est très bien. Comment avez-vous réussi à le modifier étant donné que les utilisateurs dépendaient de l'ancien comportement ? + +La demande de changement est venue des utilisateurs. Le sentiment était que si une requête fait du groupage, alors un `by=` explicite devrait être présent pour des raisons de lisibilité du code. Une option a été fournie pour retourner l'ancien comportement : `options(datatable.old.bywithoutby)`, par défaut `FALSE`. Cela a permis de tester les autres nouvelles fonctionnalités et corrections de bogues de la version 1.9.4, avec une migration ultérieure des requêtes by-without-by lorsqu'elles sont prêtes en ajoutant `by=.EACHI` à ces requêtes. Nous avons conservé 47 tests antérieurs au changement et les avons ajoutés en tant que nouveaux tests, testés sous `options(datatable.old.bywithoutby=TRUE)`. Nous avons ajouté un message de démarrage à propos du changement et de la façon de revenir à l'ancien comportement. Après 1 an, l'option a été dépréciée avec un avertissement en cas d'utilisation. Après 2 ans, l'option permettant de revenir à l'ancien comportement a été supprimée. + +Sur les 66 packages sur CRAN ou Bioconductor qui dépendaient ou importaient data.table au moment de la publication de la v1.9.4 (il y en a maintenant plus de 300), un seul a été affecté par le changement. Cela peut être dû au fait que de nombreux packages n'ont pas de tests complets, ou simplement au fait que le regroupement par chaque ligne dans `i` n'était pas beaucoup utilisé par les packages en aval. Nous testons toujours la nouvelle version avec tous les packages dépendants avant de la publier et nous coordonnons les changements avec les responsables de ces packages. Cette version a donc été assez simple à cet égard. + +Une autre raison convaincante de faire ce changement est qu'auparavant, il n'y avait pas de moyen efficace de réaliser ce que `X[Y, sum(foo*bar)]` fait maintenant. Il fallait écrire `X[Y][ , sum(foo*bar)]`. C'était sous-optimal parce que `X[Y]` joignait toutes les colonnes et les passait toutes à la seconde requête composée sans savoir que seuls `foo` et `bar` étaient nécessaires. Pour résoudre ce problème d'efficacité, un effort de programmation supplémentaire a été nécessaire : `X[Y, list(foo, bar)][ , sum(foo*bar)]`. Le passage à `by = .EACHI` a simplifié cela en permettant aux deux requêtes d'être exprimées dans une seule requête `DT[...]` pour plus d'efficacité. + +# Syntaxe générale + +## Comment éviter d'écrire une expression `j` très longue ? Vous avez dit que je devrais utiliser les *noms* de colonnes, mais j'ai beaucoup de colonnes. + +Lors du regroupement, l'expression `j` peut utiliser les noms de colonnes comme variables, comme vous le savez, mais elle peut aussi utiliser un symbole réservé `.SD` qui fait référence au **S**ous-ensemble de la **D**ata.table pour chaque groupe (à l'exclusion des colonnes de regroupement). Donc pour résumer toutes vos colonnes, c'est juste `DT[ , lapply(.SD, sum), by = grp]`. Cela peut sembler compliqué, mais c'est rapide à écrire et à exécuter. Notez que vous n'avez pas besoin de créer une fonction anonyme. L'objet `.SD` est efficacement implémenté en interne et plus efficace que de passer un argument à une fonction. Mais si le symbole `.SD` apparaît dans `j` alors data.table doit remplir `.SD` complètement pour chaque groupe même si `j` ne l'utilise pas entièrement. + +Ne faites donc pas, par exemple, `DT[ , sum(.SD[["sales"]]), by = grp]`. Cela fonctionne, mais c'est inefficace et inélégant. `DT[ , sum(sales), by = grp]` est ce qui était prévu, et il pourrait être des centaines de fois plus rapide. Si vous utilisez *toutes* les données de `.SD` pour chaque groupe (comme dans `DT[ , lapply(.SD, sum), by = grp]`) alors c'est une très bonne utilisation de `.SD`. Si vous utilisez *plusieurs* mais pas *toutes* les colonnes, vous pouvez combiner `.SD` avec `.SDcols` ; voir `?data.table`. + +## Pourquoi la valeur par défaut de `mult` est-elle maintenant `"all"` ? + +Dans la version 1.5.3, la valeur par défaut a été changée en "all". Quand `i` (ou la clé de `i` si elle en a une) a moins de colonnes que la clé de `x`, `mult` était déjà mis à `"all"` automatiquement. Changer la valeur par défaut rend la chose plus claire et plus facile pour les utilisateurs, car la question se posait assez souvent. + +Dans les versions antérieures à la v1.3, `"all"` était plus lent. En interne, `"all"` était implémenté en joignant en utilisant `"first"`, puis à nouveau à partir de zéro en utilisant `"last"`, après quoi un diff entre eux était effectué pour calculer l'étendue des correspondances dans `x` pour chaque ligne dans `i`. La plupart du temps, nous effectuons des jointures sur des lignes individuelles, où `"first"`,`"last"` et `"all"` renvoient le même résultat. Nous avons préféré une performance maximale dans la majorité des cas, c'est pourquoi nous avons choisi par défaut `"first"`. Lorsque l'on travaille avec une clé non unique (généralement une colonne unique contenant une variable de regroupement), `DT["A"]` renvoie la première ligne de ce groupe, donc `DT["A", mult = "all"]` est nécessaire pour renvoyer toutes les lignes de ce groupe. + +Dans la version 1.4, la recherche binaire en C a été modifiée pour se brancher au niveau le plus profond afin de trouver le premier et le dernier. Ce branchement se produira probablement dans les mêmes pages finales de RAM, donc il ne devrait plus y avoir de désavantage en termes de vitesse en mettant par défaut `mult` à `"all"`. Nous avons prévenu que la valeur par défaut pourrait changer et nous avons fait le changement dans la version 1.5.3. + +Une future version de data.table pourrait permettre une distinction entre une clé et une *clé unique*. En interne, `mult = "all"` fonctionnerait plus comme `mult = "first"` lorsque toutes les colonnes clés de `x` sont jointes et que la clé de `x` est une clé unique. data.table aurait besoin de vérifications lors de l'insertion et de la mise à jour pour s'assurer qu'une clé unique est maintenue. L'avantage de spécifier une clé unique serait que data.table s'assurerait qu'aucun duplicata ne puisse être inséré, en plus de la performance. + +## J'utilise `c()` dans `j` et j'obtiens des résultats étranges. + +Il s'agit d'une source de confusion fréquente. Dans `data.frame`, vous avez l'habitude de, par exemple : + +```{r} +DF = data.frame(x = 1:3, y = 4:6, z = 7:9) +DF +DF[ , c("y", "z")] +``` + +qui renvoie les deux colonnes. Dans data.table, vous savez que vous pouvez utiliser les noms de colonnes directement et vous pouvez essayer : + +```{r} +DT = data.table(DF) +DT[ , c(y, z)] +``` + +mais il renvoie un vecteur. Rappelez-vous que l'expression `j` est évaluée dans l'environnement de `DT` et que `c()` renvoie un vecteur. Si 2 colonnes ou plus sont nécessaires, utilisez `list()` ou `.()` à la place : + +```{r} +DT[ , .(y, z)] +``` + +`c()` peut également être utile dans un data.table, mais son comportement est différent de celui de `[.data.frame`. + +## J'ai créé un tableau complexe comportant de nombreuses colonnes. Je souhaite l'utiliser comme modèle pour un nouveau tableau ; *c'est-à-dire* créer un nouveau tableau sans lignes, mais avec les noms et les types de colonnes copiés à partir de mon tableau. Est-ce que je peux le faire facilement ? + +Si votre table complexe s'appelle `DT`, essayez `NEWDT = DT[0]`. + +## Un data.table nul est-il identique à `DT[0]` ? + +Non. Par « null.data.table », nous entendons le résultat de `data.table(NULL)` ou de `as.data.table(NULL)` ; *c'est-à-dire *, + +```{r} +data.table(NULL) +data.frame(NULL) +as.data.table(NULL) +as.data.frame(NULL) +is.null(data.table(NULL)) +is.null(data.frame(NULL)) +``` + +Le data.table|`frame` null est `NULL` avec quelques attributs attachés, ce qui signifie qu'il n'est plus `NULL`. Dans R, seul le `NULL` pur est `NULL`, comme testé par `is.null()`. Lorsque l'on se réfère au « nul data.table », on utilise la minuscule null pour faire la différence avec la majuscule `NULL`. Pour tester la nullité d'un data.table, utilisez `length(DT) == 0` ou `ncol(DT) == 0` (`length` est légèrement plus rapide car il s'agit d'une fonction primitive). + +Un data.table *vide* (`DT[0]`) possède une ou plusieurs colonnes, toutes vides. Ces colonnes vides ont toujours des noms et des types. + +```{r} +DT = data.table(a = 1:3, b = c(4, 5, 6), d = c(7L,8L,9L)) +DT[0] +sapply(DT[0], class) +``` + +## Pourquoi l'alias `DT()` a-t-il été supprimé ? {#DTremove1} + +`DT` a été introduit à l'origine comme une enveloppe pour une liste d'expressions `j`. Comme `DT` était un alias de data.table, c'était un moyen pratique de prendre en charge le recyclage silencieux dans les cas où chaque élément de la liste `j` était évalué à des longueurs différentes. L'alias était l'une des raisons pour lesquelles le groupage était lent. + +Depuis la version 1.3, `list()` ou `.()` devraient être passés à la place de l'argument `j`. Ces méthodes sont beaucoup plus rapides, en particulier lorsqu'il y a beaucoup de groupes. En interne, il s'agit d'un changement non trivial. Le recyclage des vecteurs est maintenant effectué en interne, ainsi que plusieurs autres améliorations de la vitesse de groupage. + +## Mais mon code utilise `j = DT(...)` et il fonctionne. La FAQ précédente dit que `DT()` a été supprimé. {#DTremove2} + +Vous utilisez alors une version antérieure à la 1.5.3. Avant la version 1.5.3, `[.data.table` détectait l'utilisation de `DT()` dans le `j` et le remplaçait automatiquement par un appel à `list()`. Ceci avait pour but de faciliter la transition pour les utilisateurs existants. + +## Quelles sont les règles des expressions en `j` ? + +Considérez le sous-ensemble comme un environnement où tous les noms de colonnes sont des variables. Lorsqu'une variable `foo` est utilisée dans le `j` d'une requête telle que `X[Y, sum(foo)]`, `foo` est recherché dans l'ordre suivant : + + 1. La portée du sous-ensemble `X` ; *c'est-à-dire *, les noms de colonnes de `X`. + 2. La portée de chaque ligne de `Y` ; *i.e.*, les noms des colonnes de `Y` (*joint inherited scope*) + 3. La portée du cadre d'appel ; *e.g.*, la ligne qui apparaît avant la requête data.table. + 4. Exercice pour le lecteur : est-ce que cela se répercute ensuite sur les cadres d'appel ou est-ce que cela va directement à `globalenv()` ? + 5. L'environnement global + +Il s'agit d'un *cadrage logique* comme expliqué dans [R FAQ 3.3.1](https://cran.r-project.org/doc/FAQ/R-FAQ.html#Lexical-scoping). L'environnement dans lequel la fonction a été créée n'est pas pertinent, cependant, parce qu'il n'y a *pas de fonction*. Aucune *fonction* anonyme n'est passée à `j`. Au lieu de cela, un *corps* anonyme est passé à `j` ; par exemple, + +```{r} +DT = data.table(x = rep(c("a", "b"), c(2, 3)), y = 1:5) +DT +DT[ , {z = sum(y) ; z + 3}, by = x] +``` + +Certains langages de programmation appellent cela un *lambda*. + +## Puis-je tracer l'expression `j` au fur et à mesure qu'elle passe dans les groupes ? {#j-trace} + +Essayez quelque chose comme ceci : + +```{r} +DT[ , { + cat("Objets :", paste(objects(), collapse = ","), "\n") + cat("Trace : x=", as.character(x), " y=", y, "\n") + sum(y)}, + by = x] +``` + +## À l'intérieur de chaque groupe, pourquoi les variables de groupe sont-elles de longueur 1 ? + +[Above](#j-trace), `x` est une variable de regroupement et (à partir de la version 1.6.1) a une longueur de 1 (si elle est inspectée ou utilisée dans `j`). C'est pour des raisons d'efficacité et de commodité. Par conséquent, il n'y a pas de différence entre les deux déclarations suivantes : + +```{r} +DT[ , .(g = 1, h = 2, i = 3, j = 4, repeatgroupname = x, sum(y)), by = x] +DT[ , .(g = 1, h = 2, i = 3, j = 4, repeatgroupname = x[1], sum(y)), by = x] +``` + +Si vous avez besoin de la taille du groupe actuel, utilisez `.N` plutôt que d'appeler `length()` sur n'importe quelle colonne. + +## Seules les 10 premières lignes sont affichées, comment en afficher d'autres ? + +Il se passe deux choses ici. Premièrement, si le nombre de lignes d'un data.table est important (`> 100` par défaut), alors un résumé du data.table est imprimé sur la console par défaut. Deuxièmement, le résumé d'un grand data.table est imprimé en prenant les `n` (`= 5` par défaut) lignes du haut et du bas du data.table et en n'imprimant que celles-ci. Ces deux paramètres (quand déclencher un résumé et quelle partie du tableau utiliser comme résumé) sont configurables par le mécanisme `options` de R, ou en appelant directement la fonction `print`. + +Par exemple, pour forcer le résumé (summary) d'un data.table à ne se produire que lorsqu'un data.table est supérieur à 50 lignes, vous pourriez faire `options(datatable.print.nrows = 50)`. Pour désactiver complètement le résumé par défaut, vous pourriez faire `options(datatable.print.nrows = Inf)`. Vous pouvez aussi appeler `print` directement, comme dans `print(your.data.table, nrows = Inf)`. + +Si vous voulez afficher plus que les 10 premières (et dernières) lignes d'un tableau de données (disons 20), mettez `options(datatable.print.topn = 20)`, par exemple. Encore une fois, vous pouvez aussi appeler directement `print`, comme dans `print(your.data.table, topn = 20)`. + +## Avec une jointure `X[Y]`, que se passe-t-il si `X` contient une colonne appelée `"Y"` ? + +Lorsque `i` est un nom unique tel que `Y`, il est évalué dans d'appel. Dans tous les autres cas, comme les appels à `.()` ou d'autres expressions, `i` est évalué dans la portée de `X`. Cela facilite les *auto-joints* comme `X[J(unique(colA)), mult = "first"]`. + +## `X[Z[Y]]` échoue parce que `X` contient une colonne `"Y"`. J'aimerais qu'il utilise la table `Y` dans l’appel. + +La partie `Z[Y]` n'est pas un nom unique, elle est donc évaluée dans le cadre de `X` et le problème se produit. Essayez `tmp = Z[Y] ; X[tmp]`. Ceci est robuste à `X` contenant une colonne `"tmp"` parce que `tmp` est un nom unique. Si vous rencontrez souvent des conflits de ce type, une solution simple peut être de nommer toutes les tables en majuscules et tous les noms de colonnes en minuscules, ou un schéma similaire. + +## Pouvez-vous nous expliquer pourquoi data.table s'inspire de la syntaxe `A[B]` de `base` ? + +Considérons la syntaxe `A[B]` en utilisant un exemple de matrice `A` : + +```{r} +A = matrix(1:12, nrow = 4) +A +``` + +Pour obtenir les cellules `(1, 2) = 5` et `(3, 3) = 11`, de nombreux utilisateurs (nous pensons) peuvent d'abord essayer ceci : + +```{r} +A[c(1, 3), c(2, 3)] +``` + +Cependant, cette méthode renvoie l'union de ces lignes et de ces colonnes. Pour référencer les cellules, une matrice à 2 colonnes est nécessaire. `?Extract` dit : + +> Lors de l'indexation des tableaux par `[`, un seul argument `i` peut être une matrice avec autant de colonnes qu'il y a de dimensions de `x` ; le résultat est alors un vecteur avec des éléments correspondant aux ensembles d'indices dans chaque ligne de `i`. + +Essayons encore une fois. + +```{r} +B = cbind(c(1, 3), c(2, 3)) +B +A[B] +``` + +Une matrice est une structure à 2 dimensions avec des noms de lignes et de colonnes. Peut-on faire la même chose avec les noms ? + +```{r} +rownames(A) = letters[1:4] +colnames(A) = LETTERS[1:3] +A +B = cbind(c("a", "c"), c("B", "C")) +A[B] +``` + +Donc oui, nous pouvons le faire. Peut-on faire la même chose avec un `data.frame` ? + +```{r} +A = data.frame(A = 1:4, B = letters[11:14], C = pi*1:4) +rownames(A) = letters[1:4] +A +B +A[B] +``` + +Mais, remarquez que le résultat a été forcé en `character.` R a forcé `A` en `matrix` d'abord pour que la syntaxe puisse fonctionner, mais le résultat n'est pas idéal. Essayons de faire de `B` un `data.frame`. + +```{r} +B = data.frame(c("a", "c"), c("B", "C")) +cat(try(A[B], silent = TRUE)) +``` + +Nous ne pouvons donc pas extraire un sous-ensemble d’un `data.frame` par un `data.frame` dans la base R. Que faire si nous voulons des noms de lignes et de colonnes qui ne sont pas des `caractères` mais des `integer` ou des `float` ? Que faire si nous voulons plus de 2 dimensions de types mixtes ? Entrez dans data.table. + +En outre, les matrices, en particulier les matrices creuses, sont souvent stockées dans un tuple à trois colonnes : (i, j, valeur)`. Cela peut être considéré comme une paire clé-valeur où `i` et `j` forment une clé à 2 colonnes. Si nous avons plus d'une valeur, peut-être de types différents, cela peut ressembler à `(i, j, val1, val2, val3, ...)`. Cela ressemble beaucoup à un `data.frame`. C'est pourquoi data.table étend `data.frame` de sorte qu'un `data.frame` `X` puisse extraire un sous-ensemble d’un `data.frame` `Y`, ce qui conduit à la syntaxe `X[Y]`. + +## Est-il possible de modifier le package base pour faire cela, plutôt que de créer un nouveau package ? + +`data.frame` est utilisé *partout* et il est donc très difficile d'y apporter un quelconque changement. data.table *hérite* de `data.frame`. C'est aussi un `data.frame`. Un data.table *peut* être passé à n'importe quel package qui *seulement* accepte `data.frame`. Quand ce package utilise la syntaxe `[.data.frame` sur le data.table, cela fonctionne. Cela fonctionne parce que `[.data.table` regarde d'où il a été appelé. S'il a été appelé à partir d'un tel package, `[.data.table` se dirige vers `[.data.frame`. + +## J'ai entendu dire que la syntaxe de data.table était analogue à celle de SQL. + +Oui : + + - `i` $\Leftrightarrow$ where + - `j` $\Leftrightarrow$ select + - ` :=` $\Leftrightarrow$ update + - `by` $\Leftrightarrow$ group by + - `i` $\Leftrightarrow$ order by (en syntaxe composée) + - `i` $\Leftrightarrow$ having (en syntaxe composée) + - `nomatch = NA` $\Leftrightarrow$ outer join + - `nomatch = NULL` $\Leftrightarrow$ inner join + - `mult = "first"|"last"` $\Leftrightarrow$ N/A parce que SQL est intrinsèquement non ordonné + - `roll = TRUE` $\Leftrightarrow$ N/A parce que SQL est intrinsèquement non ordonné + +La forme générale est la suivante : + +```r +DT[where, select|update, group by][order by][...] ... [...] +``` + +L'un des principaux avantages des vecteurs colonnes dans R est qu'ils sont *ordonnés*, contrairement à SQL[^2]. Nous pouvons utiliser des fonctions ordonnées dans les requêtes `data.table` telles que `diff()` et nous pouvons utiliser *n'importe quelle* fonction R de n'importe quel package, pas seulement les fonctions qui sont définies dans SQL. L'inconvénient est que les objets R doivent tenir dans la mémoire, mais avec plusieurs packages R tels que `ff`, `bigmemory`, `mmap` et `indexing`, cela est en train de changer. + +[^2]: Il peut être surprenant d'apprendre que `select top 10 * from ...` ne renvoie pas de manière fiable les mêmes lignes dans le temps en SQL. Vous devez inclure une clause `order by`, ou utiliser un index en grappe pour garantir l'ordre des lignes ; *i.e.*, SQL est intrinsèquement non ordonné. + +## Quelles sont les petites différences de syntaxe entre `data.frame` et data.table {#SmallerDiffs} + + - `DT[3]` fait référence à la 3ème *lignes*, mais `DF[3]` fait référence à la 3ème *colonne* + - `DT[3, ] == DT[3]`, mais `DF[ , 3] == DF[3]` (un peu déroutant dans data.frame, alors que data.table est cohérent) + - Pour cette raison, nous disons que la virgule est *optionnelle* dans `DT`, mais pas optionnelle dans `DF` + - `DT[[3]] == DF[, 3] == DF[[3]]` + - `DT[i, ]`, où `i` est un seul entier, renvoie une seule ligne, tout comme `DF[i, ]`, mais contrairement à un sous-ensemble de matrice à une seule ligne qui renvoie un vecteur. + - `DT[ , j]` où `j` est un entier renvoie un data.table à une colonne, contrairement à `DF[, j]` qui renvoie un vecteur par défaut + - `DT[ , "colA"][[1]] == DF[ , "colA"]`. + - `DT[ , colA] == DF[ , "colA"]` (actuellement dans data.table v1.9.8 mais est sur le point de changer, voir les notes de version) + - `DT[ , list(colA)] == DF[ , "colA", drop = FALSE]` + - `DT[NA]` renvoie 1 ligne de `NA`, mais `DF[NA]` renvoie une copie entière de `DF` contenant `NA` tout au long. Le symbole `NA` est de type `logique` dans R et est donc recyclé par `[.data.frame`. L'intention de l'utilisateur était probablement `DF[NA_integer_]`. par commodité, `[.data.table' se réoriente automatiquement vers cette intention probable. + - `DT[c(TRUE, NA, FALSE)]` traite le `NA` comme `FALSE`, mais `DF[c(TRUE, NA, FALSE)]` renvoie===== lignes `NA` pour chaque `NA` ===== - `DT[ColA == ColB]` est plus simple que `DF[!is.na(ColA) & !is.na(ColB) & ColA == ColB, ]` + - `data.frame(list(1:2, "k", 1:4))` crée 3 colonnes, data.table crée une colonne `list`. + - `check.names` est par défaut `TRUE` dans `data.frame` mais `FALSE` dans data.table, par commodité. + - `data.table` a toujours mis `stringsAsFactors=FALSE` par défaut. Dans R 4.0.0 (Apr 2020), la valeur par défaut de `data.frame` a été changée de `TRUE` à `FALSE` et il n'y a plus de différence à cet égard ; voir [stringsAsFactors, Kurt Hornik, Feb 2020](https://developer.r-project.org/Blog/public/2020/02/16/stringsasfactors/). + - Les vecteurs atomiques dans les colonnes de `list` sont réduits lorsqu'ils sont imprimés en utilisant `", "` dans `data.frame`, mais `","` dans data.table avec une virgule après le 6ème élément pour éviter l'impression accidentelle de gros objets intégrés. + - Contrairement aux data.frames, un data.table ne peut pas stocker des lignes sans colonnes, car les lignes sont considérées comme les enfants des colonnes : `nrow(DF[, 0])` renvoie le nombre de lignes, tandis que `nrow(DT[, 0])` renvoie toujours 0 ; mais voir le numéro [#2422](https://github.com/Rdatatable/data.table/issues/2422). + +Dans `[.data.frame`, nous mettons très souvent `drop = FALSE`. Lorsque nous l'oublions, des bogues peuvent apparaître dans les cas où une seule colonne est sélectionnée et où, tout à coup, un vecteur est retourné au lieu d'un `data.frame` à une seule colonne. Dans `[.data.table`, nous avons saisi l'opportunité de rendre les choses plus cohérentes et nous avons supprimé `drop`. + +Lorsqu'un data.table est transmis à un package ne prenant pas en compte data.table, ce package ne se préoccupe pas de ces différences ; il fonctionne simplement. + +## J'utilise `j` pour son effet secondaire uniquement, mais je reçois toujours des données en retour. Comment arrêter cela ? + +Dans ce cas, `j` peut être entouré de `invisible()` ; par exemple, `DT[ , invisible(hist(colB)), by = colA]`[^3] + +[^3]: *e.g.*, `hist()` renvoie les points d'arrêt en plus du tracé sur le périphérique graphique. + +## Pourquoi `[.data.table` a maintenant un argument `drop` depuis la version v1.5 ? + +Ainsi, data.table peut hériter de `data.frame` sans utiliser `...`. Si nous utilisions `...`, les noms d'arguments invalides ne seraient pas détectés. + +L'argument `drop` n'est jamais utilisé par `[.data.table`. C'est un substitut pour les packages non compatibles avec data.table lorsqu'ils utilisent la syntaxe `[.data.frame` directement sur un data.table. + +## Les jonctions par roulement sont cool et très rapides ! C'était difficile à programmer ? + +La ligne dominante sur ou avant la ligne `i` est la ligne finale que la recherche binaire teste de toute façon. Donc `roll = TRUE` est essentiellement un interrupteur dans le code C de la recherche binaire pour retourner cette ligne. + +## Pourquoi `DT[i, col := valeur]` retourne-t-il la totalité de `DT` ? Je m'attendais à ce qu'il n'y ait pas de valeur visible (ce qui est cohérent avec `<-`), ou à ce qu'il y ait un message ou une valeur de retour contenant le nombre de lignes mises à jour. Il n'est pas évident que les données aient été mises à jour par référence. + +Ceci a été modifié dans la version 1.8.3 pour répondre à vos attentes. Veuillez mettre à jour. + +L'ensemble de `DT` est retourné (maintenant de manière invisible) pour que la syntaxe composée puisse fonctionner ; *e.g.*, `DT[i, done := TRUE][ , sum(done)]`. Le nombre de lignes mises à jour est retourné quand `verbose` est `TRUE`, soit sur une base par requête, soit globalement en utilisant `options(datatable.verbose = TRUE)`. + +## D'accord, merci. Qu'y a-t-il de si difficile dans le fait que le résultat de `DT[i, col := value]` soit renvoyé de façon invisible ? + +R force en interne la visibilité pour `[`. La valeur de la colonne eval de FunTab (voir [src/main/names.c](https://github.com/wch/r-source/blob/trunk/src/main/names.c)) pour `[` est `0` ce qui signifie "force `R_Visible` on" (voir [R-Internals section 1.6](https://cran.r-project.org/doc/manuals/r-release/R-ints.html#Autoprinting) ). Par conséquent, lorsque nous avons essayé `invisible()` ou de mettre `R_Visible` à `0` directement nous-mêmes, `eval` dans [src/main/eval.c](https://github.com/wch/r-source/blob/trunk/src/main/eval.c) l'a forcé à nouveau. + +Pour résoudre ce problème, la clé était de ne plus essayer d'arrêter l'exécution de la méthode print après un `:=`. Au lieu de cela, à l'intérieur de `:=` nous mettons maintenant (à partir de la version 1.8.3) un drapeau global que la méthode print utilise pour savoir si elle doit imprimer ou non. + +## Pourquoi dois-je taper `DT` parfois deux fois après avoir utilisé `:=` pour imprimer le résultat dans la console ? + +C'est un inconvénient malheureux pour faire fonctionner [#869](https://github.com/Rdatatable/data.table/issues/869). Si un `:=` est utilisé à l'intérieur d'une fonction sans `DT[]` avant la fin de la fonction, alors la prochaine fois que `DT` est tapé à l'invite, rien ne sera affiché. Un `DT` répété sera affiché. Pour éviter cela : incluez un `DT[]` après le dernier `:=` dans votre fonction. Si ce n'est pas possible (par exemple, ce n'est pas une fonction que vous pouvez changer), alors `print(DT)` et `DT[]` à l'invite sont garantis de s’afficher. Comme précédemment, l'ajout d'un `[]` supplémentaire à la fin de la requête `:=` est un idiome recommandé pour mettre à jour et ensuite imprimer ; e.g.> `DT[,foo:=3L][]`. + +## J'ai remarqué que `base::cbind.data.frame` (et `base::rbind.data.frame`) semble être modifié par data.table. Comment cela est-il possible ? Pourquoi ? + +C'était une solution temporaire de dernier recours avant que le dispatching des méthodes S3 de rbind et cbind ne soit corrigé dans R >= 4.0.0. Essentiellement, le problème était que `data.table` hérite de `data.frame`, *et* `base::cbind` et `base::rbind` (uniquement) font leur propre dispatching S3 en interne comme documenté par `?cbind`. La solution pour `data.table` était d'ajouter une boucle `for` au début de chaque fonction directement dans `base`. Cette modification était faite dynamiquement, *c'est-à-dire* que la définition `base` de `cbind.data.frame` était récupérée, la boucle `for` ajoutée au début, et ensuite réassignée à `base`. Cette solution a été conçue pour être robuste aux différentes définitions de `base::cbind.data.frame` dans les différentes versions de R, y compris les changements futurs inconnus. Elle a bien fonctionné. Les exigences concurrentes étaient les suivantes : + + - `cbind(DT, DF)` doit fonctionner. Définir `cbind.data.table` ne fonctionnait pas parce que `base::cbind` fait sa propre distribution S3 et requiert (avant R 4.0.0) que la *première* méthode `cbind` pour chaque objet qui lui est passé soit *identique*. Ce n'est pas vrai dans `cbind(DT, DF)` parce que la première méthode pour `DT` est `cbind.data.table` mais la première méthode pour `DF` est `cbind.data.frame`. `base::cbind` passe alors à son code `bind` interne qui semble traiter `DT` comme une `liste` normale et renvoie une sortie `matrix` très bizarre et inutilisable. Voir [ci-dessous](#cbinderror). Nous ne pouvons pas simplement conseiller aux utilisateurs de ne pas appeler `cbind(DT, DF)` parce que des packages comme `ggplot2` font un tel appel ([test 167.2](https://github.com/Rdatatable/data.table/blob/master/inst/tests/tests.Rraw#L444-L447)). + + - Cela a naturellement conduit à essayer de masquer `cbind.data.frame` à la place. Puisqu'une data.table est un `data.frame`, `cbind` trouverait la même méthode pour `DT` et `DF`. Cependant, cela n'a pas fonctionné non plus parce que `base::cbind` semble trouver les méthodes dans `base` en premier ; *i.e.*, `base::cbind.data.frame` n'est pas masquable. + + - Finalement, nous avons essayé de masquer `cbind` lui-même (v1.6.5 et v1.6.6). Cela a permis à `cbind(DT, DF)` de fonctionner, mais a introduit des problèmes de compatibilité avec le package `IRanges`, puisque `IRanges` masque aussi `cbind`. Cela fonctionnait si `IRanges` était plus bas dans le chemin `search()` que data.table, mais si `IRanges` était plus haut que data.table, `cbind` n'était jamais appelé et l'étrange sortie `matrix` se produisait à nouveau (voir [ci-dessous](#cbinderror)). + +Un grand merci à l'équipe de base de R pour avoir résolu le problème en septembre 2019. data.table v1.12.6+ n'applique plus la solution de contournement dans R >= 4.0.0. + +## J'ai lu des articles sur la répartition des méthodes (*e.g.* `merge` peut ou non être réparti dans `merge.data.table`) mais *comment* R sait-il comment répartir ? Les points sont-ils significatifs ou spéciaux ? Comment diable R sait-il quelle fonction doit être distribuée et à quel moment ? {#r-dispatch} + +On en parle souvent, mais c'est d'une simplicité déconcertante. Une fonction telle que `merge` est *générique* si elle consiste en un appel à `UseMethod`. Quand vous voyez des gens parler de la question de savoir si les fonctions sont *génériques* ou non, ils tapent simplement la fonction sans `()` après, regardent le code du programme à l'intérieur et s'ils voient un appel à `UseMethod` alors c'est *générique*. Que fait `UseMethod` ? Elle colle littéralement le nom de la fonction avec la classe du premier argument, séparés par un point (`.`) et appelle ensuite cette fonction, en lui passant les mêmes arguments. C'est aussi simple que cela. Par exemple, `merge(X, Y)` contient un appel à `UseMethod`, ce qui signifie qu'il *dispatche* (c'est-à-dire appelle) `paste("merge", class(X), sep = ".")`. Les fonctions avec des points dans leur nom peuvent ou non être des méthodes. Le point n'est pas vraiment pertinent, autre que le point est le séparateur utilisé par `UseMethod`. Connaître ce contexte devrait maintenant permettre de comprendre pourquoi, par exemple, il est évident pour les utilisateurs de R que `as.data.table.data.frame` est la méthode `data.frame` pour la fonction générique `as.data.table`. De plus, il peut être utile d'élucider que, oui, vous avez raison, il n'est pas évident à partir de son seul nom que `ls.fit` n'est pas la méthode fit de la fonction générique `ls`. Vous ne le savez qu'en tapant `ls` (pas `ls()`) et en observant qu'il n'y a pas un seul appel à `UseMethod`. + +Vous pouvez maintenant vous demander : où cela est-il documenté dans R ? Réponse : c'est assez clair, mais vous devez d'abord savoir qu'il faut chercher dans `?UseMethod` et *ce* fichier d'aide contient : + +> Lorsqu'une fonction appelant `UseMethod('fun')` est appliquée à un objet avec l'attribut de classe `c('first', 'second')`, le système recherche une fonction appelée `fun.first` et, s'il la trouve, l'applique à l'objet. Si aucune fonction de ce type n'est trouvée, une fonction appelée `fun.second` est essayée. Si aucun nom de classe ne produit une fonction appropriée, la fonction `fun.default` est utilisée, si elle existe, ou une erreur se produit. + +Heureusement, une recherche internet sur "How does R method dispatch work" (à l'heure où j'écris ces lignes) renvoie la page d'aide `?UseMethod` dans les premiers liens. Certes, les autres liens sortent rapidement dans les subtilités de S3 vs S4, les génériques internes et ainsi de suite. + +Cependant, des fonctionnalités telles que l'envoi S3 de base (coller le nom de la fonction avec le nom de la classe) sont la raison pour laquelle certains adeptes de R aiment R. C'est tellement simple. Aucune inscription ou signature compliquée n'est requise. Il n'y a pas grand chose à apprendre. Pour créer la méthode `merge` pour data.table, tout ce qui était nécessaire, littéralement, était de créer une fonction appelée `merge.data.table`. + +# Questions relatives au temps de calcul + +## J'ai 20 colonnes et un grand nombre de lignes. Pourquoi l'expression d'une colonne est-elle si rapide ? + +Plusieurs raisons à cela : + + - Seule cette colonne est groupée, les 19 autres sont ignorées parce que data.table inspecte l'expression `j` et réalise qu'elle n'utilise pas les autres colonnes. + - Une allocation de mémoire est faite pour le plus grand groupe seulement, puis cette mémoire est réutilisée pour les autres groupes. Il y a très peu de déchets à collecter. + - R est un magasin de colonnes en mémoire, c'est-à-dire que les colonnes sont contiguës dans la RAM. Les extractions de pages de la RAM vers la mémoire cache L2 sont réduites au minimum. + +## Je n'ai pas de `key` sur une grande table, mais le regroupement est toujours très rapide. Comment cela se fait-il ? + +data.table utilise le tri par base (radix sort). Il est nettement plus rapide que les autres algorithmes de tri. Voir [nos présentations](https://github.com/Rdatatable/data.table/wiki/Presentations) pour plus d'informations, en particulier sur useR!2015 Danemark. + +C'est aussi l'une des raisons pour lesquelles `setkey()` est rapide. + +Lorsqu'aucune "clé" (‘key’) n'est définie, ou que l'on regroupe dans un ordre différent de celui de la clé, on parle d'un "by" *ad hoc. + +## Pourquoi le regroupement par colonnes dans la clé est-il plus rapide qu'un *ad hoc* `by` ? + +Parce que chaque groupe est contigu en RAM, ce qui minimise les recherches de pages et que la mémoire peut être copiée en masse (`memcpy` en C) plutôt qu'en boucle en C. + +## Que sont les indices primaires et secondaires dans data.table ? + +Manuel : [`?setkey`](https://www.rdocumentation.org/packages/data.table/functions/setkey) S.O. : [Quel est l'intérêt de définir une clé dans data.table ?](https://stackoverflow.com/questions/20039335/what-is-the-purpose-of-setting-a-key-in-data-table/20057411#20057411) + +`setkey(DT, col1, col2)` ordonne les lignes par la colonne `col1` puis à l'intérieur de chaque groupe de `col1` il ordonne par `col2`. Il s'agit d'un *index primaire*. L'ordre des lignes est modifié *par référence* en RAM. Les jointures et les groupes ultérieurs sur ces colonnes clés profitent alors de l'ordre de tri pour plus d'efficacité. (Imaginez à quel point la recherche d'un numéro de téléphone dans un annuaire imprimé serait difficile s'il n'était pas trié par nom puis par prénom. C'est littéralement tout ce que fait `setkey`. Il trie les lignes en fonction des colonnes que vous spécifiez) L'index n'utilise pas de RAM. Il change simplement l'ordre des lignes en RAM et marque les colonnes clés. Analogue à un *index groupé* en SQL. + +Cependant, vous ne pouvez avoir qu'une seule clé primaire car les données ne peuvent être triées physiquement dans la mémoire vive que d'une seule manière à la fois. Choisissez l'index primaire comme étant celui que vous utilisez le plus souvent (par exemple `[id,date]`). Parfois, il n'y a pas de choix évident pour la clé primaire ou vous devez joindre et grouper de nombreuses colonnes différentes dans des ordres différents. Entrez un index secondaire. Celui-ci utilise de la mémoire (`4*nrow` bytes indépendamment du nombre de colonnes dans l'index) pour stocker l'ordre des lignes selon les colonnes que vous spécifiez, mais ne réordonne pas réellement les lignes en RAM. Les jointures et les groupes ultérieurs profitent de l'ordre de la clé secondaire mais doivent *sauter* via cet index et ne sont donc pas aussi efficaces que les index primaires. Ils sont donc moins efficaces que les index primaires. Mais ils sont tout de même beaucoup plus rapides qu'un balayage vectoriel complet. Il n'y a pas de limite au nombre d'index secondaires puisque chacun d'entre eux est simplement un vecteur d'ordre différent. En général, il n'est pas nécessaire de créer des index secondaires. Ils sont créés automatiquement et utilisés pour vous automatiquement en utilisant data.table normalement ; *e.g.* `DT[someCol == someVal, ]` et `DT[someCol %in% someVals, ]` créeront, attacheront et utiliseront ensuite l'index secondaire. Ceci est plus rapide dans data.table qu'un balayage vectoriel, donc l'indexation automatique est activée par défaut puisqu'il n'y a pas de pénalité initiale. Il existe une option pour désactiver l'indexation automatique ; *e.g.*, si beaucoup d'index sont créés et que même la quantité relativement faible de mémoire supplémentaire devient trop importante. + +Nous utilisons les mots *index* et *key* de manière interchangeable. + +# Messages d'erreur + +## « Impossible de trouver la fonction `DT` » + +Voir ci-dessus [ici](#DTremove1) et [ici](#DTremove2). + +## « argument(s) non utilisé(s) (`MySum = sum(v)`) » + +Cette erreur est générée par `DT[ , MySum = sum(v)]`. `DT[ , .(MySum = sum(v))]` était prévu, ou `DT[ , j = .(MySum = sum(v))]`. + +## "`translateCharUTF8` doit être appelé sur un `CHARSXP`" + +Cette erreur (et d'autres similaires, *e.g.*, "`getCharCE` must be called on a `CHARSXP`") peut n'avoir rien à voir avec les données de caractères ou la locale. Au lieu de cela, cela peut être le symptôme d'une corruption de mémoire antérieure. Jusqu'à présent, ces problèmes ont pu être reproduits et corrigés (rapidement). Merci de le signaler sur notre [gestionnaire de tickets (issues tracker)](https://github.com/Rdatatable/data.table/issues). + +## `cbind(DT, DF)` renvoie un format étrange, *e.g.* `Integer,5` {#cbinderror} + +Cela se produit avant la version 1.6.5, pour `rbind(DT, DF)` également. Veuillez mettre à jour vers la version 1.6.7 ou une version ultérieure. + +## « Impossible de modifier la valeur d'une liaison verrouillée pour `.SD` » + +`.SD` est verrouillé par conception. Voir `?data.table`. Si vous voulez manipuler `.SD` avant de l'utiliser ou de le retourner, et que vous ne souhaitez pas modifier `DT` en utilisant `:=`, prenez d'abord une copie (voir `?copy`), *e.g.*, + +```{r} +DT = data.table(a = rep(1:3, 1:3), b = 1:6, c = 7:12) +DT +DT[ , { mySD = copy(.SD) + mySD[1, b := 99L] + mySD}, + by = a] +``` + +## « Impossible de modifier la valeur d'une liaison verrouillée pour `.N` » + +Veuillez mettre à jour vers la version 1.8.1 ou plus récente. A partir de cette version, si `.N` est retourné par `j`, il est renommé en `N` pour éviter toute ambiguïté dans un regroupement ultérieur entre la variable spéciale `.N` et une colonne appelée `".N"`. + +L'ancien comportement peut être reproduit en forçant `.N` à s'appeler `.N`, comme ceci : + +```{r} +DT = data.table(a = c(1,1,2,2,2), b = c(1,2,2,2,1)) +DT +DT[ , list(.N = .N), list(a, b)] # montrer le résultat intermédiaire pour l'exposition +cat(try( + DT[ , list(.N = .N), by = list(a, b)][ , unique(.N), by = a] # composer une requête plus typique +, silent = TRUE)) +``` + +Si vous utilisez déjà la version 1.8.1 ou une version ultérieure, le message d'erreur est plus utile que l'erreur « Impossible de modifier la valeur d’une liaison verrouillée », comme vous pouvez le voir ci-dessus, puisque cette vignette a été produite avec la version 1.8.1 ou une version ultérieure. + +La syntaxe plus naturelle fonctionne désormais : + +```{r} +if (packageVersion("data.table") >= "1.8.1") { + DT[ , .N, by = list(a, b)][ , unique(N), by = a] + } +if (packageVersion("data.table") >= "1.9.3") { + DT[ , .N, by = .(a, b)][ , unique(N), by = a] # same +} +``` + +# Messages d'avertissement + +## « Le(s) objet(s) suivant(s) est/sont masqué(s) dans `package:base` : `cbind`, `rbind` » + +Cet avertissement était présent dans les versions 1.6.5 et 1.6.6 uniquement, lors du chargement du package. La motivation était de permettre à `cbind(DT, DF)` de fonctionner, mais il s'est avéré que cela rompait la compatibilité (totale) avec le package `IRanges`. Veuillez mettre à jour vers la version 1.6.7 ou une version ultérieure. + +## « Coercition numérique du membre de droite (RHS) en entier pour correspondre au type de la colonne » + +J'espère que ce message s'explique de lui-même. Le message complet est le suivant : + +RHS numérique forcé en entier pour correspondre au type de la colonne ; peut avoir une précision tronquée. Vous pouvez soit changer la colonne en numérique en créant vous-même un nouveau tableau numérique de longueur 5 (nrows du tableau entier) et en l'assignant (c.-à-d. colonne "replace"), soit forcer vous-même le RHS en entier (par ex. 1L ou as.integer) pour que votre intention soit claire (et pour plus de rapidité). Ou encore, définissez correctement le type de colonne dès la création de la table et respectez-le, s'il vous plaît. + +Pour le générer, essayez : + +```{r} +DT = data.table(a = 1:5, b = 1:5) +suppressWarnings( +DT[2, b := 6] # fonctionne (plus lentement) avec l'avertissement +) +class(6) # numérique pas entier +DT[2, b := 7L] # fonctionne (plus rapidement) sans avertissement +class(7L) # L en fait un entier +DT[ , b := rnorm(5)] # « remplace » la colonne entière par une colonne numérique +``` + +## Lecture de data.table à partir d'un fichier RDS ou RData + +`*.RDS` et `*.RData` sont des types de fichiers qui permettent de stocker efficacement des objets R en mémoire sur le disque. Cependant, le stockage de data.table dans le fichier binaire perd sa sur-allocation de colonnes. Ce n'est pas très grave -- votre data.table sera copié en mémoire lors de la prochaine opération *par référence* et lancera un avertissement. Il est donc recommandé d'appeler `setalloccol()` sur chaque data.table chargée avec les appels `readRDS()` ou `load()`. + +# Questions générales sur le package + +## la version v1.3 semble être absente de l'archive CRAN ? + +C'est exact. La version 1.3 n'était disponible que sur R-Forge. Il y a eu plusieurs changements importants en interne et il a fallu du temps pour les tester en développement. + +## Data.table est-il compatible avec S-plus ? + +Pas actuellement. + + - Quelques parties essentielles du package sont écrites en C et utilisent des fonctions R internes et des structures R. + - Le package utilise le cadrage lexical qui est l'une des différences entre R et **S-plus** expliquée par [R FAQ 3.3.1](https://cran.r-project.org/doc/FAQ/R-FAQ.html#Lexical-scoping) + +## Est-il disponible pour Linux, Mac et Windows ? + +Oui, à la fois pour 32-bit et 64-bit sur toutes les plateformes. Merci au CRAN. Aucune bibliothèque spéciale ou spécifique au système d'exploitation n'est utilisée. + +## Je pense que c'est très bien. Qu'est-ce que je peux faire ? + +Veuillez déposer des suggestions, des rapports de bogues et des demandes d'amélioration sur notre [gestionnaire de tickets (issues tracker)](https://github.com/Rdatatable/data.table/issues). Cela permet d'améliorer le package. + +Merci d'ajouter le package sur [GitHub](https://github.com/Rdatatable/data.table/wiki). Cela permet d'encourager les développeurs et d'aider les autres utilisateurs de R à trouver le package. + +Vous pouvez soumettre des demandes d'extraction pour modifier le code et/ou la documentation vous-même ; voir nos [Directives de contribution](https://github.com/Rdatatable/data.table/blob/master/.github/CONTRIBUTING.md). + +## Je pense que ce n'est pas génial. Comment puis-je informer les autres de mon expérience ? + +Nous ajoutons tous les articles dont nous avons connaissance (qu'ils soient positifs ou négatifs) à la page [Articles](https://github.com/Rdatatable/data.table/wiki/Articles). Toutes les pages du wiki du projet sur GitHub sont en accès libre sans restriction de modification. N'hésitez pas à écrire un article, à faire un lien vers un article négatif écrit par quelqu'un d'autre que vous avez trouvé, ou à ajouter une nouvelle page à notre wiki pour recueillir vos critiques. Veillez à ce qu'elles soient constructives afin que nous ayons une chance de nous améliorer. + +## J'ai une question à poser. Je sais que le guide d'affichage de r-help me dit de contacter le mainteneur (pas r-help), mais y a-t-il un groupe plus large de personnes à qui je peux demander ? + +Veuillez consulter le [guide d'assistance](https://github.com/Rdatatable/data.table/wiki/Support) sur la page d'accueil du projet, qui contient des liens actualisés. + +## Où sont les archives de datatable-help ? + +La [page d'accueil](https://github.com/Rdatatable/data.table/wiki) contient des liens vers les archives en plusieurs formats. + +## Je préférerais ne pas publier sur la page "Questions" (Issues). Puis-je envoyer un email à une ou deux personnes ? + +Bien sûr, mais il est plus probable que vous obteniez une réponse plus rapide sur la page Issues ou sur Stack Overflow. De plus, le fait de poser des questions publiquement à ces endroits aide à construire la base de connaissances générale. + +## J'ai créé un package qui utilise data.table. Comment puis-je m'assurer que mon package est compatible avec data.table pour que l'héritage de `data.frame` fonctionne ? + +Voir [cette réponse](https://stackoverflow.com/a/10529888/403310). + +```{r, echo=FALSE} +setDTthreads(.old.th) +``` diff --git a/vignettes/fr/datatable-importing.Rmd b/vignettes/fr/datatable-importing.Rmd new file mode 100644 index 0000000000..12424f76da --- /dev/null +++ b/vignettes/fr/datatable-importing.Rmd @@ -0,0 +1,283 @@ +--- +title: "Importation dans data.table" +date: "`r Sys.Date()`" +output: + markdown::html_format +vignette: > + %\VignetteIndexEntry{Importation dans data.table} + %\VignetteEngine{knitr::knitr} + \usepackage[utf8]{inputenc} +--- + + + +Ce document se concentre sur l'utilisation de `data.table` comme dépendance dans d'autres packages R. Si vous souhaitez utiliser le code C de `data.table` à partir d'une application non-R, ou appeler directement ses fonctions C, passez à la [dernière section](#non-r-API) de cette vignette. + +Importer `data.table` n'est pas différent qu'importer d'autres packages R. Cette vignette a pour but de répondre aux questions les plus courantes à ce sujet; les indications présentées ici peuvent être appliquées à d'autres packages R. + +## Pourquoi importer `data.table` + +L'une des principales caractéristiques de `data.table` est sa syntaxe concise qui rend l'analyse exploratoire plus rapide et plus facile à écrire et à percevoir ; cette commodité peut pousser les auteurs de package à utiliser `data.table`. Une autre raison, peut-être plus importante, est la haute performance. Lorsque vous confiez des tâches de calcul lourdes de votre package à `data.table`, vous obtenez généralement de très bonnes performances sans avoir besoin de réinventer vous-même ces astuces d'optimisation numérique. + +## Importer `data.table` est facile + +Il est très facile d'utiliser `data.table` comme dépendance car `data.table` n'a pas de dépendances propres. Ceci s'applique à la fois au système d'exploitation et aux dépendances de R. Cela signifie que si R est installé sur votre machine, il a déjà tout ce qu'il faut pour installer `data.table`. Cela signifie aussi qu'ajouter `data.table` comme dépendance de votre package n'entraînera pas une chaîne d'autres dépendances récursives à installer, ce qui le rend très pratique pour une installation hors ligne. + +## fichier `DESCRIPTION` {#DESCRIPTION} + +Le premier endroit pour définir une dépendance dans un package est le fichier `DESCRIPTION`. Le plus souvent, vous devrez ajouter `data.table` dans le champ `Imports:`. Cela nécessitera l'installation de `data.table` avant que votre package ne puisse être compilé/installé. Comme mentionné ci-dessus, aucun autre package ne sera installé car `data.table` n'a pas de dépendances propres. Vous pouvez aussi spécifier la version minimale requise d'une dépendance ; par exemple, si votre package utilise la fonction `fwrite`, qui a été introduite dans `data.table` dans la version 1.9.8, vous devriez l'incorporer comme `Imports: data.table (>= 1.9.8)`. De cette façon, vous pouvez vous assurer que la version de `data.table` installée est 1.9.8 ou plus récente avant que vos utilisateurs ne puissent installer votre package. En plus du champ `Imports:`, vous pouvez aussi utiliser `Depends: data.table` mais nous décourageons fortement cette approche (et nous pourrions l'interdire dans le futur) parce que cela charge `data.table` dans l'espace de travail de votre utilisateur ; i.e. cela active la fonctionnalité `data.table` dans les scripts de votre utilisateur sans qu'il ne le demande. `Imports:` est la bonne façon d'utiliser `data.table` dans votre package sans infliger `data.table` à votre utilisateur. En fait, nous espérons que le champ `Depends:` sera un jour déprécié dans R car ceci est vrai pour tous les packages. + +## fichier `NAMESPACE` {#NAMESPACE} + +La prochaine chose à faire est de définir le contenu de `data.table` que votre package utilise. Cela doit être fait dans le fichier `NAMESPACE`. Le plus souvent, les auteurs de package voudront utiliser `import(data.table)` qui importera toutes les fonctions exportées (c'est-à-dire listées dans le fichier `NAMESPACE` de `data.table`) de `data.table`. + +Vous pouvez aussi ne vouloir utiliser qu'un sous-ensemble des fonctions de `data.table` ; par exemple, certains packages peuvent simplement utiliser les fonctions d'écriture et lecture CSV haute performance de `data.table`, pour lesquelles vous pouvez ajouter `importFrom(data.table, fread, fwrite)` dans votre fichier `NAMESPACE`. Il est également possible d'importer toutes les fonctions d'un package *en excluant* certaines d'entre elles en utilisant `import(data.table, except=c(fread, fwrite))`. + +Assurez-vous de lire également la note sur l'évaluation non standard dans `data.table` dans [la section sur les "globales non définies"](#globals) + +## Utilisation + +A titre d'exemple, nous allons définir deux fonctions dans le package `a.pkg` qui utilise `data.table`. Une fonction, `gen`, générera un simple `data.table` ; une autre, `aggr`, en fera une simple agrégation. + +```r +gen = function (n = 100L) { + dt = as.data.table(list(id = seq_len(n))) + dt[, grp := ((id - 1) %% 26) + 1 + ][, grp := letters[grp] + ][] +} +aggr = function (x) { + stopifnot( + is.data.table(x), + "grp" %in% names(x) + ) + x[, .N, by = grp] +} +``` + +## Tests + +Assurez-vous d'inclure des tests dans votre package. Avant chaque version majeure de `data.table`, nous vérifions les dépendances inverses. Cela signifie que si un changement dans `data.table` casse votre code, nous serons capables de repérer les changements et de vous en informer avant de publier la nouvelle version. Cela suppose bien sûr que vous publiiez votre package sur CRAN ou Bioconductor. Le test le plus basique peut être un script R en clair dans le répertoire `tests/test.R` de votre package : + +```r +library(a.pkg) +dt = gen() +stopifnot(nrow(dt) == 100) +dt2 = aggr(dt) +stopifnot(nrow(dt2) < 100) +``` + +Lorsque vous testez votre package, vous pouvez utiliser `R CMD check --no-stop-on-test-error`, qui continuera après une erreur et exécutera tous vos tests (au lieu de s'arrêter à la première ligne de script qui a échoué) NB ceci nécessite R 3.4.0 ou plus. + +## Tester en utilisant `testthat` + +Il est très courant d'utiliser le package `testthat` pour effectuer des tests. Tester un package qui importe `data.table` n'est pas différent de tester d'autres packages. Un exemple de script de test `tests/testthat/test-pkg.R` : + +```r +context("pkg tests") + +test_that("generate dt", { expect_true(nrow(gen()) == 100) }) +test_that("aggregate dt", { expect_true(nrow(aggr(gen())) < 100) }) +``` + +Si `data.table` est dans Suggests (mais pas dans Imports) alors vous devez déclarer `.datatable.aware=TRUE` dans un des fichiers R/* pour éviter les erreurs "object not found" lors des tests via `testthat::test_package` ou `testthat::test_check`. + +## Traitement des "fonctions ou variables globales indéfinies" ("undefined global functions or variables") {#globals} + +l'utilisation par `data.table` de l'évaluation différée de R (en particulier sur le côté gauche de `:=`) n'est pas bien reconnue par `R CMD check`. Il en résulte des `NOTE`s comme la suivante lors de la vérification du package : + +``` +* checking R code for possible problems ... NOTE +aggr: no visible binding for global variable 'grp' +gen: no visible binding for global variable 'grp' +gen: no visible binding for global variable 'id' +Undefined global functions or variables: +grp id +``` + +La façon la plus simple de gérer cela est de prédéfinir ces variables dans votre package et de leur donner la valeur `NULL`, en ajoutant éventuellement un commentaire (comme c'est le cas dans la version raffinée de `gen` ci-dessous). Quand c'est possible, vous pouvez aussi utiliser un vecteur de caractères à la place des symboles (comme dans `aggr` ci-dessous) : + +```r +gen = function (n = 100L) { + id = grp = NULL # en raison des notes NSE dans la vérification CMD R + dt = as.data.table(list(id = seq_len(n))) + dt[, grp := ((id - 1) %% 26) + 1 + ][, grp := letters[grp] + ][] +} +aggr = function (x) { + stopifnot( + is.data.table(x), + "grp" %in% names(x) + ) + x[, .N, by = "grp"] +} +``` + +Le cas des symboles spéciaux de `data.table` (par exemple `.SD` et `.N`) et de l'opérateur d'affectation (`:=`) est légèrement différent (voir ` ?.N` pour plus d'informations, y compris une liste complète de ces symboles). Vous devriez importer n'importe laquelle de ces valeurs que vous utilisez de l'espace de noms de `data.table` pour vous protéger contre tout problème provenant du scénario improbable où nous changerions la valeur exportée de ces valeurs dans le futur, par exemple, si vous voulez utiliser `.N`, `.I`, et `:=`, un `NAMESPACE` minimal devrait avoir : + +```r +importFrom(data.table, .N, .I, ':=') +``` + +Il est beaucoup plus simple d'utiliser `import(data.table)` qui autorisera avidement l'utilisation dans le code de votre package de tout objet exporté de `data.table`. + +Si cela ne vous dérange pas d'avoir `id` et `grp` enregistrés comme variables globalement dans l'espace de noms de votre package, vous pouvez utiliser `?globalVariables`. Soyez conscient que ces notes n'ont aucun impact sur le code ou ses fonctionnalités ; si vous n'avez pas l'intention de publier votre package, vous pouvez simplement choisir de les ignorer. + +## Précautions à prendre lors de la fourniture et de l'utilisation des options + +La pratique courante des packages R est de fournir des options de personnalisation définies par `options(name=val)` et récupérées en utilisant `getOption("name", default)`. Les arguments des fonctions spécifient souvent un appel à `getOption()` pour que l'utilisateur connaisse (grâce à `?fun` ou `args(fun)`) le nom de l'option contrôlant la valeur par défaut de ce paramètre ; par exemple `fun(..., verbose=getOption("datatable.verbose", FALSE))`. Toutes les options de `data.table` commencent par `datatable.` afin de ne pas entrer en conflit avec les options d'autres packages. Un utilisateur appelle simplement `options(datatable.verbose=TRUE)` pour activer la verbosité. Cela affecte tous les appels de fonctions de data.table à moins que `verbose=FALSE` ne soit fourni explicitement ; par exemple `fun(..., verbose=FALSE)`. + +Le mécanisme des options dans R est *global*. Cela signifie que si un utilisateur définit une option `data.table` pour son propre usage, ce réglage affecte également le code de tout package qui utilise `data.table`. Pour une option comme `datable.verbose`, c'est exactement le comportement désiré puisque le but est de tracer et d'enregistrer toutes les opérations de `data.table` d'où qu'elles viennent ; activer la verbosité n'affecte pas les résultats. Une autre option unique à R et excellente pour la production est `options(warn=2)` qui transforme tous les avertissements en erreurs. Encore une fois, le but est d'affecter n'importe quel avertissement dans n'importe quel package afin de ne manquer aucun avertissement en production. Il y a 6 options `datable.print.*` et 3 options d'optimisation qui n'affectent pas le résultat des opérations. Cependant, il y a une option `data.table` qui l'affecte et qui est maintenant un problème : `datatable.nomatch`. Cette option change la jointure par défaut d'externe à interne. [A côté de cela, la jointure par défaut est externe parce que outer est plus sûr ; il ne laisse pas tomber les données manquantes silencieusement ; de plus, il est cohérent avec la façon dont la base R fait correspondre les noms et les indices]. Certains utilisateurs préfèrent que la jointure interne soit la valeur par défaut et nous avons prévu cette option pour eux. Cependant, un utilisateur qui met en place cette option peut involontairement changer le comportement des jointures à l'intérieur des packages qui utilisent `data.table`. En conséquence, dans la version 1.12.4 (Oct 2019), un message était affiché lorsque l'option `datable.nomatch` était utilisée, et à partir de la version 1.14.2, elle est maintenant ignorée avec un avertissement. C'était la seule option `datable.table` qui posait ce problème. + +## Dépannage + +Si vous rencontrez des problèmes lors de la création d'un package qui utilise data.table, veuillez confirmer que le problème est reproductible dans une session R propre en utilisant la console R : `R CMD check nom.package`. + +Certains des problèmes les plus courants auxquels les développeurs sont confrontés sont généralement liés à des outils d'aide destinés à automatiser certaines tâches de développement de package, par exemple, l'utilisation de `roxygen` pour générer votre fichier `NAMESPACE` à partir des métadonnées des fichiers de code R. D'autres sont liés aux outils d'aide qui construisent et vérifient les package. D'autres sont liées aux aides qui construisent et vérifient le package. Malheureusement, ces aides ont parfois des effets secondaires inattendus/cachés qui peuvent masquer la source de vos problèmes. Ainsi, assurez-vous de faire une double vérification en utilisant la console R (lancez R sur la ligne de commande) et assurez-vous que l'importation est définie dans les fichiers `DESCRIPTION` et `NAMESPACE` en suivant les [instructions](#DESCRIPTION) [ci-dessus](#NAMESPACE). + +Si vous n'êtes pas en mesure de reproduire les problèmes que vous rencontrez en utilisant la simple console R pour construire ("build") et vérifier ("check"), vous pouvez essayer d'obtenir de l'aide en vous basant sur les problèmes que nous avons rencontrés dans le passé avec `data.table` interagissant avec des outils d'aide : [devtools#192](https://github.com/r-lib/devtools/issues/192) ou [devtools#1472](https://github.com/r-lib/devtools/issues/1472). + +## Licence + +Depuis la version 1.10.5, `data.table` est sous licence Mozilla Public License (MPL). Les raisons du changement de la GPL peuvent être lues en entier [ici](https://github.com/Rdatatable/data.table/pull/2456) et vous pouvez en savoir plus sur la MPL sur Wikipedia [ici](https://en.wikipedia.org/wiki/Mozilla_Public_License) et [ici](https://en.wikipedia.org/wiki/Comparison_of_free_and_open-source_software_licenses). + +## Importe optionnellement `data.table` : `Suggests` + +Si vous voulez utiliser `data.table` de manière conditionnelle, c'est-à-dire seulement quand il est installé, vous devriez utiliser `Suggests: data.table` dans votre fichier `DESCRIPTION` au lieu d'utiliser `Imports: data.table`. Par défaut, cette définition ne forcera pas l'installation de `data.table` lors de l'installation de votre package. Cela vous oblige aussi à utiliser conditionnellement `data.table` dans le code de votre package, ce qui doit être fait en utilisant la fonction `?requireNamespace`. L'exemple ci-dessous démontre l'utilisation conditionnelle de la fonction d'écriture de CSV rapide de `?fwrite` du package `data.table`. Si le package `data.table` n'est pas installé, la fonction de base R `?write.table`, beaucoup plus lente, est utilisée à la place. + +```r +my.write = function (x) { + if(requireNamespace("data.table", quietly=TRUE)) { + data.table::fwrite(x, "data.csv") + } else { + write.table(x, "data.csv") + } +} +``` + +Une version légèrement plus étendue de cette méthode permettrait également de s'assurer que la version installée de `data.table` est suffisamment récente pour que la fonction `fwrite` soit disponible : + +```r +my.write = function (x) { + if(requireNamespace("data.table", quietly=TRUE) && + utils::packageVersion("data.table") >= "1.9.8") { + data.table::fwrite(x, "data.csv") + } else { + write.table(x, "data.csv") + } +} +``` + +Lorsque vous utilisez un package comme dépendance suggérée, vous ne devez pas l'"importer" dans le fichier `NAMESPACE`. Mentionnez-le simplement dans le fichier `DESCRIPTION`. Lorsque vous utilisez les fonctions `data.table` dans le code d'un package (fichiers R/*), vous devez utiliser le préfixe `data.table::` car aucune d'entre elles n'est importée. Lorsque vous utilisez `data.table` dans des packages de tests (par exemple des fichiers tests/testthat/test*), vous devez déclarer `.datatable.aware=TRUE` dans l'un des fichiers R/*. + +## `data.table` dans `Imports` mais rien d'importé + +Certains utilisateurs ([e.g.](https://github.com/Rdatatable/data.table/issues/2341)) peuvent préférer éviter d'utiliser `importFrom` ou `import` dans leur fichier `NAMESPACE` et utiliser à la place la syntaxe `data.table::` sur tout le code interne (en gardant bien sûr `data.table` sous leurs `Imports:` dans `DESCRIPTION`). + +Dans ce cas, la fonction non exportée `[.data.table` reviendra à appeler `[.data.frame` comme filet de sécurité puisque `data.table` n'a aucun moyen de savoir que le package parent est conscient qu'il tente de faire des appels en utilisant la syntaxe de l'API de requête de `data.table` (ce qui pourrait conduire à un comportement inattendu car la structure des appels à `[.data.frame` et `[.data.table` diffère fondamentalement, par exemple, ce dernier a beaucoup plus d'arguments). + +Si c'est l'approche que vous préférez pour le développement de packages, définissez `.datatable.aware = TRUE` n'importe où dans votre code source R (pas besoin d'exporter). Cela indique à `data.table` que vous, en tant que développeur du package, avez conçu votre code pour qu'il s'appuie intentionnellement sur les fonctionnalités de `data.table`, même si cela n'est pas évident en inspectant votre fichier `NAMESPACE`. + +`data.table` détermine à la volée si la fonction appelante est consciente qu'elle puise dans `data.table` avec la fonction interne `cedta` (**C**alling **E**nvironment is **D**ata **T**able **A**ware), qui, en plus de vérifier le `?getNamespaceImports` de votre package, vérifie également l'existence de cette variable (entre autres choses). + +## Plus d'informations sur les dépendances + +Pour une documentation plus canonique sur la définition de la dépendance des packages, consultez le manuel officiel : [Writing R Extensions](https://cran.r-project.org/doc/manuals/r-release/R-exts.html). + +## Importation des routines C de data.table + +Certaines routines C utilisées en interne sont maintenant exportées au niveau C et peuvent donc être utilisées dans les packages R directement à partir de leur code C. Voir [`?cdt`](https://rdatatable.gitlab.io/data.table/reference/cdt.html) pour les détails et [Writing R Extensions](https://cran.r-project.org/doc/manuals/r-release/R-exts.html) dans la section *Linking to native routines in other packages* pour l'utilisation. + +## Importation à partir d'applications non-r {#non-r-api} + +Certaines petites parties du code C de `data.table` ont été isolées de l'API C de R et peuvent maintenant être utilisées à partir d'applications non-R en liant les fichiers .so / .dll. Des détails plus concrets seront fournis ultérieurement ; pour l'instant, vous pouvez étudier le code C qui a été isolé de l'API C de R dans [src/fread.c](https://github.com/Rdatatable/data.table/blob/master/src/fread.c) et [src/fwrite.c](https://github.com/Rdatatable/data.table/blob/master/src/fwrite.c). + +## Comment convertir votre dépendance à data.table de Depends à Imports + +Pour convertir une dépendance `Depends` sur `data.table` en une dépendance `Imports` dans votre package, suivez ces étapes : + +### Étape 0. S'assurer que votre package passe le contrôle R CMD dans un premier temps + +### Étape 1. Mettre à jour le fichier DESCRIPTION pour placer data.table dans Imports, et non dans Depends + +**Avant :** + +```dcf +Depends: + R (>= 3.5.0), + data.table +Imports: +``` + +**Après :** + +```dcf +Depends: + R (>= 3.5.0) +Imports: + data.table +``` + +### Étape 2.1 : Exécuter `R CMD check` + +Lancez `R CMD check` pour identifier tout import ou symbole manquant. Cette étape aide à : + +- Détecter automatiquement toutes les fonctions ou symboles de `data.table` qui ne sont pas explicitement importés. +- Signaler les symboles spéciaux manquants comme `.N`, `.SD`, et `:=`. +- Fournir immédiatement une information sur ce qui doit être ajouté au fichier NAMESPACE. + +Note : Toutes ces utilisations ne sont pas prises en compte par `R CMD check`. En particulier, `R CMD check` ne tient pas compte de certains symboles/fonctions dans les formules et manquera complètement des expressions analysées comme `parse(text = "data.table(a = 1)")`. Les packages auront besoin d'une bonne couverture de test pour détecter ces cas limites. + +### Étape 2.2 : Modifier le fichier NAMESPACE + +En se basant sur les résultats du `R CMD check`, s'assurer que toutes les fonctions utilisées, les symboles spéciaux, les génériques S3, et les classes S4 de `data.table` sont importés. + +Cela signifie qu'il faut ajouter les directives `importFrom(data.table, ...)` pour les symboles, les fonctions et les génériques S3, et/ou les directives `importClassesFrom(data.table, ...)` pour les classes S4, selon le cas. Voir 'Writing R Extensions' pour plus de détails sur la façon de procéder. + +#### Importation complète + +Vous pouvez également importer toutes les fonctions de `data.table` en une seule fois, bien que cela ne soit généralement pas recommandé : + +```r +import(data.table) +``` + +**Justification Pour Eviter Les Importations Globales :** =====1. **Documentation** : Le fichier NAMESPACE peut servir de bonne documentation sur la façon dont vous dépendez de certains packages. +2. **Éviter Les Conflits** : Les importations générales vous exposent à des ruptures subtiles. Par exemple, si vous importez deux packages avec `import(pkgA)` et `import(pkgB)`, mais que plus tard pkgB exporte une fonction également exportée par pkgA, cela cassera votre package à cause de conflits dans votre espace de noms, ce qui est interdit par `R CMD check` et CRAN.===== + +### Étape 3 : Mettre à jour vos fichiers de code R en dehors du répertoire R/ du package + +Lorsque vous déplacez un package de `Depends` vers `Imports`, il ne sera plus automatiquement attaché lorsque votre package sera chargé. Cela peut être important pour les exemples, les tests, les vignettes et les démos, où les packages `Imports` doivent être attachés explicitement. + +**Avant (avec `Depends`) :** + +```r +# les fonctions de data.table sont directement disponibles +library(MyPkgDependsDataTable) +dt <- data.table(x = 1:10, y = letters[1:10]) +setDT(dt) +result <- merge(dt, other_dt, by = "x") +``` + +**Après (avec `Imports`) :** + +```r +# Charger explicitement data.table dans les scripts utilisateurs ou les vignettes +library(data.table) +library(MyPkgDependsDataTable) +dt <- data.table(x = 1:10, y = letters[1:10]) +setDT(dt) +result <- merge(dt, other_dt, by = "x") +``` + +### Avantages de l'utilisation de `Imports` + +- **Convivialité** : `Depends` modifie le chemin `search()` de vos utilisateurs, éventuellement sans qu'ils le veuillent. +- **Gestion de l'espace de noms** : Seules les fonctions que votre package importe explicitement sont disponibles, ce qui réduit le risque de conflit de noms de fonctions. +- **Chargement de package plus propre** : Les dépendances de votre package ne sont pas attachées au chemin de recherche, ce qui rend le processus de chargement plus propre et potentiellement plus rapide. +- **Maintenance plus facile** : Cela simplifie les tâches de maintenance au fur et à mesure que les API des dépendances en amont évoluent. Trop dépendre de `Depends` peut conduire à des conflits et des problèmes de compatibilité au fil du temps. diff --git a/vignettes/fr/datatable-intro.Rmd b/vignettes/fr/datatable-intro.Rmd new file mode 100644 index 0000000000..ff9294a510 --- /dev/null +++ b/vignettes/fr/datatable-intro.Rmd @@ -0,0 +1,698 @@ +--- +title: "Introduction à data.table" +date: "`r Sys.Date()`" +output: + markdown::html_format +vignette: > + %\VignetteIndexEntry{Introduction à data.table} + %\VignetteEngine{knitr::knitr} + \usepackage[utf8]{inputenc} +--- + +```{r, echo = FALSE, message = FALSE} +require(data.table) +knitr::opts_chunk$set( + comment = "#", + error = FALSE, + tidy = FALSE, + cache = FALSE, + collapse = TRUE +) +.old.th = setDTthreads(1) +``` + +Cette vignette présente la syntaxe de `data.table` , sa forme générale, comment *extraire les lignes*, *sélectionner et faire des opérations* sur les colonnes, et réaliser des agrégations *par groupe*. Il est avantageux d'être familiarisé avec la structure de données `data.frame` de base du R, mais cela n'est pas essentiel pour suivre cette vignette. + +*** + +## Analyser des données en utilisant `data.table` + +Les opérations concernant le traitement des données telles que *subset*, *group*, *update*, *join*, etc. sont toutes intimement liées. En regroupant *ces opérations apparentées* cela nous permet : + +* syntaxe *concise* et *cohérente* quel que soit l'ensemble des opérations que vous souhaitez effectuer pour atteindre votre objectif final. + +* effectuer une analyse *fluide* sans la charge cognitive de devoir faire correspondre chaque opération à une fonction particulière à partir d'un ensemble potentiellement énorme de fonctions disponibles avant d'effectuer l'analyse. + +* *automatiquement* optimiser les opérations en interne et de manière très efficace en connaissant précisément les données requises pour chaque opération, ce qui permet d'obtenir un code très rapide et efficace sur le plan de la mémoire. + +En résumé, si vous souhaitez réduire drastiquement le temps de *programmation* et de *compilation*, alors ce package est fait pour vous. C'est la philosophie suivie par `data.table` pour rendre cela possible. Notre but est d'illustrer ceci au travers de cette série de vignettes. + +## Données {#data} + +Dans cette vignette, nous utiliseront les données [NYC-flights14](https://raw.githubusercontent.com/Rdatatable/data.table/master/vignettes/flights14.csv) obtenues du package [flights](https://github.com/arunsrinivasan/flights) (disponible sur GitHub seulement). Il contient les horaires des vols d'avions du Bureau of Transportation Statistics à propos de tous les vols partant des aéroports de New York City en 2014 (inspiré de [nycflights13](https://github.com/tidyverse/nycflights13)). Les données ne concernent que les mois de janvier à octobre 2014. + +Vous pouvez utiliser le lecteur de fichiers rapide et convivial 'fread' de 'data.table' pour charger 'flights' ditectement ainsi : + +```{r echo = FALSE} +options(width = 100L) +``` + +```{r} +input <- if (file.exists("../flights14.csv")) { + "../flights14.csv" +} else { + "https://raw.githubusercontent.com/Rdatatable/data.table/master/vignettes/flights14.csv" +} +flights <- fread(input) +flights +dim(flights) +``` + +A noter : 'fread' accepte directement les URLS 'http' et 'https', ainsi que les commandes système opérationnelles telles que les sorties de 'sed' et 'awk'. Voir '?fread' pour les exemples. + +## Introduction + +Dans cette vignette, nous allons + +1. Commencez par les bases - qu'est-ce qu'un `data.table`, sa forme générale, comment réaliser un *sous-ensemble* des lignes, comment *sélectionner et effectuer des calculs* sur les colonnes; + +2. Nous verrons ensuite comment effectuer des agrégations de données par groupe + +## 1. Les bases {#basics-1} + +### a) 'data.table' c'est quoi ? {#what-is-datatable-1a} + +'data.table' est un package R qui fournit **une version étendue** d'un 'data.frame', qui est la structure de données standard pour stocker des données dans la 'base' R. Dans la [Data](#data) section ci-dessus, nous avons vu comment créer une 'data.table' avec 'fread()', mais on peut aussi en créer une en utilisant la fonction 'data.table()' . Voici un exemple : + +```{r} +DT = data.table( + ID = c("b","b","b","a","a","c"), + a = 1:6, + b = 7:12, + c = 13:18 +) +DT +class(DT$ID) +``` + +Vous pouvez aussi convertir des objets existants en une `data.table` en utilisant `setDT()` (pour les structures `data.frame` et `list`) ou `as.data.table()` (pour les autres structures). Pour les autres détails concernant les différences (ce qui est hors du champ de cette vignette), voir `?setDT` et `?as.data.table`. + +#### Notez que : + +* Les numéros de ligne sont imprimés avec un `:` afin de séparer visuellement le numéro de ligne de la première colonne. + +* Lorsque le nombre de lignes à imprimer dépasse l'option globale `datatable.print.nrows` (défaut = `r getOption("datatable.print.nrows")`), il n'imprime automatiquement que les 5 premières et les 5 dernières lignes (comme on peut le voir dans la section [Data](#data)). Pour un grand `data.frame`, vous avez pu vous retrouver à attendre que des tables plus grandes s'impriment et se mettent en page, parfois sans fin. Cette restriction permet d'y remédier, et vous pouvez demander le nombre par défaut de la façon suivante : + + ```{.r} + getOption("datatable.print.nrows") + ``` + +* `data.table` ne définit ni n'utilise jamais de *nom de ligne*. Nous verrons pourquoi dans la vignette *"Sous-ensemble basé sur des clés et recherche binaire rapide"*. + +### b) Forme générale - dans quel sens la 'data.table' est-elle *étendue* ? {#enhanced-1b} + +Par rapport à un `data.frame`, vous pouvez faire *beaucoup plus de choses* qu'extraire des lignes et sélectionner des colonnes dans la structure d'une `data.table`, par exemple, avec `[ ... ]` (Notez bien : nous pourrions aussi faire référence à écrire quelque chose dans `DT[...]` comme "interroger `DT`", par analogie ou similairement à SQL). Pour le comprendre il faut d'abord que nous regardions la *forme générale* de la syntaxe `data.table`, comme indiqué ci-dessous : + +```{r eval = FALSE} +DT[i, j, by] + +## R: i j by +## SQL: where | order by select | update group by +``` + +Les utilisateurs ayant des connaissances SQL feront peut être directement le lien avec cette syntaxe. + +#### La manière de le lire (à haute voix) est : + +Utiliser `DT`, extraire ou trier les lignes en utilisant `i`, puis calculer `j`, grouper avec `by`. + +Commençons par voir 'i' et 'j' d'abord - en indiçant les lignes et en travaillant sur les colonnes. + +### c) Regrouper les lignes en 'i' {#subset-i-1c} + +#### -- Obtenir tous les vols qui ont "JFK" comme aéroport de départ pendant le mois de juin. + +```{r} +ans <- flights[origin == "JFK" & month == 6L] +head(ans) +``` + +* Dans le cadre d'un `data.table`, on peut se référer aux colonnes *comme s'il s'agissait de variables*, un peu comme dans SQL ou Stata. Par conséquent, nous nous référons simplement à `origin` et `month` comme s'il s'agissait de variables. Nous n'avons pas besoin d'ajouter le préfixe `vol$` à chaque fois. Néanmoins, l'utilisation de `flights$origin` et `flights$month` fonctionnerait parfaitement. + +* Les *indices de ligne* qui satisfont la condition `origin == "JFK" & month == 6L` sont calculés, et puisqu'il n'y a rien d'autre à faire, toutes les colonnes de `flights` aux lignes correspondant à ces *indices de ligne* sont simplement renvoyées sous forme d’un `data.table`. + +* Une virgule après la condition dans `i` n'est pas nécessaire. Mais `flights[origin == "JFK" & month == 6L, ]` fonctionnerait parfaitement. Avec un `data.frame`, cependant, la virgule est indispensable. + +#### -- Récupérer les deux premières lignes de `flights`. {#subset-rows-integer} + +```{r} +ans <- flights[1:2] +ans +``` + +* Dans ce cas, il n'y a pas de condition. Les indices des lignes sont déjà fournis dans `i`. Nous retournons donc un `data.table` avec toutes les colonnes de `flights` aux lignes pour ces *index de ligne*. + +#### -- Trier `flights` d'abord sur la colonne `origin` dans l'ordre *ascending*, puis par `dest` dans l'ordre *descendant* : + +Nous pouvons utiliser la fonction R 'order()' pour faire cela. + +```{r} +ans <- flights[order(origin, -dest)] +head(ans) +``` + +#### `order()` est optimisé en interne + +* Nous pouvons utiliser "-" sur les colonnes `character` dans le cadre d'un `data.table` pour trier par ordre décroissant. + +* De plus, `order(...)` dans le cadre d'un `data.table` utilise l'ordre radix rapide interne de `data.table` `forder()`. Ce tri a apporté une telle amélioration par rapport à `base::order` de R que le projet R a adopté l'algorithme `data.table` comme tri par défaut en 2016 pour R 3.3.0 (pour référence, voir `?sort` et les [R Release NEWS](https://cran.r-project.org/doc/manuals/r-release/NEWS.pdf)). + +Nous discuterons de l'ordonnancement rapide de la `data.table` plus en détails dans la vignette *fonctionnement interne de `data.table` internals*. + +### d) Sélection de colonne(s) dans `j` {#select-j-1d} + +#### -- Sélectionner la colonne `arr_delay`, mais la renvoyer en tant que *vector*. + +```{r} +ans <- flights[, arr_delay] +head(ans) +``` + +* Puisque les colonnes peuvent être appelées comme si elles étaient des variables dans le cadre d'un `data.table`, nous nous référons directement à la *variable* dont nous voulons créer un sous-ensemble. Puisque nous voulons *toutes les lignes*, nous sautons simplement `i`. + +* Il renvoie *toutes* les lignes de la colonne `arr_delay`. + +#### -- Sélectionner la colonne `arr_delay`, mais la renvoyer en tant que `data.table`. + +```{r} +ans <- flights[, list(arr_delay)] +head(ans) +``` + +* Nous enveloppons les *variables* (noms de colonnes) dans `list()`, ce qui assure qu'un `data.table` est retourné. Dans le cas d'un seul nom de colonne, le fait de ne pas utiliser `list()` renvoie un vecteur, comme on peut le voir dans l'exemple précédent](#select-j-1d). + +* `data.table` permet aussi d'envelopper les colonnes avec `.()` au lieu de `list()`. C'est un *alias* de `list()` ; les deux signifient la même chose. N'hésitez pas à utiliser ce que vous préférez ; nous avons remarqué que la plupart des utilisateurs semblent préférer `.()` pour la concision, donc nous continuerons à utiliser `.()` par la suite. + +Un `data.table` (et également un `data.frame`) est aussi en interne une `list` , avec la caractéristique que chaque élément a la même longueur et que la `list` possède un attribut `class`. En permettant à `j` de renvoyer une `list` cela permet de convertir et de renvoyer des `data.table` très efficacement. + +#### Conseil : {#tip-1} + +Tant que `j-expression` renvoie une `list`, chaque élément de la liste sera converti en colonne dans la `data.table` résultante. Ce qui fait que `j` est très puissant, comme nous le verrons bientôt. Il est aussi très important de comprendre cela dans le cas où vous auriez à faire des requêtes plus compliquées !! + +#### -- Sélectionner à la fois les colonnes `arr_delay` et `dep_delay`. + +```{r} +ans <- flights[, .(arr_delay, dep_delay)] +head(ans) + +## forme alternative +# ans <- flights[, list(arr_delay, dep_delay)] +``` + +* Enveloppez les deux colonnes dans `.()`, ou `list()`. C'est tout. + +#### -- Sélectionner à la fois les colonnes `arr_delay` et `dep_delay` *et* les renommer en `delay_arr` et `delay_dep`. + +Comme `.()` est juste un alias pour `list()`, nous pouvons donner un nom quelconque aux colonnes comme si on créait une `list`. + +```{r} +ans <- flights[, .(delay_arr = arr_delay, delay_dep = dep_delay)] +head(ans) +``` + +### e) Calcul ou *do* dans 'j' + +#### -- Combien de voyages on eu un retard total < 0 ? + +```{r} +ans <- flights[, sum( (arr_delay + dep_delay) < 0 )] +ans +``` + +#### Que se passe-t-il dans ce cas ? + +* Le `j` de `data.table` peut gérer plus que la *sélection de colonnes* - il peut gérer des *expressions*, c'est-à-dire *calculer sur des colonnes*. Cela ne devrait pas être surprenant, car on peut se référer aux colonnes comme si elles étaient des variables. Nous devrions donc pouvoir *calculer* en appelant des fonctions sur ces variables. Et c'est précisément ce qui se passe ici. + +### f) Sous-ensemble de `i` *et* do dans `j` + +#### -- Calculer le nombre moyen de retards des arrivées et des départs pour tous les vols au départ de l'aéroport "JFK" pendant le mois de juin. + +```{r} +ans <- flights[origin == "JFK" & month == 6L, + .(m_arr = mean(arr_delay), m_dep = mean(dep_delay))] +ans +``` + +* Nous commençons par effectuer un sous-ensemble dans `i` pour trouver les *indices de ligne* correspondants à `origin` égal à l’aéroport `"JFK"`, et où le `mois` est égal à `6L`. Nous *n'effectuons pas encore* le sous-ensemble de *toutes* les `data.table` correspondant à ces lignes. + +* Maintenant, nous regardons `j` et nous constatons qu'il n'utilise que *deux colonnes*. Et ce que nous devons faire, c'est calculer leur moyenne avec `mean()`. Par conséquent, nous regroupons uniquement les colonnes d’intérêt aux lignes correspondantes, et nous calculons leurs moyennes. + +Parce que les trois composants principaux de la requête (`i`, `j` et `by`) figurent *ensemble* dans `[...]`, `data.table` peut les voir tous trois et optimiser la requête dans sa totalité *avant l'évaluation*, plutôt que d'optimiser chacun séparément. Par conséquent nous pouvons éviter le sous-ensemble complet (par exemple trier les colonnes *annexes* `arr_delay` et `dep_delay`), pour la rapidité et l'efficacité de la mémoire. + +#### -- Combien de voyages ont été réalisés en 2014 au départ de l'aéroport "JFK" au mois de juin ? + +```{r} +ans <- flights[origin == "JFK" & month == 6L, length(dest)] +ans +``` + +La fonction `length()` nécessite un argument d'entrée. Il suffit juste de calculer le nombre de lignes du sous-ensemble. On aurait pu utiliser n'importe quelle colonne comme argument d'entrée de `length()`. Cette approche est une réminiscence de `SELECT COUNT(dest) FROM flights WHERE origin = 'JFK' AND month = 6` en SQL. + +Ce type d'opération arrive assez fréquement, particulièrement lors des regroupements (comme nous le verrons dans la section suivante), au point que `data.table` fournit un *symbole spécial* `.N` pour cela. + +### g) Gérer les éléments absents dans `i` + +#### -- Que se passe-t-il quand on interroge des éléments non-existants ? + +Lorsque vous interrogez une `data.table` pour des éléments qui n'existent pas, le comportement dépend de la méthode utilisée. + +```r +setkeyv(flights, "origin") +``` + +* **Sous-ensemble basé sur les clés : `dt["d"]`** + +Ceci réalise une jointure parfaite sur la colonne clé `x`, fournissant une rangée avec `d` et `NA` pour les colonnes absentes. En utilisant `setkeyv`, la table est triée en fonction des clés fournies et un index interne est créé, permettant une recherche binaire et des performances optimisées. + +```r +flights["XYZ"] +# Retourne: +# origin year month day dep_time sched_dep_time dep_delay arr_time sched_arr_time arr_delay carrier flight tailnum ... +# 1: XYZ NA NA NA NA NA NA NA NA NA NA NA NA ... +``` + +* **Sous-ensemble logique : `dt[x == "d"]`** + +Ceci réalise une opération standard de sous-ensemble qui ne trouve aucune correspondance de lignes et donc renvoie une `data.table` vide. + +```r + flights[origin == "XYZ"] +# Retourne: +# Empty data.table (0 rows and 19 cols): year,month,day,dep_time,sched_dep_time,dep_delay,arr_time,sched_arr_time,arr_delay,... +``` + +* **Correspondance exacte en utilisant `nomatch=NULL`** + +Pour une correspondance stricte sans `NA` pour les éléments absents, utiliser `nomatch=NULL` : + +```r +flights["XYZ", nomatch=NULL] +# Retourne: +# Empty data.table (0 rows and 19 cols): year,month,day,dep_time,sched_dep_time,dep_delay,arr_time,sched_arr_time,arr_delay,... +``` + +En assimilant ces comportements, cela vous ôtera toute confusion lorsque vous trouverez des éléments absents parmi vos données. + +#### Symbol spécial `.N`: {#special-N} + +`.N` est une variable interne spéciale qui contient le nombre d'observations *dans le groupe actuel*. Elle est particulièrement utile combinée avec `by` comme nous le verrons dans la prochaine section. S'il n'y a pas de groupe pour les opérations, le nombre de lignes dans le sous-ensemble sera simplement renvoyé. + +Maintenant que nous savons, nous pouvons accomplir la même tâche en utilisant `.N` ainsi : + +```{r} +ans <- flights[origin == "JFK" & month == 6L, .N] +ans +``` + +* Une fois de plus, nous introduisons `i` pour obtenir les *indices de lignes* pour lesquels l'aéroport `origin` est *"JFK"*, et le `mois` est *6*. + +* Nous voyons que `j` n'utilise que `.N` et aucune autre colonne. Par conséquent, le sous-ensemble complet n'est pas matérialisé. Nous renvoyons simplement le nombre de lignes dans le sous-ensemble (qui est juste la longueur des indices de ligne). + +* Notez que nous n'avons pas enveloppé `.N` avec `list()` ou `.()`. Par conséquent, un vecteur est retourné. + +On aurait pu faire la même opération en écrivant `nrow(flights[origin == "JFK" & month == 6L])`. Néanmoins il aurait fallu d'abord dissocier la `data.table` entière en fonction des *indices de lignes* dans `i` *puis* renvoyer les lignes en utilisant `nrow()`, ce qui est inutile et pas efficace. Nous aborderons en détails ce sujet et d'autres aspects de l'optimisation dans la vignette *architecture de `data.table`*. + +### h) Super ! Mais comment référencer les colonnes par nom dans `j` (comme avec un `data.frame`) ? {#refer_j} + +Si vous imprimez le nom des colonnes explicitement, il n'y a pas de différence avec un `data.frame` (depuis v1.9.8). + +#### -- Sélectionner simultanément les colonnes `arr_delay` et `dep_delay` à la manière d'un `data.frame`. + +```{r j_cols_no_with} +ans <- flights[, c("arr_delay", "dep_delay")] +head(ans) +``` + +Si vous avez stocké les colonnes souhaitées dans un vecteur de caractères, il y a deux options : utiliser le préfixe `..` , ou utiliser l'argument `with`. + +#### -- Sélectionnez les colonnes nommées dans une variable en utilisant le préfixe `..` + +```{r j_cols_dot_prefix} +select_cols = c("arr_delay", "dep_delay") +flights[ , ..select_cols] +``` + +Pour les habitués du terminal Unix, le préfixe `..` devrait rappeler la commande de "remontée d'un niveau", qui est analogue à ce qui se passe ici -- le `..` demande à `data.table` de chercher la variable `select_cols` "un nivau au-dessus", c'est à dire dans ce cas, dans l'envronnement global. + +#### -- Sélectionner les colonnes nommées dans une variable en utilisant `with = FALSE` + +```{r j_cols_with} +flights[ , select_cols, with = FALSE] +``` + +L'argument s'appelle `with` d'après la fonction R `with()` à cause de la fonctionnalité similaire. Supposez que vous ayiez une `data.frame` `DF` et que vous vouliez dissocier toutes les lignes où `x > 1`. Dans la `base` R vous pouvez écrire : + +```{r} +DF = data.frame(x = c(1,1,1,2,2,3,3,3), y = 1:8) + +## (1) méthode classique +DF[DF$x > 1, ] # data.frame needs that ',' as well + +## (2) en utilisant with +DF[with(DF, x > 1), ] +``` + +* L'utilisation de `with()` dans (2) permet d'utiliser la colonne `x` de `DF` comme s'il s'agissait d'une variable. + + D'où le nom de l'argument `with` dans `data.table`. Mettre `with = FALSE` désactive la possibilité de se référer aux colonnes comme si elles étaient des variables, restaurant ainsi le « mode `data.frame` ». + +* Nous pouvons également *désélectionner* des colonnes en utilisant `-` ou `!`. Par exemple : + + ```{r eval = FALSE} + ## pas d'exécution + + # renvoie toutes les colonnes sauf arr_delay et dep_delay + ans <- flights[, !c("arr_delay", "dep_delay")] + # ou + ans <- flights[, -c("arr_delay", "dep_delay")] + ``` + +* A partir de la `v1.9.5+`, on peut aussi sélectionner en spécifiant les noms des colonnes de début et de fin, par exemple, `year:day` pour sélectionner les trois premières colonnes. + + ```{r eval = FALSE} + ## pas d'exécution + + # renvoie year,month et day + ans <- flights[, year:day] + # renvoie day, month et year + ans <- flights[, day:year] + # renvoie toutes les colonnes sauf year, month et day + ans <- flights[, -(year:day)] + ans <- flights[, !(year:day)] + ``` + + Ceci est particulièrement pratique lorsque l'on travaille de manière interactive. + +`with = TRUE` est la valeur par défaut dans `data.table` car nous pouvons faire plus en permettant à `j` de gérer des expressions - particulièrement en combinant avec `by`, comme nous le verrons dans un instant. + +## 2. Aggrégations + +Nous avons déjà vu `i` et `j` dans la forme générale d'une `data.table` dans la secton précédente. Dans cette section, nous allons voir comment ils peuvent être combinés ensemble avec `by` pour réaliser des opérations *par groupe*. Voyons quelques exemples. + +### a) Regrouper avec `by` + +#### -- Comment obtenir le nombre de voyages au départ de chaque aéroport ? + +```{r} +ans <- flights[, .(.N), by = .(origin)] +ans + +## ou résultat identique en utilisant un vecteur de chaînes de caractères dans 'by' +# ans <- flights[, .(.N), by = "origin"] +``` + +* Nous savons que `.N` [est une variable spéciale](#special-N) qui contient le nombre de lignes dans le groupe courant. En groupant par `origine`, on obtient le nombre de lignes, `.N`, pour chaque groupe. + +* En faisant `head(flights)` vous pouvez voir que les aéroports d'origine sont dans l'ordre *"JFK"*, *"LGA"*, et *"EWR"*. L'ordre original de regroupement des variables est préservé dans le résultat. *Il est important de garder cela à l'esprit!* + +* Comme nous n'avons pas fourni de nom pour la colonne retournée dans `j`, elle a été nommée `N` automatiquement en reconnaissant le symbole spécial `.N`. + +* `by` accepte également un vecteur de caractères de noms de colonnes. Ceci est particulièrement utile pour le codage par programmation, par exemple pour concevoir une fonction avec les colonnes de regroupement (sous la forme d'un vecteur `character`) comme argument de la fonction. + +* Lorsqu'il n'y a qu'une seule colonne ou expression à laquelle se référer dans `j` et `by`, nous pouvons abandonner la notation `.()`. Ceci est purement pratique. Nous pourrions plutôt faire : + + ```{r} + ans <- flights[, .N, by = origin] + ans + ``` + + Nous utiliserons cette forme pratique chaque fois que cela sera possible. + +#### -- Comment calculer le nombre de voyages au départ de chaque aéroport pour le transporteur ayant le code `"AA"`? {#origin-.N} + +Le code unique de transporteur `"AA"` correspond à *American Airlines Inc.* + +```{r} +ans <- flights[carrier == "AA", .N, by = origin] +ans +``` + +* Nous obtenons d'abord les indices de ligne pour l'expression `carrier == "AA"` à partir de `i`. + +* En utilisant ces *index de ligne*, nous obtenons le nombre de lignes groupées par `origine`. Une fois de plus, aucune colonne n'est matérialisée ici, car l'expression `j' ne nécessite aucune colonne pour définir le sous-ensemble et le calcul est donc rapide et peu gourmand en mémoire. + +#### -- Comment obtenir le nombre total de voyages pour chaque paire `origin, dest` du transporteur ayant pour code `"AA"`? {#origin-dest-.N} + +```{r} +ans <- flights[carrier == "AA", .N, by = .(origin, dest)] +head(ans) + +## ou résultat identique en utilisant une chaîne de caractères dans 'by' +# ans <- flights[carrier == "AA", .N, by = c("origin", "dest")] +``` + +* `by` accepte plusieurs colonnes. Nous fournissons simplement toutes les colonnes par lesquelles il faut grouper. Notez l'utilisation de `.()` dans `by` -- encore une fois, c'est juste un raccourci pour `list()`, et `list()` peut être utilisé ici aussi. Nous nous en tiendrons à nouveau à `.()` dans cette vignette. + +#### -- Comment obtenir les valeurs moyennes menselles du retard des arrivées et des départs pour chaque paire `orig,dest` pour le transporteur ayant le code `"AA"`? {#origin-dest-month} + +```{r} +ans <- flights[carrier == "AA", + .(mean(arr_delay), mean(dep_delay)), + by = .(origin, dest, month)] +ans +``` + +* Comme nous n'avons pas fourni de noms de colonnes pour les expressions dans `j`, elles ont été automatiquement générées en tant que `V1` et `V2`. + +* Une fois de plus, notez que l'ordre d'entrée des colonnes de regroupement est préservé dans le résultat. + +Maintenant qu'adviendrait-il si nous voulions trier les résultats en groupant les colonnes `origin`, `dest` et `month` ? + +### b) Tri `by` : `keyby` + +`data.table` conserve l'ordre original des groupes; c'est intentionnel et défini à la conception. Il existe des cas où conserver l'ordre original est essentiel. Mais à certains moments, nous aimerions trier automatiquement par variables dans notre regroupement. + +#### -- Donc comment pourrions-nous trier directement sur toutes les variables de regroupement ? + +```{r} +ans <- flights[carrier == "AA", + .(mean(arr_delay), mean(dep_delay)), + keyby = .(origin, dest, month)] +ans +``` + +* Tout ce que nous avons fait, c'est remplacer `by` par `keyby`. Cela ordonne automatiquement le résultat par ordre croissant des variables de regroupement. En fait, à cause de l'implémentation interne de `by` qui nécessite d'abord un tri avant de récupérer l'ordre de la table originale, `keyby` est typiquement plus rapide que `by` parce qu'il ne nécessite pas cette seconde étape. + +**Clés :** actuellement `keyby` en fait un peu plus que *simplement trier*. Il *définit une clé* également après le tri en initialisant un `attribute` appelé `sorted`. + +Nous en apprendrons plus au sujet des `clés` dans la vignette *Clés et sous-ensembles basés sur la recherche binaire rapide*; pour l'instant, tout ce que vous devez savoir est que vous pouvez utiliser `keyby` pour trier automatiquement le résultat selon les colonnes spécifiées dans `by`. + +### c) Chaînage + +Considérons la tâche consistant à [récupérer le nombre total de voyages pour chaque couple `origin, dest` du transporteur *"AA"*](#origin-dest-.N). + +```{r} +ans <- flights[carrier == "AA", .N, by = .(origin, dest)] +``` + +#### -- Comment trier `ans` en utilisant la colonne `origin` en mode croissant, et la colonne `dest` en mode décroissant ? + +On peut stocker le résultat intermédiaire dans une variable, puis passer `order(origin, -dest)` sur cette variable. Cela semble plus direct. + +```{r} +ans <- ans[order(origin, -dest)] +head(ans) +``` + +* Rappelons que nous pouvons utiliser `-` sur une colonne `character` dans `order()` dans le cadre d'un `data.table`. Ceci est possible grâce à l'optimisation interne des requêtes de `data.table`. + +* Rappelez-vous aussi que `order(...)` dans le contexte d'un `data.table` est *automatiquement optimisé* pour utiliser l’algorithme de tri radix rapide interne de `data.table` `forder()` pour plus de rapidité. + +Mais ceci nécessite d'avoir assigné le résultat intermédiaire et de réécrire ce résultat. On peut faire mieux et éviter cette assignation intermédiaire à une variable temporaire en *chaînant* les expressions ensemble. + +```{r} +ans <- flights[carrier == "AA", .N, by = .(origin, dest)][order(origin, -dest)] +head(ans, 10) +``` + +* Nous pouvons ajouter des expressions l'une après l'autre, *formant une chaîne* d'opérations, c'est-à-dire `DT[ ... ][ ... ][ ... ]`. + +* Vous pouvez également les enchaîner verticalement : + + ```{r eval = FALSE} + DT[ ... + ][ ... + ][ ... + ] + ``` + +### d) Expressions de `by` + +#### -- `by` accepte-t-il également *expressions*, ou simplement des colonnes ? + +Oui, il le fait. Par exemple, si nous avions voulu chercher combien de vols sont partis en retard mais sont arrivés plus tôt (ou à l'heure), ou parts à l'heure mais arrivés en retard, etc... + +```{r} +ans <- flights[, .N, .(dep_delay>0, arr_delay>0)] +ans +``` + +* La dernière ligne correspond à `dep_delay > 0 = TRUE` et `arr_delay > 0 = FALSE`. Nous pouvons voir que les vols `r flights[!is.na(arr_delay) & !is.na(dep_delay), .N, .(dep_delay>0, arr_delay>0)][, N[4L]]` ont commencé en retard mais sont arrivés en avance (ou à l'heure). + +* Notez que nous n'avons pas fourni de noms à `by-expression`. Par conséquent, les noms ont été automatiquement assignés dans le résultat. Comme pour `j`, vous pouvez nommer ces expressions comme vous le feriez pour des éléments de n'importe quelle liste, comme par exemple `DT[, .N, .(dep_delayed = dep_delay>0, arr_delayed = arr_delay>0)]`. + +* Vous pouvez fournir d'autres colonnes avec des expressions, par exemple : `DT[, .N, by = .(a, b>0)]`. + +### e) Colonnes multiples dans `j` - `.SD` + +#### -- Faut-il calculer `mean()` pour chaque colonne individuellement ? + +Bien sûr il n'est pas pratique de devoir entrer `mean(myCol)` pour chaque colonne, une par une. Et s'il fallait faire la moyenne `mean()` sur 100 colonnes ? + +Comment faire cela de manière efficace et concise ? Pour y arriver, relisons [ce conseil](#tip-1) - *"Tant que la `j`-expression renvoie une `list`, chaque élément de cette `list` sera converti en une colonne de la `data.table` résultat"*. Si nous pouvons adresser le *sous-ensemble de données de chaque groupe* comme une variable *de regroupement*, nous pourrons ensuite boucler sur toutes les colonnes de cette variables en utilisant la fonction de base familière (ou en passe de le devenir) `lapply()`. Il n'y a pas de nouveaux noms à apprendre particuliers pour `data.table`. + +#### Symbole spécial `.SD`: {#special-SD} + +`data.table` fournit le symbole *spécial* `.SD`. Il tire son nom de **S**ous-ensemble de **D**onnées. C'est une `data.table` qui contient les données du *groupe actuel* tel qu'il a été défini avec `by`. + +Souvenez-vous qu'une `data.table` est représentée en interne comme une `list` dont toutes les colonnes ont la même longueur. + +Utilisons la [`data.table` `DT` précédente](#what-is-datatable-1a) pour avoir un aperçu de ce à quoi ressemble `.SD` . + +```{r} +DT + +DT[, print(.SD), by = ID] +``` + +* `.SD` contient toutes les colonnes *à l'exception des colonnes de regroupement* par défaut. + +* Il est également généré en conservant l'ordre original - les données correspondant à `ID = "b"`, puis `ID = "a"`, et enfin `ID = "c"`. + +Pour calculer sur uneou plusieurs colonnes vous pouvez utiliser simplement la fonction de base R `lapply()`. + +```{r} +DT[, lapply(.SD, mean), by = ID] +``` + +* `.SD` contient les lignes correspondant aux colonnes `a`, `b` et `c` pour ce groupe. Nous calculons la moyenne avec `mean()` sur chacune de ces colonnes en utilisant la fonction de base déjà familière `lapply()`. + +* Chaque groupe renvoie une liste de trois éléments contenant la valeur moyenne qui deviendra les colonnes du `data.table` résultant. + +* Puisque `lapply()` renvoie une liste, il n'est pas nécessaire de l'entourer d'un `.()` supplémentaire (si nécessaire, référez-vous à [cette astuce](#tip-1)). + +Nous y sommes presque. Il reste encore une petite chose à régler. Dans notre `data.table` `flights` , nous avons voulu calculer seulement la `mean()` des deux colonnes `arr_delay` et `dep_delay`. Mais `.SD` contiendrait par défaut toutes les colonnes autres que les variables de groupement. + +#### -- Comment spécifier uniquement les colonnes sur lesquelles nous voulons appliquer `mean()` ? + +#### .SDcols + +En utilisant l'argument `.SDcols`. Il accepte soit des noms soit des indices de colonnes. Par exemple, `.SDcols = c("arr_delay", "dep_delay")` permet que `.SD` ne comporte que ces deux colonnes pour chaque groupe. + +De la même manière que [part g)](#refer_j), vous pouvez également spécifier les colonnes à supprimer au lieu des colonnes à garder en utilisant le `-` ou `!`. De plus, vous pouvez sélectionner des colonnes consécutives avec `colA:colB` et les désélectionner avec `!(colA:colB)` ou `-(colA:colB)`. + +Maintenant essayons d'utiliser `.SD` avec `.SDcols` pour obtenir la moyenne `mean()` des colonnes `arr_delay` et `dep_delay` groupées par `origin`, `dest` et `month`. + +```{r} +flights[carrier == "AA", ## Seulement les vols sur porteurs "AA" + lapply(.SD, mean), ## calcule la moyenne + by = .(origin, dest, month), ## pour chaque 'origin,dest,month' + .SDcols = c("arr_delay", "dep_delay")] ## pour seulement ceux spécifiés dans .SDcols +``` + +### f) Extraire `.SD` pour chaque groupe : + +#### -- Comment renvoyer les deux premières lignes de chque 'month`? + +```{r} +ans <- flights[, head(.SD, 2), by = month] +head(ans) +``` + +* `.SD` est un `data.table` qui contient toutes les lignes de *ce groupe*. Nous allons simplement subdiviser les deux premières lignes comme nous l'avons déjà vu [ici](#subset-rows-integer). + +* Pour chaque groupe, `head(.SD, 2)` renvoie les deux premières lignes sous forme de `data.table`, qui est également une liste, ce qui nous évite de l'entourer de `.()`. + +### g) Pourquoi garder `j` si flexible ? + +Ainsi nous avons une syntaxe cohérente et continuons l'utilisation de fonctions de base déja existantes (et familières) au lieu d'apprendre de nouvelles fonctions. Pour illustrer cela utilisons la `data.table` `DT` que nous avons créée tout au début dans la section [Qu'est-ce qu'une data.table ?](#what-is-datatable-1a). + +#### -- Comment concaténer les colonnes `a` et `b` pour chaque groupe de `ID` ? + +```{r} +DT[, .(val = c(a,b)), by = ID] +``` + +* C'est tout. Aucune syntaxe particulière n'est requise. Tout ce que nous avons besoin de connaître est la fonction de base `c()` qui concatène des vecteurs, ainsi que [l'astuce de tout à l'heure](#tip-1). + +#### -- Que se passerait-il si nous voulions avoir toutes les valeurs des colonnes `a` et `b` concaténées, mais renvoyées en tant que colonne de liste ? + +```{r} +DT[, .(val = list(c(a,b))), by = ID] +``` + +* Ici, nous concaténons d'abord les valeurs avec `c(a,b)` pour chaque groupe, et nous les enveloppons avec `list()`. Ainsi, pour chaque groupe, nous renvoyons une liste de toutes les valeurs concaténées. + +* Notez que ces virgules ne servent qu'à l'affichage. Une colonne de liste peut contenir n'importe quel objet dans chaque cellule et, dans cet exemple, chaque cellule est elle-même un vecteur et certaines cellules contiennent des vecteurs plus longs que d'autres. + +Une fois que vous commencerez à utiliser `j`, vous découvrirez la puissance de sa syntaxe. Une manière pratique de l'aborder est de la tester en utilisant `print()`. + +Par exemple : + +```{r} +## inspectez la différence entre +DT[, print(c(a,b)), by = ID] # (1) + +## et +DT[, print(list(c(a,b))), by = ID] # (2) +``` + +Dans (1), pour chaque groupe, un vecteur est renvoyé, de longueur = 6,4,2 ici. Néanmoins, (2) renvoie une liste de longueur 1 pour chaque groupe, dont chaque premier élément contient des vecteurs de longueur 6,4,2. C'est pourquoi, (1) a pour longueur totale `6+4+2 =`r 6+4+2``, alors que (2) renvoie `1+1+1=`r 1+1+1``. + +## Résumé + +La forme générale de la syntaxe de `data.table` est : + +```{r eval = FALSE} +DT[i, j, by] +``` + +Jusqu'ici nous avons vu que, + +#### En utilisant `i` : + +* Nous pouvons subdiviser les lignes comme dans un `data.frame` - sauf que vous n'avez pas besoin d'utiliser `DT$` de façon répétitive puisque les colonnes dans le contexte d'un `data.table` sont vues comme si elles étaient des *variables*. + +* Nous pouvons également trier un `data.table` en utilisant `order()`, qui utilise en interne l’algorithme de tri rapide de data.table pour de meilleures performances. + +Nous pouvons faire beaucoup plus dans `i` en créant une `data.table` avec clés, ce qui permet de réaliser rapidement les sous-ensembles et les jointures. Nous verrons cela dans les vignettes *"Clés et sous-ensembles basés sur la recherche binaire rapide"* et *"Jointures et jointures liées au temps"*. + +#### En utilisant `j` : + +1. Sélectionner les colonnes à la manière de `data.table` : `DT[, .(colA, colB)]`. + +2. Sélectionner les colonnes à la manière de `data.frame` : `DT[, c("colA", "colB")]`. + +3. Effectuer des calculs sur les colonnes : `DT[, .(sum(colA), mean(colB))]`. + +4. Indiquer les noms si nécessaire : `DT[, .(sA =sum(colA), mB = mean(colB))]`. + +5. Combiner avec `i` : `DT[colA > valeur, sum(colB)]`. + +#### En utilisant `by` : + +* En utilisant `by`, nous pouvons grouper par colonnes en spécifiant une *liste de colonnes* ou un *vecteur de caractères de noms de colonnes* ou même des *expressions*. La flexibilité de `j`, combinée à `by` et `i`, en fait une syntaxe très puissante. + +* `by` peut gérer plusieurs colonnes ainsi que des *expressions*. + +* Nous pouvons regrouper les colonnes par 'keyby' pour trier automatiquement les résultats groupés. + +* Nous pouvons utiliser `.SD` et `.SDcols` dans `j` pour opérer sur plusieurs colonnes en utilisant des fonctions de base déjà connues. Voici quelques exemples: + + 1. `DT[, lapply(.SD, fun), by = ..., .SDcols = ...]` - applique `fun` à toutes les colonnes spécifiées dans `.SDcols` tout en groupant par les colonnes spécifiées dans `by`. + + 2. `DT[, head(.SD, 2), by = ...]` - renvoie les deux premières lignes pour chaque groupe. + + 3. `DT[col > val, head(.SD, 1), by = ...]` - combine `i` avec `j` et `by`. + +#### Et souvenez-vous du conseil : + +Tant que `j` renvoie un objet `list`, chaque élément de la liste va devenir une colonne du `data.table` résultant. + +Nous verrons dans la vignette suivante comment *ajouter / mettre à jour / supprimer* des colonnes *par référence* et comment les combiner avec `i` et `by` . + +*** + +```{r, echo=FALSE} +setDTthreads(.old.th) +``` diff --git a/vignettes/fr/datatable-keys-fast-subset.Rmd b/vignettes/fr/datatable-keys-fast-subset.Rmd new file mode 100644 index 0000000000..40a111d1da --- /dev/null +++ b/vignettes/fr/datatable-keys-fast-subset.Rmd @@ -0,0 +1,501 @@ +--- +title: "Extraire des sous-ensemble avec les clés et la recherche binaire rapide" +date: "`r Sys.Date()`" +output: + markdown::html_format +vignette: > + %\VignetteIndexEntry{Extraire des sous-ensemble avec les clés et la recherche binaire rapide} + %\VignetteEngine{knitr::knitr} + \usepackage[utf8]{inputenc} +--- + +```{r, echo = FALSE, message = FALSE} +require(data.table) +knitr::opts_chunk$set( + comment = "#", + error = FALSE, + tidy = FALSE, + cache = FALSE, + collapse = TRUE) +.old.th = setDTthreads(1) +``` + +Cette vignette s'adresse à ceux qui sont déjà familiers avec la syntaxe de *data.table*, sa forme générale, comment extraire des sous-ensembles de lignes dans `i`, sélectionner et faire des opérations sur des colonnes, ajouter/modifier/supprimer des colonnes *par référence* dans `j` et grouper en utilisant `by`. Si vous n'êtes pas familier avec ces concepts, veuillez d'abord lire les vignettes *"Introduction à data.table"* et *"Sémantique de référence"*. + +*** + +## Données {#data} + +Nous utiliserons les mêmes données `flights` que dans la vignette *"Introduction à data.table"*. + +```{r echo = FALSE} +options(with = 100L) +``` + +```{r} +flights <- fread("../flights14.csv") +head(flights) +dim(flights) +``` + +## Introduction + +Dans cette vignette, nous allons + +* introduire le concept de `clé` dans une *data.table*, définir et utiliser des clés pour extraire des sous-ensembles basés sur une *recherche binaire rapide* dans `i`, + +* voir que nous pouvons combiner des sous-ensembles basés sur les clés avec `j` et `by` exactement de la même manière que précédemment, + +* voir d'autres arguments utiles - `mult` et `nomatch`, + +* et enfin conclure en examinant l'avantage de définir des clés - extraire des *sous-ensembles basés sur la recherche binaire rapide* et comparer avec l'approche traditionnelle du balayage vectoriel. + +## 1. Clés + +### a) Qu'est-ce qu'une *clé* ? + +Dans la vignette *"Introduction à data.table"*, nous avons vu comment sous-diviser des lignes dans `i` en utilisant des expressions logiques, des numéros de lignes et en utilisant `order()`. Dans cette section, nous allons voir une autre façon d'extraire des sous-ensembles de façon incroyablement rapide - en utilisant les *clés*. + +Mais tout d'abord, commençons par examiner les *data.frames*. Tous les *data.frames* ont un attribut de noms de lignes (row names). Considérons le *data.frame* `DF` ci-dessous. + +```{r} +set.seed(1L) +DF = data.frame(ID1 = sample(letters[1:2], 10, TRUE), + ID2 = sample(1:3, 10, TRUE), + val = sample(10), + stringsAsFactors = FALSE, + row.names = sample(LETTERS[1:10])) +DF + +rownames(DF) +``` + +Nous pouvons récupérer un *sous-ensemble* composé d'une ligne particulière en utilisant son nom de ligne comme indiqué ci-dessous : + +```{r} +DF["C", ] +``` + +autrement dit, les noms de lignes sont plus ou moins *un indice* des lignes d'un *data.frame*. Cependant, + +1. Chaque ligne est limitée à *exactement* un nom de ligne. + + Mais une personne (par exemple) a au moins deux noms - un *prénom* et un *second* nom. Il est utile d'organiser un annuaire téléphonique par *nom* puis *prénom*. + +2. Et les noms de ligne doivent être *uniques*. + + ```{r eval = FALSE} + rownames(DF) = sample(LETTERS[1:5], 10, TRUE) + + # Warning: non-unique values when setting 'row.names': 'C', 'D' + # Error in `.rowNamesDF<-`(x, value = value): duplicate 'row.names' are not allowed + ``` + +Nous allons maintenant le convertir en *data.table*. + +```{r} +DT = as.data.table(DF) +DT + +rownames(DT) +``` + +* Notez que les noms des lignes ont été réinitialisés. + +* Les *data.tables* n'utilisent jamais de noms de ligne. Puisque les *data.tables* **héritent** des *data.frames*, ils possèdent toujours l'attribut des noms de lignes (`row names`). Mais ils ne les utilisent jamais. Nous verrons dans un instant pourquoi. + + Si vous souhaitez conserver les noms des lignes, utilisez `keep.rownames = TRUE` dans `as.data.table()` - cela créera une nouvelle colonne appelée `rn` et attribuera les noms des lignes à cette colonne. + +Au lieu de cela, dans les *data.tables*, nous définissons et utilisons des `clés`. Pensez aux `clés` comme à des **"super" noms de lignes**. + +#### Les clés et leurs propriétés {#key-properties} + +1. Nous pouvons définir des clés sur *plusieurs colonnes* et les colonnes peuvent être de *différents types* -- *entier*, *numérique*, *caractère*, *facteur*, *entier64* etc. Les types *liste* et *complexe* ne sont pas encore supportés. + +2. L'unicité n'est pas requise, c'est-à-dire que les valeurs de clé dupliquées sont autorisées. Les lignes étant triées par clé, tout doublon dans les colonnes de la clé apparaîtra consécutivement. + +3. Définir une `clé` fait *deux* choses : + + a. les lignes de la *data.table* sont réorganisées physiquement en fonction des colonnes fournies *par référence*, toujours dans un ordre *incrémentiel*. + + b. ces colonnes sont marquées comme des colonnes de *clés* en définissant un attribut appelé `sorted` à *data.table*. + + Puisque les lignes sont réordonnées, une *data.table* ne peut avoir qu'une seule clé car elle ne peut pas être triée de plusieurs façons simultanément. + +Pour le reste de la vignette, nous travaillerons avec le jeu de données `flights`. + +### b) Définir, obtenir et utiliser des clés sur une *data.table* + +#### -- Comment définir la colonne `origin` comme clé dans la *data.table* `flights` ? + +```{r} +setkey(flights, origin) +head(flights) + +## nous pouvons aussi fournir des vecteurs de caractères à la fonction 'setkeyv()' +# setkeyv(flights, "origin") # utile pour la programmation +``` + +* Vous pouvez utiliser la fonction `setkey()` et fournir les noms des colonnes (sans les entourer de guillemets). Ceci est utile lors d'une utilisation interactive. + +* Alternativement, vous pouvez passer un vecteur de caractères contenant les noms de colonnes à la fonction `setkeyv()`. Cela est particulièrement utile lors de la conception de fonctions pour passer des colonnes à définir comme clé via des arguments de fonction. + +* Notez que nous n'avons pas eu besoin d'assigner le résultat à une variable. C'est parce que, comme la fonction `:=` que nous avons vue dans la vignette *"Sémantique de référence"*, `setkey()` et `setkeyv()` modifient l'entrée *data.table* *par référence*. Elles renvoient le résultat de manière invisible. + +* La *data.table* est maintenant réordonnée (ou triée) par la colonne que nous avons fournie - `origin`. Comme nous réorganisons par référence, nous n'avons besoin que de la mémoire supplémentaire d'une colonne dont la longueur est égale au nombre de lignes de la *data.table*, ce qui est donc très efficace en termes de mémoire. + +* Vous pouvez également définir les clés directement lors de la création de *data.tables* en utilisant la fonction `data.table()` avec l'argument `key`. Elle prend un vecteur de caractères de noms de colonnes. + +#### set* et `:=` : + +Dans *data.table*, l'opérateur `:=` et toutes les fonctions `set*` (par exemple, `setkey`, `setorder`, `setnames`, etc.) sont les seules qui modifient l'objet d'entrée *par référence*. + +Une fois que vous avez défini une *clé* pour une *data.table* par certaines colonnes, vous pouvez sous-sélectionner en interrogeant ces colonnes clés en utilisant la notation `.()` dans `i`. Rappelez-vous que `.()` est un *alias de* `list()`. + +#### -- Utilisez la colonne `origin` définie comme clé pour extraire toutes les lignes dont l'aéroport d'origine correspond à *"JFK"* + +```{r} +flights[.("JFK")] + +## ou alors : +# flights[J("JFK")] (ou) +# flights[list("JFK")] +``` + +* La `clé` a déjà été définie sur la colonne `origin`. Il suffit donc de fournir la valeur, ici *"JFK"*, directement. La syntaxe `.()` permet d'identifier que la tâche nécessite de rechercher la valeur *"JFK"* dans la colonne définie comme clé de *data.table* (ici la colonne `origin` du *data.table* `flights`). + +* Les *indices de ligne* correspondant à la valeur *"JFK"* dans `origin` sont obtenus en premier. Et comme il n'y a pas d'expression dans `j`, toutes les colonnes correspondant à ces indices de ligne sont renvoyées. + +* Pour une clé sur une seule colonne de type *caractère*, vous pouvez omettre la notation `.()` et utiliser les valeurs directement lors de l'extraction du sous-ensemble, comme si vous faisiez un sous-ensemble avec les noms de lignes dans un *data.frames*. + + ```{r eval = FALSE} + flights["JFK"] ## identique à flights[.("JFK")] + ``` + +* Nous pouvons extraire autant de valeurs que nécessaire + + ```{r eval = FALSE} + flights[c("JFK", "LGA")] ## same as flights[.(c("JFK", "LGA"))] + ``` + + Ceci renvoie toutes les colonnes correspondant aux lignes où la colonne `origin` correspond à *"JFK"* ou *"LGA"*. + +#### -- Comment obtenir la (les) colonne(s) d'une *data.table* définie(s) par une clé ? + +En utilisant la fonction `key()`. + +```{r} +key(flights) +``` + +* Elle renvoie un vecteur de caractères contenant toutes les colonnes définies comme clés. + +* Si aucune clé n'est définie, elle renvoie `NULL`. + +### c) Clés et colonnes multiples + +Pour rappel, les clés sont comme des noms de lignes *surpuissants*. Nous pouvons définir des clés sur plusieurs colonnes, et elles peuvent être de types multiples. + +#### -- Comment puis-je définir des clés sur les colonnes `origin` *et* `dest` ? + +```{r} +setkey(flights, origin, dest) +head(flights) + +## ou alors : +# setkeyv(flights, c("origin", "dest")) # fournir un vecteur de caractères pour les noms de colonnes + +key(flights) +``` + +* Cela trie la *data.table* d'abord par la colonne `origin` et ensuite par `dest` *par référence*. + +#### -- Extraire toutes les lignes en utilisant les colonnes définies comme clés où la première clé `origin` correspond à *"JFK"* et la deuxième clé `dest` correspond à *"MIA"* + +```{r} +flights[.("JFK", "MIA")] +``` + +#### Comment l'extraction du sous-ensemble fonctionne ici ? {#multiple-key-point} + +* Il est important de comprendre comment cela fonctionne en interne. *"JFK"* est d'abord comparé à la première colonne clé `origin`. Et *dans ces lignes correspondantes*, *"MIA"* est comparé à la deuxième colonne clé `dest` pour obtenir des *indices de ligne* où `origin` et `dest` correspondent aux valeurs données. + +* Étant donné qu'il n'y a pas d'expression dans `j`, nous renvoyons simplement *toutes les colonnes* correspondant à ces indices de ligne. + +#### -- Extraire toutes les lignes où seule la première colonne clé `origin` correspond à *"JFK"* + +```{r} +key(flights) + +flights[.("JFK")] ## ou dans ce cas simplement flights["JFK"], par commodité +``` + +* Puisque nous n'avons pas fourni de valeurs pour la deuxième colonne clé `dest`, il fait simplement correspondre *"JFK"* à la première colonne clé `origin` et renvoie toutes les lignes correspondantes. + +#### -- Extraire toutes les lignes où seule la deuxième colonne clé `dest` correspond à *"MIA"* + +```{r} +flights[.(unique(origin), "MIA")] +``` + +#### Que se passe-t-il ici ? + +* Relisez bien [ceci](#multiple-key-point). La valeur fournie pour la deuxième colonne clé *"MIA"* doit trouver les valeurs correspondantes dans la colonne clé `dest` *parmi les lignes correspondantes fournies par la première colonne clé `origin`*. Nous ne pouvons pas ignorer les valeurs des colonnes clés *précédentes*. Par conséquent, nous fournissons *toutes* les valeurs uniques de la colonne clé `origin`. + +* *"MIA"* est automatiquement recyclée pour s'adapter à la longueur de `unique(origin)` qui est de *3*. + +## 2. Combiner les clés avec `j` et `by` + +Tout ce que nous avons vu jusqu'à présent repose sur le même concept -- obtenir les *indices de lignes* dans `i`, mais en utilisant une méthode différente -- en utilisant des `clés`. Il n'est donc pas surprenant que nous puissions faire exactement les mêmes opérations pour `j` et `by`, comme vu dans les vignettes précédentes. Nous allons illustrer cela avec quelques exemples. + +### b) Sélection dans `j` + +#### -- Renvoie la colonne `arr_delay` sous forme de *data.table* correspondant à `origin = "LGA"` et `dest = "TPA"`. + +```{r} +key(flights) +flights[.("LGA", "TPA"), .(arr_delay)] +``` + +* Les *indices de ligne* correspondant à `origin == "LGA"` et `dest == "TPA"` sont obtenus à l'aide d'un *sous-ensemble basé sur une clé*. + +* Une fois que nous avons les indices des lignes, nous examinons `j` qui ne nécessite que la colonne `arr_delay`. Nous sélectionnons donc simplement la colonne `arr_delay` pour ces *indices de lignes* de la même manière que nous l'avons vu dans la vignette *Introduction à data.table*. + +* Nous aurions également pu renvoyer le résultat en utilisant `with = FALSE`. + + ```{r eval = FALSE} + flights[.("LGA", "TPA"), "arr_delay", with = FALSE] + ``` + +### b) Chaînage + +#### -- Sur la base du résultat obtenu ci-dessus, utilisez le chaînage pour trier la colonne dans l'ordre décroissant. + +```{r} +flights[.("LGA", "TPA"), .(arr_delay)][order(-arr_delay)] +``` + +### c) Calculer ou *exécuter* dans `j` + +#### -- Trouvez le retard d'arrivée maximal correspondant à `origin = "LGA"` et `dest = "TPA"`. + +```{r} +flights[.("LGA", "TPA"), max(arr_delay)] +``` + +* Nous pouvons vérifier que le résultat est identique à la première valeur (486) de l'exemple précédent. + +### d) *sous-affectation* par référence en utilisant `:=` dans `j` + +Nous avons déjà vu cet exemple dans la vignette *Sémantique de référence*. Jetons un coup d'œil à toutes les heures (`hour`) disponibles dans la *data.table* `flights` : + +```{r} +# récupère toutes les 'hours' de flights +flights[, sort(unique(hour))] +``` + +Nous voyons qu'il y a au total `25` valeurs uniques dans les données. Les heures *0* et *24* semblent toutes les deux être présentes. Allons-y et remplaçons *24* par *0*, mais cette fois en utilisant *key*. + +```{r} +setkey(flights, hour) +key(flights) +flights[.(24), hour := 0L] +key(flights) +``` + +* Nous définissons d'abord la clé (`key`) sur `hour`. Cela réorganise `flights` en fonction de la colonne `hour` et marque cette colonne comme `clé`. + +* Nous pouvons maintenant faire un sous-ensemble sur `hour` en utilisant la notation `.()`. Nous extrayons les valeurs pour *24* et obtenons les *indices des lignes* correspondants. + +* Et sur ces indices de lignes, nous remplaçons la colonne `clé` par la valeur `0`. + +* Comme nous avons remplacé les valeurs de la colonne *clé*, le *data.table* `flights` n'est plus triée par `hour`. Par conséquent, la clé a été automatiquement supprimée en la définissant sur NULL. + +Maintenant, Il ne devrait plus y avoir de *24* dans la colonne `hour`. + +```{r} +flights[, sort(unique(hour))] +``` + +### e) Agrégation avec `by` + +Remettons d'abord la clé sur `origin, dest`. + +```{r} +setkey(flights, origin, dest) +key(flights) +``` + +#### -- Obtenir le retard maximum de départ pour chaque mois (`month`) correspondant à `origin = "JFK"`. Classer les résultats par `mois` + +```{r} +ans <- flights["JFK", max(dep_delay), keyby = month] +head(ans) +key(ans) +``` + +* Nous extrayons un sous-ensemble à partir de la colonne `clé` *origin* pour obtenir les *indices des lignes* correspondant à *"JFK"*. + +* Une fois que nous avons obtenu les indices des lignes, nous n'avons besoin que de deux colonnes - `month` pour grouper et `dep_delay` pour obtenir `max()` pour chaque groupe. L'optimisation des requêtes de *data.table* permet d'extraire un sous-ensemble juste à partir de ces deux colonnes, correspondant aux *indices de lignes* obtenus dans `i`, pour la rapidité et l'efficacité mémoire. + +* Et sur ce sous-ensemble, nous regroupons par *mois* (*month*) et calculons `max(dep_delay)`. + +* Nous utilisons `keyby` pour définir automatiquement cette clé par *mois*. Nous comprenons maintenant ce que cela signifie. En plus de l'ordre, cela définit *month* comme la colonne `key`. + +## 3. Arguments supplémentaires - `mult` et `nomatch` + +### g) L'argument *mult* + +Nous pouvons choisir, pour chaque requête, si *"toutes"* les lignes correspondantes doivent être retournées, ou seulement la *"première"* ou la *"dernière"* en utilisant l'argument `mult`. La valeur par défaut est *"all"* - ce que nous avons vu jusqu'à présent. + +#### -- Extraire uniquement la première ligne correspondante parmi toutes les lignes où `origin` correspond à *"JFK"* et `dest` correspond à *"MIA"* + +```{r} +flights[.("JFK", "MIA"), mult = "first"] +``` + +#### -- Extraire uniquement la dernière ligne correspondante parmi toutes les lignes où `origin` correspond à *"LGA", "JFK", "EWR"* et `dest` correspond à *"XNA"* + +```{r} +flights[.(c("LGA", "JFK", "EWR"), "XNA"), mult = "last"] +``` + +* La requête *"JFK", "XNA"* ne correspond à aucune ligne dans `flights` et renvoie donc `NA`. + +* Encore une fois, la requête pour la deuxième colonne clé `dest`, *"XNA"*, est réutilisée pour correspondre à la longueur de la requête pour la première colonne clé `origin`, qui est de longueur de 3. + +### b) L'argument *nomatch* + +Nous pouvons choisir si les requêtes qui ne correspondent pas doivent renvoyer `NA` ou être ignorées en utilisant l'argument `nomatch`. + +#### -- D'après l'exemple précédent, extraire toutes les lignes uniquement si elles correspondent + +```{r} +flights[.(c("LGA", "JFK", "EWR"), "XNA"), mult = "last", nomatch = NULL] +``` + +* La valeur par défaut de `nomatch` est `NA`. En définissant `nomatch = NULL`, on ignore les requêtes qui n'ont pas de correspondance. + +* La requête "JFK", "XNA" ne correspond à aucune ligne dans `flights` et est donc ignorée. + +## 4. recherche binaire vs balayage vectoriel + +Nous avons vu jusqu'à présent comment définir et utiliser des clés pour extraire des sous-ensembles. Mais quel est l'avantage ? Par exemple, au lieu de faire : + +```{r eval = FALSE} +# clé par origin,dest columns +flights[.("JFK", "MIA")] +``` + +nous aurions pu faire : + +```{r eval = FALSE} +flights[origin == "JFK" & dest == "MIA"] +``` + +Un avantage évident est d'avoir une syntaxe plus courte. Mais plus encore, *extraire des sous-ensembles basés par recherche binaire* est **incroyablement rapide**. + +Au fil du temps, `data.table` bénéficie de nouvelles optimisations et actuellement, obtenir un sous-ensemble basé sur cette méthode applique automatiquement la *recherche binaire*. Afin d'utiliser la méthode lente par *balayage vectoriel*, la clé doit être supprimée. + +```{r eval = FALSE} +setkey(flights, NULL) +flights[origin == "JFK" & dest == "MIA"] +``` + +### a) Performance de l'approche par recherche binaire + +Pour illustrer cela, créons un *data.table* avec 20 millions de lignes et trois colonnes, avec pour clés les colonnes `x` et `y`. + +```{r} +set.seed(2L) +N = 2e7L +DT = data.table(x = sample(letters, N, TRUE), + y = sample(1000L, N, TRUE), + val = runif(N)) +print(object.size(DT), units = "Mb") +``` + +`DT` est de ~380Mo. Ce n'est pas vraiment énorme, mais suffisant pour illustrer le propos. + +D'après ce que nous avons vu dans la section Introduction à data.table, nous pouvons faire un sous-ensemble des lignes où les colonnes `x = "g"` et `y = 877` comme suit : + +```{r} +key(DT) +## (1) Méthode habituelle pour extraire un sous-ensemble - approche par balayage vectoriel +t1 <- system.time(ans1 <- DT[x == "g" & y == 877L]) +t1 +head(ans1) +dim(ans1) +``` + +Essayons maintenant de faire un sous-ensemble en utilisant des clés. + +```{r} +setkeyv(DT, c("x", "y")) +key(DT) +## (2) Sous-ensemble à l'aide de clés +t2 <- system.time(ans2 <- DT[.("g", 877L)]) +t2 +head(ans2) +dim(ans2) + +identical(ans1$val, ans2$val) +``` + +* Le gain de vitesse est d'envrion **~`r round(t1[3]/max(t2[3], .001))`x**! + +### b) Pourquoi le fait de définir une clé pour une *data.table* permet-il d'obtenir des sous-ensembles extrêmement rapides ? + +Pour comprendre cela, examinons d'abord ce que fait l'approche par *balayage vectoriel* (méthode 1). + +#### Approche par balayage vectoriel + +* La colonne `x` est parcourue ligne par ligne pour rechercher la valeur *"g"* parmi les 20 millions de lignes. Cela produit un *vecteur logique* de taille 20 millions, avec les valeurs `TRUE, FALSE ou NA` correspondant à la valeur de `x`. + +* De même, la colonne `y` est parcourue pour rechercher la valeur `877` parmi les 20 millions de lignes, et les résultats sont stockés dans un autre vecteur logique. + +* Ensuite, une opération élément par élément `&` est effectuée sur les vecteurs logiques intermédiaires et toutes les lignes où l'expression est évaluée à `TRUE` sont renvoyées. + +C'est ce que nous appelons une *approche par balayage vectoriel*. Cette méthode est assez inefficace, en particulier pour les tableaux volumineux ou lorsque des sous-ensembles doivent être créés de manière répétée, car elle doit parcourir toutes les lignes à chaque fois. + +Examinons maintenant l'approche de la recherche binaire (méthode 2). Rappelons que dans [Les clés et leurs propriétés](#key-properties) - *lorsque l’on définit des clés, cela réorganise la data.table selon les colonnes clés*. Étant donné que les données sont triées, nous n'avons pas besoin de *parcourir toute la longueur de la colonne* ! Nous pouvons utiliser *la recherche binaire* pour rechercher une valeur en `O(log n)` au lieu de `O(n)` dans le cas de *l'approche par balayage vectoriel*, où `n` est le nombre de lignes dans la *data.table*. + +#### Approche par recherche binaire + +Prenons un exemple très simple. Considérons les nombres (triés) ci-dessous : + +``` +1, 5, 10, 19, 22, 23, 30 +``` + +Supposons que nous voulions trouver la position correspondant à la valeur *1*, en utilisant la recherche binaire. Voici comment nous procéderions -(en sachant que les données sont *triées*). + +* Commencez par la valeur du milieu = 19. Est-ce que 1 == 19 ? Non. 1 < 19. + +* Comme la valeur recherchée est plus petite que 19, elle doit se trouver quelque part avant 19. Nous pouvons donc écarter le reste de la moitié qui est >= 19. + +* Notre ensemble est maintenant réduit à *1, 5, 10*. Prenons à nouveau la valeur centrale = 5. Est-ce que 1 == 5 ? Non. 1 < 5. + +* Notre ensemble est réduit à *1*. Est-ce que 1 == 1 ? Oui. L'indice correspondant est également 1. Et c'est la seule correspondance. + +Avec une approche de balayage vectoriel, nous aurions dû parcourir toutes les valeurs (ici, 7 valeurs). + +On peut constater qu'à chaque recherche, le nombre de recherches est réduit de moitié. C'est pourquoi la construction de sous-ensembles en utilisant la recherche binaire est **incroyablement rapide**. Étant donné que les lignes de chaque colonne des *data.tables* sont stockées de manière contiguë en mémoire, les opérations sont effectuées de manière très efficace en termes de cache (ce qui contribue également à la *vitesse*). + +De plus, comme nous obtenons directement les indices des lignes correspondantes sans avoir à créer ces énormes vecteurs logiques (égal au nombre de lignes d'un *data.table*), cette méthode est également très **très efficace en termes de mémoire**. + +## Résumé + +Dans cette vignette, nous avons appris une autre méthode pour subdiviser les lignes dans `i` en utilisant les clés d'une *data.table*. Définir des clés nous permet de créer des sous-ensembles extrêmement rapidement en utilisant la *recherche binaire*. En particulier, nous avons vu comment + +* définir une clé et utiliser cette clé pour créer des sous-ensembles dans une *data.table*. + +* utiliser les clés pour obtenir des *indices de lignes* en `i`, mais beaucoup plus rapidement. + +* combiner les sous-ensembles basés sur les clés avec `j` et `by`. Notez que les opérations `j` et `by` sont exactement les mêmes que précédemment. + +La création de sous-ensembles basés sur les clés est **incroyablement rapide** et particulièrement utile lorsque la tâche implique de créer des *sous-ensembles de manière répété*. Cependant, il peut ne pas toujours être souhaitable de définir une clé et de réorganiser physiquement la *data.table*. Dans la prochaine vignette, nous aborderons ce problème en utilisant une *nouvelle* fonctionnalité -- les *indices secondaires*. + +```{r, echo=FALSE} +setDTthreads(.old.th) +``` diff --git a/vignettes/fr/datatable-programming.Rmd b/vignettes/fr/datatable-programming.Rmd new file mode 100644 index 0000000000..efd990157e --- /dev/null +++ b/vignettes/fr/datatable-programming.Rmd @@ -0,0 +1,420 @@ +--- +title: "Programmation avec data.table" +date: "`r Sys.Date()`" +output: + markdown::html_format +vignette: > + %\VignetteIndexEntry{Programmation avec data.table} + %\VignetteEngine{knitr::knitr} + \usepackage[utf8]{inputenc} +--- + +```{r init, include = FALSE} +require(data.table) +knitr::opts_chunk$set( + comment = "#", + error = FALSE, + tidy = FALSE, + cache = FALSE, + collapse = TRUE +) +``` + +## Introduction + +`data.table`, dès ses premières versions, a permis l'utilisation des fonctions `subset` et `with` (ou `within`) en définissant la méthode `[.data.table`. `subset` et `with` sont des fonctions de base de R qui sont utiles pour réduire les répétitions dans le code, améliorer la lisibilité, et réduire le nombre total de caractères que l'utilisateur doit taper. Cette fonctionnalité est possible dans R grâce à une fonction unique appelée *évaluation paresseuse* ('lazy evaluation'). Cette fonctionnalité permet à une fonction de récupérer ses arguments, avant qu'ils ne soient évalués, et de les évaluer dans un cadre différente de celle dans laquelle ils ont été appelés. Récapitulons l'utilisation de la fonction `subset`. + +```{r df_print, echo=FALSE} +registerS3method("print", "data.frame", function(x, ...) { + base::print.data.frame(head(x, 2L), ...) + cat("...\n") + invisible(x) +}) +.opts = options( + datatable.print.topn=2L, + datatable.print.nrows=20L +) +``` + +```{r subset} +subset(iris, Species == "setosa") +``` + +Ici, `subset` prend le second argument et l'évalue dans le cadre du `data.frame` donné comme premier argument. Cela supprime le besoin de répéter les variables, ce qui réduit le risque d'erreurs et rend le code plus lisible. + +## Description du problème + +Le problème de ce type d'interface est qu'il n'est pas facile de paramétrer le code qui l'utilise. En effet, les expressions passées à ces fonctions sont substituées avant d'être évaluées. + +### Exemple + +```{r subset_error, error=TRUE, purl=FALSE} +my_subset = function(data, col, val) { + subset(data, col == val) +} +my_subset(iris, Species, "setosa") +``` + +### Approches du problème + +Il existe plusieurs façons de contourner ce problème. + +#### Éviter les *lazy evaluation* + +La solution la plus simple est d'éviter les *évaluations paresseuses* ('lazy evaluation'), et de se rabattre sur des approches moins intuitives et plus sujettes aux erreurs comme `df[["variable"]]`, etc. + +```{r subset_nolazy} +my_subset = function(data, col, val) { + data[data[[col]] == val & !is.na(data[[col]]), ] +} +my_subset(iris, col = "Species", val = "setosa") +``` + +Ici, nous calculons un vecteur logique de longueur `nrow(iris)`, puis ce vecteur est fourni à l'argument `i` de `[.data.frame` pour effectuer un sous-ensemble ordinaire basé sur un "vecteur logique". Pour s'aligner avec `subset()`, qui supprime aussi les NA, nous devons inclure une utilisation supplémentaire de `data[[col]]`. Cela fonctionne assez bien pour cet exemple simple, mais cela manque de flexibilité, introduit des répétitions de variables, et demande à l'utilisateur de changer l'interface de la fonction pour passer le nom de la colonne comme un caractère plutôt qu'un symbole sans guillemet. Plus l'expression à paramétrer est complexe, moins cette approche est pratique. + +#### Utilisation de `parse` / `eval` + +Cette méthode est généralement préférée par les nouveaux venus dans R, car elle est peut-être la plus simple sur le plan conceptuel. Cette méthode consiste à produire l'expression requise à l'aide de la concaténation de chaînes, à l'analyser, puis à l'évaluer. + +```{r subset_parse} +my_subset = function(data, col, val) { + data = deparse(substitute(data)) + col = deparse(substitute(col)) + val = paste0("'", val, "'") + text = paste0("subset(", data, ", ", col, " == ", val, ")") + eval(parse(text = text)[[1L]]) +} +my_subset(iris, Species, "setosa") +``` + +Nous devons utiliser `deparse(substitute(...))` pour récupérer les noms réels des objets passés à la fonction, afin de pouvoir construire l'appel à la fonction `subset` en utilisant ces noms originaux. Bien que cela offre une flexibilité illimitée avec une complexité relativement faible, **l'utilisation de `eval(parse(...))` devrait être évitée**. Les raisons principales sont les suivantes : + +- absence de validation syntaxique +- [vulnérabilité à l'injection de code](https://github.com/Rdatatable/data.table/issues/2655#issuecomment-376781159) +- existence de meilleures alternatives + +Martin Machler, R Project Core Developer, [a dit](https://stackoverflow.com/a/40164111/2490497) : + +> Désolé, mais je ne comprends pas pourquoi tant de gens pensent qu'une chaîne de caractères est quelque chose qui peut être évalué. Il faut vraiment changer d'état d'esprit. Oubliez toutes les connexions entre les chaînes d'un côté et les expressions, les appels, l'évaluation de l'autre côté. La (possible) seule connexion est via `parse(text = ....)` et tous les bons programmeurs R devraient savoir que c'est rarement un moyen efficace ou sûr de construire des expressions (ou des appels). Apprenez plutôt à connaître `substitute()`, `quote()`, et peut-être la puissance de l'utilisation de `do.call(substitute, ......)`. + +#### Calculs sur le langage + +Les fonctions mentionnées ci-dessus, ainsi que quelques autres (y compris `as.call`, `as.name`/`as.symbol`, `bquote`, et `eval`), peuvent être catégorisées comme des fonctions pour *calculer sur le langage*, puisqu'elles opèrent sur des objets du *langage* (par exemple `call`, `name`/`symbol`). + +```{r subset_substitute} +my_subset = function(data, col, val) { + eval(substitute(subset(data, col == val))) +} +my_subset(iris, Species, "setosa") +``` + +Ici, nous avons utilisé la fonction de base R `substitute` pour transformer l'appel `subset(data, col = val)` en `subset(iris, Species == "setosa")` en remplaçant `data`, `col`, et `val` par leurs noms (ou valeurs) d'origine dans leur environnement parent. Les avantages de cette approche par rapport aux précédentes devraient être clairs. Notez que parce que nous opérons au niveau des objets du langage, et que nous n'avons pas à recourir à la manipulation de chaînes de caractères, nous nous référons à cela comme *calcul sur le langage* ('computing on the language'). Il existe un chapitre dédié au *calcul sur le langage* dans le [Manuel du langage R](https://cran.r-project.org/doc/manuals/r-release/R-lang.html). Bien qu'il ne soit pas nécessaire pour *programmer sur data.table*, nous encourageons les lecteurs à lire ce chapitre afin de mieux comprendre cette fonctionnalité puissante et unique du langage R. + +#### Utiliser des packages tiers + +Il existe des packages tiers qui peuvent réaliser ce que les routines de calcul du R de base sur le langage font (`pryr`, `lazyeval` et `rlang`, pour n'en citer que quelques-uns). + +Bien qu'ils puissent être utiles, nous discuterons ici d'une approche propre à `data.table`. + +## Programmation sur data.table + +Maintenant que nous avons établi la bonne façon de paramétrer le code qui utilise l'évaluation paresseuse ('*lazy evaluation*'), nous pouvons passer au sujet principal de cette vignette, *la programmation sur data.table*. + +A partir de la version 1.15.0, data.table fournit un mécanisme robuste pour paramétrer les expressions passées aux arguments `i`, `j`, et `by` (ou `keyby`) de `[.data.table`. Il est construit sur la fonction de base R `substitute`, et imite son interface. Nous présentons ici `substitute2` comme une version plus robuste et plus conviviale de la fonction `substitute` de R de base. Pour une liste complète des différences entre `base::substitute` et `data.table::substitute2`, veuillez lire le [manuel `substitute2`](https://rdatatable.gitlab.io/data.table/library/data.table/html/substitute2.html). + +### Substitution de variables et de noms + +Disons que nous voulons une fonction générale qui applique une fonction à la somme de deux arguments auxquels une autre fonction a été appliquée. Comme exemple concret, nous avons ci-dessous une fonction qui calcule la longueur de l'hypoténuse dans un triangle droit, connaissant la longueur de ses côtés. + +${\displaystyle c = \sqrt{a^2 + b^2}}$ + +```{r hypotenuse} +square = function(x) x^2 +quote( + sqrt(square(a) + square(b)) +) +``` + +L'objectif est de faire en sorte que chaque nom dans l'appel ci-dessus puisse être passé en tant que paramètre. + +```{r hypotenuse_substitute2} +substitute2( + outer(inner(var1) + inner(var2)), + env = list( + outer = "sqrt", + inner = "square", + var1 = "a", + var2 = "b" + ) +) +``` + +Nous pouvons voir dans la sortie que les noms des fonctions, ainsi que les noms des variables passées à ces fonctions, ont été remplacés. Nous avons utilisé `substitute2` par commodité. Dans ce cas simple, le `substitute` de base R aurait pu être utilisé aussi, bien qu'il aurait fallu utiliser `lapply(env, as.name)`. + +Maintenant, pour utiliser la substitution à l'intérieur de `[.data.table`, nous n'avons pas besoin d'appeler la fonction `substitute2`. Comme elle est maintenant utilisée en interne, tout ce que nous avons à faire est de fournir l'argument `env`, de la même manière que nous l'avons fourni à la fonction `substitute2` dans l'exemple ci-dessus. La substitution peut être appliquée aux arguments `i`, `j` et `by` (ou `keyby`) de la méthode `[.data.table`. Notez que le fait de mettre l'argument `verbose` à `TRUE` peut être utilisé pour afficher les expressions après que la substitution ait été appliquée. Ceci est très utile pour le débogage. + +Utilisons le jeu de données `iris` comme démonstration. A titre d'exemple, imaginons que nous voulions calculer la `Sepal.Hypotenuse`, en traitant la largeur et la longueur du sépale comme s'il s'agissait des côtés d'un triangle rectangle. + +```{r hypotenuse_datable} +DT = as.data.table(iris) + +str( + DT[, outer(inner(var1) + inner(var2)), + env = list( + outer = "sqrt", + inner = "square", + var1 = "Sepal.Length", + var2 = "Sepal.Width" + )] +) + +# retourner le résultat sous forme de data.table +DT[, .(Species, var1, var2, out = outer(inner(var1) + inner(var2))), + env = list( + outer = "sqrt", + inner = "square", + var1 = "Sepal.Length", + var2 = "Sepal.Width", + out = "Sepal.Hypotenuse" + )] +``` + +Dans le dernier appel, nous avons ajouté un autre paramètre, `out = "Sepal.Hypotenuse"`, qui transmet le nom prévu de la colonne de sortie. Contrairement à `substitute` de base R, `substitute2` gérera également la substitution des noms des arguments d'appel. + +La substitution fonctionne également pour `i` et `by` (ou `keyby`). + +```{r hypotenuse_datable_i_j_by} +DT[filter_col %in% filter_val, + .(var1, var2, out = outer(inner(var1) + inner(var2))), + by = by_col, + env = list( + outer = "sqrt", + inner = "square", + var1 = "Sepal.Length", + var2 = "Sepal.Width", + out = "Sepal.Hypotenuse", + filter_col = "Species", + filter_val = I(c("versicolor", "virginica")), + by_col = "Species" + )] +``` + +### Remplacer des variables et des valeurs de caractères + +Dans l'exemple ci-dessus, nous avons vu une fonctionnalité pratique de `substitute2` : la conversion automatique de chaînes de caractères en noms/symboles. Une question évidente se pose : que se passe-t-il si nous voulons substituer un paramètre par une valeur *caractère*, afin d'avoir le comportement `substitute` de R de base. Nous fournissons un mécanisme pour échapper à la conversion automatique en enveloppant les éléments dans l'appel de base R `I()`. La fonction `I` marque un objet comme *AsIs*, empêchant ses arguments d'être convertis automatiquement de caractère à symbole. (Lisez la documentation `?AsIs` pour plus de détails.) Si le comportement de R de base est souhaité pour l'ensemble de l'argument `env`, alors il est préférable d'envelopper l'ensemble de l'argument dans `I()`. Alternativement, chaque élément de la liste peut être enveloppé dans `I()` individuellement. Explorons les deux cas ci-dessous. + +```{r rank} +substitute( # comportement de base de R + rank(input, ties.method = ties), + env = list(input = as.name("Sepal.Width"), ties = "first") +) + +substitute2( # imite le comportement "substitute" de base R en utilisant "I" + rank(input, ties.method = ties), + env = I(list(input = as.name("Sepal.Width"), ties = "first")) +) + +substitute2( # seuls certains éléments de env sont utilisés "AsIs" + rank(input, ties.method = ties), + env = list(input = "Sepal.Width", ties = I("first")) +) +``` + +Notez que la conversion s'effectue de manière récursive sur chaque élément de la liste, y compris le mécanisme d'échappement bien sûr. + +```{r substitute2_recursive} +substitute2( # tous sont des symboles + f(v1, v2), + list(v1 = "a", v2 = list("b", list("c", "d"))) +) +substitute2( # 'a' et 'd' doivent rester des chaines de caractères + f(v1, v2), + list(v1 = I("a"), v2 = list("b", list("c", I("d")))) +) +``` + +### Substituer des listes de longueur arbitraire + +L'exemple présenté ci-dessus illustre un moyen propre et puissant de rendre votre code plus dynamique. Cependant, il existe de nombreux autres cas beaucoup plus complexes auxquels un développeur peut être confronté. Un problème courant consiste à gérer une liste d'arguments de longueur arbitraire. + +Un cas d'utilisation évident pourrait être d'imiter la fonctionnalité `.SD` en injectant un appel `list` dans l'argument `j`. + +```{r splice_sd} +cols = c("Sepal.Length", "Sepal.Width") +DT[, .SD, .SDcols = cols] +``` + +Avec le paramètre `cols`, nous voudrions l'intégrer dans un appel `list`, en faisant ressembler l'argument `j` au code ci-dessous. + +```{r splice_tobe} +DT[, list(Sepal.Length, Sepal.Width)] +``` + +Le *'splicing'* est une opération où une liste d'objets doit être intégrée dans une expression comme une séquence d'arguments à appeler. Dans R de base, le 'splicing' de `cols` dans une `liste` peut être réalisé en utilisant `as.call(c(quote(list), lapply(cols, as.name)))`. De plus, à partir de R 4.0.0, il y a une nouvelle interface pour une telle opération dans la fonction `bquote`. + +Dans data.table, nous facilitons les choses en transformant automatiquement en liste une liste d'objets en un appel de liste avec ces objets. Cela signifie que tout objet `list` à l'intérieur de l'argument `env` list sera transformé en `call` list, rendant l'API pour ce cas d'utilisation aussi simple que présenté ci-dessous. + +```{r splice_datable} +# cela fonctionne +DT[, j, + env = list(j = as.list(cols)), + verbose = TRUE] + +# cela ne fonctionnera pas +#DT[, list(cols), +# env = list(cols = cols)] +``` + +Il est important de fournir un appel à `as.list`, plutôt qu'une simple liste, à l'intérieur de l'argument list de `env`, comme le montre l'exemple ci-dessus. + +Examinons plus en détail la question de l'ajout à la liste ('*enlist*-ing'). + +```{r splice_enlist} +DT[, j, # data.table met automatiquement en liste les listes imbriquées dans des appels de liste + env = list(j = as.list(cols)), + verbose = TRUE] + +DT[, j, # transformer la liste 'j' ci-dessus en un appel de liste + env = list(j = quote(list(Sepal.Length, Sepal.Width))), + verbose = TRUE] + +DT[, j, # la même chose que ci-dessus mais accepte un vecteur de caractères + env = list(j = as.call(c(quote(list), lapply(cols, as.name)))), + verbose = TRUE] +``` + +Essayons maintenant de passer une liste de symboles, plutôt qu'un appel de liste à ces symboles. Nous utiliserons `I()` pour échapper à la mise en liste (*enlist*-ing) automatique, mais comme cela désactivera aussi la conversion des caractères en symboles, nous devrons aussi utiliser `as.name`. + +```{r splice_not, error=TRUE, purl=FALSE} +DT[, j, # liste de symboles + env = I(list(j = lapply(cols, as.name))), + verbose = VRAI] + +DT[, j, # encore une fois de la meilleure façon, ajout automatique de la liste à l'appel de liste + env = list(j = as.list(cols)), + verbose = TRUE] +``` + +Notez que les deux expressions, bien qu'elles semblent visuellement identiques, ne le sont pas. + +```{r splice_substitute2_not} +str(substitute2(j, env = I(list(j = lapply(cols, as.name))))) + +str(substitute2(j, env = list(j = as.list(cols)))) +``` + +Pour une explication plus détaillée à ce sujet, veuillez consulter les exemples dans la [documentation `substitute2`](https://rdatatable.gitlab.io/data.table/library/data.table/html/substitute2.html). + +### Substitution d'une requête complexe + +Prenons l'exemple d'une fonction plus complexe, le calcul de la moyenne quadratique. + +${\displaystyle x_{\text{RMS}}={\sqrt{{\frac{1}{n}}\left(x_{1}^{2}+x_{2}^{2}+\cdots +x_{n}^{2}\right)}}}$ + +Il prend un nombre arbitraire de variables en entrée, mais maintenant nous ne pouvons pas simplement ajouter (splice) une liste d'arguments dans un appel de liste parce que chacun de ces arguments doit être enveloppé dans un appel `square`. Dans ce cas, nous devons faire l'opération à la main plutôt que de compter sur la transformation automatique en liste (*'enlist'*) de data.table. + +Tout d'abord, nous devons construire des appels à la fonction `square` pour chacune des variables (voir `inner_calls`). Ensuite, nous devons réduire la liste des appels en un seul appel, avec une séquence imbriquée d'appels `+` (voir `add_calls`). Enfin, nous devons substituer l'appel construit dans l'expression environnante (voir `rms`). + +```{r complexe} +outer = "sqrt" +inner = "square" +vars = c("Sepal.Length", "Sepal.Width", "Petal.Length", "Petal.Width") + +syms = lapply(vars, as.name) +to_inner_call = function(var, fun) call(fun, var) +inner_calls = lapply(syms, to_inner_call, inner) +print(inner_calls) + +to_add_call = function(x, y) call("+", x, y) +add_calls = Reduce(to_add_call, inner_calls) +print(add_calls) + +rms = substitute2( + expr = outer((add_calls) / len), + env = list( + outer = outer, + add_calls = add_calls, + len = length(vars) + ) +) +print(rms) + +str( + DT[, j, env = list(j = rms)] +) + +# idem, mais en sautant le dernier appel à substitute2 et en utilisant directement add_calls +str( + DT[, outer((add_calls) / len), + env = list( + outer = outer, + add_calls = add_calls, + len = length(vars) + )] +) + +# retourner le résultat en tant que data.table +j = substitute2(j, list(j = as.list(setNames(nm = c(vars, "Species", "rms"))))) +j[["rms"]] = rms +print(j) +DT[, j, env = list(j = j)] + +# ou alors : +j = as.call(c( + quote(list), + lapply(setNames(nm = vars), as.name), + list(Species = as.name("Species")), + list(rms = rms) +)) +print(j) +DT[, j, env = list(j = j)] +``` + +## Interfaces supprimées + +Dans `[.data.table`, il est aussi possible d'utiliser d'autres mécanismes pour la substitution de variables ou pour passer des expressions entre guillemets. Ceux-ci incluent `get` et `mget` pour l'injection en ligne de variables en fournissant leurs noms sous forme de chaînes, et `eval` qui indique à `[.data.table` que l'expression passée en argument est une expression entre guillemets et qu'elle doit être traitée différemment. Ces interfaces doivent maintenant être considérées comme retirées et nous recommandons d'utiliser le nouvel argument `env` à la place. + +### `get` + +```{r old_get} +v1 = "Petal.Width" +v2 = "Sepal.Width" + +DT[, .(total = sum(get(v1), get(v2)))] + +DT[, .(total = sum(v1, v2)), + env = list(v1 = v1, v2 = v2)] +``` + +### `mget` + +```{r old_mget} +v = c("Petal.Width", "Sepal.Width") + +DT[, lapply(mget(v), mean)] + +DT[, lapply(v, mean), + env = list(v = as.list(v))] + +DT[, lapply(v, mean), + env = list(v = as.list(setNames(nm = v)))] +``` + +### `eval` + +Au lieu d'utiliser la fonction `eval`, nous pouvons fournir une expression citée dans l'élément de l'argument `env`, aucun appel supplémentaire à `eval` n'est alors nécessaire. + +```{r old_eval} +cl = quote( + .(Petal.Width = mean(Petal.Width), Sepal.Width = mean(Sepal.Width)) +) + +DT[, eval(cl)] + +DT[, cl, env = list(cl = cl)] +``` + +```{r cleanup, echo=FALSE} +options(.opts) +registerS3method("print", "data.frame", base::print.data.frame) +``` diff --git a/vignettes/fr/datatable-reference-semantics.Rmd b/vignettes/fr/datatable-reference-semantics.Rmd new file mode 100644 index 0000000000..32651eac09 --- /dev/null +++ b/vignettes/fr/datatable-reference-semantics.Rmd @@ -0,0 +1,386 @@ +--- +title: "Sémantique de référence" +date: "`r Sys.Date()`" +output: + markdown::html_format +vignette: > + %\VignetteIndexEntry{Sémantique de référence} + %\VignetteEngine{knitr::knitr} + \usepackage[utf8]{inputenc} +--- + +```{r, echo = FALSE, message = FALSE} +require(data.table) +knitr::opts_chunk$set( + comment = "#", + error = FALSE, + tidy = FALSE, + cache = FALSE, + collapse = TRUE) +.old.th = setDTthreads(1) +``` + +Cette vignette traite de la sémantique de référence de *data.table* qui permet d'ajouter, de mettre à jour ou de supprimer des colonnes d'un *data.table par référence*, ainsi que de les combiner avec `i` et `by`. Elle s'adresse à ceux qui sont déjà familiers avec la syntaxe de *data.table*, avec sa forme générale, avec la façon de filtrer des lignes avec `i`, de sélectionner et calculer sur des colonnes, et d'effectuer des agrégations par groupe. Si vous n'êtes pas familier avec ces concepts, veuillez d'abord lire la vignette *"Introduction à data.table "*. + +*** + +## Données {#data} + +Nous utiliserons les mêmes données `flights` que dans la vignette *"Introduction à data.table"*. + +```{r echo = FALSE} +options(with = 100L) +``` + +```{r} +flights <- fread("../flights14.csv") +flights +dim(flights) +``` + +## Introduction + +Dans cette vignette, nous allons + +1. d’abord discuter brièvement les sémantiques de référence et examiner les deux formes différentes pour lesquelles l’opérateur `:=` peut être utilisé + +2. ensuite, voir comment ajouter/mettre à jour/supprimer des colonnes *par référence* dans `j` en utilisant l'opérateur `:=` et comment le combiner avec `i` et `by`. + +3. et enfin, nous examinerons l'utilisation de `:=` pour ses *effets secondaires* et comment nous pouvons éviter ces effets secondaires en utilisant `copy()`. + +## 1. Sémantique de référence + +Toutes les opérations que nous avons vues jusqu'à présent dans la vignette précédente ont abouti à un nouveau jeu de données. Nous allons voir comment *ajouter* de nouvelles colonnes, *mettre à jour* ou *supprimer* des colonnes existantes sur les données originales. + +### a) Contexte + +Avant d'examiner la *sémantique de référence*, considérons le *data.frame* ci-dessous : + +```{r} +DF = data.frame(ID = c("b", "b", "b", "a", "a", "c"), a = 1:6, b = 7:12, c = 13:18) +DF +``` + +Quand nous faisions : + +```{r eval = FALSE} +DF$c <- 18:13 # (1) -- remplacer toute une colonne +# ou +DF$c[DF$ID == "b"] <- 15:13 # (2) -- sous-assignation dans la colonne 'c' +``` + +À la fois (1) et (2) ont tous deux entraîné une copie profonde de l'ensemble du `data.frame` dans les versions de R < 3.1. [Ces version copiaient plus d’une fois](https://stackoverflow.com/q/23898969/559784). Pour améliorer les performances en évitant ces copies redondantes, *data.table* a utilisé l'opérateur [`:=` disponible mais inutilisé dans R](https://stackoverflow.com/q/7033106/559784). + +D’importantes améliorations de performance ont été réalisées dans `R v3.1`, à la suite desquelles seule une copie *superficielle* est faite pour (1) et non une copie *profonde*. Cependant, pour (2), la colonne entière est encore *copiée en profondeur* même dans `R v3.1+`. Cela signifie que plus on effectue de sous-assignations de colonnes dans une *même requête*, plus R fait de *copies profondes*. + +#### Copie *superficielle* vs copie *profonde* + +Une copie *superficielle* consiste uniquement en une copie du vecteur de pointeurs de colonnes (correspondant aux colonnes d'un *data.frame* ou d'un *data.table*). Les données réelles ne sont pas physiquement copiées en mémoire. + +Une copie *profonde*, en revanche, copie l'intégralité des données à un autre emplacement en mémoire. + +Lorsque l'on utilise `i` (par exemple, `DT[1:10]`) pour sélectionner des lignes dans une *data.table*, une copie *profonde* est effectuée. Cependant, lorsque `i` n'est pas fourni ou est égal à `TRUE`, une copie *superficielle* est faite. + +# + +Avec l'opérateur `:=` de *data.table*, absolument aucune copie n'est effectuée dans *les deux cas* (1) et (2), quelle que soit la version de R que vous utilisez. Cela s’explique par le fait que l’opérateur `:=` met à jour les colonnes de *data.table* en place (par référence). + +### b) L'opérateur `:=` + +Il peut être utilisé dans `j` de deux façons : + +(a) La forme `LHS := RHS` (côté gauche := côté droit) + +```{r eval = FALSE} +DT[, c("colA", "colB", ...) := list(valA, valB, ...)] + +# lorsque vous n'avez qu'une seule colonne à assigner +# vous pouvez omettre les guillemets et `list(), pour plus de commodité +DT[, colA := valA] +``` + +(b) La forme fonctionnelle + +```{r eval = FALSE} +DT[, `:=`(colA = valA, # valA est assigné à colA + colB = valB, # valB est assigné à colB + ... +)] +``` + +Notez que le code ci-dessus explique comment `:=` peut être utilisé. Ce ne sont pas des exemples pratiques. Nous en proposerons un premier avec le *data.table* `flights` dans la section suivante. + +# + +* Dans (a), `LHS` prend un vecteur de caractères de noms de colonnes et `RHS` une *liste de valeurs*. `RHS` doit juste être un objet `list`, indépendamment de la façon dont elle est générée (par exemple, en utilisant `lapply()`, `list()`, `mget()`, `mapply()`, etc.) Cette forme est généralement facile à programmer et est particulièrement utile lorsque vous ne connaissez pas à l'avance les colonnes auxquelles attribuer des valeurs. + +* En revanche, le point (b) est pratique si vous souhaitez commenter votre code (voir exemple sur `flights`). + +* Le résultat est renvoyé de manière *invisible*. + +* Puisque `:=` est disponible dans `j`, nous pouvons le combiner avec les opérations `i` et `by` tout comme les opérations d'agrégation que nous avons vues dans la vignette précédente. + +# + +Dans les deux formes de `:=` présentées ci-dessus, notez que nous n'assignons pas le résultat à une variable, parce que nous n'en avons pas besoin. La *data.table* en entrée est modifiée par référence. Prenons des exemples pour comprendre ce que nous entendons par là. + +Pour la suite de cette vignette, nous travaillerons avec la *data.table* `flights`. + +## 2. Ajouter/mettre à jour/supprimer des colonnes *par référence* + +### a) Ajouter des colonnes par référence {#ref-j} + +#### -- Comment ajouter les colonnes vitesse *speed* et retard total *total delay* de chaque vol à la *data.table* `flights` ? + +```{r} +flights[, `:=`(speed = distance / (air_time/60), # vitesse en mph (mi/h) + delay = arr_delay + dep_delay)] # retard en minutes +head(flights) + +## ou alors, en utilisant la forme 'LHS := RHS' +# flights[, c("speed", "delay") := list(distance/(air_time/60), arr_delay + dep_delay)] +``` + +#### Notez que + +* Nous n'avons pas eu à réaffecter le résultat à `flights`. + +* La *data.table* `flights` contient maintenant les deux colonnes nouvellement ajoutées. C'est ce que nous entendons par *ajouté par référence*. + +* Nous avons utilisé la forme fonctionnelle pour pouvoir ajouter des commentaires sur le côté afin d'expliquer ce que fait le calcul. Vous pouvez également voir la forme `LHS := RHS` (en commentaire). + +### b) Mise à jour de certaines lignes de colonnes par référence - *sous-assignation* par référence {#ref-i-j} + +Examinons toutes les heures (`hours`) disponibles dans la *data.table* `flights` : + +```{r} +# récupère toutes les heures de flights +flights[, sort(unique(hour))] +``` + +Nous constatons qu'il y a au total `25` valeurs uniques dans les données. Les heures *0* et *24* semblent toutes les deux être présentes. Remplaçons *24* par *0*. + +#### -- Remplacer les lignes où `hour == 24` par la valeur `0` + +```{r} +# sous-assignation par référence +flights[hour == 24L, hour := 0L] +``` + +* Nous pouvons utiliser `i` avec `:=` dans `j` de la même manière que nous l'avons déjà vu dans la vignette *"Introduction à data.table "*. + +* La colonne `hour` est remplacée par `0` uniquement sur les *indices de ligne* où la condition `hour == 24L` spécifiée dans `i` est évaluée à `TRUE`. + +* `:=` renvoie le résultat de manière invisible. Parfois, il peut être nécessaire de voir le résultat après l'affectation. Nous pouvons y parvenir en ajoutant des crochets vides `[]` à la fin de la requête, comme indiqué ci-dessous : + + ```{r} + flights[hour == 24L, hour := 0L][] + ``` + +# + +Regardons toutes les heures pour vérifier. + +```{r} +# vérifier à nouveau la présence de '24' +flights[, sort(unique(hour))] +``` + +#### Exercice : {#update-by-reference-question} + +Quelle est la différence entre `flights[hour == 24L, hour := 0L]` et `flights[hour == 24L][, hour := 0L]` ? Indice : le dernier a besoin d'une affectation (`<-`) si vous voulez utiliser le résultat plus tard. + +Si vous ne parvenez pas à le comprendre, consultez la section `Note` de ` ?":="`. + +### c) Suppression de colonne par référence + +#### -- Supprimer la colonne `delay` + +```{r} +flights[, c("delay") := NULL] +head(flights) + +## ou en utilisant la forme fonctionnelle +# flights[, `:=`(delay = NULL)] +``` + +#### {#delete-convenience} + +* Assigner `NULL` à une colonne *supprime* cette colonne. Et cela se produit *instantanément*. + +* Nous pouvons également passer des numéros de colonnes au lieu de noms dans le membre de gauche (`LHS`), bien qu'il soit de bonne pratique de programmation d'utiliser des noms de colonnes. + +* Lorsqu'il n'y a qu'une seule colonne à supprimer, nous pouvons omettre le `c()` et les guillemets doubles et simplement utiliser le nom de la colonne *sans guillemets*, pour plus de commodité. C'est-à-dire : + + ```{r eval = FALSE} + flights[, delay := NULL] + ``` + + est équivalent au code ci-dessus. + +### d) `:=` avec regroupement utilisant `by` {#ref-j-by} + +Nous avons déjà vu l'utilisation de `i` avec `:=` dans la [Section 2b] (#ref-i-j). Voyons maintenant comment nous pouvons utiliser `:=` avec `by`. + +#### -- Comment ajouter une nouvelle colonne qui contienne pour chaque paire `orig,dest` la vitesse maximale ? + +```{r} +flights[, max_speed := max(speed), by = .(origin, dest)] +head(flights) +``` + +* Nous ajoutons une nouvelle colonne `max_speed` en utilisant l'opérateur `:=` par référence. + +* Nous fournissons les colonnes pour le regroupement de la même manière qu’indiqué dans la vignette *Introduction à data.table*. Pour chaque groupe, `max(speed)` est calculé, ce qui renvoie une seule valeur. Cette valeur est recyclée pour s'adapter à la longueur du groupe. Encore une fois, aucune copie n'est faite. La *data.table* `flights` est modifié directement « sur place ». + +* Nous aurions également pu fournir à `by` un *vecteur de caractères* comme nous l'avons vu dans la vignette *Introduction à data.table*, par exemple en utilisant `by = c("origin", "dest")`. + +# + +### e) Colonnes multiples et `:=` + +#### -- Comment peut-on ajouter deux colonnes supplémentaires en calculant `max()` de `dep_delay` et `arr_delay` pour chaque mois, en utilisant `.SD` ? + +```{r} +in_cols = c("dep_delay", "arr_delay") +out_cols = c("max_dep_delay", "max_arr_delay") +flights[, c(out_cols) := lapply(.SD, max), by = month, .SDcols = in_cols] +head(flights) +``` + +* Nous utilisons la forme `LHS := RHS`. Nous stockons les noms des colonnes d'entrée et les nouvelles colonnes à ajouter dans des variables séparées, puis les fournissons à `.SDcols` et à `LHS` (pour une meilleure lisibilité). + +* Notez que puisque nous autorisons l'assignation par référence sans mettre les noms de colonnes entre guillemets lorsqu'il n'y a qu'une seule colonne comme expliqué dans la [Section 2c](#delete-convenience), nous ne pouvons pas faire `out_cols := lapply(.SD, max)`. Cela rajouterait une nouvelle colonne nommée `out_col`. À la place, nous devrions utiliser soit `c(out_cols)`, soit simplement `(out_cols)`. Envelopper le nom de la variable dans des parenthèses `(` est suffisant pour différencier les deux cas. + +* La forme `LHS := RHS` nous permet d'opérer sur plusieurs colonnes. Dans le membre de droite (RHS), pour calculer le `max` sur les colonnes spécifiées dans `.SDcols`, nous utilisons la fonction de base `lapply()` avec `.SD` de la même manière que nous l'avons vu précédemment dans la vignette *"Introduction to data.table "*. Ceci renvoie une liste de deux éléments, contenant la valeur maximale correspondant à `dep_delay` et `arr_delay` pour chaque groupe. + +# + +Avant de passer à la section suivante, nettoyons les colonnes nouvellement créées `speed`, `max_speed`, `max_dep_delay` et `max_arr_delay`. + +```{r} +# RHS est automatiquement recyclé à la longueur de LHS +flights[, c("speed", "max_speed", "max_dep_delay", "max_arr_delay") := NULL] +head(flights) +``` + +#### -- Comment peut-on mettre à jour plusieurs colonnes existantes par référence en utilisant `.SD` ? + +```{r} +flights[, names(.SD) := lapply(.SD, as.factor), .SDcols = is.character] +``` + +Nettoyons à nouveau et convertissons nos colonnes de facteurs nouvellement créées en colonnes de caractères. Cette fois, nous allons utiliser `.SDcols` qui accepte une fonction pour décider quelles colonnes inclure. Dans ce cas, `is.factor()` retournera les colonnes qui sont des facteurs. Pour en savoir plus sur le **S**ous-ensemble des **D**onnées (**S**ubset of the **D**ata), il y a aussi une [vignette sur l’utilisation de SD](https://cran.r-project.org/package=data.table/vignettes/datatable-sd-usage.html). + +Parfois, il est également utile de garder une trace des colonnes que nous transformons. Ainsi, même après avoir converti nos colonnes, nous pourrons toujours appeler les colonnes spécifiques que nous avons mises à jour. + +```{r} +factor_cols <- sapply(flights, is.factor) +flights[, names(.SD) := lapply(.SD, as.character), .SDcols = factor_cols] +str(flights[, ..factor_cols]) +``` + +#### {.bs-callout .bs-callout-info} + +* Nous aurions également pu utiliser `(factor_cols)` sur le membre de gauche (`LHS`) au lieu de `names(.SD)`. + +## 3. `:=` et `copy()` + +`:=` modifie l'objet d'entrée par référence. En dehors des fonctionnalités que nous avons déjà discutées, il arrive parfois que nous souhaitions utiliser la fonctionnalité de mise à jour par référence pour ses effets secondaires. À d’autres moments, il n'est pas souhaitable de modifier l'objet original, auquel cas nous pouvons utiliser la fonction `copy()`, comme nous le verrons dans un instant. + +### a) `:=` pour ses effets secondaires + +Supposons que nous voulions créer une fonction qui renvoie la vitesse maximale (*maximum speed*) pour chaque mois. Mais en même temps, nous aimerions aussi ajouter la colonne `speed` à *flights*. Nous pourrions écrire une petite fonction comme suit : + +```{r} +foo <- function(DT) { + DT[, speed := distance / (air_time/60)] + DT[, .(max_speed = max(speed)), by = month] +} +ans = foo(flights) +head(flights) +head(ans) +``` + +* Notez que la nouvelle colonne `speed` a été ajoutée à la *data.table* `flights`. C'est parce que `:=` effectue des opérations par référence. Puisque `DT` (l'argument de la fonction) et `flights` font référence au même objet en mémoire, la modification de `DT` se répercute également sur `flights`. + +* Et `ans` contient la vitesse maximale pour chaque mois. + +### b) La fonction `copy()` + +Dans la section précédente, nous avons utilisé `:=` pour son effet secondaire. Mais bien sûr, ce n'est pas toujours souhaitable. Parfois, nous voudrions passer un objet *data.table* à une fonction, et nous pourrions vouloir utiliser l'opérateur `:=`, mais *ne voudrions pas* mettre à jour l'objet original. Nous pouvons accomplir cela en utilisant la fonction `copy()`. + +La fonction `copy()` effectue une copie *profonde* de l'objet d'entrée, et donc, toutes les opérations de mise à jour par référence effectuées sur l'objet copié n'affecteront pas l'objet d'origine. + +# + +Il y a deux situations particulières où la fonction `copy()` est essentielle : + +1. Contrairement à ce que nous avons vu au point précédent, nous pouvons ne pas vouloir que les données d'entrée d'une fonction soient modifiées *par référence*. A titre d'exemple, considérons la tâche de la section précédente, sauf que nous ne voulons pas modifier `flights` par référence. + + Supprimons d'abord la colonne `speed` que nous avons générée dans la section précédente. + + ```{r} + flights[, vitesse := NULL] + ``` + Maintenant, nous pourrions accomplir la tâche comme suit : + + ```{r} + foo <- function(DT) { + DT <- copy(DT) ## copie profonde + DT[, speed := distance / (air_time/60)] ## n'affecte pas les vols + DT[, .(max_speed = max(speed)), by = month] + } + ans <- foo(flights) + head(flights) + head(ans) + ``` + +* L'utilisation de la fonction `copy()` n'a pas modifié la *data.table* `flights` par référence. Elle ne contient pas la colonne `speed`. + +* Et `ans` contient la vitesse maximale correspondant à chaque mois. + +Cependant, nous pourrions encore améliorer cette fonctionnalité en faisant une copie *superficielle* au lieu d'une copie *profonde*. En fait, nous aimerions beaucoup [fournir cette fonctionnalité pour `v1.9.8`](https://github.com/Rdatatable/data.table/issues/617). Nous reviendrons sur ce point dans la vignette *design de data.table*. + +# + +2. Lorsque nous stockons les noms de colonnes dans une variable, par exemple, `DT_n = names(DT)`, puis que nous *ajoutons/mettons à jour/supprimons* une ou plusieurs colonne(s) *par référence*, cela modifierait également `DT_n`, à moins que nous ne fassions `copy(names(DT))`. + + ```{r} + DT = data.table(x = 1L, y = 2L) + DT_n = names(DT) + DT_n + + ## ajouter une nouvelle colonne par référence + DT[, z := 3L] + + ## DT_n est également mis à jour + DT_n + + ## utiliser `copy()` + DT_n = copy(names(DT)) + DT[, w := 4L] + + ## DT_n n'est pas mis à jour + DT_n + ``` + +## Résumé + +#### L'opérateur `:=` + +* Il est utilisé pour *ajouter/mettre à jour/supprimer* des colonnes par référence. + +* Nous avons aussi vu comment utiliser `:=` avec `i` et `by` de la même manière que nous l'avons vu dans la vignette *Introduction à data.table*. Nous pouvons de la même manière utiliser `keyby`, enchaîner des opérations, et passer des expressions à `by` de la même manière. La syntaxe est *consistante*. + +* Nous pouvons utiliser `:=` pour ses effets secondaires ou utiliser `copy()` pour ne pas modifier l'objet original tout en mettant à jour par référence. + +```{r, echo=FALSE} +setDTthreads(.old.th) +``` + +# + +Jusqu'à présent, nous avons vu beaucoup d’opérations en `j`, et comment les combiner avec `by`, mais peu de choses concernant `i`. Tournons notre attention vers `i` dans la prochaine vignette *"Clés et sous-ensembles basés sur une recherche binaire rapide"* pour réaliser des *sous-ensembles ultra-rapides* en *utilisant des clés dans data.tables*. + +*** diff --git a/vignettes/fr/datatable-reshape.Rmd b/vignettes/fr/datatable-reshape.Rmd new file mode 100644 index 0000000000..a251e682bd --- /dev/null +++ b/vignettes/fr/datatable-reshape.Rmd @@ -0,0 +1,297 @@ +--- +title: "Restructurer efficacement avec les data.tables" +date: "`r Sys.Date()`" +output: + markdown::html_format +vignette: > + %\VignetteIndexEntry{Restructurer efficacement avec les data.tables} + %\VignetteEngine{knitr::knitr} + \usepackage[utf8]{inputenc} +--- + +```{r, echo = FALSE, message = FALSE} +require(data.table) +knitr::opts_chunk$set( + comment = "#", + error = FALSE, + tidy = FALSE, + cache = FALSE, + collapse = TRUE) +.old.th = setDTthreads(1) +``` + +Cette vignette traite de l'utilisation par défaut des fonctions de transformation `melt` (du format large au long) et `dcast` (du format long à large) pour les *data.tables* ainsi que des **nouvelles fonctionnalités étendues** de transformation `melt` et `cast` sur *plusieurs colonnes* disponibles depuis la version `v1.9.6`. + +*** + +```{r echo = FALSE} +options(with = 100L) +``` + +## Données + +Nous chargerons les ensembles de données directement dans chaque section. + +## Introduction + +Les fonctions `melt` et `dcast` pour `data.table` sont respectivement utilisées pour la restructuration de large en long et de long en large des données ; les implémentations sont spécifiquement conçues pour gérer de grandes quantités de données en mémoire (par exemple 10Go). + +Dans cette vignette, nous allons + +1. Examiner brievement l'utilisation par défaut des fonctions de transformation `melt` et `dcast` sur les data.tables pour les convertir du format *large* au format *long* et *vice versa* + +2. Examiner des scénarios où les fonctionnalités actuelles deviennent fastidieuses et inefficaces. + +3. Enfin, explorer les nouvelles améliorations apportées aux méthodes `melt` et `dcast` pour les objets de type `data.table` afin de gérer plusieurs colonnes simultanément. + +Les fonctionnalités étendues sont conformes à la philosophie de `data.table` qui consiste à effectuer des opérations de manière efficace et simple. + +## 1. Fonctionnalité par défaut + +### a) Transformation (`melt`) des colonnes dans une `data.table` (format large vers long) + +Supposons que nous ayons un `data.table` (données artificielles) comme indiqué ci-dessous : + +```{r} +s1 <- "family_id age_mother dob_child1 dob_child2 dob_child3 +1 30 1998-11-26 2000-01-29 NA +2 27 1996-06-22 NA NA +3 26 2002-07-11 2004-04-05 2007-09-02 +4 32 2004-10-10 2009-08-27 2012-07-21 +5 29 2000-12-05 2005-02-28 NA" +DT <- fread(s1) +DT + +## dob signifie date de naissance. + +str(DT) +``` + +#### - Convertir `DT` en format *long* où chaque `dob` est une observation séparée. + +Nous pouvons réaliser ceci en utilisant `melt()` en spécifiant les arguments `id.vars` et `measure.vars` comme suit : + +```{r} +DT.m1 = melt(DT, id.vars = c("family_id", "age_mother"), + measure.vars = c("dob_child1", "dob_child2", "dob_child3")) +DT.m1 +str(DT.m1) +``` + +* `measure.vars` spécifie l'ensemble des colonnes que nous souhaitons fusionner (ou combiner). + +* Nous pouvons également spécifier les *indices* des colonnes au lieu de leurs *noms*. + +* Par défaut, la colonne `variable` est de type `facteur`. Mettez l'argument `variable.factor` à `FALSE` si vous souhaitez retourner un vecteur de type *`caractère`* à la place. + +* Par défaut, les colonnes fusionnées sont automatiquement nommées `variable` et `value`. + +* `melt` préserve les attributs des colonnes. + +#### - Nommez les colonnes `variable` et `value` respectivement `child` et `dob` + +```{r} +DT.m1 = melt(DT, measure.vars = c("dob_child1", "dob_child2", "dob_child3"), + variable.name = "child", value.name = "dob") +DT.m1 +``` + +* Par défaut, lorsque l'une des variables `id.vars` ou `measure.vars` est manquante, les autres colonnes sont *automatiquement affectées* à l'argument manquant. + +* Lorsque ni `id.vars` ni `measure.vars` ne sont spécifiés, comme mentionné sous `?melt`, toutes les colonnes *non*-`numériques`, `intégrales`, `logiques` seront assignées à `id.vars`. + + De plus, un message d'avertissement est émis pour mettre en évidence les colonnes qui sont automatiquement considérées comme des `id.vars`. + +### b) Transformation (`dcast`) des lignes (format long au large) + +Dans la section précédente, nous avons vu comment passer de la forme large à la forme longue. Dans cette section, nous verrons l'opération inverse. + +#### - Comment revenir à la table de données originale `DT` à partir de `DT.m1` ? + +En d'autres termes, nous aimerions collecter toutes les observations *enfants* correspondant à chaque `family_id, age_mother` dans la même ligne. Nous pouvons le faire en utilisant la fonction `dcast` comme suit : + +```{r} +dcast(DT.m1, family_id + age_mother ~ child, value.var = "dob") +``` + +* `dcast` utilise la notation *formule* ( *formula* ). Les variables du côté gauche (*LHS*) de la formule correspondent aux variables *id* et celles sur le côté droit (*RHS*) aux variables *measure*. + +* `value.var` indique la colonne à remplir lors du passage au format large. + +* `dcast` essaie également de préserver les attributs du résultat dans la mesure du possible. + +#### - En partant de `DT.m1`, comment obtenir le nombre d'enfants dans chaque famille ? + +Vous pouvez également passer une fonction d'agrégation dans `dcast` avec l'argument `fun.aggregate`. Ceci est particulièrement essentiel lorsque la formule fournie ne permet pas d'identifier une seule observation pour chaque cellule. + +```{r} +dcast(DT.m1, family_id ~ ., fun.agg = function(x) sum(!is.na(x)), value.var = "dob") +``` + +Voir `?dcast` pour d'autres arguments utiles et des exemples supplémentaires. + +## 2. Limitations des approches actuelles `melt/dcast` + +Jusqu'à présent, nous avons vu des fonctionnalités de `melt` et `dcast` qui sont implémentées efficacement pour les objets `data.table`, en utilisant la machinerie interne de `data.table` (*tri par base rapide*, *recherche binaire* etc...). + +Cependant, il existe des situations où l'opération souhaitée ne s'exprime pas de manière simple. Par exemple, considérons l'objet `data.table` présenté ci-dessous : + +```{r} +s2 <- "family_id age_mother dob_child1 dob_child2 dob_child3 gender_child1 gender_child2 gender_child3 +1 30 1998-11-26 2000-01-29 NA 1 2 NA +2 27 1996-06-22 NA NA 2 NA NA +3 26 2002-07-11 2004-04-05 2007-09-02 2 2 1 +4 32 2004-10-10 2009-08-27 2012-07-21 1 1 1 +5 29 2000-12-05 2005-02-28 NA 2 1 NA" +DT <- fread(s2) +DT + +## 1 = femme, 2 = homme +``` + +Et vous aimeriez combiner (avec `melt`) toutes les colonnes `dob` ensemble, ainsi que toutes les colonnes `gender` ensemble. Avec la fonctionnalité actuelle, nous pouvons faire quelque chose comme ceci : + +```{r} +DT.m1 = melt(DT, id = c("family_id", "age_mother")) +DT.m1[, c("variable", "child") := tstrsplit(variable, "_", fixed = TRUE)] +DT.c1 = dcast(DT.m1, family_id + age_mother + child ~ variable, value.var = "value") +DT.c1 + +str(DT.c1) ## la colonne 'gender' est un type de caractère maintenant ! +``` + +#### Problèmes + +1. Ce que nous voulions faire était de combiner toutes les colonnes de type `dob` ensemble, et toutes les colonnes de type `gender` ensemble. Au lieu de cela, nous combinons tout, puis nous les scindons à nouveau. On voit aisément que c'est une approche détournée (et inefficace). + + Comme analogie, imaginez un placard avec quatre étagères de vêtements, et vous souhaitez rassembler les vêtements des étagères 1 et 2 (dans l'étagère 1), et ceux des étagères 3 et 4 (dans l'étagère 3). Ce que nous faisons, en quelque sorte, c'est de mélanger tous les vêtements ensemble, puis de les séparer à nouveau sur les étagères 1 et 3 ! + +2. Les colonnes à transformer (`melt`) peuvent être de types différents, comme c'est le cas ici (types `character` et `integer`). En les transformant toutes ensemble avec `melt`, les colonnes seront forcées d'être du même type, comme l'explique le message d'avertissement ci-dessus, et on le voit dans la sortie de str(DT.c1), où la colonne `gender` a été convertie en type `character`. + +3. Nous générons une colonne supplémentaire en scindant la colonne variable en deux colonnes, dont l'utilité est plutôt obscure. Nous faisons cela parce que nous en avons besoin pour la transformation (`cast`) dans l'étape suivante. + +4. Enfin, nous transformons le jeu de données. Mais le problème est qu'il s'agit d'une opération beaucoup plus coûteuse en calcul que *melt*. En particulier, il faut calculer l'ordre des variables dans la formule, ce qui est coûteux. + +En fait, `stats::reshape` est capable d'effectuer cette opération de manière très simple. C'est une fonction extrêmement utile et souvent sous-estimée. Vous devriez vraiment l'essayer ! + +## 3. (nouvelle) Fonctionnalité améliorée + +### a) `melt` améliorée + +Puisque nous aimerions que `data.table` effectue cette opération de façon simple et efficace en utilisant la même interface, nous avons donc implémenté une *fonctionnalité additionnelle*, où nous pouvons appliquer la fonction `melt` sur plusieurs colonnes *simultanément*. + +#### - Appliquer `melt` sur plusieurs colonnes simultanément + +L'idée est assez simple. Nous passons une liste de colonnes à `measure.vars`, où chaque élément de la liste contient les colonnes qui doivent être combinées ensemble. + +```{r} +colA = paste0("dob_child", 1:3) +colB = paste0("gender_child", 1:3) +DT.m2 = melt(DT, measure = list(colA, colB), value.name = c("dob", "gender")) +DT.m2 + +str(DT.m2) ## le type de col est préservé +``` + +* Nous pouvons supprimer la colonne `variable` si nécessaire. + +* Cette fonctionnalité est entièrement implémentée en C, ce qui la rend à la fois *rapide* et *économe en mémoire* en plus d'être *simple à utiliser*. + +#### - Utilisation de `patterns()` + +En général, dans ce type de problème, les colonnes que l'on souhaite transformer avec `melt` peuvent être distinguées par un motif commun. Nous pouvons utiliser la fonction `patterns()`, implémentée pour faciliter cette tâche, pour fournir des expressions régulières correspondant aux colonnes à combiner ensemble. L'opération ci-dessus peut alors être réécrite comme suit : + +```{r} +DT.m2 = melt(DT, measure = patterns("^dob", "^gender"), value.name = c("dob", "gender")) +DT.m2 +``` + +#### - Utilisation de `measure()` pour spécifier `measure.vars` via un séparateur ou un motif + +Si, comme dans les données ci-dessus, les colonnes d'entrée à transformer (`melt `) ont des noms réguliers, alors nous pouvons utiliser `measure`, qui permet de spécifier les colonnes à transformer via un séparateur ou une expression régulière. Par exemple, considérons les données `iris`, + +```{r} +(two.iris = data.table(datasets::iris)[c(1,150)]) +``` + +Les données iris possèdent quatre colonnes numériques avec une structure régulière : d'abord la partie de la fleur, suivie d'un point, puis le type de mesure. Pour spécifier que nous voulons transformer (`melt`) ces quatre colonnes, nous pouvons utiliser `measure` avec `sep="."` ce qui signifie utiliser `strsplit` sur tous les noms de colonnes ; les colonnes qui résultent en un nombre maximum de groupes après division seront utilisées comme `measure.vars` : + +```{r} +melt(two.iris, measure.vars = measure(part, dim, sep=".")) +``` + +Les deux premiers arguments de `measure` dans le code ci-dessus (`part` et `dim`) sont utilisés pour nommer les colonnes de sortie ; le nombre d'arguments doit être égal au nombre maximum de groupes après division avec `sep`. + +Si nous voulons deux colonnes de valeurs, une pour chaque partie, nous pouvons utiliser le mot-clé spécial `value.name`, qui signifie produire une colonne de valeurs pour chaque nom unique trouvé dans ce groupe : + +```{r} +melt(two.iris, measure.vars = measure(value.name, dim, sep=".")) +``` + +En utilisant le code ci-dessus, nous obtenons une colonne de valeurs par partie de fleur. Si nous voulons une colonne de valeurs pour chaque type de mesure, nous pouvons faire + +```{r} +melt(two.iris, measure.vars = measure(part, value.name, sep=".")) +``` + +En revenant à l'exemple des données sur les familles et les enfants, nous pouvons voir une utilisation plus complexe de `measure`, impliquant une fonction utilisée pour convertir les valeurs de la chaîne `child` en entiers : + +```{r} +DT.m3 = melt(DT, measure = measure(value.name, child=as.integer, sep="_child")) +DT.m3 +``` + +Dans le code ci-dessus, nous avons utilisé `sep="_child"`, ce qui entraîne la transformation des colonnes uniquement si elle contiennent cette chaîne (six noms de colonnes séparés en deux groupes chacun). L'argument `child=as.integer` signifie que le second groupe donnera lieu à une colonne de sortie nommée `child` avec des valeurs définies en appliquant la fonction `as.integer` aux chaînes de caractères de ce groupe. + +Enfin, nous considérons un exemple (emprunté au package tidyr) où nous devons définir les groupes à l'aide d'une expression régulière plutôt qu'un séparateur. + +```{r} +(who <- data.table(id=1, new_sp_m5564=2, newrel_f65=3)) +melt(who, measure.vars = measure( + diagnosis, gender, ages, pattern="new_?(.*)_(.)(.*)")) +``` + +Lorsque vous utilisez l'argument `pattern`, il doit s'agir d'une expression régulière compatible avec Perl contenant le même nombre de groupes de capture (sous-expressions entre parenthèses) que le nombre d'autres arguments (noms de groupes). Le code ci-dessous montre comment utiliser une expression régulière plus complexe avec cinq groupes, deux colonnes de sortie numériques et une fonction de conversion de type anonyme, + +```{r} +melt(who, measure.vars = measure( + diagnosis, gender, age, + ymin=as.numeric, + ymax=function(y) ifelse(nzchar(y), as.numeric(y), Inf), + pattern="new_?(.*)_(.)(([0-9]{2})([0-9]{0,2}))" +)) +``` + +### b) `dcast` améliorée + +Parfait ! Nous pouvons maintenant transformer (`melt`) plusieurs colonnes simultanément. Maintenant, étant donné le jeu de données `DT.m2`, comment pouvons-nous revenir au même format que le jeu de données avec lequel nous avons commencé ? + +Si nous utilisons la fonctionnalité actuelle de `dcast`, nous devrions effectuer la transformation via `cast` deux fois et combiner les résultats. Mais c'est une fois de plus verbeux, compliqué et inefficace. + +#### - Transformation (`cast`) de plusieurs `value.var`s simultanément + +Nous pouvons désormais fournir **plusieurs colonnes `value.var`** à `dcast` pour les objets `data.table` directement, de sorte que les opérations soient gérées en interne de manière efficace. + +```{r} +## nouvelle fonctionnalité 'cast' - plusieurs value.vars +DT.c2 = dcast(DT.m2, family_id + age_mother ~ variable, value.var = c("dob", "gender")) +DT.c2 +``` + +* Les attributs sont préservés dans le résultat dans la mesure du possible. + +* Tout est pris en charge de manière interne et efficace. En plus d'être rapide, il est également très économe en mémoire. + +# + +#### Plusieurs fonctions pour `fun.aggregate` : + +Vous pouvez également *plusieurs fonctions* à `fun.aggregate` dans `dcast` pour les *data.tables*. Consultez les exemples dans `?dcast` qui illustrent cette fonctionnalité. + +```{r, echo=FALSE} +setDTthreads(.old.th) +``` + +# + +*** diff --git a/vignettes/fr/datatable-sd-usage.Rmd b/vignettes/fr/datatable-sd-usage.Rmd new file mode 100644 index 0000000000..2ea29c19ed --- /dev/null +++ b/vignettes/fr/datatable-sd-usage.Rmd @@ -0,0 +1,256 @@ +--- +title: "Utiliser .SD pour l’analyse de données" +date: "`r Sys.Date()`" +output: + markdown::html_format: + options: + toc: true + number_sections: true +vignette: > + %\VignetteIndexEntry{Utiliser .SD pour l’analyse de données} + %\VignetteEngine{knitr::knitr} + \usepackage[utf8]{inputenc} +--- + +```{r, echo = FALSE, message = FALSE} +require(data.table) +knitr::opts_chunk$set( + comment = "#", + error = FALSE, + tidy = FALSE, + cache = FALSE, + collapse = TRUE, + out.width = '100%', + dpi = 144 +) +.old.th = setDTthreads(1) +``` + +Cette vignette explique les manières habituelles d'utiliser la variable `.SD` dans vos analyses de `data.table` . C'est une adaptation ce [cette réponse](https://stackoverflow.com/a/47406952/3576984) donnée sur StackOverflow. + +# C'est quoi `.SD` ? + +Au sens large, `.SD` est simplement un raccourci pour capturer une variable qui apparait fréquemment dans le contexte de l'analyse de données. Il faut comprendre *S* pour *S*ubset, *S*elfsame, ou *S*elf-reference et *D* pour *D*onnée. Ce qui donne, `.SD` qui dans sa forme la plus basique est une *référence réflexive* de la `data.table` elle-même -- comme nous le verrons dans les exemples ci-dessous, ceci est particulièrement utile pour chaîner ensemble les "requêtes" (extractions/sous-ensembles/etc... en utilisant `[`). E particulier cela signifie aussi que *`.SD` est lui-même une `data.table`* (avec la mise en garde qu'il ne peut être assigné avec `:=`). + +L'utilisation la plus simple de `.SD` est pour le sous-ensemble de colonnes (i.e., quand `.SDcols` est spécifié) ; comme cette version est beaucoup plus simple à comprendre, nous allons la couvrir en premier ci-dessous. L'interprétation de `.SD` dans sa seconde utilisation, les scénarios de regroupement (i.e., quand `by = ` ou `keyby = ` est spécifié), est légèrement différente, conceptuellement (bien qu'au fond ce soit la même chose, puisque, après tout, une opération non regroupée est un cas limite de regroupement avec un seul groupe). + +## Charger et afficher les données Lahman + +Pour rendre cela un peu plus concret, plutôt que de modifier les données, chargeons quelques ensembles de données concernant le baseball à partir de la [base de données Lahman](https://github.com/cdalzell/Lahman). Dans R typiquement, nous aurions simplement chargé ces ensembles de données du package R `Lahman`; dans cette vignette, nous les avons préchargés à la place, directement à partir de la page GitHub du package. + +```{r download_lahman} +load('../Teams.RData') +setDT(Teams) +Teams + +load('../Pitching.RData') +setDT(Pitching) +Pitching +``` + +Les lecteurs connaissant le jargon du baseball devraient trouver le contenu des tableaux familier ; `Teams` enregistre certaines statistiques pour une équipe et une année donnée, alors que `Pitching` enregistre les statistiques pour un lanceur et une année donnée. Veuillez lire la [documentation](https://github.com/cdalzell/Lahman) et explorer un peu les données avant d'aller plus loin afin de vous familiariser avec leur structure. + +# `.SD` sur des données non groupées + +Pour illustrer ce que l'on entend par nature réflexive de `.SD`, considérons son utilisation la plus banale : + +```{r plain_sd} +Pitching[ , .SD] +``` + +C'est à dire que `Pitching[ , .SD]` a simplement renvoyé la table complète, et c'est une manière exagérément verbeuse d'écrire `Pitching` ou `Pitching[]`: + +```{r plain_sd_is_table} +identical(Pitching, Pitching[ , .SD]) +``` + +En terme de sous-groupe, `.SD` est un sous-groupe des données, le plus évident (c'est l'ensemble lui-même). + +## Extraction de colonnes : `.SDcols` + +La première façon d'impacter ce que représente `.SD` c'est de limiter les *colonnes* contenues dans `.SD` en utilisant l'argument `.SDcols` dans `[` : + +```{r simple_sdcols} +# W: Wins; L: Losses; G: Games +Pitching[ , .SD, .SDcols = c('W', 'L', 'G')] +``` + +Ceci ne sert que d'illustration et était très ennuyeux. En plus d'accepter un vecteur de caractères `.SDcols` accepte également : + +1. toute fonction telle que `is.character` pour filtrer les *colonnes* +2. la fonction^*^ `patterns()` pour filtrer les *noms de colonnes* par expression régulière +3. les vecteurs entiers et logiques + +*voir `?patterns` pour davantage de détails + +Cette simple utilisation permet une large variété d'opérations avantageuses ou équivalentes de manipulation des données : + +## Convertir un type de colonne + +La conversion du type de colonne est une réalité en gestion des données. Bien que [`fwrite` a récemment gagné la possibilité de déclarer en amont la classe de chaque colonne](https://github.com/Rdatatable/data.table/pull/2545), chaque ensemble de données n'est pas forcément issu d'un `fread` (comme dans cette vignette) et les conversions alternatives parmi les types `character`, `factor`, et `numeric` sont courantes. Nous pouvons utiliser `.SD` et `.SDcols` pour convertir par lots des groupes de colonnes vers un type commun. + +Remarquons que les colonnes suivantes sont rangées en tant que `character` dans l'ensemble de données `Teams`, mais qu'elles pourraient avantageusement être rangées comme `factor` : + +```{r identify_factors} +# teamIDBR: Team ID utilisé par le site de référence du baseball +# teamIDlahman45: Team ID utilisé dans la base de données Lahman v4.5 +# teamIDretro: Team ID utilisé par Retrosheet +fkt = c('teamIDBR', 'teamIDlahman45', 'teamIDretro') +# confirmer que ce sont bien des `character` +str(Teams[ , ..fkt]) +``` + +La syntaxe pour convertir ces colonnes en `factor` est simple : + +```{r assign_factors} +Teams[ , names(.SD) := lapply(.SD, factor), .SDcols = patterns('teamID')] +# imprime la première colonne pour montrer que c’est correct +head(unique(Teams[[fkt[1L]]])) +``` + +Note : + +1. Le `:=` est un opérateur d'affectation qui permet de mettre à jour `data.table` sans faire de copie. Voir [reference semantics](https://cran.r-project.org/package=data.table/vignettes/datatable-reference-semantics.html) pour plus d'informations. +2. Le membre de gauche, `names(.SD)`, indique quelles colonnes nous mettons à jour - dans ce cas, nous mettons à jour l'intégralité de `.SD`. +3. Le membre de droite, `lapply()`, parcourt chaque colonne du `.SD` et convertit la colonne en un facteur. +4. Nous utilisons `.SDcols` pour sélectionner uniquement les colonnes qui ont le motif `teamID`. + +A nouveau, l'argument `.SDcols` est très souple ; nous avons fourni ci-dessus `patterns` mais nous aurions pu passer également `fkt` ou tout vecteur `character` de noms de colonnes. Dans d'autres situations, il est plus pratique de fournir un vecteur `integer` de *positions* des colonnes ou un vecteur de `booléens` indiquant pour chaque colonne s'il faut l'inclure ou l'exclure. Finalement nous utilisons une fonction pour filtrer les colonnes ce qui est très pratique. + +Par exemple nous pourrions faire ceci pour convertir toutes les colonnes `factor` en `character` : + +```{r sd_as_logical} +fct_idx = Teams[, which(sapply(.SD, is.factor))] # numéros de colonnes (changement de classe) +str(Teams[[fct_idx[1L]]]) +Teams[ , names(.SD) := lapply(.SD, as.character), .SDcols = is.factor] +str(Teams[[fct_idx[1L]]]) +``` + +Enfin, nous pouvons faire une correspondance basée sur les motifs des colonnes dans `.SDcols` pour sélectionner toutes les colonnes qui contiennent `team` vers `factor` : + +```{r sd_patterns} +Teams[ , .SD, .SDcols = patterns('team')] +Teams[ , names(.SD) := lapply(.SD, factor), .SDcols = patterns('team')] +``` + +** En plus de ce qui a été dit ci-dessus : *utiliser *explicitement* le numéro des colonnes (comme `DT[ , (1) := rnorm(.N)]`) n'est pas recommandé et peut conduire progressivement à obtenir un code corrompu au fil du temps si la position des colonnes change. Même l'utilisation implicite de numéros peut être dangereuse si nous ne gardons pas un contrôle intelligent et strict de l'ordre quand nous créons et utilisons l'index numéroté. + +## Contrôler le membre droit d'un modèle + +Modifier les spécifications du modèle est une fonctionnalité de base en analyse statistique robuste. Essayons de prédire l'ERA d'un lanceur (Earned Runs Average, moyenne des tournois gagnés, une mesure de performance) en utilisant le petit ensemble des covariables disponible dans la table `Pitching`. Comment varie la relation (linéaire) entre `W` (wins) et `ERA` en fonction des autres covariables que l'on inclut dans la spécification ? + +Voici une courte description qui évalue la puissance de `.SD` explorant cette question : + +```{r sd_for_lm, cache = FALSE, fig.cap="Ajustement OLS pour le coefficient W, diverses spécifications, représentées par des barres de couleurs distinctes."} +# ceci génère une liste des 2^k variables extra possibles +# pour les modèles de forme ERA ~ G + (...) +extra_var = c('yearID', 'teamID', 'G', 'L') +models = unlist( + lapply(0L:length(extra_var), combn, x = extra_var, simplify = FALSE), + recursive = FALSE +) + +# voici 16 couleurs distinctes, choisis dans une liste de 20 ici: +# https://sashat.me/2017/01/11/list-of-20-simple-distinct-colors/ +col16 = c('#e6194b', '#3cb44b', '#ffe119', '#0082c8', + '#f58231', '#911eb4', '#46f0f0', '#f032e6', + '#d2f53c', '#fabebe', '#008080', '#e6beff', + '#aa6e28', '#fffac8', '#800000', '#aaffc3') + +par(oma = c(2, 0, 0, 0)) +lm_coef = sapply(models, function(rhs) { + # utilisation de ERA ~ . et data = .SD, puis variation de + # quelles colonnes sont incluses dans .SD, ce qui nous permet + # de varier les iterations sur les 16 modèles facilement. + # coef(.)['W'] extrait le coefficient W de chaque modèle ajusté + Pitching[ , coef(lm(ERA ~ ., data = .SD))['W'], .SDcols = c('W', rhs)] +}) +barplot(lm_coef, names.arg = sapply(models, paste, collapse = '/'), + main = 'Wins Coefficient\nWith Various Covariates', + col = col16, las = 2L, cex.names = 0.8) +``` + +Le coefficient a toujours le signe attendu (les meilleurs lanceurs ont tendance à avoir plus de victoires et moins de tours autorisés), mais l'amplitude peut varier substantiellement en fonction de ce qui est contrôlé par ailleurs. + +## Jointures conditionnelles + +La syntaxe de `data.table` est belle par sa simplicité et sa robustesse. La syntaxe `x[i]` gère de manière souple trois approches communes du sous-groupement -- si `i` est un vecteur `booléen`, `x[i]` renvoie les lignes de `x` qui correspondent aux indices où `i` vaut `TRUE`; si `i` est une *autre `data.table`* (ou une `list`), une `jointure droite` (join right) est réalisée (dans la forme à plat, en utilisant les `clés` de `x` et `i`, sinon, si `on = ` est spécifié, en utilisant les colonnes qui correspondent); et si `i` est un caratère, il est interprété comme raccourci pour `x[list(i)]`, c'est à dire comme une jointure. + +C'est très bien en général, mais ce n'est pas suffisant lorsque nous souhaitons effectuer une "jointure conditionnelle", dans laquelle la nature exacte de la relation entre les tables dépend de certaines caractéristiques des lignes dans une ou plusieurs colonnes. + +Cet exemple est certes un peu artificiel, mais il illustre l'idée ; voir ici ([1](https://stackoverflow.com/questions/31329939/conditional-keyed-join-update-and-update-a-flag-column-for-matches), [2](https://stackoverflow.com/questions/29658627/conditional-binary-join-and-update-by-reference-using-the-data-table-package)) pour plus d'informations. + +Le but est d'ajouter une colonne `team_performance` à la table `Pitching` qui enregistre les performances de l'équipe (rang) du meilleur lanceur de chaque équipe (tel que mesuré par le ERA le plus faible, parmi les lanceurs ayant au moins 6 jeux enregistrés). + +```{r conditional_join} +# pour exclure les pichers ayant des performances exceptionnelles dans peu de jeux, +# faire un sous-ensemble ; ensuite définir le rang des pichers dans leur équipe chaque +# année (en général, nous nous focaliserions sur 'ties.method' de frank) +Pitching[G > 5, rank_in_team := frank(ERA), by = .(teamID, yearID)] +Pitching[rank_in_team == 1, team_performance := + Teams[.SD, Rank, on = c('teamID', 'yearID')]] +``` + +Notez que la syntaxe de `x[y]` renvoie `nrow(y)` values (c'est une jointure droite), c'est pourquoi `.SD` se trouve à droite dans `Teams[.SD]` (parce que le membre de droite de `:=` dans ce cas nécessite les valeurs de `nrow(Pitching[rank_in_team == 1])` ). + +# Opérations `.SD` groupées + +Nous aimerions souvent réaliser une opération sur nos données *au niveau groupe*. Si nous indiquons `by =` (ou `keyby = `), le modèle que nous imaginons mentalement pour ce qui se passe quand `data.table` traite `j` est de considérer que la `data.table` est constituée de plusieurs composants sous-`data.table`, dont chacun correspond à une seule valeur des variables du `by` : + +![Regroupement, illustré](../plots/grouping_illustration.png) + + + +En cas de groupement, `.SD` est multiple par nature -- il se réfère à *chaque* sous-`data.table, *une à la fois* (ou plus précisément, la visibilité de `.SD` est une sous-`data.table` unique). Ceci nous permet d'indiquer précisément une opération à réaliser sur *chaque sous-`data.table`* avant de réassembler et renvoyer le résultat. + +C'est utile pour diverses initialisations, les plus communes sont présentées ici : + +## Sous-groupes + +Essayons d'obtenir la saison la plus récente des données pour chaque équipe des données Lahman. Ceci peut être fait simplement avec : + +```{r group_sd_last} +# les données sont déjà triées par année ; si ce n’était pas le cas +# nous pourrions faire Teams[order(yearID), .SD[.N], by = teamID] +Teams[ , .SD[.N], by = teamID] +``` + +Rappelez-vous que `.SD` est lui-même une `data.table`, et que `.N` se rapporte au nombre total de lignes dans un groupe (c'est égal à `nrow(.SD)` à l'intérieur de chaque groupe), donc `.SD[.N]` renvoie la *totalité de `.SD`* pour la dernière ligne associée à chaque `teamID`. + +Une autre version commune de ceci est l'utilisation de `.SD[1L]` à la place, pour obtenir la *première* observation de chaque groupe, ou `.SD[sample(.N, 1L)]` pour renvoyer une ligne *aléatoire* pour chaque groupe. + +## Groupe Optima + +Supposons que nous voulions renvoyer la *meilleure* année pour chaque équipe, tel que mesuré par leur nombre total de tournois enregistrés (`R`; il est facile d'ajuster cela pour s'adapter à d'autres métriques, bien sûr). Au lieu de prendre un élément *fixe* de chaque sous-`data.table`, nous définissons maintenant *dynamiquement* l'indice souhaité ainsi : + +```{r sd_team_best_year} +Teams[ , .SD[which.max(R)], by = teamID] +``` + +Notez que cette approche peut bien sûr être combinée avec `.SDcols` pour renvoyer uniquement les portions de `data.table` pour chaque `.SD` (avec la mise en garde que `.SDcols` soit initialisé en fonction des différents sous-ensembles) + +*NB* : `.SD[1L]` est actuellement optimisé par [*`GForce`*](https://Rdatatable.gitlab.io/data.table/library/data.table/html/datatable-optimize.html) ([voir aussi](https://stackoverflow.com/questions/22137591/about-gforce-in-data-table-1-9-2)), `data.table` interne qui accélère massivement les opérations groupées les plus courantes comme `sum` ou `mean` -- voir ` ?GForce` pour plus de détails et gardez un oeil sur le support pour les demandes d'amélioration des fonctionnalités pour les mises à jour sur ce front : [1](https://github.com/Rdatatable/data.table/issues/735), [2](https://github.com/Rdatatable/data.table/issues/2778), [3](https://github.com/Rdatatable/data.table/issues/523), [4](https://github.com/Rdatatable/data.table/issues/971), [5](https://github.com/Rdatatable/data.table/issues/1197), [6](https://github.com/Rdatatable/data.table/issues/1414) + +## Régression groupée + +Revenons à la requête ci-dessus à propos des relations entre `ERA` et `W`; supposez que nous espérions que cette relation soit différente en fonction de l'équipe (c'est à dire que la pente soit différente pour chaque équipe). Nous pouvons facilement réexécuter cette régression pour explorer l'hétérogenéité dans cette relation comme ceci (en notant que les erreurs standard de cette approche sont généralement incorrectes -- la spécification `ERA ~ W*teamID` sera meilleurs -- cette approche est plus facile à lire et les *coefficients* sont OK) : + +```{r group_lm, results = 'hide', fig.cap="Histogramme de la distribution des coefficients ajustés. Il a plus ou moins une forme en cloche centrée autour de -.2"} +# Coefficients globaux pour comparaison +overall_coef = Pitching[ , coef(lm(ERA ~ W))['W']] +# utilisation du filtre .N > 20 pour exclure les équipes où il y a peu de données +Pitching[ , if (.N > 20L) .(w_coef = coef(lm(ERA ~ W))['W']), by = teamID + ][ , hist(w_coef, 20L, las = 1L, + xlab = 'Fitted Coefficient on W', + ylab = 'Number of Teams', col = 'darkgreen', + main = 'Team-Level Distribution\nWin Coefficients on ERA')] +abline(v = overall_coef, lty = 2L, col = 'red') +``` + +Tandis qu'il existe une grande hétérogénéité, la concentration autour de la valeur générale observée reste très distincte. + +Tout ceci n'est simplement qu'une brève introduction sur la puissance de `.SD` qui facilite la beauté et l'efficacité du code dans `data.table` ! + +```{r, echo=FALSE} +setDTthreads(.old.th) +``` diff --git a/vignettes/fr/datatable-secondary-indices-and-auto-indexing.Rmd b/vignettes/fr/datatable-secondary-indices-and-auto-indexing.Rmd new file mode 100644 index 0000000000..d25b0641c8 --- /dev/null +++ b/vignettes/fr/datatable-secondary-indices-and-auto-indexing.Rmd @@ -0,0 +1,332 @@ +--- +title: "Indices secondaires et auto-indexation" +date: "`r Sys.Date()`" +output: + markdown::html_format +vignette: > + %\VignetteIndexEntry{Indices secondaires et auto-indexation} + %\VignetteEngine{knitr::knitr} + \usepackage[utf8]{inputenc} +--- + +```{r, echo = FALSE, message = FALSE} +require(data.table) +knitr::opts_chunk$set( + comment = "#", + error = FALSE, + tidy = FALSE, + cache = FALSE, + collapse = TRUE) +.old.th = setDTthreads(1) +``` + +Cette vignette suppose que le lecteur est familier avec la syntaxe `[i, j, by]` de data.table, et sur la façon d’effectuer des sous-ensembles basés sur des clés rapides. Si vous n'êtes pas familier avec ces concepts, veuillez d'abord lire les vignettes *"Introduction à data.table"*, *"Sémantique de référence"* et *"Sous-ensembles basés sur les clés et la recherche binaire rapide"*. + +*** + +## Données {#data} + +Nous utiliserons les mêmes données `flights` que dans la vignette *"Introduction à data.table"*. + +```{r echo = FALSE} +options(width = 100L) +``` + +```{r} +flights <- fread("../flights14.csv") +head(flights) +dim(flights) +``` + +## Introduction + +Dans cette vignette, nous allons + +* discuter des *indices secondaires* et justifie leur nécessité en citant des cas où l'établissement de clés n'est pas nécessairement idéal, + +* effectuer un sous-ensemble rapide, une fois de plus, mais en utilisant le nouvel argument `on`, qui calcule des indices secondaires en interne pour la tâche (temporairement), et les réutilise s'il en existe déjà un, + +* et enfin, explorer l’*auto-indexation* qui va plus loin et crée des indices secondaires automatiquement, mais en utilisant la syntaxe native de R pour le sous-ensemble. + +## 1. Indices secondaires + +### a) Qu'est-ce qu'un indice secondaire ? + +Les indices secondaires sont similaires aux `clés` dans *data.table*, à l'exception de deux différences majeures : + +* Il ne réorganise pas physiquement l'ensemble de la table de données en RAM. Au lieu de cela, il calcule uniquement l'ordre pour l'ensemble des colonnes fournies et stocke ce *vecteur d'ordre* dans un attribut supplémentaire appelé `index`. + +* Il peut y avoir plus d'un index secondaire pour une table de données (comme nous le verrons plus loin). + +### b) Définir et obtenir des indices secondaires + +#### -- Comment définir la colonne `origin` comme index secondaire dans l’objet *data.table* `flights` ? + +```{r} +setindex(flights, origin) +head(flights) + +## nous pouvons aussi fournir des chaînes de caractères à la fonction ‘setindexv()’ +# setindexv(flights, "origin") # utile en programmation + +# attribut 'index' ajouté +names(attributes(flights)) +``` + +* `setindex` et `setindexv()` permettent d'ajouter un index secondaire à data.table. + +* Notez que `flights` n'est **pas** physiquement réordonné dans l'ordre croissant de `origin`, comme cela aurait été le cas avec `setkey()`. + +* Notez également que l'attribut `index` a été ajouté à `flights`. + +* `setindex(flights, NULL)` supprimerait tous les indices secondaires. + +#### -- Comment obtenir tous les indices secondaires définis jusqu'à présent dans `flights` ? + +```{r} +indices(flights) + +setindex(flights, origin, dest) +indices(flights) +``` + +* La fonction `indices()` renvoie tous les indices secondaires actuels dans la table data.table. Si aucun n'existe, `NULL` est retourné. + +* Notez qu'en créant un autre index sur les colonnes `origin, dest`, nous ne perdons pas le premier index créé sur la colonne `origin`, c'est-à-dire que nous pouvons avoir plusieurs index secondaires. + +### c) Pourquoi avons-nous besoin d'indices secondaires ? + +#### -- La réorganisation d'une table de données peut être coûteuse et n'est pas toujours idéale + +Considérons le cas où vous voudriez effectuer un sous-ensemble basé sur une clé rapide sur la colonne `origin` pour la valeur "JFK". Nous ferions cela comme suit : + +```{r, eval = FALSE} +## pas exécuté +setkey(flights, origin) +flights["JFK"] # or flights[.("JFK")] +``` + +#### `setkey()` nécessite de : + +a) calculer le vecteur d'ordre pour la (les) colonne(s) fournie(s), ici, `origin`, et + +b) réordonner l'ensemble du tableau de données, par référence, sur la base du vecteur d'ordre calculé. + +# + +Le calcul de l'ordre n'est pas la partie qui prend le plus de temps, puisque data.table utilise un vrai tri radix sur les vecteurs d'entiers, de caractères et de nombres. Cependant, réordonner le tableau data.table peut prendre du temps (en fonction du nombre de lignes et de colonnes). + +À moins que notre tâche n'implique un sous-ensemble répété sur la même colonne, le sous-ensemble basé sur une clé rapide pourrait effectivement être annulé par le temps nécessaire pour réorganiser, en fonction des dimensions de notre data.table. + +#### -- Il ne peut y avoir qu'une seule `clé` au maximum + +Maintenant, si nous voulons répéter la même opération mais sur la colonne `dest` à la place, pour la valeur "LAX", alors nous devons utiliser `setkey()`, *une fois de plus*. + +```{r, eval = FALSE} +## pas exécuté +setkey(flights, dest) +flights["LAX"] +``` + +Et cela réordonne les `vols` par `dest`, *encore une fois*. Ce que nous aimerions vraiment, c'est pouvoir effectuer le sous-ensemble rapidement en éliminant l'étape de réorganisation. + +Et c'est précisément ce que permettent les *indices secondaires* ! + +#### -- Les indices secondaires peuvent être réutilisés + +Comme il peut y avoir plusieurs indices secondaires et que la création d'un indice est aussi simple que le stockage du vecteur d'ordre en tant qu'attribut, cela nous permet même d'éliminer le temps nécessaire pour recalculer le vecteur d'ordre si un indice existe déjà. + +#### -- Le nouvel argument `on` permet une syntaxe plus propre ainsi que la création et la réutilisation automatiques d'indices secondaires + +Comme nous le verrons dans la section suivante, l'argument `on` présente plusieurs avantages : + +#### Argument `on` + +* permet d’effectuer des sous-ensembles en calculant les indices secondaires à la volée. Cela évite d'avoir à faire `setindex()` à chaque fois. + +* permet de réutiliser facilement les indices existants en vérifiant simplement les attributs. + +* permet une syntaxe plus propre en intégrant dans la syntaxe les colonnes sur lesquelles le sous-ensemble est effectué. Le code est ainsi plus facile à suivre lorsqu'on le consulte ultérieurement. + + Notez que l'argument `on` peut également être utilisé pour les sous-ensembles à clés. En fait, nous encourageons à fournir l'argument `on` même lorsque le sous-ensemble utilise des clés pour une meilleure lisibilité. + +# + +## 2. Sous-ensemble rapide utilisant l'argument `on` et les indices secondaires + +### a) Sous-ensembles rapides dans `i` + +#### -- Sous-ensemble de toutes les lignes où l'aéroport d'origine correspond à *"JFK"* en utilisant `on` + +```{r} +flights["JFK", on = "origin"] + +## ou alors +# flights[.("JFK"), on = "origin"] (or) +# flights[list("JFK"), on = "origin"] +``` + +* Cette instruction effectue également une recherche binaire rapide basée sur le sous-ensemble, en calculant l'index à la volée. Cependant, notez qu'elle n'enregistre pas automatiquement l'index en tant qu'attribut. Cela pourrait changer à l'avenir. + +* Si nous avions déjà créé un index secondaire en utilisant `setindex()`, alors `on` le réutiliserait au lieu de le (re)calculer. Nous pouvons le voir en utilisant `verbose = TRUE`: + + ```{r} + setindex(flights, origin) + flights["JFK", on = "origin", verbose = TRUE][1:5] + ``` + +#### -- Comment puis-je faire un sous-ensemble basé sur les colonnes `origin` *et* `dest` ? + +Par exemple, si nous voulons un sous-ensemble combinant `c("JFK", "LAX")`, alors : + +```{r} +flights[.("JFK", "LAX"), on = c("origin", "dest")][1:5] +``` + +* l’argument `on` accepte un vecteur de caractères de noms de colonnes correspondant à l'ordre fourni à `i-argument`. + +* Comme le temps de calcul de l'index secondaire est assez faible, nous n'avons pas besoin d'utiliser `setindex()`, sauf si, une fois de plus, la tâche implique un sous-ensemble répété sur la même colonne. + +### b) Sélection dans `j` + +Toutes les opérations que nous allons discuter ci-dessous ne sont pas différentes de celles que nous avons déjà vues dans la vignette *Clé et recherche binaire rapide basée sur un sous-ensemble*. Sauf que nous utiliserons l'argument `on` au lieu de définir des clés. + +#### -- Retourner la colonne `arr_delay` seule en tant que data.table correspondant à `origin = "LGA"` et `dest = "TPA"` + +```{r} +flights[.("LGA", "TPA"), .(arr_delay), on = c("origin", "dest")] +``` + +### c) Chaînage + +#### -- Sur la base du résultat obtenu ci-dessus, utilisez le chaînage pour classer la colonne par ordre décroissant. + +```{r} +flights[.("LGA", "TPA"), .(arr_delay), on = c("origin", "dest")][order(-arr_delay)] +``` + +### d) Calculer ou *do* dans `j` + +#### -- Trouvez le délai d'arrivée maximal correspondant à `origin = "LGA"` et `dest = "TPA"`. + +```{r} +flights[.("LGA", "TPA"), max(arr_delay), on = c("origin", "dest")] +``` + +### e) *sous-assignation* par référence en utilisant `:=` dans `j` + +Nous avons déjà vu cet exemple dans les vignettes *Sémantique des références* et *Clé et sous-ensemble basé sur la recherche binaire rapide*. Regardons toutes les `heures` disponibles dans le *data.table* `flights` : + +```{r} +# récupère toutes les 'hours' de flights +flights[, sort(unique(hour))] +``` + +Nous constatons qu'il y a au total `25` valeurs uniques dans les données. Les heures *0* et *24* semblent être présentes. Remplaçons *24* par *0*, mais cette fois-ci en utilisant `on` au lieu de définir des clés. + +```{r} +flights[.(24L), hour := 0L, on = "hour"] +``` + +Maintenant, vérifions si `24` est remplacé par `0` dans la colonne `hour`. + +```{r} +flights[, sort(unique(hour))] +``` + +* C'est notamment un énorme avantage des index secondaires. Auparavant, pour mettre à jour quelques lignes de `hour`, nous devions utiliser `setkey()` sur celui-ci, ce qui réorganisait inévitablement l'ensemble de la data.table. Avec `on`, l'ordre est préservé, et l'opération est beaucoup plus rapide ! En inspectant le code, la tâche que nous voulions effectuer est également assez claire. + +### f) Agrégation à l'aide de `by` + +#### -- Obtenir le retard maximum au départ pour chaque `mois` correspondant à `origine = "JFK"`. Classer les résultats par `mois` + +```{r} +ans <- flights["JFK", max(dep_delay), keyby = month, on = "origin"] +head(ans) +``` + +* Nous aurions dû remettre `key` à `origin, dest`, si nous n'avions pas utilisé `on` qui construit en interne des index secondaires à la volée. + +### g) L'argument *mult* + +Les autres arguments, y compris `mult`, fonctionnent exactement de la même manière que nous l'avons vu dans la vignette *Keys and fast binary search based subset*. La valeur par défaut de `mult` est "all". Nous pouvons choisir de ne renvoyer que les "premières" ou "dernières" lignes correspondantes. + +#### -- Sous-ensemble contenant uniquement la première ligne correspondante où `dest` correspond à *"BOS"* et *"DAY"* + +```{r} +flights[c("BOS", "DAY"), on = "dest", mult = "first"] +``` + +#### -- Sous-ensemble contenant uniquement la dernière ligne correspondante où `origin` correspond à *"LGA", "JFK", "EWR"* et `dest` correspond à *"XNA"* + +```{r} +flights[.(c("LGA", "JFK", "EWR"), "XNA"), on = c("origin", "dest"), mult = "last"] +``` + +### h) L'argument *nomatch* + +Nous pouvons choisir si les requêtes qui ne correspondent pas doivent retourner `NA` ou être ignorées en utilisant l'argument `nomatch`. + +#### -- D'après l'exemple précédent, le sous-ensemble de toutes les lignes n'est pris en compte que s'il y a une correspondance + +```{r} +flights[.(c("LGA", "JFK", "EWR"), "XNA"), mult = "last", on = c("origin", "dest"), nomatch = NULL] +``` + +* Aucun vol ne relie "JFK" à "XNA". Par conséquent, cette ligne est ignorée dans le résultat. + +## 3. Indexation automatique + +Dans un premier temps, nous avons étudié comment effectuer un sous-ensemble rapide à l'aide d'une recherche binaire en utilisant des *clés*. Ensuite, nous avons découvert que nous pouvions améliorer encore les performances et avoir une syntaxe plus propre en utilisant des indices secondaires. + +C'est ce que fait l'indexation automatique. Pour l'instant, il n'est implémenté que pour les opérateurs binaires `==` et `%in%`. Un indice est automatiquement créé *et* sauvegardé en tant qu'attribut. C'est-à-dire que contrairement à l'argument `on` qui calcule l'indice à la volée à chaque fois (à moins qu'il n'en existe déjà un), un indice secondaire est créé ici. + +Commençons par créer un tableau data.table suffisamment grand pour mettre en évidence l'avantage. + +```{r} +set.seed(1L) +dt = data.table(x = sample(1e5L, 1e7L, TRUE), y = runif(100L)) +print(object.size(dt), units = "Mb") +``` + +Lorsque nous utilisons `==` ou `%in%` sur une seule colonne pour la première fois, un indice secondaire est créé automatiquement, et il est utilisé pour effectuer le sous-ensemble. + +```{r} +## inspection de tous les noms d’attributs +names(attributes(dt)) + +## première exécution +(t1 <- system.time(ans <- dt[x == 989L])) +head(ans) + +## indice secondaire créé +names(attributes(dt)) + +indices(dt) +``` + +Le temps nécessaire pour créer un sous-ensemble la première fois est égal au temps nécessaire pour créer l'indice + le temps nécessaire pour créer un sous-ensemble. Étant donné que la création d'un indice secondaire n'implique que la création du vecteur d'ordre, cette opération combinée est plus rapide que les balayages vectoriels dans de nombreux cas. Mais le véritable avantage réside dans les sous-ensembles successifs. Ils sont extrêmement rapides. + +```{r} +## sous-ensembles successifs +(t2 <- system.time(dt[x == 989L])) +system.time(dt[x %in% 1989:2012]) +``` + +* L'exécution la première fois a pris `r sprintf("%.3f", t1["elapsed"])` secondes tandis que la deuxième fois, elle a pris `r sprintf("%.3f", t2["elapsed"])` secondes. + +* L'indexation automatique peut être désactivée en définissant l'argument global `options(datatable.auto.index = FALSE)`. + +* Désactiver l'indexation automatique permet toujours d'utiliser les index créés explicitement avec `setindex` ou `setindexv`. Vous pouvez désactiver complètement les index en définissant l'argument global `options(datatable.use.index = FALSE)`. + +# + +Dans la version récente, nous avons étendu l'indexation automatique aux expressions impliquant plus d'une colonne (combinées avec l'opérateur `&`). Dans le futur, nous prévoyons d'étendre la recherche binaire à d'autres opérateurs binaires comme `<`, `<=`, `>` et `>=`. + +Nous aborderons les *sous-ensembles* rapides utilisant des clés et des indices secondaires pour les *joints* dans la prochaine vignette, *"Joints et jointures roulantes"*. + +*** + +```{r, echo=FALSE} +setDTthreads(.old.th) +```