Il formato Apache Parquet fornisce una rappresentazione binaria efficiente dei dati della tabella colonnare, come visto con un uso diffuso nella serializzazione di Apache Hadoop e Spark, AWS Athena e Glue e Pandas DataFrame. Sebbene Parquet offra un’ampia interoperabilità con prestazioni superiori ai formati di testo (come CSV o JSON), è dieci volte più lento di NPZ, un formato di serializzazione DataFrame alternativo introdotto in Telaio statico.
StaticFrame (una libreria DataFrame open source di cui sono autore) si basa sui formati NumPy NPY e NPZ per codificare DataFrame. Il formato NPY (una codifica binaria dei dati dell’array) e il formato NPZ (pacchetti compressi di file NPY) sono definiti in un Proposta di miglioramento di NumPy dal 2007. Estendendo il formato NPZ con metadati JSON specializzati, StaticFrame fornisce un formato di serializzazione DataFrame completo che supporta tutti i dtype NumPy.
Questo articolo estende il lavoro presentato per la prima volta a PyCon USA 2022 con ulteriori ottimizzazioni delle prestazioni e benchmark più ampi.
I DataFrame non sono solo raccolte di dati colonnari con etichette di colonna di tipo stringa, come quelle presenti nei database relazionali. Oltre ai dati colonnari, i DataFrames hanno righe e colonne etichettate e tali etichette di righe e colonne possono essere di qualsiasi tipo o (con etichette gerarchiche) di molti tipi. Inoltre, è comune archiviare i metadati con un file name
attributo, sul DataFrame o sulle etichette dell’asse.
Poiché Parquet è stato originariamente progettato solo per archiviare raccolte di dati colonnari, l’intera gamma di caratteristiche DataFrame non è supportata direttamente. Pandas fornisce queste informazioni aggiuntive aggiungendo metadati JSON nel file Parquet.
Inoltre, Parquet supporta una selezione minima di tipi; l’intera gamma di dtype NumPy non è supportata direttamente. Ad esempio, Parquet non supporta in modo nativo numeri interi senza segno o alcun tipo di data.
Sebbene i pickle Python siano in grado di serializzare in modo efficiente DataFrames e array NumPy, sono adatti solo per cache a breve termine da fonti attendibili. Sebbene i pickle siano veloci, possono diventare non validi a causa di modifiche al codice e non è sicuro caricarli da fonti non attendibili.
Un’altra alternativa al Parquet, originata dal progetto Arrow, è Piuma. Sebbene Feather supporti tutti i tipi di frecce e riesca a essere più veloce di Parquet, è comunque almeno due volte più lento nella lettura dei DataFrames rispetto a NPZ.
Parquet e Feather supportano la compressione per ridurre le dimensioni del file. Parquet utilizza per impostazione predefinita la compressione “scattante”, mentre Feather utilizza per impostazione predefinita “lz4”. Poiché il formato NPZ dà priorità alle prestazioni, non supporta ancora la compressione. Come verrà mostrato di seguito, NPZ supera di fattori significativi le prestazioni dei file Parquet sia compressi che non compressi.
Numerose pubblicazioni offrono benchmark DataFrame testando solo uno o due set di dati. McKinney e Richardson (2020) è un esempio in cui due set di dati, Fannie Mae Loan Performance e NYC Yellow Taxi Trip, vengono utilizzati per generalizzare le prestazioni. Tali set di dati peculiari sono insufficienti, poiché sia la forma del DataFrame che il grado di eterogeneità di tipo colonnare possono differenziare significativamente le prestazioni.
Per evitare questa carenza, confronto le prestazioni con un panel di nove set di dati sintetici. Questi set di dati variano lungo due dimensioni: forma (alto, quadrato e largo) ed eterogeneità colonnare (colonnare, mista e uniforme). Le variazioni di forma alterano la distribuzione degli elementi tra geometrie alte (ad esempio, 10.000 righe e 100 colonne), quadrate (ad esempio, 1.000 righe e colonne) e larghe (ad esempio, 100 righe e 10.000 colonne). Le variazioni dell’eterogeneità colonnare alterano la diversità dei tipi tra colonnare (nessuna colonna adiacente ha lo stesso tipo), misto (alcune colonne adiacenti hanno lo stesso tipo) e uniforme (tutte le colonne hanno lo stesso tipo).
IL frame-fixtures
la libreria definisce un linguaggio specifico del dominio per creare DataFrames deterministici e generati casualmente per i test; i nove set di dati vengono generati con questo strumento.
Per dimostrare alcune delle interfacce StaticFrame e Pandas valutate, la seguente sessione IPython esegue test prestazionali di base utilizzando %time
. Come mostrato di seguito, un DataFrame quadrato e tipizzato in modo uniforme può essere scritto e letto con NPZ molte volte più velocemente di Parquet non compresso.
>>> import numpy as np
>>> import static_frame as sf
>>> import pandas as pd>>> # an square, uniform float array
>>> array = np.random.random_sample((10_000, 10_000))
>>> # write peformance
>>> f1 = sf.Frame(array)
>>> %time f1.to_npz('/tmp/frame.npz')
CPU times: user 710 ms, sys: 396 ms, total: 1.11 s
Wall time: 1.11 s
>>> df1 = pd.DataFrame(array)
>>> %time df1.to_parquet('/tmp/df.parquet', compression=None)
CPU times: user 6.82 s, sys: 900 ms, total: 7.72 s
Wall time: 7.74 s
>>> # read performance
>>> %time f2 = f1.from_npz('/tmp/frame.npz')
CPU times: user 2.77 ms, sys: 163 ms, total: 166 ms
Wall time: 165 ms
>>> %time df2 = pd.read_parquet('/tmp/df.parquet')
CPU times: user 2.55 s, sys: 1.2 s, total: 3.75 s
Wall time: 866 ms
I test delle prestazioni forniti di seguito estendono questo approccio di base utilizzando frame-fixtures
per variazione sistematica dell’eterogeneità di forma e tipo e risultati medi su dieci iterazioni. Sebbene la configurazione hardware influisca sulle prestazioni, le caratteristiche relative vengono mantenute su macchine e sistemi operativi diversi. Per tutte le interfacce vengono utilizzati i parametri predefiniti, ad eccezione della disabilitazione della compressione secondo necessità. Il codice utilizzato per eseguire questi test è disponibile all’indirizzo GitHub.
Leggi Prestazioni
Poiché i dati vengono generalmente letti più spesso di quanto vengono scritti, le prestazioni di lettura sono una priorità. Come mostrato per tutti e nove i DataFrame da un milione (1e+06) di elementi, NPZ supera significativamente Parquet e Feather con ogni dispositivo. Le prestazioni di lettura NPZ sono oltre dieci volte più veloci rispetto al Parquet compresso. Ad esempio, con l’apparecchio Uniform Tall la lettura Parquet compressa è di 21 ms rispetto a 1,5 ms con NPZ.
Il grafico seguente mostra il tempo di elaborazione, dove le barre inferiori corrispondono a prestazioni più veloci.
Questa impressionante prestazione NPZ viene mantenuta con la scala. Passando a 100 milioni (1e+08) di elementi, NPZ continua a funzionare almeno due volte più velocemente di Parquet e Feather, indipendentemente dall’utilizzo della compressione.
Scrivi prestazioni
Nella scrittura di DataFrames su disco, NPZ supera Parquet (sia compresso che non compresso) in tutti gli scenari. Ad esempio, con il dispositivo Uniform Square, la scrittura Parquet compressa è di 200 ms rispetto ai 18,3 ms con NPZ. Le prestazioni di scrittura di NPZ sono generalmente paragonabili a Feather non compresso: in alcuni scenari NPZ è più veloce, in altri Feather è più veloce.
Come per le prestazioni di lettura, le prestazioni di scrittura NPZ vengono mantenute con la scala. Passando a 100 milioni (1e+08) di elementi, NPZ continua ad essere almeno due volte più veloce di Parquet, indipendentemente dal fatto che venga utilizzata o meno la compressione.
Prestazioni idiosincratiche
Come ulteriore riferimento, paragoneremo anche gli stessi dati di NYC Yellow Taxi Trip (da gennaio 2010) utilizzati in McKinney e Richardson (2020). Questo set di dati contiene quasi 300 milioni (3e+08) di elementi in un DataFrame alto e tipizzato in modo eterogeneo di 14.863.778 righe e 19 colonne.
Le prestazioni di lettura di NPZ sono circa quattro volte più veloci rispetto a Parquet e Feather (con o senza compressione). Mentre le prestazioni di scrittura NPZ sono più veloci di Parquet, la scrittura Feather qui è più veloce.
Dimensione del file
Come mostrato di seguito per DataFrames da un milione (1e+06) di elementi e 100 milioni (1e+08) di elementi, NPZ non compresso ha generalmente dimensioni uguali su disco rispetto a Feather non compresso e sempre più piccolo del Parquet non compresso (a volte più piccolo anche del Parquet compresso). Poiché la compressione fornisce solo modeste riduzioni delle dimensioni dei file per Parquet e Feather, il vantaggio in termini di velocità di NPZ non compresso potrebbe facilmente superare il costo di dimensioni maggiori.
StaticFrame memorizza i dati come una raccolta di array NumPy 1D e 2D. Le matrici rappresentano valori colonnari, nonché indici di profondità variabile ed etichette di colonna. Oltre agli array NumPy, informazioni sui tipi di componenti (ovvero, la classe Python utilizzata per l’indice e le colonne), nonché sul componente name
attributi, sono necessari per ricostruire completamente a Frame
. La serializzazione completa di un DataFrame richiede la scrittura e la lettura di questi componenti in un file.
I componenti DataFrame possono essere rappresentati dal diagramma seguente, che isola array, tipi di array, tipi di componenti e nomi di componenti. Questo diagramma verrà utilizzato per dimostrare come una NPZ codifica un DataFrame.
I componenti di quel diagramma si associano ai componenti di a Frame
rappresentazione di stringhe in Python. Ad esempio, dato a Frame
di numeri interi e booleani con etichette gerarchiche sia sull’indice che sulle colonne (scaricabili tramite GitHub con StaticFrame WWW
interfaccia), StaticFrame fornisce la seguente rappresentazione di stringa:
>>> frame = sf.Frame.from_npz(sf.WWW.from_file('https://github.com/static-frame/static-frame/raw/master/doc/source/articles/serialize/frame.npz', encoding=None))
>>> frame
<Frame: p>
<IndexHierarchy: q> data data data valid <<U5>
A B C * <<U1>
<IndexHierarchy: r>
2012-03 x 5 4 7 False
2012-03 y 9 1 8 True
2012-04 x 3 6 2 True
<datetime64(M)> <<U1> <int64> <int64> <int64> <bool>
I componenti della rappresentazione della stringa possono essere mappati nel diagramma DataFrame in base al colore:
Codifica di un array in NPY
Un NPY memorizza un array NumPy come un file binario con sei componenti: (1) un prefisso “magico”, (2) un numero di versione, (3) una lunghezza dell’intestazione e (4) un’intestazione (dove l’intestazione è una rappresentazione di stringa di un dizionario Python) e (5) riempimento seguito da (6) dati di byte dell’array non elaborati. Questi componenti sono mostrati di seguito per un array binario di tre elementi memorizzato in un file denominato “__blocks_1__.npy”.
Dato un file NPZ denominato “frame.npz”, possiamo estrarre i dati binari leggendo il file NPY dall’NPZ con la libreria standard ZipFile
:
>>> from zipfile import ZipFile
>>> with ZipFile('/tmp/frame.npz') as zf: print(zf.open('__blocks_1__.npy').read())
b'\x93NUMPY\x01\x006\x00{"descr":"|b1","fortran_order":True,"shape":(3,)} \n\x00\x01\x01
Poiché NPY è ben supportato in NumPy, il file np.load()
la funzione può essere utilizzata per convertire questo file in un array NumPy. Ciò significa che i dati dell’array sottostante in uno StaticFrame NPZ sono facilmente estraibili da lettori alternativi.
>>> with ZipFile('/tmp/frame.npz') as zf: print(repr(np.load(zf.open('__blocks_1__.npy'))))
array((False, True, True))
Poiché un file NPY può codificare qualsiasi array, è possibile caricare grandi array bidimensionali da dati di byte contigui, fornendo prestazioni eccellenti in StaticFrame quando più colonne contigue sono rappresentate da un singolo array.
Creazione di un file NPZ
Uno StaticFrame NPZ è un file ZIP standard non compresso che contiene dati di array in file NPY e metadati (contenenti tipi e nomi di componenti) in un file JSON.
Dato il file NPZ per il Frame
sopra, possiamo elencarne il contenuto con ZipFile
. L’archivio contiene sei file NPY e un file JSON.
>>> with ZipFile('/tmp/frame.npz') as zf: print(zf.namelist())
('__values_index_0__.npy', '__values_index_1__.npy', '__values_columns_0__.npy', '__values_columns_1__.npy', '__blocks_0__.npy', '__blocks_1__.npy', '__meta__.json')
L’illustrazione seguente associa questi file ai componenti del diagramma DataFrame.
StaticFrame estende il formato NPZ per includere metadati in un file JSON. Questo file definisce gli attributi del nome, i tipi di componenti e i conteggi di profondità.
>>> with ZipFile('/tmp/frame.npz') as zf: print(zf.open('__meta__.json').read())
b'{"__names__": ("p", "r", "q"), "__types__": ("IndexHierarchy", "IndexHierarchy"), "__types_index__": ("IndexYearMonth", "Index"), "__types_columns__": ("Index", "Index"), "__depths__": (2, 2, 2)}'
Nell’illustrazione seguente, i componenti di __meta__.json
i file sono mappati sui componenti del diagramma DataFrame.
Come semplice file ZIP, gli strumenti per estrarre il contenuto di uno StaticFrame NPZ sono onnipresenti. D’altro canto, il formato ZIP, data la sua storia e le sue ampie caratteristiche, comporta un sovraccarico in termini di prestazioni. StaticFrame implementa un lettore ZIP personalizzato ottimizzato per l’utilizzo di NPZ, che contribuisce alle eccellenti prestazioni di lettura di NPZ.
Le prestazioni della serializzazione DataFrame sono fondamentali per molte applicazioni. Anche se il parquet gode di un ampio consenso, la sua generalità compromette la specificità del tipo e le prestazioni. StaticFrame NPZ può leggere e scrivere DataFrame fino a dieci volte più velocemente di Parquet con o senza compressione, con dimensioni di file simili (o solo modestamente più grandi). Sebbene Feather sia un’alternativa interessante, le prestazioni di lettura NPZ sono ancora generalmente due volte più veloci di Feather. Se l’I/O dei dati rappresenta un collo di bottiglia (e spesso lo è), StaticFrame NPZ offre una soluzione.
Fonte: towardsdatascience.com