Cómo agregar filas a un marco de datos R

121

He mirado StackOverflow, pero no puedo encontrar una solución específica para mi problema, que implica agregar filas a un marco de datos R.

Estoy inicializando un marco de datos de 2 columnas vacío, de la siguiente manera.

df = data.frame(x = numeric(), y = character())

Entonces, mi objetivo es recorrer una lista de valores y, en cada iteración, agregar 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 tratado las funciones c, appendy mergesin éxito. Por favor, avíseme si tiene alguna sugerencia.

Gyan Veda
fuente
2
No presumo saber cómo se debe usar R, pero quería ignorar la línea adicional de código que se requeriría para actualizar los índices en cada iteración y no puedo preasignar fácilmente el tamaño del marco de datos porque no No sé cuántas filas tomará finalmente. Recuerde que lo anterior es simplemente un ejemplo de juguete destinado a ser reproducible. De cualquier manera, ¡gracias por tu sugerencia!
Gyan Veda

Respuestas:

115

Actualizar

Sin saber lo que está tratando de hacer, compartiré una sugerencia más: preasigne vectores del tipo que desee para cada columna, inserte valores en esos vectores y luego, al final, cree su archivo data.frame.

Continuando con Julian's f3(una preasignada data.frame) 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
}

Este es un enfoque similar, pero en el que data.framese crea como ú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)
}

microbenchmarkdel paquete "microbenchmark" nos brindará informació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 a continuación) es increíblemente ineficiente debido a la frecuencia con la que llama data.framey porque el crecimiento de objetos de esa manera generalmente es lento en R. f3()se ha mejorado mucho debido a la preasignación, pero la data.frameestructura en sí misma podría ser parte del cuello de botella aquí. f4()intenta evitar ese cuello de botella sin comprometer el enfoque que desea adoptar.


Respuesta original

Esto realmente no es una buena idea, pero si quisiera hacerlo de esta manera, creo que puede intentarlo:

for (i in 1:10) {
  df <- rbind(df, data.frame(x = i, y = toString(i)))
}

Tenga en cuenta que en su código, hay otro problema:

  • Debe usar stringsAsFactorssi desea que los caracteres no se conviertan en factores. Utilizar:df = data.frame(x = numeric(), y = character(), stringsAsFactors = FALSE)
A5C1D2H2I1M1N2O1R2T1
fuente
6
¡Gracias! Eso resuelve mi problema. ¿Por qué esto "realmente no es una buena idea"? ¿Y de qué manera se mezclan xey en el ciclo for?
Gyan Veda
5
@ user2932774, es increíblemente ineficiente hacer crecer un objeto de esta manera en R. Una mejora (pero aún no necesariamente la mejor manera) sería preasignar un data.framedel tamaño final que espera y agregar los valores con [extracción / reemplazo.
A5C1D2H2I1M1N2O1R2T1
1
Gracias, Ananda. Normalmente opto por la preasignación, pero no estoy de acuerdo con que esto no sea una buena idea. Depende de la situación. En mi caso, estoy tratando con datos pequeños y la alternativa llevará más tiempo codificar. Además, este es un código más elegante en comparación con el requerido para actualizar índices numéricos para llenar las porciones apropiadas del marco de datos preasignado en cada iteración. Solo por curiosidad, ¿cuál es la "mejor manera" de realizar esta tarea en su opinión? Pensé que lo mejor hubiera sido la preasignación.
Gyan Veda
2
@ user2932774, es genial. También aprecio tu perspectiva: yo tampoco trabajo con grandes conjuntos de datos. Dicho esto, si voy a trabajar en la escritura de una función o algo, por lo general gastaría un poco más de esfuerzo tratando de modificar el código para obtener mejores velocidades siempre que sea posible. Vea mi actualización para ver un ejemplo de una diferencia de velocidad bastante grande.
A5C1D2H2I1M1N2O1R2T1
1
¡Vaya, esa es una gran diferencia! Gracias por ejecutar esa simulación y enseñarme sobre el paquete microbenchmark. Definitivamente estoy de acuerdo contigo en que es bueno poner ese esfuerzo extra. En mi caso particular, supongo que solo quería algo rápido y sucio en un código que quizás nunca tenga que ejecutar de nuevo. :)
Gyan Veda
34

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 en función de estos resultados de sincronización) parece serlo rbind.

