Jednoczesne scalanie wielu data.frames na liście


258

Mam listę wielu ramek data.frame, które chcę scalić. Problem polega na tym, że każda ramka data.frame różni się pod względem liczby wierszy i kolumn, ale wszystkie mają wspólne zmienne kluczowe (które wywołałem "var1"i "var2"w poniższym kodzie). Gdyby data.frames były identyczne pod względem kolumn, mógłbym tylko rbind, dla których rbind.fill plyr wykonałby zadanie, ale nie jest tak w przypadku tych danych.

Ponieważ mergepolecenie działa tylko na 2 ramkach danych., Szukałem pomysłów w Internecie. Dostałem ten stąd , który działał doskonale w wersji R 2.7.2, a miałem to wtedy:

merge.rec <- function(.list, ...){
    if(length(.list)==1) return(.list[[1]])
    Recall(c(list(merge(.list[[1]], .list[[2]], ...)), .list[-(1:2)]), ...)
}

Nazwałbym tę funkcję tak:

df <- merge.rec(my.list, by.x = c("var1", "var2"), 
                by.y = c("var1", "var2"), all = T, suffixes=c("", ""))

Ale w dowolnej wersji R po wersji 2.7.2, w tym 2.11 i 2.12, ten kod kończy się niepowodzeniem z następującym błędem:

Error in match.names(clabs, names(xi)) : 
  names do not match previous names

(Nawiasem mówiąc, widzę inne odniesienia do tego błędu gdzie indziej bez rozdzielczości).

Czy jest jakiś sposób na rozwiązanie tego?

Odpowiedzi:


182

Kolejne pytanie zadawane konkretnie sposobu wykonywania wielu lewej dołącza użyciu dplyr w R . Pytanie zostało oznaczone jako duplikat tego pytania, więc odpowiadam tutaj, używając 3 przykładowych ramek danych poniżej:

x <- data.frame(i = c("a","b","c"), j = 1:3, stringsAsFactors=FALSE)
y <- data.frame(i = c("b","c","d"), k = 4:6, stringsAsFactors=FALSE)
z <- data.frame(i = c("c","d","a"), l = 7:9, stringsAsFactors=FALSE)

Aktualizacja z czerwca 2018 r . : Podzieliłem odpowiedź na trzy sekcje reprezentujące trzy różne sposoby wykonania scalenia. Prawdopodobnie zechcesz użyć tej purrrmetody, jeśli już korzystasz z pakietów Tidyverse . Dla celów porównania poniżej znajdziesz podstawową wersję R przy użyciu tego samego przykładowego zestawu danych.


1) Dołącz je reducez purrrpakietu:

purrrPakiet zawiera reducefunkcję, która ma składnię zwięzły:

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

Możesz także wykonywać inne sprzężenia, takie jak a full_joinlub 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

2) dplyr::left_join()z podstawą 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

3) Baza R merge()z bazą R Reduce():

I dla celów porównawczych, tutaj jest podstawowa wersja R lewego złączenia

 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
Wariant full_join działa idealnie i wygląda o wiele mniej przerażająco niż zaakceptowana odpowiedź. Jednak niewielka różnica prędkości.
bshor,

1
@Axeman ma rację, ale możesz uniknąć (widocznego) zwracania listy ramek danych za pomocą map_dfr()lubmap_dfc()
DaveRGP

