Modo efficiente per rbind data.frames con colonne diverse

Ho una lista di frame di dati con diversi set di colonne, mi piacerebbe combinarli per righe in un frame di dati. Io uso plyr::rbind.fill per farlo. Sto cercando qualcosa che lo farebbe in modo più efficiente. Simile alla risposta fornita qui

 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) 

AGGIORNAMENTO: vedi invece questa risposta aggiornata .

UPDATE (eddi): ora è stato implementato nella versione 1.8.11 come argomento fill per rbind . Per esempio:

 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 aggiunto ora – rbind.fill (da plyr) come funzionalità per unire l’elenco di data.frames / data.tables

Nota 1:

Questa soluzione utilizza la funzione rbindlist di data.table nell’elenco “rbind” di data.tables e per questo, assicurarsi di utilizzare la versione 1.8.9 a causa di questo errore nelle versioni <1.8.9 .

Nota 2:

rbindlist quando si legano gli elenchi di data.frames / data.tables, a partire da ora, manterrà il tipo di dati della prima colonna. Cioè, se una colonna nel primo data.frame è un carattere e la stessa colonna nel secondo data.frame è “fattore”, quindi, rbindlist farà sì che questa colonna sia un carattere. Quindi, se il tuo data.frame consistesse di tutte le colonne di caratteri, la tua soluzione con questo metodo sarà identica al metodo plyr. In caso contrario, i valori saranno uguali, ma alcune colonne saranno invece di carattere. Dovrai convertire in “fattore” te stesso dopo. Speriamo che questo comportamento cambi in futuro .

E ora data.table usando data.table (e confronto comparativo con rbind.fill da 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 

Si noti che plyr 's rbind.fill supera questa particolare soluzione data.table fino alla dimensione dell'elenco di circa 500.

Trama di benchmarking:

Ecco la trama in esecuzione con lunghezza lista di data.frames con seq(1000, 10000, by=1000) . Ho usato microbenchmark con 10 ripetizioni su ciascuna di queste diverse lunghezze di lista.

inserisci la descrizione dell'immagine qui

Analisi comparativa:

Ecco l'essenziale per il benchmarking , nel caso qualcuno volesse replicare i risultati.

Ora che rbindlist (e rbind ) per data.table ha migliorato la funzionalità e la velocità con le modifiche / commit recenti nella v1.9.3 (versione di sviluppo), e dplyr ha una versione più veloce di plyr di rbind.fill , chiamata rbind_all , questa risposta il mio sembra un po ‘obsoleto.

Ecco la voce NEWS relativa a 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. 

Quindi, ho messo a confronto le versioni più recenti (e più veloci) su dati relativamente più grandi di seguito.


Nuovo punto di riferimento:

Creeremo un totale di 10.000 data.tables con colonne che vanno da 200 a 300 con il numero totale di colonne dopo il binding da 500.

Funzioni per creare dati:

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

E qui ci sono i tempi:

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

C’è ancora qualcosa da guadagnare se parallelizzi sia rbind.fill che rbindlist . I risultati sono stati eseguiti con data.table versione 1.8.8 poiché la versione 1.8.9 è stata murata quando l’ho provata con la funzione parallelizzata. Quindi i risultati non sono identici tra data.table e plyr , ma sono identici nella soluzione data.table o plyr . Il significato di plyr parallelo corrisponde a plyr ineguagliabile e viceversa.

Ecco i benchmark / script. Il parallel.rbind.fill.DT sembra orribile, ma quello è il più veloce che potrei tirare.

 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 

Come puoi vedere parallelizzare rbind.fill lo ha reso paragonabile a data.table , e potresti ottenere un aumento marginale della velocità parallelizzando data.table anche con questo basso di un conteggio dei dati.