Come unire (unire) i frame di dati (interno, esterno, sinistro, destro)


1233

Dati due frame di dati:

df1 = data.frame(CustomerId = c(1:6), Product = c(rep("Toaster", 3), rep("Radio", 3)))
df2 = data.frame(CustomerId = c(2, 4, 6), State = c(rep("Alabama", 2), rep("Ohio", 1)))

df1
#  CustomerId Product
#           1 Toaster
#           2 Toaster
#           3 Toaster
#           4   Radio
#           5   Radio
#           6   Radio

df2
#  CustomerId   State
#           2 Alabama
#           4 Alabama
#           6    Ohio

Come posso fare lo stile del database, cioè lo stile sql, i join ? Cioè, come posso ottenere:

  • Un join interno di df1e df2:
    restituisce solo le righe in cui la tabella di sinistra ha le chiavi corrispondenti nella tabella di destra.
  • Un join esterno di df1e df2:
    restituisce tutte le righe da entrambe le tabelle, unisce i record da sinistra che hanno le chiavi corrispondenti nella tabella di destra.
  • Un join esterno sinistro (o semplicemente un join sinistro) di df1e df2
    Restituisce tutte le righe dalla tabella di sinistra e tutte le righe con le chiavi corrispondenti della tabella di destra.
  • Un join esterno destro di df1e df2
    Restituisce tutte le righe dalla tabella di destra e tutte le righe con le chiavi corrispondenti dalla tabella di sinistra.

Credito extra:

Come posso fare un'istruzione select in stile SQL?


4
stat545-ubc.github.io/bit001_dplyr-cheatsheet.html ← la mia risposta preferita a questa domanda
isomorfismi

La trasformazione dei dati con cheat sheet dplyr creato e gestito da RStudio ha anche delle belle infografiche su come funzionano i join in dplyr rstudio.com/resources/cheatsheets
Arthur Yip,

2
Se sei arrivato qui invece voler sapere sull'unione panda dataframes, tale risorsa può essere trovato qui .
cs95,

Risposte:


1350

Utilizzando la mergefunzione e i suoi parametri opzionali:

Join interno: merge(df1, df2) funzionerà per questi esempi perché R unisce automaticamente i frame con nomi di variabili comuni, ma molto probabilmente vorrai specificaremerge(df1, df2, by = "CustomerId")per assicurarti di trovare la corrispondenza solo sui campi desiderati. È inoltre possibile utilizzare iparametriby.xeby.yse le variabili corrispondenti hanno nomi diversi nei diversi frame di dati.

Join esterno: merge(x = df1, y = df2, by = "CustomerId", all = TRUE)

Esterno sinistro: merge(x = df1, y = df2, by = "CustomerId", all.x = TRUE)

Esterno destro: merge(x = df1, y = df2, by = "CustomerId", all.y = TRUE)

Cross join: merge(x = df1, y = df2, by = NULL)

Proprio come con il join interno, probabilmente vorrai passare esplicitamente "CustomerId" a R come variabile corrispondente. Penso che sia quasi sempre meglio dichiarare esplicitamente gli identificatori su cui si desidera unire; è più sicuro se l'input data.frames cambia in modo imprevisto e più facile da leggere in seguito.

Puoi unire più colonne dando byun vettore, ad es.by = c("CustomerId", "OrderId") .

Se i nomi delle colonne su cui fondersi non coincidono, è possibile specificare, ad esempio, by.x = "CustomerId_in_df1", by.y = "CustomerId_in_df2"dove si CustomerId_in_df1trova il nome della colonna nel primo frame di dati e CustomerId_in_df2il nome della colonna nel secondo frame di dati. (Questi possono anche essere vettori se è necessario unire più colonne.)


2
@MattParker Ho usato il pacchetto sqldf per un'intera serie di query complesse contro i dataframe, ne avevo davvero bisogno per fare un self-cross join (ovvero data.frame cross-join stesso) Mi chiedo come si paragona dal punto di vista delle prestazioni ... . ???
Nicholas Hamilton,

9
@ADP Non ho mai usato sqldf, quindi non sono sicuro della velocità. Se le prestazioni sono un grosso problema per te, dovresti anche esaminare il data.tablepacchetto: è un set completamente nuovo di sintassi del join, ma è radicalmente più veloce di qualsiasi cosa di cui stiamo parlando qui.
Matt Parker,

5
Con più chiarezza e spiegazione ..... mkmanu.wordpress.com/2016/04/08/…
Manoj Kumar

42
Un'aggiunta minore che mi è stata utile - Quando vuoi merge(x=df1,y=df2, by.x=c("x_col1","x_col2"), by.y=c("y_col1","y_col2"))
unirti

8
Funziona data.tableora, stessa funzione appena più veloce.
Marbel,

222

Consiglio di dare un'occhiata al pacchetto sqldf di Gabor Grothendieck , che consente di esprimere queste operazioni in SQL.

library(sqldf)