Myślałem, że mógłbym dołączyć do wielu DF opartych na wzorcu za pomocą ´ls (pattern = "DF_name_contains_this") ´, ale nie. Użyłem „noquote (paste (())”, ale wciąż produkuję wektor znaków zamiast listy DF. Skończyło się na wpisaniu imion, co jest wstrętne.
Pióro George'a Williama Russela

Inną kwestią zapewnia realizację Pythona : listę ramek danych pandy dfs = [df1, df2, df3]potem reduce(pandas.merge, dfs).
Paul Rougieux,

222

Zmniejszenie sprawia, że ​​jest to dość łatwe:

merged.data.frame = Reduce(function(...) merge(..., all=T), list.of.data.frames)

Oto w pełni przykład z wykorzystaniem niektórych próbnych danych:

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

A oto przykład wykorzystania tych danych do replikacji 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>

Uwaga: wygląda na to, że jest to prawdopodobnie błąd merge. Problem polega na tym, że nie ma możliwości sprawdzenia, czy dodanie sufiksów (w celu obsługi nakładających się niepasujących nazw) faktycznie czyni je unikalnymi. W pewnym momencie używa, [.data.framektóre robi make.unique nazwy, powodując rbindawarię.

# 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.

Najprostszym sposobem na naprawę jest pozostawienie zmiany nazwy pola dla duplikatów pól (których jest tutaj wiele) aż do merge. Na przykład:

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/ ReduceBędzie wtedy działać.


Dzięki! Widziałem to rozwiązanie również na łączu z Ramnath. Wygląda dość łatwo. Ale pojawia się następujący błąd: „Błąd w match.names (clabs, names (xi)): nazwy nie pasują do poprzednich nazw”. Zmienne, na których pasuję, są obecne we wszystkich ramkach danych na liście, więc nie rozumiem, co mówi mi ten błąd.
bshor

1
Przetestowałem to rozwiązanie na R2.7.2 i otrzymuję ten sam błąd match.names. Jest więc bardziej podstawowy problem z tym rozwiązaniem i moimi danymi. Użyłem kodu: Zmniejsz (funkcja (x, y) scal (x, y, all = T, by.x = match.by, by.y = match.by), my.list, akumulate = F)
bshor

1
Dziwne, dodałem kod, który przetestowałem, dzięki czemu działa dobrze. Wydaje mi się, że występuje zmiana nazwy pola na podstawie argumentów scalania, których używasz? Scalony wynik musi nadal mieć odpowiednie klucze, aby można go było scalić z kolejną ramką danych.
Charles,

Podejrzewam, że coś się dzieje z pustymi ramkami danych. Wypróbowałem kilka takich przykładów: empty <- data.frame(x=numeric(0),a=numeric(0); L3 <- c(empty,empty,list.of.data.frames,empty,empty,empty)zdarzyło się coś dziwnego, czego jeszcze nie odkryłem.
Ben Bolker

@Charles Jesteś na czymś. Twój kod działa dobrze dla mnie powyżej. A kiedy dostosowuję go do mojego, działa również dobrze - poza tym, że łączy się, ignorując kluczowe zmienne, które chcę. Kiedy próbuję dodać kluczowe zmienne zamiast je pomijać, pojawia się nowy błąd „Błąd w is.null (x): brakuje„ x ””. Wiersz kodu to „test.reduce <- Redukcja (funkcja (...) scalanie (przez = match.by, all = T), moja.lista)”, gdzie match.by to wektor nazw zmiennych kluczowych, które chcę scalić przez.
bshor,

52

Można to zrobić przy użyciu merge_allw reshapeopakowaniu. Możesz przekazać parametry mergeza pomocą ...argumentu

reshape::merge_all(list_of_dataframes, ...)

Oto doskonały zasób na temat różnych metod scalania ramek danych .


wygląda na to, że właśnie zreplikowałem merge_recurse =) Dobrze wiedzieć, że ta funkcja już istnieje.
SFun28

16
tak. ilekroć mam pomysł, zawsze sprawdzam, czy @hadley już to zrobił i przez większość czasu ma :-)
Ramnath 11.11.11

1
Jestem trochę zmieszany; powinienem zrobić merge_all lub merge_recurse? W każdym razie, gdy próbuję dodać moje dodatkowe argumenty do jednego z nich, pojawia się błąd „argument formalny” wszystko „dopasowany przez wiele rzeczywistych argumentów”.
bshor 11.11.11

2
Myślę, że upuściłem to z reshape2. Redukcja + scalanie jest równie prosta.
hadley,

2
@Ramnath, link nie działa, czy istnieje lustro?
Eduardo

4

Aby to zrobić, możesz użyć rekurencji. Nie zweryfikowałem następujących elementów, ale powinien dać ci właściwy pomysł:

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

2

Wykorzystam ponownie przykład danych z @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)

