Indices secondaires et auto-indexation
2025-06-16
Source:vignettes/fr/datatable-secondary-indices-and-auto-indexing.Rmd
datatable-secondary-indices-and-auto-indexing.Rmd
Translations of this document are available in: en | fr
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
Nous utiliserons les mêmes données flights
que dans la
vignette “Introduction à data.table”.
flights <- fread("../flights14.csv")
head(flights)
# year month day dep_delay arr_delay carrier origin dest air_time distance hour
# <int> <int> <int> <int> <int> <char> <char> <char> <int> <int> <int>
# 1: 2014 1 1 14 13 AA JFK LAX 359 2475 9
# 2: 2014 1 1 -3 13 AA JFK LAX 363 2475 11
# 3: 2014 1 1 2 9 AA JFK LAX 351 2475 19
# 4: 2014 1 1 -8 -26 AA LGA PBI 157 1035 7
# 5: 2014 1 1 2 1 AA JFK LAX 350 2475 13
# 6: 2014 1 1 4 0 AA EWR LAX 339 2454 18
dim(flights)
# [1] 253316 11
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
?
setindex(flights, origin)
head(flights)
# year month day dep_delay arr_delay carrier origin dest air_time distance hour
# <int> <int> <int> <int> <int> <char> <char> <char> <int> <int> <int>
# 1: 2014 1 1 14 13 AA JFK LAX 359 2475 9
# 2: 2014 1 1 -3 13 AA JFK LAX 363 2475 11
# 3: 2014 1 1 2 9 AA JFK LAX 351 2475 19
# 4: 2014 1 1 -8 -26 AA LGA PBI 157 1035 7
# 5: 2014 1 1 2 1 AA JFK LAX 350 2475 13
# 6: 2014 1 1 4 0 AA EWR LAX 339 2454 18
## 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))
# [1] "names" "row.names" "class" ".internal.selfref"
# [5] "index"
setindex
etsetindexv()
permettent d’ajouter un index secondaire à data.table.Notez que
flights
n’est pas physiquement réordonné dans l’ordre croissant deorigin
, comme cela aurait été le cas avecsetkey()
.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
?
indices(flights)
# [1] "origin"
setindex(flights, origin, dest)
indices(flights)
# [1] "origin" "origin__dest"
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 colonneorigin
, 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 :
## pas exécuté
setkey(flights, origin)
flights["JFK"] # or flights[.("JFK")]
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.
## 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’argumenton
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
flights["JFK", on = "origin"]
# year month day dep_delay arr_delay carrier origin dest air_time distance hour
# <int> <int> <int> <int> <int> <char> <char> <char> <int> <int> <int>
# 1: 2014 1 1 14 13 AA JFK LAX 359 2475 9
# 2: 2014 1 1 -3 13 AA JFK LAX 363 2475 11
# 3: 2014 1 1 2 9 AA JFK LAX 351 2475 19
# 4: 2014 1 1 2 1 AA JFK LAX 350 2475 13
# 5: 2014 1 1 -2 -18 AA JFK LAX 338 2475 21
# ---
# 81479: 2014 10 31 -4 -21 UA JFK SFO 337 2586 17
# 81480: 2014 10 31 -2 -37 UA JFK SFO 344 2586 18
# 81481: 2014 10 31 0 -33 UA JFK LAX 320 2475 17
# 81482: 2014 10 31 -6 -38 UA JFK SFO 343 2586 9
# 81483: 2014 10 31 -6 -38 UA JFK LAX 323 2475 11
## 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()
, alorson
le réutiliserait au lieu de le (re)calculer. Nous pouvons le voir en utilisantverbose = TRUE
:setindex(flights, origin) flights["JFK", on = "origin", verbose = TRUE][1:5] # i.V1 has same type (character) as x.origin. No coercion needed. # on= matches existing index, using index # Starting bmerge ... # forder.c received 1 rows and 1 columns # forderReuseSorting: opt=-1, took 0.000s # bmerge: looping bmerge_r took 0.000s # bmerge: took 0.000s # bmerge done in 0.000s elapsed (0.000s cpu) # Constructing irows for '!byjoin || nqbyjoin' ... 0.000s elapsed (0.000s cpu) # year month day dep_delay arr_delay carrier origin dest air_time distance hour # <int> <int> <int> <int> <int> <char> <char> <char> <int> <int> <int> # 1: 2014 1 1 14 13 AA JFK LAX 359 2475 9 # 2: 2014 1 1 -3 13 AA JFK LAX 363 2475 11 # 3: 2014 1 1 2 9 AA JFK LAX 351 2475 19 # 4: 2014 1 1 2 1 AA JFK LAX 350 2475 13 # 5: 2014 1 1 -2 -18 AA JFK LAX 338 2475 21
– 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 :
flights[.("JFK", "LAX"), on = c("origin", "dest")][1:5]
# year month day dep_delay arr_delay carrier origin dest air_time distance hour
# <int> <int> <int> <int> <int> <char> <char> <char> <int> <int> <int>
# 1: 2014 1 1 14 13 AA JFK LAX 359 2475 9
# 2: 2014 1 1 -3 13 AA JFK LAX 363 2475 11
# 3: 2014 1 1 2 9 AA JFK LAX 351 2475 19
# 4: 2014 1 1 2 1 AA JFK LAX 350 2475 13
# 5: 2014 1 1 -2 -18 AA JFK LAX 338 2475 21
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.
c) Chaînage
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écupère toutes les 'hours' de flights
flights[, sort(unique(hour))]
# [1] 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
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.
flights[.(24L), hour := 0L, on = "hour"]
Maintenant, vérifions si 24
est remplacé par
0
dans la colonne hour
.
- C’est notamment un énorme avantage des index secondaires.
Auparavant, pour mettre à jour quelques lignes de
hour
, nous devions utilisersetkey()
sur celui-ci, ce qui réorganisait inévitablement l’ensemble de la data.table. Avecon
, 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
ans <- flights["JFK", max(dep_delay), keyby = month, on = "origin"]
head(ans)
# Key: <month>
# month V1
# <int> <int>
# 1: 1 881
# 2: 2 1014
# 3: 3 920
# 4: 4 1241
# 5: 5 853
# 6: 6 798
- 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”
flights[c("BOS", "DAY"), on = "dest", mult = "first"]
# year month day dep_delay arr_delay carrier origin dest air_time distance hour
# <int> <int> <int> <int> <int> <char> <char> <char> <int> <int> <int>
# 1: 2014 1 1 3 1 AA JFK BOS 39 187 12
# 2: 2014 1 1 25 35 EV EWR DAY 102 533 17
– Sous-ensemble contenant uniquement la dernière ligne
correspondante où origin
correspond à “LGA”, “JFK”,
“EWR” et dest
correspond à “XNA”
flights[.(c("LGA", "JFK", "EWR"), "XNA"), on = c("origin", "dest"), mult = "last"]
# year month day dep_delay arr_delay carrier origin dest air_time distance hour
# <int> <int> <int> <int> <int> <char> <char> <char> <int> <int> <int>
# 1: 2014 10 31 -5 -11 MQ LGA XNA 165 1147 6
# 2: NA NA NA NA NA <NA> JFK XNA NA NA NA
# 3: 2014 10 31 -2 -25 EV EWR XNA 160 1131 6
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
flights[.(c("LGA", "JFK", "EWR"), "XNA"), mult = "last", on = c("origin", "dest"), nomatch = NULL]
# year month day dep_delay arr_delay carrier origin dest air_time distance hour
# <int> <int> <int> <int> <int> <char> <char> <char> <int> <int> <int>
# 1: 2014 10 31 -5 -11 MQ LGA XNA 165 1147 6
# 2: 2014 10 31 -2 -25 EV EWR XNA 160 1131 6
- 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.
set.seed(1L)
dt = data.table(x = sample(1e5L, 1e7L, TRUE), y = runif(100L))
print(object.size(dt), units = "Mb")
# 114.4 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.
## inspection de tous les noms d’attributs
names(attributes(dt))
# [1] "names" "row.names" "class" ".internal.selfref"
## première exécution
(t1 <- system.time(ans <- dt[x == 989L]))
# user system elapsed
# 0.584 0.062 0.646
head(ans)
# x y
# <int> <num>
# 1: 989 0.7757157
# 2: 989 0.6813302
# 3: 989 0.2815894
# 4: 989 0.4954259
# 5: 989 0.7885886
# 6: 989 0.5547504
## indice secondaire créé
names(attributes(dt))
# [1] "names" "row.names" "class" ".internal.selfref"
# [5] "index"
indices(dt)
# [1] "x"
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.
## sous-ensembles successifs
(t2 <- system.time(dt[x == 989L]))
# user system elapsed
# 0.012 0.013 0.025
system.time(dt[x %in% 1989:2012])
# user system elapsed
# 0.016 0.010 0.026
L’exécution la première fois a pris 0.646 secondes tandis que la deuxième fois, elle a pris 0.025 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
ousetindexv
. Vous pouvez désactiver complètement les index en définissant l’argument globaloptions(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”.