## inner join
df3 <- sqldf("SELECT CustomerId, Product, State 
              FROM df1
              JOIN df2 USING(CustomerID)")

## left join (substitute 'right' for right join)
df4 <- sqldf("SELECT CustomerId, Product, State 
              FROM df1
              LEFT JOIN df2 USING(CustomerID)")

Trovo che la sintassi SQL sia più semplice e più naturale del suo equivalente R (ma questo potrebbe riflettere solo il mio pregiudizio RDBMS).

Vedi GitHub sqldf di Gabor per ulteriori informazioni sui join.


198

Esiste l' approccio data.table per un join interno, che è molto efficiente in termini di tempo e memoria (e necessario per alcuni data.frames più grandi):

library(data.table)

dt1 <- data.table(df1, key = "CustomerId") 
dt2 <- data.table(df2, key = "CustomerId")

joined.dt1.dt.2 <- dt1[dt2]

mergefunziona anche su data.tables (in quanto è generico e chiama merge.data.table)

merge(dt1, dt2)

data.table documentato su stackoverflow:
come eseguire un'operazione di unione data.table
Tradurre join SQL su chiavi esterne in sintassi R data.table
Alternative efficienti da unire per data.frames più grandi R
Come eseguire un join esterno sinistro di base con data.table in R?

Ancora un'altra opzione è la joinfunzione trovata nel pacchetto plyr

library(plyr)

join(df1, df2,
     type = "inner")

#   CustomerId Product   State
# 1          2 Toaster Alabama
# 2          4   Radio Alabama
# 3          6   Radio    Ohio

Opzioni per type: inner, left, right,full .

Da ?join: A differenza di merge, [ join] conserva l'ordine di x indipendentemente dal tipo di join utilizzato.


8
+1 per la menzione plyr::join. Il microbenchmarking indica che esegue circa 3 volte più velocemente di merge.
Beasterfield,

20
Tuttavia, data.tableè molto più veloce di entrambi. C'è anche un grande supporto in SO, non vedo molti autori di pacchetti che rispondono alle domande qui tanto quanto lo data.tablescrittore o i collaboratori.
Marbel

1
Qual è la data.tablesintassi per unire un elenco di frame di dati ?
Aleksandr Blekh,

5
Nota: dt1 [dt2] è un join esterno destro (non un join interno "puro") in modo che TUTTE le righe da dt2 facciano parte del risultato anche se non c'è riga corrispondente in dt1. Impatto: si ottengono righe potenzialmente indesiderate se si hanno valori chiave in dt2 che non corrispondono ai valori chiave di dt1.
R Yoda,

8
@RYoda puoi semplicemente specificare nomatch = 0Lin quel caso.
David Arenburg,

181

Puoi anche iscriverti usando il fantastico pacchetto dplyr di Hadley Wickham .

library(dplyr)

#make sure that CustomerId cols are both type numeric
#they ARE not using the provided code in question and dplyr will complain
df1$CustomerId <- as.numeric(df1$CustomerId)
df2$CustomerId <- as.numeric(df2$CustomerId)

Join mutanti: aggiungi colonne a df1 usando le corrispondenze in df2

#inner
inner_join(df1, df2)

#left outer
left_join(df1, df2)

#right outer
right_join(df1, df2)

#alternate right outer
left_join(df2, df1)

#full join
full_join(df1, df2)

Filtering join: filtra le righe in df1, non modificare le colonne

semi_join(df1, df2) #keep only observations in df1 that match in df2.
anti_join(df1, df2) #drops all observations in df1 that match in df2.

16
Perché è necessario convertire CustomerIdin numerico? Non vedo alcuna menzione nella documentazione (per entrambi plyre dplyr) su questo tipo di restrizione. Il tuo codice funzionerebbe in modo errato, se la colonna di unione fosse di charactertipo (particolarmente interessato a plyr)? Mi sto perdendo qualcosa?
Aleksandr Blekh,

Si potrebbe usare semi_join (df1, df2, df3, df4) per mantenere solo le osservazioni in df1 che corrispondono al resto delle colonne?
Ghose Bishwajit,

@GhoseBishwajit Supponendo che tu intenda il resto dei frame di dati anziché le colonne, potresti usare rbind su df2, df3 e df4 se hanno la stessa struttura, ad esempio semi_join (df1, rbind (df2, df3, df4))
abhy3

Sì, intendevo frame di dati. Ma non hanno la stessa struttura di alcuni mancanti in alcune righe. Per quattro dataframe, ho i dati su quattro diversi indicatori (PIL, GNP GINI, MMR) per diversi numeri di paesi. Voglio unirmi ai frame di dati in modo da mantenere presenti solo quei paesi per tutti e quattro gli indicatori.
Ghose Bishwajit

86

Ci sono alcuni buoni esempi di come farlo nel R Wiki . Ne ruberò un paio qui:

Metodo di unione

Poiché le tue chiavi hanno lo stesso nome, il modo più rapido per eseguire un join interno è merge ():

merge(df1,df2)

un join interno completo (tutti i record di entrambe le tabelle) può essere creato con la parola chiave "all":

merge(df1,df2, all=TRUE)

un join esterno sinistro di df1 e df2:

merge(df1,df2, all.x=TRUE)

un join esterno destro di df1 e df2:

merge(df1,df2, all.y=TRUE)

puoi capovolgerli, schiaffeggiarli e strofinarli per ottenere gli altri due join esterni di cui hai chiesto :)

Metodo di sottoscrizione

Un join esterno sinistro con df1 a sinistra utilizzando un metodo pedice sarebbe:

df1[,"State"]<-df2[df1[ ,"Product"], "State"]

L'altra combinazione di join esterni può essere creata mescolando l'esempio di sottoscrizione del join esterno sinistro. (sì, lo so che è l'equivalente di dire "lo lascerò come esercizio per il lettore ...")


4
Il link "R Wiki" è interrotto.
zx8754,

79

Novità 2014:

Soprattutto se sei anche interessato alla manipolazione dei dati in generale (inclusi ordinamento, filtro, sottoimpostazione, riepilogo ecc.), Dovresti assolutamente dare un'occhiata a dplyr , che viene fornito con una varietà di funzioni tutte progettate per facilitare il tuo lavoro in particolare con i frame di dati e alcuni altri tipi di database. Offre persino un'interfaccia SQL piuttosto elaborata e persino una funzione per convertire (la maggior parte) codice SQL direttamente in R.

Le quattro funzioni relative all'unione nel pacchetto dplyr sono (per citare):

  • inner_join(x, y, by = NULL, copy = FALSE, ...): restituisce tutte le righe da x dove ci sono valori corrispondenti in y e tutte le colonne da xe y
  • left_join(x, y, by = NULL, copy = FALSE, ...): restituisce tutte le righe da xe tutte le colonne da xey
  • semi_join(x, y, by = NULL, copy = FALSE, ...): restituisce tutte le righe da x dove ci sono valori corrispondenti in y, mantenendo solo le colonne da x.
  • anti_join(x, y, by = NULL, copy = FALSE, ...): restituisce tutte le righe da x dove non ci sono valori corrispondenti in y, mantenendo solo le colonne da x

È tutto qui in grande dettaglio.

La selezione delle colonne può essere eseguita da select(df,"column"). Se questo non è abbastanza SQL per te, allora c'è la sql()funzione, in cui puoi inserire il codice SQL così com'è, e farà l'operazione che hai specificato proprio come stavi scrivendo sempre in R (per maggiori informazioni, fai riferimento alla vignetta dplyr / database ). Ad esempio, se applicato correttamente, sql("SELECT * FROM hflights")selezionerà tutte le colonne dalla tabella dplyr "hflights" (a "tbl").


Decisamente la soluzione migliore data l'importanza che il pacchetto dplyr ha guadagnato negli ultimi due anni.
Marco Fumagalli,

72

Aggiornamento sui metodi data.table per unire set di dati. Vedi gli esempi seguenti per ogni tipo di join. Esistono due metodi, uno da [.data.tablequando si passa il secondo data.table come primo argomento al sottoinsieme, un altro modo è utilizzare la mergefunzione che invia al metodo data.table veloce.

df1 = data.frame(CustomerId = c(1:6), Product = c(rep("Toaster", 3), rep("Radio", 3)))
df2 = data.frame(CustomerId = c(2L, 4L, 7L), State = c(rep("Alabama", 2), rep("Ohio", 1))) # one value changed to show full outer join

library(data.table)

dt1 = as.data.table(df1)
dt2 = as.data.table(df2)
setkey(dt1, CustomerId)
setkey(dt2, CustomerId)
# right outer join keyed data.tables
dt1[dt2]

setkey(dt1, NULL)
setkey(dt2, NULL)
# right outer join unkeyed data.tables - use `on` argument
dt1[dt2, on = "CustomerId"]

# left outer join - swap dt1 with dt2
dt2[dt1, on = "CustomerId"]

# inner join - use `nomatch` argument
dt1[dt2, nomatch=NULL, on = "CustomerId"]

# anti join - use `!` operator
dt1[!dt2, on = "CustomerId"]

# inner join - using merge method
merge(dt1, dt2, by = "CustomerId")

# full outer join
merge(dt1, dt2, by = "CustomerId", all = TRUE)

# see ?merge.data.table arguments for other cases

Sotto i test di benchmark base R, sqldf, dplyr e data.table.
Il benchmark testa set di dati non chiave / non indicizzati. Il benchmark viene eseguito su set di dati di righe 50M-1, ci sono valori comuni 50M-2 sulla colonna join in modo che ogni scenario (interno, sinistro, destro, completo) possa essere testato e l'unione non sia ancora banale da eseguire. È il tipo di join che ben sollecita gli algoritmi di join. Tempi sono come dei sqldf:0.4.11, dplyr:0.7.8, data.table:1.12.0.

# inner
Unit: seconds
   expr       min        lq      mean    median        uq       max neval
   base 111.66266 111.66266 111.66266 111.66266 111.66266 111.66266     1
  sqldf 624.88388 624.88388 624.88388 624.88388 624.88388 624.88388     1
  dplyr  51.91233  51.91233  51.91233  51.91233  51.91233  51.91233     1
     DT  10.40552  10.40552  10.40552  10.40552  10.40552  10.40552     1
# left
Unit: seconds
   expr        min         lq       mean     median         uq        max 
   base 142.782030 142.782030 142.782030 142.782030 142.782030 142.782030     
  sqldf 613.917109 613.917109 613.917109 613.917109 613.917109 613.917109     
  dplyr  49.711912  49.711912  49.711912  49.711912  49.711912  49.711912     
     DT   9.674348   9.674348   9.674348   9.674348   9.674348   9.674348       
# right
Unit: seconds
   expr        min         lq       mean     median         uq        max
   base 122.366301 122.366301 122.366301 122.366301 122.366301 122.366301     
  sqldf 611.119157 611.119157 611.119157 611.119157 611.119157 611.119157     
  dplyr  50.384841  50.384841  50.384841  50.384841  50.384841  50.384841     
     DT   9.899145   9.899145   9.899145   9.899145   9.899145   9.899145     
# full
Unit: seconds
  expr       min        lq      mean    median        uq       max neval
  base 141.79464 141.79464 141.79464 141.79464 141.79464 141.79464     1
 dplyr  94.66436  94.66436  94.66436  94.66436  94.66436  94.66436     1
    DT  21.62573  21.62573  21.62573  21.62573  21.62573  21.62573     1

Tenere presente che esistono altri tipi di join che è possibile eseguire utilizzando data.table:
- aggiorna su join - se si desidera cercare i valori da un'altra tabella alla tabella principale
- aggregare su join - se si desidera aggregare su chiave a cui si sta effettuando l'unione, non è necessario materializzarsi tutti si riuniscono i risultati
- sovrapposizione unirsi - se si desidera unire da catene
- rolling unirsi - se si vuole si fondono per essere in grado di eguagliare i valori da precede / seguente file da loro rotazione in avanti o indietro
- non equi join - se il vostro la condizione di join non è uguale

Codice da riprodurre:

library(microbenchmark)
library(sqldf)
library(dplyr)
library(data.table)
sapply(c("sqldf","dplyr","data.table"), packageVersion, simplify=FALSE)

n = 5e7
set.seed(108)
df1 = data.frame(x=sample(n,n-1L), y1=rnorm(n-1L))
df2 = data.frame(x=sample(n,n-1L), y2=rnorm(n-1L))
dt1 = as.data.table(df1)
dt2 = as.data.table(df2)

mb = list()
# inner join
microbenchmark(times = 1L,
               base = merge(df1, df2, by = "x"),
               sqldf = sqldf("SELECT * FROM df1 INNER JOIN df2 ON df1.x = df2.x"),
               dplyr = inner_join(df1, df2, by = "x"),
               DT = dt1[dt2, nomatch=NULL, on = "x"]) -> mb$inner

# left outer join
microbenchmark(times = 1L,
               base = merge(df1, df2, by = "x", all.x = TRUE),
               sqldf = sqldf("SELECT * FROM df1 LEFT OUTER JOIN df2 ON df1.x = df2.x"),
               dplyr = left_join(df1, df2, by = c("x"="x")),
               DT = dt2[dt1, on = "x"]) -> mb$left

# right outer join
microbenchmark(times = 1L,
               base = merge(df1, df2, by = "x", all.y = TRUE),
               sqldf = sqldf("SELECT * FROM df2 LEFT OUTER JOIN df1 ON df2.x = df1.x"),
               dplyr = right_join(df1, df2, by = "x"),
               DT = dt1[dt2, on = "x"]) -> mb$right

# full outer join
microbenchmark(times = 1L,
               base = merge(df1, df2, by = "x", all = TRUE),
               dplyr = full_join(df1, df2, by = "x"),
               DT = merge(dt1, dt2, by = "x", all = TRUE)) -> mb$full

lapply(mb, print) -> nul

Vale la pena aggiungere un esempio che mostri come utilizzare nomi di colonna diversi on = anche in?
SymbolixAU,

1
@Symbolix potremmo aspettare la versione 1.9.8 in quanto aggiungerà operatori non-equi agli onarg
jangorecki,

Un altro pensiero; vale la pena aggiungere una nota che con merge.data.tablec'è l' sort = TRUEargomento predefinito , che aggiunge una chiave durante l'unione e la lascia lì nel risultato. Questo è qualcosa a cui fare attenzione, specialmente se stai cercando di evitare di impostare le chiavi.
SymbolixAU

1
Sono sorpreso che nessuno abbia menzionato che la maggior parte di quelli non funzionano se ci sono
duplicati

@statquant Puoi fare un join cartesiano con data.table, cosa vuoi dire? Puoi essere più specifico per favore.
David Arenburg,

32

dplyr dal 0.4 ha implementato tutti quei join inclusi outer_join, ma vale la pena notare che per le prime versioni precedenti alla 0.4 non era in grado di offrire outer_joine, di conseguenza, c'era un sacco di codice utente davvero pessimo per risolvere il problema che galleggiava per un po ' successivamente (puoi ancora trovare tale codice in SO, Kaggle risponde, github di quel periodo. Quindi questa risposta ha ancora uno scopo utile.)

Aspetti salienti della versione relativa a Join :

v0.5 ( 6/2016 )

  • Gestione per tipo POSIXct, fusi orari, duplicati, diversi livelli di fattore. Migliori errori e avvisi.
  • Nuovo argomento suffisso per controllare quali suffissi ricevono i nomi delle variabili duplicate (# 1296)

v0.4.0 (1/2015)

  • Implementa join destro e join esterno (# 96)
  • Join mutanti, che aggiungono nuove variabili a una tabella dalle righe corrispondenti in un'altra. Join di filtro, che filtrano le osservazioni da una tabella in base alla corrispondenza con un'osservazione nell'altra tabella.

v0.3 (10/2014)

  • Ora può left_join da diverse variabili in ogni tabella: df1%>% left_join (df2, c ("var1" = "var2"))

v0.2 (5/2014)

  • * _join () non riordina più i nomi delle colonne (# 324)

v0.1.3 (4/2014)

Soluzioni alternative per i commenti di hadley in quel numero:

  • right_join (x, y) è uguale a left_join (y, x) in termini di righe, solo le colonne avranno ordini diversi. Facilmente lavorato con select (new_column_order)
  • outer_join è fondamentalmente unione (left_join (x, y), right_join (x, y)) - vale a dire preservare tutte le righe in entrambi i frame di dati.

1
@Gregor: no, non dovrebbe essere eliminato. È importante che gli utenti R sappiano che le funzionalità di join sono mancate da molti anni, poiché la maggior parte del codice disponibile contiene soluzioni alternative o implementazioni manuali ad hoc o ad hoc con vettori di indici, o peggio ancora evita di usare questi pacchetti o operazioni a tutti. Ogni settimana vedo tali domande su SO. Annulleremo la confusione per molti anni a venire.
smci,

@Gregor e altri che hanno chiesto: aggiornato, riassumendo i cambiamenti storici e ciò che mancava da diversi anni quando è stata posta questa domanda. Questo illustra perché il codice di quel periodo era principalmente confuso, o evitato di usare i join dplyr e ricadde in unione. Se controlli i codebase storici su SO e Kaggle puoi comunque vedere il ritardo nell'adozione e il codice utente molto confuso che ne risulta. Fammi sapere se trovi ancora questa risposta carente.
smci

@Gregor: Quelli di noi che l'hanno adottato a metà 2014 non hanno scelto il momento migliore. (Pensavo ci fossero versioni precedenti (0.0.x) in giro per il 2013, ma no, il mio errore.) Indipendentemente da ciò, nel 2015 c'era ancora molto codice di merda, questo è ciò che mi ha motivato a postare questo, stavo cercando di demistificare il grezzo che ho trovato su Kaggle, Github, SO.
smci

2
Sì, ho capito, e penso che tu faccia un buon lavoro. (Sono stato anche uno dei primi ad adottare, e mentre mi piace ancora la dplyrsintassi, il passaggio da lazyevala rlangbackend ha rotto un sacco di codice per me, che mi ha spinto a saperne di più data.table, e ora lo uso principalmente data.table.)
Gregor Thomas,

@Gregor: interessante, puoi indicarmi eventuali domande e risposte (le tue o quelle di altri) che lo riguardano? Sembra che ogni nostra adozione di plyr/ dplyr/ data.table/ tidyverse dipenda enormemente da quale anno abbiamo iniziato e in quale stato (embrionale) ci fossero i pacchi allora, al contrario di adesso ...
smci

25

Unendo due frame di dati con ~ 1 milione di righe ciascuno, uno con 2 colonne e l'altro con ~ 20, ho scoperto sorprendentemente merge(..., all.x = TRUE, all.y = TRUE)di essere più veloce allora dplyr::full_join(). Questo è con dplyr v0.4

L'unione richiede ~ 17 secondi, full_join richiede ~ 65 secondi.

Un po 'di cibo, però, dal momento che generalmente non considero dplyr per compiti di manipolazione.


24

Nel caso di un join sinistro con una 0..*:0..1cardinalità o un join destro con una 0..1:0..*cardinalità è possibile assegnare sul posto le colonne unilaterali dal joiner (la 0..1tabella) direttamente al joinee (il0..* tabella), evitando così la creazione di una tabella di dati completamente nuova. Ciò richiede l'abbinamento delle colonne chiave dal joinee al joiner e l'indicizzazione + ordinando le righe del joiner di conseguenza per l'assegnazione.

Se la chiave è una singola colonna, allora possiamo usare una sola chiamata a match() per fare la corrispondenza. Questo è il caso che tratterò in questa risposta.

Ecco un esempio basato sull'OP, tranne per il fatto che ho aggiunto una riga aggiuntiva df2con un ID di 7 per testare il caso di una chiave non corrispondente nel joiner. Questo è effettivamente df1lasciato unire df2:

df1 <- data.frame(CustomerId=1:6,Product=c(rep('Toaster',3L),rep('Radio',3L)));
df2 <- data.frame(CustomerId=c(2L,4L,6L,7L),State=c(rep('Alabama',2L),'Ohio','Texas'));
df1[names(df2)[-1L]] <- df2[match(df1[,1L],df2[,1L]),-1L];
df1;
##   CustomerId Product   State
## 1          1 Toaster    <NA>
## 2          2 Toaster Alabama
## 3          3 Toaster    <NA>
## 4          4   Radio Alabama
## 5          5   Radio    <NA>
## 6          6   Radio    Ohio

In quanto precede, ho codificato il presupposto che la colonna chiave è la prima colonna di entrambe le tabelle di input. Direi che, in generale, questo non è un presupposto irragionevole, poiché, se si dispone di un data.frame con una colonna chiave, sarebbe strano se non fosse stato impostato come prima colonna del data.frame da all'inizio. E puoi sempre riordinare le colonne per renderlo tale. Una conseguenza vantaggiosa di questo presupposto è che il nome della colonna chiave non deve essere codificato, anche se suppongo che sostituisca solo un presupposto con un altro. La concisione è un altro vantaggio dell'indicizzazione di numeri interi e della velocità. Nei benchmark di seguito cambierò l'implementazione per utilizzare l'indicizzazione del nome stringa per adattarla alle implementazioni concorrenti.

Penso che questa sia una soluzione particolarmente appropriata se hai diverse tabelle che vuoi lasciare unite contro una singola tabella di grandi dimensioni. La ricostruzione ripetuta dell'intera tabella per ogni unione sarebbe inutile e inefficiente.

D'altra parte, se è necessario che il membro del team rimanga inalterato attraverso questa operazione per qualsiasi motivo, questa soluzione non può essere utilizzata, poiché modifica direttamente il membro del personale. Anche se in quel caso potresti semplicemente fare una copia ed eseguire le assegnazioni sul posto sulla copia.


Come nota a margine, ho esaminato brevemente le possibili soluzioni di corrispondenza per i tasti a più colonne. Sfortunatamente, le uniche soluzioni corrispondenti che ho trovato sono state:

  • concatenazioni inefficienti. ad esempio match(interaction(df1$a,df1$b),interaction(df2$a,df2$b)), o la stessa idea con paste().
  • congiunzioni cartesiane inefficienti, ad es outer(df1$a,df2$a,`==`) & outer(df1$b,df2$b,`==`).
  • base R merge()ed equivalenti funzioni di unione basate su pacchetto, che allocano sempre una nuova tabella per restituire il risultato unito e quindi non sono adatti per una soluzione basata sull'assegnazione sul posto.

Ad esempio, vedi Corrispondenza di più colonne su diversi frame di dati e ottenimento di altre colonne come risultato , corrispondenza di due colonne con altre due colonne , Corrispondenza su più colonne e duplicazione di questa domanda in cui inizialmente ho trovato la soluzione sul posto, Combina due frame di dati con diverso numero di righe R .


Analisi comparativa

Ho deciso di fare il mio benchmarking per vedere come l'approccio di assegnazione sul posto si confronta con le altre soluzioni che sono state offerte in questa domanda.

Codice di prova:

library(microbenchmark);
library(data.table);
library(sqldf);
library(plyr);
library(dplyr);

solSpecs <- list(
    merge=list(testFuncs=list(
        inner=function(df1,df2,key) merge(df1,df2,key),
        left =function(df1,df2,key) merge(df1,df2,key,all.x=T),
        right=function(df1,df2,key) merge(df1,df2,key,all.y=T),
        full =function(df1,df2,key) merge(df1,df2,key,all=T)
    )),
    data.table.unkeyed=list(argSpec='data.table.unkeyed',testFuncs=list(
        inner=function(dt1,dt2,key) dt1[dt2,on=key,nomatch=0L,allow.cartesian=T],
        left =function(dt1,dt2,key) dt2[dt1,on=key,allow.cartesian=T],
        right=function(dt1,dt2,key) dt1[dt2,on=key,allow.cartesian=T],
        full =function(dt1,dt2,key) merge(dt1,dt2,key,all=T,allow.cartesian=T) ## calls merge.data.table()
    )),
    data.table.keyed=list(argSpec='data.table.keyed',testFuncs=list(
        inner=function(dt1,dt2) dt1[dt2,nomatch=0L,allow.cartesian=T],
        left =function(dt1,dt2) dt2[dt1,allow.cartesian=T],
        right=function(dt1,dt2) dt1[dt2,allow.cartesian=T],
        full =function(dt1,dt2) merge(dt1,dt2,all=T,allow.cartesian=T) ## calls merge.data.table()
    )),
    sqldf.unindexed=list(testFuncs=list( ## note: must pass connection=NULL to avoid running against the live DB connection, which would result in collisions with the residual tables from the last query upload
        inner=function(df1,df2,key) sqldf(paste0('select * from df1 inner join df2 using(',paste(collapse=',',key),')'),connection=NULL),
        left =function(df1,df2,key) sqldf(paste0('select * from df1 left join df2 using(',paste(collapse=',',key),')'),connection=NULL),
        right=function(df1,df2,key) sqldf(paste0('select * from df2 left join df1 using(',paste(collapse=',',key),')'),connection=NULL) ## can't do right join proper, not yet supported; inverted left join is equivalent
        ##full =function(df1,df2,key) sqldf(paste0('select * from df1 full join df2 using(',paste(collapse=',',key),')'),connection=NULL) ## can't do full join proper, not yet supported; possible to hack it with a union of left joins, but too unreasonable to include in testing
    )),
    sqldf.indexed=list(testFuncs=list( ## important: requires an active DB connection with preindexed main.df1 and main.df2 ready to go; arguments are actually ignored
        inner=function(df1,df2,key) sqldf(paste0('select * from main.df1 inner join main.df2 using(',paste(collapse=',',key),')')),
        left =function(df1,df2,key) sqldf(paste0('select * from main.df1 left join main.df2 using(',paste(collapse=',',key),')')),
        right=function(df1,df2,key) sqldf(paste0('select * from main.df2 left join main.df1 using(',paste(collapse=',',key),')')) ## can't do right join proper, not yet supported; inverted left join is equivalent
        ##full =function(df1,df2,key) sqldf(paste0('select * from main.df1 full join main.df2 using(',paste(collapse=',',key),')')) ## can't do full join proper, not yet supported; possible to hack it with a union of left joins, but too unreasonable to include in testing
    )),
    plyr=list(testFuncs=list(
        inner=function(df1,df2,key) join(df1,df2,key,'inner'),
        left =function(df1,df2,key) join(df1,df2,key,'left'),
        right=function(df1,df2,key) join(df1,df2,key,'right'),
        full =function(df1,df2,key) join(df1,df2,key,'full')
    )),
    dplyr=list(testFuncs=list(
        inner=function(df1,df2,key) inner_join(df1,df2,key),
        left =function(df1,df2,key) left_join(df1,df2,key),
        right=function(df1,df2,key) right_join(df1,df2,key),
        full =function(df1,df2,key) full_join(df1,df2,key)
    )),
    in.place=list(testFuncs=list(
        left =function(df1,df2,key) { cns <- setdiff(names(df2),key); df1[cns] <- df2[match(df1[,key],df2[,key]),cns]; df1; },
        right=function(df1,df2,key) { cns <- setdiff(names(df1),key); df2[cns] <- df1[match(df2[,key],df1[,key]),cns]; df2; }
    ))
);

getSolTypes <- function() names(solSpecs);
getJoinTypes <- function() unique(unlist(lapply(solSpecs,function(x) names(x$testFuncs))));
getArgSpec <- function(argSpecs,key=NULL) if (is.null(key)) argSpecs$default else argSpecs[[key]];

initSqldf <- function() {
    sqldf(); ## creates sqlite connection on first run, cleans up and closes existing connection otherwise
    if (exists('sqldfInitFlag',envir=globalenv(),inherits=F) && sqldfInitFlag) { ## false only on first run
        sqldf(); ## creates a new connection
    } else {
        assign('sqldfInitFlag',T,envir=globalenv()); ## set to true for the one and only time
    }; ## end if
    invisible();
}; ## end initSqldf()

setUpBenchmarkCall <- function(argSpecs,joinType,solTypes=getSolTypes(),env=parent.frame()) {
    ## builds and returns a list of expressions suitable for passing to the list argument of microbenchmark(), and assigns variables to resolve symbol references in those expressions
    callExpressions <- list();
    nms <- character();
    for (solType in solTypes) {
        testFunc <- solSpecs[[solType]]$testFuncs[[joinType]];
        if (is.null(testFunc)) next; ## this join type is not defined for this solution type
        testFuncName <- paste0('tf.',solType);
        assign(testFuncName,testFunc,envir=env);
        argSpecKey <- solSpecs[[solType]]$argSpec;
        argSpec <- getArgSpec(argSpecs,argSpecKey);
        argList <- setNames(nm=names(argSpec$args),vector('list',length(argSpec$args)));
        for (i in seq_along(argSpec$args)) {
            argName <- paste0('tfa.',argSpecKey,i);
            assign(argName,argSpec$args[[i]],envir=env);
            argList[[i]] <- if (i%in%argSpec$copySpec) call('copy',as.symbol(argName)) else as.symbol(argName);
        }; ## end for
        callExpressions[[length(callExpressions)+1L]] <- do.call(call,c(list(testFuncName),argList),quote=T);
        nms[length(nms)+1L] <- solType;
    }; ## end for
    names(callExpressions) <- nms;
    callExpressions;
}; ## end setUpBenchmarkCall()

harmonize <- function(res) {
    res <- as.data.frame(res); ## coerce to data.frame
    for (ci in which(sapply(res,is.factor))) res[[ci]] <- as.character(res[[ci]]); ## coerce factor columns to character
    for (ci in which(sapply(res,is.logical))) res[[ci]] <- as.integer(res[[ci]]); ## coerce logical columns to integer (works around sqldf quirk of munging logicals to integers)
    ##for (ci in which(sapply(res,inherits,'POSIXct'))) res[[ci]] <- as.double(res[[ci]]); ## coerce POSIXct columns to double (works around sqldf quirk of losing POSIXct class) ----- POSIXct doesn't work at all in sqldf.indexed
    res <- res[order(names(res))]; ## order columns
    res <- res[do.call(order,res),]; ## order rows
    res;
}; ## end harmonize()

checkIdentical <- function(argSpecs,solTypes=getSolTypes()) {
    for (joinType in getJoinTypes()) {
        callExpressions <- setUpBenchmarkCall(argSpecs,joinType,solTypes);
        if (length(callExpressions)<2L) next;
        ex <- harmonize(eval(callExpressions[[1L]]));
        for (i in seq(2L,len=length(callExpressions)-1L)) {
            y <- harmonize(eval(callExpressions[[i]]));
            if (!isTRUE(all.equal(ex,y,check.attributes=F))) {
                ex <<- ex;
                y <<- y;
                solType <- names(callExpressions)[i];
                stop(paste0('non-identical: ',solType,' ',joinType,'.'));
            }; ## end if
        }; ## end for
    }; ## end for
    invisible();
}; ## end checkIdentical()

testJoinType <- function(argSpecs,joinType,solTypes=getSolTypes(),metric=NULL,times=100L) {
    callExpressions <- setUpBenchmarkCall(argSpecs,joinType,solTypes);
    bm <- microbenchmark(list=callExpressions,times=times);
    if (is.null(metric)) return(bm);
    bm <- summary(bm);
    res <- setNames(nm=names(callExpressions),bm[[metric]]);
    attr(res,'unit') <- attr(bm,'unit');
    res;
}; ## end testJoinType()

testAllJoinTypes <- function(argSpecs,solTypes=getSolTypes(),metric=NULL,times=100L) {
    joinTypes <- getJoinTypes();
    resList <- setNames(nm=joinTypes,lapply(joinTypes,function(joinType) testJoinType(argSpecs,joinType,solTypes,metric,times)));
    if (is.null(metric)) return(resList);
    units <- unname(unlist(lapply(resList,attr,'unit')));
    res <- do.call(data.frame,c(list(join=joinTypes),setNames(nm=solTypes,rep(list(rep(NA_real_,length(joinTypes))),length(solTypes))),list(unit=units,stringsAsFactors=F)));
    for (i in seq_along(resList)) res[i,match(names(resList[[i]]),names(res))] <- resList[[i]];
    res;
}; ## end testAllJoinTypes()

testGrid <- function(makeArgSpecsFunc,sizes,overlaps,solTypes=getSolTypes(),joinTypes=getJoinTypes(),metric='median',times=100L) {

    res <- expand.grid(size=sizes,overlap=overlaps,joinType=joinTypes,stringsAsFactors=F);
    res[solTypes] <- NA_real_;
    res$unit <- NA_character_;
    for (ri in seq_len(nrow(res))) {

        size <- res$size[ri];
        overlap <- res$overlap[ri];
        joinType <- res$joinType[ri];

        argSpecs <- makeArgSpecsFunc(size,overlap);

        checkIdentical(argSpecs,solTypes);

        cur <- testJoinType(argSpecs,joinType,solTypes,metric,times);
        res[ri,match(names(cur),names(res))] <- cur;
        res$unit[ri] <- attr(cur,'unit');

    }; ## end for

    res;

}; ## end testGrid()

Ecco un benchmark dell'esempio basato sull'OP che ho dimostrato in precedenza:

## OP's example, supplemented with a non-matching row in df2
argSpecs <- list(
    default=list(copySpec=1:2,args=list(
        df1 <- data.frame(CustomerId=1:6,Product=c(rep('Toaster',3L),rep('Radio',3L))),
        df2 <- data.frame(CustomerId=c(2L,4L,6L,7L),State=c(rep('Alabama',2L),'Ohio','Texas')),
        'CustomerId'
    )),
    data.table.unkeyed=list(copySpec=1:2,args=list(
        as.data.table(df1),
        as.data.table(df2),
        'CustomerId'
    )),
    data.table.keyed=list(copySpec=1:2,args=list(
        setkey(as.data.table(df1),CustomerId),
        setkey(as.data.table(df2),CustomerId)
    ))
);
## prepare sqldf
initSqldf();
sqldf('create index df1_key on df1(CustomerId);'); ## upload and create an sqlite index on df1
sqldf('create index df2_key on df2(CustomerId);'); ## upload and create an sqlite index on df2

checkIdentical(argSpecs);

testAllJoinTypes(argSpecs,metric='median');
##    join    merge data.table.unkeyed data.table.keyed sqldf.unindexed sqldf.indexed      plyr    dplyr in.place         unit
## 1 inner  644.259           861.9345          923.516        9157.752      1580.390  959.2250 270.9190       NA microseconds
## 2  left  713.539           888.0205          910.045        8820.334      1529.714  968.4195 270.9185 224.3045 microseconds
## 3 right 1221.804           909.1900          923.944        8930.668      1533.135 1063.7860 269.8495 218.1035 microseconds
## 4  full 1302.203          3107.5380         3184.729              NA            NA 1593.6475 270.7055       NA microseconds

Qui faccio un benchmark su dati di input casuali, provando scale diverse e diversi schemi di sovrapposizione dei tasti tra le due tabelle di input. Questo benchmark è ancora limitato al caso di una chiave intera a colonna singola. Inoltre, per garantire che la soluzione sul posto funzionasse per i join sinistro e destro delle stesse tabelle, tutti i dati di test casuali utilizzano la 0..1:0..1cardinalità. Questo viene implementato campionando senza sostituire la colonna chiave del primo data.frame quando si genera la colonna chiave del secondo data.frame.

makeArgSpecs.singleIntegerKey.optionalOneToOne <- function(size,overlap) {

    com <- as.integer(size*overlap);

    argSpecs <- list(
        default=list(copySpec=1:2,args=list(
            df1 <- data.frame(id=sample(size),y1=rnorm(size),y2=rnorm(size)),
            df2 <- data.frame(id=sample(c(if (com>0L) sample(df1$id,com) else integer(),seq(size+1L,len=size-com))),y3=rnorm(size),y4=rnorm(size)),
            'id'
        )),
        data.table.unkeyed=list(copySpec=1:2,args=list(
            as.data.table(df1),
            as.data.table(df2),
            'id'
        )),
        data.table.keyed=list(copySpec=1:2,args=list(
            setkey(as.data.table(df1),id),
            setkey(as.data.table(df2),id)
        ))
    );
    ## prepare sqldf
    initSqldf();
    sqldf('create index df1_key on df1(id);'); ## upload and create an sqlite index on df1
    sqldf('create index df2_key on df2(id);'); ## upload and create an sqlite index on df2

    argSpecs;

}; ## end makeArgSpecs.singleIntegerKey.optionalOneToOne()

## cross of various input sizes and key overlaps
sizes <- c(1e1L,1e3L,1e6L);
overlaps <- c(0.99,0.5,0.01);
system.time({ res <- testGrid(makeArgSpecs.singleIntegerKey.optionalOneToOne,sizes,overlaps); });
##     user   system  elapsed
## 22024.65 12308.63 34493.19

Ho scritto del codice per creare grafici di log dei risultati sopra riportati. Ho generato un grafico separato per ogni percentuale di sovrapposizione. È un po 'ingombra, ma mi piace avere tutti i tipi di soluzione e tipi di join rappresentati nella stessa trama.

Ho usato l'interpolazione spline per mostrare una curva uniforme per ogni combinazione di soluzione / tipo di join, disegnata con singoli simboli pch. Il tipo di unione viene catturato dal simbolo pch, usando un punto per parentesi angolari interne, sinistra e destra per sinistra e destra e un diamante per intero. Il tipo di soluzione viene acquisito dal colore come mostrato nella legenda.

plotRes <- function(res,titleFunc,useFloor=F) {
    solTypes <- setdiff(names(res),c('size','overlap','joinType','unit')); ## derive from res
    normMult <- c(microseconds=1e-3,milliseconds=1); ## normalize to milliseconds
    joinTypes <- getJoinTypes();
    cols <- c(merge='purple',data.table.unkeyed='blue',data.table.keyed='#00DDDD',sqldf.unindexed='brown',sqldf.indexed='orange',plyr='red',dplyr='#00BB00',in.place='magenta');
    pchs <- list(inner=20L,left='<',right='>',full=23L);
    cexs <- c(inner=0.7,left=1,right=1,full=0.7);
    NP <- 60L;
    ord <- order(decreasing=T,colMeans(res[res$size==max(res$size),solTypes],na.rm=T));
    ymajors <- data.frame(y=c(1,1e3),label=c('1ms','1s'),stringsAsFactors=F);
    for (overlap in unique(res$overlap)) {
        x1 <- res[res$overlap==overlap,];
        x1[solTypes] <- x1[solTypes]*normMult[x1$unit]; x1$unit <- NULL;
        xlim <- c(1e1,max(x1$size));
        xticks <- 10^seq(log10(xlim[1L]),log10(xlim[2L]));
        ylim <- c(1e-1,10^((if (useFloor) floor else ceiling)(log10(max(x1[solTypes],na.rm=T))))); ## use floor() to zoom in a little more, only sqldf.unindexed will break above, but xpd=NA will keep it visible
        yticks <- 10^seq(log10(ylim[1L]),log10(ylim[2L]));
        yticks.minor <- rep(yticks[-length(yticks)],each=9L)*1:9;
        plot(NA,xlim=xlim,ylim=ylim,xaxs='i',yaxs='i',axes=F,xlab='size (rows)',ylab='time (ms)',log='xy');
        abline(v=xticks,col='lightgrey');
        abline(h=yticks.minor,col='lightgrey',lty=3L);
        abline(h=yticks,col='lightgrey');
        axis(1L,xticks,parse(text=sprintf('10^%d',as.integer(log10(xticks)))));
        axis(2L,yticks,parse(text=sprintf('10^%d',as.integer(log10(yticks)))),las=1L);
        axis(4L,ymajors$y,ymajors$label,las=1L,tick=F,cex.axis=0.7,hadj=0.5);
        for (joinType in rev(joinTypes)) { ## reverse to draw full first, since it's larger and would be more obtrusive if drawn last
            x2 <- x1[x1$joinType==joinType,];
            for (solType in solTypes) {
                if (any(!is.na(x2[[solType]]))) {
                    xy <- spline(x2$size,x2[[solType]],xout=10^(seq(log10(x2$size[1L]),log10(x2$size[nrow(x2)]),len=NP)));
                    points(xy$x,xy$y,pch=pchs[[joinType]],col=cols[solType],cex=cexs[joinType],xpd=NA);
                }; ## end if
            }; ## end for
        }; ## end for
        ## custom legend
        ## due to logarithmic skew, must do all distance calcs in inches, and convert to user coords afterward
        ## the bottom-left corner of the legend will be defined in normalized figure coords, although we can convert to inches immediately
        leg.cex <- 0.7;
        leg.x.in <- grconvertX(0.275,'nfc','in');
        leg.y.in <- grconvertY(0.6,'nfc','in');
        leg.x.user <- grconvertX(leg.x.in,'in');
        leg.y.user <- grconvertY(leg.y.in,'in');
        leg.outpad.w.in <- 0.1;
        leg.outpad.h.in <- 0.1;
        leg.midpad.w.in <- 0.1;
        leg.midpad.h.in <- 0.1;
        leg.sol.w.in <- max(strwidth(solTypes,'in',leg.cex));
        leg.sol.h.in <- max(strheight(solTypes,'in',leg.cex))*1.5; ## multiplication factor for greater line height
        leg.join.w.in <- max(strheight(joinTypes,'in',leg.cex))*1.5; ## ditto
        leg.join.h.in <- max(strwidth(joinTypes,'in',leg.cex));
        leg.main.w.in <- leg.join.w.in*length(joinTypes);
        leg.main.h.in <- leg.sol.h.in*length(solTypes);
        leg.x2.user <- grconvertX(leg.x.in+leg.outpad.w.in*2+leg.main.w.in+leg.midpad.w.in+leg.sol.w.in,'in');
        leg.y2.user <- grconvertY(leg.y.in+leg.outpad.h.in*2+leg.main.h.in+leg.midpad.h.in+leg.join.h.in,'in');
        leg.cols.x.user <- grconvertX(leg.x.in+leg.outpad.w.in+leg.join.w.in*(0.5+seq(0L,length(joinTypes)-1L)),'in');
        leg.lines.y.user <- grconvertY(leg.y.in+leg.outpad.h.in+leg.main.h.in-leg.sol.h.in*(0.5+seq(0L,length(solTypes)-1L)),'in');
        leg.sol.x.user <- grconvertX(leg.x.in+leg.outpad.w.in+leg.main.w.in+leg.midpad.w.in,'in');
        leg.join.y.user <- grconvertY(leg.y.in+leg.outpad.h.in+leg.main.h.in+leg.midpad.h.in,'in');
        rect(leg.x.user,leg.y.user,leg.x2.user,leg.y2.user,col='white');
        text(leg.sol.x.user,leg.lines.y.user,solTypes[ord],cex=leg.cex,pos=4L,offset=0);
        text(leg.cols.x.user,leg.join.y.user,joinTypes,cex=leg.cex,pos=4L,offset=0,srt=90); ## srt rotation applies *after* pos/offset positioning
        for (i in seq_along(joinTypes)) {
            joinType <- joinTypes[i];
            points(rep(leg.cols.x.user[i],length(solTypes)),ifelse(colSums(!is.na(x1[x1$joinType==joinType,solTypes[ord]]))==0L,NA,leg.lines.y.user),pch=pchs[[joinType]],col=cols[solTypes[ord]]);
        }; ## end for
        title(titleFunc(overlap));
        readline(sprintf('overlap %.02f',overlap));
    }; ## end for
}; ## end plotRes()

titleFunc <- function(overlap) sprintf('R merge solutions: single-column integer key, 0..1:0..1 cardinality, %d%% overlap',as.integer(overlap*100));
plotRes(res,titleFunc,T);

R-merge-benchmark-colonna singola-integer-key-opzionale-one-to-one-99

R-merge-benchmark-colonna singola-integer-key-opzionale-one-to-one-50

R-merge-benchmark-colonna singola-integer-key-opzionale-one-to-one-1


Ecco un secondo benchmark su larga scala che è più pesante, rispetto al numero e ai tipi di colonne chiave, nonché alla cardinalità. Per questo benchmark uso tre colonne chiave: un carattere, un intero e uno logico, senza restrizioni sulla cardinalità (ovvero, 0..*:0..*). (In generale non è consigliabile definire colonne chiave con valori doppi o complessi a causa di complicazioni del confronto in virgola mobile, e praticamente nessuno usa mai il tipo raw, tanto meno per le colonne chiave, quindi non ho incluso quei tipi nella chiave Inoltre, per motivi di informazione, inizialmente ho cercato di utilizzare quattro colonne chiave includendo una colonna chiave POSIXct, ma il tipo POSIXct non ha funzionato bene con la sqldf.indexedsoluzione per qualche motivo, probabilmente a causa di anomalie di confronto in virgola mobile, quindi rimosso.)

makeArgSpecs.assortedKey.optionalManyToMany <- function(size,overlap,uniquePct=75) {

    ## number of unique keys in df1
    u1Size <- as.integer(size*uniquePct/100);

    ## (roughly) divide u1Size into bases, so we can use expand.grid() to produce the required number of unique key values with repetitions within individual key columns
    ## use ceiling() to ensure we cover u1Size; will truncate afterward
    u1SizePerKeyColumn <- as.integer(ceiling(u1Size^(1/3)));

    ## generate the unique key values for df1
    keys1 <- expand.grid(stringsAsFactors=F,
        idCharacter=replicate(u1SizePerKeyColumn,paste(collapse='',sample(letters,sample(4:12,1L),T))),
        idInteger=sample(u1SizePerKeyColumn),
        idLogical=sample(c(F,T),u1SizePerKeyColumn,T)
        ##idPOSIXct=as.POSIXct('2016-01-01 00:00:00','UTC')+sample(u1SizePerKeyColumn)
    )[seq_len(u1Size),];

    ## rbind some repetitions of the unique keys; this will prepare one side of the many-to-many relationship
    ## also scramble the order afterward
    keys1 <- rbind(keys1,keys1[sample(nrow(keys1),size-u1Size,T),])[sample(size),];

    ## common and unilateral key counts
    com <- as.integer(size*overlap);
    uni <- size-com;

    ## generate some unilateral keys for df2 by synthesizing outside of the idInteger range of df1
    keys2 <- data.frame(stringsAsFactors=F,
        idCharacter=replicate(uni,paste(collapse='',sample(letters,sample(4:12,1L),T))),
        idInteger=u1SizePerKeyColumn+sample(uni),
        idLogical=sample(c(F,T),uni,T)
        ##idPOSIXct=as.POSIXct('2016-01-01 00:00:00','UTC')+u1SizePerKeyColumn+sample(uni)
    );

    ## rbind random keys from df1; this will complete the many-to-many relationship
    ## also scramble the order afterward
    keys2 <- rbind(keys2,keys1[sample(nrow(keys1),com,T),])[sample(size),];

    ##keyNames <- c('idCharacter','idInteger','idLogical','idPOSIXct');
    keyNames <- c('idCharacter','idInteger','idLogical');
    ## note: was going to use raw and complex type for two of the non-key columns, but data.table doesn't seem to fully support them
    argSpecs <- list(
        default=list(copySpec=1:2,args=list(
            df1 <- cbind(stringsAsFactors=F,keys1,y1=sample(c(F,T),size,T),y2=sample(size),y3=rnorm(size),y4=replicate(size,paste(collapse='',sample(letters,sample(4:12,1L),T)))),
            df2 <- cbind(stringsAsFactors=F,keys2,y5=sample(c(F,T),size,T),y6=sample(size),y7=rnorm(size),y8=replicate(size,paste(collapse='',sample(letters,sample(4:12,1L),T)))),
            keyNames
        )),
        data.table.unkeyed=list(copySpec=1:2,args=list(
            as.data.table(df1),
            as.data.table(df2),
            keyNames
        )),
        data.table.keyed=list(copySpec=1:2,args=list(
            setkeyv(as.data.table(df1),keyNames),
            setkeyv(as.data.table(df2),keyNames)
        ))
    );
    ## prepare sqldf
    initSqldf();
    sqldf(paste0('create index df1_key on df1(',paste(collapse=',',keyNames),');')); ## upload and create an sqlite index on df1
    sqldf(paste0('create index df2_key on df2(',paste(collapse=',',keyNames),');')); ## upload and create an sqlite index on df2

    argSpecs;

}; ## end makeArgSpecs.assortedKey.optionalManyToMany()

sizes <- c(1e1L,1e3L,1e5L); ## 1e5L instead of 1e6L to respect more heavy-duty inputs
overlaps <- c(0.99,0.5,0.01);
solTypes <- setdiff(getSolTypes(),'in.place');
system.time({ res <- testGrid(makeArgSpecs.assortedKey.optionalManyToMany,sizes,overlaps,solTypes); });
##     user   system  elapsed
## 38895.50   784.19 39745.53

I grafici risultanti, utilizzando lo stesso codice di stampa sopra indicato:

titleFunc <- function(overlap) sprintf('R merge solutions: character/integer/logical key, 0..*:0..* cardinality, %d%% overlap',as.integer(overlap*100));
plotRes(res,titleFunc,F);

R-merge-benchmark-assortiti-key-optional-molti-a-molti-99

R-merge-benchmark-assortiti-key-optional-molti-a-molti-50

R-merge-benchmark-assortiti-key-optional-molti-a-molti-1


analisi molto bella, ma è un peccato impostare la scala da 10 ^ 1 a 10 ^ 6, sono insiemi così piccoli che la differenza di velocità è quasi irrilevante. 10 ^ 6 a 10 ^ 8 sarebbe interessante da vedere!
jangorecki,

1
Ho anche notato che includi il timing della coercizione di classe nel benchmark che lo rende non valido per l'operazione di join.
jangorecki,

8
  1. Usando la mergefunzione possiamo selezionare la variabile della tabella di sinistra o della tabella di destra, allo stesso modo in cui tutti abbiamo familiarità con l'istruzione select in SQL (EX: Seleziona a. * ... o Seleziona b. * Da .....)
  2. Dobbiamo aggiungere un codice aggiuntivo che verrà inserito nella tabella appena unita.

    • SQL: - select a.* from df1 a inner join df2 b on a.CustomerId=b.CustomerId

    • R: - merge(df1, df2, by.x = "CustomerId", by.y = "CustomerId")[,names(df1)]

Stessa strada

  • SQL: - select b.* from df1 a inner join df2 b on a.CustomerId=b.CustomerId

  • R: - merge(df1, df2, by.x = "CustomerId", by.y = "CustomerId")[,names(df2)]


7

Per un join interno su tutte le colonne, è possibile utilizzare anche fintersectdal pacchetto data.table o intersectdal pacchetto dplyr in alternativa a mergesenza specificare le bycolonne. questo darà le righe uguali tra due frame di dati:

merge(df1, df2)
#   V1 V2
# 1  B  2
# 2  C  3
dplyr::intersect(df1, df2)
#   V1 V2
# 1  B  2
# 2  C  3
data.table::fintersect(setDT(df1), setDT(df2))
#    V1 V2
# 1:  B  2
# 2:  C  3

Dati di esempio:

df1 <- data.frame(V1 = LETTERS[1:4], V2 = 1:4)
df2 <- data.frame(V1 = LETTERS[2:3], V2 = 2:3)

5

Aggiorna join. Un altro importante join in stile SQL è un " aggiornamento join " in cui le colonne di una tabella vengono aggiornate (o create) utilizzando un'altra tabella.

Modifica delle tabelle di esempio dell'OP ...

sales = data.frame(
  CustomerId = c(1, 1, 1, 3, 4, 6), 
  Year = 2000:2005,
  Product = c(rep("Toaster", 3), rep("Radio", 3))
)
cust = data.frame(
  CustomerId = c(1, 1, 4, 6), 
  Year = c(2001L, 2002L, 2002L, 2002L),
  State = state.name[1:4]
)

sales
# CustomerId Year Product
#          1 2000 Toaster
#          1 2001 Toaster
#          1 2002 Toaster
#          3 2003   Radio
#          4 2004   Radio
#          6 2005   Radio

cust
# CustomerId Year    State
#          1 2001  Alabama
#          1 2002   Alaska
#          4 2002  Arizona
#          6 2002 Arkansas

Supponiamo di voler aggiungere lo stato del cliente custalla tabella degli acquisti sales, ignorando la colonna dell'anno. Con la base R, possiamo identificare le righe corrispondenti e quindi copiare i valori su:

sales$State <- cust$State[ match(sales$CustomerId, cust$CustomerId) ]

# CustomerId Year Product    State
#          1 2000 Toaster  Alabama
#          1 2001 Toaster  Alabama
#          1 2002 Toaster  Alabama
#          3 2003   Radio     <NA>
#          4 2004   Radio  Arizona
#          6 2005   Radio Arkansas

# cleanup for the next example
sales$State <- NULL

Come si può vedere qui, matchseleziona la prima riga corrispondente dalla tabella dei clienti.


Aggiorna join con più colonne.L'approccio sopra funziona bene quando ci uniamo a una sola colonna e siamo soddisfatti della prima partita. Supponiamo che vogliamo che l'anno di misurazione nella tabella dei clienti corrisponda all'anno di vendita.

Come menziona la risposta di @ bgoldst, matchcon interactionpotrebbe essere un'opzione per questo caso. Più semplicemente, si potrebbe usare data.table:

library(data.table)
setDT(sales); setDT(cust)

sales[, State := cust[sales, on=.(CustomerId, Year), x.State]]

#    CustomerId Year Product   State
# 1:          1 2000 Toaster    <NA>
# 2:          1 2001 Toaster Alabama
# 3:          1 2002 Toaster  Alaska
# 4:          3 2003   Radio    <NA>
# 5:          4 2004   Radio    <NA>
# 6:          6 2005   Radio    <NA>

# cleanup for next example
sales[, State := NULL]

Join di aggiornamento a rotazione. In alternativa, potremmo voler prendere l'ultimo stato in cui è stato trovato il cliente:

sales[, State := cust[sales, on=.(CustomerId, Year), roll=TRUE, x.State]]

#    CustomerId Year Product    State
# 1:          1 2000 Toaster     <NA>
# 2:          1 2001 Toaster  Alabama
# 3:          1 2002 Toaster   Alaska
# 4:          3 2003   Radio     <NA>
# 5:          4 2004   Radio  Arizona
# 6:          6 2005   Radio Arkansas

I tre esempi si concentrano soprattutto sulla creazione / aggiunta di una nuova colonna. Vedere le domande frequenti su R per un esempio di aggiornamento / modifica di una colonna esistente.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.