Julián Urbano
fuente
¡Gracias! Aunque no estoy de acuerdo con la sugerencia de Ananda. Si quiero que los caracteres se conviertan a niveles de un factor o no, dependerá de lo que quiera hacer con la salida. Aunque supongo que con la solución que propones, es necesario establecer stringsAsFactors en FALSE.
Gyan Veda
Gracias por la simulación. Me doy cuenta de que la preasignación es mejor en términos de velocidad de procesamiento, pero ese no es el único factor que consideré al tomar esta decisión de codificación.
Gyan Veda
1
En f1 confundió al asignar una cadena al vector numérico x. La línea correcta es:df <- rbind(df, data.frame(x = i, y = toString(i)))
Eldar Agalarov
14

Suponga que simplemente no conoce el tamaño del data.frame de antemano. Bien pueden ser unas pocas filas o unos pocos millones. Necesita 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:

  1. rbindlist al data.frame

  2. Utilice data.tablela setoperación rápida y combínela con doblar manualmente la mesa cuando sea necesario.

  3. Use RSQLitey agregue a la tabla que se guarda en la memoria.

  4. data.frameLa propia capacidad de crecer y usar un entorno personalizado (que tiene semántica de referencia) para almacenar el data.frame para que no se copie al regresar.

Aquí hay una prueba de todos los métodos para un número pequeño y grande de filas agregadas. Cada método tiene 3 funciones asociadas:

  • create(first_element)que devuelve el objeto de respaldo apropiado con first_elementput in.

  • append(object, element)que agrega el elemental final de la tabla (representado por object).

  • access(object)obtiene el data.framecon todos los elementos insertados.

rbindlist al data.frame

Eso 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 + doblando manualmente la mesa cuando sea necesario.

Almacenaré la longitud real de la tabla en un rowcountatributo.

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,]))
}

SQL debería estar optimizado para una rápida inserción de registros, por lo que inicialmente tenía grandes esperanzas de RSQLitesolución

Esto es básicamente copiar y pegar la respuesta de Karsten W. 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.framepropio entorno personalizado de adición de filas.

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

La suite de pruebas:

Por conveniencia, usaré una función de prueba para cubrirlos todos con llamadas indirectas. (Lo comprobé: usar en do.calllugar 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 agregué funciones de 'placebo' (con sufijo 0) que no realizan nada, solo 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)

Tiempos para agregar n = 10 filas

Tiempos para n = 100 filas Tiempos para n = 1000 filas

Para 1E5 filas (mediciones realizadas en CPU Intel (R) Core (TM) i7-4710HQ a 2,50 GHz):

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 sulution basada en SQLite, aunque recupera algo de velocidad en datos grandes, no está ni cerca de data.table + crecimiento exponencial manual. ¡La diferencia es de casi dos órdenes de magnitud!

Resumen

Si sabe que agregará un número bastante pequeño de filas (n <= 100), siga adelante y use la solución más simple posible: simplemente asigne las filas al data.frame usando la notación entre corchetes e ignore el hecho de que el data.frame es no poblado previamente.

Para todo lo demás, use data.table::sety haga crecer el data.table exponencialmente (por ejemplo, usando mi código).

Adam Ryczkowski
fuente
2
La razón por la que SQLite es lento es que en cada INSERT INTO, tiene que REINDEX, que es O (n), donde n es el número de filas. Esto significa que insertar en una base de datos SQL una fila a la vez es O (n ^ 2). SQLite puede ser muy rápido si inserta un data.frame completo a la vez, pero no es el mejor para crecer línea por línea.
Julian Zucker
5

Actualización con purrr, tidyr y dplyr

Como la pregunta ya está fechada (6 años), a las respuestas les falta una solución con los paquetes más nuevos tidyr y purrr. Entonces, para las personas que trabajan con estos paquetes, quiero agregar una solución a las respuestas anteriores, todo bastante interesante, especialmente.

