Beschleunigen Sie den Schleifenbetrieb in R.


193

Ich habe ein großes Leistungsproblem in R. Ich habe eine Funktion geschrieben, die über ein data.frameObjekt iteriert . Es fügt einfach eine neue Spalte zu a hinzu data.frameund sammelt etwas an. (einfache Bedienung). Der data.framehat ungefähr 850K Zeilen. Mein PC funktioniert noch (ungefähr 10 Stunden) und ich habe keine Ahnung von der Laufzeit.

dayloop2 <- function(temp){
    for (i in 1:nrow(temp)){    
        temp[i,10] <- i
        if (i > 1) {             
            if ((temp[i,6] == temp[i-1,6]) & (temp[i,3] == temp[i-1,3])) { 
                temp[i,10] <- temp[i,9] + temp[i-1,10]                    
            } else {
                temp[i,10] <- temp[i,9]                                    
            }
        } else {
            temp[i,10] <- temp[i,9]
        }
    }
    names(temp)[names(temp) == "V10"] <- "Kumm."
    return(temp)
}

Irgendwelche Ideen, wie man diesen Vorgang beschleunigt?

Antworten:


433

Das größte Problem und die Wurzel der Ineffektivität ist die Indizierung von data.frame. Ich meine all diese Zeilen, in denen Sie sie verwenden temp[,].
Versuchen Sie dies so weit wie möglich zu vermeiden. Ich habe Ihre Funktion übernommen, die Indizierung geändert und hier version_A

dayloop2_A <- function(temp){
    res <- numeric(nrow(temp))
    for (i in 1:nrow(temp)){    
        res[i] <- i
        if (i > 1) {             
            if ((temp[i,6] == temp[i-1,6]) & (temp[i,3] == temp[i-1,3])) { 
                res[i] <- temp[i,9] + res[i-1]                   
            } else {
                res[i] <- temp[i,9]                                    
            }
        } else {
            res[i] <- temp[i,9]
        }
    }
    temp$`Kumm.` <- res
    return(temp)
}

Wie Sie sehen können, erstelle ich einen Vektor, resder Ergebnisse sammelt. Am Ende füge ich es hinzu data.frameund ich muss mich nicht mit Namen anlegen. Wie ist es besser?

Ich führe jede Funktion für data.framemit nrow1.000 bis 10.000 mal 1.000 aus und messe die Zeit mitsystem.time

X <- as.data.frame(matrix(sample(1:10, n*9, TRUE), n, 9))
system.time(dayloop2(X))

Ergebnis ist

Performance

Sie können sehen, dass Ihre Version exponentiell von abhängt nrow(X). Die modifizierte Version hat eine lineare Beziehung, und ein einfaches lmModell sagt voraus, dass die Berechnung für 850.000 Zeilen 6 Minuten und 10 Sekunden dauert.

Kraft der Vektorisierung

Wie Shane und Calimo in ihren Antworten angeben, ist die Vektorisierung ein Schlüssel zu einer besseren Leistung. Von Ihrem Code aus können Sie sich außerhalb der Schleife bewegen:

  • Konditionierung
  • Initialisierung der Ergebnisse (welche sind temp[i,9])

Dies führt zu diesem Code

dayloop2_B <- function(temp){
    cond <- c(FALSE, (temp[-nrow(temp),6] == temp[-1,6]) & (temp[-nrow(temp),3] == temp[-1,3]))
    res <- temp[,9]
    for (i in 1:nrow(temp)) {
        if (cond[i]) res[i] <- temp[i,9] + res[i-1]
    }
    temp$`Kumm.` <- res
    return(temp)
}

Vergleichen Sie das Ergebnis für diese Funktionen, diesmal für nrow10.000 bis 100.000 mal 10.000.

Performance

Tuning das abgestimmte

Eine weitere Optimierung besteht darin, eine Schleifenindizierung temp[i,9]auf zu ändern res[i](die in der i-ten Schleifeniteration genau gleich ist). Es ist wieder ein Unterschied zwischen der Indizierung eines Vektors und der Indizierung eines data.frame.
Zweitens: Wenn Sie sich die Schleife ansehen, sehen Sie, dass keine Schleife über alle Schleifen erforderlich ist i, sondern nur für diejenigen, die dem Zustand entsprechen.
Auf geht's

