Cara bergabung (menggabungkan) bingkai data (dalam, luar, kiri, kanan)

1233

Diberikan dua frame data:

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

Bagaimana saya bisa melakukan gaya database, yaitu, gaya sql, bergabung ? Yaitu, bagaimana saya mendapatkan:

  • Sebuah bergabung dalam dari df1dan df2:
    Kembali hanya baris di mana tabel kiri telah sesuai dengan kunci di tabel kanan.
  • Sebuah luar bergabung dari df1dan df2:
    Pengembalian semua baris dari kedua tabel, bergabung catatan dari kiri yang memiliki pencocokan kunci di tabel kanan.
  • Sebuah meninggalkan luar bergabung (atau hanya kiri bergabung) dari df1dan df2
    Kembali semua baris dari tabel kiri, dan setiap baris dengan pencocokan kunci dari tabel kanan.
  • Sebuah luar benar bergabung dari df1dan df2
    Kembali semua baris dari tabel kanan, dan setiap baris dengan pencocokan kunci dari tabel kiri.

Kredit tambahan:

Bagaimana saya bisa melakukan pernyataan pilih gaya SQL?

Dan Goldstein
sumber
4
stat545-ubc.github.io/bit001_dplyr-cheatsheet.html ← jawaban favorit saya untuk pertanyaan ini
isomorphismes
Transformasi Data dengan lembar contekan dplyr dibuat dan dikelola oleh RStudio juga memiliki infografik yang bagus tentang bagaimana bergabung bekerja di dplyr rstudio.com/resources/cheatsheets
Arthur Yip
2
Jika Anda datang ke sini sebagai gantinya ingin tahu tentang menggabungkan kerangka data panda , sumber daya itu dapat ditemukan di sini .
cs95

Jawaban:

1350

Dengan menggunakan mergefungsi dan parameter opsionalnya:

Gabungan dalam: merge(df1, df2) akan berfungsi untuk contoh-contoh ini karena R secara otomatis menggabungkan frame dengan nama variabel yang umum, tetapi Anda kemungkinan besar ingin menentukanmerge(df1, df2, by = "CustomerId")untuk memastikan bahwa Anda hanya mencocokkan pada bidang yang Anda inginkan. Anda juga dapat menggunakanby.xdanby.yparameter jika variabel yang cocok memiliki nama yang berbeda di bingkai data yang berbeda.

Gabung luar: merge(x = df1, y = df2, by = "CustomerId", all = TRUE)

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

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

Silang bergabung: merge(x = df1, y = df2, by = NULL)

Sama seperti dengan gabungan dalam, Anda mungkin ingin secara eksplisit meneruskan "CustomerId" ke R sebagai variabel yang cocok. Saya pikir itu hampir selalu terbaik untuk secara eksplisit menyatakan pengidentifikasi yang ingin Anda gabungkan; lebih aman jika frame data input berubah secara tak terduga dan lebih mudah dibaca nanti.

Anda dapat menggabungkan beberapa kolom dengan memberikan byvektor, mis by = c("CustomerId", "OrderId"). , .

Jika nama kolom untuk digabungkan tidak sama, Anda dapat menentukan, misalnya, di by.x = "CustomerId_in_df1", by.y = "CustomerId_in_df2"mana CustomerId_in_df1nama kolom dalam bingkai data pertama dan CustomerId_in_df2adalah nama kolom dalam bingkai data kedua. (Ini juga bisa menjadi vektor jika Anda perlu menggabungkan beberapa kolom.)

Matt Parker
sumber
2
@MattParker Saya telah menggunakan paket sqldf untuk seluruh host pertanyaan kompleks terhadap dataframe, benar-benar membutuhkannya untuk melakukan self-cross join (yaitu data.frame cross-join sendiri) Saya ingin tahu bagaimana cara membandingkannya dari perspektif kinerja ... . ???
Nicholas Hamilton
9
@ADP Saya tidak pernah benar-benar menggunakan sqldf, jadi saya tidak yakin tentang kecepatan. Jika kinerja adalah masalah utama bagi Anda, Anda juga harus melihat ke dalam data.tablepaket - itu adalah set baru gabungan sintaksis, tetapi secara radikal lebih cepat dari apa pun yang kita bicarakan di sini.
Matt Parker
5
Dengan lebih jelas dan jelas ..... mkmanu.wordpress.com/2016/04/08/...
Manoj Kumar
42
Tambahan kecil yang bermanfaat bagi saya - Ketika Anda ingin bergabung menggunakan lebih dari satu kolom:merge(x=df1,y=df2, by.x=c("x_col1","x_col2"), by.y=c("y_col1","y_col2"))
Dileep Kumar Patchigolla
8
Ini berfungsi data.tablesekarang, fungsi yang sama lebih cepat.
marbel
222

