So hängen Sie Zeilen an einen R-Datenrahmen an

121

Ich habe mich in StackOverflow umgesehen, kann jedoch keine spezifische Lösung für mein Problem finden, bei der Zeilen an einen R-Datenrahmen angehängt werden.

Ich initialisiere einen leeren 2-Spalten-Datenrahmen wie folgt.

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

Dann ist es mein Ziel, eine Werteliste zu durchlaufen und in 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 versucht , auch die Funktionen c, appendund mergeohne Erfolg. Bitte lassen Sie mich wissen, wenn Sie Vorschläge haben.

Gyan Veda
quelle
2
Ich nehme nicht an zu wissen, wie R verwendet werden sollte, aber ich wollte die zusätzliche Codezeile ignorieren, die erforderlich wäre, um die Indizes bei jeder Iteration zu aktualisieren, und ich kann die Größe des Datenrahmens nicht einfach vorab zuordnen, da ich dies nicht tue Ich weiß nicht, wie viele Zeilen es letztendlich dauern wird. Denken Sie daran, dass das Obige lediglich ein Spielzeugbeispiel ist, das reproduzierbar sein soll. Wie auch immer, danke für deinen Vorschlag!
Gyan Veda

Antworten:

115

Aktualisieren

Da ich nicht weiß, was Sie versuchen, möchte ich Ihnen noch einen Vorschlag unterbreiten: Ordnen Sie Vektoren des gewünschten Typs für jede Spalte vor, fügen Sie Werte in diese Vektoren ein und erstellen Sie am Ende Ihre data.frame .

Fortsetzung von Julians f3(einer vorab zugewiesenen data.frame) als bisher schnellste Option, 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, aber einer, bei dem data.frameder als letzter 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)
}

microbenchmarkAus dem "microbenchmark" -Paket erhalten wir umfassendere Einblicke 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 folgende Ansatz) ist unglaublich ineffizient, da es so oft aufgerufen wird data.frameund weil das Wachsen von Objekten auf diese Weise in R. im Allgemeinen langsam f3()ist. Dies wird aufgrund der Vorbelegung erheblich verbessert, aber die data.frameStruktur selbst könnte hier Teil des Engpasses sein. f4()versucht, diesen Engpass zu umgehen, ohne den gewünschten Ansatz zu beeinträchtigen.


Ursprüngliche Antwort

Das ist wirklich keine gute Idee, aber wenn Sie es so machen wollten, können Sie es 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:

  • Sie sollten verwenden, stringsAsFactorswenn die Zeichen nicht in Faktoren umgewandelt werden sollen. Verwenden:df = data.frame(x = numeric(), y = character(), stringsAsFactors = FALSE)
A5C1D2H2I1M1N2O1R2T1
quelle
6
Vielen Dank! Das löst mein Problem. Warum ist das "wirklich keine gute Idee"? Und wie werden x und y in der for-Schleife gemischt?
Gyan Veda
5
@ user2932774, Es ist unglaublich ineffizient, ein Objekt auf diese Weise in R zu vergrößern. Eine Verbesserung (aber immer noch nicht unbedingt die beste) wäre, eine data.frameder erwarteten Endgrößen zuzuweisen und die Werte beim [Extrahieren / Ersetzen hinzuzufügen .
A5C1D2H2I1M1N2O1R2T1
1
Danke, Ananda. Normalerweise gehe ich mit Vorbelegung, aber ich bin nicht der Meinung, dass dies wirklich keine gute Idee ist. Es hängt von der Situation ab. In meinem Fall habe ich es mit kleinen Daten zu tun und die Alternative ist zeitaufwändiger beim Codieren. Außerdem ist dies ein eleganterer Code als der, der zum Aktualisieren numerischer Indizes erforderlich ist, um die entsprechenden Teile des vorab zugewiesenen Datenrahmens bei jeder Iteration zu füllen. Nur neugierig, was ist Ihrer Meinung nach der "beste Weg", um diese Aufgabe zu erfüllen? Ich hätte gedacht, dass die Vorbelegung am besten gewesen wäre.
Gyan Veda
2
@ user2932774, es ist cool. Ich schätze auch Ihre Perspektive - ich arbeite auch so gut wie nie wirklich mit großen Datenmengen. Das heißt, wenn ich daran arbeiten möchte, eine Funktion oder etwas anderes zu schreiben, würde ich normalerweise ein wenig mehr Aufwand betreiben, um den Code zu optimieren, um nach Möglichkeit bessere Geschwindigkeiten zu erzielen. In meinem Update finden Sie ein Beispiel für einen ziemlich großen Geschwindigkeitsunterschied.
A5C1D2H2I1M1N2O1R2T1
1
Whoa, das ist ein großer Unterschied! Vielen Dank, dass Sie diese Simulation ausgeführt und mir das Microbenchmark-Paket beigebracht haben. Ich stimme Ihnen definitiv zu, dass es schön ist, diese zusätzlichen Anstrengungen zu unternehmen. In meinem speziellen Fall wollte ich wohl nur etwas schnelles und schmutziges an einem Code, den ich möglicherweise nie wieder ausführen muss. :)
Gyan Veda
34

