Die Ăbersetzung des Artikels wurde speziell fĂŒr die Studierenden des Kurses erstellt .

In den letzten Wochen haben wir mit in das binĂ€re Streaming-Format integriert, ergĂ€nzt um das bereits vorhandene Random Access / IPC-Dateiformat. Wir haben Implementierungen in Java und C++ sowie Python-Bindings. In diesem Artikel werde ich erklĂ€ren, wie das Format funktioniert und zeigen, wie man eine sehr hohe DatenĂŒbertragungsrate fĂŒr DataFrame pandas erreichen kann.
Streaming von Spaltendaten
Eine hĂ€ufige Frage, die ich von Arrow-Nutzern erhalte, ist die hohe Kosten fĂŒr die Ăbertragung groĂer tabellarischer DatensĂ€tze von einem zeilen- oder aufzeichnungenorientierten Format in ein spaltenorientiertes Format. Bei Multigigabyte-DatensĂ€tzen kann die Transponierung im Speicher oder auf der Festplatte eine unĂŒberwindbare Aufgabe sein.
FĂŒr das Streaming von Daten, unabhĂ€ngig davon, ob die Quelldaten zeilen- oder spaltenbasiert sind, bleibt eine Möglichkeit, kleine Datenpakete zu senden, von denen jedes eine spaltenbasierte Anordnung enthĂ€lt.
In Apache Arrow wird eine Sammlung von Spaltenarrays im Speicher, die einen Chunk einer Tabelle reprÀsentiert, als Record Batch bezeichnet. Um eine einheitliche Datenstruktur einer logischen Tabelle darzustellen, können mehrere Record Batches zusammengestellt werden.
Im bestehenden Random Access-Dateiformat schreiben wir Metadaten, die das Schema der Tabelle und die Position der Blöcke am Ende der Datei enthalten, was es Ihnen ermöglicht, sehr kostengĂŒnstig jeden Record Batch oder jede Spalte aus dem Datensatz auszuwĂ€hlen. Im Streaming-Format senden wir eine Reihe von Nachrichten: das Schema und dann einen oder mehrere Record Batches.
Die verschiedenen Formate sehen ungefÀhr so aus, wie auf diesem Bild dargestellt:

Datenstreaming in PyArrow: Anwendung
Um Ihnen zu zeigen, wie das funktioniert, werde ich ein Beispielformat erstellen, das einen Streaming-Chunk darstellt:
import time
import numpy as np
import pandas as pd
import pyarrow as pa
def generate_data(total_size, ncols):
nrows = int(total_size / ncols / np.dtype('float64').itemsize)
return pd.DataFrame({
'c' + str(i): np.random.randn(nrows)
for i in range(ncols)
}) Nehmen wir nun an, dass wir 1 GB an Daten aufzeichnen möchten, bestehend aus 1 MB groĂen Chunks, insgesamt 1024 Chunks. Zuerst erstellen wir das erste DataFrame mit einer GröĂe von 1 MB und 16 Spalten:
KILOBYTE = 1 << 10
MEGABYTE = KILOBYTE * KILOBYTE
DATA_SIZE = 1024 * MEGABYTE
NCOLS = 16
df = generate_data(MEGABYTE, NCOLS) Dann konvertiere ich sie in pyarrow.RecordBatch:
batch = pa.RecordBatch.from_pandas(df) Jetzt werde ich einen Ausgabestream erstellen, der in den Speicher schreibt und erstellen StreamWriter:
sink = pa.InMemoryOutputStream()
stream_writer = pa.StreamWriter(sink, batch.schema)Dann schreiben wir 1024 Chunks, die letztendlich 1GB Datensatz ausmachen:
for i in range(DATA_SIZE // MEGABYTE):
stream_writer.write_batch(batch)Da wir im RAM geschrieben haben, können wir den gesamten Stream in einem Buffer erhalten:
In [13]: source = sink.get_result()
In [14]: source
Out[14]:
In [15]: source.size
Out[15]: 1074750744 Da sich diese Daten im Speicher befinden, ist das Lesen der Arrow-Datensatzpakete eine zero-copy Operation. Ich öffne StreamReader und lese die Daten in pyarrow.Table, und konvertiere sie dann in DataFrame pandas:
In [16]: reader = pa.StreamReader(source)
In [17]: table = reader.read_all()
In [18]: table
Out[18]:
In [19]: df = table.to_pandas()
In [20]: df.memory_usage().sum()
Out[20]: 1073741904Das ist natĂŒrlich alles gut, aber Sie haben vielleicht Fragen. Wie schnell geht das? Wie beeinflusst die Chunk-GröĂe die Leistung beim Erhalten eines pandas DataFrame?
Leistung der DatenĂŒbertragung
Mit abnehmender Chunk-GröĂe steigt die Kosten fĂŒr die Rekonstruktion eines kontinuierlichen Column-Frames des DataFrame in pandas aufgrund ineffizienter Cachezugriffs-Schemata. Es gibt auch einige Overheads durch die Arbeit mit C++-Datenstrukturen und Arrays sowie deren Speicherpuffern.
FĂŒr 1 MB, wie oben angegeben, ergibt sich auf meinem Laptop (Quad-Core Xeon E3-1505M):
In [20]: %timeit pa.StreamReader(source).read_all().to_pandas()
10 loops, best of 3: 129 ms per loopSo betrĂ€gt die effektive Bandbreite 7,75 GB/s fĂŒr die Wiederherstellung eines 1GB DataFrames aus 1024 Chunks von 1MB. Was passiert, wenn wir gröĂere oder kleinere Chunks verwenden? Hier sind die Ergebnisse:

Die Leistung verringert sich erheblich von 256K auf 64K Chunks. Es hat mich ĂŒberrascht, dass Chunks von 1 MB schneller verarbeitet wurden als 16 MB. Es wĂ€re sinnvoll, eine genauere Untersuchung durchzufĂŒhren und herauszufinden, ob dies eine normale Verteilung ist oder ob noch etwas anderes eine Rolle spielt.
In der aktuellen Implementierung des Formats werden die Daten grundsĂ€tzlich nicht komprimiert, daher sind GröĂe im Speicher und 'auf den DrĂ€hten' etwa gleich. In Zukunft könnte die Kompression eine zusĂ€tzliche Option werden.
Fazit
Die Streaming-Ăbertragung von Spaltendaten kann eine effektive Methode zum Ăbertragen groĂer DatensĂ€tze in spaltenbasierte Analysetools, wie zum Beispiel in pandas, mithilfe kleiner Chunks sein. Datenservices, die speicherorientierte Zeilenbanken verwenden, können kleine Daten-Chunks ĂŒbertragen und transponieren, die besser fĂŒr den L2- und L3-Cache Ihres Prozessors geeignet sind.
VollstÀndiger Code
import time
import numpy as np
import pandas as pd
import pyarrow as pa
def generate_data(total_size, ncols):
nrows = total_size / ncols / np.dtype('float64').itemsize
return pd.DataFrame({
'c' + str(i): np.random.randn(nrows)
for i in range(ncols)
})
KILOBYTE = 1 << 10
MEGABYTE = KILOBYTE * KILOBYTE
DATA_SIZE = 1024 * MEGABYTE
NCOLS = 16
def get_timing(f, niter):
start = time.clock_gettime(time.CLOCK_REALTIME)
for i in range(niter):
f()
return (time.clock_gettime(time.CLOCK_REALTIME) - start) / NITER
def read_as_dataframe(klass, source):
reader = klass(source)
table = reader.read_all()
return table.to_pandas()
NITER = 5
results = []
CHUNKSIZES = [16 * KILOBYTE, 64 * KILOBYTE, 256 * KILOBYTE, MEGABYTE, 16 * MEGABYTE]
for chunksize in CHUNKSIZES:
nchunks = DATA_SIZE // chunksize
batch = pa.RecordBatch.from_pandas(generate_data(chunksize, NCOLS))
sink = pa.InMemoryOutputStream()
stream_writer = pa.StreamWriter(sink, batch.schema)
for i in range(nchunks):
stream_writer.write_batch(batch)
source = sink.get_result()
elapsed = get_timing(lambda: read_as_dataframe(pa.StreamReader, source), NITER)
result = (chunksize, elapsed)
print(result)
results.append(result)Quelle: habr.com