La mayor ventaja de purrr y tidyr es una mejor legibilidad en mi humilde opinión. purrr reemplaza lapply con la familia map () más flexible, tidyr ofrece el método súper intuitivo add_row - simplemente hace lo que dice :)

map_df(1:1000, function(x) { df %>% add_row(x = x, y = toString(x)) })

Esta solución es breve e intuitiva de leer, y es relativamente rápida:

system.time(
   map_df(1:1000, function(x) { df %>% add_row(x = x, y = toString(x)) })
)
   user  system elapsed 
   0.756   0.006   0.766

Escala casi linealmente, por lo que para 1e5 filas, el rendimiento es:

system.time(
  map_df(1:100000, function(x) { df %>% add_row(x = x, y = toString(x)) })
)
   user  system elapsed 
 76.035   0.259  76.489 

lo que lo ubicaría en segundo lugar justo después de data.table (si ignora el placebo) en el punto de referencia de @Adam Ryczkowski:

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
Frijol ágil
fuente
No necesitas usar add_row. Por ejemplo: map_dfr(1:1e5, function(x) { tibble(x = x, y = toString(x)) }).
user3808394
@ user3808394 gracias, ¡es una alternativa interesante! Si alguien quiere crear un marco de datos desde cero, el suyo es más corto, por lo que es la mejor solución. en caso de que ya tenga un marco de datos, mi solución es, por supuesto, mejor.
Agile Bean
Si ya tiene un marco de datos, lo haría en bind_rows(df, map_dfr(1:1e5, function(x) { tibble(x = x, y = toString(x)) }))lugar de usar add_row.
user3808394
2

Tomemos un 'punto' vectorial que tenga números del 1 al 5

point = c(1,2,3,4,5)

si queremos agregar un número 6 en cualquier lugar dentro del vector, el siguiente comando puede ser útil

i) Vectores

new_var = append(point, 6 ,after = length(point))

ii) columnas de una tabla

new_var = append(point, 6 ,after = length(mtcars$mpg))

El comando appendtoma tres argumentos:

  1. el vector / columna que se va a modificar.
  2. valor que se incluirá en el vector modificado.
  3. un subíndice, después del cual se agregarán los valores.

sencillo...!! Disculpas en caso de alguna ...!

Praneeth Krishna
fuente
1

Una solución más genérica para podría ser la siguiente.

    extendDf <- function (df, n) {
    withFactors <- sum(sapply (df, function(X) (is.factor(X)) )) > 0
    nr          <- nrow (df)
    colNames    <- names(df)
    for (c in 1:length(colNames)) {
        if (is.factor(df[,c])) {
            col         <- vector (mode='character', length = nr+n) 
            col[1:nr]   <- as.character(df[,c])
            col[(nr+1):(n+nr)]<- rep(col[1], n)  # to avoid extra levels
            col         <- as.factor(col)
        } else {
            col         <- vector (mode=mode(df[1,c]), length = nr+n)
            class(col)  <- class (df[1,c])
            col[1:nr]   <- df[,c] 
        }
        if (c==1) {
            newDf       <- data.frame (col ,stringsAsFactors=withFactors)
        } else {
            newDf[,c]   <- col 
        }
    }
    names(newDf) <- colNames
    newDf
}

La función extendDf () extiende un marco de datos con n filas.

Como ejemplo:

aDf <- data.frame (l=TRUE, i=1L, n=1, c='a', t=Sys.time(), stringsAsFactors = TRUE)
extendDf (aDf, 2)
#      l i n c                   t
# 1  TRUE 1 1 a 2016-07-06 17:12:30
# 2 FALSE 0 0 a 1970-01-01 01:00:00
# 3 FALSE 0 0 a 1970-01-01 01:00:00

system.time (eDf <- extendDf (aDf, 100000))
#    user  system elapsed 
#   0.009   0.002   0.010
system.time (eDf <- extendDf (eDf, 100000))
#    user  system elapsed 
#   0.068   0.002   0.070
Pisca46
fuente
0

Mi solución es casi la misma que la respuesta original, pero no me funcionó.

Entonces, di nombres para las columnas y funciona:

painel <- rbind(painel, data.frame("col1" = xtweets$created_at,
                                   "col2" = xtweets$text))
Brun Ijbh
fuente