Manera eficiente de recostackr data.frames con diferentes columnas

Tengo una lista de marcos de datos con diferentes conjuntos de columnas, me gustaría combinarlos por filas en un dataframe. Yo uso plyr::rbind.fill para hacer eso. Estoy buscando algo que lo haría de manera más eficiente. Similar a la respuesta dada aquí

 require(plyr) set.seed(45) sample.fun <- function() { nam <- sample(LETTERS, sample(5:15)) val <- data.frame(matrix(sample(letters, length(nam)*10,replace=TRUE),nrow=10)) setNames(val, nam) } ll <- replicate(1e4, sample.fun()) rbind.fill(ll) 

ACTUALIZACIÓN: vea esta respuesta actualizada en su lugar.

ACTUALIZACIÓN (eddi): Esto ahora se ha implementado en la versión 1.8.11 como argumento de fill para rbind . Por ejemplo:

 DT1 = data.table(a = 1:2, b = 1:2) DT2 = data.table(a = 3:4, c = 1:2) rbind(DT1, DT2, fill = TRUE) # abc #1: 1 1 NA #2: 2 2 NA #3: 3 NA 1 #4: 4 NA 2 

FR # 4790 agregado ahora – rbind.fill (de plyr) como funcionalidad para fusionar la lista de data.frames / data.tables

Nota 1:

Esta solución usa la función rbindlist para “enlazar” la lista de data.tables y para esto, asegúrese de usar la versión 1.8.9 debido a este error en las versiones <1.8.9 .

Nota 2:

rbindlist al enlazar listas de data.frames / data.tables, a partir de ahora, conservará el tipo de datos de la primera columna. Es decir, si una columna en first data.frame es character y la misma columna en 2nd data.frame es “factor”, entonces, rbindlist dará como resultado que esta columna sea un caracter. Entonces, si su data.frame consistió en todas las columnas de caracteres, entonces, su solución con este método será idéntica al método plyr. De lo contrario, los valores seguirán siendo los mismos, pero algunas columnas serán carácter en lugar de factor. Tendrás que convertirte a “factor” tú mismo después. Esperemos que este comportamiento cambie en el futuro .

