Por que a junção X [Y] do data.tables não permite uma junção externa completa ou uma junção esquerda?

123

Essa é uma pergunta filosófica sobre a sintaxe de junção data.table. Estou encontrando mais e mais usos para data.tables, mas ainda estou aprendendo ...

O formato de junção X[Y]para data.tables é muito conciso, prático e eficiente, mas, tanto quanto posso dizer, ele suporta apenas junções internas e externas. Para obter uma junção externa esquerda ou total, preciso usar merge:

  • X[Y, nomatch = NA] - todas as linhas em Y - junção externa direita (padrão)
  • X[Y, nomatch = 0] - apenas linhas com correspondências em X e Y - junção interna
  • merge(X, Y, all = TRUE) - todas as linhas de X e Y - junção externa completa
  • merge(X, Y, all.x = TRUE) - todas as linhas em X - junção externa esquerda

Parece-me que seria útil se o X[Y]formato de junção suportasse todos os 4 tipos de junções. Existe uma razão para apenas dois tipos de junções serem suportados?

Para mim, os valores de parâmetro nomatch = 0e nomatch = NAnão são muito intuitivos para as ações que estão sendo executadas. É mais fácil para mim compreender e lembrar a mergesintaxe: all = TRUE, all.x = TRUEe all.y = TRUE. Como a X[Y]operação se assemelha a mergemuito mais do que match, por que não usar a mergesintaxe para junções em vez matchdo nomatchparâmetro da função ?

Aqui estão exemplos de código dos 4 tipos de junção:

# 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

Atualização: data.table v1.9.6 introduziu a on=sintaxe, que permite junções ad hoc em outros campos que não a chave primária. resposta de jangorecki à pergunta Como unir (mesclar) quadros de dados (interno, externo, esquerdo, direito)? fornece alguns exemplos de tipos de junção adicionais que o data.table pode manipular.

