Одновременное объединение нескольких фреймов данных в списке
У меня есть список многих data.frames, которые я хочу объединить. Проблема заключается в том, что каждый data.frame отличается количеством строк и столбцов, но все они имеют общие переменные (которые я назвал "var1"
а также "var2"
в коде ниже). Если бы data.frames были идентичны с точки зрения столбцов, я мог бы просто rbind
, для которого plyr rbind.fill сделает эту работу, но это не так с этими данными.
Поскольку merge
Команда работает только на 2 data.frames, я обратился в Интернет за идеями. Я получил вот этот, который отлично работал в R 2.7.2, что у меня было в то время:
merge.rec <- function(.list, ...){
if(length(.list)==1) return(.list[[1]])
Recall(c(list(merge(.list[[1]], .list[[2]], ...)), .list[-(1:2)]), ...)
}
И я бы назвал функцию так:
df <- merge.rec(my.list, by.x = c("var1", "var2"),
by.y = c("var1", "var2"), all = T, suffixes=c("", ""))
Но в любой версии R после 2.7.2, включая 2.11 и 2.12, этот код завершается ошибкой со следующей ошибкой:
Error in match.names(clabs, names(xi)) :
names do not match previous names
(Кстати, я вижу другие ссылки на эту ошибку в другом месте без разрешения).
Есть ли способ решить это?
6 ответов
Другой вопрос, заданный конкретно, как выполнить несколько левых объединений, используя dplyr в R. Этот вопрос был помечен как дубликат этого вопроса, поэтому я отвечаю здесь, используя 3 образца данных ниже:
library(dplyr)
x <- data_frame(i = c("a","b","c"), j = 1:3)
y <- data_frame(i = c("b","c","d"), k = 4:6)
z <- data_frame(i = c("c","d","a"), l = 7:9)
Обновление от июня 2018 года: я разделил ответ на три части, представляющие три различных способа выполнения слияния. Вы, вероятно, хотите использовать purrr
Кстати, если вы уже используете пакеты Tidyverse. Для сравнения ниже вы найдете базовую версию R, использующую тот же образец набора данных.
Присоединяйтесь к ним с reduce
от purrr
пакет
purrr
Пакет обеспечивает reduce
функция с кратким синтаксисом:
library(tidyverse)
list(x, y, z) %>% reduce(left_join, by = "i")
# A tibble: 3 x 4
# i j k l
# <chr> <int> <int> <int>
# 1 a 1 NA 9
# 2 b 2 4 NA
# 3 c 3 5 7
Вы также можете выполнять другие объединения, такие как full_join
или же inner_join
:
list(x, y, z) %>% reduce(full_join, by = "i")
# A tibble: 4 x 4
# i j k l
# <chr> <int> <int> <int>
# 1 a 1 NA 9
# 2 b 2 4 NA
# 3 c 3 5 7
# 4 d NA 6 8
list(x, y, z) %>% reduce(inner_join, by = "i")
# A tibble: 1 x 4
# i j k l
# <chr> <int> <int> <int>
# 1 c 3 5 7
dplyr::left_join()
с основанием R Reduce()
list(x,y,z) %>%
Reduce(function(dtf1,dtf2) left_join(dtf1,dtf2,by="i"), .)
# i j k l
# 1 a 1 NA 9
# 2 b 2 4 NA
# 3 c 3 5 7
База R merge()
с основанием R Reduce()
И для сравнения, вот базовая версия левого соединения R
Reduce(function(dtf1, dtf2) merge(dtf1, dtf2, by = "i", all.x = TRUE),
list(x,y,z))
# i j k l
# 1 a 1 NA 9
# 2 b 2 4 NA
# 3 c 3 5 7
Уменьшить делает это довольно легко:
merged.data.frame = Reduce(function(...) merge(..., all=T), list.of.data.frames)
Вот полный пример использования некоторых фиктивных данных:
set.seed(1)
list.of.data.frames = list(data.frame(x=1:10, a=1:10), data.frame(x=5:14, b=11:20), data.frame(x=sample(20, 10), y=runif(10)))
merged.data.frame = Reduce(function(...) merge(..., all=T), list.of.data.frames)
tail(merged.data.frame)
# x a b y
#12 12 NA 18 NA
#13 13 NA 19 NA
#14 14 NA 20 0.4976992
#15 15 NA NA 0.7176185
#16 16 NA NA 0.3841037
#17 19 NA NA 0.3800352
А вот пример использования этих данных для репликации my.list
:
merged.data.frame = Reduce(function(...) merge(..., by=match.by, all=T), my.list)
merged.data.frame[, 1:12]
# matchname party st district chamber senate1993 name.x v2.x v3.x v4.x senate1994 name.y
#1 ALGIERE 200 RI 026 S NA <NA> NA NA NA NA <NA>
#2 ALVES 100 RI 019 S NA <NA> NA NA NA NA <NA>
#3 BADEAU 100 RI 032 S NA <NA> NA NA NA NA <NA>
Примечание: похоже, это ошибка в merge
, Проблема в том, что нет проверки, что добавление суффиксов (для обработки перекрывающихся несовпадающих имен) фактически делает их уникальными. В определенный момент он использует [.data.frame
который делает make.unique
имена, в результате чего rbind
терпеть неудачу.
# first merge will end up with 'name.x' & 'name.y'
merge(my.list[[1]], my.list[[2]], by=match.by, all=T)
# [1] matchname party st district chamber senate1993 name.x
# [8] votes.year.x senate1994 name.y votes.year.y
#<0 rows> (or 0-length row.names)
# as there is no clash, we retain 'name.x' & 'name.y' and get 'name' again
merge(merge(my.list[[1]], my.list[[2]], by=match.by, all=T), my.list[[3]], by=match.by, all=T)
# [1] matchname party st district chamber senate1993 name.x
# [8] votes.year.x senate1994 name.y votes.year.y senate1995 name votes.year
#<0 rows> (or 0-length row.names)
# the next merge will fail as 'name' will get renamed to a pre-existing field.
Самый простой способ исправить это не оставлять переименование полей для полей дубликатов (которых здесь много) до merge
, Например:
my.list2 = Map(function(x, i) setNames(x, ifelse(names(x) %in% match.by,
names(x), sprintf('%s.%d', names(x), i))), my.list, seq_along(my.list))
merge
/Reduce
тогда будет работать нормально.
Вы можете сделать это с помощью merge_all
в reshape
пакет. Вы можете передать параметры merge
с использованием ...
аргумент
reshape::merge_all(list_of_dataframes, ...)
Вот отличный ресурс по различным методам объединения фреймов данных.
Функция eat
моего пакета safejoin имеет такую особенность, если вы дадите ему список data.frames в качестве второго входа, он рекурсивно присоединит их к первому входу.
Заимствование и распространение данных принятого ответа:
x <- data_frame(i = c("a","b","c"), j = 1:3)
y <- data_frame(i = c("b","c","d"), k = 4:6)
z <- data_frame(i = c("c","d","a"), l = 7:9)
z2 <- data_frame(i = c("a","b","c"), l = rep(100L,3),l2 = rep(100L,3)) # for later
# devtools::install_github("moodymudskipper/safejoin")
library(safejoin)
eat(x, list(y,z), .by = "i")
# # A tibble: 3 x 4
# i j k l
# <chr> <int> <int> <int>
# 1 a 1 NA 9
# 2 b 2 4 NA
# 3 c 3 5 7
Нам не нужно брать все столбцы, мы можем использовать select helpers из tidyselect и выбирать (как мы начинаем с .x
все .x
колонки сохраняются):
eat(x, list(y,z), starts_with("l") ,.by = "i")
# # A tibble: 3 x 3
# i j l
# <chr> <int> <int>
# 1 a 1 9
# 2 b 2 NA
# 3 c 3 7
или удалить конкретные:
eat(x, list(y,z), -starts_with("l") ,.by = "i")
# # A tibble: 3 x 3
# i j k
# <chr> <int> <int>
# 1 a 1 NA
# 2 b 2 4
# 3 c 3 5
Если список назван, имена будут использоваться в качестве префиксов:
eat(x, dplyr::lst(y,z), .by = "i")
# # A tibble: 3 x 4
# i j y_k z_l
# <chr> <int> <int> <int>
# 1 a 1 NA 9
# 2 b 2 4 NA
# 3 c 3 5 7
Если есть конфликты столбцов .conflict
Аргумент позволяет вам разрешить это, например, взяв первый / второй, добавив их, объединив их или вложив их.
держись первым:
eat(x, list(y, z, z2), .by = "i", .conflict = ~.x)
# # A tibble: 3 x 4
# i j k l
# <chr> <int> <int> <int>
# 1 a 1 NA 9
# 2 b 2 4 NA
# 3 c 3 5 7
держать в прошлом:
eat(x, list(y, z, z2), .by = "i", .conflict = ~.y)
# # A tibble: 3 x 4
# i j k l
# <chr> <int> <int> <dbl>
# 1 a 1 NA 100
# 2 b 2 4 100
# 3 c 3 5 100
добавлять:
eat(x, list(y, z, z2), .by = "i", .conflict = `+`)
# # A tibble: 3 x 4
# i j k l
# <chr> <int> <int> <dbl>
# 1 a 1 NA 109
# 2 b 2 4 NA
# 3 c 3 5 107
сливаться:
eat(x, list(y, z, z2), .by = "i", .conflict = dplyr::coalesce)
# # A tibble: 3 x 4
# i j k l
# <chr> <int> <int> <dbl>
# 1 a 1 NA 9
# 2 b 2 4 100
# 3 c 3 5 7
гнездо:
eat(x, list(y, z, z2), .by = "i", .conflict = ~tibble(first=.x, second=.y))
# # A tibble: 3 x 4
# i j k l$first $second
# <chr> <int> <int> <int> <int>
# 1 a 1 NA 9 100
# 2 b 2 4 NA 100
# 3 c 3 5 7 100
NA
значения могут быть заменены с помощью .fill
аргумент.
eat(x, list(y, z), .by = "i", .fill = 0)
# # A tibble: 3 x 4
# i j k l
# <chr> <int> <dbl> <dbl>
# 1 a 1 0 9
# 2 b 2 4 0
# 3 c 3 5 7
По умолчанию это расширенный left_join
но все соединения dplyr поддерживаются через .mode
аргумент, нечеткие объединения также поддерживаются через match_fun
аргумент (он обернут вокруг пакета fuzzyjoin
) или дать формулу, такую как ~ X("var1") > Y("var2") & X("var3") < Y("var4")
к by
аргумент.
Вы можете использовать рекурсию, чтобы сделать это. Я не проверял следующее, но это должно дать вам правильную идею:
MergeListOfDf = function( data , ... )
{
if ( length( data ) == 2 )
{
return( merge( data[[ 1 ]] , data[[ 2 ]] , ... ) )
}
return( merge( MergeListOfDf( data[ -1 ] , ... ) , data[[ 1 ]] , ... ) )
}
Я буду использовать пример данных из @PaulRougieux
x <- data_frame(i = c("a","b","c"), j = 1:3)
y <- data_frame(i = c("b","c","d"), k = 4:6)
z <- data_frame(i = c("c","d","a"), l = 7:9)
Вот короткое и сладкое решение с использованием purrr
а также tidyr
library(tidyverse)
list(x, y, z) %>%
map_df(gather, key=key, value=value, -i) %>%
spread(key, value)
У меня был список фреймов данных без общего столбца идентификаторов.
У меня отсутствовали данные о многих DFS. Были нулевые значения. Кадры данных были созданы с использованием табличной функции. Снижение, Слияние, rbind, rbind.fill и тому подобное не могли помочь мне в моей цели. Моей целью было создать понятный объединенный фрейм данных, не имеющий отношения к отсутствующим данным и общему столбцу идентификаторов.
Поэтому я сделал следующую функцию. Может быть, эта функция может кому-то помочь.
##########################################################
#### Dependencies #####
##########################################################
# Depends on Base R only
##########################################################
#### Example DF #####
##########################################################
# Example df
ex_df <- cbind(c( seq(1, 10, 1), rep("NA", 0), seq(1,10, 1) ),
c( seq(1, 7, 1), rep("NA", 3), seq(1, 12, 1) ),
c( seq(1, 3, 1), rep("NA", 7), seq(1, 5, 1), rep("NA", 5) ))
# Making colnames and rownames
colnames(ex_df) <- 1:dim(ex_df)[2]
rownames(ex_df) <- 1:dim(ex_df)[1]
# Making an unequal list of dfs,
# without a common id column
list_of_df <- apply(ex_df=="NA", 2, ( table) )
это следует за функцией
##########################################################
#### The function #####
##########################################################
# The function to rbind it
rbind_null_df_lists <- function ( list_of_dfs ) {
length_df <- do.call(rbind, (lapply( list_of_dfs, function(x) length(x))))
max_no <- max(length_df[,1])
max_df <- length_df[max(length_df),]
name_df <- names(length_df[length_df== max_no,][1])
names_list <- names(list_of_dfs[ name_df][[1]])
df_dfs <- list()
for (i in 1:max_no ) {
df_dfs[[i]] <- do.call(rbind, lapply(1:length(list_of_dfs), function(x) list_of_dfs[[x]][i]))
}
df_cbind <- do.call( cbind, df_dfs )
rownames( df_cbind ) <- rownames (length_df)
colnames( df_cbind ) <- names_list
df_cbind
}
Выполнение примера
##########################################################
#### Running the example #####
##########################################################
rbind_null_df_lists ( list_of_df )
Вот общая оболочка, которую можно использовать для преобразования двоичной функции в функцию с несколькими параметрами. Преимущество этого решения в том, что оно очень универсальное и может применяться к любым двоичным функциям. Вам просто нужно сделать это один раз, а затем вы сможете применить его где угодно.
Чтобы продемонстрировать идею, я использую простую рекурсию. Конечно, это может быть реализовано более элегантным способом, который выиграет от хорошей поддержки функциональной парадигмы в R.
fold_left <- function(f) {
return(function(...) {
args <- list(...)
return(function(...){
iter <- function(result,rest) {
if (length(rest) == 0) {
return(result)
} else {
return(iter(f(result, rest[[1]], ...), rest[-1]))
}
}
return(iter(args[[1]], args[-1]))
})
})}
Затем вы можете просто обернуть им любые двоичные функции и вызвать их с позиционными параметрами (обычно data.frames) в первых скобках и именованными параметрами во вторых скобках (например, by =
или suffix =
). Если именованных параметров нет, оставьте вторую круглую скобку пустой.
merge_all <- fold_left(merge)
merge_all(df1, df2, df3, df4, df5)(by.x = c("var1", "var2"), by.y = c("var1", "var2"))
left_join_all <- fold_left(left_join)
left_join_all(df1, df2, df3, df4, df5)(c("var1", "var2"))
left_join_all(df1, df2, df3, df4, df5)()
Если у вас есть список dfs, а столбец содержит "ID", но в некоторых списках некоторые идентификаторы отсутствуют, вы можете использовать эту версию Reduce / Merge для объединения нескольких Dfs с отсутствующими идентификаторами строк или метками:
Reduce(function(x, y) merge(x=x, y=y, by="V1", all.x=T, all.y=T), list_of_dfs)