¿Por qué es plyr tan lento?

Creo que estoy usando plyr incorrectamente. ¿Podría alguien decirme si se trata de un código plyr ‘eficiente’?

require(plyr) plyr <- function(dd) ddply(dd, .(price), summarise, ss=sum(volume)) 

Un pequeño contexto: tengo algunos grandes problemas de agregación y he notado que cada uno tomaba algo de tiempo. Al tratar de resolver los problemas, me interesé en la realización de varios procedimientos de agregación en R.

Probé algunos métodos de agregación y me encontré esperando todo el día.

Cuando finalmente obtuve los resultados, descubrí una gran brecha entre el método plyr y los demás, lo que me hace pensar que he hecho algo completamente mal.

Ejecuté el siguiente código (pensé que revisaría el nuevo paquete de dataframe mientras estaba en él):

 require(plyr) require(data.table) require(dataframe) require(rbenchmark) require(xts) plyr <- function(dd) ddply(dd, .(price), summarise, ss=sum(volume)) t.apply <- function(dd) unlist(tapply(dd$volume, dd$price, sum)) t.apply.x <- function(dd) unlist(tapply(dd[,2], dd[,1], sum)) l.apply <- function(dd) unlist(lapply(split(dd$volume, dd$price), sum)) l.apply.x <- function(dd) unlist(lapply(split(dd[,2], dd[,1]), sum)) by <- function(dd) unlist(by(dd$volume, dd$price, sum)) byx <- function(dd) unlist(by(dd[,2], dd[,1], sum)) agg <- function(dd) aggregate(dd$volume, list(dd$price), sum) agg.x <- function(dd) aggregate(dd[,2], list(dd[,1]), sum) dtd <- function(dd) dd[, sum(volume), by=(price)] obs <- c(5e1, 5e2, 5e3, 5e4, 5e5, 5e6, 5e6, 5e7, 5e8) timS <- timeBasedSeq('20110101 083000/20120101 083000') bmkRL <- list(NULL) for (i in 1:5){ tt <- timS[1:obs[i]] for (j in 1:8){ pxl <- seq(0.9, 1.1, by= (1.1 - 0.9)/floor(obs[i]/(11-j))) px <- sample(pxl, length(tt), replace=TRUE) vol <- rnorm(length(tt), 1000, 100) d.df <- base::data.frame(time=tt, price=px, volume=vol) d.dfp <- dataframe::data.frame(time=tt, price=px, volume=vol) d.matrix <- as.matrix(d.df[,-1]) d.dt <- data.table(d.df) listLabel <- paste('i=',i, 'j=',j) bmkRL[[listLabel]] <- benchmark(plyr(d.df), plyr(d.dfp), t.apply(d.df), t.apply(d.dfp), t.apply.x(d.matrix), l.apply(d.df), l.apply(d.dfp), l.apply.x(d.matrix), by(d.df), by(d.dfp), byx(d.matrix), agg(d.df), agg(d.dfp), agg.x(d.matrix), dtd(d.dt), columns =c('test', 'elapsed', 'relative'), replications = 10, order = 'elapsed') } } 

Se suponía que la prueba verificaba hasta 5e8, pero tardó demasiado, principalmente debido a plyr. El 5e5 la mesa final muestra el problema:

 $`i= 5 j= 8` test elapsed relative 15 dtd(d.dt) 4.156 1.000000 6 l.apply(d.df) 15.687 3.774543 7 l.apply(d.dfp) 16.066 3.865736 8 l.apply.x(d.matrix) 16.659 4.008422 4 t.apply(d.dfp) 21.387 5.146054 3 t.apply(d.df) 21.488 5.170356 5 t.apply.x(d.matrix) 22.014 5.296920 13 agg(d.dfp) 32.254 7.760828 14 agg.x(d.matrix) 32.435 7.804379 12 agg(d.df) 32.593 7.842397 10 by(d.dfp) 98.006 23.581809 11 byx(d.matrix) 98.134 23.612608 9 by(d.df) 98.337 23.661453 1 plyr(d.df) 9384.135 2257.972810 2 plyr(d.dfp) 9384.448 2258.048123 

¿Es esto correcto? ¿Por qué es plyr 2250x más lento que data.table ? ¿Y por qué el uso del nuevo paquete de marcos de datos no hizo la diferencia?

