1zahvnofmdc8hxoeleodybw.jpeg

Lea y escriba marcos de datos hasta diez veces más rápido que Parquet con StaticFrame NPZ

Agua en una hoja
Foto por autor

El formato Apache Parquet proporciona una representación binaria eficiente de datos de tablas en columnas, como se ve con el uso generalizado en Apache Hadoop y Spark, AWS Athena y Glue, y la serialización de Pandas DataFrame. Si bien Parquet ofrece una amplia interoperabilidad con un rendimiento superior a los formatos de texto (como CSV o JSON), es hasta diez veces más lento que NPZ, un formato alternativo de serialización de DataFrame introducido en Marco estático.

StaticFrame (una biblioteca DataFrame de código abierto de la que soy autor) se basa en los formatos NumPy NPY y NPZ para codificar DataFrames. El formato NPY (una codificación binaria de datos de matriz) y el formato NPZ (paquetes comprimidos de archivos NPY) se definen en un Propuesta de mejora de NumPy desde 2007. Al ampliar el formato NPZ con metadatos JSON especializados, StaticFrame proporciona un formato de serialización de DataFrame completo que admite todos los tipos de NumPy.

Este artículo amplía el trabajo presentado por primera vez en PyCon Estados Unidos 2022 con mayores optimizaciones de rendimiento y evaluaciones comparativas más amplias.

Los DataFrames no son solo colecciones de datos en columnas con etiquetas de columnas de cadena, como las que se encuentran en las bases de datos relacionales. Además de los datos en columnas, los DataFrames tienen filas y columnas etiquetadas, y esas etiquetas de filas y columnas pueden ser de cualquier tipo o (con etiquetas jerárquicas) de muchos tipos. Además, es común almacenar metadatos con un name atributo, ya sea en el DataFrame o en las etiquetas de los ejes.

Como Parquet se diseñó originalmente solo para almacenar colecciones de datos en columnas, no se admite directamente la gama completa de características de DataFrame. Pandas proporciona esta información adicional agregando metadatos JSON al archivo Parquet.

Además, Parquet admite una selección mínima de tipos; No se admite directamente toda la gama de tipos de NumPy. Por ejemplo, Parquet no admite de forma nativa números enteros sin signo ni ningún tipo de fecha.

Si bien los pickles de Python son capaces de serializar eficientemente DataFrames y matrices NumPy, solo son adecuados para cachés a corto plazo de fuentes confiables. Si bien los pickles son rápidos, pueden dejar de ser válidos debido a cambios de código y no son seguros para cargar desde fuentes no confiables.

Otra alternativa al Parquet, originada en el proyecto Arrow, es Pluma. Si bien Feather admite todos los tipos de Arrow y logra ser más rápido que Parquet, sigue siendo al menos dos veces más lento al leer DataFrames que NPZ.

Parquet y Feather admiten la compresión para reducir el tamaño del archivo. Parquet utiliza de forma predeterminada la compresión «rápida», mientras que Feather utiliza de forma predeterminada «lz4». Como el formato NPZ prioriza el rendimiento, aún no admite la compresión. Como se mostrará a continuación, NPZ supera a los archivos Parquet comprimidos y sin comprimir por factores significativos.

Numerosas publicaciones ofrecen puntos de referencia de DataFrame probando solo uno o dos conjuntos de datos. McKinney y Richardson (2020) es un ejemplo en el que se utilizan dos conjuntos de datos, Fannie Mae Loan Performance y NYC Yellow Taxi Trip, para generalizar sobre el rendimiento. Estos conjuntos de datos idiosincrásicos son insuficientes, ya que tanto la forma del DataFrame como el grado de heterogeneidad del tipo de columnas pueden diferenciar significativamente el rendimiento.

Para evitar esta deficiencia, comparo el desempeño con un panel de nueve conjuntos de datos sintéticos. Estos conjuntos de datos varían en dos dimensiones: forma (alta, cuadrada y ancha) y heterogeneidad columnar (columnar, mixta y uniforme). Las variaciones de forma alteran la distribución de elementos entre geometrías altas (por ejemplo, 10.000 filas y 100 columnas), cuadradas (por ejemplo, 1.000 filas y columnas) y anchas (por ejemplo, 100 filas y 10.000 columnas). Las variaciones de heterogeneidad de columnas alteran la diversidad de tipos entre columnas (ninguna columna adyacente tiene el mismo tipo), mixta (algunas columnas adyacentes tienen el mismo tipo) y uniforme (todas las columnas tienen el mismo tipo).