Saya akan merekomendasikan memeriksa paket sqldf Gabor Grothendieck , yang memungkinkan Anda untuk mengekspresikan operasi ini dalam 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)")

Saya menemukan sintaks SQL menjadi lebih sederhana dan lebih alami daripada yang setara dengan R (tapi ini mungkin hanya mencerminkan bias RDBMS saya).

Lihat GitHub sqldf Gabor untuk informasi lebih lanjut tentang bergabung.

medriscoll
sumber
198

Ada pendekatan data.table untuk gabungan internal, yang sangat efisien waktu dan memori (dan diperlukan untuk beberapa kerangka data lebih besar):

library(data.table)

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

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

mergejuga berfungsi pada data.tables (karena generik dan panggilan merge.data.table)

merge(dt1, dt2)

data.table didokumentasikan di stackoverflow:
Bagaimana melakukan operasi gabungan data.table
Penerjemahan SQL bergabung pada kunci asing untuk R data.table sintaks
alternatif Efisien untuk menggabungkan untuk lebih besar data.frames R
Bagaimana melakukan kiri luar dasar bergabung dengan data.table dalam R?

Namun opsi lain adalah joinfungsi yang ditemukan dalam paket plyr

library(plyr)

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

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

Pilihan untuk type: inner, left, right, full.

Dari ?join: Berbeda merge, [ join] mempertahankan urutan x tidak peduli apa pun tipe join yang digunakan.

Etienne Low-Décarie
sumber
8
+1 untuk disebutkan plyr::join. Microbenchmarking menunjukkan, bahwa kinerjanya sekitar 3 kali lebih cepat daripada merge.
Beasterfield
20
Namun, data.tablejauh lebih cepat dari keduanya. Ada juga dukungan besar di SO, saya tidak melihat banyak penulis paket menjawab pertanyaan di sini sesering data.tablepenulis atau kontributor.
marbel
1
Apa data.tablesintaksis untuk menggabungkan daftar bingkai data ?
Aleksandr Blekh
5
Harap dicatat: dt1 [dt2] adalah gabungan luar kanan (bukan gabungan batin "murni") sehingga SEMUA baris dari dt2 akan menjadi bagian dari hasil bahkan jika tidak ada baris yang cocok di dt1. Dampak: Hasil Anda berpotensi baris yang tidak diinginkan jika Anda memiliki nilai kunci dalam dt2 yang tidak cocok dengan nilai kunci dt1.
R Yoda
8
@RYoda Anda bisa menentukan nomatch = 0Ldalam hal ini.
David Arenburg
181

Anda juga dapat bergabung menggunakan paket dplyr Hadley Wickham yang mengagumkan .

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)

Mutating joins: tambahkan kolom ke df1 menggunakan kecocokan di 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)

Penyaringan bergabung: menyaring baris di df1, jangan memodifikasi kolom

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.
Andrew Barr
sumber
16
Mengapa Anda perlu mengonversi CustomerIdke angka? Saya tidak melihat disebutkan dalam dokumentasi (untuk keduanya plyrdan dplyr) tentang jenis pembatasan ini. Apakah kode Anda berfungsi secara salah, jika kolom gabungan characterbertipe (terutama tertarik plyr)? Apakah saya melewatkan sesuatu?
Aleksandr Blekh
Bisakah seseorang menggunakan semi_join (df1, df2, df3, df4) untuk menyimpan hanya pengamatan di df1 yang cocok dengan sisa kolom?
Ghose Bishwajit
@GhoseBishwajit Dengan asumsi Anda berarti sisa dari kerangka data, bukan kolom, Anda dapat menggunakan rbind pada df2, df3 dan df4 jika mereka memiliki struktur yang sama misalnya semi_join (df1, rbind (df2, df3, df4))
abhy3
Ya saya maksudkan dataframe. Tetapi mereka bukan struktur yang sama seperti beberapa hilang pada baris tertentu. Untuk empat kerangka data, saya memiliki data pada empat indikator berbeda (PDB, GNP GINI, MMR) untuk sejumlah negara yang berbeda. Saya ingin bergabung dengan kerangka data dengan cara yang hanya membuat negara-negara tersebut hadir untuk keempat indikator.
Ghose Bishwajit
86

