569 Stimmen

Schnelles Einlesen sehr großer Tabellen als Dataframes

Ich habe sehr große Tabellen (30 Millionen Zeilen), die ich als Dataframes in R laden möchte. read.table() hat viele praktische Funktionen, aber es scheint, dass die Implementierung eine Menge Logik enthält, die alles verlangsamen würde. In meinem Fall gehe ich davon aus, dass ich die Spaltentypen im Voraus kenne, dass die Tabelle keine Spaltenüberschriften oder Zeilennamen enthält und dass es keine pathologischen Zeichen gibt, über die ich mir Sorgen machen müsste.

Ich weiß, dass das Einlesen einer Tabelle als Liste mit scan() kann recht schnell sein, z.B.:

datalist <- scan('myfile',sep='\t',list(url='',popularity=0,mintime=0,maxtime=0)))

Aber einige meiner Versuche, dies in einen Datenrahmen zu konvertieren, scheinen die Leistung des oben genannten um den Faktor 6 zu verringern:

df <- as.data.frame(scan('myfile',sep='\t',list(url='',popularity=0,mintime=0,maxtime=0))))

Gibt es eine bessere Möglichkeit, dies zu tun? Oder möglicherweise eine ganz andere Herangehensweise an das Problem?

15voto

Hector Haffenden Punkte 1290

Eine Alternative ist die Verwendung der vroom Paket. Jetzt auf CRAN. vroom lädt nicht die gesamte Datei, sondern indiziert, wo sich jeder Datensatz befindet, und wird später gelesen, wenn Sie die Datei verwenden.

Zahlen Sie nur für das, was Sie nutzen.

Voir Einführung in vroom , Starten Sie mit vroom y el Vroom-Benchmarks .

Der grundlegende Überblick ist, dass das anfängliche Lesen einer großen Datei viel schneller sein wird und nachfolgende Änderungen an den Daten etwas langsamer sein können. Je nach Verwendungszweck könnte dies also die beste Option sein.

Siehe ein vereinfachtes Beispiel aus Vroom-Benchmarks unten, die wichtigsten Teile zu sehen ist die super schnelle Lesezeiten, aber etwas sower Operationen wie Aggregat usw..

package                 read    print   sample   filter  aggregate   total
read.delim              1m      21.5s   1ms      315ms   764ms       1m 22.6s
readr                   33.1s   90ms    2ms      202ms   825ms       34.2s
data.table              15.7s   13ms    1ms      129ms   394ms       16.3s
vroom (altrep) dplyr    1.7s    89ms    1.7s     1.3s    1.9s        6.7s

11voto

Adam Punkte 7302

Ich lese Daten sehr schnell mit dem neuen arrow Paket. Es scheint sich in einem recht frühen Stadium zu befinden.

Konkret verwende ich die Parkett spaltenförmiges Format. Dies konvertiert zurück in ein data.frame in R, aber Sie können sogar noch größere Geschwindigkeitssteigerungen erzielen, wenn Sie dies nicht tun. Dieses Format ist praktisch, da es auch in Python verwendet werden kann.

Mein Hauptanwendungsfall für diese ist auf einem ziemlich zurückhaltenden RShiny-Server. Aus diesen Gründen ziehe ich es vor, die Daten an die Apps zu binden (d.h. außerhalb von SQL), und benötige daher eine kleine Dateigröße sowie eine geringe Geschwindigkeit.

Dieser verlinkte Artikel bietet ein Benchmarking und einen guten Überblick. Ich habe einige interessante Punkte unten zitiert.

https://ursalabs.org/blog/2019-10-columnar-perf/

Größe der Datei

Das heißt, die Parquet-Datei ist nur halb so groß wie die gepackte CSV-Datei. Einer der Gründe, warum die Parquet-Datei so klein ist, liegt in der Wörterbuchkodierung (auch "Wörterbuchkomprimierung" genannt). Die Wörterbuchkomprimierung kann zu einer wesentlich besseren Komprimierung führen als die Verwendung eines Allzweck-Bytes-Kompressors wie LZ4 oder ZSTD (die im FST-Format verwendet werden). Parquet wurde entwickelt, um sehr kleine Dateien zu erzeugen, die schnell zu lesen sind.

Lesegeschwindigkeit

Bei der Kontrolle nach Ausgabetypen (z. B. beim Vergleich aller R data.frame-Ausgaben untereinander) zeigt sich, dass die Leistung von Parquet, Feather und FST innerhalb eines relativ geringen Abstands zueinander liegt. Das Gleiche gilt für die pandas.DataFrame-Ausgaben. data.table::fread ist bei einer Dateigröße von 1,5 GB beeindruckend wettbewerbsfähig, liegt aber bei einer CSV-Datei von 2,5 GB hinter den anderen zurück.


