Як з'єднати (об'єднати) кадри даних (внутрішні, зовнішні, ліві, праві)


1231

Дано два кадри даних:

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

Як я можу створити стиль бази даних, тобто стиль sql ? Тобто, як я можу отримати:


Додатковий кредит:

Як я можу зробити оператор вибору стилю SQL?


4
stat545-ubc.github.io/bit001_dplyr-cheatsheet.html ← моя улюблена відповідь на це питання
ізоморфізми

Трансформація даних із шпаргалкою dplyr, створена та підтримується RStudio, також має приємну інфографіку про те, як приєднується робота в dplyr rstudio.com/resources/cheatsheets
Артур Іп

2
Якщо ви прийшли сюди, хочете дізнатися про об'єднання фреймів даних панд , цей ресурс можна знайти тут .
cs95

Відповіді:


1348

Використовуючи mergeфункцію та її необов'язкові параметри:

Внутрішнє з'єднання: merge(df1, df2) буде працювати для цих прикладів, оскільки R автоматично приєднується до кадрів загальними іменами змінних, але ви, швидше за все, хочете вказати,merge(df1, df2, by = "CustomerId")щоб переконатися, що ви відповідали лише бажаним полям. Ви також можете використовуватипараметриby.xта,by.yякщо відповідні змінні мають різні назви в різних кадрах даних.

Зовнішнє приєднання: merge(x = df1, y = df2, by = "CustomerId", all = TRUE)

Зліва зовні: merge(x = df1, y = df2, by = "CustomerId", all.x = TRUE)

Правий зовнішній: merge(x = df1, y = df2, by = "CustomerId", all.y = TRUE)

Перехресне з'єднання: merge(x = df1, y = df2, by = NULL)

Так само, як і при внутрішньому з'єднанні, ви, ймовірно, захочете явно передати "CustomerId" в R як відповідну змінну. Я думаю, що майже завжди найкраще чітко вказати ідентифікатори, на яких потрібно об'єднатись; безпечніше, якщо вхідні дані.кадри змінюються несподівано і простіше читати згодом.

Ви можете об'єднатись у кілька стовпців, надаючи byвектор, наприклад,by = c("CustomerId", "OrderId") .

Якщо імена стовпців, для яких слід об'єднатись, не є однаковими, ви можете вказати, наприклад, by.x = "CustomerId_in_df1", by.y = "CustomerId_in_df2"де CustomerId_in_df1ім'я стовпця в першому фреймі даних та CustomerId_in_df2ім'я стовпця у другому кадрі даних. (Це також можуть бути вектори, якщо вам потрібно об'єднатись у кілька стовпців.)


2
@MattParker Я використовував пакет sqldf для цілої низки складних запитів проти фреймів даних, йому справді потрібен був для самостійного з'єднання (тобто перехресне з'єднання data.frame), мені цікаво, як це порівнюється з точки зору продуктивності ... . ???
Ніколас Гамільтон

9
@ADP Я ніколи не використовував sqldf, тому не впевнений у швидкості. Якщо продуктивність є для вас головним питанням, ви також повинні заглянути в data.tableпакет - це цілий новий набір синтаксису приєднання, але це радикально швидше, ніж усе, про що ми говоримо тут.
Метт Паркер

5
З більшою чіткістю та поясненнями ..... mkmanu.wordpress.com/2016/04/08/…
Кумар

42
Невелике доповнення, яке було корисним для мене - Коли ви хочете об'єднатись, використовуючи більше одного стовпця:merge(x=df1,y=df2, by.x=c("x_col1","x_col2"), by.y=c("y_col1","y_col2"))
Dileep Kumar Patchigolla

8
Це працює data.tableзараз, та сама функція просто швидше.
марбель

222

Я рекомендую перевірити пакет sqldf Gabor Grothendieck , який дозволяє виразити ці операції в 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)")

Я вважаю, що синтаксис SQL є більш простим і природним, ніж його еквівалент R (але це може просто відображати моє зміщення RDBMS).

Щоб отримати докладнішу інформацію про приєднання, див. GitHub Gabor's sqldf .


198

Існує підхід даних.table для внутрішнього з'єднання, який є дуже ефективним для часу та пам’яті (і необхідний для деяких великих data.frames):

