He buscado en StackOverflow, pero no encuentro una solución específica para mi problema, que consiste en añadir filas a un marco de datos de R.
Estoy inicializando un marco de datos vacío de 2 columnas, de la siguiente manera.
df = data.frame(x = numeric(), y = character())
Luego, mi objetivo es iterar a través de una lista de valores y, en cada iteración, añadir un valor al final de la lista. Empecé con el siguiente código.
for (i in 1:10) {
df$x = rbind(df$x, i)
df$y = rbind(df$y, toString(i))
}
También he intentado las funciones c
, append
y merge
sin éxito. Por favor, hágame saber si tiene alguna sugerencia.
Sin saber lo que estás tratando de hacer, voy a compartir una sugerencia más: Preasigna vectores del tipo que quieras para cada columna, inserta valores en esos vectores y luego, al final, crea tu data.frame
.
Siguiendo con Julian's f3
(un data.frame
preasignado) como la opción más rápida hasta ahora, definida como:
# pre-allocate space
f3 <- function(n){
df <- data.frame(x = numeric(n), y = character(n), stringsAsFactors = FALSE)
for(i in 1:n){
df$x[i] <- i
df$y[i] <- toString(i)
}
df
}
Aquí's un enfoque similar, pero uno donde el data.frame
se crea como el último paso.
# Use preallocated vectors
f4 <- function(n) {
x <- numeric(n)
y <- character(n)
for (i in 1:n) {
x[i] <- i
y[i] <- i
}
data.frame(x, y, stringsAsFactors=FALSE)
}
microbenchmark
del paquete "microbenchmark" nos dará una visión más completa que system.time
:
library(microbenchmark)
microbenchmark(f1(1000), f3(1000), f4(1000), times = 5)
# Unit: milliseconds
# expr min lq median uq max neval
# f1(1000) 1024.539618 1029.693877 1045.972666 1055.25931 1112.769176 5
# f3(1000) 149.417636 150.529011 150.827393 151.02230 160.637845 5
# f4(1000) 7.872647 7.892395 7.901151 7.95077 8.049581 5
f1()(el enfoque de abajo) es increíblemente ineficiente debido a la frecuencia con la que llama a
data.framey porque el crecimiento de objetos de esa manera es generalmente lento en R.
f3()es mucho mejor debido a la preasignación, pero la estructura de
data.frameen sí misma podría ser parte del cuello de botella aquí. f4()
trata de evitar ese cuello de botella sin comprometer el enfoque que se quiere adoptar.
Esto no es realmente una buena idea, pero si querías hacerlo de esta manera, supongo que puedes intentarlo:
for (i in 1:10) {
df <- rbind(df, data.frame(x = i, y = toString(i)))
}
Ten en cuenta que en tu código hay otro problema:
cadenasComoFactores
si quieres que los caracteres no se conviertan en factores. Utilice: df = data.frame(x = numeric(), y = character(), stringsAsFactors = FALSE)
Comparemos las tres soluciones propuestas:
# use rbind
f1 <- function(n){
df <- data.frame(x = numeric(), y = character())
for(i in 1:n){
df <- rbind(df, data.frame(x = i, y = toString(i)))
}
df
}
# use list
f2 <- function(n){
df <- data.frame(x = numeric(), y = character(), stringsAsFactors = FALSE)
for(i in 1:n){
df[i,] <- list(i, toString(i))
}
df
}
# pre-allocate space
f3 <- function(n){
df <- data.frame(x = numeric(1000), y = character(1000), stringsAsFactors = FALSE)
for(i in 1:n){
df$x[i] <- i
df$y[i] <- toString(i)
}
df
}
system.time(f1(1000))
# user system elapsed
# 1.33 0.00 1.32
system.time(f2(1000))
# user system elapsed
# 0.19 0.00 0.19
system.time(f3(1000))
# user system elapsed
# 0.14 0.00 0.14
La mejor solución es preasignar espacio (como se pretende en R). La siguiente mejor solución es usar list
, y la peor solución (al menos según estos resultados de tiempo) parece ser rbind
.
Supongamos que simplemente no conoce el tamaño del data.frame de antemano. Bien puede ser unas pocas filas, o unos pocos millones. Necesitas tener algún tipo de contenedor que crezca dinámicamente. Teniendo en cuenta mi experiencia y todas las respuestas relacionadas en SO vengo con 4 soluciones distintas:
rbindlist
al data.frame
**2. Utilizar la operación rápida de data.table
y combinarla con la duplicación manual de la tabla cuando sea necesario.
**3. Utilizar RSQLite
y añadir a la tabla mantenida en memoria.
data.frame
's propia capacidad de crecimiento y utilizar entorno personalizado (que tiene semántica de referencia) para almacenar el data.frame para que no se copie en el retorno..
Aquí hay una prueba de todos los métodos para un número pequeño y grande de filas anexadas. Cada método tiene 3 funciones asociadas:
create(first_element)
que devuelve el objeto de respaldo apropiado con first_element
puesto.
append(object, element)
que añade el elemento
al final de la tabla (representado por object
).
access(object)
que obtiene el data.frame
con todos los elementos insertados.
rbindlist
al data.frameEsto es bastante fácil y sencillo:
create.1<-function(elems)
{
return(as.data.table(elems))
}
append.1<-function(dt, elems)
{
return(rbindlist(list(dt, elems),use.names = TRUE))
}
access.1<-function(dt)
{
return(dt)
}
data.table::set
+ duplicar manualmente la tabla cuando sea necesario.Almacenaré la longitud real de la tabla en un atributo rowcount
.
create.2<-function(elems)
{
return(as.data.table(elems))
}
append.2<-function(dt, elems)
{
n<-attr(dt, 'rowcount')
if (is.null(n))
n<-nrow(dt)
if (n==nrow(dt))
{
tmp<-elems[1]
tmp[[1]]<-rep(NA,n)
dt<-rbindlist(list(dt, tmp), fill=TRUE, use.names=TRUE)
setattr(dt,'rowcount', n)
}
pos<-as.integer(match(names(elems), colnames(dt)))
for (j in seq_along(pos))
{
set(dt, i=as.integer(n+1), pos[[j]], elems[[j]])
}
setattr(dt,'rowcount',n+1)
return(dt)
}
access.2<-function(elems)
{
n<-attr(elems, 'rowcount')
return(as.data.table(elems[1:n,]))
}
Esto es básicamente copiar&pegar de Karsten W. answer en un hilo similar.
create.3<-function(elems)
{
con <- RSQLite::dbConnect(RSQLite::SQLite(), ":memory:")
RSQLite::dbWriteTable(con, 't', as.data.frame(elems))
return(con)
}
append.3<-function(con, elems)
{
RSQLite::dbWriteTable(con, 't', as.data.frame(elems), append=TRUE)
return(con)
}
access.3<-function(con)
{
return(RSQLite::dbReadTable(con, "t", row.names=NULL))
}
data.frame
's propia fila-apagado + entorno personalizado.create.4<-function(elems)
{
env<-new.env()
env$dt<-as.data.frame(elems)
return(env)
}
append.4<-function(env, elems)
{
env$dt[nrow(env$dt)+1,]<-elems
return(env)
}
access.4<-function(env)
{
return(env$dt)
}
Por comodidad utilizaré una sola función de prueba para cubrirlas todas con llamadas indirectas. (Lo he comprobado: el uso de do.call
en lugar de llamar a las funciones directamente no hace que el código se ejecute durante más tiempo).
test<-function(id, n=1000)
{
n<-n-1
el<-list(a=1,b=2,c=3,d=4)
o<-do.call(paste0('create.',id),list(el))
s<-paste0('append.',id)
for (i in 1:n)
{
o<-do.call(s,list(o,el))
}
return(do.call(paste0('access.', id), list(o)))
}
Veamos el rendimiento para n=10 inserciones.
También he añadido una función "placebo" (con el sufijo "0") que no realiza nada, sólo para medir la sobrecarga de la configuración de la prueba.
r<-microbenchmark(test(0,n=10), test(1,n=10),test(2,n=10),test(3,n=10), test(4,n=10))
autoplot(r)
[]
Para 1E5 filas (mediciones realizadas en una CPU Intel(R) Core(TM) i7-4710HQ a 2,50GHz):
nr function time
4 data.frame 228.251
3 sqlite 133.716
2 data.table 3.059
1 rbindlist 169.998
0 placebo 0.202
Parece que la sulución basada en SQLite, aunque recupera algo de velocidad en datos grandes, no se acerca en absoluto a data.table + crecimiento exponencial manual. ¡La diferencia es de casi dos órdenes de magnitud!
*Si sabe que va a añadir un número bastante pequeño de filas (n<=100), siga adelante y utilice la solución más sencilla posible: simplemente asigne las filas al data.frame utilizando la notación de corchetes e ignore el hecho de que el data.frame no está prepoblado.
*Para todo lo demás, utilice data.table::set
y haga crecer la tabla de datos exponencialmente (por ejemplo, utilizando mi código).