Douglas Clark
fonte
4
Você leu a FAQ 1.12 ? Você pode sempre chamar Y[X]se quiser que a junção externa esquerda do X[Y]e rbind(Y[X],X[Y])se você quiser uma junção externa completa
mnel
Veja a minha resposta para uma abordagem mais data.table à junção externa completa
mnel
@mnel, suponho que sua unique()abordagem abaixo para a junção completa seja preferível rbind(Y[X],X[Y]), uma vez que o rbind envolveria a cópia da tabela. Isso está certo?
Douglas Clark
com o melhor de meu conhecimento, sim. Não testei se três chamadas únicas menores são mais rápidas que uma grande (por exemplo unique(c(unique(X[,t]), unique(Y[,t]))- isso deve ser mais eficiente em termos de memória, pois combina apenas duas listas que serão menores ou iguais ao número de linhas em X e Y .
mnel
2
Sua pergunta é uma descrição tão boa; Encontrei respostas para minhas perguntas na sua pergunta. Obrigado
irriss

Respostas:

71

Para citar a data.table FAQ 1.11 Qual é a diferença entre X[Y]e merge(X, Y)?

X[Y] é uma junção, procurando as linhas de X usando Y (ou a chave de Y, se houver) como um índice.

Y[X] é uma junção, procurando as linhas de Y usando X (ou a chave de X, se houver uma)

merge(X,Y)faz os dois lados ao mesmo tempo. O número de linhas de X[Y]e Y[X]geralmente diferem, enquanto o número de linhas retornadas por merge(X,Y)e merge(Y,X)é o mesmo.

MAS isso perde o ponto principal. A maioria das tarefas exige que algo seja feito nos dados após uma junção ou mesclagem. Por que mesclar todas as colunas de dados, apenas para usar um pequeno subconjunto delas depois? Você pode sugerir merge(X[,ColsNeeded1],Y[,ColsNeeded2]), mas isso requer que o programador descubra quais colunas são necessárias. X[Y,j] no data.table faz tudo isso em uma etapa para você. Quando você escreve X[Y,sum(foo*bar)], o data.table inspeciona automaticamente a jexpressão para ver quais colunas ela usa. Ele somente subconecta essas colunas; os outros são ignorados. A memória é criada apenas para as colunas jusadas e as Ycolunas desfrutam de regras R de reciclagem padrão no contexto de cada grupo. Digamos que fooestá dentro Xe a barra está dentro Y(junto com outras 20 colunas em Y). Não éX[Y,sum(foo*bar)] mais rápido para programar e mais rápido para executar do que uma mescla de tudo desperdiçado seguido por um subconjunto?


Se você deseja uma junção externa esquerda 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

Se você deseja uma junção externa completa

# 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
fonte
5
Obrigado @mnel. A FAQ 1.12 não menciona junção externa completa ou esquerda. Sua sugestão de junção externa completa com unique () é uma grande ajuda. Isso deve estar no FAQ. Eu sei que Matthew Dowle "o projetou para seu próprio uso, e ele quis assim". (FAQ 1.9), mas achei que X[Y,all=T]poderia ser uma maneira elegante de especificar uma junção externa completa na sintaxe data.table X [Y]. Ou X[Y,all.x=T]para a junção esquerda. Gostaria de saber por que não foi projetado dessa maneira. Apenas um pensamento.
Douglas Clark
1
@DouglasClark Adicionaram resposta e arquivaram 2302: Adicione a sintaxe de junção de mesclagem de mnel à FAQ (com horários) . Ótimas sugestões!
quer
1
@mnel Graças para a solução ... fez o meu dia ... :)
Ankit
@mnel Existe alguma maneira de atribuir NAs com 0 ao executar X[Y[J(unique_keys)]]?
Ankit
11
O que me impressiona sobre a documentação data.table é que ele pode ser tão detalhado, ainda permanecem tão enigmática ...
NiuBiBang
24

A resposta do @ mnel é imediata, então aceite essa resposta. Isso é apenas acompanhamento, muito tempo para comentários.

Como mnel diz, a junção externa esquerda / direita é obtida trocando Ye X: Y[X]-vs- X[Y]. Portanto, três dos quatro tipos de junção são suportados nessa sintaxe, não em 2, iiuc.

Adicionando o quarto parece uma boa idéia. Digamos que adicionemos full=TRUEou both=TRUEou merge=TRUE(não tenho certeza do melhor nome do argumento?), Então não me ocorreu antes que isso X[Y,j,merge=TRUE]seria útil pelos motivos após o MAS, na FAQ 1.12. Agora, a solicitação de novos recursos foi adicionada e vinculada aqui, obrigado:

FR # 2301: Adicionar argumento de mesclagem = VERDADEIRO para as junções X [Y] e Y [X] como mescla ().

Versões recentes foram aceleradas merge.data.table(fazendo uma cópia superficial internamente para definir as chaves com mais eficiência, por exemplo). Portanto, estamos tentando aproximar merge()e X[Y]aproximar e fornecer todas as opções ao usuário para obter total flexibilidade. Existem prós e contras de ambos. Outra solicitação de recurso pendente é:

FR # 2033: adicione by.xe by.y para mesclar.data.table

Se houver outros, mantenha-os próximos.

Por esta parte da pergunta:

por que não usar a sintaxe de mesclagem para junções em vez do parâmetro nomatch da função de correspondência?

Se preferir merge()sintaxe e seus 3 argumentos all, all.xe all.ydepois é só usar que em vez de X[Y]. Pense que deve cobrir todos os casos. Ou você quis dizer por que é o argumento de um único nomatchem [.data.table? Nesse caso, é exatamente o que parecia natural, dada a FAQ 2.14: "Você pode explicar melhor por que o data.table é inspirado na sintaxe A [B] na base?". Mas também nomatchleva apenas dois valores atualmente 0e NA. Isso poderia ser estendido para que um valor negativo significasse alguma coisa, ou 12 significaria usar os valores da 12ª linha para preencher NAs, por exemplo, ou nomatchno futuro poderia ser um vetor ou mesmo a data.table.

Hum. Como o by-sem-by interage com merge = TRUE? Talvez devêssemos levar isso para a ajuda de dados .

Matt Dowle
fonte
Obrigado @Matthew. A resposta do @ mnel é excelente, mas minha pergunta não era como fazer uma junção completa ou esquerda, mas "Existe uma razão pela qual apenas dois tipos de junções são suportados?" Portanto, agora é um pouco mais filosófico ;-) Na verdade, eu não prefiro a sintaxe de mesclagem, mas parece haver uma tradição em R para desenvolver coisas existentes com as quais as pessoas estão familiarizadas. Eu havia rabiscado join="all", join="all.x", join="all.y" and join="x.and.y"na margem das minhas anotações. Não tenho certeza se isso é melhor.
Douglas Clark
@DouglasClark Talvez joinassim, boa ideia. Eu postei na datatable-help, então vamos ver. Talvez dê data.tablealgum tempo para se instalar também. Você já conseguiu passar por sem, por exemplo, e ingressar no escopo herdado ?
quer
Como indicado no meu comentário acima, eu sugiro a adição de uma joinpalavra-chave para, quando i é uma tabela de dados: X[Y,j,join=string]. Os possíveis valores de cadeia para se juntar são sugeridas para ser: 1) "all.y" e "direita" -
Douglas Clark
1
Oi Matt, a biblioteca data.table é fantástica; obrigado por isso; embora eu ache que o comportamento da junção (sendo uma junção externa direita por padrão) deve ser explicado com destaque na documentação principal; Levei 3 dias para descobrir isso.
Timothée HENRY
1
@ tucson Apenas para vincular aqui, agora arquivado como edição # 709 .
precisa
17