Ada beberapa contoh bagus untuk melakukan hal ini di R Wiki . Saya akan mencuri pasangan di sini:

Metode Penggabungan

Karena kunci Anda dinamai sama, cara singkat untuk melakukan gabungan dalam adalah menggabungkan ():

merge(df1,df2)

gabungan bagian dalam penuh (semua catatan dari kedua tabel) dapat dibuat dengan kata kunci "semua":

merge(df1,df2, all=TRUE)

gabungan luar kiri dari df1 dan df2:

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

gabungan luar kanan dari df1 dan df2:

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

Anda dapat membalik mereka, menampar mereka dan menggosok mereka ke bawah untuk mendapatkan dua bergabung luar lainnya yang Anda tanyakan :)

Metode Subskrip

Gabung luar kiri dengan df1 di kiri menggunakan metode subskrip adalah:

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

Kombinasi lain dari gabungan luar dapat dibuat dengan mungling contoh bergabung luar kiri. (Ya, saya tahu itu setara dengan mengatakan "Saya akan meninggalkannya sebagai latihan untuk pembaca ...")

JD Long
sumber
4
Tautan "R Wiki" rusak.
zx8754
79

Baru di 2014:

Terutama jika Anda juga tertarik pada manipulasi data secara umum (termasuk menyortir, memfilter, mengatur ulang, meringkas dll.), Anda harus memperhatikan dplyr , yang dilengkapi dengan berbagai fungsi yang semuanya dirancang untuk memfasilitasi pekerjaan Anda secara khusus dengan bingkai data dan beberapa tipe database lain. Bahkan menawarkan antarmuka SQL yang cukup rumit, dan bahkan fungsi untuk mengubah (sebagian besar) kode SQL langsung menjadi R.

Keempat fungsi yang terkait dengan bergabung dalam paket dplyr adalah (mengutip):

  • inner_join(x, y, by = NULL, copy = FALSE, ...): kembalikan semua baris dari x di mana ada nilai yang cocok di y, dan semua kolom dari x dan y
  • left_join(x, y, by = NULL, copy = FALSE, ...): kembalikan semua baris dari x, dan semua kolom dari x dan y
  • semi_join(x, y, by = NULL, copy = FALSE, ...): kembalikan semua baris dari x di mana ada nilai yang cocok di y, menjaga kolom saja dari x.
  • anti_join(x, y, by = NULL, copy = FALSE, ...): kembalikan semua baris dari x di mana tidak ada nilai yang cocok di y, menjaga hanya kolom dari x

Semuanya ada di sini dengan sangat rinci.

Memilih kolom dapat dilakukan oleh select(df,"column"). Jika itu tidak cukup untuk Anda, maka ada sql()fungsinya, di mana Anda dapat memasukkan kode SQL apa adanya, dan itu akan melakukan operasi yang Anda tentukan seperti yang Anda tulis di R selama ini (untuk informasi lebih lanjut, silakan lihat ke sketsa dplyr / database ). Misalnya, jika diterapkan dengan benar, sql("SELECT * FROM hflights")akan memilih semua kolom dari tabel dplyr "hflights" (a "tbl").

Mayor
sumber
Solusi pasti terbaik mengingat pentingnya paket dplyr telah diperoleh selama dua tahun terakhir.
Marco Fumagalli
72