library(data.table)

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

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

mergeтакож працює над data.tables (як це є загальним та викликом merge.data.table)

merge(dt1, dt2)

data.table задокументовано на stackoverflow:
як зробити операцію злиття data.table
Переклад SQL приєднується за допомогою зовнішніх ключів до R синтаксису data.table
Ефективні альтернативи для об'єднання для великих data.frames R
Як зробити основний лівий зовнішній з’єднання з data.table в R?

Ще один варіант - це joinфункція, знайдена в plyr пакеті

library(plyr)

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

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

Параметри для type:inner , left, right, full.

Від ?join: На відміну від merge[ join] зберігає порядок x незалежно від того, який тип з'єднання використовується.


8
+1 для згадки plyr::join. Microbenchmarking вказує на те, що воно працює приблизно в 3 рази швидше, ніж merge.
Бістерфілд

20
Однак data.tableнабагато швидше, ніж обидва. Також існує велика підтримка в програмі SO, я не бачу багатьох авторів пакетів, які відповідають на запитання тут так часто, як data.tableписьменник чи автори.
марбель

1
Який data.tableсинтаксис для об'єднання списку кадрів даних ?
Олександр Блех

5
Зверніть увагу: dt1 [dt2] - це правильне зовнішнє з'єднання (не "чисте" внутрішнє з'єднання), тому ВСІ рядки з dt2 будуть частиною результату, навіть якщо у dt1 немає відповідного рядка. Вплив: у вас є потенційно небажані рядки, якщо у вас є ключові значення в dt2, які не відповідають ключовим значенням dt1.
R Yoda

8
@RYoda ви можете просто вказати nomatch = 0Lв цьому випадку.
Девід Аренбург

181

Ви також можете приєднатися, використовуючи дивовижний пакет dplyr 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)

Мутація приєднання: додайте стовпці до df1, використовуючи відповідність у 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)

Фільтрація приєднується: фільтруйте рядки в df1, не змінюйте стовпці

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
Чому вам потрібно перетворити CustomerIdна числовий? Я не бачу жодної згадки в документації (як для, так plyrі dplyrпро) такого типу обмежень. Чи працював би ваш код неправильно, якби стовпець злиття мав би characterтип (особливо цікавить plyr)? Я щось пропускаю?
Олександр Блех

Чи можна використовувати semi_join (df1, df2, df3, df4), щоб зберегти у df1 лише спостереження, що відповідають решті стовпців?
Ghose Bishwajit

@GhoseBishwajit Припустимо, що ви маєте на увазі решту кадрів даних замість стовпців, ви можете використовувати rbind на df2, df3 та df4, якщо вони мають однакову структуру, наприклад semi_join (df1, rbind (df2, df3, df4))
abhy3

Так, я мав на увазі рамки даних. Але вони не є такою ж структурою, як деякі відсутні в певних рядах. Для чотирьох фреймів даних я маю дані щодо чотирьох різних показників (ВВП, ВНП GINI, MMR) для різних країн. Я хочу приєднатися до фреймів даних таким чином, щоб утримувались лише ті країни, які були присутні за всіма чотирма показниками.
Ghose Bishwajit

86

У R Wiki є кілька хороших прикладів цього . Я вкраду пару тут:

Спосіб злиття

Оскільки ваші ключі названі однаково, короткий спосіб зробити внутрішнє з'єднання - merge ():

merge(df1,df2)

повне внутрішнє з'єднання (усі записи з обох таблиць) можна створити за допомогою ключового слова "всі":

merge(df1,df2, all=TRUE)

ліве зовнішнє з'єднання df1 і df2:

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

праве зовнішнє з'єднання df1 і df2:

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

ви можете перевернути їх, постукати їх і потерти, щоб отримати інші два зовнішніх з'єднання, про яких ви просили :)

Підписний метод

Ліве зовнішнє з'єднання з df1 зліва за допомогою методу індекса:

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

Інша комбінація зовнішніх з'єднань може бути створена заглушкою прикладу лівого зовнішнього приєднання. (так, я знаю, що це еквівалент сказання "Я залишу це як вправу для читача ...")


4
Посилання "R Wiki" розірвано.
zx8754

79

Новий 2014 рік:

Особливо, якщо ви також зацікавлені в маніпулюванні даними в цілому (включаючи сортування, фільтрування, підмножини, підбиття підсумків тощо), вам обов'язково слід ознайомитись із цим dplyr, який постачається з різноманітними функціями, усі призначені для полегшення вашої роботи спеціально з кадрами даних. та деякі інші типи баз даних. Він навіть пропонує досить складний інтерфейс SQL і навіть функцію перетворення (більшості) SQL-коду безпосередньо в Р.

Чотири функції, пов'язані з приєднанням у пакеті dplyr, є:

  • inner_join(x, y, by = NULL, copy = FALSE, ...): повертає всі рядки з x, де є відповідні значення у, і всі стовпці з x і y
  • left_join(x, y, by = NULL, copy = FALSE, ...): повертає всі рядки з x, а всі стовпці з x та y
  • semi_join(x, y, by = NULL, copy = FALSE, ...): повертає всі рядки з x, де є відповідні значення у, зберігаючи лише стовпці з x.
  • anti_join(x, y, by = NULL, copy = FALSE, ...): повертає всі рядки з x, де у y немає відповідних значень, зберігаючи лише стовпці з x

Це все тут в деталях.

Вибір стовпців може здійснити компанія select(df,"column"). Якщо цього вам недостатньо SQL-ish, то є sql()функція, в яку ви можете ввести SQL-код як є, і він буде виконувати операцію, яку ви вказали так само, як ви писали в R весь час (для отримання додаткової інформації, будь ласка, зверніться до до віньєтки dplyr / баз даних ). Наприклад, якщо застосовано правильно, sql("SELECT * FROM hflights")вибере всі стовпці із таблиці "hflight" dplyr ("tbl").


Безумовно, найкраще рішення з огляду на важливість пакету dplyr за останні два роки.
Марко Фумагаллі,

72

