Одновременно слияние нескольких кадров данных в списке

У меня есть список многих data.frames, которые я хочу объединить. Проблема здесь в том, что каждый data.frame отличается по количеству строк и столбцов, но все они разделяют ключевые переменные (которые я назвал "var1" и "var2" в коде ниже). Если data.frames были идентичны в терминах столбцов, я мог бы просто rbind, для которого plyr rbind.fill выполнил бы эту работу, но это не так эти данные.

Поскольку команда merge работает только на 2 файлах данных, я обратился к Интернету за идеями. Я получил этот от здесь, который отлично работал в 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

(Кстати, я вижу другие ссылки на эту ошибку в другом месте без разрешения).

Есть ли способ решить это?

Ответ 1

Другой вопрос, заданный конкретно, как выполнить несколько левых объединений, используя 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

Base R merge() с Base 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

Ответ 2

Уменьшить делает это довольно легко:

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 будет работать нормально.

Ответ 4

Вы можете использовать рекурсию для этого. Я не проверял следующее, но он должен дать вам правильную идею:

MergeListOfDf = function( data , ... )
{
    if ( length( data ) == 2 ) 
    {
        return( merge( data[[ 1 ]] , data[[ 2 ]] , ... ) )
    }    
    return( merge( MergeListOfDf( data[ -1 ] , ... ) , data[[ 1 ]] , ... ) )
}

Ответ 5

Я буду повторно использовать пример данных из @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)

Ответ 6

Функция 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 из tidyselect и select (поскольку мы начинаем с .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 аргумент .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.

Ответ 7

Другие решения здесь хорошо подходят для небольших данных, но они рекурсивно создают и уничтожают множество переменных для этого. Чтобы избежать сложности N ^ 2, сделав что-то вроде

X = A
X = merge(X,B)
X = merge(X,C)
...
X = merge(X,Z)

можно использовать rbind. Вам просто нужно самостоятельно управлять именами столбцов, что является болью. Я не думаю, что есть хороший способ сделать это, кроме как выталкивать какой-то код, как показано ниже.

allnames <- unique(unlist(sapply(myBigDataframeList,names)))
for(i in 1:length(myBigDataframeList)){
  columnmap <- match(allnames,names(myBigDataframeList[[i]]))
  columnmap <- ifelse(is.na(columnmap),1,columnmap+1)
  myBigDataframeList[[i]] <- cbind(data.frame(dummycolumn=NA),myBigDataframeList[[i]])[,columnmap]
  names(myBigDataframeList[[i]]) <- allnames
}
myBiggerDataframe <- do.call(rbind,myBigDataframeList)

Единственное, на что нужно обратить внимание, это тип данных конечных столбцов. Все, что связано с NA, будет применено к правильному типу, но вам нужно будет управлять двумя столбцами с тем же именем, но с другим типом.

Ответ 8

У меня был список фреймов данных без общего столбца идентификатора.
У меня отсутствовали данные о многих 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 )

Ответ 9

В пакете purrr может быть доступно более новое решение. Для вашего точного вопроса вы можете использовать reduce() отметить малый r по сравнению с base::Reduce, но вы можете полностью устранить проблему, используя map_dfr() или map_dfc, что может помешать проблеме, сделав карту и уменьшите шаг в одном.