Dlaczego łączenie X [Y] data.tables nie zezwala na pełne sprzężenie zewnętrzne lub łączenie lewe?

123

To jest trochę filozoficzne pytanie dotyczące składni łączenia danych. Znajduję coraz więcej zastosowań dla danych. Tabele, ale wciąż się uczę ...

Format sprzężenia X[Y]dla data.tables jest bardzo zwięzły, poręczny i wydajny, ale o ile wiem, obsługuje tylko sprzężenia wewnętrzne i prawe sprzężenia zewnętrzne. Aby uzyskać lewe lub pełne sprzężenie zewnętrzne, muszę użyć merge:

  • X[Y, nomatch = NA] - wszystkie wiersze w Y - prawe sprzężenie zewnętrzne (domyślne)
  • X[Y, nomatch = 0] - tylko wiersze z dopasowaniami w X i Y - sprzężenie wewnętrzne
  • merge(X, Y, all = TRUE) - wszystkie rzędy od X i Y - pełne połączenie zewnętrzne
  • merge(X, Y, all.x = TRUE) - wszystkie wiersze w X - lewe połączenie zewnętrzne

Wydaje mi się, że przydałoby się, gdyby X[Y]format złączeń obsługiwał wszystkie 4 typy złączeń. Czy jest jakiś powód, dla którego obsługiwane są tylko dwa typy złączeń?

Dla mnie wartości parametrów nomatch = 0i nomatch = NAnie są zbyt intuicyjne dla wykonywanych czynności. Łatwiej jest mi zrozumieć i zapamiętać mergeskładnię: all = TRUE, all.x = TRUEi all.y = TRUE. Ponieważ X[Y]operacja przypomina mergeznacznie więcej niż match, dlaczego nie użyć mergeskładni dla złączeń zamiast parametru matchfunkcji nomatch?

Oto przykłady kodu 4 typów złączeń:

# 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

Aktualizacja: data.table v1.9.6 wprowadziła on=składnię, która umożliwia łączenia ad hoc w polach innych niż klucz podstawowy. odpowiedź jangoreckiego na pytanie Jak łączyć (scalać) ramki danych (wewnętrzne, zewnętrzne, lewe, prawe)? zawiera kilka przykładów dodatkowych typów złączeń, które może obsługiwać data.table.