Unabhängiger Test

Ich habe ein unabhängiges Benchmarking mit einem simulierten Datensatz von 1.000.000 Zeilen durchgeführt. Dabei habe ich einige Dinge umgestellt, um die Komprimierung in Frage zu stellen. Außerdem habe ich ein kurzes Textfeld mit zufälligen Wörtern und zwei simulierte Faktoren hinzugefügt.

Daten

library(dplyr)
library(tibble)
library(OpenRepGrid)

n <- 1000000

set.seed(1234)
some_levels1 <- sapply(1:10, function(x) paste(LETTERS[sample(1:26, size = sample(3:8, 1), replace = TRUE)], collapse = ""))
some_levels2 <- sapply(1:65, function(x) paste(LETTERS[sample(1:26, size = sample(5:16, 1), replace = TRUE)], collapse = ""))

test_data <- mtcars %>%
  rownames_to_column() %>%
  sample_n(n, replace = TRUE) %>%
  mutate_all(~ sample(., length(.))) %>%
  mutate(factor1 = sample(some_levels1, n, replace = TRUE),
         factor2 = sample(some_levels2, n, replace = TRUE),
         text = randomSentences(n, sample(3:8, n, replace = TRUE))
         )

Lesen und Schreiben

Das Schreiben der Daten ist einfach.

library(arrow)

write_parquet(test_data , "test_data.parquet")

# you can also mess with the compression
write_parquet(test_data, "test_data2.parquet", compress = "gzip", compression_level = 9)

Auch das Ablesen der Daten ist einfach.

read_parquet("test_data.parquet")

# this option will result in lightning fast reads, but in a different format.
read_parquet("test_data2.parquet", as_data_frame = FALSE)

Ich habe das Lesen dieser Daten mit einigen der konkurrierenden Optionen verglichen und erhielt etwas andere Ergebnisse als in dem obigen Artikel, was zu erwarten war.

benchmarking

Diese Datei ist bei weitem nicht so groß wie der Benchmark-Artikel, vielleicht ist das der Unterschied.

Tests

  • rds: test_data.rds (20,3 MB)
  • parquet2_native: (14,9 MB mit höherer Kompression und as_data_frame = FALSE )
  • Parkett2: test_data2.parquet (14,9 MB mit höherer Kompression)
  • Parkett: test_data.parquet (40,7 MB)
  • fst2: test_data2.fst (27,9 MB mit höherer Kompression)
  • fst: test_data.fst (76,8 MB)
  • fread2: test_data.csv.gz (23,6MB)
  • fread: test_data.csv (98.7MB)
  • feder_pfeil: test_data.feather (157,2 MB gelesen mit arrow )
  • feder: test_data.feather (157,2 MB gelesen mit feather )

Beobachtungen

Für diese spezielle Datei, fread ist tatsächlich sehr schnell. Ich mag die geringe Dateigröße der hochkomprimierten parquet2 Test. Ich werde vielleicht die Zeit investieren, um mit dem nativen Datenformat zu arbeiten, anstatt mit einem data.frame wenn ich die höhere Geschwindigkeit wirklich brauche.

Aquí fst ist ebenfalls eine gute Wahl. Ich würde entweder das hochkomprimierte fst Format oder das hochkomprimierte parquet je nachdem, ob ich einen Kompromiss zwischen Geschwindigkeit oder Dateigröße brauche.

7voto

Stephen Henderson Punkte 6080

Ein kleiner zusätzlicher Punkt, der erwähnenswert ist. Wenn Sie eine sehr große Datei haben, können Sie die Anzahl der Zeilen (wenn keine Kopfzeile vorhanden ist) im Handumdrehen berechnen, indem Sie (wobei bedGraph ist der Name der Datei in Ihrem Arbeitsverzeichnis):

>numRow=as.integer(system(paste("wc -l", bedGraph, "| sed 's/[^0-9.]*\\([0-9.]*\\).*/\\1/'"), intern=T))

Diese können Sie dann entweder in read.csv , read.table ...

>system.time((BG=read.table(bedGraph, nrows=numRow, col.names=c('chr', 'start', 'end', 'score'),colClasses=c('character', rep('integer',3)))))
   user  system elapsed 
 25.877   0.887  26.752 
>object.size(BG)
203949432 bytes

6voto

rferrisx Punkte 1258