Оновлення методів таблиць data.table для приєднання до наборів даних. Дивіться нижче приклади для кожного типу приєднання. Існує два способи, один - [.data.tableколи передається другий таблиця даних в якості першого аргументу до підмножини, інший спосіб - використовувати mergeфункцію, яка передає швидкий метод data.table.

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

Нижче наведено базові тестові бази R, sqldf, dplyr та data.table.
Бенчмарк тестує безперебійні / недекларовані набори даних. Тест виконується на наборах даних 50M-1 рядків, у стовпчику приєднання є 50M-2 загальних значень, тому кожен сценарій (внутрішній, лівий, правий, повний) може бути протестований і приєднання досі не є тривіальним для виконання. Це тип з'єднання, який добре підкреслює алгоритми приєднання. Затримки є з 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

Будьте в курсі, що існують інші типи приєднань, які ви можете виконати, використовуючи data.table:
- оновлення при з’єднанні - якщо ви хочете шукати значення з іншої таблиці до основної таблиці
- агрегувати при з'єднанні - якщо ви хочете об'єднати за ключем, ви приєднуєтесь, у вас немає матеріалізувати все приєднатися результатами
- перекриваючи приєднатися - якщо ви хочете об'єднати діапазони
- прокат приєднатися - якщо ви хочете , щоб злиття бути в змозі відповідати значенням з попередніх / наступних рядків шляхом їх прокаток вперед або назад
- без оборудов приєднатися - якщо ваш умова приєднання неравна

Код для відтворення:

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

Чи варто додати приклад, який показує, як теж використовувати різні назви стовпців on = ?
SymbolixAU

1
@Symbolix ми можемо дочекатися випуску 1.9.8, оскільки це додасть не-екві приєднується до операторів onаргументу
jangorecki

Інша думка; чи варто додати зауваження, що при merge.data.tableцьому є sort = TRUEаргумент за замовчуванням , який додає ключ під час злиття і залишає його в результаті. Це те, на що слід спостерігати, особливо якщо ви намагаєтесь уникати налаштування ключів.
SymbolixAU

1
Я здивований, що ніхто не згадував, що більшість із них не працюють, якщо є дупи ...
статте

@statquant Ви можете зробити декартовий приєднання data.table, що ви маєте на увазі? Чи можете ви бути більш конкретними, будь ласка.
Девід Аренбург

32

dplyr з 0.4 впроваджував усі ці об'єднання, в тому числі outer_join, але варто зазначити, що перші кілька релізів до 0,4 він не пропонував outer_join, і в результаті було дуже багато поганого хакі-хай-кодового коду користувача, що плавав довгий час згодом (ви все ще можете знайти такий код у SO, відповіді Kaggle, github з того періоду. Отже, ця відповідь все ще служить корисною метою.)

Реєстрація пов'язаного основних моментів релізу :

v0.5 (6/2016)

  • Обробка для типу POSIXct, часових поясів, дублікатів, різних рівнів фактора. Кращі помилки та попередження.
  • Новий аргумент суфіксу для управління тим, що отримують повторювані імена змінних суфіксів (# 1296)

v0.4.0 (1/2015)

  • Реалізувати право з'єднання та зовнішнє з'єднання (# 96)
  • Приєднується мутація, яка додає нові змінні в одну таблицю з відповідних рядків в іншу. Приєднується фільтрування, яке фільтрує спостереження з однієї таблиці на основі того, чи відповідають вони спостереженням в іншій таблиці.

v0.3 (10/2014)

  • Тепер можна left_join за різними змінними у кожній таблиці: df1%>% left_join (df2, c ("var1" = "var2"))

v0.2 (5/2014)

  • * _join () більше не змінює назви стовпців (# 324)

v0.1.3 (4/2014)

Обхідні шляхи на думку коментарів Хадлі в цьому випуску:

  • right_join (x, y) - це те саме, що left_join (y, x) з точки зору рядків, просто стовпці будуть різними порядками. Легко обробляється з Select (new_column_order)
  • external_join в основному є об'єднанням (left_join (x, y), right_join (x, y)) - тобто зберігає всі рядки в обох кадрах даних.

1
@Gregor: ні, його не слід видаляти. Користувачам R важливо знати, що можливості приєднання відсутні протягом багатьох років, оскільки більша частина коду там містить обхідні шляхи або спеціальні вручні реалізації, або ad-hoce з векторами індексів, або, що ще гірше, уникає використання цих пакетів або операції взагалі. Щотижня я бачу такі питання на ТАК. Ми будемо скасовувати плутанину на довгі роки.
smci

@Gregor та інші, хто запитував: оновлено, підсумовуючи історичні зміни та те, чого бракувало протягом кількох років, коли це питання було задано. Це ілюструє, чому код з цього періоду був головним чином хакі, або уникав використання dplyr приєднується і відпадав назад на злиття. Якщо ви перевіряєте історичні бази коду на SO та Kaggle, ви все ще можете побачити затримку прийняття, і це призвело до серйозно заплутаного коду користувача. Повідомте мене, якщо вам все ще не вистачає цієї відповіді.
smci

@Gregor: Ті з нас, хто прийняв його в середині 2014 року, обрали не найкращий момент. (Я думав, що було раніше (0.0.x) випусків у 2013 році, але ні, моя помилка.) Незалежно, у 2015 році було ще багато лайнового коду, саме це мотивувало мене опублікувати це, я намагався демістифікувати сире я знайшов на Kaggle, github, SO.
smci

2
Так, я розумію, і я думаю, що ви добре зробите це. (Я теж був раннім усиновлювачем, і, хоча мені ще подобається dplyrсинтаксис, зміна lazyevalна rlangbackends порушила для мене купу коду, який змусив мене дізнатися більше data.table, і тепер я в основному використовую data.table.)
Грегор Томас,

@Gregor: цікаво, чи можете ви вказати мені на будь-які запитання (ваші чи чиїсь інші), які висвітлюють це? Здається, кожне наше прийняття plyr/ dplyr/ data.table/ tidyverse дуже залежить від того, з якого року ми розпочали, і в якому (ембріональному) стані пакети були тоді, на відміну від нині ...
smci

25

Приєднуючись до двох кадрів даних з ~ 1 млн рядків у кожному, один з 2 стовпцями, а другий з ~ 20, я на диво тоді виявився merge(..., all.x = TRUE, all.y = TRUE)швидшим dplyr::full_join(). Це з dplyr v0.4

Об'єднання займає ~ 17 секунд, full_join займає ~ 65 секунд.

Деякої їжі, хоча, оскільки я зазвичай замовчую dplyr для маніпуляційних завдань.


24

У випадку лівого з'єднання з 0..*:0..1кардинальністю або правого з'єднання з 0..1:0..*кардинальністю можна призначити на місці односторонні стовпчики з столяра ( 0..1таблиці) безпосередньо на приєднаний ( 0..*стіл), і тим самим уникнути створення абсолютно нова таблиця даних. Для цього потрібно зіставити стовпчики ключів від приєднаного до столяра та індексувати + упорядкувати рядки столяра відповідно до призначення.

Якщо ключ - це один стовпець, то ми можемо використовувати один виклик, match()щоб виконати відповідність. Це справа, яку я висвітлюю у цій відповіді.

Ось приклад на основі ОП, за винятком того, що я додав додатковий рядок до df2ідентифікатора 7, щоб перевірити випадок невідповідного ключа в столярі. Це фактично df1ліве приєднання 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

У вищесказаному я жорстко кодував припущення, що ключовий стовпець є першим стовпцем обох вхідних таблиць. Я б заперечував, що в цілому це не необгрунтоване припущення, оскільки, якщо у вас є data.frame з ключовим стовпцем, було б дивно, якби він не був створений як перший стовпчик data.frame від початок. І ви завжди можете впорядкувати стовпці, щоб зробити це так. Вигідним наслідком цього припущення є те, що ім'я стовпця ключів не повинно бути жорстко закодованим, хоча я припускаю, що це просто заміна одного припущення іншим. Конкретність - ще одна перевага цілочислової індексації, а також швидкість. У орієнтирах нижче я зміню реалізацію, щоб використовувати індексацію імен рядків, щоб відповідати конкуруючим реалізаціям.

Я думаю, що це особливо підходяще рішення, якщо у вас є кілька таблиць, які ви хочете залишити, щоб об'єднатися проти однієї великої таблиці. Повторна перебудова всієї таблиці для кожного злиття була б непотрібною та неефективною.

З іншого боку, якщо вам потрібно, щоб приєднаний користувач залишався незмінним через цю операцію з будь-якої причини, це рішення не можна використовувати, оскільки воно безпосередньо модифікує приєднаного. Хоча в такому випадку ви можете просто зробити копію та виконати призначення на місці копії.


В якості побічної ноти я коротко розглянув можливі відповідні рішення для клавіш з багато стовпчиків. На жаль, єдині відповідні рішення, які я знайшов, були:

  • неефективні конкатенації. наприклад match(interaction(df1$a,df1$b),interaction(df2$a,df2$b)), або та сама ідея з paste().
  • неефективні декартові сполучники, напр outer(df1$a,df2$a,`==`) & outer(df1$b,df2$b,`==`).
  • базові R merge()та еквівалентні функції злиття на основі пакетів, які завжди виділяють нову таблицю для повернення об'єднаного результату, і, таким чином, не підходять для рішення на основі призначення на місці.

Наприклад, див. « Збіг декількох стовпців на різних кадрах даних та отримання іншого стовпця в результаті» , відповідність двох стовпців з двома іншими стовпцями , « Збіг» у декількох стовпцях та відповідність цього питання, де я спочатку придумав рішення на місці, Поєднати два кадри даних з різною кількістю рядків в R .


Бенчмаркінг

Я вирішив зробити власний бенчмаркінг, щоб побачити, як підхід на місце призначення порівнюється з іншими рішеннями, запропонованими в цьому питанні.

Код тестування:

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

Ось орієнтир прикладу на основі ОП, який я демонстрував раніше:

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

Тут я орієнтуюсь на випадкові вхідні дані, намагаючись різні масштаби та різні шаблони перекриття ключів між двома вхідними таблицями. Цей орієнтир все ще обмежений випадком цілочислового ключа одного стовпця. Крім того, щоб гарантувати, що рішення на місці буде працювати як для лівого, так і для правого з'єднань одних і тих же таблиць, усі випадкові дані тесту використовують 0..1:0..1кардинальність. Це реалізується шляхом вибірки без заміни стовпця ключів першого data.frame при генерації стовпця ключів другого 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

Я написав деякий код, щоб створити графіки лог-журналу з наведених вище результатів. Я створив окремий графік для кожного відсотка перекриття. Це трохи захаращено, але мені подобається, що всі типи рішення та типи приєднання представлені в одному сюжеті.

Я використовував сплайновану інтерполяцію, щоб показати плавну криву для кожної комбінації рішення / з'єднання, намальовану окремими символами pch. Тип з'єднання фіксується символом pch, використовуючи крапку для внутрішніх, лівих та правих кутових дужок для лівої та правої та алмаз для повних. Тип розчину фіксується кольором, як показано в легенді.

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-злиття-орієнтир-один-стовпець-ціле число-ключ-необов’язково-один-до-99

R-злиття-орієнтир-один-стовпець-ціле число-ключ-необов’язково-один-до-50

R-злиття-орієнтир-один-стовпець-ціле число-ключ-необов’язково-один-до-1


Ось другий масштабний орієнтир, який більш важкий, що стосується кількості та типів ключових стовпців, а також простоти. Для цього еталону я використовую три ключові стовпці: один символ, одне ціле число та один логічний, без обмежень щодо кардинальності (тобто 0..*:0..*). (Взагалі не бажано визначати ключові стовпці з подвійними або складними значеннями через ускладнення порівняння з плаваючою комою, і в основному ніхто ніколи не використовує тип "необроблений", тим більше для ключових стовпців, тому я не включав ці типи в ключ Крім того, для інформації я спочатку намагався використовувати чотири стовпчики ключів, включивши стовпець ключа POSIXct, але тип POSIXct чомусь не грає з sqldf.indexedрішенням, можливо, через аномалії порівняння з плаваючою комою, тому я видалили його.)

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

Отримані графіки, використовуючи той самий код графіки, що наведений вище:

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-злиття-орієнтир-асорті-ключ-необов'язково-багато-багато-99

R-злиття-орієнтир-асорті-ключ-необов’язково-багато-до-багатьох-50

R-злиття-орієнтир-асорті-ключ-необов'язково-багато-до-багатьох-1


дуже приємний аналіз, але шкода, що ви встановлюєте шкалу від 10 ^ 1 до 10 ^ 6, це настільки крихітні набори, що різниця швидкостей майже не має значення. 10 ^ 6 до 10 ^ 8 було б цікаво подивитися!
jangorecki

1
Я також помітив, що ви включаєте терміни примусу до класу в еталоні, що робить його недійсним для роботи приєднання.
jangorecki

8
  1. За допомогою mergeфункції ми можемо вибрати змінну лівої таблиці або правої таблиці так само, як ми всі знайомі з оператором select у SQL (EX: Select a. * ... or Select b. * From .....)
  2. Ми повинні додати додатковий код, який буде підмножитися з щойно приєднаної таблиці.

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

Точно так же

  • 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

Для внутрішнього сполучення на всіх колонках, ви можете також використовувати fintersectвід data.table -package або intersectз dplyr -package в якості альтернативи mergeбез вказівки by-columns. це дасть рядки, рівні між двома фреймами даних:

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

Приклад даних:

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

5

Оновити приєднання. Ще одне важливе приєднання у стилі SQL - це " приєднання оновлення ", коли стовпці в одній таблиці оновлюються (або створюються) за допомогою іншої таблиці.

Зміна таблиць прикладів ОП

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

Припустимо, ми хочемо додати стан клієнта custдо таблиці закупівель sales, ігноруючи стовпець року. За допомогою бази R ми можемо визначити відповідні рядки та скопіювати значення у:

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

Як видно тут, matchвибирає перший відповідний рядок із таблиці клієнтів.


Оновлення приєднайте до кількох стовпців. Вищенаведений підхід працює добре, коли ми приєднуємось лише до однієї колонки і задоволені першим матчем. Припустимо, ми хочемо, щоб рік вимірювання в таблиці клієнтів відповідав року продажу.

Як зазначає відповідь @ bgoldst, в matchс interactionможе бути одним з варіантів для цього випадку. Простіше кажучи, можна використовувати 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]

Приєднуйтесь до оновленого оновлення. Крім того, ми можемо захотіти прийняти останній стан, у якому був знайдений клієнт:

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

Три приклади насамперед зосереджені на створенні / додаванні нового стовпця. Дивіться відповідні R-запитання для прикладу оновлення / зміни існуючого стовпця.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.