Pourquoi la jointure X [Y] de data.tables ne permet-elle pas une jointure externe complète ou une jointure à gauche?

123

C'est un peu une question philosophique sur la syntaxe de jointure data.table. Je trouve de plus en plus d'utilisations pour les data.tables, mais j'apprends toujours ...

Le format X[Y]de jointure pour data.tables est très concis, pratique et efficace, mais pour autant que je sache, il ne prend en charge que les jointures internes et les jointures externes droites. Pour obtenir une jointure externe gauche ou complète, je dois utiliser merge:

  • X[Y, nomatch = NA] - toutes les lignes en Y - jointure externe droite (par défaut)
  • X[Y, nomatch = 0] - uniquement les lignes avec des correspondances en X et en Y - jointure interne
  • merge(X, Y, all = TRUE) - toutes les lignes de X et Y - jointure externe complète
  • merge(X, Y, all.x = TRUE) - toutes les lignes en X - jointure externe gauche

Il me semble qu'il serait utile que le X[Y]format de jointure prenne en charge les 4 types de jointures. Y a-t-il une raison pour laquelle seuls deux types de jointures sont pris en charge?

Pour moi, les valeurs des paramètres nomatch = 0et nomatch = NAne sont pas très intuitives pour les actions effectuées. Il me est plus facile à comprendre et à mémoriser la mergesyntaxe: all = TRUE, all.x = TRUEet all.y = TRUE. Puisque l' X[Y]opération ressemble mergebeaucoup plus à match, pourquoi ne pas utiliser la mergesyntaxe des jointures plutôt que le paramètre de la matchfonction nomatch?

Voici des exemples de code des 4 types de jointure:

# sample X and Y data.tables
library(data.table)
X <- data.table(t = 1:4, a = (1:4)^2)
setkey(X, t)
X
#    t  a
# 1: 1  1
# 2: 2  4
# 3: 3  9
# 4: 4 16

Y <- data.table(t = 3:6, b = (3:6)^2)
setkey(Y, t)
Y
#    t  b
# 1: 3  9
# 2: 4 16
# 3: 5 25
# 4: 6 36

# all rows from Y - right outer join
X[Y]  # default
#  t  a  b
# 1: 3  9  9
# 2: 4 16 16
# 3: 5 NA 25
# 4: 6 NA 36

X[Y, nomatch = NA]  # same as above
#    t  a  b
# 1: 3  9  9
# 2: 4 16 16
# 3: 5 NA 25
# 4: 6 NA 36

merge(X, Y, by = "t", all.y = TRUE)  # same as above
#    t  a  b
# 1: 3  9  9
# 2: 4 16 16
# 3: 5 NA 25
# 4: 6 NA 36

identical(X[Y], merge(X, Y, by = "t", all.y = TRUE))
# [1] TRUE

# only rows in both X and Y - inner join
X[Y, nomatch = 0]  
#    t  a  b
# 1: 3  9  9
# 2: 4 16 16

merge(X, Y, by = "t")  # same as above
#    t  a  b
# 1: 3  9  9
# 2: 4 16 16

merge(X, Y, by = "t", all = FALSE)  # same as above
#    t  a  b
# 1: 3  9  9
# 2: 4 16 16

identical( X[Y, nomatch = 0], merge(X, Y, by = "t", all = FALSE) )
# [1] TRUE

# all rows from X - left outer join
merge(X, Y, by = "t", all.x = TRUE)
#    t  a  b
# 1: 1  1 NA
# 2: 2  4 NA
# 3: 3  9  9
# 4: 4 16 16

# all rows from both X and Y - full outer join
merge(X, Y, by = "t", all = TRUE)
#    t  a  b
# 1: 1  1 NA
# 2: 2  4 NA
# 3: 3  9  9
# 4: 4 16 16
# 5: 5 NA 25
# 6: 6 NA 36

Mise à jour: data.table v1.9.6 a introduit la on=syntaxe, qui permet des jointures ad hoc sur des champs autres que la clé primaire. réponse de jangorecki à la question Comment joindre (fusionner) des trames de données (interne, externe, gauche, droite)? fournit quelques exemples de types de jointures supplémentaires que data.table peut gérer.