dayloop2_D <- function(temp){
    cond <- c(FALSE, (temp[-nrow(temp),6] == temp[-1,6]) & (temp[-nrow(temp),3] == temp[-1,3]))
    res <- temp[,9]
    for (i in (1:nrow(temp))[cond]) {
        res[i] <- res[i] + res[i-1]
    }
    temp$`Kumm.` <- res
    return(temp)
}

Die Leistung, die Sie in hohem Maße erzielen, hängt von einer Datenstruktur ab. Genau - auf Prozent der TRUEWerte im Zustand. Für meine simulierten Daten dauert die Rechenzeit 850.000 Zeilen unter einer Sekunde.

Performance

Wenn du willst, dass du weiter gehen kannst, sehe ich mindestens zwei Dinge, die getan werden können:

  • Schreiben Sie einen CCode, um bedingte Cumsum zu machen
  • Wenn Sie wissen, dass in Ihrer Datenmaximalsequenz nicht groß ist, können Sie die Schleife in "vektorisiert" ändern, während

    while (any(cond)) {
        indx <- c(FALSE, cond[-1] & !cond[-n])
        res[indx] <- res[indx] + res[which(indx)-1]
        cond[indx] <- FALSE
    }

Der für Simulationen und Abbildungen verwendete Code ist auf GitHub verfügbar .


2
Da ich keinen Weg finden kann, Marek privat zu fragen, wie wurden diese Grafiken erstellt?
carbontwelve

@carbontwelve Fragen Sie nach Daten oder Plots? Die Diagramme wurden mit einem Gitterpaket erstellt. Wenn ich Zeit habe, stelle ich den Code irgendwo ins Internet und gebe Ihnen Bescheid.
Marek

@carbontwelve Hoppla, ich habe mich geirrt :) Dies sind Standard-Plots (von Basis R).
Marek

@ Gregor Leider nicht. Es ist kumulativ, sodass Sie es nicht vektorisieren können. Einfaches Beispiel: res = c(1,2,3,4)und condist alles TRUE, dann sollte das Endergebnis sein : 1, 3(Ursache 1+2), 6(Ursache zweite ist jetzt 3und dritte ist 3auch), 10( 6+4). Doing einfache Summierung du hast 1, 3, 5, 7.
Marek

Ah, ich hätte es mir genauer überlegen sollen. Danke, dass du mir den Fehler gezeigt hast.
Gregor Thomas

132

Allgemeine Strategien zur Beschleunigung des R-Codes

Stellen Sie zunächst fest, wo sich der langsame Teil wirklich befindet. Code, der nicht langsam ausgeführt wird, muss nicht optimiert werden. Bei kleinen Codemengen kann es funktionieren, einfach darüber nachzudenken. Wenn dies fehlschlägt, können RProf und ähnliche Profiling-Tools hilfreich sein.

Wenn Sie den Engpass herausgefunden haben, überlegen Sie sich effizientere Algorithmen, um das zu tun, was Sie wollen. Berechnungen sollten nach Möglichkeit nur einmal ausgeführt werden.

  • Speichern Sie die Ergebnisse und greifen Sie darauf zu, anstatt sie wiederholt neu zu berechnen
  • Nehmen Sie nicht schleifenabhängige Berechnungen aus Schleifen heraus
  • Vermeiden Sie Berechnungen, die nicht erforderlich sind (z. B. verwenden Sie keine regulären Ausdrücke mit festen Suchvorgängen )

Mit mehr effizienten Funktionen kann zu moderaten oder großen Geschwindigkeitsgewinnen führen. paste0Erzeugt zum Beispiel einen kleinen Effizienzgewinn, aber .colSums()seine Verwandten produzieren etwas ausgeprägtere Gewinne. meanist besonders langsam .

Dann können Sie einige besonders häufige Probleme vermeiden :

  • cbind wird dich sehr schnell verlangsamen.
  • Initialisieren Sie Ihre Datenstrukturen und füllen Sie sie aus. anstatt sie jedes Mal zu erweitern .
  • Selbst bei einer Vorabzuweisung können Sie eher zu einem Pass-by-Reference-Ansatz als zu einem Pass-by-Value-Ansatz wechseln, aber der Aufwand lohnt sich möglicherweise nicht.
  • Schauen Sie sich das R Inferno an um weitere Fallstricke zu vermeiden.