Perbarui metode data.tabel untuk menggabungkan kumpulan data. Lihat contoh di bawah ini untuk setiap jenis bergabung. Ada dua metode, satu dari [.data.tableketika melewati data.table kedua sebagai argumen pertama untuk subset, cara lain adalah dengan menggunakan mergefungsi yang dikirim ke metode data.table cepat.

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

Di bawah ini adalah tes benchmark base R, sqldf, dplyr dan data.table.
Benchmark menguji set data yang tidak dikunci / tidak terindeks. Benchmark dilakukan pada dataset 50M-1 rows, terdapat 50M-2 nilai umum pada kolom join sehingga setiap skenario (inner, kiri, kanan, penuh) dapat diuji dan bergabung masih belum sepele untuk dilakukan. Ini adalah tipe join yang menekankan algoritma join. Timing adalah sebagai dari 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

Waspadai ada beberapa jenis gabungan yang dapat Anda lakukan menggunakan data.table:
- perbarui saat bergabung - jika Anda ingin mencari nilai dari tabel lain ke tabel utama Anda
- agregat saat bergabung - jika Anda ingin agregat pada kunci yang Anda bergabung, Anda tidak memiliki untuk mewujudkan semua bergabung hasil
- tumpang tindih bergabung - jika Anda ingin menggabungkan dengan rentang
- bergulir bergabung - jika Anda ingin bergabung untuk bisa cocok dengan nilai-nilai dari preceeding / berikut baris dengan menggulung mereka maju atau mundur
- non-equi bergabung - jika Anda kondisi bergabung tidak setara

Kode untuk direproduksi:

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
jangorecki
sumber
Apakah perlu menambahkan contoh yang menunjukkan cara menggunakan nama kolom yang berbeda di on = juga?
SymbolixAU
1
@ Symbolix kita bisa menunggu rilis 1.9.8 karena akan menambahkan operator non-equi bergabung ke onarg
jangorecki
Pikiran lain; apakah perlu menambahkan catatan bahwa dengan merge.data.tableada sort = TRUEargumen default , yang menambahkan kunci selama penggabungan dan membiarkannya ada di hasilnya. Ini adalah sesuatu yang harus diwaspadai, terutama jika Anda mencoba menghindari pengaturan tombol.
SymbolixAU
1
Saya tidak ada terkejut disebutkan bahwa sebagian besar dari mereka tidak bekerja jika ada dups ...
statquant
@statquant Anda dapat bergabung dengan Cartesian data.table, apa maksud Anda? Bisakah Anda lebih spesifik?
David Arenburg
32

dplyr sejak 0,4 menerapkan semua bergabung termasuk outer_join, tetapi perlu dicatat bahwa untuk beberapa rilis pertama sebelum 0,4 digunakan untuk tidak menawarkan outer_join, dan sebagai akibatnya ada banyak kode pengguna solusi peretasan yang sangat buruk melayang-layang di sekitar untuk sementara waktu sesudahnya (Anda masih dapat menemukan kode seperti itu di SO, Kaggle menjawab, github dari periode itu. Karenanya jawaban ini masih memiliki tujuan yang bermanfaat.)

Sorotan rilis terkait-bergabung :

v0.5 (6/2016)

  • Penanganan untuk tipe POSIXct, zona waktu, duplikat, level faktor yang berbeda. Kesalahan dan peringatan yang lebih baik.
  • Argumen sufiks baru untuk mengontrol apa yang diterima nama variabel duplikat sufiks (# 1296)

v0.4.0 (1/2015)

  • Terapkan gabung kanan dan gabung luar (# 96)
  • Mutating joins, yang menambahkan variabel baru ke satu tabel dari mencocokkan baris di yang lain. Penyaringan bergabung, yang memfilter pengamatan dari satu tabel berdasarkan pada apakah mereka cocok atau tidak dengan pengamatan di tabel lain.

v0.3 (10/2014)

  • Sekarang dapat left_join oleh variabel yang berbeda di setiap tabel: df1%>% left_join (df2, c ("var1" = "var2"))

v0.2 (5/2014)

  • * _join () tidak lagi mengurutkan kembali nama kolom (# 324)

v0.1.3 (4/2014)

Solusi per komentar Hadley dalam masalah itu:

  • right_join (x, y) adalah sama dengan left_join (y, x) dalam hal baris, hanya kolom akan urutan berbeda. Mudah dikerjakan dengan select (new_column_order)
  • outer_join pada dasarnya adalah gabungan (left_join (x, y), right_join (x, y)) - yaitu mempertahankan semua baris di kedua frame data.
smci
sumber
1
@ Gregor: tidak seharusnya tidak dihapus. Penting bagi pengguna R untuk mengetahui bahwa kemampuan bergabung tidak ada selama bertahun-tahun, karena sebagian besar kode di luar sana berisi solusi atau penerapan manual ad-hoc, atau ad-hocery dengan vektor indeks, atau lebih buruk lagi menghindari menggunakan paket-paket ini atau operasi sama sekali. Setiap minggu saya melihat pertanyaan seperti itu di SO. Kami akan membatalkan kebingungan untuk tahun-tahun mendatang.
smci
@ Gregor dan lainnya yang bertanya: diperbarui, meringkas perubahan historis dan apa yang hilang selama beberapa tahun ketika pertanyaan ini diajukan. Ini mengilustrasikan mengapa kode dari periode itu sebagian besar diretas, atau dihindari menggunakan dplyr bergabung dan kembali bergabung. Jika Anda memeriksa basis kode historis pada SO dan Kaggle, Anda masih dapat melihat penundaan adopsi dan kode pengguna yang benar-benar membingungkan ini. Beri tahu saya jika Anda masih menemukan jawaban ini kurang.
smci
@ Gregor: Kita yang mengadopsinya pertengahan 2014 tidak memilih momen terbaik. (Saya pikir ada rilis sebelumnya (0.0.x) sekitar tahun 2013, tapi tidak, kesalahan saya.) Terlepas dari itu, masih ada banyak kode omong kosong di tahun 2015, itulah yang memotivasi saya untuk memposting ini, saya mencoba untuk menghilangkan mitos crud yang saya temukan di Kaggle, github, SO.
smci
2
Ya, saya mengerti, dan saya pikir Anda melakukan pekerjaan dengan baik. (Saya adalah pengadopsi awal juga, dan sementara saya masih menyukai dplyrsintaks, perubahan dari lazyevalke rlangbackend memecahkan banyak kode untuk saya, yang mendorong saya untuk belajar lebih banyak data.table, dan sekarang saya lebih banyak menggunakan data.table.)
Gregor Thomas
@ Gregor: menarik, dapatkah Anda mengarahkan saya ke T&J apa pun (milik Anda atau orang lain) yang membahas hal itu? Tampaknya setiap adopsi kami terhadap plyr/ dplyr/ data.table/ tidyverse sangat tergantung pada tahun mana kami memulai, dan bagaimana (embrionik) keadaan paket saat itu, yang bertentangan dengan sekarang ...
smci
25

Dalam menggabungkan dua frame data dengan ~ 1 juta baris masing-masing, satu dengan 2 kolom dan yang lainnya dengan ~ 20, saya secara mengejutkan ternyata merge(..., all.x = TRUE, all.y = TRUE)lebih cepat dplyr::full_join(). Ini dengan dplyr v0.4

Penggabungan membutuhkan ~ 17 detik, full_join membutuhkan ~ 65 detik.

Beberapa makanan untuk, karena saya biasanya default untuk dplyr untuk tugas manipulasi.

BradP
sumber
24

Untuk kasus gabungan kiri dengan 0..*:0..1kardinalitas atau gabungan kanan dengan 0..1:0..*kardinalitas, dimungkinkan untuk menetapkan kolom unilateral dari joiner ( 0..1tabel) langsung ke joinee ( 0..*tabel), dan dengan demikian menghindari penciptaan tabel data yang sama sekali baru. Ini membutuhkan pencocokan kolom kunci dari joinee ke dalam joiner dan pengindeksan + memesan baris joiner yang sesuai untuk penugasan.

Jika kuncinya adalah satu kolom, maka kita dapat menggunakan satu panggilan match()untuk melakukan pencocokan. Inilah yang akan saya bahas dalam jawaban ini.

Berikut ini adalah contoh berdasarkan OP, kecuali saya telah menambahkan baris tambahan df2dengan id dari 7 untuk menguji kasus kunci yang tidak cocok di dalam joiner. Ini secara efektif df1dibiarkan bergabung 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

Pada contoh di atas saya membuat asumsi bahwa kolom kunci adalah kolom pertama dari kedua tabel input. Saya berpendapat bahwa, secara umum, ini bukan asumsi yang tidak masuk akal, karena, jika Anda memiliki data.frame dengan kolom kunci, akan aneh jika tidak diatur sebagai kolom pertama dari data.frame dari permulaan. Dan Anda selalu dapat menyusun ulang kolom untuk membuatnya jadi. Konsekuensi yang menguntungkan dari asumsi ini adalah bahwa nama kolom kunci tidak harus dikodekan secara keras, meskipun saya kira itu hanya mengganti satu asumsi dengan yang lain. Concision adalah keuntungan lain dari pengindeksan bilangan bulat, serta kecepatan. Dalam tolok ukur di bawah ini saya akan mengubah implementasi untuk menggunakan pengindeksan nama string agar sesuai dengan implementasi yang bersaing.

Saya pikir ini adalah solusi yang sangat tepat jika Anda memiliki beberapa tabel yang ingin Anda gabung dengan satu meja besar. Membangun kembali seluruh tabel secara berulang untuk setiap penggabungan akan menjadi tidak perlu dan tidak efisien.

Di sisi lain, jika Anda memerlukan joinee untuk tetap tidak berubah melalui operasi ini untuk alasan apa pun, maka solusi ini tidak dapat digunakan, karena ia memodifikasi joinee secara langsung. Meskipun dalam hal ini Anda bisa membuat salinan dan melakukan tugas di tempat pada salinan.


Sebagai catatan, saya secara singkat melihat kemungkinan solusi yang cocok untuk kunci multicolumn. Sayangnya, satu-satunya solusi yang cocok yang saya temukan adalah:

  • rangkaian tidak efisien. misalnya match(interaction(df1$a,df1$b),interaction(df2$a,df2$b)), atau ide yang sama dengan paste().
  • konjungsi kartesius yang tidak efisien, misalnya outer(df1$a,df2$a,`==`) & outer(df1$b,df2$b,`==`).
  • basis R merge()dan fungsi gabungan berbasis paket yang setara, yang selalu mengalokasikan tabel baru untuk mengembalikan hasil yang digabungkan, dan karenanya tidak cocok untuk solusi berbasis penugasan di tempat.

Misalnya, lihat Mencocokkan beberapa kolom pada bingkai data yang berbeda dan mendapatkan kolom lainnya sebagai hasilnya , cocokkan dua kolom dengan dua kolom lainnya , Cocokkan pada beberapa kolom , dan dupe dari pertanyaan ini di mana saya awalnya menemukan solusi di tempat, Gabungkan dua frame data dengan nomor yang berbeda dari baris dalam R .


Benchmarking

Saya memutuskan untuk melakukan tolok ukur sendiri untuk melihat bagaimana pendekatan penugasan di tempat dibandingkan dengan solusi lain yang telah ditawarkan dalam pertanyaan ini.

Kode pengujian:

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()

Inilah patokan contoh berdasarkan OP yang saya tunjukkan sebelumnya:

## 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

Di sini saya membandingkan data input acak, mencoba skala yang berbeda dan pola tumpang tindih kunci yang berbeda antara dua tabel input. Penghitungan ini masih terbatas pada kasus kunci integer satu kolom. Selain itu, untuk memastikan bahwa solusi di tempat akan bekerja untuk gabungan kiri dan kanan dari tabel yang sama, semua data uji acak menggunakan 0..1:0..1kardinalitas. Ini diimplementasikan dengan pengambilan sampel tanpa mengganti kolom kunci dari data.frame pertama saat membuat kolom kunci dari data.frame kedua.

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

Saya menulis beberapa kode untuk membuat plot log-log dari hasil di atas. Saya membuat plot terpisah untuk setiap persentase yang tumpang tindih. Agak berantakan, tapi saya suka semua tipe solusi dan tipe join diwakili dalam plot yang sama.

Saya menggunakan interpolasi spline untuk menunjukkan kurva halus untuk setiap kombinasi solusi / tipe gabungan, yang digambar dengan simbol pch individu. Jenis gabungan ditangkap oleh simbol pch, menggunakan titik untuk kurung sudut dalam, kiri dan kanan untuk kiri dan kanan, dan berlian untuk penuh. Jenis solusi ditangkap oleh warna seperti yang ditunjukkan dalam 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-gabungkan-patok-tunggal-kolom-integer-kunci-opsional-satu-ke-satu-99

R-gabungkan-patok-tunggal-kolom-integer-kunci-opsional-satu-ke-satu-50

R-gabungkan-patok-tunggal-kolom-integer-kunci-opsional-satu-ke-satu-1


Inilah patokan skala besar kedua yang lebih berat, berkenaan dengan jumlah dan jenis kolom utama, serta kardinalitas. Untuk tolok ukur ini saya menggunakan tiga kolom utama: satu karakter, satu integer, dan satu logis, tanpa batasan kardinalitas (yaitu, 0..*:0..*). (Secara umum tidak disarankan untuk mendefinisikan kolom kunci dengan nilai ganda atau kompleks karena komplikasi perbandingan floating-point, dan pada dasarnya tidak ada yang pernah menggunakan jenis mentah, apalagi untuk kolom kunci, jadi saya belum memasukkan jenis-jenis itu dalam kunci Juga, demi informasi, saya awalnya mencoba menggunakan empat kolom kunci dengan memasukkan kolom kunci POSIXct, tetapi tipe POSIXct tidak cocok dengan sqldf.indexedsolusi untuk beberapa alasan, mungkin karena anomali perbandingan floating-point, jadi saya dihapus.)

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

Plot yang dihasilkan, menggunakan kode plot yang sama seperti yang diberikan di atas:

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-gabungkan-tolok-ukur-aneka-kunci-opsional-banyak-ke-banyak-99

R-gabungkan-tolok-ukur-aneka-kunci-opsional-banyak-ke-banyak-50

R-gabungkan-tolok-ukur-aneka-kunci-opsional-banyak-ke-banyak-1

bgoldst
sumber
analisis yang sangat bagus, tetapi sangat disayangkan Anda mengatur skala dari 10 ^ 1 hingga 10 ^ 6, itu adalah perangkat yang sangat kecil sehingga perbedaan kecepatan hampir tidak relevan. 10 ^ 6 hingga 10 ^ 8 akan menarik untuk dilihat!
jangorecki
1
Saya juga melihat Anda memasukkan waktu pemaksaan kelas dalam patokan yang membuatnya tidak valid untuk bergabung dengan operasi.
jangorecki
8
  1. Menggunakan merge fungsi kita dapat memilih variabel tabel kiri atau tabel kanan, dengan cara yang sama seperti yang kita semua kenal dengan pernyataan pilih dalam SQL (EX: Select a. * ... atau Select b. From .....)
  2. Kita harus menambahkan kode tambahan yang akan di subset dari tabel yang baru bergabung.

    • 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)]

Cara yang sama

  • 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)]

