-
Notifications
You must be signed in to change notification settings - Fork 1k
Expand file tree
/
Copy pathdatatable-reference-semantics.Rmd
More file actions
385 lines (247 loc) · 18.7 KB
/
Copy pathdatatable-reference-semantics.Rmd
File metadata and controls
385 lines (247 loc) · 18.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
---
title: "Sémantique de référence"
date: "`{r} Sys.Date()`"
output:
litedown::html_format
vignette: >
%\VignetteIndexEntry{Sémantique de référence}
%\VignetteEngine{litedown::vignette}
\usepackage[utf8]{inputenc}
---
```{r, echo=FALSE, file='../_translation_links.R'}
```
`{r} .write.translation.links("Une traduction de ce document est disponible en : %s")`
```{r, echo = FALSE, message = FALSE}
require(data.table)
litedown::reactor(comment = "# ")
.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("datatable-intro", package="data.table")`](datatable-intro.html).
***
## Données {#data}
Nous utiliserons les mêmes données `flights` que dans la [`vignette("datatable-intro", package="data.table")`](datatable-intro.html).
```{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
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
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
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("datatable-intro", package="data.table")`](datatable-intro.html).
* 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
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("datatable-intro", package="data.table")`](datatable-intro.html), 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("datatable-intro", package="data.table")`](datatable-intro.html). 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 ('vignette("datatable-sd-usage", package="data.table")')](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("datatable-intro", package="data.table")`](datatable-intro.html). 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 (`vignette("datatable-keys-fast-subset", package="data.table")`)](datatable-keys-fast-subset.html) pour réaliser des *sous-ensembles ultra-rapides* en *utilisant des clés dans data.tables*.
***