Versuchen Sie eine bessere Vektorisierung , die oft, aber nicht immer helfen kann. In dieser Hinsicht bieten inhärent vektorisierte Befehle wie ifelse, diffund dergleichen mehr Verbesserungen als die applyBefehlsfamilie (die über eine gut geschriebene Schleife nur einen geringen bis keinen Geschwindigkeitsschub bietet).

Sie können auch versuchen , R-Funktionen weitere Informationen bereitzustellen . Verwenden Sie beispielsweise vapplyanstelle vonsapply und geben Sie an, colClasseswenn Sie textbasierte Daten einlesen . Die Geschwindigkeitsgewinne variieren je nachdem, wie viele Vermutungen Sie eliminieren.

Betrachten Sie als nächstes optimierte Pakete : Das data.tablePaket kann massive Geschwindigkeitsgewinne erzielen, wenn seine Verwendung möglich ist, bei der Datenmanipulation und beim Lesen großer Datenmengen ( fread).

Versuchen Sie als nächstes, die Geschwindigkeit durch effizientere Mittel zum Aufrufen von R zu steigern :

  • Kompilieren Sie Ihr R-Skript. Oder nutzen Sie dieRa und jitPakete im Konzert für die Just-in-Time - Kompilierung (Dirk hat ein Beispiel in dieser Präsentation ).
  • Stellen Sie sicher, dass Sie ein optimiertes BLAS verwenden. Diese sorgen für allgemeine Geschwindigkeitsgewinne. Ehrlich gesagt ist es eine Schande, dass R bei der Installation nicht automatisch die effizienteste Bibliothek verwendet. Hoffentlich wird Revolution R die Arbeit, die sie hier geleistet haben, wieder in die gesamte Community einbringen.
  • Radford Neal hat eine Reihe von Optimierungen vorgenommen, von denen einige in R Core übernommen wurden und viele andere in R Core umgewandelt wurden pqR übernommen wurden .

Und wenn Sie mit all dem noch nicht so schnell sind, wie Sie es benötigen, müssen Sie möglicherweise für das langsame Code-Snippet zu einer schnelleren Sprache wechseln . Die Kombination von Rcppundinline hier macht das Ersetzen nur des langsamsten Teils des Algorithmus durch C ++ - Code besonders einfach. Hier ist zum Beispiel mein erster Versuch, dies zu tun , und es werden sogar hochoptimierte R-Lösungen weggeblasen.