Douglas Clark
la source
4
Avez-vous lu la FAQ 1.12 ? Vous pouvez toujours appeler Y[X]si vous voulez la jointure externe gauche de X[Y]et rbind(Y[X],X[Y])si vous voulez une jointure externe complète
mnel
Voir ma réponse pour une approche plus data.table de la jointure externe complète
mnel
@mnel, je suppose que votre unique()approche ci-dessous pour la jointure complète est préférable rbind(Y[X],X[Y]), car la rbind impliquerait de copier la table. Est-ce correct?
Douglas Clark
au meilleur de ma connaissance, oui. Je n'ai pas testé si trois petits appels uniques sont plus rapides qu'un grand (par exemple unique(c(unique(X[,t]), unique(Y[,t])), cela devrait être plus efficace en mémoire car il ne combine que deux listes qui vont être inférieures ou égales au nombre de lignes dans X et Y .
mnel
2
Votre question est une si bonne description; J'ai trouvé des réponses à mes questions dans votre question. Merci
irriss

Réponses:

71

Pour citer la data.table FAQ 1.11 Quelle est la différence entre X[Y]et merge(X, Y)?

X[Y] est une jointure, recherchant les lignes de X en utilisant Y (ou la clé de Y si elle en a une) comme index.

Y[X] est une jointure, recherchant les lignes de Y en utilisant X (ou la clé de X si elle en a une)

merge(X,Y)fait les deux sens en même temps. Le nombre de lignes de X[Y]et Y[X]diffère généralement, tandis que le nombre de lignes renvoyées par merge(X,Y)et merge(Y,X)est le même.

MAIS cela manque le point principal. La plupart des tâches nécessitent que quelque chose soit fait 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[,ColsNeeded2]), mais cela nécessite que le programmeur détermine quelles colonnes sont nécessaires. X[Y,j] in data.table fait tout cela en une seule étape pour vous. Lorsque vous écrivez X[Y,sum(foo*bar)], data.table inspecte automatiquement l' jexpression pour voir quelles colonnes elle utilise. Il ne sous-ensemble que ces colonnes; les autres sont ignorés. La mémoire n'est créée que pour les colonnes utilisées j, et les Ycolonnes bénéficient des règles de recyclage R standard dans le contexte de chaque groupe. Disons que fooc'est dans X, et que la barre est dans Y(avec 20 autres colonnes dans Y). N'est pasX[Y,sum(foo*bar)] plus rapide à programmer et plus rapide à exécuter qu'une fusion de tout ce qui est gaspillé suivi d'un sous-ensemble?


Si vous voulez une jointure externe gauche de X[Y]

le <- Y[X]
mallx <- merge(X, Y, all.x = T)
# the column order is different so change to be the same as `merge`
setcolorder(le, names(mallx))
identical(le, mallx)
# [1] TRUE

Si vous voulez une jointure externe complète

# the unique values for the keys over both data sets
unique_keys <- unique(c(X[,t], Y[,t]))
Y[X[J(unique_keys)]]
##   t  b  a
## 1: 1 NA  1
## 2: 2 NA  4
## 3: 3  9  9
## 4: 4 16 16
## 5: 5 25 NA
## 6: 6 36 NA

# The following will give the same with the column order X,Y
X[Y[J(unique_keys)]]
mnel
la source
5
Merci @mnel. La FAQ 1.12 ne mentionne pas la jointure externe complète ou gauche. Votre suggestion de jointure externe complète avec unique () est d'une grande aide. Cela devrait être dans la FAQ. Je sais que Matthew Dowle «l'a conçu pour son propre usage, et il le voulait ainsi». (FAQ 1.9), mais je pensais que cela X[Y,all=T]pourrait être un moyen élégant de spécifier une jointure externe complète dans la syntaxe data.table X [Y]. Ou X[Y,all.x=T]pour la jointure gauche. Je me suis demandé pourquoi ce n'était pas conçu de cette façon. Juste une pensée.
Douglas Clark
1
@DouglasClark Ont ajouté la réponse et déposé 2302: Ajout de la syntaxe de jointure de fusion de mnel à la FAQ (avec minutages) . Excellentes suggestions!
Matt Dowle
1
@mnel Merci pour la solution ... fait ma journée ... :)
Ankit
@mnel Existe-t-il un moyen pour imputer des NA avec 0 lors de l'exécution X[Y[J(unique_keys)]]?
Ankit le
11
ce qui m'impressionne à propos de la documentation data.table, c'est qu'elle peut être si verbeuse, mais rester si cryptique ...
NiuBiBang
24

