Combinar simultáneamente múltiples data.frames en una lista

259

Tengo una lista de muchos data.frames que quiero fusionar. El problema aquí es que cada data.frame difiere en términos del número de filas y columnas, pero todos comparten las variables clave (que he llamado "var1"y "var2"en el código a continuación). Si los data.frames fueran idénticos en términos de columnas, podría simplemente rbind, para lo cual rbind.fill de plyr haría el trabajo, pero ese no es el caso con estos datos.

Debido a que el mergecomando solo funciona en 2 data.frames, recurrí a Internet para obtener ideas. Obtuve este de aquí , que funcionó perfectamente en R 2.7.2, que es lo que tenía en ese momento:

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

Y llamaría a la función así:

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

Pero en cualquier versión R posterior a 2.7.2, incluidas 2.11 y 2.12, este código falla con el siguiente error:

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

(Incidentalmente, veo otras referencias a este error en otra parte sin resolución).

¿Hay alguna forma de resolver esto?

bshor
fuente

Respuestas:

183

Otra pregunta específicamente cómo realizar múltiples izquierda se une mediante dplyr en I . La pregunta se marcó como un duplicado de esta, así que respondo aquí, usando los 3 marcos de datos de muestra a continuación:

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)

Actualización de junio de 2018 : dividí la respuesta en tres secciones que representan tres formas diferentes de realizar la fusión. Probablemente quiera usar el purrrcamino si ya está usando los paquetes tidyverse . Para fines de comparación a continuación, encontrará una versión base R que utiliza el mismo conjunto de datos de muestra.


1) Únete a ellos reducedesde el purrrpaquete:

El purrrpaquete proporciona una reducefunción que tiene una sintaxis concisa:

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

También puede realizar otras uniones, como full_joinao 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()con base 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) Base R merge()con base R Reduce():

Y para fines de comparación, aquí hay una versión base R de la combinación izquierda

 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
Paul Rougieux
fuente
1
La variante full_join funciona perfectamente y se ve mucho menos aterradora que la respuesta aceptada. Sin embargo, no hay mucha diferencia de velocidad.
bshor
1
@Axeman tiene razón, pero es posible que pueda evitar (visiblemente) devolver una lista de marcos de datos utilizando map_dfr()omap_dfc()
DaveRGP
Pensé que podría unirme a varios DF basados ​​en un patrón usando ´ls (pattern = "DF_name_contains_this") ´, pero no. Usé ´noquote (paste (()) ´, pero todavía estoy produciendo un vector de caracteres en lugar de una lista de DF. Terminé escribiendo los nombres, lo cual es desagradable.
El bolígrafo de George William Russel el
Otra pregunta proporciona una implementación de Python : lista de marcos de datos de pandas dfs = [df1, df2, df3]entonces reduce(pandas.merge, dfs).
Paul Rougieux
222

Reducir hace esto bastante fácil:

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

Aquí hay un ejemplo completo usando algunos datos simulados:

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

Y aquí hay un ejemplo usando estos datos para replicar 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>

Nota: Parece que esto podría decirse que es un error merge. El problema es que no hay verificación de que agregar los sufijos (para manejar nombres superpuestos no coincidentes) los haga únicos. En un momento determinado se utiliza [.data.framela cual hace make.unique los nombres, haciendo que la rbindfalle.

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

La forma más fácil de solucionarlo es no dejar el campo renombrando campos duplicados (de los cuales hay muchos aquí) merge. P.ej:

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

El merge/ Reduceentonces funcionará bien.

Charles
fuente
¡Gracias! Vi esta solución también en el enlace de Ramnath. Parece lo suficientemente fácil. Pero me sale el siguiente error: "Error en match.names (clabs, names (xi)): los nombres no coinciden con los nombres anteriores". Las variables con las que coincido están presentes en todos los marcos de datos de la lista, por lo que no entiendo lo que me dice este error.
bshor
1
Probé esta solución en R2.7.2 y obtengo el mismo error de match.names. Entonces, hay un problema más fundamental con esta solución y mis datos. Usé el código: Reduce (function (x, y) merge (x, y, all = T, by.x = match.by, by.y = match.by), my.list, acumular = F)
bshor
1
Extraño, agregué el código con el que lo probé que funciona bien. ¿Supongo que se produce un cambio de nombre de campo en función de los argumentos de fusión que está utilizando? El resultado combinado aún debe tener las claves relevantes para fusionarse con el marco de datos posterior.
Charles
Sospecho que algo sucede con marcos de datos vacíos. Probé algunos ejemplos como este: empty <- data.frame(x=numeric(0),a=numeric(0); L3 <- c(empty,empty,list.of.data.frames,empty,empty,empty)y sucedieron algunas cosas extrañas que aún no he descubierto.
Ben Bolker
@ Charles Estás en algo. Tu código funciona bien arriba para mí. Y cuando lo adapto al mío, también funciona bien, excepto que se fusiona ignorando las variables clave que quiero. Cuando intento agregar variables clave en lugar de omitirlas, aparece un nuevo error "Error en is.null (x): 'x' no se encuentra". La línea de código es "test.reduce <- Reduce (function (...) merge (by = match.by, all = T), my.list)" donde match.by es el vector de los nombres de variables clave que quiero fusionar por.
bshor
52

Puedes hacerlo usando merge_allel reshapepaquete. Puede pasar parámetros para mergeusar el ...argumento

reshape::merge_all(list_of_dataframes, ...)

Aquí hay un excelente recurso sobre diferentes métodos para fusionar marcos de datos .

Ramnath
fuente
parece que acabo de replicar merge_recurse =) bueno saber que esta función ya existe.
SFun28
16
si. cada vez que tengo una idea, siempre verifico si @hadley ya lo ha hecho, y la mayoría de las veces lo ha hecho :-)
Ramnath
1
Estoy un poco confundida; ¿Debo hacer merge_all o merge_recurse? En cualquier caso, cuando trato de agregar mis argumentos adicionales a cualquiera de los dos, obtengo el error "argumento formal" todo "combinado con múltiples argumentos reales".
bshor
2
Creo que se me cayó esto de reshape2. Reducir + fusionar es igual de simple.
Hadley
2
@Ramnath, el enlace está muerto, ¿hay un espejo?
Eduardo
4

Puedes usar la recursión para hacer esto. No he verificado lo siguiente, pero debería darte la idea correcta:

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

Reutilizaré el ejemplo de datos de @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)