Oto krótkie i słodkie rozwiązanie przy użyciu purrritidyr

library(tidyverse)

 list(x, y, z) %>% 
  map_df(gather, key=key, value=value, -i) %>% 
  spread(key, value)

1

Funkcja eatmojego pakietu safejoin ma taką funkcję, jeśli podasz mu listę data.frames jako drugie wejście, to połączy je rekurencyjnie z pierwszym wejściem.

Pożyczanie i rozszerzanie danych akceptowanej odpowiedzi:

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

Nie musimy brać wszystkich kolumn, możemy użyć pomocników select z tidyselect i wybrać (kiedy zaczynamy od .xwszystkich .xkolumn są zachowane):

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

lub usuń określone:

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

Jeśli lista ma nazwę, nazwy będą używane jako przedrostki:

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

Jeśli występują konflikty kolumn, .conflictargument pozwala rozwiązać problem, na przykład biorąc pierwszą / drugą, dodając je, łącząc je lub zagnieżdżając.

trzymaj pierwszy:

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

zachowaj ostatnią:

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

Dodaj:

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

łączyć:

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

gniazdo:

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

NAwartości można zastąpić za pomocą .fillargumentu.

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

Domyślnie jest to udoskonalona left_joinale dplyr sprzężeń są obsługiwane przez .modeargumentu, rozmyte sprzężeń są również obsługiwane przez match_fun argumentu (jest owinięty wokół pakietu fuzzyjoin) lub dając wzór taki jak ~ X("var1") > Y("var2") & X("var3") < Y("var4")do byargumentu.


0

Miałem listę ramek danych bez wspólnej kolumny identyfikatora.
Brakowało mi danych na wielu systemach plików. Były wartości Null. Ramki danych zostały utworzone przy użyciu funkcji tabel. Reduce, Scalanie, rbind, rbind.fill i im podobne nie mogły mi pomóc w osiągnięciu celu. Moim celem było stworzenie zrozumiałej scalonej ramki danych, bez znaczenia dla brakujących danych i wspólnej kolumny identyfikatora.

Dlatego wykonałem następującą funkcję. Może ta funkcja może komuś pomóc.

##########################################################
####             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) )

podąża za funkcją

##########################################################
####             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

}

Uruchamianie przykładu

##########################################################
####             Running the example                 #####
##########################################################

rbind_null_df_lists ( list_of_df )

0

Jeśli masz listę plików dfs, a kolumna zawiera „identyfikator”, ale na niektórych listach brakuje niektórych identyfikatorów, możesz użyć tej wersji funkcji Reduce / Merge, aby dołączyć wiele plików Df brakujących identyfikatorów wierszy lub etykiet:

Reduce(function(x, y) merge(x=x, y=y, by="V1", all.x=T, all.y=T), list_of_dfs)

0

Oto ogólne opakowanie, którego można użyć do konwersji funkcji binarnej na funkcję wielu parametrów. Zaletą tego rozwiązania jest to, że jest ono bardzo ogólne i może być stosowane do dowolnych funkcji binarnych. Musisz to zrobić tylko raz, a następnie możesz zastosować go w dowolnym miejscu.

Aby zilustrować ten pomysł, używam prostej rekurencji do wdrożenia. Można go oczywiście wdrożyć w bardziej elegancki sposób, który korzysta z dobrego wsparcia R dla funkcjonalnego paradygmatu.

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]))
    })
})}

Następnie możesz po prostu zawinąć w nią dowolne funkcje binarne i wywołać z parametrami pozycyjnymi (zwykle data.frames) w pierwszych nawiasach i nazwanymi parametrami w drugich nawiasach (takich jak by =lub suffix =). Jeśli nie ma nazwanych parametrów, pozostaw puste nawiasy.

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)()
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.