La réponse de @ mnel est parfaite, alors acceptez cette réponse. C'est juste un suivi, trop long pour les commentaires.

Comme le dit mnel, la jointure externe gauche / droite est obtenue en échangeant Yet X: Y[X]-vs- X[Y]. Donc, 3 des 4 types de jointure sont pris en charge dans cette syntaxe, et non 2, iiuc.

L'ajout du 4ème semble une bonne idée. Disons que nous ajoutons full=TRUEou both=TRUEou merge=TRUE(vous n'êtes pas sûr du meilleur nom d'argument?) Alors cela ne m'est pas venu à l'esprit avant que ce X[Y,j,merge=TRUE]serait utile pour les raisons après le MAIS dans la FAQ 1.12. Nouvelle demande de fonctionnalité maintenant ajoutée et liée ici, merci:

FR # 2301: Ajoutez l'argument merge = TRUE pour les jointures X [Y] et Y [X] comme le fait merge ().

Les versions récentes ont accéléré merge.data.table(en prenant une copie superficielle en interne pour définir les clés plus efficacement, par exemple). Nous essayons donc de faire merge()et X[Y]plus, et de fournir toutes les options à l' utilisateur une flexibilité totale. Il y a des avantages et des inconvénients à la fois. Une autre demande de fonctionnalité exceptionnelle est:

FR # 2033: Ajoutez by.x et by.y à merge.data.table

S'il y en a d'autres, veuillez les continuer à venir.

Par cette partie de la question:

pourquoi ne pas utiliser la syntaxe de fusion pour les jointures plutôt que le paramètre nomatch de la fonction de correspondance?

Si vous préférez la merge()syntaxe et ses 3 arguments all, all.xet all.ypuis il suffit d' utiliser qu'au lieu de X[Y]. Je pense que cela devrait couvrir tous les cas. Ou avez - vous dire pourquoi l'argument un seul nomatchdans [.data.table? Si c'est le cas, c'est juste la manière qui semblait naturelle étant donné la FAQ 2.14: "Pouvez-vous expliquer plus en détail pourquoi data.table est inspiré de la syntaxe A [B] en base?". Mais aussi, nomatchne prend que deux valeurs actuellement 0et NA. Cela pourrait être étendu de sorte qu'une valeur négative signifiait quelque chose, ou 12 signifierait utiliser les valeurs de la 12e ligne pour remplir les NA, par exemple, ou nomatchà l'avenir pourrait être un vecteur ou même lui-même a data.table.

Hm. Comment interagirait by-without-by avec merge = TRUE? Peut-être devrions-nous prendre cela en charge pour datatable-help .

Matt Dowle
la source
Merci @Matthew. La réponse de @ mnel est excellente, mais ma question n'était pas de savoir comment faire une jointure complète ou gauche, mais "Y a-t-il une raison pour laquelle seuls deux types de jointures sont pris en charge?" Alors maintenant, c'est un peu plus philosophique ;-) En fait, je ne préfère pas la syntaxe de fusion, mais il semble qu'il y ait une tradition R pour construire sur des choses existantes que les gens connaissent. J'avais griffonné join="all", join="all.x", join="all.y" and join="x.and.y"en marge de mes notes. Je ne sais pas si c'est mieux.
Douglas Clark
@DouglasClark Peut-être joincomme ça, bonne idée. J'ai posté sur datatable-help alors voyons voir. Peut-être aussi donner un data.tablepeu de temps pour vous installer. Avez-vous déjà passé par-sans-par par exemple, et également suivre une portée héritée ?
Matt Dowle
Comme indiqué dans mon commentaire ci - dessus, je vous suggère d' ajouter un joinmot - clé, quand i est un datatable: X[Y,j,join=string]. Les valeurs de chaîne possibles pour join sont suggérées comme suit: 1) "all.y" et "right" -
Douglas Clark
1
Salut Matt, la bibliothèque data.table est fantastique; Merci pour ça; bien que je pense que le comportement de jointure (étant une jointure externe droite par défaut) devrait être expliqué en évidence dans la documentation principale; il m'a fallu 3 jours pour comprendre cela.
Timothée HENRY
1
@tucson Juste pour créer un lien ici, maintenant classé sous le numéro 709 .
Matt Dowle
17

