Creciendo un data.frame de una manera eficiente con la memoria

De acuerdo con Crear un dataframe R fila por fila , no es ideal data.frame a un data.frame utilizando rbind , ya que crea una copia del cuadro de datos completo cada vez. ¿Cómo acumulo datos en R resultan en un data.frame sin incurrir en esta penalización? El formato intermedio no necesita ser un data.frame .

Primer enfoque

Intenté acceder a cada elemento de un data.frame preasignado:

 res <- data.frame(x=rep(NA,1000), y=rep(NA,1000)) tracemem(res) for(i in 1:1000) { res[i,"x"] <- runif(1) res[i,"y"] <- rnorm(1) } 

Pero tracemem se vuelve loco (por ejemplo, el data.frame se copia a una nueva dirección cada vez).

Enfoque alternativo (tampoco funciona)

Un enfoque (no estoy seguro de que sea más rápido ya que aún no lo he comparado) es crear una lista de data.frames, luego astackrlos todos juntos:

 makeRow <- function() data.frame(x=runif(1),y=rnorm(1)) res <- replicate(1000, makeRow(), simplify=FALSE ) # returns a list of data.frames library(taRifx) res.df <- stack(res) 

Lamentablemente, al crear la lista, creo que será difícil preasignarla. Por ejemplo:

 > tracemem(res) [1] "<0x79b98b0>" > res[[2]] <- data.frame() tracemem[0x79b98b0 -> 0x71da500]: 

En otras palabras, reemplazar un elemento de la lista hace que la lista se copie. Supongo que toda la lista, pero es posible que sea solo ese elemento de la lista. No estoy íntimamente familiarizado con los detalles de la administración de memoria de R.

Probablemente el mejor enfoque

Al igual que con muchos procesos de velocidad o de memoria limitada en estos días, el mejor enfoque puede ser usar data.table lugar de data.frame . Como data.table tiene el operador := assign by reference, puede actualizar sin volver a copiar:

 library(data.table) dt <- data.table(x=rep(0,1000), y=rep(0,1000)) tracemem(dt) for(i in 1:1000) { dt[i,x := runif(1)] dt[i,y := rnorm(1)] } # note no message from tracemem 

Pero como señala @MatthewDowle, set() es la forma adecuada de hacer esto dentro de un bucle. Hacerlo lo hace aún más rápido:

 library(data.table) n <- 10^6 dt <- data.table(x=rep(0,n), y=rep(0,n)) dt.colon <- function(dt) { for(i in 1:n) { dt[i,x := runif(1)] dt[i,y := rnorm(1)] } } dt.set <- function(dt) { for(i in 1:n) { set(dt,i,1L, runif(1) ) set(dt,i,2L, rnorm(1) ) } } library(microbenchmark) m <- microbenchmark(dt.colon(dt), dt.set(dt),times=2) 

(Los resultados se muestran a continuación)

Benchmarking

Con el bucle ejecutado 10.000 veces, la tabla de datos es casi un orden completo de magnitud más rápida:

 Unit: seconds expr min lq median uq max 1 test.df() 523.49057 523.49057 524.52408 525.55759 525.55759 2 test.dt() 62.06398 62.06398 62.98622 63.90845 63.90845 3 test.stack() 1196.30135 1196.30135 1258.79879 1321.29622 1321.29622 

puntos de referencia

Y la comparación de := con set() :

 > m Unit: milliseconds expr min lq median uq max 1 dt.colon(dt) 654.54996 654.54996 656.43429 658.3186 658.3186 2 dt.set(dt) 13.29612 13.29612 15.02891 16.7617 16.7617 

Tenga en cuenta que n aquí es 10 ^ 6 no 10 ^ 5 como en los puntos de referencia trazados anteriormente. Entonces hay un orden de magnitud más de trabajo, y el resultado se mide en milisegundos en lugar de segundos. Impresionante de hecho.

También podría tener un objeto de lista vacío donde los elementos se llenan con marcos de datos; luego recoge los resultados al final con sapply o similar. Un ejemplo se puede encontrar aquí . Esto no incurrirá en las penalidades de hacer crecer un objeto.

Bien, estoy muy sorprendido de que nadie haya mencionado la conversión a una matriz todavía …

Comparando con las funciones dt.colon y dt.set definidas por Ari B. Friedman , la conversión a una matriz tiene el mejor tiempo de ejecución (un poco más rápido que dt.colon ). Todas las afectaciones dentro de una matriz se realizan por referencia, por lo que no se realiza una copia de memoria innecesaria en este código.

CÓDIGO:

 library(data.table) n <- 10^4 dt <- data.table(x=rep(0,n), y=rep(0,n)) use.matrix <- function(dt) { mat = as.matrix(dt) # converting to matrix for(i in 1:n) { mat[i,1] = runif(1) mat[i,2] = rnorm(1) } return(as.data.frame(mat)) # converting back to a data.frame } dt.colon <- function(dt) { # same as Ari's function for(i in 1:n) { dt[i,x := runif(1)] dt[i,y := rnorm(1)] } } dt.set <- function(dt) { # same as Ari's function for(i in 1:n) { set(dt,i,1L, runif(1) ) set(dt,i,2L, rnorm(1) ) } } library(microbenchmark) microbenchmark(dt.colon(dt), dt.set(dt), use.matrix(dt),times=10) 

RESULTADO:

 Unit: milliseconds expr min lq median uq max neval dt.colon(dt) 7107.68494 7193.54792 7262.76720 7277.24841 7472.41726 10 dt.set(dt) 93.25954 94.10291 95.07181 97.09725 99.18583 10 use.matrix(dt) 48.15595 51.71100 52.39375 54.59252 55.04192 10 

Ventajas de usar una matriz:

  • este es el método más rápido hasta el momento
  • no tiene que aprender / usar objetos data.table

Con de usar una matriz:

  • solo puede manejar un tipo de datos en una matriz (en particular, si tiene tipos mixtos en las columnas de su data.frame, entonces todos serán convertidos a caracteres por la línea: mat = as.matrix (dt) # converting a la matriz )

Me gusta RSQLite para el caso: dbWriteTable(...,append=TRUE) durante la recostackción, y la instrucción dbReadTable al final.

Si los datos son lo suficientemente pequeños, se puede usar el archivo “: memoria:”, si es grande, el disco duro.

Por supuesto, no puede competir en términos de velocidad:

 makeRow <- function() data.frame(x=runif(1),y=rnorm(1)) library(RSQLite) con <- dbConnect(RSQLite::SQLite(), ":memory:") collect1 <- function(n) { for (i in 1:n) dbWriteTable(con, "test", makeRow(), append=TRUE) dbReadTable(con, "test", row.names=NULL) } collect2 <- function(n) { res <- data.frame(x=rep(NA, n), y=rep(NA, n)) for(i in 1:n) res[i,] <- makeRow()[1,] res } > system.time(collect1(1000)) User System verstrichen 7.01 0.00 7.05 > system.time(collect2(1000)) User System verstrichen 0.80 0.01 0.81 

Pero podría verse mejor si los data.frame s tienen más de una fila. Y no necesita saber el número de filas por adelantado.