Одновременное объединение нескольких фреймов данных в списке

У меня есть список многих 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)
Другие вопросы по тегам