Aquí hay una solución corta y dulce usando purrrytidyr

library(tidyverse)

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

La función eatde mi paquete safejoin tiene esa característica, si le da una lista de data.frames como una segunda entrada, los unirá recursivamente a la primera entrada.

Pedir prestado y ampliar los datos de la respuesta aceptada:

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

No tenemos que tomar todas las columnas, podemos usar ayudantes seleccionados de tidyselect y elegir (a medida que comenzamos desde que se mantienen .xtodas las .xcolumnas):

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

o eliminar los específicos:

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

Si se nombra la lista, los nombres se utilizarán como prefijos:

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

Si hay conflictos de columna, el .conflictargumento le permite resolverlo, por ejemplo, tomando el primero / segundo, agregándolos, fusionándolos o anidándolos.

mantener primero:

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

mantener el último:

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

añadir:

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

juntarse:

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

nido:

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

NAlos valores se pueden reemplazar usando el .fillargumento

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

De manera predeterminada, es una opción mejorada, left_joinpero todas las uniones dplyr son compatibles con el .modeargumento, las uniones difusas también son compatibles con el match_fun argumento (está envuelto alrededor del paquete fuzzyjoin) o dan una fórmula como ~ X("var1") > Y("var2") & X("var3") < Y("var4")la del byargumento.

Moody_Mudskipper
fuente
0

Tenía una lista de marcos de datos sin columna de identificación común.
Me faltaban datos en muchos dfs. Había valores nulos. Los marcos de datos se produjeron utilizando la función de tabla. Reducir, Fusionar, rbind, rbind.fill y sus similares no pudieron ayudarme a lograr mi objetivo. Mi objetivo era producir un marco de datos combinado comprensible, irrelevante de los datos faltantes y la columna de identificación común.

Por lo tanto, hice la siguiente función. Quizás esta función pueda ayudar a alguien.

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

está siguiendo la función

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

}

Ejecutando el ejemplo

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

rbind_null_df_lists ( list_of_df )
Elias EstatisticsEU
fuente
0

Cuando tiene una lista de dfs, y una columna contiene la "ID", pero en algunas listas faltan algunas ID, entonces puede usar esta versión de Reducir / Fusionar para unir múltiples Dfs de ID o etiquetas de fila faltantes:

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

Aquí hay un contenedor genérico que se puede usar para convertir una función binaria en función de parámetros múltiples. El beneficio de esta solución es que es muy genérico y se puede aplicar a cualquier función binaria. Solo necesita hacerlo una vez y luego puede aplicarlo en cualquier lugar.

Para demostrar la idea, utilizo una recursión simple para implementar. Por supuesto, se puede implementar de una manera más elegante que se beneficie del buen soporte de R para el paradigma funcional.

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

Luego puede simplemente envolver cualquier función binaria con él y llamar con parámetros posicionales (generalmente data.frames) en los primeros paréntesis y parámetros con nombre en los segundos paréntesis (como by =o suffix =). Si no hay parámetros con nombre, deje los segundos paréntesis vacíos.

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)()
englealuze
fuente