Vergleichen wir die drei vorgeschlagenen Lösungen:

# 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 besteht darin, Speicherplatz vorab zuzuweisen (wie in R vorgesehen). Die nächstbeste Lösung ist die Verwendung list, und die schlechteste Lösung (zumindest basierend auf diesen Timing-Ergebnissen) scheint zu sein rbind.

Julián Urbano
quelle
Vielen Dank! Obwohl ich Anandas Vorschlag nicht zustimme. Ob die Zeichen in Ebenen eines Faktors konvertiert werden sollen oder nicht, hängt davon ab, was ich mit der Ausgabe tun möchte. Obwohl ich denke, dass es bei der von Ihnen vorgeschlagenen Lösung notwendig ist, stringAsFactors auf FALSE zu setzen.
Gyan Veda
Danke für die Simulation. Mir ist klar, dass die Vorbelegung in Bezug auf die Verarbeitungsgeschwindigkeit am besten ist, aber das ist nicht der einzige Faktor, den ich bei dieser Codierungsentscheidung berücksichtigt habe.
Gyan Veda
1
In f1 haben Sie verwirrt, indem Sie dem numerischen Vektor x eine Zeichenfolge zugewiesen haben. Richtige Zeile ist:df <- rbind(df, data.frame(x = i, y = toString(i)))
Eldar Agalarov
14

Angenommen, Sie kennen die Größe des data.frame einfach nicht im Voraus. Es können durchaus ein paar Zeilen oder ein paar Millionen sein. Sie benötigen eine Art Container, der dynamisch wächst. Unter Berücksichtigung meiner Erfahrung und aller damit verbundenen Antworten in SO komme ich mit 4 verschiedenen Lösungen:

  1. rbindlist zum data.frame

  2. Verwenden Sie data.tabledie schnelle setBedienung und koppeln Sie sie bei Bedarf manuell mit dem Verdoppeln des Tisches.

  3. Verwenden Sie RSQLitedie im Speicher befindliche Tabelle und hängen Sie sie an.

  4. data.frameDie eigene Fähigkeit zu wachsen und eine benutzerdefinierte Umgebung (mit Referenzsemantik) zum Speichern des data.frame zu verwenden, damit er bei der Rückgabe nicht kopiert wird.

Hier finden Sie einen Test aller Methoden für kleine und große Anzahl angehängter Zeilen. Jeder Methode sind 3 Funktionen zugeordnet:

  • create(first_element)das gibt das entsprechende Hintergrundobjekt mit first_elementput in zurück.

  • append(object, element)das hängt das elementan das Ende der Tabelle (dargestellt durch object).

  • access(object)bekommt das data.framemit allen eingefügten Elementen.

rbindlist zum data.frame

Das ist ganz 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 + Verdoppeln Sie den Tisch bei Bedarf manuell.

Ich werde die wahre Länge der Tabelle in einem rowcountAttribut 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,]))
}

SQL sollte für das schnelle Einfügen von Datensätzen optimiert werden, daher hatte ich anfangs große Hoffnungen RSQLite Lösung

Dies ist im Grunde ein Kopieren und Einfügen von Karsten W. Antwort auf einen ä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.frameeigene zeilenanhängende + benutzerdefinierte Umgebung.

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

Die Testsuite:

Der Einfachheit halber werde ich eine Testfunktion verwenden, um sie alle mit indirekten Aufrufen abzudecken. (Ich habe überprüft: do.callWenn Sie die Funktionen nicht direkt aufrufen, wird der Code nicht länger messbar.)

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

Sehen wir uns die Leistung für n = 10 Einfügungen an.

Ich habe auch eine 'Placebo'-Funktion (mit 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)

