Saltar al contenido

lección 3

Parquet: el formato que cambió las reglas del juego

Row groups, column chunks, compresión, predicate pushdown, estadísticas y demo completa con PyArrow. El formato que usarás cada día.

60 min

Si tuviera que elegir una sola tecnología que todo ingeniero de datos debe dominar, sería Parquet. No Spark, no Airflow, no Kubernetes — Parquet. Es el formato de archivo que silenciosamente revolucionó toda la industria de datos. Cada data lake moderno usa Parquet. Cada motor de consultas lo soporta. Y entender CÓMO funciona por dentro te dará una ventaja brutal a la hora de optimizar tus pipelines.

Hoy vamos a abrir el capó de Parquet y mirar dentro. No nos conformamos con "es un formato columnar" — vamos a entender los row groups, los column chunks, los page headers, la compresión, el encoding, las estadísticas embebidas y el predicate pushdown. Y lo vamos a hacer con código real usando PyArrow.

### Un momento: ¿de dónde sale PyArrow y por qué no sigo usando Pandas?

Antes de meternos en el capó, un puente importante. En la Skill 9 leíste Parquet con una sola línea: pd.read_parquet("ventas.parquet"). Simple, cómodo, sin dramas. Lo que quizá no sabías es que, por debajo, Pandas NO sabe leer Parquet por sí mismo — le pasa el trabajo a una librería llamada PyArrow. Pandas era la recepción del hotel; PyArrow es el motor que estaba en el sótano haciendo el trabajo pesado todo este tiempo. Simplemente no lo veías.

Entonces, ¿por qué bajamos ahora un nivel y empezamos a llamar a PyArrow directamente? Porque pd.read_parquet te da los datos, pero te oculta TODO lo interesante: no puedes controlar el esquema exacto (¿int32 o int64?), no decides cómo se agrupan las filas en row groups, no eliges la compresión con precisión, ni inspeccionas las estadísticas embebidas. Para leer un CSV y hacer un análisis rápido, Pandas sobra. Pero para CONSTRUIR el lake — decidir cómo se escriben los archivos que vivirán años y que leerán Spark, Athena y DuckDB — necesitas el volante, no el asiento del copiloto. Ese volante es PyArrow.

La buena noticia: la API de PyArrow es más pequeña de lo que parece. Gira en torno a tres conceptos que verás una y otra vez. Un pa.array es una columna tipada (como una Serie de Pandas, pero con el tipo bajo tu control). Un pa.schema es la definición de la tabla: qué columnas hay y de qué tipo es cada una. Y una pa.table es el conjunto de columnas + esquema, lista para escribirse a disco. Si entiendes estos tres, ya sabes el 80% de PyArrow.

1import pyarrow as pa
2import pyarrow.parquet as pq
3
4# 1) pa.array: una columna tipada. Fíjate en type=pa.int32():
5# NO dejamos que infiera int64 (8 bytes) — pedimos int32 (4 bytes).
6edades = pa.array([28, 35, 42, 29], type=pa.int32())
7nombres = pa.array(["Ana", "Bob", "Eva", "Carlos"]) # string por defecto
8
9# 2) pa.schema: el "contrato" de la tabla. Nombre + tipo de cada columna.
10esquema = pa.schema([
11 ("nombre", pa.string()),
12 ("edad", pa.int32()),
13])
14
15# 3) pa.table: columnas + esquema = tabla lista para persistir.
16tabla = pa.table({"nombre": nombres, "edad": edades}, schema=esquema)
17print(tabla.schema) # ves los tipos EXACTOS que tú decidiste
18
19# 4) pq.write_table: lo que Pandas hacía por debajo, ahora bajo tu control.
20pq.write_table(tabla, "personas.parquet", compression="snappy")
21
22# Y para volver a Pandas cuando quieras la comodidad, sigue ahí:
23# import pandas as pd; df = pd.read_parquet("personas.parquet")

Los tres pilares de PyArrow (array, schema, table) puenteados desde el pd.read_parquet que ya conocías

Regla mental para no agobiarte: si el objetivo es MIRAR datos, usa Pandas (pd.read_parquet). Si el objetivo es ESCRIBIR datos que otros van a leer durante años, baja a PyArrow para controlar tipos y compresión. No es que uno sea mejor — es que resuelven momentos distintos del trabajo. De hecho, cuando haces df.to_parquet() Pandas llama a pq.write_table() por ti; hoy simplemente le quitamos el intermediario.

