J'utilise data.table et de nombreuses fonctions m'obligent à définir une clé (par exemple X[Y]
). En tant que tel, je souhaite comprendre ce que fait une clé pour définir correctement les clés dans mes tableaux de données.
Une source que j'ai lue était ?setkey
.
setkey()
trie undata.table
et le marque comme trié. Les colonnes triées sont la clé. La clé peut être n'importe quelle colonne dans n'importe quel ordre. Les colonnes sont toujours triées par ordre croissant. Le tableau est modifié par référence. Aucune copie n’est faite, à part la mémoire de travail temporaire d’une colonne.
Ce que je retiens ici, c'est qu'une clé "trierait" le fichier data.table, produisant un effet très similaire à order()
. Cependant, cela n'explique pas le but d'avoir une clé.
La data.table FAQ 3.2 et 3.3 explique:
3.2 Je n'ai pas de clé sur une grande table, mais le regroupement reste très rapide. Pourquoi donc?
data.table utilise le tri de base. C'est nettement plus rapide que les autres algorithmes de tri. Radix est spécifiquement pour les entiers seulement, voir
?base::sort.list(x,method="radix")
. C'est également l'une des raisons pour lesquellessetkey()
est rapide. Lorsqu'aucune clé n'est définie ou que nous groupons dans un ordre différent de celui de la clé, nous l'appelons un ad hoc.3.3 Pourquoi le regroupement par colonnes de la clé est-il plus rapide qu’un ad hoc?
Parce que chaque groupe est contigu dans la RAM, minimisant ainsi les extractions de page, la mémoire peut être copiée en bloc (
memcpy
en C) plutôt qu'en boucle en C.
À partir de là, je suppose que le fait de définir une clé permet à R d’utiliser le "tri de base" par rapport à d’autres algorithmes, et c’est pourquoi il est plus rapide.
Le guide de démarrage rapide de 10 minutes comprend également un guide sur les touches.
- Clés
Commençons par examiner data.frame, en particulier les noms de noms (ou en anglais, les noms de lignes). C'est-à-dire que les noms multiples appartiennent à une seule ligne. Les multiples noms appartenant à la même ligne? Ce n’est pas ce à quoi nous sommes habitués dans un data.frame. Nous savons que chaque ligne a au plus un nom. Une personne a au moins deux noms, un prénom et un deuxième nom. Cela est utile pour organiser un répertoire téléphonique, par exemple, qui est trié par nom de famille, puis par prénom. Cependant, chaque ligne d'un data.frame ne peut avoir qu'un seul nom.
Une clé consiste en une ou plusieurs colonnes de noms de noms, qui peuvent être des nombres entiers, des facteurs, des caractères ou une autre classe, pas simplement des caractères. De plus, les lignes sont triées par la clé. Par conséquent, un fichier data.table peut avoir au plus une clé car il ne peut pas être trié de plusieurs manières.
L'unicité n'est pas appliquée, c'est-à-dire que les valeurs de clé en double sont autorisées. Puisque les lignes sont triées par la clé, tous les doublons de la clé apparaissent de manière consécutive.
L'annuaire téléphonique a été utile pour comprendre ce qu'est une clé, mais il semble qu'une clé ne soit pas différente d'une colonne de facteur. En outre, cela n'explique pas pourquoi une clé est nécessaire (en particulier pour utiliser certaines fonctions) et comment choisir la colonne à définir comme clé. De plus, il semble que dans un data.table avec time en tant que colonne, définir une autre colonne comme clé perturberait probablement la colonne time aussi, ce qui la rend encore plus confuse car je ne sais pas si je suis autorisé à définir clé. Quelqu'un peut-il m'éclairer s'il vous plaît?
Mise à jour mineure: Veuillez également vous référer aux nouvelles vignettes HTML . Ce numéro met en évidence les autres vignettes que nous prévoyons.
J'ai mis à jour cette réponse à nouveau (février 2016) à la lumière de la nouvelle fonctionnalité on=
Qui permet ad-hoc se joint également. Voir l'historique des réponses précédentes (obsolètes).
setkey(DT, a, b)
?Il fait deux choses:
DT
par la ou les colonnes fournies ( a , b ) par référence , toujours dans croissant ordre.sorted
à DT
.La réorganisation est rapide (en raison de data.table du tri interne de la base) et en mémoire (une seule colonne supplémentaire de type double est alloué).
setkey()
est-il requis?Pour les opérations de regroupement, setkey()
n'a jamais été une exigence absolue. En d’autres termes, nous pouvons effectuer un cold-by ou adhoc-by .
## "cold" by
require(data.table)
DT <- data.table(x=rep(1:5, each=2), y=1:10)
DT[, mean(y), by=x] # no key is set, order of groups preserved in result
Cependant, avant v1.9.6
, Les jointures de la forme x[i]
Nécessitaient que key
soit défini sur x
. Avec le nouvel argument on=
De v1.9.6 + , ce n'est plus vrai, et la définition des clés est donc not une exigence absolue ici aussi.
## joins using < v1.9.6
setkey(X, a) # absolutely required
setkey(Y, a) # not absolutely required as long as 'a' is the first column
X[Y]
## joins using v1.9.6+
X[Y, on="a"]
# or if the column names are x_a and y_a respectively
X[Y, on=c("x_a" = "y_a")]
Notez que l'argument on=
Peut être explicitement spécifié même pour les jointures keyed
.
La seule opération nécessitant le paramétrage absolu de
key
est la fonction foverlaps () . Mais nous travaillons sur quelques fonctionnalités supplémentaires qui, une fois cela fait, élimineraient cette exigence.
Alors, quelle est la raison de mettre en œuvre l'argument on=
?
Il y a pas mal de raisons.
Cela permet de distinguer clairement l'opération en tant qu'opération impliquant deux data.tables . Le simple fait de X[Y]
Ne fait pas cette distinction, bien que cela puisse être clair en nommant les variables de manière appropriée.
Cela permet également de comprendre les colonnes sur lesquelles join/subset == est exécuté immédiatement en regardant cette ligne de code (sans avoir à retracer la fonction correspondante setkey()
ligne).
Dans les opérations où des colonnes sont ajoutées ou mises à jour par référence , les opérations on=
Sont bien plus performantes, car il n'est pas nécessaire de réorganiser l'intégralité du fichier data.table. ajouter/mettre à jour une ou plusieurs colonnes. Par exemple,
## compare
setkey(X, a, b) # why physically reorder X to just add/update a column?
X[Y, col := i.val]
## to
X[Y, col := i.val, on=c("a", "b")]
Dans le second cas, nous n'avons pas eu à réorganiser. Il ne s'agit pas de calculer l'ordre qui prend du temps, mais de réorganiser physiquement les données.table dans la RAM, et en les évitant, nous conservons l'ordre d'origine, qui est également performant.
Même autrement, sauf si vous effectuez des jointures de manière répétitive, il ne devrait y avoir aucune différence de performance notable entre un clé et ad-hoc joint .
Cela nous amène à nous demander quel est l'avantage de la saisie d'un data.table ?
Y at-il un avantage à saisir un data.table?
La saisie d'un data.table le réorganise physiquement en fonction de ces colonnes dans la RAM. Le calcul de la commande ne prend généralement pas beaucoup de temps, mais le réordonnancement lui-même. Cependant, une fois les données triées dans la RAM, les lignes appartenant au même groupe sont toutes contiguës dans la RAM et sont donc très efficaces en cache. C'est le tri qui accélère les opérations sur les data.tables à clé.
Il est donc essentiel de déterminer si le temps passé à réordonner l'intégralité de data.table vaut la peine de faire une jointure/agrégation efficace en cache. Généralement, à moins que des opérations répétitives de regroupement/jointure soient effectuées sur le même keyed data.table, il ne devrait pas y avoir de différence notable.
Dans la plupart des cas, il ne devrait donc plus être nécessaire de définir des clés. Nous vous recommandons d'utiliser
on=
Dans la mesure du possible, à moins que la clé de réglage n'améliore considérablement les performances que vous souhaitez exploiter.
Question: Que pensez-vous que la performance ressemble à la comparaison avec un clé rejoindre, si vous utilisez setorder()
pour réorganiser le data.table et utilisez on=
? Si vous avez suivi jusqu'ici, vous devriez pouvoir le comprendre :-).
Une clé est essentiellement un index dans un jeu de données, ce qui permet des opérations de tri, de filtrage et de jointure très rapides et efficaces. Ce sont probablement les meilleures raisons d'utiliser des tables de données plutôt que des trames de données (la syntaxe d'utilisation des tables de données est également beaucoup plus conviviale, mais cela n'a rien à voir avec des clés).
Si vous ne comprenez pas les index, considérez ceci: un annuaire est "indexé" par son nom. Donc, si je veux rechercher le numéro de téléphone de quelqu'un, c'est assez simple. Mais supposons que je veuille chercher par numéro de téléphone (par exemple, chercher qui a un numéro de téléphone particulier)? À moins que je ne puisse "réindexer" l'annuaire par numéro, cela prendra beaucoup de temps.
Prenons l'exemple suivant: supposons que j'ai un tableau, Zip, de tous les codes Zip aux États-Unis (> 33 000) ainsi que les informations associées (ville, état, population, revenu médian, etc.). Si je veux rechercher les informations d'un code postal spécifique, la recherche (filtre) est environ 1000 fois plus rapide si je setkey(Zip,zipcode)
en premier.
Un autre avantage est lié aux jointures. Supposons que vous ayez une liste de personnes et leurs codes Zip dans une table de données (appelez-la "PPL") et que je veuille ajouter des informations à partir de la table Zip (par exemple, une ville, un état, etc.). Le code suivant le fera:
setkey(Zip,zipcode)
setkey(PPL,zipcode)
full.info <- PPL[Zip, nomatch=F]
Il s'agit d'une "jointure" dans le sens où je joins les informations de 2 tables basées sur un champ commun (code postal). Les jointures de ce type sur de très grandes tables sont extrêmement lentes avec des trames de données et extrêmement rapides avec des tables de données. Dans un exemple réel, je devais faire plus de 20 000 jointures comme celle-ci sur une table complète de codes postaux. Avec les tableaux de données, le script prenait environ 20 minutes. courir. Je ne l'ai même pas essayé avec des trames de données car cela aurait pris plus de deux semaines.
IMHO vous ne devriez pas simplement lire mais étudier le FAQ et le matériel Intro. Il est plus facile à comprendre si vous avez un problème à appliquer à.
[Réponse au commentaire de @ Frank]
Re: tri/indexation - En fonction de la réponse à cette question , il apparaît que setkey(...)
réorganise en fait les colonnes de la table (par exemple, un tri physique) et ne crée pas d'index au sens de la base de données. Cela a des implications pratiques: si vous définissez la clé dans une table avec setkey(...)
puis modifiez les valeurs de la colonne clé, data.table déclare simplement que la table n'est plus triée ( en désactivant l'attribut sorted
); il ( n'est pas dynamiquement réindexé pour conserver le bon ordre de tri (comme cela se produirait dans une base de données). En outre, "supprimer la clé" à l'aide de setky(DT,NULL)
ne () pas restaure la table dans son ordre d'origine, non trié.
Re: filtre et jointure - la différence pratique réside dans le fait que le filtrage extrait un sous-ensemble d'un seul jeu de données, alors que la jointure combine les données de deux jeux de données basées sur un champ. Il existe de nombreux types de jointures (interne, externe, gauche). L'exemple ci-dessus est une jointure interne (seuls les enregistrements avec des clés communes aux deux tables sont renvoyés), ce qui présente de nombreuses similitudes avec le filtrage.