-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdatatable-joins-fr.Rmd
More file actions
839 lines (643 loc) · 27.9 KB
/
datatable-joins-fr.Rmd
File metadata and controls
839 lines (643 loc) · 27.9 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
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
---
title: "Jointures avec data.table"
date: "`r Sys.Date()`"
output:
markdown::html_format
vignette: >
%\VignetteIndexEntry{Jointures avec 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)
```
Dans cette vignette nous apprendrons à réaliser les opérations de jointure en
utilisant les ressources de la syntaxe `data.table`.
Cela suppose que vous êtes déjà familiarisé avec la syntaxe `data.table` . Si ce
n'est pas le cas, reportez-vous aux vignettes suivantes :
- `vignette("datatable-intro", package="data.table")`
- `vignette("datatable-reference-semantics", package="data.table")`
- `vignette("datatable-keys-fast-subset", package="data.table")`
***
## 1. Définissons nos données d'exemple
Pour illustrer la méthode proposée avec des exemples de la vie courante, nous
allons simuler une **base de données normalisée** à partir d'un petit
supermarché en réalisant les étapes suivantes :
1. Définir une `data.table` où chaque produit est représenté par une ligne avec
quelques attributs, mais en laissant un produit sans `id` pour montrer
comment d'environnement réagit avec les ***valeurs absentes***.
```{r}
Products = data.table(
id = c(1:4,
NA_integer_),
name = c("banana",
"carrots",
"popcorn",
"soda",
"toothpaste"),
price = c(0.63,
0.89,
2.99,
1.49,
2.99),
unit = c("unit",
"lb",
"unit",
"ounce",
"unit"),
type = c(rep("natural", 2L),
rep("processed", 3L))
)
Products
```
2. Définir une `data.table` donnant le pourcentage des taxes à appliquer aux
produits traités en fonction de leurs unités.
```{r}
NewTax = data.table(
unit = c("unit","ounce"),
type = "processed",
tax_prop = c(0.65, 0.20)
)
NewTax
```
3. Définir une `data.table` simulant les produits reçus chaque lundi avec des
`product_id` non présents dans la table `Products` .
```{r}
set.seed(2156)
ProductReceived = data.table(
id = 1:10,
date = seq(from = as.IDate("2024-01-08"), length.out = 10L, by = "week"),
product_id = sample(c(NA_integer_, 1:3, 6L), size = 10L, replace = TRUE),
count = sample(c(50L, 100L, 150L), size = 10L, replace = TRUE)
)
ProductReceived
```
4. Définir une `data.table` pour afficher des promotions certains jours de la
semaine avec un autre `product_id` qui n'est pas dans la table `Products` .
```{r}
sample_date = function(from, to, size, ...){
all_days = seq(from = from, to = to, by = "day")
weekdays = all_days[wday(all_days) %in% 2:6]
days_sample = sample(weekdays, size, ...)
days_sample_desc = sort(days_sample)
days_sample_desc
}
set.seed(5415)
ProductSales = data.table(
id = 1:10,
date = ProductReceived[, sample_date(min(date), max(date), 10L)],
product_id = sample(c(1:3, 7L), size = 10L, replace = TRUE),
count = sample(c(50L, 100L, 150L), size = 10L, replace = TRUE)
)
ProductSales
```
## 2. Syntaxe de la jointure `data.table`
Avant de voir les avantages de la syntaxe `data.table` pour faire des opérations
de jointure nous devons savoir quels arguments peuvent nous aider à les réaliser
avec succès.
Le diagramme suivant affiche une description pour chaque argument de base. Dans les
sections suivantes nous verrons progressivement comment les utiliser tout en
apportant un peu de complexité à chaque fois.
```
x[i, on, nomatch]
| | | |
| | | \__ Si NULL, renvoie seulement les lignes liées des tables x et i
| | \____ vecteur de char ou liste définissant la logique de correspondance
| \_____ data.table primaire, liste ou data.frame
\____ data.table secondaire
```
> N'oubliez pas que l'ordre standard des arguments dans data.table est
`dt[i, j, by]`. Pour les opérations de jointure, il est recommandé de passer
les arguments `on` et `nomatch` par nom pour éviter d'utiliser `j` et `by` quand
ce n'est pas nécessaire.
## 3. Jointures équilibrées
C'est le cas général et facile où il y a des éléments communs entre les tables à
combiner.
La relation entre les tables peut être :
- **De un vers un**: lorsque chaque valeur sélectionnée est unique dans chaque table.
- **De un vers plusieurs** : lorsque certaines valeurs sélectionnées sont répétées
dans une table et à la fois uniques dans l'autre.
- **De plusieurs vers plusieurs** : lorsque les valeurs sélectionnées sont répétées
plusieurs fois dans chaque table.
Dans la plupart des exemples suivants nous réaliserons des correspondances *de un
vers plusieurs* , mais nous prendrons aussi le temps d'expliquer les ressources
disponibles pour réaliser les correspondances *de plusieurs vers plusieurs* .
### 3.1. Jointure droite
Utilisez cette méthode pour combiner les colonnes de deux tables en se basant sur
une ou plusieurs références mais ***en gardant toutes les lignes présentes dans
la table située à droite (entre crochets)***.
Dans le contexte de notre supermarché nous pouvons réaliser une jointure droite
pour voir davantage de détails à propos des produits reçus parce que c'est une
relation de *un vers plusieurs* en passant un vecteur dans l'argument `on`.
```{r}
Products[ProductReceived,
on = c(id = "product_id")]
```
Comme beaucoup de choses ont changé, nous allons expliquer les nouvelles
caractéristiques dans les groupes suivants :
- **Niveau colonne**
- Le *premier groupe* de colonnes dans le nouveau data.table vient de la table
`x` .
- Le *second groupe* de colonnes de la nouvelle data.table vient de la table `i`.
- Si l'opération de jointure fait apparaître un **conflit de nom** (les deux
tables ont le même nom de colonne) le ***prefixe*** `i.` est ajouté aux noms
des colonnes de la **table de droite** (table en position `i`).
- **Niveau ligne**
- Le `product_id` qui est absent, mais présent dans la table `ProductReceived` à
la ligne 1 a été sélectionné avec le `id` absent de la table `Products`, donc
***les valeurs `NA` sont traitées comme les autres valeurs***.
- Toutes les lignes de la table `i` ont été gardées y compris :
- Les lignes qui ne correspondent pas, comme celle avec `product_id = 6`.
- Lignes qui répètent les même `product_id` plusieurs fois.
#### 3.1.1. Jointure avec un argument de liste
Si vous suivez la vignette, vous avez pu voir que nous avons utilisé un vecteur
pour définir les relations entre les tables dans l'argument `on`, ce qui
réellement utile si vous **créez vos propres fonctions**, mais une autre
alternative est d'utiliser une **liste** pour définir les colonnes à
sélectionner.
Pour utiliser cette capacité, nous avons a disposition les alternatives
suivantes :
- Inclure les colonnes associées dans la fonction R de base `list` .
```{r,
Products[ProductReceived,
on = list(id = product_id)]
```
- Inclure les colonnes associées dans la data.table `list`alias `.`.
```{r,
Products[ProductReceived,
on = .(id = product_id)]
```
#### 3.1.2. Alternatives pour définir l'argument `on`
Dans tous les exemples précédents, nous avons passé les noms des colonnes à
sélectionner avec l'argument `on` mais `data.table` possède également des
alternatives à cette syntaxe.
- **Jointure naturelle**: sélectionne les colonnes pour réaliser la
correspondance en fonction des noms des colonnes communes. Pour illustrer
cette méthode, modifions la colonne de la table `Products` de `id` en
`product_id` et utilisons le mot clé `.NATURAL`.
```{r}
ProductsChangedName = setnames(copy(Products), "id", "product_id")
ProductsChangedName
ProductsChangedName[ProductReceived, on = .NATURAL]
```
- **Jointure par clé**: sélectionne les colonnes pour rechercher la
correspondance en fonction des colonnes clé quelque soit leur nom. Pour
illustrer cette méthode, nous devons définir les clés dans le même ordre pour
les deux tables.
```{r}
ProductsKeyed = setkey(copy(Products), id)
key(ProductsKeyed)
ProductReceivedKeyed = setkey(copy(ProductReceived), product_id)
key(ProductReceivedKeyed)
ProductsKeyed[ProductReceivedKeyed]
```
#### 3.1.3. Opérations après la jointure
La plupart du temps après avoir terminé une jointure nous souhaitons faire des
adaptations supplémentaires. Pour cela plusieurs alternatives vous sont
proposées :
- Chaîner une nouvelle instruction en ajoutant une paire de crochets `[]`.
- En passant comme argument `j` une liste des colonnes que l'on veut conserver ou
créer.
Notre recommendation est d'utiliser la seconde alternative si possible, car elle
est **plus rapide** et demande **moins de mémoire** que la première.
##### Gestion de la colonne partagée Names avec l'argument j
L'argument `j` autorise plusieurs alternatives intéressantes pour gérer les
jointures avec les tables en **partageant les mêmes noms de plusieurs
colonnes**. Par défaut toutes les colonnes prennent leur source dans la table
`x`, mais nous pouvons aussi utiliser le préfixe `x.` pour clarifier la source
et utiliser le préfixe `i.` pour toutes les colonnes de la table déclarée dans
l'argument `i` de la table `x`.
Si nous retournons au petit supermarché, après avoir mis à jour la table
`ProductReceived` avec la table `Products` , il semble pratique d'appliquer les
modifications suivantes :
- Modifier le nom des colonnes de `id` en `product_id` et de `i.id` en
`received_id`.
- Ajout de `total_value`.
```{r}
Products[
ProductReceived,
on = c("id" = "product_id"),
j = .(product_id = x.id,
name = x.name,
price,
received_id = i.id,
date = i.date,
count,
total_value = price * count)
]
```
##### Résumé avec on dans data.table
Nous pouvons aussi utiliser cette alternative pour renvoyer les résultats agrégés
enn fonction des colonnes présentes dans la table `x` .
Par exemple on pourrait s'intéresser à combien on dépense à acheter des produits
à chaque date quelque soit ces produits.
```{r}
dt1 = ProductReceived[
Products,
on = c("product_id" = "id"),
by = .EACHI,
j = .(total_value_received = sum(price * count))
]
dt2 = ProductReceived[
Products,
on = c("product_id" = "id"),
][, .(total_value_received = sum(price * count)),
by = "product_id"
]
identical(dt1, dt2)
```
#### 3.1.4. Jointure basée sur plusieurs colonnes
Jusqu'à présent, nous avons réalisé les jointures en se basant sur une colonne
`data.table`, mais il est important de savoir que le package peut joindre des
tables en prenant en compte plusieurs colonnes.
Pour illustrer cela supposons que nous voulions ajouter `tax_prop` de `NewTax`
pour **mettre à jour** la table `Products`.
```{r}
NewTax[Products, on = c("unit", "type")]
```
### 3.2. Jointure interne
Utilisez cette méthode pour combiner les colonnes de deux tables en se basant sur
une ou plusieurs références mais ***en gardant seulement les lignes
sélectionnées dans les deux tables***.
Pour réaliser cette opération il suffit d'ajouter `nomatch = NULL` ou
`nomatch = 0` à l'une quelconque des opérations de jointure précédentes pour
renvoyer le même résultat.
```{r}
# First Table
Products[ProductReceived,
on = c("id" = "product_id"),
nomatch = NULL]
# Second Table
ProductReceived[Products,
on = .(product_id = id),
nomatch = NULL]
```
Même si les deux tables contiennent la même information, il existe quelques
différences importantes :
- Elles présentent un ordre différent pour leurs colonnes
- Elles comportent certaines différences de noms dans le nom des colonnes :
- La colonne `id` de la première table contient les mêmes informations que
`product_id` de la seconde table.
- La colonne `i.id` de la première table a les mêmes informations que `id` de la
seconde table.
### 3.3. Non jointure
Cette méthode extrait **ne garde que les lignes qui ne correspondent pas à
n'importe quelle ligne d'une seconde table**.
Pour appliquer cette technique il suffit d'utiliser la négation (`!`) sur la
table qui se trouve dans l'argument `i` .
```{r}
Products[!ProductReceived,
on = c("id" = "product_id")]
```
Comme on peut le voir, le résultat ne comporte que les bananes car c'est le seul
produit qui ne figure pas dans la table `ProductReceived`.
```{r}
ProductReceived[!Products,
on = c("product_id" = "id")]
```
Dans ce cas l'opération renvoie la ligne de `product_id = 6,` car il ne figure
pas dans la table `Products`.
### 3.4. Semi jointure
Cette méthode extrait **ne garde que les lignes qui correspondent à n'importe
quelle ligne d'une seconde table** sans combiner la colonne des tables.
En tant que jointure ceci est très similaire au sous-ensemble, mais comme cette
fois nous passons une table complète dans `i` nous devons vérifier que :
- Chaque ligne de la table `x` est dupliquée à cause de la duplication des lignes
dans la table passée dans l'argument `i`.
- Toutes les lignes renommées de `x` doivent conserver l'ordre originel des
lignes.
Pour faire ceci, suivez les étapes ci-après :
1. Réaliser une **jointure interne** avec `which = TRUE` pour sauvegarder les
numéros de ligne liés à chaque ligne sélectionnée de la table `x` .
```{r}
SubSetRows = Products[
ProductReceived,
on = .(id = product_id),
nomatch = NULL,
which = TRUE
]
SubSetRows
```
1. Sélectionner et trier les id uniques de lignes.
```{r}
SubSetRowsSorted = sort(unique(SubSetRows))
SubSetRowsSorted
```
1. Sélectionner les `x` rangées à garder.
```{r}
Products[SubSetRowsSorted]
```
### 3.5. Jointure gauche
Utilisez cette méthode pour combiner les colonnes de deux tables en se basant sur
une ou plusieurs références mais ***en gardant toutes les lignes présentes dans
la table située à gauche***.
Pour réaliser cette opération, il suffit d'**échanger l'ordre entre les deux
tables** ainsi que le nom des colonnes dans l'argument `on`.
```{r}
ProductReceived[Products,
on = list(product_id = id)]
```
Voici des éléments importants à prendre en compte :
- **Niveau colonne**
- Le *premier groupe* de colonnes vient maintenant de la table `ProductReceived`
car il est la table `x` .
- Le *second groupe* de colonnes vient maintenant de la table `Products` car il
est la table `i` .
- Le préfixe `i.` n'a été ajouté à aucune colonne.
- **Niveau ligne**
- Toutes les lignes de la table `i` ont été gardées car nous n'avons jamais reçu
de bananes et la ligne fait toujours partie des résultats.
- La ligne concernant `product_id = 6` ne fait plus partie des résultats car elle
n'est pas présente dans la table `Products`.
#### 3.5.1. Jointure après des opérations sur les chaînes
Une des fonctionnalités clé de `data.table` est que l'on peut appliquer plusieurs
opérations en chaînant les crochets, avant d'enregistrer le résultat final.
```r
DT[
...
][
...
][
...
]
```
Jusqu'à présent, si après avoir exécuté toutes ces opérations **nous souhaitons
joindre de nouvelles colonnes sans supprimer aucune ligne**, il faut arrêter le
processus de chaînage, sauvegarder dans une table temporaire puis effectuer
ultérieurement l'opération de jointure.
Pour éviter cette situation, nous pouvons utiliser le symbole spécial `.SD`, pour
appliquer une **jointure droite en fonction de la table modifiée**.
```{r}
NewTax[Products,
on = c("unit", "type")
][, ProductReceived[.SD,
on = list(product_id = id)],
.SDcols = !c("unit", "type")]
```
### 3.6. Jointure de plusieurs à plusieurs
Sometimes we want to join tables based on columns with **duplicated `id` values**
to later perform some transformations later.
Pour illustrer cette situation, prenons par exemple le `product_id == 1L`, qui a
quatre lignes dans notre table `ProductReceived` .
```{r}
ProductReceived[product_id == 1L]
```
Et quatre rangées dans notre table `ProductSales`.
```{r}
ProductSales[product_id == 1L]
```
Pour réaliser cette jointure il nous suffit de filtrer `product_id == 1L` dans la
table `i` pour limiter la jointure uniquement à ce produit et déclarer
l'argument `allow.cartesian = TRUE` pour permettre la combinaison de chaque
ligne d'une table avec chaque ligne de l'autre table.
```{r}
ProductReceived[ProductSales[list(1L),
on = "product_id",
nomatch = NULL],
on = "product_id",
allow.cartesian = TRUE]
```
Une fois que nous avons assimilé les résultats, nous pouvont appliquer le même
processus pour **tous les produits**.
```{r}
ProductReceived[ProductSales,
on = "product_id",
allow.cartesian = TRUE]
```
> `allow.cartesian` vaut par défaut FALSE car c'est ce que l'utilisateur a
souhaité, et une telle jointure croisée peut conduire à un très grand nombre
de lignes dans le résultat. Par exemple, si Table A possède 100 lignes et Table
B en a 50, leur produit cartésien sera 5000 lignes (100 * 50). Ce qui peut
rapidement accroître la mémoire occupée pour de grands ensembles de données.
#### 3.6.1. Selection d'une correspondance
Après avoir fait la jointure de la table, nous pourrions penser qu'on aurait pu
utiliser une seule jointure pour extraire les informations nécessaires. Dans ce
cas il y a deux alternatives :
- Nous pouvons sélectionner la **première correspondance**, représentée dans
l'exemple suivant par `id = 2`.
```{r}
ProductReceived[ProductSales[product_id == 1L],
on = .(product_id),
allow.cartesian = TRUE,
mult = "first"]
```
- Nous pouvons sélectionner la **dernière correspondance**, représentée dans
l'exemple suivant par `id = 9`.
```{r}
ProductReceived[ProductSales[product_id == 1L],
on = .(product_id),
allow.cartesian = TRUE,
mult = "last"]
```
#### 3.6.2. Jointure croisée
Si vous voulez obtenir **toutes les combinaisons possibles de lignes** quelque
soit l'id de colonne, vous pouvez suivre le processus suivant :
1. Créer une nouvelle colonne dans les deux tables avec une constante.
```{r}
ProductsTempId = copy(Products)[, temp_id := 1L]
```
1. Joindre les deux tables en fonction de la nouvelle colonne et supprimer cette
dernnière à la fin de la manipulation parce qu'il n'y a pas de raison pour la
garder.
```{r}
AllProductsMix =
ProductsTempId[ProductsTempId,
on = "temp_id",
allow.cartesian = TRUE]
AllProductsMix[, temp_id := NULL]
# Removing type to make easier to see the result when printing the table
AllProductsMix[, !c("type", "i.type")]
```
### 3.7. Jointure complète
Utilisez cette méthode pour combiner les colonnes de deux tables en se basant sur
une ou plusieurs références mais ***sans supprimer aucune ligne***.
Comme vu dans la section précédente, toutes les opérations ci-avant peuvent
conserver le `product_id = 6` absent et le **soda** (`product_id = 4`) comme
faisant partie du résultat.
Pour résoudre ce problème nous pouvons utiliser la fonction `merge` bien qu'elle
soit moins préférable à l'utilisation de la syntaxe de jointure native de
`data.table`.
```{r}
merge(x = Products,
y = ProductReceived,
by.x = "id",
by.y = "product_id",
all = TRUE,
sort = FALSE)
```
## 4. Jointure non équilibrée
Une jointure de non équivalence est un type de jointure où la condition pour
sélectionner les lignes n'est pas basée sur une égalité mais sur d'autres
opérateurs de comparaison tels que <, >, <=, ou >=. Ceci permet des **critères
plus flexibles de jointure**. Dans `data.table`, le jointures non équivalentes
sont particulièrement utiles pour les opérations telles que :
- Recherche de la correspondance la plus proche
- Comparaison d'intervalles de valeurs entre deux tables
C'est une alternative intéressante si elle est appliquée à droite d'une jointure
interne.
- Vous souhaitez diminuer le nombre de lignes renvoyées en fonction du résultat
de la comparaison des colonnes numériques de tables différentes.
- Il n'est pas nécessaire de garder les colonnes de la table `x`*(data.table
secondaire)* dans la table finale.
Pour illustrer le fonctionnement, concentrons-nous sur les promotions et les
réceptions de product 2.
```{r}
ProductSalesProd2 = ProductSales[product_id == 2L]
ProductReceivedProd2 = ProductReceived[product_id == 2L]
```
Si l'on veut savoir par exemple si quelque chose a été reçu avant la date des
promotions, nous pouvons utiliser le code suivant.
```{r}
ProductReceivedProd2[ProductSalesProd2,
on = "product_id",
allow.cartesian = TRUE
][date < i.date]
```
Que se passe-t-il si nous appliquons simplement la même logique à la liste passée
à `on` ?
- Comme cette opération est encore une jointure droite, elle renvoie toutes les
lignes de la table `i` , mais n'affiche que les valeurs de `id` et `count`
lorsque les règles sont vérifiées.
- La date correspondant à `ProductReceivedProd2` a été omise de cette nouvelle
table.
```{r}
ProductReceivedProd2[ProductSalesProd2,
on = list(product_id, date < date)]
```
Maintenant après avoir appliqué la jointure, nous pouvons limiter les résultats
pour n'afficher que les cas qui satisfont tous les critères de jointure.
```{r}
ProductReceivedProd2[ProductSalesProd2,
on = list(product_id, date < date),
nomatch = NULL]
```
## 5. Jointure glissante
Les jointures glissantes sont particulièrement utiles si vous faites des analyses
de données sur des séries temporelles. Elles permettent de **prendre en compte
les lignes basées sur la valeur la plus proche** dans une colonne triée,
typiquement une colonne avec une date ou un horodatage.
C'est utile lorsque vous avez besoin d'aligner des données de sources différentes
**qui n'ont pas forcément les mêmes horodatages**, ou si vous souhaitez
continuer avec la valeur la plus récente.
Par exemple, avec des données financières, vous pourriez utiliser une jointure
glissante pour assigner la valeur la plus récente d'une action à chaque
transaction, même si les mises à jour du prix et les transactions ne
correspondent pas exactement aux mêmes instants.
Dans notre exemple de supermarché nous pouvons utiliser une jointure glissante
pour correspondre aux promotions avec les informations de produit les plus
récentes.
Supposons que le prix des bananes et des carottes change le premier jour de
chaque mois.
```{r}
ProductPriceHistory = data.table(
product_id = rep(1:2, each = 3),
date = rep(as.IDate(c("2024-01-01", "2024-02-01", "2024-03-01")), 2),
price = c(0.59, 0.63, 0.65, # Banana prices
0.79, 0.89, 0.99) # Carrot prices
)
ProductPriceHistory
```
Maintenant nous pouvons réaliser une jointure droite en donnant un prix différent
à chaque produit en fonction de la date de promotion.
```{r}
ProductPriceHistory[ProductSales,
on = .(product_id, date),
roll = TRUE,
j = .(product_id, date, count, price)]
```
Si nous voulons simplement voir les cas de correspondance, il suffit d'ajouter
l'argument `nomatch = NULL` pour réaliser une jointure glissante en interne.
```{r}
ProductPriceHistory[ProductSales,
on = .(product_id, date),
roll = TRUE,
nomatch = NULL,
j = .(product_id, date, count, price)]
```
## 7. Avantage de la vitesse de jointure
### 7.1. Sous-ensembles en tant que jointures
Comme nous venons de le voir, dans la section précédente la table `x` est filtrée
par les valeurs de la table `i` . Actuellement cette méthode est plus rapide que
de passer une expression booléenne dans l'argument `i`.
Pour filtrer la table `x` rapidement nous ne passons pas la `data.table` entière,
nous pouvons passer une `list()` de vecteurs avec les valeurs de la table
originale que nous voulons garder ou omettre.
Par exemple pour filtrer les dates auxquelles le marché a reçu 100 unités de
bananes (`product_id = 1`) ou de popcorn (`product_id = 3`) nous pouvons
utiliser ceci :
```{r}
ProductReceived[list(c(1L, 3L), 100L),
on = c("product_id", "count")]
```
Nous voici à la fin, nous filtrons le code **ligne absente dans la table
originale** renvoyé suite à la jointure. Pour éviter ce comportement il est
recommandé de toujours ajouter l'argument `nomatch = NULL`.
```{r}
ProductReceived[list(c(1L, 3L), 100L),
on = c("product_id", "count"),
nomatch = NULL]
```
Nous pouvons aussi utiliser cette technique pour filtrer toute combinaison de
valeurs en les préfixant avec `!` pour obtenir la négation de l'expression dans
l'argument `i` et en gardant le `nomatch` à sa valeur par défaut. Par exemple
nous pouvons filtrer les deux lignes filtrées précédemment.
```{r}
ProductReceived[!list(c(1L, 3L), 100L),
on = c("product_id", "count")]
```
Si vous voulez simplement filtrer une valeur pour une **colonne de caractères**
seule, vous pouvez ne pas appeler la fonction `list()` et passer la valeur pour
être filtrée dans l'argument `i`.
```{r}
Products[c("banana","popcorn"),
on = "name",
nomatch = NULL]
Products[!"popcorn",
on = "name"]
```
### 7.2. Mise à jour par référence
L'opérateur `:=` de data.table est utilisé pour mettre à jour ou ajouter des
colonnes par référence. Cela signifie qu'il modifie la data.table originale sans
créer de copie , ce qui est très efficace en termes de mémoire, particulièrement
avec les grands ensembles de données. Lorsqu'il est utilisé à l'intérieur d'une
data.table, `:=` permet d'**ajouter de nouvelles colonnes** ou de **modifier des
colonnes existantes** à l'intérieur d'une même requête.
Mettons à jour notre table `Products` avec le dernier prix connu de
`ProductPriceHistory` :
```{r}
copy(Products)[ProductPriceHistory,
on = .(id = product_id),
j = `:=`(price = tail(i.price, 1),
last_updated = tail(i.date, 1)),
by = .EACHI][]
```
Dans cette opération :
- La fonction `copy` empêche que `:=` modifie par référence les tables `Products`.
- Nous réalisons la jointure de `Products` avec `ProductPriceHistory` en fonction
de `id` et de `product_id`.
- Nous mettons à jour la colonne `price` avec le prix le plus récent de
`ProductPriceHistory`.
- Nous ajoutons une nouvelle colonne `last_updated` qui mémorisera la date du
dernier changement de prix.
- Le `by = .EACHI` assure que la fonction `tail` est appliquée à chaque produit
de `ProductPriceHistory`.
***
## Références
- *Comprendre les jointures glissantes de data.table*:
https://www.r-bloggers.com/2016/06/understanding-data-table-rolling-joins/
- *Demi-jointure avec data.table*:
https://stackoverflow.com/questions/18969420/perform-a-semi-join-with-data-table
- *Jointure croisée avec data.table*:
https://stackoverflow.com/questions/10600060/how-to-do-cross-join-in-r
- *Comment réaliser une jointure complète à l'aide de data.table ?*:
https://stackoverflow.com/questions/15170741/how-does-one-do-a-full-join-using-data-table
- *data.frame étendu*:
https://rdatatable.gitlab.io/data.table/reference/data.table.html