Cette « réponse » est une proposition de discussion: Comme indiqué dans mon commentaire, je suggère d' ajouter un joinparamètre à [.data.table () pour permettre d' autres types de jointures, à savoir: X[Y,j,join=string]. En plus des 4 types de jointures ordinaires, je suggère également de prendre en charge 3 types de jointures exclusives et la jointure croisée .

Les joinvaleurs de chaîne (et les alias) pour les différents types de jointure sont proposées comme suit:

  1. "all.y"et "right"- jointure à droite, la valeur par défaut de la table data.table actuelle (nomatch = NA) - toutes les lignes Y avec NA où il n'y a pas de correspondance X;
  2. "both"et "inner" - jointure interne (nomatch = 0) - uniquement les lignes où X et Y correspondent;

  3. "all.x"et "left" - jointure gauche - toutes les lignes de X, NA où aucun Y ne correspond:

  4. "outer"et "full" - jointure externe complète - toutes les lignes de X et Y, NA où aucune correspondance

  5. "only.x" et "not.y" - non-jointure ou anti-jointure renvoyant X lignes où il n'y a pas de correspondance Y

  6. "only.y" et "not.x" - non-jointure ou anti-jointure renvoyant des lignes Y où il n'y a pas de correspondance X
  7. "not.both" - jointure exclusive renvoyant des lignes X et Y où il n'y a pas de correspondance avec l'autre table, c'est-à-dire un ou exclusif (XOR)
  8. "cross"- jointure croisée ou produit cartésien avec chaque ligne de X correspondant à chaque ligne de Y

La valeur par défaut est join="all.y" correspond à la valeur par défaut actuelle.

Les valeurs de chaîne "all", "all.x" et "all.y" correspondent à merge() paramètres. Les chaînes «droite», «gauche», «interne» et «externe» peuvent être plus adaptées aux utilisateurs SQL.

Les chaînes "both" et "not.both" sont ma meilleure suggestion pour le moment - mais quelqu'un peut avoir de meilleures suggestions de chaînes pour la jointure interne et la jointure exclusive. (Je ne sais pas si "exclusif" est la bonne terminologie, corrigez-moi s'il existe un terme approprié pour une jointure "XOR".)

L'utilisation de join="not.y"est une alternative pour la syntaxe X[-Y,j]ou X[!Y,j]non-join et peut-être plus claire (pour moi), même si je ne suis pas sûr si elles sont identiques (nouvelle fonctionnalité de data.table version 1.8.3).

La jointure croisée peut parfois être pratique, mais elle ne rentre pas dans le paradigme data.table.

Douglas Clark
la source
1
Veuillez l'envoyer à Datatable-help pour discussion.
Matt Dowle
3
+1 Mais, veuillez envoyer à datatable-help ou déposer une demande de fonctionnalité . Cela ne me dérange pas d'ajouter joinmais à moins qu'il n'atteigne le tracker, il sera oublié.
Matt Dowle
1
Je vois que vous ne vous êtes pas connecté à SO depuis un moment. J'ai donc déposé ceci dans FR # 2301
Matt Dowle
@MattDowle, +1 pour cette fonctionnalité. ( J'ai essayé de le faire via FR # 2301 mais j'obtiens un message d'autorisation refusée).
adilapapaya du
@adilapapaya Nous sommes passés de RForge à GitHub. Veuillez +1 ici: github.com/Rdatatable/data.table/issues/614 . Arun a porté les problèmes pour qu'ils ne soient pas perdus.
Matt Dowle du