Douglas Clark
źródło
4
Czy przeczytałeś FAQ 1.12 ? Zawsze można zadzwonić Y[X], jeśli chcesz lewo sprzężenie zewnętrzne od X[Y]i rbind(Y[X],X[Y])jeśli chcesz pełne sprzężenie zewnętrzne
mnel
Zobacz moją odpowiedź na bardziej szczegółowe podejście do pełnego sprzężenia zewnętrznego
data.table
@mnel, zakładam, że twoje unique()podejście poniżej dla pełnego sprzężenia jest lepsze rbind(Y[X],X[Y]), ponieważ rbind wymagałoby skopiowania tabeli. Czy to prawda?
Douglas Clark
o ile wiem, tak. Nie testowałem, czy trzy mniejsze unikalne wywołania są szybsze niż jedno duże (np. unique(c(unique(X[,t]), unique(Y[,t]))- powinno to być bardziej wydajnepamięć, ponieważ łączy tylko dwie listy, które będą mniejsze lub równe liczbie wierszy w X i Y .
mnel
2
Twoje pytanie tak dobry opis; W twoim pytaniu znalazłem odpowiedzi na swoje pytania. Dzięki
iriss

Odpowiedzi:

72

Cytując z data.table FAQ 1.11 Jaka jest różnica między X[Y]i merge(X, Y)?

X[Y] jest złączeniem, wyszukując wiersze X za pomocą Y (lub klucza Y, jeśli ma taki) jako indeksu.

Y[X] jest złączeniem, wyszukuje wiersze Y za pomocą X (lub klawisza X, jeśli taki ma)

merge(X,Y)działa w obie strony w tym samym czasie. Liczba wierszy X[Y]i Y[X]zwykle się różni, natomiast liczba wierszy zwracanych przez merge(X,Y)i merge(Y,X)jest taka sama.

ALE to mija się z głównym punktem. Większość zadań wymaga wykonania czynności na danych po połączeniu lub scaleniu. Po co łączyć wszystkie kolumny danych, by później wykorzystać tylko niewielki ich podzbiór? Możesz zasugerować merge(X[,ColsNeeded1],Y[,ColsNeeded2]), ale wymaga to od programisty ustalenia, które kolumny są potrzebne. X[Y,j] w data.table robi to wszystko w jednym kroku. Podczas pisania X[Y,sum(foo*bar)]data.table automatycznie sprawdza jwyrażenie, aby zobaczyć, których kolumn używa. Spowoduje tylko podzbiór tylko tych kolumn; inne są ignorowane. Pamięć jest tworzona tylko dla jużywanych kolumn , a Ykolumny korzystają ze standardowych reguł odtwarzania języka R w kontekście każdej grupy. Powiedzmy, że foojest w X, a słupek jest w Y(wraz z 20 innymi kolumnami w Y). Nie jestX[Y,sum(foo*bar)] szybszy w programowaniu i szybszym uruchomieniu niż marnotrawstwo scalania wszystkiego, po którym następuje podzbiór?


Jeśli chcesz, aby lewe zewnętrzne połączenie 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

Jeśli chcesz mieć pełne sprzężenie zewnętrzne

# 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
źródło
5
Dzięki @mnel. FAQ 1.12 nie wspomina o pełnym lub lewym złączeniu zewnętrznym. Twoja pełna sugestia sprzężenia zewnętrznego z unique () jest bardzo pomocna. To powinno być w FAQ. Wiem, że Matthew Dowle „zaprojektował go na swój własny użytek i tak chciał”. (FAQ 1.9), ale pomyślałem, że X[Y,all=T]mógłby być eleganckim sposobem określenia pełnego sprzężenia zewnętrznego w składni data.table X [Y]. Lub X[Y,all.x=T]dla lewego złączenia. Zastanawiałem się, dlaczego nie został zaprojektowany w ten sposób. Tylko myśl.
Douglas Clark
1
@DouglasClark Dodałem odpowiedź i złożyłem 2302: Dodaj składnię łączenia scalającego mnel do FAQ (z czasami) . Świetne sugestie!
Matt Dowle
1
@mnel Dzięki za rozwiązanie ... zrobiłem mój dzień ... :)
Ankit
@mnel Czy jest jakiś sposób, aby podczas wykonywania przypisać NA z 0 X[Y[J(unique_keys)]]?
Ankit
11
co mnie imponuje w dokumentacji data.table to to, że może być tak szczegółowa, a jednocześnie tak tajemnicza ...
NiuBiBang
24

Odpowiedź @ mnel jest na miejscu, więc zaakceptuj tę odpowiedź. To tylko kontynuacja, zbyt długa na komentarze.

Jak mówi mnel, lewe / prawe sprzężenie zewnętrzne uzyskuje się przez zamianę Yi X: Y[X]-vs- X[Y]. Tak więc 3 z 4 typów złączeń są obsługiwane w tej składni, a nie 2, iiuc.

Dodanie czwartego wydaje się dobrym pomysłem. Powiedzmy, że dodajemy full=TRUElub both=TRUElub merge=TRUE(nie jesteś pewien najlepszej nazwy argumentu?), To nie przyszło mi do głowy wcześniej, że X[Y,j,merge=TRUE]byłoby to przydatne z powodów po ALE w FAQ 1.12. Nowa prośba o funkcję jest teraz dodana i połączona z powrotem tutaj, dzięki:

FR # 2301: Dodaj argument merge = TRUE zarówno dla złączenia X [Y], jak i Y [X], tak jak robi to merge ().

Najnowsze wersje przyspieszyły merge.data.table(na przykład pobierając płytką kopię wewnętrznie w celu wydajniejszego ustawiania kluczy). Dlatego staramy się przybliżać merge()i X[Y]przybliżać oraz udostępniać użytkownikowi wszystkie opcje, aby zapewnić pełną elastyczność. Obie mają wady i zalety. Kolejna zaległa prośba o funkcję to:

FR # 2033: Dodaj by.x i by.y do merge.data.table

Jeśli są jacyś inni, proszę, niech nadchodzą.

W tej części pytania:

dlaczego nie użyć składni scalania dla złączeń zamiast parametru nomatch funkcji dopasowania?

Jeśli wolisz merge()składnię i jego 3 argumenty all, all.xa all.ypotem po prostu użyć że zamiast X[Y]. Myślę, że powinno to obejmować wszystkie przypadki. Albo pan myśli, dlaczego jest argument jeden nomatchw [.data.table? Jeśli tak, to po prostu sposób, który wydawał się naturalny, biorąc pod uwagę FAQ 2.14: „Czy możesz dalej wyjaśnić, dlaczego data.table jest inspirowana składnią A [B] w bazie?”. Ale nomatchobecnie przyjmuje tylko dwie wartości 0i NA. Można to rozszerzyć tak, aby wartość ujemna coś oznaczała, lub 12 oznaczałoby na przykład użycie wartości 12 wiersza do wypełnienia NA, lub nomatchw przyszłości może to być wektor lub nawet sam a data.table.

Hm. Jak by -without-by współdziałał z merge = TRUE? Może powinniśmy przejąć to do datatable-help .

Matt Dowle
źródło
Dzięki @Matthew. Odpowiedź @ mnel jest doskonała, ale moje pytanie nie brzmiało: jak wykonać pełne lub lewe sprzężenie, ale „Czy istnieje powód, dla którego obsługiwane są tylko dwa typy złączeń?” Więc teraz jest to trochę bardziej filozoficzne ;-) Właściwie nie wolę składni scalania, ale wydaje się, że istnieje tradycja R polegająca na budowaniu na istniejących rzeczach, które ludzie znają. Nabazgrałem join="all", join="all.x", join="all.y" and join="x.and.y"na marginesie moich notatek. Nie jestem pewien, czy to jest lepsze.
Douglas Clark
@DouglasClark Może jointak, dobry pomysł. Wysłałem do datatable-help, więc zobaczmy. Może też poświęć data.tabletrochę czasu na osiedlenie się. Czy na przykład musiałeś już obejść się bez-przez i dołączyć do dziedziczonego zakresu ?
Matt Dowle
Jak wskazano w moim komentarzu wyżej, sugeruję dodanie joinsłowa kluczowego do, kiedy jest DataTable: X[Y,j,join=string]. Sugerowane możliwe wartości ciągów dla łączenia to: 1) „all.y” i „right” -
Douglas Clark
1
Cześć Matt, biblioteka data.table jest fantastyczna; Dziękuję za to; chociaż myślę, że zachowanie złączenia (które jest domyślnie prawym złączeniem zewnętrznym) powinno być wyraźnie wyjaśnione w głównej dokumentacji; zajęło mi to 3 dni.
Timothée HENRY
1
@tucson Tylko do połączenia tutaj, teraz zgłoszone jako numer 709 .
Matt Dowle,
17