### El problema: por qué CSV no escala

Imagina que tienes una tabla de ventas con 20 columnas y 100 millones de filas. Almacenada como CSV, pesa unos 15 GB. Ahora el analista quiere sumar la columna "importe" — solo esa columna, de las 20 disponibles. Con CSV, el motor tiene que leer los 15 GB enteros, parsear cada línea (split por comas), extraer el campo 7 (importe), convertirlo de string a número, y sumarlo. Está leyendo 19 columnas que NO necesita.

Es como si para saber la edad de todos los empleados de una empresa, tuvieras que leer la ficha completa de cada uno — nombre, dirección, historial laboral, foto — solo para extraer un número. Con un formato columnar, solo lees la "columna de edades", sin tocar nada más.

### Row storage vs Column storage

La diferencia fundamental entre CSV/JSON (formatos de fila) y Parquet (formato columnar) es cómo se organizan los bytes en disco:

  • Row storage (CSV): almacena todas las columnas de la fila 1, luego todas las columnas de la fila 2, etc. Bueno para leer filas completas (OLTP), malo para analítica.
  • Column storage (Parquet): almacena TODOS los valores de la columna 1 juntos, luego todos los valores de la columna 2 juntos, etc. Bueno para analítica, malo para acceso por fila individual.
Con almacenamiento columnar, leer una sola columna de 20 solo requiere ~5% del I/O total

### Anatomía interna de un archivo Parquet

Un archivo Parquet no es simplemente "columnas una detrás de otra". Tiene una estructura interna sofisticada diseñada para maximizar la eficiencia de lectura en datasets masivos. Vamos a diseccionarlo capa por capa:

  1. 01.File: el archivo completo .parquet. Contiene un Magic Number al principio y al final ("PAR1") para identificarse.
  2. 02.Row Group: el archivo se divide en "grupos de filas" (row groups). Cada row group contiene un subconjunto de las filas totales (típicamente 128MB de datos). Esto permite leer el archivo en paralelo — cada worker procesa un row group diferente.
  3. 03.Column Chunk: dentro de cada row group, cada columna se almacena como un "trozo de columna" (column chunk). Es decir: row group 1 tiene un chunk para "nombre", otro para "edad", otro para "ciudad".
  4. 04.Page: cada column chunk se divide en páginas (pages) de ~1MB. Las páginas son la unidad mínima de lectura/descompresión. Cada página tiene un header con estadísticas (min, max, null count).
  5. 05.Footer: al final del archivo hay un footer con todo el esquema (nombres de columna, tipos, encoding) y un índice de offsets para localizar cada row group y column chunk sin recorrer el archivo.

Esta estructura jerárquica (file → row group → column chunk → page) es la clave de la eficiencia de Parquet. El motor de consultas lee PRIMERO el footer (unos pocos KB al final del archivo), entiende el esquema y la ubicación de cada columna, y después hace seeks directos a las columnas que necesita, saltándose el resto.

### Compresión y encoding: por qué Parquet es tan pequeño

Parquet consigue ratios de compresión brutales (5-10x sobre CSV) gracias a dos técnicas que se potencian mutuamente: encoding a nivel de columna y compresión a nivel de página.

  • Dictionary encoding: si una columna tiene pocos valores únicos (ej: "país" con 50 valores posibles entre millones de filas), Parquet crea un diccionario y almacena solo los índices. En vez de guardar "España" 10 millones de veces, guarda el número 7 (el índice en el diccionario).
  • Run-length encoding (RLE): si los datos están ordenados, secuencias de valores repetidos se comprimen a "valor + cantidad". Ideal para columnas de baja cardinalidad ya ordenadas.
  • Delta encoding: para columnas numéricas secuenciales (timestamps, IDs), almacena solo las diferencias entre valores consecutivos.
  • Bit packing: si los valores caben en menos bits que el tipo nativo (ej: una columna INT32 donde todos los valores son 0-100), usa solo los bits necesarios.
  • Compresión de página: además del encoding, cada página se comprime con Snappy (rápido), ZSTD (mejor ratio), LZ4, o GZIP. Snappy es el default por su balance velocidad/compresión.