El frame-fixtures la biblioteca define un lenguaje específico de dominio para crear DataFrames deterministas generados aleatoriamente para realizar pruebas; Los nueve conjuntos de datos se generan con esta herramienta.

Para demostrar algunas de las interfaces StaticFrame y Pandas evaluadas, la siguiente sesión de IPython realiza pruebas de rendimiento básicas utilizando %time. Como se muestra a continuación, un DataFrame cuadrado de tipo uniforme se puede escribir y leer con NPZ muchas veces más rápido que Parquet sin comprimir.

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

Las pruebas de rendimiento proporcionadas a continuación amplían este enfoque básico utilizando frame-fixtures para la variación sistemática de la forma y la heterogeneidad del tipo, y resultados promedio en diez iteraciones. Si bien la configuración del hardware afectará el rendimiento, las características relativas se conservan en diversas máquinas y sistemas operativos. Para todas las interfaces se utilizan los parámetros predeterminados, excepto para deshabilitar la compresión según sea necesario. El código utilizado para realizar estas pruebas está disponible en GitHub.

Leer rendimiento

Como los datos generalmente se leen con más frecuencia de los que se escriben, el rendimiento de lectura es una prioridad. Como se muestra para los nueve DataFrames de un millón (1e+06) elementos, NPZ supera significativamente a Parquet y Feather en cada partida. El rendimiento de lectura de NPZ es diez veces más rápido que el de Parquet comprimido. Por ejemplo, con el dispositivo Uniform Tall, la lectura de Parquet comprimido es de 21 ms en comparación con 1,5 ms con NPZ.

El siguiente gráfico muestra el tiempo de procesamiento, donde las barras inferiores corresponden a un rendimiento más rápido.

Este impresionante rendimiento del NPZ se mantiene a escala. Pasando a 100 millones (1e+08) elementos, NPZ continúa funcionando al menos dos veces más rápido que Parquet y Feather, independientemente de si se utiliza compresión.

Rendimiento de escritura

Al escribir DataFrames en el disco, NPZ supera a Parquet (tanto comprimido como sin comprimir) en todos los escenarios. Por ejemplo, con el dispositivo Uniform Square, la escritura Parquet comprimida es de 200 ms en comparación con 18,3 ms con NPZ. El rendimiento de escritura de NPZ es generalmente comparable al de Feather sin comprimir: en algunos escenarios, NPZ es más rápido, en otros, Feather es más rápido.

Al igual que con el rendimiento de lectura, el rendimiento de escritura NPZ se conserva con la escala. Pasando a 100 millones (1e+08) elementos, NPZ sigue siendo al menos dos veces más rápido que Parquet, independientemente de si se utiliza compresión o no.

Rendimiento idiosincrásico

Como referencia adicional, también compararemos los mismos datos de viajes en taxi amarillo de Nueva York (de enero de 2010) utilizados en McKinney y Richardson (2020). Este conjunto de datos contiene casi 300 millones (3e+08) de elementos en un DataFrame alto, escrito de forma heterogénea, de 14.863.778 filas y 19 columnas.

Se ha demostrado que el rendimiento de lectura de NPZ es aproximadamente cuatro veces más rápido que el de Parquet y Feather (con o sin compresión). Si bien el rendimiento de escritura de NPZ es más rápido que el de Parquet, la escritura de Feather aquí es más rápida.

Tamaño del archivo

Como se muestra a continuación para DataFrames de un millón (1e+06) de elementos y 100 millones (1e+08) de elementos, NPZ sin comprimir generalmente tiene el mismo tamaño en el disco que Feather sin comprimir y siempre es más pequeño que Parquet sin comprimir (a veces también más pequeño que Parquet comprimido). Como la compresión proporciona sólo modestas reducciones en el tamaño de los archivos para Parquet y Feather, el beneficio de NPZ sin comprimir en cuanto a velocidad podría fácilmente superar el costo de un mayor tamaño.