Ta „odpowiedź” jest propozycją do dyskusji: Jak wskazałem w moim komentarzu, sugeruję dodanie joinparametru do [.data.table (), aby umożliwić dodatkowe typy złączeń, tj X[Y,j,join=string]. : . Oprócz 4 typów zwykłych sprzężeń sugeruję również obsługę 3 typów łączeń wyłącznych i łączeń krzyżowych .

Proponowane joinwartości ciągów (i aliasy) dla różnych typów złączeń to:

  1. "all.y"i "right"- łączenie prawe, obecne data.table default (nomatch = NA) - wszystkie wiersze Y z NA, gdzie nie ma dopasowania X;
  2. "both"oraz "inner" - łączenie wewnętrzne (nomatch = 0) - tylko wiersze, w których X i Y pasują;

  3. "all.x"i "left" - left join - wszystkie wiersze z X, NAs, gdzie nie ma dopasowania Y:

  4. "outer"oraz "full" - pełne sprzężenie zewnętrzne - wszystkie wiersze z X i Y, NA, gdzie nie ma zgodności

  5. "only.x"oraz "not.y"- non-join lub anti-join zwracające wiersze X, w których nie ma dopasowania Y.

  6. "only.y" oraz "not.x"- non-join lub anti-join zwracające wiersze Y, w których nie ma dopasowania X.
  7. "not.both" - łączenie wyłączne zwracające wiersze X i Y, gdzie nie ma dopasowania do innej tabeli, tj. wyłączne lub (XOR)
  8. "cross"- łączenie krzyżowe lub iloczyn kartezjański z każdym wierszem X dopasowanym do każdego wiersza Y

Wartość domyślna join="all.y"odpowiada bieżącej wartości domyślnej.

Wartości ciągów „all”, „all.x” i „all.y” odpowiadają merge()parametrom. Łańcuchy „prawy”, „lewy”, „wewnętrzny” i „zewnętrzny” mogą być bardziej odpowiednie dla użytkowników SQL.

Ciągi „zarówno”, jak i „not.both” to moja najlepsza propozycja w tej chwili - ale ktoś może mieć lepsze sugestie dotyczące łączenia wewnętrznego i wyłącznego. (Nie jestem pewien, czy „wyłączność” to właściwa terminologia, popraw mnie, jeśli istnieje odpowiedni termin na złączenie „XOR”).

Użycie join="not.y"jest alternatywą dla składni X[-Y,j]lub X[!Y,j]bez złączenia i może być bardziej zrozumiałe (dla mnie), chociaż nie jestem pewien, czy są one takie same (nowa funkcja w data.table w wersji 1.8.3).

Łączenie krzyżowe może być czasami przydatne, ale może nie pasować do paradygmatu data.table.

Douglas Clark
źródło
1
Wyślij to do datatable-help w celu omówienia.
Matt Dowle
3
+1 Ale proszę albo wyślij do elementu datatable-pomoc , lub złożyć wniosek fabularny . Nie mam nic przeciwko dodaniu, joinale jeśli nie dostanie się do trackera, zostanie zapomniany.
Matt Dowle
1
Widzę, że od jakiegoś czasu nie logowałeś się do SO. Złożyłem to więc w FR # 2301
Matt Dowle,
@MattDowle, +1 za tę funkcję. (Próbowałem to zrobić przez FR # 2301, ale otrzymałem komunikat o odmowie uprawnień).
adilapapaya,
@adilapapaya Przenieśliśmy się z RForge do GitHub. Prosimy o +1 tutaj: github.com/Rdatatable/data.table/issues/614 . Arun przeniósł problemy, aby nie zostały utracone.
Matt Dowle,