Wenn Sie nach all dem immer noch Probleme haben, brauchen Sie nur mehr Rechenleistung. Untersuchen Sie Parallelisierung ( http://cran.r-project.org/web/views/HighPerformanceComputing.html ) oder sogar GPU-basierte Lösungen (gpu-tools ) an.

Links zu anderen Anleitungen


35

Wenn Sie forSchleifen verwenden, codieren Sie R höchstwahrscheinlich so, als wäre es C oder Java oder etwas anderes. R-Code, der richtig vektorisiert ist, ist extrem schnell.

Nehmen Sie zum Beispiel diese zwei einfachen Codebits, um eine Liste von 10.000 Ganzzahlen nacheinander zu generieren:

Das erste Codebeispiel ist, wie man eine Schleife unter Verwendung eines traditionellen Codierungsparadigmas codiert. Der Vorgang dauert 28 Sekunden

system.time({
    a <- NULL
    for(i in 1:1e5)a[i] <- i
})
   user  system elapsed 
  28.36    0.07   28.61 

Sie können eine fast 100-fache Verbesserung erzielen, indem Sie einfach Speicher vorab zuweisen:

system.time({
    a <- rep(1, 1e5)
    for(i in 1:1e5)a[i] <- i
})

   user  system elapsed 
   0.30    0.00    0.29 

Bei Verwendung der Basis-R-Vektoroperation unter Verwendung des Doppelpunktoperators erfolgt :diese Operation jedoch praktisch augenblicklich:

system.time(a <- 1:1e5)

   user  system elapsed 
      0       0       0 

+1, obwohl ich Ihr zweites Beispiel als nicht überzeugend betrachten würde, da a[i]sich nichts ändert. Hat system.time({a <- NULL; for(i in 1:1e5){a[i] <- 2*i} }); system.time({a <- 1:1e5; for(i in 1:1e5){a[i] <- 2*i} }); system.time({a <- NULL; a <- 2*(1:1e5)})aber ein ähnliches Ergebnis.
Henry

@ Henry, fairer Kommentar, aber wie Sie betonen, sind die Ergebnisse die gleichen. Ich habe das Beispiel geändert, um a to zu initialisieren rep(1, 1e5)- die Timings sind identisch.
Andrie

17

Dies könnte viel schneller gemacht werden, indem die Schleifen mithilfe von Indizes oder verschachtelten ifelse()Anweisungen übersprungen werden .

idx <- 1:nrow(temp)
temp[,10] <- idx
idx1 <- c(FALSE, (temp[-nrow(temp),6] == temp[-1,6]) & (temp[-nrow(temp),3] == temp[-1,3]))
temp[idx1,10] <- temp[idx1,9] + temp[which(idx1)-1,10] 
temp[!idx1,10] <- temp[!idx1,9]    
temp[1,10] <- temp[1,9]
names(temp)[names(temp) == "V10"] <- "Kumm."

Danke für die Antwort. Ich versuche Ihre Aussagen zu verstehen. Die Zeile 4: "temp [idx1,10] <- temp [idx1,9] + temp [welches (idx1) -1,10]" verursachte einen Fehler, da die Länge eines längeren Objekts nicht ein Vielfaches der Länge des Objekts ist kürzeres Objekt. "temp [idx1,9] = num [1: 11496]" und "temp [which (idx1) -1,10] = int [1: 11494]", sodass 2 Zeilen fehlen.
Kay

Wenn Sie ein Datenbeispiel bereitstellen (verwenden Sie dput () mit einigen Zeilen), werde ich es für Sie reparieren. Aufgrund des welchen () - 1 Bits sind die Indizes ungleich. Aber Sie sollten von hier aus sehen, wie es funktioniert: Es ist keine Schleife oder Anwendung erforderlich. Verwenden Sie einfach vektorisierte Funktionen.
Shane

1
Beeindruckend! Ich habe gerade einen verschachtelten if..else-Funktionsblock und Mapply in eine verschachtelte ifelse-Funktion geändert und eine 200-fache Beschleunigung erhalten!
James

Ihr allgemeiner Rat ist richtig, aber in Code, den Sie übersehen haben, ihängt dieser -th-Wert von -th ab, i-1sodass sie nicht so festgelegt werden können, wie Sie es tun (mit which()-1).
Marek

8

Ich mag es nicht, Code neu zu schreiben ... Natürlich sind auch ifelse und lapply bessere Optionen, aber manchmal ist es schwierig, dies anzupassen.

Häufig verwende ich data.frames wie Listen wie df$var[i]

Hier ist ein erfundenes Beispiel:

nrow=function(x){ ##required as I use nrow at times.
  if(class(x)=='list') {
    length(x[[names(x)[1]]])
  }else{
    base::nrow(x)
  }
}

system.time({
  d=data.frame(seq=1:10000,r=rnorm(10000))
  d$foo=d$r
  d$seq=1:5
  mark=NA
  for(i in 1:nrow(d)){
    if(d$seq[i]==1) mark=d$r[i]
    d$foo[i]=mark
  }
})

system.time({
  d=data.frame(seq=1:10000,r=rnorm(10000))
  d$foo=d$r
  d$seq=1:5
  d=as.list(d) #become a list
  mark=NA
  for(i in 1:nrow(d)){
    if(d$seq[i]==1) mark=d$r[i]
    d$foo[i]=mark
  }
  d=as.data.frame(d) #revert back to data.frame
})

data.frame version:

   user  system elapsed 
   0.53    0.00    0.53

Listenversion:

   user  system elapsed 
   0.04    0.00    0.03 

17x mal schneller, um eine Liste von Vektoren als einen data.frame zu verwenden.

Irgendwelche Kommentare dazu, warum intern data.frames in dieser Hinsicht so langsam sind? Man könnte denken, sie funktionieren wie Listen ...

Für noch schnelleren Code tun Sie dies class(d)='list'anstelle von d=as.list(d)undclass(d)='data.frame'

system.time({
  d=data.frame(seq=1:10000,r=rnorm(10000))
  d$foo=d$r
  d$seq=1:5
  class(d)='list'
  mark=NA
  for(i in 1:nrow(d)){
    if(d$seq[i]==1) mark=d$r[i]
    d$foo[i]=mark
  }
  class(d)='data.frame'
})
head(d)

1
Dies ist wahrscheinlich dem Overhead von zu verdanken, der [<-.data.frameirgendwie aufgerufen wird, wenn Sie dies tun, d$foo[i] = markund möglicherweise bei jeder <-Änderung eine neue Kopie des Vektors des möglicherweise gesamten data.frame erstellt . Es wäre eine interessante Frage zu SO.
Frank

2
@Frank (i) muss sicherstellen, dass das geänderte Objekt noch ein gültiger data.frame ist, und (ii) afaik erstellt mindestens eine Kopie, möglicherweise mehr als eine. Die Unterzuweisung von Datenrahmen ist bekanntermaßen langsam, und wenn Sie sich den langen Quellcode ansehen, ist dies nicht wirklich überraschend.
Roland

@Frank, @Roland: Durchläuft die df$var[i]Notation dieselbe [<-.data.frameFunktion? Mir ist aufgefallen, dass es in der Tat ziemlich lang ist. Wenn nicht, welche Funktion wird verwendet?
Chris

@ Chris wird meiner Meinung d$foo[i]=marknach grob übersetzt d <- `$<-`(d, 'foo', `[<-`(d$foo, i, mark)), aber mit einigen temporären Variablen.
Tim Goodman

7

Als Ari am Ende seiner Antwort erwähnt, das Rcppund inlinemachen Pakete es unglaublich einfach , schnell Dinge zu machen. Versuchen Sie als Beispiel diesen inlineCode (Warnung: nicht getestet):

body <- 'Rcpp::NumericMatrix nm(temp);
         int nrtemp = Rccp::as<int>(nrt);
         for (int i = 0; i < nrtemp; ++i) {
             temp(i, 9) = i
             if (i > 1) {
                 if ((temp(i, 5) == temp(i - 1, 5) && temp(i, 2) == temp(i - 1, 2) {
                     temp(i, 9) = temp(i, 8) + temp(i - 1, 9)
                 } else {
                     temp(i, 9) = temp(i, 8)
                 }
             } else {
                 temp(i, 9) = temp(i, 8)
             }
         return Rcpp::wrap(nm);
        '

settings <- getPlugin("Rcpp")
# settings$env$PKG_CXXFLAGS <- paste("-I", getwd(), sep="") if you want to inc files in wd
dayloop <- cxxfunction(signature(nrt="numeric", temp="numeric"), body-body,
    plugin="Rcpp", settings=settings, cppargs="-I/usr/include")

dayloop2 <- function(temp) {
    # extract a numeric matrix from temp, put it in tmp
    nc <- ncol(temp)
    nm <- dayloop(nc, temp)
    names(temp)[names(temp) == "V10"] <- "Kumm."
    return(temp)
}

Es gibt ein ähnliches Verfahren für #includeDinge, bei denen Sie nur einen Parameter übergeben

inc <- '#include <header.h>

zu cxxfunction, as include=inc . Das wirklich Coole daran ist, dass es die gesamte Verknüpfung und Kompilierung für Sie übernimmt, sodass das Prototyping sehr schnell ist.

Haftungsausschluss: Ich bin nicht ganz sicher, ob die Klasse von tmp eine numerische und keine numerische Matrix oder etwas anderes sein sollte. Aber ich bin mir größtenteils sicher.

Bearbeiten: Wenn Sie danach noch mehr Geschwindigkeit benötigen, ist OpenMP eine Parallelisierungsfunktion C++. Ich habe nicht versucht, es von zu verwenden inline, aber es sollte funktionieren. Die Idee wäre, im Fall von nKernen eine Schleifeniteration von kdurchführen zu lassen k % n. Eine passende Einführung finden Sie in Matloffs The Art of R Programming , das hier verfügbar ist in Kapitel 16, Rückgriff auf C .


3

Die Antworten hier sind großartig. Ein kleiner Aspekt, der nicht behandelt wird, ist, dass die Frage lautet: " Mein PC funktioniert noch (ca. 10 Stunden) und ich habe keine Ahnung von der Laufzeit ". Ich habe bei der Entwicklung immer den folgenden Code in Schleifen eingefügt, um ein Gefühl dafür zu bekommen, wie sich Änderungen auf die Geschwindigkeit auswirken, und um zu überwachen, wie lange es dauern wird, bis der Vorgang abgeschlossen ist.

dayloop2 <- function(temp){
  for (i in 1:nrow(temp)){
    cat(round(i/nrow(temp)*100,2),"%    \r") # prints the percentage complete in realtime.
    # do stuff
  }
  return(blah)
}

Funktioniert auch mit lapply.

dayloop2 <- function(temp){
  temp <- lapply(1:nrow(temp), function(i) {
    cat(round(i/nrow(temp)*100,2),"%    \r")
    #do stuff
  })
  return(temp)
}

Wenn die Funktion innerhalb der Schleife recht schnell ist, die Anzahl der Schleifen jedoch groß ist, sollten Sie nur gelegentlich drucken, da das Drucken auf die Konsole selbst einen Overhead hat. z.B

dayloop2 <- function(temp){
  for (i in 1:nrow(temp)){
    if(i %% 100 == 0) cat(round(i/nrow(temp)*100,2),"%    \r") # prints every 100 times through the loop
    # do stuff
  }
  return(temp)
}

Eine ähnliche Option, drucken Sie den Bruch i / n. Ich habe immer so etwas wie, cat(sprintf("\nNow running... %40s, %s/%s \n", nm[i], i, n))da ich normalerweise über benannte Dinge (mit Namen in nm) schleife .
Frank

2

In R können Sie die Schleifenverarbeitung häufig mithilfe der applyFamilienfunktionen beschleunigen (in Ihrem Fall wahrscheinlich replicate). Schauen Sie sich das anplyr Paket an, das Fortschrittsbalken enthält.

Eine andere Möglichkeit besteht darin, Schleifen vollständig zu vermeiden und durch vektorisierte Arithmetik zu ersetzen. Ich bin mir nicht sicher, was Sie genau tun, aber Sie können Ihre Funktion wahrscheinlich auf alle Zeilen gleichzeitig anwenden:

temp[1:nrow(temp), 10] <- temp[1:nrow(temp), 9] + temp[0:(nrow(temp)-1), 10]

Dies wird viel schneller sein, und dann können Sie die Zeilen mit Ihrer Bedingung filtern:

cond.i <- (temp[i, 6] == temp[i-1, 6]) & (temp[i, 3] == temp[i-1, 3])
temp[cond.i, 10] <- temp[cond.i, 9]

Vektorisierte Arithmetik erfordert mehr Zeit und das Nachdenken über das Problem, aber dann können Sie manchmal mehrere Größenordnungen in der Ausführungszeit sparen.


14
Sie wissen genau, dass Vektorfunktionen schneller als Schleifen oder apply () sind, aber es ist nicht wahr, dass apply () schneller als Schleifen ist. In vielen Fällen abstrahiert apply () einfach die Schleife vom Benutzer weg, führt aber immer noch eine Schleife durch. Siehe diese vorherige Frage: stackoverflow.com/questions/2275896/…
JD Long

0

Die Verarbeitung mit data.tableist eine praktikable Option:

n <- 1000000
df <- as.data.frame(matrix(sample(1:10, n*9, TRUE), n, 9))
colnames(df) <- paste("col", 1:9, sep = "")

library(data.table)

dayloop2.dt <- function(df) {
  dt <- data.table(df)
  dt[, Kumm. := {
    res <- .I;
    ifelse (res > 1,             
      ifelse ((col6 == shift(col6, fill = 0)) & (col3 == shift(col3, fill = 0)) , 
        res <- col9 + shift(res)                   
      , # else
        res <- col9                                 
      )
     , # else
      res <- col9
    )
  }
  ,]
  res <- data.frame(dt)
  return (res)
}

res <- dayloop2.dt(df)

m <- microbenchmark(dayloop2.dt(df), times = 100)
#Unit: milliseconds
#       expr      min        lq     mean   median       uq      max neval
#dayloop2.dt(df) 436.4467 441.02076 578.7126 503.9874 575.9534 966.1042    10

Wenn Sie die möglichen Vorteile der Zustandsfilterung ignorieren, ist dies sehr schnell. Wenn Sie die Berechnung für die Teilmenge der Daten durchführen können, ist dies natürlich hilfreich.


2
Warum wiederholen Sie den Vorschlag, data.table zu verwenden? Es wurde bereits mehrfach in den früheren Antworten gemacht.
IRTFM
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.