StaticFrame almacena datos como una colección de matrices NumPy 1D y 2D. Las matrices representan valores de columnas, así como índices de profundidad variable y etiquetas de columnas. Además de las matrices NumPy, información sobre los tipos de componentes (es decir, la clase Python utilizada para el índice y las columnas), así como el componente. name atributos, son necesarios para reconstruir completamente una Frame. La serialización completa de un DataFrame requiere escribir y leer estos componentes en un archivo.

Los componentes de DataFrame se pueden representar mediante el siguiente diagrama, que aísla matrices, tipos de matrices, tipos de componentes y nombres de componentes. Este diagrama se utilizará para demostrar cómo una NPZ codifica un DataFrame.

Los componentes de ese diagrama se asignan a los componentes de un Frame Representación de cadenas en Python. Por ejemplo, dado un Frame de números enteros y booleanos con etiquetas jerárquicas tanto en el índice como en las columnas (descargable a través de GitHub con StaticFrame’s WWW interfaz), StaticFrame proporciona la siguiente representación de cadena:

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

Los componentes de la representación de cadenas se pueden asignar al diagrama de DataFrame por color:

Codificación de una matriz en NPY

Un NPY almacena una matriz NumPy como un archivo binario con seis componentes: (1) un prefijo «mágico», (2) un número de versión, (3) una longitud del encabezado y (4) un encabezado (donde el encabezado es una representación de cadena de un diccionario de Python) y (5) relleno seguido de (6) datos de bytes de matriz sin procesar. Estos componentes se muestran a continuación para una matriz binaria de tres elementos almacenada en un archivo llamado «__blocks_1__.npy».

Dado un archivo NPZ llamado «frame.npz», podemos extraer los datos binarios leyendo el archivo NPY del NPZ con la biblioteca estándar. 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

Como NPY está bien soportado en NumPy, el np.load() La función se puede utilizar para convertir este archivo en una matriz NumPy. Esto significa que los lectores alternativos pueden extraer fácilmente los datos de matriz subyacentes en un StaticFrame NPZ.

>>> with ZipFile('/tmp/frame.npz') as zf: print(repr(np.load(zf.open('__blocks_1__.npy'))))
array([False, True, True])

Como un archivo NPY puede codificar cualquier matriz, se pueden cargar grandes matrices bidimensionales a partir de datos de bytes contiguos, lo que proporciona un rendimiento excelente en StaticFrame cuando varias columnas contiguas están representadas por una única matriz.

Construyendo un archivo NPZ

Un StaticFrame NPZ es un archivo ZIP estándar sin comprimir que contiene datos de matriz en archivos NPY y metadatos (que contienen tipos y nombres de componentes) en un archivo JSON.

Dado el expediente NPZ de la Frame arriba, podemos enumerar su contenido con ZipFile. El archivo contiene seis archivos NPY y un archivo 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']

La siguiente ilustración asigna estos archivos a los componentes del diagrama de DataFrame.

StaticFrame amplía el formato NPZ para incluir metadatos en un archivo JSON. Este archivo define atributos de nombre, tipos de componentes y recuentos de profundidad.

>>> 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]}'

En la siguiente ilustración, los componentes del __meta__.json El archivo se asigna a los componentes del diagrama DataFrame.

Al ser un simple archivo ZIP, las herramientas para extraer el contenido de un StaticFrame NPZ son omnipresentes. Por otro lado, el formato ZIP, dada su historia y sus amplias características, genera una sobrecarga de rendimiento. StaticFrame implementa un lector ZIP personalizado optimizado para el uso de NPZ, lo que contribuye al excelente rendimiento de lectura de NPZ.

El rendimiento de la serialización de DataFrame es fundamental para muchas aplicaciones. Si bien Parquet tiene un amplio apoyo, su generalidad compromete la especificidad y el rendimiento del tipo. StaticFrame NPZ puede leer y escribir DataFrames hasta diez veces más rápido que Parquet con o sin compresión, con tamaños de archivo similares (o sólo ligeramente más grandes). Si bien Feather es una alternativa atractiva, el rendimiento de lectura de NPZ sigue siendo generalmente el doble de rápido que Feather. Si la E/S de datos es un cuello de botella (y a menudo lo es), StaticFrame NPZ ofrece una solución.