Timings zum Hinzufügen von n = 10 Zeilen

Timings für n = 100 Zeilen Timings für n = 1000 Zeilen

Für 1E5-Zeilen (Messungen mit Intel (R) Core (TM) i7-4710HQ-CPU bei 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

Es sieht so aus, als ob die SQLite-basierte Lösung, obwohl sie bei großen Datenmengen wieder an Geschwindigkeit gewinnt, bei weitem nicht in der Nähe von data.table + manuellem exponentiellem Wachstum liegt. Der Unterschied beträgt fast zwei Größenordnungen!

Zusammenfassung

Wenn Sie wissen, dass Sie eine relativ kleine Anzahl von Zeilen anhängen (n <= 100), verwenden Sie die einfachste mögliche Lösung: Weisen Sie die Zeilen einfach dem data.frame in Klammernotation zu und ignorieren Sie die Tatsache, dass es sich um den data.frame handelt nicht vorbestellt.

Für alles andere verwenden data.table::setund erweitern Sie die data.table exponentiell (z. B. mit meinem Code).

Adam Ryczkowski
quelle
2
Der Grund, warum SQLite langsam ist, ist, dass es bei jedem INSERT INTO REINDEX muss, was O (n) ist, wobei n die Anzahl der Zeilen ist. Dies bedeutet, dass das zeilenweise Einfügen in eine SQL-Datenbank O (n ^ 2) ist. SQLite kann sehr schnell sein, wenn Sie eine ganze Datei data.frame auf einmal einfügen, aber es ist nicht die beste Methode, um Zeile für Zeile zu wachsen.
Julian Zucker
5

Update mit purrr, tidyr & dplyr

Da die Frage bereits datiert ist (6 Jahre), fehlt den Antworten eine Lösung mit neueren Paketen tidyr und purrr. Für Leute, die mit diesen Paketen arbeiten, möchte ich eine Lösung zu den vorherigen Antworten hinzufügen - alles sehr interessant, besonders.

Der größte Vorteil von Purrr und Tidyr ist meiner Meinung nach eine bessere Lesbarkeit. purrr ersetzt lapply durch die flexiblere map () -Familie, tidyr bietet die super-intuitive Methode add_row - macht einfach das, was es sagt :)

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

Diese Lösung ist kurz und intuitiv zu lesen und relativ schnell:

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

Es skaliert fast linear, sodass die Leistung für 1e5-Zeilen wie folgt lautet:

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 

Dies würde es direkt nach data.table (wenn Sie das Placebo ignorieren) im Benchmark von @Adam Ryczkowski auf den zweiten Platz bringen:

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
Agile Bohne
quelle
Sie müssen nicht verwenden add_row. Zum Beispiel : map_dfr(1:1e5, function(x) { tibble(x = x, y = toString(x)) }).
user3808394
@ user3808394 danke, das ist eine interessante alternative! Wenn jemand einen Datenrahmen von Grund auf neu erstellen möchte, ist Ihr Datenrahmen kürzer und somit die bessere Lösung. Falls Sie bereits einen Datenrahmen haben, ist meine Lösung natürlich besser.
Agile Bean
Wenn Sie bereits einen Datenrahmen haben, würden Sie dies bind_rows(df, map_dfr(1:1e5, function(x) { tibble(x = x, y = toString(x)) }))anstelle von verwenden add_row.
user3808394
2

Nehmen wir einen Vektorpunkt mit Zahlen von 1 bis 5

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

Wenn wir eine Zahl 6 irgendwo im Vektor anhängen möchten, kann der folgende Befehl nützlich sein

i) Vektoren

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

ii) Spalten einer Tabelle

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

Der Befehl appendakzeptiert drei Argumente:

  1. der zu ändernde Vektor / die zu ändernde Spalte.
  2. Wert, der in den modifizierten Vektor aufgenommen werden soll.
  3. ein Index, nach dem die Werte angehängt werden sollen.

einfach...!! Entschuldigung für den Fall, dass ...!

Praneeth Krishna
quelle
1

Eine allgemeinere Lösung für könnte die folgende sein.

    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
}

Die Funktion verlängernDf () erweitert einen Datenrahmen mit n Zeilen.

Als Beispiel:

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
quelle
0

Meine Lösung ist fast die gleiche wie die ursprüngliche Antwort, aber sie hat bei mir nicht funktioniert.

Also habe ich Namen für die Spalten gegeben und es funktioniert:

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