Come aggiungere righe a un frame di dati R.


121

Ho guardato intorno a StackOverflow, ma non riesco a trovare una soluzione specifica per il mio problema, che prevede l'aggiunta di righe a un frame di dati R.

Sto inizializzando un data frame vuoto a 2 colonne, come segue.

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

Quindi, il mio obiettivo è scorrere un elenco di valori e, in ogni iterazione, aggiungere un valore alla fine dell'elenco. Ho iniziato con il codice seguente.

for (i in 1:10) {
    df$x = rbind(df$x, i)
    df$y = rbind(df$y, toString(i))
}

Ho anche cercato le funzioni c, appende mergesenza successo. Per favore fatemi sapere se avete suggerimenti.


2
Non presumo di sapere come R doveva essere utilizzato, ma volevo ignorare la riga di codice aggiuntiva che sarebbe stata richiesta per aggiornare gli indici ad ogni iterazione e non posso preallocare facilmente la dimensione del frame di dati perché non Non so quante righe ci vorranno alla fine. Ricorda che quanto sopra è solo un esempio di giocattolo pensato per essere riproducibile. Ad ogni modo, grazie per il tuo suggerimento!
Gyan Veda

Risposte:


115

Aggiornare

Non sapendo cosa stai cercando di fare, condividerò un altro suggerimento: prealloca i vettori del tipo che desideri per ogni colonna, inserisci i valori in quei vettori e poi, alla fine, crea il tuo data.frame.

Continuando con Julian's f3(un preallocato data.frame) come l'opzione più veloce finora, definita come:

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

Ecco un approccio simile, ma in cui data.frameviene creato come ultimo passaggio.

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

microbenchmarkdal pacchetto "microbenchmark" ci fornirà informazioni più complete rispetto a 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()(l'approccio seguente) è incredibilmente inefficiente a causa della frequenza con cui chiama data.framee perché la crescita di oggetti in questo modo è generalmente lenta in R. f3()è molto migliorata a causa della preallocazione, ma la data.framestruttura stessa potrebbe essere parte del collo di bottiglia qui. f4()cerca di aggirare quel collo di bottiglia senza compromettere l'approccio che desideri adottare.


Risposta originale

Questa non è davvero una buona idea, ma se vuoi farlo in questo modo, immagino che tu possa provare:

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

Nota che nel tuo codice c'è un altro problema:

  • Dovresti usare stringsAsFactorsse vuoi che i caratteri non vengano convertiti in fattori. Uso:df = data.frame(x = numeric(), y = character(), stringsAsFactors = FALSE)

6
Grazie! Questo risolve il mio problema. Perché questa "non è davvero una buona idea"? E in che modo xey sono mescolati nel ciclo for?
Gyan Veda

5
@ user2932774, È incredibilmente inefficiente far crescere un oggetto in questo modo in R. Un miglioramento (ma non necessariamente il modo migliore) sarebbe quello di preallocare un data.framedella dimensione finale che ti aspetti e aggiungere i valori con l' [estrazione / sostituzione.
A5C1D2H2I1M1N2O1R2T1

1
Grazie, Ananda. Normalmente vado con la preallocazione, ma non sono d'accordo sul fatto che questa non sia davvero una buona idea. Dipende dalla situazione. Nel mio caso, ho a che fare con piccoli dati e l'alternativa richiederà più tempo per il codice. Inoltre, questo è un codice più elegante rispetto a quello richiesto per aggiornare gli indici numerici per riempire le parti appropriate del frame di dati pre-allocato ad ogni iterazione. Solo curioso, qual è il "modo migliore" per svolgere questo compito secondo te? Avrei pensato che la preallocazione sarebbe stata la migliore.
Gyan Veda

2
@ user2932774, è fantastico. Apprezzo anche la tua prospettiva: praticamente non lavoro mai nemmeno con grandi set di dati. Detto questo, se ho intenzione di lavorare sulla scrittura di una funzione o qualcosa del genere, di solito spenderei un po 'di sforzo in più cercando di modificare il codice per ottenere velocità migliori quando possibile. Vedi il mio aggiornamento per un esempio di una differenza di velocità piuttosto enorme.
A5C1D2H2I1M1N2O1R2T1

1
Whoa, questa è un'enorme differenza! Grazie per aver eseguito la simulazione e per avermi insegnato il pacchetto microbenchmark. Sono assolutamente d'accordo con te sul fatto che è bello fare questo sforzo in più. Nel mio caso particolare, immagino che volessi solo qualcosa di veloce e sporco su un codice che potrei non dover mai più eseguire. :)
Gyan Veda

34

Confrontiamo le tre soluzioni proposte:

# 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 soluzione migliore è pre-allocare lo spazio (come previsto in R). La prossima soluzione migliore è quella di utilizzare list, e la soluzione peggiore (almeno in base a questi risultati temporali) sembra essere rbind.


Grazie! Anche se non sono d'accordo con il suggerimento di Ananda. Se voglio che i caratteri vengano convertiti in livelli di un fattore o meno dipenderà da ciò che voglio fare con l'output. Anche se immagino che con la soluzione che proponi, sia necessario impostare stringsAsFactors su FALSE.
Gyan Veda

Grazie per la simulazione. Mi rendo conto che la preallocazione è la migliore in termini di velocità di elaborazione, ma questo non è l'unico fattore che ho considerato nel prendere questa decisione di codifica.
Gyan Veda

1
In f1 hai confuso assegnando una stringa al vettore numerico x. La linea corretta è:df <- rbind(df, data.frame(x = i, y = toString(i)))
Eldar Agalarov

14