Esta "resposta" é uma proposta para discussão: Como indicado no meu comentário, sugiro adicionando um joinparâmetro para [.data.table () para permitir que outros tipos de junta, ou seja: X[Y,j,join=string]. Além dos 4 tipos de junções comuns, também sugiro oferecer suporte a 3 tipos de junções exclusivas e a junção cruzada .

Os joinvalores de sequência (e aliases) para os vários tipos de junção são propostos como:

  1. "all.y"e "right"- junção direita, o presente padrão data.table (nomatch = NA) - todas as linhas Y com NAs onde não há correspondência X;
  2. "both"e "inner" - junção interna (nomatch = 0) - apenas linhas onde X e Y correspondem;

  3. "all.x"e "left" - junção esquerda - todas as linhas de X, NAs onde nenhum Y corresponde:

  4. "outer"e "full" - junção externa completa - todas as linhas de X e Y, NAs onde não há correspondência

  5. "only.x"e "not.y"- linhas X retornando não-junção ou anti-junção, onde não há correspondência Y

  6. "only.y" e "not.x"- linhas Y que retornam sem associação ou anti-associação, onde não há correspondência X
  7. "not.both" - junção exclusiva retornando linhas X e Y onde não há correspondência com a outra tabela, ou seja, um exclusivo ou (XOR)
  8. "cross"- junção cruzada ou produto cartesiano com cada linha de X correspondente a cada linha de Y

O valor padrão é o join="all.y"que corresponde ao padrão atual.

Os valores das strings "all", "all.x" e "all.y" correspondem aos merge()parâmetros. As cadeias "direita", "esquerda", "interna" e "externa" podem ser mais acessíveis aos usuários do SQL.

As strings "both" e "not.both" são minha melhor sugestão no momento - mas alguém pode ter sugestões de strings melhores para a junção interna e exclusiva. (Não tenho certeza se "exclusivo" é a terminologia correta, corrija-me se houver um termo adequado para uma associação "XOR".)

O uso de join="not.y"uma alternativa para X[-Y,j]ou X[!Y,j]não participar de sintaxe e talvez mais claro (para mim), embora eu não tenho certeza se eles são os mesmos (recurso novo na versão data.table 1.8.3).

Às vezes, a junção cruzada pode ser útil, mas pode não se encaixar no paradigma data.table.

Douglas Clark
fonte
1
Envie-o para a ajuda da tabela de dados para discussão.
precisa
3
+1 Mas, por favor , quer enviar para datatable-ajuda , ou apresentar um pedido de recurso . Não me importo de adicionar, joinmas, a menos que chegue ao rastreador, será esquecido.
precisa
1
Vejo que você não está conectado ao SO há um tempo. Então, eu tenho apresentado esta em FR # 2301
Matt Dowle
@MattDowle, +1 para este recurso. (Tentei fazê-lo via FR # 2301, mas recebo uma mensagem de permissão negada).
adilapapaya
@adilapapaya Passamos do RForge para o GitHub. Marque +1 aqui: github.com/Rdatatable/data.table/issues/614 . Arun transmitiu os problemas para que não se perdessem.
precisa saber é o seguinte