1import pyarrow as pa
2import pyarrow.parquet as pq
3import os
4
5# Crear datos de ejemplo: 1 millón de filas
6import random
7n = 1_000_000
8data = {
9 "id": list(range(n)),
10 "pais": [random.choice(["España", "Francia", "Italia", "Alemania"]) for _ in range(n)],
11 "importe": [round(random.uniform(10, 500), 2) for _ in range(n)],
12 "fecha": [f"2024-01-{random.randint(1,31):02d}" for _ in range(n)],
13}
14
15table = pa.table(data)
16
17# Guardar como CSV para comparar
18import csv
19with open("ventas.csv", "w", newline="") as f:
20 writer = csv.DictWriter(f, fieldnames=data.keys())
21 writer.writeheader()
22 for i in range(n):
23 writer.writerow({k: v[i] for k, v in data.items()})
24
25# Guardar como Parquet con diferentes compresiones
26for comp in ["NONE", "SNAPPY", "ZSTD", "GZIP"]:
27 pq.write_table(table, f"ventas_{comp.lower()}.parquet", compression=comp)
28
29# Comparar tamaños
30csv_size = os.path.getsize("ventas.csv") / 1024 / 1024
31print(f"CSV: {csv_size:.1f} MB")
32for comp in ["NONE", "SNAPPY", "ZSTD", "GZIP"]:
33 size = os.path.getsize(f"ventas_{comp.lower()}.parquet") / 1024 / 1024
34 ratio = csv_size / size
35 print(f"Parquet ({comp:6s}): {size:.1f} MB ({ratio:.1f}x más pequeño)")

Comparación de tamaño: CSV vs Parquet con diferentes compresiones

Usa ZSTD como compresión en casi todo: es más pequeño que SNAPPY y se lee igual de rápido o más (porque mueve menos bytes). SNAPPY solo gana en velocidad de escritura (~20% más rápido al escribir), así que úsalo si escribes mucho más de lo que lees. GZIP comprime peor que ZSTD y es 40× más lento escribiendo: no lo uses salvo para compatibilidad con sistemas legacy. En el ejercicio 4 de esta lección lo verás con números.

### Predicate pushdown: la magia de leer solo lo necesario

Aquí está el truco más poderoso de Parquet y que muy pocos juniors entienden: predicate pushdown. Cada column chunk almacena estadísticas en su header: el valor mínimo, el valor máximo y la cantidad de nulls. Cuando haces una query con filtro (ej: WHERE fecha = "2024-01-15"), el motor de consultas puede mirar las estadísticas del column chunk y decidir: "el mínimo de fecha en este row group es 2024-02-01, así que SEGURO no contiene datos del 15 de enero → me lo salto entero".

Esto significa que una query que filtra por fecha en un archivo con 100 row groups podría leer solo 3 o 4 row groups — saltándose el 96% de los datos sin siquiera descomprimirlos. Es como si el índice de un libro no solo te dijera "capítulo 5, página 87" sino que además te dijera "en este capítulo la temperatura va de 15°C a 25°C, así que si buscas algo de 40°C no hace falta que lo abras".

1import pyarrow.parquet as pq
2
3# Leer metadata sin cargar datos
4parquet_file = pq.ParquetFile("ventas_snappy.parquet")
5metadata = parquet_file.metadata
6
7print(f"Filas totales: {metadata.num_rows:,}")
8print(f"Row groups: {metadata.num_row_groups}")
9print(f"Columnas: {metadata.num_columns}")
10print(f"Tamaño: {metadata.serialized_size:,} bytes\n")
11
12# Ver estadísticas de un row group
13for i in range(min(2, metadata.num_row_groups)):
14 rg = metadata.row_group(i)
15 print(f"--- Row Group {i} ({rg.num_rows:,} filas) ---")
16 for j in range(rg.num_columns):
17 col = rg.column(j)
18 if col.statistics:
19 stats = col.statistics
20 print(f" {col.path_in_schema}: min={stats.min}, max={stats.max}, nulls={stats.null_count}")

Inspeccionar metadata y estadísticas de un archivo Parquet con PyArrow

Predicate pushdown solo funciona bien si los datos están ORDENADOS por la columna que filtras. Si la columna "fecha" tiene valores mezclados aleatoriamente en cada row group, las estadísticas min/max serán amplias y no se podrá saltar ningún row group. Siempre ordena por la columna que más se filtra ANTES de escribir el Parquet.

### Column projection: lee solo las columnas que necesitas