Supponi di non conoscere in anticipo la dimensione di data.frame. Può benissimo essere poche righe o pochi milioni. Devi avere una sorta di contenitore, che cresce dinamicamente. Prendendo in considerazione la mia esperienza e tutte le relative risposte in SO, vengo con 4 soluzioni distinte:

  1. rbindlist al data.frame

  2. Usa data.tableil setfunzionamento veloce di e abbinalo al raddoppio manuale del tavolo quando necessario.

  3. Usa RSQLitee aggiungi alla tabella tenuta in memoria.

  4. data.frameLa capacità di crescere e utilizzare un ambiente personalizzato (che ha semantica di riferimento) per memorizzare data.frame in modo che non venga copiato al ritorno.

Ecco un test di tutti i metodi per un numero piccolo e grande di righe aggiunte. Ogni metodo ha 3 funzioni ad esso associate:

  • create(first_element)che restituisce l'oggetto di supporto appropriato con first_elementput in.

  • append(object, element)che aggiunge il elementalla fine della tabella (rappresentato da object).

  • access(object)ottiene il data.framecon tutti gli elementi inseriti.

rbindlist al data.frame

Questo è abbastanza facile e diretto:

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 + raddoppiando manualmente il tavolo quando necessario.

Memorizzerò la lunghezza reale della tabella in un rowcountattributo.

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 dovrebbe essere ottimizzato per l'inserimento rapido dei record, quindi inizialmente avevo grandi speranze per una RSQLitesoluzione

Questo è fondamentalmente copia e incolla della risposta di Karsten W. su un thread simile.

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.frameil proprio ambiente di aggiunta di righe + personalizzato.

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 di test:

Per comodità userò una funzione di test per coprirli tutti con chiamate indirette. (Ho controllato: usare do.callinvece di chiamare direttamente le funzioni non rende il codice misurabile più a lungo).

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

Vediamo l'andamento per n = 10 inserimenti.

Ho anche aggiunto una funzione "placebo" (con suffisso 0) che non esegue nulla, solo per misurare il sovraccarico della configurazione del test.

r<-microbenchmark(test(0,n=10), test(1,n=10),test(2,n=10),test(3,n=10), test(4,n=10))
autoplot(r)

Tempi per l'aggiunta di n = 10 righe

Tempi per n = 100 righe Tempistiche per n = 1000 righe

Per le righe 1E5 (misurazioni effettuate su 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

Sembra che la soluzione basata su SQLite, sebbene riacquisti un po 'di velocità su dati di grandi dimensioni, non è neanche lontanamente vicina a data.table + crescita esponenziale manuale. La differenza è di quasi due ordini di grandezza!

Sommario

Se sai che aggiungerai un numero di righe piuttosto piccolo (n <= 100), vai avanti e utilizza la soluzione più semplice possibile: assegna semplicemente le righe a data.frame usando la notazione delle parentesi e ignora il fatto che data.frame è non pre-popolato.

Per tutto il resto usa data.table::sete fai crescere il data.table in modo esponenziale (ad esempio usando il mio codice).


2
Il motivo per cui SQLite è lento è che su ogni INSERT INTO, deve essere REINDICATO, che è O (n), dove n è il numero di righe. Ciò significa che inserire in un database SQL una riga alla volta è O (n ^ 2). SQLite può essere molto veloce, se inserisci un intero data.frame in una volta, ma non è il massimo per crescere riga per riga.
Julian Zucker

5

Aggiorna con purrr, tidyr e dplyr

Poiché la domanda è già datata (6 anni), nelle risposte manca una soluzione con i nuovi pacchetti tidyr e purrr. Quindi, per le persone che lavorano con questi pacchetti, voglio aggiungere una soluzione alle risposte precedenti, tutte piuttosto interessanti, soprattutto.

Il più grande vantaggio di purrr e tidyr sono una migliore leggibilità IMHO. purrr sostituisce lapply con la famiglia map () più flessibile, tidyr offre il metodo super intuitivo add_row - fa solo quello che dice :)

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

Questa soluzione è breve e intuitiva da leggere ed è relativamente veloce:

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

Scala quasi linearmente, quindi per le righe 1e5, le prestazioni sono:

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 

che lo renderebbe al secondo posto subito dopo data.table (se ignori il placebo) nel benchmark di @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

Non è necessario utilizzare add_row. Ad esempio: map_dfr(1:1e5, function(x) { tibble(x = x, y = toString(x)) }).
user3808394

@ user3808394 grazie, questa è un'alternativa interessante! se qualcuno vuole creare un dataframe da zero, il tuo è più corto, quindi la soluzione migliore. nel caso in cui tu abbia già un dataframe, la mia soluzione è ovviamente migliore.
Agile Bean

Se hai già un dataframe, lo faresti bind_rows(df, map_dfr(1:1e5, function(x) { tibble(x = x, y = toString(x)) }))invece di usare add_row.
user3808394

2

Prendiamo un 'punto' vettoriale che ha numeri da 1 a 5

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

se vogliamo aggiungere un numero 6 ovunque all'interno del vettore, il comando sottostante potrebbe tornare utile

i) Vettori

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

ii) colonne di una tabella

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

Il comando appendaccetta tre argomenti:

  1. il vettore / colonna da modificare.
  2. valore da includere nel vettore modificato.
  3. un pedice, dopo di che i valori devono essere aggiunti.

semplice...!! Mi scuso in caso di qualsiasi ...!


1

Una soluzione più generica per potrebbe essere la seguente.

    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 funzione extentDf () estende un data frame con n righe.

Come esempio:

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

0

La mia soluzione è quasi la stessa della risposta originale ma non ha funzionato per me.

Quindi, ho dato i nomi per le colonne e funziona:

painel <- rbind(painel, data.frame("col1" = xtweets$created_at,
                                   "col2" = xtweets$text))
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.