Ich habe mich auf StackOverflow umgesehen, aber ich kann keine Lösung für mein Problem finden, das das Anhängen von Zeilen an einen R-Datenrahmen beinhaltet.
Ich initialisiere einen leeren 2-spaltigen Datenrahmen, wie folgt.
df = data.frame(x = numeric(), y = character())
Mein Ziel ist es, eine Liste von Werten zu durchlaufen und bei jeder Iteration einen Wert an das Ende der Liste anzuhängen. Ich habe mit dem folgenden Code begonnen.
for (i in 1:10) {
df$x = rbind(df$x, i)
df$y = rbind(df$y, toString(i))
}
Ich habe auch die Funktionen c
, append
, und merge
ohne Erfolg ausprobiert. Bitte lassen Sie mich wissen, wenn Sie irgendwelche Vorschläge haben.
Da ich nicht weiß, was Sie vorhaben, möchte ich Ihnen noch einen Vorschlag machen: Weisen Sie Vektoren des gewünschten Typs für jede Spalte vor, fügen Sie Werte in diese Vektoren ein und erstellen Sie dann am Ende Ihr "data.frame".
Weiter mit Julian's f3
(ein vorab zugewiesener data.frame
) als die schnellste Option bisher, definiert als:
# 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
}
Hier ist ein ähnlicher Ansatz, bei dem das "data.frame" erst im letzten Schritt erstellt wird.
# 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" aus dem "microbenchmark" Paket wird uns einen umfassenderen Einblick geben als "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()
(der Ansatz unten) ist unglaublich ineffizient, weil es data.frame
so oft aufruft und weil das Wachsen von Objekten auf diese Weise in R generell langsam ist. f3()
ist wegen der Vorabzuweisung viel besser, aber die Struktur von data.frame
selbst könnte hier ein Teil des Engpasses sein. f4()
versucht, diesen Engpass zu umgehen, ohne den gewünschten Ansatz zu gefährden.
Das ist wirklich keine gute Idee, aber wenn Sie es auf diese Weise machen wollen, können Sie es wohl versuchen:
for (i in 1:10) {
df <- rbind(df, data.frame(x = i, y = toString(i)))
}
Beachten Sie, dass es in Ihrem Code ein weiteres Problem gibt:
stringsAsFactors
verwenden, wenn Sie wollen, dass die Zeichen nicht in Faktoren umgewandelt werden. Verwenden Sie: df = data.frame(x = numeric(), y = character(), stringsAsFactors = FALSE)
Lassen Sie uns die drei vorgeschlagenen Lösungen miteinander vergleichen:
# 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
Die beste Lösung ist die Vorab-Zuweisung von Speicherplatz (wie in R vorgesehen). Die nächstbeste Lösung ist die Verwendung von list
, und die schlechteste Lösung (zumindest basierend auf diesen Timing-Ergebnissen) scheint rbind
zu sein.
Angenommen, Sie kennen die Größe des Datenrahmens nicht im Voraus. Es können durchaus ein paar Zeilen sein, aber auch ein paar Millionen. Sie brauchen eine Art von Container, der dynamisch wächst. Unter Berücksichtigung meiner Erfahrung und aller Antworten in SO komme ich auf 4 verschiedene Lösungen:
rbindlist
in den data.frame
Nutzen Sie die schnelle "Set"-Operation von "data.table" und koppeln Sie sie bei Bedarf mit der manuellen Verdopplung der Tabelle.
**Verwenden Sie "RSQLite" und fügen Sie an die im Speicher gehaltene Tabelle an.
4. "data.frame": eigene Fähigkeit zu wachsen und benutzerdefinierte Umgebung (mit Referenzsemantik) verwenden, um das data.frame zu speichern, damit es bei der Rückkehr nicht kopiert wird.
Hier ist ein Test aller Methoden für eine kleine und eine große Anzahl von angehängten Zeilen. Jede Methode hat 3 Funktionen, die mit ihr verbunden sind:
create(first_element)
, die das entsprechende Backing-Objekt mit dem eingefügten first_element
zurückgibt.
append(object, element)
, das das Element
an das Ende der Tabelle anhängt (dargestellt durch object
).
access(object)
liefert den data.frame
mit allen eingefügten Elementen.
rbindlist
zum data.frameDas ist recht einfach und unkompliziert:
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
+ manuelles Verdoppeln der Tabelle bei Bedarf.Ich werde die wahre Länge der Tabelle in einem Attribut "rowcount" speichern.
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,]))
}
Dies ist im Grunde copy&paste von Karsten W. Antwort in einem ähnlichen Thread.
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 own row-appending + custom environment.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)
}
Der Einfachheit halber werde ich eine Testfunktion verwenden, um sie alle mit indirektem Aufruf abzudecken. (Ich habe es überprüft: die Verwendung von do.call
anstelle des direkten Aufrufs der Funktionen führt nicht dazu, dass der Code messbar länger läuft).
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)))
}
Schauen wir uns die Leistung für n=10 Einfügungen an.
Ich habe auch eine 'Placebo' Funktion (mit dem Suffix 0
) hinzugefügt, die nichts ausführt - nur um den Overhead des Testaufbaus zu messen.
r<-microbenchmark(test(0,n=10), test(1,n=10),test(2,n=10),test(3,n=10), test(4,n=10))
autoplot(r)
Für 1E5 Zeilen (Messungen auf 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
Es sieht so aus, als ob die SQLite-basierte Sulution, obwohl sie bei großen Daten eine gewisse Geschwindigkeit wiedererlangt, bei weitem nicht an das exponentielle Wachstum von data.table + manuell herankommt. Der Unterschied beträgt fast zwei Größenordnungen!
*Wenn Sie wissen, dass Sie nur eine kleine Anzahl von Zeilen anhängen werden (n<=100), verwenden Sie die einfachste Lösung: weisen Sie die Zeilen dem data.frame mit Hilfe der Klammerschreibweise zu und ignorieren Sie die Tatsache, dass das data.frame nicht vorausgefüllt ist.
Für alles andere verwenden Sie data.table::set
und lassen die data.table exponentiell wachsen (z.B. mit meinem Code).