La segunda optimización clave es column projection: leer solo las columnas que tu query necesita. En un archivo con 50 columnas, si solo necesitas 3, Parquet salta directamente a esas 3 column chunks y no toca las otras 47. Con PyArrow:

1import pyarrow.parquet as pq
2import time
3
4# Leer TODAS las columnas
5start = time.time()
6df_all = pq.read_table("ventas_snappy.parquet").to_pandas()
7t_all = time.time() - start
8
9# Leer SOLO 2 columnas
10start = time.time()
11df_partial = pq.read_table(
12 "ventas_snappy.parquet",
13 columns=["pais", "importe"] # Solo las que necesito
14).to_pandas()
15t_partial = time.time() - start
16
17print(f"Todas las columnas ({len(df_all.columns)}): {t_all:.3f}s")
18print(f"Solo 2 columnas: {t_partial:.3f}s")
19print(f"Speedup: {t_all/t_partial:.1f}x")

Column projection: leer solo las columnas necesarias acelera dramáticamente la lectura

### PyArrow: tu herramienta para trabajar con Parquet

PyArrow es la librería de referencia para trabajar con Parquet en Python. Es el binding de Python para Apache Arrow — un formato en memoria columnar que actúa como "lingua franca" entre herramientas de datos. Pandas, Spark, DuckDB, Polars — todos hablan Arrow internamente.

1import pyarrow as pa
2import pyarrow.parquet as pq
3
4# Crear tabla Arrow desde diccionario
5table = pa.table({
6 "cliente_id": [1, 2, 3, 4, 5],
7 "nombre": ["Ana", "Bob", "Carlos", "Diana", "Eva"],
8 "gasto_total": [1500.50, 890.00, 2340.75, 450.25, 3100.00],
9 "segmento": ["gold", "silver", "gold", "bronze", "gold"],
10})
11
12# Escribir como Parquet
13pq.write_table(table, "clientes.parquet", compression="snappy")
14
15# Leer de vuelta
16table_leida = pq.read_table("clientes.parquet")
17print(table_leida.to_pandas())
18
19# Ver el esquema (tipos)
20print("\nEsquema:")
21print(table_leida.schema)

Crear, escribir y leer archivos Parquet con PyArrow

Instala pyarrow con: pip install pyarrow. En 🟦 Windows PowerShell: pip install pyarrow. En 🍎 Mac Terminal: pip install pyarrow. Es la misma librería en ambos. PyArrow viene incluido si ya tienes Pandas instalado con pip install pandas[all].

### Parquet vs otros formatos: cuándo usar cada uno

  • CSV: solo para intercambio simple con humanos o sistemas legacy que no entienden otra cosa. Nunca para almacenamiento analítico.
  • JSON: para APIs, logs y datos con estructura variable. Malo para analítica por su overhead de parsing.
  • Parquet: el estándar para almacenamiento analítico. Úsalo siempre que puedas.
  • ORC: alternativa a Parquet del ecosistema Hive. Funcionalmente similar pero menos adoptado fuera de Hadoop.
  • Avro: orientado a filas con soporte de evolución de esquema. Ideal para streaming y mensajería (Kafka).

En la práctica moderna: Parquet para el lake (almacenamiento analítico), Avro para streaming (Kafka), JSON para APIs. CSV solo como "último recurso" para compatibilidad con Excel.

## ejercicios

[01]

Crear tu primer Parquet con PyArrow

El equipo de producto te pasa un CSV con los datos del Black Friday. Conviértelo a Parquet con los tipos correctos y compara tamaños.

Cargando editor...
[02]

Inspector de metadata Parquet

Escribe una función que actúe como "inspector" de archivos Parquet: muestra row groups, estadísticas por columna y compresión usada.

Cargando editor...
[03]

Demostrar predicate pushdown en acción

Crea un Parquet con datos ordenados y demuestra cómo el predicate pushdown salta row groups enteros. Mide la diferencia de rendimiento.

Cargando editor...
[04]

Benchmark de compresiones Parquet

El equipo de infra necesita decidir qué compresión usar en el lake. Crea un benchmark que compare NONE, SNAPPY, ZSTD y GZIP midiendo tamaño, velocidad de escritura y velocidad de lectura.

Cargando editor...

Regístrate para guardar tu progreso.

## comentarios

Reporta erratas, ayuda a otros o comparte tu opinión. Sé constructivo.

Inicia sesión para comentar y responder.

cargando comentarios...