Oft denke ich, dass es einfach eine gute Praxis ist, größere Datenbanken innerhalb einer Datenbank (z.B. Postgres) zu halten. Ich benutze nichts, was viel größer ist als (nrow * ncol) ncell = 10M, was ziemlich klein ist; aber ich stelle oft fest, dass ich möchte, dass R speicherintensive Graphen nur erstellt und hält, während ich Abfragen von mehreren Datenbanken mache. In der Zukunft von 32 GB Laptops werden einige dieser Speicherprobleme verschwinden. Aber der Reiz, die Daten in einer Datenbank zu speichern und dann den Speicher von R für die resultierenden Abfrageergebnisse und Diagramme zu nutzen, kann immer noch nützlich sein. Einige Vorteile sind:

(1) Die Daten bleiben in Ihrer Datenbank geladen. Wenn Sie Ihren Laptop wieder einschalten, stellen Sie in pgadmin einfach eine neue Verbindung zu den gewünschten Datenbanken her.

(2) Es stimmt, dass R viel mehr raffinierte statistische und grafische Operationen durchführen kann als SQL. Aber ich denke, SQL ist besser für die Abfrage großer Datenmengen geeignet als R.

# Looking at Voter/Registrant Age by Decade

library(RPostgreSQL);library(lattice)

con <- dbConnect(PostgreSQL(), user= "postgres", password="password",
                 port="2345", host="localhost", dbname="WC2014_08_01_2014")

Decade_BD_1980_42 <- dbGetQuery(con,"Select PrecinctID,Count(PrecinctID),extract(DECADE from Birthdate) from voterdb where extract(DECADE from Birthdate)::numeric > 198 and PrecinctID in (Select * from LD42) Group By PrecinctID,date_part Order by Count DESC;")

Decade_RD_1980_42 <- dbGetQuery(con,"Select PrecinctID,Count(PrecinctID),extract(DECADE from RegistrationDate) from voterdb where extract(DECADE from RegistrationDate)::numeric > 198 and PrecinctID in (Select * from LD42) Group By PrecinctID,date_part Order by Count DESC;")

with(Decade_BD_1980_42,(barchart(~count | as.factor(precinctid))));
mtext("42LD Birthdays later than 1980 by Precinct",side=1,line=0)

with(Decade_RD_1980_42,(barchart(~count | as.factor(precinctid))));
mtext("42LD Registration Dates later than 1980 by Precinct",side=1,line=0)

1voto

Konrad Punkte 16338

Ich wollte eine Spark-basierte Lösung in der einfachsten Form beisteuern:

# Test Data ---------------------------------------------------------------

set.seed(123)
bigdf <-
    data.frame(
        dim = sample(letters, replace = T, 4e7),
        fact1 = rnorm(4e7),
        fact2 = rnorm(4e7, 20, 50)
    )
tmp_csv <- fs::file_temp(pattern = "big_df", ext = ".csv")
readr::write_csv(x = bigdf, file = tmp_csv)

# Spark -------------------------------------------------------------------

# Installing if needed
# sparklyr::spark_available_versions()
# sparklyr::spark_install()

library("sparklyr")
sc <- spark_connect(master = "local")

# Uploading CSV
system.time(tbl_big_df <- spark_read_csv(sc = sc, path = tmp_csv))

Spark erzielte recht gute Ergebnisse:

>> system.time(tbl_big_df <- spark_read_csv(sc = sc, path = tmp_csv))
   user  system elapsed 
  0.278   0.034  11.747 

Dies wurde auf einem MacBook Pro mit 32 GB Arbeitsspeicher getestet.

Bemerkungen

Funken, in der Regel sollte nicht in der Lage sein, gegen auf Geschwindigkeit optimierte Pakete zu "gewinnen". Trotzdem wollte ich eine Antwort mit Spark beisteuern:

  • Für einige der Kommentare und Antworten, bei denen der Prozess nicht funktioniert hat, kann die Verwendung von Spark eine brauchbare Alternative sein
  • Langfristig ist es wichtig, so viele Daten wie möglich in data.frame kann sich später als problematisch erweisen, wenn andere Operationen an diesem Objekt versucht werden und die Leistungsgrenze der Architektur erreichen

Ich denke, dass für Fragen wie diese, bei denen die Aufgabe darin besteht, 1e7 oder mehr Zeilen zu verarbeiten, Spark in Betracht gezogen werden sollte. Selbst wenn es möglich ist, diese Daten in eine einzige Zeile zu "hämmern", sollte data.frame Es fühlt sich einfach nicht richtig an. Es ist wahrscheinlich, dass dieses Objekt schwer zu handhaben ist und Probleme bei der Bereitstellung von Modellen usw. verursacht.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X