sanjeeb
sumber
7

Untuk gabung bagian dalam pada semua kolom, Anda juga dapat menggunakan fintersectdari data.tabel -paket atau intersectdari dplyr -paket sebagai alternatif mergetanpa menentukan by-kolom. ini akan memberikan baris yang sama antara dua kerangka data:

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

Contoh data:

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

Perbarui bergabung. Satu gabungan gaya SQL penting lainnya adalah " pembaruan bergabung " di mana kolom dalam satu tabel diperbarui (atau dibuat) menggunakan tabel lain.

Mengubah tabel contoh 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

Misalkan kita ingin menambahkan keadaan pelanggan dari custke tabel pembelian sales,, mengabaikan kolom tahun. Dengan basis R, kami dapat mengidentifikasi baris yang cocok dan kemudian menyalin nilai lebih dari:

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

Seperti yang bisa dilihat di sini, matchpilih baris pertama yang cocok dari tabel pelanggan.


Perbarui bergabung dengan banyak kolom.Pendekatan di atas bekerja dengan baik ketika kita bergabung hanya pada satu kolom dan puas dengan pertandingan pertama. Misalkan kita ingin tahun pengukuran di tabel pelanggan agar sesuai dengan tahun penjualan.

Sebagai jawaban @ bgoldst menyebutkan, matchdengan interactionmungkin menjadi opsi untuk kasus ini. Lebih mudahnya, seseorang dapat menggunakan data.tabel:

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]

Bergulir pembaruan bergabung. Sebagai alternatif, kami mungkin ingin mengambil status terakhir tempat pelanggan ditemukan:

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

Tiga contoh di atas semuanya berfokus pada membuat / menambahkan kolom baru. Lihat FAQ R terkait untuk contoh memperbarui / memodifikasi kolom yang ada.

Frank
sumber