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

176

У меня есть список многих 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

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

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

Теги:
dataframe
list
merge
r-faq

7 ответов

67
Лучший ответ

Другой вопрос задал конкретно, как выполнять несколько левых объединений, используя dplyr в R. Вопрос был отмечен как дубликат этого, поэтому я отвечу здесь, используя три примера данных:

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() с базой 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
  • 1
    Вариант full_join отлично работает и выглядит намного менее страшно, чем принятый ответ. Не большая разница в скорости, хотя.
  • 1
    @Axeman прав, но вы можете вообще избежать (видимого) возврата списка фреймов данных, используя map_dfr() или map_dfc()
Показать ещё 2 комментария
202

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

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

  • 0
    Спасибо! Я видел это решение также по ссылке от Рамнатха. Выглядит достаточно просто. Но я получаю следующую ошибку: «Ошибка в match.names (clabs, names (xi)): имена не соответствуют предыдущим именам». Все переменные, по которым я сопоставляюсь, присутствуют во всех кадрах данных в списке, поэтому я не понимаю, о чем говорит эта ошибка.
  • 1
    Я тестировал это решение на R2.7.2 и получаю ту же ошибку match.names. Так что есть некоторые более фундаментальные проблемы с этим решением и моими данными. Я использовал код: Reduce (функция (x, y) merge (x, y, all = T, by.x = match.by, by.y = match.by), my.list, аккумулировать = F)
Показать ещё 9 комментариев
44

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

reshape::merge_all(list_of_dataframes, ...)

Вот отличный ресурс по различным методам для объединения кадров данных.

  • 0
    похоже, я только что повторил merge_recurse =) приятно знать, что эта функция уже существует.
  • 16
    да. всякий раз, когда у меня есть идея, я всегда проверяю, @hadley уже сделал это, и в большинстве случаев он это делает :-)
Показать ещё 6 комментариев
4

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

MergeListOfDf = function( data , ... )
{
    if ( length( data ) == 2 ) 
    {
        return( merge( data[[ 1 ]] , data[[ 2 ]] , ... ) )
    }    
    return( merge( MergeListOfDf( data[ -1 ] , ... ) , data[[ 1 ]] , ... ) )
}
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)
0

Другие решения здесь хорошо подходят для небольших данных, но они рекурсивно создают и уничтожают множество переменных для этого. Чтобы избежать сложности 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, будет применено к правильному типу, но вам нужно будет управлять двумя столбцами с тем же именем, но с другим типом.

-2

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

Ещё вопросы

Сообщество Overcoder
Наверх
Меню