Y ahora aquí está usando data.table (y comparación de benchmarking con rbind.fill de plyr ):

 require(data.table) rbind.fill.DT <- function(ll) { # changed sapply to lapply to return a list always all.names <- lapply(ll, names) unq.names <- unique(unlist(all.names)) ll.m <- rbindlist(lapply(seq_along(ll), function(x) { tt <- ll[[x]] setattr(tt, 'class', c('data.table', 'data.frame')) data.table:::settruelength(tt, 0L) invisible(alloc.col(tt)) tt[, c(unq.names[!unq.names %chin% all.names[[x]]]) := NA_character_] setcolorder(tt, unq.names) })) } rbind.fill.PLYR <- function(ll) { rbind.fill(ll) } require(microbenchmark) microbenchmark(t1 <- rbind.fill.DT(ll), t2 <- rbind.fill.PLYR(ll), times=10) # Unit: seconds # expr min lq median uq max neval # t1 <- rbind.fill.DT(ll) 10.8943 11.02312 11.26374 11.34757 11.51488 10 # t2 <- rbind.fill.PLYR(ll) 121.9868 134.52107 136.41375 184.18071 347.74724 10 # for comparison change t2 to data.table setattr(t2, 'class', c('data.table', 'data.frame')) data.table:::settruelength(t2, 0L) invisible(alloc.col(t2)) setcolorder(t2, unique(unlist(sapply(ll, names)))) identical(t1, t2) # [1] TRUE 

Debe tenerse en cuenta que el plyr de rbind.fill a esta solución de data.table particular hasta un tamaño de lista de aproximadamente 500.

Parcela de Benchmarking:

Aquí está la ttwig en ejecuciones con la longitud de la lista de data.frames con seq(1000, 10000, by=1000) . He usado microbenchmark con 10 repeticiones en cada una de estas diferentes longitudes de lista.

enter image description here

Benchmarking Gist:

Aquí está la esencia de la evaluación comparativa , en caso de que alguien quiera replicar los resultados.

Ahora que rbindlist (y rbind ) para data.table ha mejorado la funcionalidad y la velocidad con los recientes cambios / confirmaciones en v1.9.3 (versión de desarrollo), y dplyr tiene una versión más rápida de plyr de rbind.fill , llamada rbind_all , esta respuesta mío parece demasiado anticuado.

Aquí está la entrada relevante de NOTICIAS para rbindlist :

 o 'rbindlist' gains 'use.names' and 'fill' arguments and is now implemented entirely in C. Closes #5249 -> use.names by default is FALSE for backwards compatibility (doesn't bind by names by default) -> rbind(...) now just calls rbindlist() internally, except that 'use.names' is TRUE by default, for compatibility with base (and backwards compatibility). -> fill by default is FALSE. If fill is TRUE, use.names has to be TRUE. -> At least one item of the input list has to have non-null column names. -> Duplicate columns are bound in the order of occurrence, like base. -> Attributes that might exist in individual items would be lost in the bound result. -> Columns are coerced to the highest SEXPTYPE, if they are different, if/when possible. -> And incredibly fast ;). -> Documentation updated in much detail. Closes DR #5158. 

Por lo tanto, he comparado las versiones más recientes (y más rápidas) en datos relativamente más grandes a continuación.


Nuevo punto de referencia:

Crearemos un total de 10,000 data.tables con columnas que van desde 200-300 con el número total de columnas después del enlace hasta 500.

Funciones para crear datos:

 require(data.table) ## 1.9.3 commit 1267 require(dplyr) ## commit 1504 devel set.seed(1L) names = paste0("V", 1:500) foo <- function() { cols = sample(200:300, 1) data = setDT(lapply(1:cols, function(x) sample(10))) setnames(data, sample(names)[1:cols]) } n = 10e3L ll = vector("list", n) for (i in 1:n) { .Call("Csetlistelt", ll, i, foo()) } 

Y aquí están los tiempos:

 ## Updated timings on data.table v1.9.5 - three consecutive runs: system.time(ans1 <- rbindlist(ll, fill=TRUE)) # user system elapsed # 1.993 0.106 2.107 system.time(ans1 <- rbindlist(ll, fill=TRUE)) # user system elapsed # 1.644 0.092 1.744 system.time(ans1 <- rbindlist(ll, fill=TRUE)) # user system elapsed # 1.297 0.088 1.389 ## dplyr's rbind_all - Timings for three consecutive runs system.time(ans2 <- rbind_all(ll)) # user system elapsed # 9.525 0.121 9.761 # user system elapsed # 9.194 0.112 9.370 # user system elapsed # 8.665 0.081 8.780 identical(ans1, setDT(ans2)) # [1] TRUE 

Todavía hay algo que ganar si paraleliza tanto rbind.fill como rbindlist . Los resultados se hacen con data.table versión 1.8.8 como la versión 1.8.9 tiene bricked cuando lo intenté con la función paralelizada. Por lo tanto, los resultados no son idénticos entre data.table y plyr , pero son idénticos en la solución data.table o plyr . plyr coincidencias de plyr paralelas con plyr sin plyr , y viceversa.

Aquí está el benchmark / scripts. El parallel.rbind.fill.DT ve horrible, pero ese es el más rápido que pude extraer.

 require(plyr) require(data.table) require(ggplot2) require(rbenchmark) require(parallel) # data.table::rbindlist solutions rbind.fill.DT <- function(ll) { all.names <- lapply(ll, names) unq.names <- unique(unlist(all.names)) rbindlist(lapply(seq_along(ll), function(x) { tt <- ll[[x]] setattr(tt, 'class', c('data.table', 'data.frame')) data.table:::settruelength(tt, 0L) invisible(alloc.col(tt)) tt[, c(unq.names[!unq.names %chin% all.names[[x]]]) := NA_character_] setcolorder(tt, unq.names) })) } parallel.rbind.fill.DT <- function(ll, cluster=NULL){ all.names <- lapply(ll, names) unq.names <- unique(unlist(all.names)) if(is.null(cluster)){ ll.m <- rbindlist(lapply(seq_along(ll), function(x) { tt <- ll[[x]] setattr(tt, 'class', c('data.table', 'data.frame')) data.table:::settruelength(tt, 0L) invisible(alloc.col(tt)) tt[, c(unq.names[!unq.names %chin% all.names[[x]]]) := NA_character_] setcolorder(tt, unq.names) })) }else{ cores <- length(cluster) sequ <- as.integer(seq(1, length(ll), length.out = cores+1)) Call <- paste(paste("list", seq(cores), sep=""), " = ll[", c(1, sequ[2:cores]+1), ":", sequ[2:(cores+1)], "]", sep="", collapse=", ") ll <- eval(parse(text=paste("list(", Call, ")"))) rbindlist(clusterApply(cluster, ll, function(ll, unq.names){ rbindlist(lapply(seq_along(ll), function(x, ll, unq.names) { tt <- ll[[x]] setattr(tt, 'class', c('data.table', 'data.frame')) data.table:::settruelength(tt, 0L) invisible(alloc.col(tt)) tt[, c(unq.names[!unq.names %chin% colnames(tt)]) := NA_character_] setcolorder(tt, unq.names) }, ll=ll, unq.names=unq.names)) }, unq.names=unq.names)) } } # plyr::rbind.fill solutions rbind.fill.PLYR <- function(ll) { rbind.fill(ll) } parallel.rbind.fill.PLYR <- function(ll, cluster=NULL, magicConst=400){ if(is.null(cluster) | ceiling(length(ll)/magicConst) < length(cluster)){ rbind.fill(ll) }else{ cores <- length(cluster) sequ <- as.integer(seq(1, length(ll), length.out = ceiling(length(ll)/magicConst))) Call <- paste(paste("list", seq(cores), sep=""), " = ll[", c(1, sequ[2:(length(sequ)-1)]+1), ":", sequ[2:length(sequ)], "]", sep="", collapse=", ") ll <- eval(parse(text=paste("list(", Call, ")"))) rbind.fill(parLapply(cluster, ll, rbind.fill)) } } # Function to generate sample data of varying list length set.seed(45) sample.fun <- function() { nam <- sample(LETTERS, sample(5:15)) val <- data.frame(matrix(sample(letters, length(nam)*10,replace=TRUE),nrow=10)) setNames(val, nam) } ll <- replicate(10000, sample.fun()) cl <- makeCluster(4, type="SOCK") clusterEvalQ(cl, library(data.table)) clusterEvalQ(cl, library(plyr)) benchmark(t1 <- rbind.fill.PLYR(ll), t2 <- rbind.fill.DT(ll), t3 <- parallel.rbind.fill.PLYR(ll, cluster=cl, 400), t4 <- parallel.rbind.fill.DT(ll, cluster=cl), replications=5) stopCluster(cl) # Results for rbinding 10000 dataframes # done with 4 cores, i5 3570k and 16gb memory # test reps elapsed relative # rbind.fill.PLYR 5 321.80 16.682 # rbind.fill.DT 5 26.10 1.353 # parallel.rbind.fill.PLYR 5 28.00 1.452 # parallel.rbind.fill.DT 5 19.29 1.000 # checking are results equal t1 <- as.matrix(t1) t2 <- as.matrix(t2) t3 <- as.matrix(t3) t4 <- as.matrix(t4) t1 <- t1[order(t1[, 1], t1[, 2]), ] t2 <- t2[order(t2[, 1], t2[, 2]), ] t3 <- t3[order(t3[, 1], t3[, 2]), ] t4 <- t4[order(t4[, 1], t4[, 2]), ] identical(t2, t4) # TRUE identical(t1, t3) # TRUE identical(t1, t2) # FALSE, mismatch between plyr and data.table 

Como puede ver parallesizing rbind.fill hizo comparable con data.table , y podría obtener un aumento marginal de velocidad al paralizar data.table incluso con este bajo de un conteo de dataframe.