Ho cercato su StackOverflow, ma non riesco a trovare una soluzione specifica per il mio problema, che comporta l'aggiunta di righe a un frame di dati R.
Sto inizializzando un frame di dati vuoto a 2 colonne, come segue.
df = data.frame(x = numeric(), y = character())
Poi, il mio obiettivo è quello di iterare attraverso una lista di valori e, in ogni iterazione, aggiungere un valore alla fine della lista. Ho iniziato con il seguente codice.
for (i in 1:10) {
df$x = rbind(df$x, i)
df$y = rbind(df$y, toString(i))
}
Ho anche provato le funzioni c
, append
e merge
senza successo. Per favore fatemi sapere se avete qualche suggerimento.
Non sapendo cosa stai cercando di fare, condivido un altro suggerimento: Prealloca dei vettori del tipo che vuoi per ogni colonna, inserisci dei valori in questi vettori e poi, alla fine, crea il tuo data.frame
.
Continuando con Julian f3
(un data.frame
preallocato) come l'opzione più veloce finora, definito 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 uno in cui il data.frame
viene creato come ultimo passo.
# 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
dal pacchetto "microbenchmark" ci darà una visione più completa di 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 qui sotto) è incredibilmente inefficiente a causa della frequenza con cui chiama data.frame
e perché la crescita degli oggetti in questo modo è generalmente lenta in R. f3()
è molto migliorata grazie alla preallocazione, ma la stessa struttura data.frame
potrebbe essere parte del collo di bottiglia. f4()
cerca di bypassare questo collo di bottiglia senza compromettere l'approccio che volete adottare.
Questa non è davvero una buona idea, ma se volevi farlo in questo modo, credo 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:
stringsAsFactors
se vuoi che i caratteri non vengano convertiti in fattori. Usa: df = data.frame(x = numeric(), y = character(), stringsAsFactors = FALSE)
Mettiamo a confronto 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 è preallocare lo spazio (come previsto in R). La soluzione successiva migliore è usare list
, e la soluzione peggiore (almeno in base a questi risultati sui tempi) sembra essere rbind
.
Supponiamo che semplicemente non si conosca in anticipo la dimensione del data.frame. Può benissimo essere qualche riga, o qualche milione. È necessario avere una sorta di contenitore che cresca dinamicamente. Prendendo in considerazione la mia esperienza e tutte le risposte relative in SO ho trovato 4 soluzioni distinte:
rbindlist
al data.frame
Utilizzare la veloce operazione data.table
'e accoppiarla con il raddoppio manuale della tabella quando necessario.
**Usare RSQLite
e aggiungere alla tabella tenuta in memoria.
data.frame
di crescere e utilizzare l'ambiente personalizzato (che ha una semantica di riferimento) per memorizzare il data.frame in modo che non venga copiato al ritorno.Ecco un test di tutti i metodi sia per un piccolo che per un grande numero di righe aggiunte. Ogni metodo ha 3 funzioni associate ad esso:
create(first_element)
che restituisce l'oggetto di supporto appropriato con first_element
inserito.
append(object, element)
che aggiunge l' elemento
alla fine della tabella (rappresentata da object
).
access(object)
ottiene il data.frame
con tutti gli elementi inseriti.
rbindlist
al data.frameQuesto è 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
+ raddoppiare manualmente la tabella quando necessario.Memorizzerò la vera lunghezza della tabella in un attributo 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,]))
}
RSQLite
.Questo è fondamentalmente copia&incolla di Karsten W. risposta su 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.frame
's proprio row-appending + ambiente 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)
}
Per comodità userò una sola funzione di test per coprirli tutti con la chiamata indiretta. (Ho controllato: usare do.call
invece di chiamare direttamente le funzioni non rende il codice più 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 le prestazioni per n=10 inserimenti.
Ho anche aggiunto un 'placebo'funzioni (con suffisso 0
) che non eseguono nulla - solo per misurare l'overhead 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)
Per 1E5 righe (misurazioni effettuate su Intel(R) Core(TM) i7-4710HQ CPU @ 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
Sembra che la soluzione basata su SQLite, anche se recupera un po 'di velocità su grandi dati, non è da nessuna parte vicino a data.table + crescita esponenziale manuale. La differenza è quasi due ordini di grandezza!
Se sapete che aggiungerete un numero piuttosto piccolo di righe (n<=100), andate avanti e usate la soluzione più semplice possibile: assegnate semplicemente le righe al data.frame usando la notazione a parentesi e ignorate il fatto che il data.frame non è pre-popolato.
Per tutto il resto usate data.table::set
e fate crescere la data.table in modo esponenziale (ad esempio usando il mio codice).