La información de la sesión es:

 > sessionInfo() R version 2.15.1 (2012-06-22) Platform: x86_64-apple-darwin9.8.0/x86_64 (64-bit) locale: [1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8 attached base packages: [1] stats graphics grDevices utils datasets methods base other attached packages: [1] xts_0.8-6 zoo_1.7-7 rbenchmark_0.3 dataframe_2.5 data.table_1.8.1 plyr_1.7.1 loaded via a namespace (and not attached): [1] grid_2.15.1 lattice_0.20-6 tools_2.15.1 

¿Por qué es tan lento? Una pequeña investigación localizó una publicación de un grupo de correo desde agosto de 2011, donde @hadley, el autor del paquete, afirma

Esto es un inconveniente de la forma en que ddply siempre funciona con marcos de datos. Será un poco más rápido si usa summarize en lugar de data.frame (porque data.frame es muy lento), pero aún estoy pensando en cómo superar esta limitación fundamental del enfoque ddply.


En cuanto a ser un código plyr eficiente , tampoco lo sabía. Después de un montón de pruebas param y benchmarking, parece que podemos hacerlo mejor.

El summarize() en su comando es una función de ayuda justa, pura y simple. Podemos reemplazarlo con nuestra propia función de sum ya que no ayuda con nada que no sea simple y los argumentos .data y .(price) pueden hacerse más explícitos. El resultado es

 ddply( dd[, 2:3], ~price, function(x) sum( x$volume ) ) 

El summarize puede parecer agradable, pero simplemente no es más rápido que una simple llamada a función. Que tiene sentido; solo mira nuestra pequeña función versus el código para summarize . Ejecutar sus puntos de referencia con la fórmula revisada produce una ganancia notable. No tome eso para decir que ha usado plyr incorrectamente, no lo ha hecho, simplemente no es eficiente; nada de lo que puedas hacer va a ser tan rápido como otras opciones.

En mi opinión, la función optimizada todavía huele mal, ya que no está clara y debe ser analizada mentalmente y aún así es ridículamente lenta en comparación con data.table (incluso con un 60% de ganancia).


En el mismo hilo mencionado anteriormente, con respecto a la lentitud de plyr, se menciona un proyecto plyr2. Desde el momento de la respuesta original a la pregunta, el autor del plyr ha publicado dplyr como el sucesor de plyr. Aunque tanto plyr como dplyr se facturan como herramientas de manipulación de datos y su principal interés declarado es la agregación, es posible que aún esté interesado en los resultados de referencia del nuevo paquete para comparación, ya que cuenta con un backend rediseñado para mejorar el rendimiento.

 plyr_Original < - function(dd) ddply( dd, .(price), summarise, ss=sum(volume)) plyr_Optimized <- function(dd) ddply( dd[, 2:3], ~price, function(x) sum( x$volume ) ) dplyr <- function(dd) dd %.% group_by(price) %.% summarize( sum(volume) ) data_table <- function(dd) dd[, sum(volume), keyby=price] 

El paquete de dataframe se ha eliminado de CRAN y, posteriormente, de las pruebas, junto con las versiones de la función de matriz.

Aquí están los resultados de i=5, j=8 benchmark:

 $`obs= 500,000 unique prices= 158,286 reps= 5` test elapsed relative 9 data_table(d.dt) 0.074 1.000 4 dplyr(d.dt) 0.133 1.797 3 dplyr(d.df) 1.832 24.757 6 l.apply(d.df) 5.049 68.230 5 t.apply(d.df) 8.078 109.162 8 agg(d.df) 11.822 159.757 7 by(d.df) 48.569 656.338 2 plyr_Optimized(d.df) 148.030 2000.405 1 plyr_Original(d.df) 401.890 5430.946 

Sin duda, la optimización ayudó un poco. Eche un vistazo a las funciones d.df ; simplemente no pueden competir.

Para tener una pequeña perspectiva sobre la lentitud de la estructura de data.frame aquí hay micro-benchmarks de los tiempos de agregación de data_table y dplyr usando un dataset de prueba más grande ( i=8,j=8 ).

 $`obs= 50,000,000 unique prices= 15,836,476 reps= 5` Unit: seconds expr min lq median uq max neval data_table(d.dt) 1.190 1.193 1.198 1.460 1.574 10 dplyr(d.dt) 2.346 2.434 2.542 2.942 9.856 10 dplyr(d.df) 66.238 66.688 67.436 69.226 86.641 10 

El data.frame aún está en el polvo. No solo eso, sino que aquí está el sistema transcurrido. Tiempo para completar las estructuras de datos con los datos de prueba:

 `d.df` (data.frame) 3.181 seconds. `d.dt` (data.table) 0.418 seconds. 

Tanto la creación como la agregación de data.frame es más lenta que la de data.table.

Trabajar con data.frame en R es más lento que algunas alternativas, pero como los puntos de referencia muestran, las funciones R incorporadas expulsan a Plyr del agua. Incluso administrar el data.frame como lo hace dplyr, que mejora los built-ins, no da una velocidad óptima; donde como data.table es más rápido tanto en creación como en agregación y data.table hace lo que hace mientras trabaja con / sobre data.frames.

En el final...

Plyr es lento debido a la forma en que funciona y gestiona la manipulación de data.frame .

[punt :: vea los comentarios a la pregunta original].


 ## R version 3.0.2 (2013-09-25) ## Platform: x86_64-pc-linux-gnu (64-bit) ## ## attached base packages: ## [1] stats graphics grDevices utils datasets methods base ## ## other attached packages: ## [1] microbenchmark_1.3-0 rbenchmark_1.0.0 xts_0.9-7 ## [4] zoo_1.7-11 data.table_1.9.2 dplyr_0.1.2 ## [7] plyr_1.8.1 knitr_1.5.22 ## ## loaded via a namespace (and not attached): ## [1] assertthat_0.1 evaluate_0.5.2 formatR_0.10.4 grid_3.0.2 ## [5] lattice_0.20-27 Rcpp_0.11.0 reshape2_1.2.2 stringr_0.6.2 ## [9] tools_3.0.2 

Generador de datos .rmd