Я хотел бы объединить полное соединение и обновление соединения с переменным количеством столбцов и не нашел ответа, охватывающего этот случай, в stackoverflow.
DT
моя исходная таблица:
key_col value_col_1 value_col_2
<char> <int> <int>
1: a 1 11
2: b 2 12
3: c 3 13
4: d 4 14
5: e 5 15
который необходимо обновить данными в updateDT
:
key_col value_col_1 value_col_2 value_col_3
<char> <int> <int> <int>
1: d 11 21 31
2: e 12 22 32
3: f 13 23 33
4: g 14 24 34
updateDT
может добавлять новые строки и столбцы (число варьируется) и всегда имеет приоритет над существующими данными в DT
.
Ожидаемый результат (resultDT
) для приведенного выше примера:
key_col value_col_1 value_col_2 value_col_3
<char> <int> <int> <int>
1: a 1 11 NA
2: b 2 12 NA
3: c 3 13 NA
4: d 11 21 31
5: e 12 22 32
6: f 13 23 33
7: g 14 24 34
Я могу добиться этого с помощью жестко закодированных имен столбцов, однако код повторяется, и я изо всех сил пытаюсь обобщить этот подход для обработки, например. третий или четвертый value_col
в updateDT
динамически:
library(data.table)
DT <- data.table(key_col = letters[1:5], value_col_1 = 1:5, value_col_2 = 11:15)
updateDT <- data.table(key_col = letters[4:7], value_col_1 = 11:14, value_col_2 = 21:24, value_col_3 = 31:34)
key_column <- "key_col"
value_columns <- setdiff(c(names(DT), names(updateDT)), key_column)
resultDT <- merge(DT, updateDT, by = "key_col", all = TRUE)
resultDT[, value_col_1 := fifelse(is.na(value_col_1.y), yes = value_col_1.x, no = value_col_1.y)]
resultDT[, value_col_2 := fifelse(is.na(value_col_2.y), yes = value_col_2.x, no = value_col_2.y)]
resultDT[, c("value_col_1.x", "value_col_1.y", "value_col_2.x", "value_col_2.y") := NULL]
print(resultDT)
Обновлено: принять ответ Исмиршерегала, поскольку он быстрее - Тест:
library(microbenchmark)
microbenchmark(
ismirsehregal = {
resultDT1 <- rbindlist(list(old = DT, new = updateDT), use.names = TRUE, fill = TRUE, idcol = "origin")[, key_count := .N, by = key_column]
resultDT1 <- resultDT1[key_count == 1L | key_count > 1L & origin == 'new'][, c("key_count", "origin") := NULL]
},
Wimpel = {
DT[updateDT, on=.(key_col), (value_columns) := mget(paste0("i.", value_columns))]
resultDT2 <- rbindlist(list(DT, updateDT[!key_col %in% DT$key_col,]), use.names = TRUE, fill = TRUE)
}, times = 10L)
Unit: milliseconds
expr min lq mean median uq max neval
ismirsehregal 1.2166 1.2345 1.33790 1.2933 1.3531 1.8147 10
Wimpel 3.5286 3.5989 3.85624 3.7680 4.0916 4.5984 10
2. Обновлено: пример updateDT, не содержащий всех столбцов DT:
library(data.table)
DT <- data.table(key_col = letters[1:5], value_col_1 = 1:5, value_col_2 = 11:15)
updateDT <- data.table(key_col = letters[4:7], value_col_2 = 11:14, value_col_3 = 21:24, value_col_4 = 31:34)
key_column <- "key_col"
# value_columns <- setdiff(c(names(DT), names(updateDT)), key_column) # Error: value for ‘i.value_col_1’ not found
value_columns <- setdiff(intersect(names(DT), names(updateDT)), key_column) # does not insert all data from updateDT
DT[updateDT, on=.(key_col), (value_columns) := mget(paste0("i.", value_columns))]
rbindlist(list(DT, updateDT[!key_col %in% DT$key_col,]), use.names = TRUE, fill = TRUE)
Кроме того, если вы действительно имеете в виду, что updateDT
всегда содержит все столбцы DT
(как в примере и принятом решении), возможно, это однострочный вариант: rbind(DT[!updateDT, on=.(key_col)], updateDT, use.names=TRUE, fill=TRUE)
.
Ответ @Tobo Wimpel не вставляет все данные updateDT
, когда, например. value_col_1
не существует в updateDT
. Пожалуйста, посмотрите мое редактирование. Тот же результат при установке origin == 'old'
в моем коде.
@ismirsehregal Ваша добавленная версия — это то, что у меня было для общего случая, и вы правы насчет ответа Wimpel. Исправление/адаптация: key_column <- "key_col"
, update_cols <- setdiff(names(updateDT), key_column)
, rbind(DT[updateDT, on=key_column, (update_cols) := mget(paste0("i.", update_cols))], updateDT[!DT, on=key_column], fill=TRUE)
. Это работает для различных столбцов в обеих таблицах (и делает имя ключевого столбца переменной), хотя, конечно, это все равно изменяет DT как побочный эффект. Итак, на самом деле три ответа. Было бы интересно понять, как они работают и почему.
Вы можете использовать rbindlist
, чтобы добиться этого:
library(data.table)
DT <- data.table(key_col = letters[1:5], value_col_1 = 1:5, value_col_2 = 11:15)
updateDT <- data.table(key_col = letters[4:7], value_col_1 = 11:14, value_col_2 = 21:24, value_col_3 = 31:34)
key_column <- "key_col"
value_columns <- setdiff(c(names(DT), names(updateDT)), key_column)
resultDT <- rbindlist(list(old = DT, new = updateDT), use.names = TRUE, fill = TRUE, idcol = "origin")[, key_count := .N, by = key_column]
resultDT <- resultDT[key_count == 1L | key_count > 1L & origin == 'new'][, c("key_count", "origin") := NULL]
print(resultDT)
По сути, это привязка строк новой таблицы к старой и удаление дубликатов в зависимости от источника.
Обновлено: чтобы избежать удаления данных, если в updateDT существуют не все столбцы DT, мы можем просто добавить их заранее:
library(data.table)
DT <- data.table(key_col = letters[1:5], value_col_1 = 1:5, value_col_2 = 11:15)
updateDT <- data.table(key_col = letters[4:7], value_col_2 = 11:14, value_col_3 = 21:24, value_col_4 = 31:34)
key_column <- "key_col"
value_columns <- setdiff(c(names(DT), names(updateDT)), key_column)
keep_columns <- c(key_column, setdiff(names(DT), names(updateDT)))
updateDT <- DT[, ..keep_columns][updateDT, on = key_column]
resultDT <- rbindlist(list(old = DT, new = updateDT), use.names = TRUE, fill = TRUE, idcol = "origin")[, key_count := .N, by = key_column]
resultDT <- resultDT[key_count == 1L | key_count > 1L & origin == 'new'][, c("key_count", "origin") := NULL]
print(resultDT)
Таким же образом мы можем использовать однострочник @Tobo:
library(data.table)
DT <- data.table(key_col = letters[1:5], value_col_1 = 1:5, value_col_2 = 11:15)
updateDT <- data.table(key_col = letters[4:7], value_col_2 = 11:14, value_col_3 = 21:24, value_col_4 = 31:34)
key_column <- "key_col"
keep_columns <- c(key_column, setdiff(names(DT), names(updateDT)))
updateDT <- DT[, ..keep_columns][updateDT, on = key_column]
rbind(DT[!updateDT, on = key_column], updateDT, use.names = TRUE, fill = TRUE)
Немного другой подход, при котором вы сначала обновляете значения для уже существующих значений key_col, а затем привязываете строки к новым значениям key_col.
# first update join new values for matching key_col-values
DT[updateDT, on=.(key_col), (value_columns) := mget(paste0("i.", value_columns))]
# then rowbind missing key_col-values
rbindlist(list(DT, updateDT[!key_col %in% DT$key_col,]), use.names = TRUE, fill = TRUE)
выходы в
key_col value_col_1 value_col_2 value_col_3
<char> <int> <int> <int>
1: a 1 11 NA
2: b 2 12 NA
3: c 3 13 NA
4: d 11 21 31
5: e 12 22 32
6: f 13 23 33
7: g 14 24 34
как бы вы изменили свой код, чтобы использовать переменную key_column <- "key_col"
вместо жесткого кодирования key_col
?
Я бы рассмотрел ответ @Wimpel, потому что: 1) он учитывает случай, когда не все столбцы
DT
находятся вupdateDT
(принятый ответ превратит значенияDT
в<NA>
в обновленных строках для этих столбцов) и 2) Я думаю, что это может быть гораздо более эффективно с точки зрения использования памяти для реальных данных, потому что оно обновляетDT
на месте, а затем складывает его только с отдельными строкамиupdateDT
, чтобы создать новый объект. Другой ответ создает очень большой новый объект (составляющий обе таблицы), а затем еще один, состоящий только из выбранных строк.