~/bda/m01 $ cat 05-formato-de-datos2.md
- Big Data Aplicado
- M01
- lección 05/09
Formatos de datos
Queremos guardar los mismos datos en distintos formatos y comprobar cómo cambian su tamaño y su tiempo de escritura. Primero veremos ejemplos pequeños y después realizarás una práctica de hasta dos horas, inspirada en el ejercicio del curso anterior.
Los ejemplos utilizan tres ventas para presentar las instrucciones de cada formato. En la práctica aplicarás esas operaciones al archivo original de compras y comprobarás cómo se comporta el programa con todos sus registros.
Filas y columnas
Section titled “Filas y columnas”En un formato por filas, cada registro guarda juntos sus campos. CSV, JSONL y Avro siguen esa organización. Resulta útil cuando queremos intercambiar o procesar registros completos.
En un formato por columnas, los valores de un campo se agrupan dentro de unidades del archivo. Parquet y ORC siguen esta organización. Un lector puede solicitar determinadas columnas para una consulta analítica.
En un archivo columnar también existen filas lógicas. Lo que cambia es cómo se almacenan sus valores. Parquet utiliza grupos de filas y ORC utiliza stripes; dentro de esas unidades, los datos se organizan por columnas.
La figura utiliza unas compras de fruta. A la izquierda se recorren las líneas del CSV; a la derecha se solicitan fecha y precio del Parquet. El dibujo simplifica los grupos internos de los archivos.
Preparar el entorno y una muestra pequeña
Section titled “Preparar el entorno y una muestra pequeña”En el anfitrión, entra en BigDataAplicadoLab_2627/modulo01/ y abre el contenedor de formatos.
make formatos-shellSin Make, utiliza esta orden desde la misma carpeta.
docker compose -f formatos/compose.yaml run --rm --build formatos bashLa imagen bda-m01-formatos:3.12-r3 contiene Python 3.12.8, pandas 2.2.3, PyArrow 18.1.0, fastavro 1.10.0, nano, vi, curl, wget y gzip. Dentro del contenedor trabajas como alumno. El montaje /lab corresponde a modulo01/ del anfitrión y conserva los archivos al salir.
Dentro del contenedor, crea una carpeta para esta propuesta.
mkdir -p /lab/trabajo/formatos2cd /lab/trabajo/formatos2Crea el archivo siguiente con nano ventas_ejemplo.csv o vi ventas_ejemplo.csv.
ventas_ejemplo.csv
tienda,categoria,importeSan Jose,Ropa,214.05Fort Worth,Ropa,153.57Fort Worth,Juguetes,213.88Cada script de ejemplo se guarda y ejecuta en esta carpeta. Usaremos la misma fuente para observar cada formato.
Una primera mirada a pandas
Section titled “Una primera mirada a pandas”pandas es una biblioteca de Python para trabajar con datos organizados en tablas. Permite leer archivos, seleccionar columnas, filtrar filas, realizar cálculos y guardar los resultados en distintos formatos. Ya está instalada en el contenedor.
Su estructura principal es el DataFrame, una tabla con filas y columnas con nombre. En nuestro ejemplo, cada fila representa una venta y las columnas son tienda, categoria e importe.
Crea este script para abrir el CSV que acabas de preparar e inspeccionar su tabla.
import pandas as pd
# Leer archivo CSV y crear un DataFrameventas = pd.read_csv('ventas_ejemplo.csv')
print(ventas)print('Filas y columnas:', ventas.shape)print('Tipos de datos:')print(ventas.dtypes)
# Seleccionar una columnaprint(ventas['importe'])
# Seleccionar varias columnas y mantener una tablaprint(ventas[['tienda', 'importe']])python introduccion_pandas.pyimport pandas as pd permite utilizar la biblioteca con el nombre corto pd. pd.read_csv(...) lee el archivo y devuelve un DataFrame que guardamos en la variable ventas.
ventas.shape devuelve (3, 3), tres filas y tres columnas. ventas.dtypes muestra el tipo de cada columna. Una selección como ventas['importe'] devuelve una columna, denominada Series en pandas. Con una lista de nombres entre los corchetes, ventas[['tienda', 'importe']] devuelve otro DataFrame con esas dos columnas.
En los ejemplos siguientes utilizaremos métodos como ventas.to_parquet(...) para guardar la tabla. El método se llama sobre el DataFrame que contiene los datos.
CSV y JSONL — registros en texto
Section titled “CSV y JSONL — registros en texto”CSV organiza los registros en líneas y separa sus campos mediante un delimitador. Su cabecera identifica las columnas; el lector decide sus tipos.
JSONL guarda un objeto JSON por línea. Es práctico para registros de aplicaciones, eventos y exportaciones de documentos porque permite añadir y procesar objetos uno a uno. Un documento JSON convencional puede contener una lista completa entre corchetes; JSONL utiliza el salto de línea como separación entre documentos independientes.
JSONL está orientado a registros. Conserva la sintaxis JSON de cada objeto, pero establece que el salto de línea separa un registro del siguiente. El archivo completo se lee como una secuencia de documentos JSON, uno por línea.
Esto resulta práctico para registros de aplicaciones, exportaciones de muchos documentos y eventos procesados en secuencia.
Crear y leer un JSONL con pandas
Section titled “Crear y leer un JSONL con pandas”import pandas as pd
# Leer archivo CSV de la muestra pequeñaventas = pd.read_csv('ventas_ejemplo.csv')
# Crear archivo con un objeto JSON por líneaventas.to_json('ventas_ejemplo.jsonl', orient='records', lines=True)
# Leer archivo JSONLrecuperadas = pd.read_json('ventas_ejemplo.jsonl', lines=True)print(recuperadas)python ejemplo_jsonl.pypd.read_csv carga los datos en un DataFrame, una tabla con filas y columnas. to_json escribe esa tabla; orient='records' representa cada fila como un objeto y lines=True separa los objetos mediante saltos de línea. Aquí pandas carga la muestra completa, aunque el formato JSONL también permite lectura incremental con otras interfaces.
Avro — registros binarios con esquema
Section titled “Avro — registros binarios con esquema”También orientado a filas, Avro guarda registros en binario. En un archivo contenedor .avro, el esquema queda incluido en la cabecera y declara los nombres y tipos de los campos. string representa texto y double representa un número de coma flotante de doble precisión.
Crear y leer un Avro
Section titled “Crear y leer un Avro”import pandas as pdfrom fastavro import writer, reader
ventas = pd.read_csv('ventas_ejemplo.csv')esquema = { 'type': 'record', 'name': 'Venta', 'fields': [ {'name': 'tienda', 'type': 'string'}, {'name': 'categoria', 'type': 'string'}, {'name': 'importe', 'type': 'double'}, ],}
# Crear archivo binario a partir de los registros de la tablawith open('ventas_ejemplo.avro', 'wb') as salida: writer(salida, esquema, ventas.to_dict('records'), codec='null')
# Leer archivo registro a registrowith open('ventas_ejemplo.avro', 'rb') as entrada: for venta in reader(entrada): print(venta)python ejemplo_avro.pyto_dict('records') convierte el DataFrame en una lista de diccionarios, uno por fila. Esta conversión utiliza memoria y por eso la aplicamos a una muestra limitada. Los modos wb y rb indican escritura y lectura binarias. codec='null' escribe sin compresión.
Avro también permite que un lector utilice un esquema compatible con el de escritura. Por ejemplo, un campo nuevo puede recibir un valor por defecto al leer datos antiguos. Esa capacidad requiere diseñar versiones compatibles del esquema.
Este fragmento se puede añadir a ejemplo_avro.py después de definir esquema. Construye la nueva versión del esquema; su uso como esquema de lectura requiere pasarlo a reader(..., reader_schema=esquema_nuevo).
# Fragmento de evolución del esquema de ejemplo_avro.pyesquema_nuevo = { **esquema, 'fields': esquema['fields'] + [ {'name': 'canal', 'type': 'string', 'default': 'desconocido'}, ],}Parquet — columnas para el análisis
Section titled “Parquet — columnas para el análisis”Parquet organiza los datos por columnas dentro de grupos de filas. Es habitual en históricos analíticos, donde una consulta necesita unas pocas columnas de muchas filas.
Por ejemplo, si solo queremos sumar importe por categoria, el lector puede solicitar esas dos columnas. Esta lectura selectiva evita recuperar producto, unidades y precio.
Crear y leer un Parquet
Section titled “Crear y leer un Parquet”import pandas as pd
ventas = pd.read_csv('ventas_ejemplo.csv')
# Crear archivo Parquet sin compresiónventas.to_parquet('ventas_ejemplo.parquet', index=False, compression=None)
# Leer archivo completorecuperadas = pd.read_parquet('ventas_ejemplo.parquet')print(recuperadas)
# Leer únicamente las columnas solicitadasreducidas = pd.read_parquet('ventas_ejemplo.parquet', columns=['tienda', 'importe'])print(reducidas)python ejemplo_parquet.pyindex=False evita guardar el índice de filas que pandas crea al cargar la tabla. compression=None desactiva la compresión. columns selecciona las columnas que queremos recuperar.
ORC — columnas organizadas en stripes
Section titled “ORC — columnas organizadas en stripes”ORC, Optimized Row Columnar, es un formato binario orientado a columnas utilizado en el ecosistema de Hive. Para organizar un archivo grande, divide sus registros en conjuntos llamados stripes.
Imagina seis ventas. Un primer stripe puede contener las ventas 1 a 3 y un segundo las ventas 4 a 6. Dentro de cada stripe se guardan por separado los valores de cada columna, por ejemplo los tres productos juntos y los tres importes juntos. Cada stripe incluye también índices y un pie con información que ayuda a interpretar sus datos. Al final del archivo hay metadatos para localizar los stripes y conocer el esquema.
El dibujo muestra dos stripes uno junto al otro. Dentro de cada uno, los valores de producto, unidades e importe aparecen en columnas verticales separadas. La consulta solicita importe, resaltado en verde, en ambos stripes; puede omitir los datos de las columnas producto y unidades. Los índices, los pies y los metadatos permiten localizar e interpretar esas columnas. Los límites de tres ventas del dibujo son ilustrativos; el tamaño real de los stripes lo determina el escritor y su configuración.
Crear y leer un ORC
Section titled “Crear y leer un ORC”import pandas as pd
ventas = pd.read_csv('ventas_ejemplo.csv')
# Crear archivo ORC sin compresiónventas.to_orc('ventas_ejemplo.orc', index=False, engine_kwargs={'compression': 'uncompressed'})
# Leer archivo ORCrecuperadas = pd.read_orc('ventas_ejemplo.orc')print(recuperadas)python ejemplo_orc.pyengine_kwargs pasa opciones al escritor de PyArrow que utiliza pandas. Para probar Snappy, sustituye 'uncompressed' por 'snappy' y utiliza otro nombre de archivo.
Comparar tamaño, escritura y compresión
Section titled “Comparar tamaño, escritura y compresión”Un formato determina cómo se organizan los datos. Un códec, como Snappy, comprime su representación. Para distinguir sus efectos, compararemos ORC sin compresión con ORC comprimido, y Avro con Parquet usando las mismas columnas y sin compresión.
Podemos medir una escritura con este patrón. Crea y ejecuta el ejemplo después de los anteriores.
from pathlib import Pathfrom time import perf_counterimport pandas as pd
# Leer la fuente antes de iniciar el cronómetroventas = pd.read_csv('ventas_ejemplo.csv')
# Crear archivo y medir solo la escritura desde el DataFrameinicio = perf_counter()ventas.to_parquet('medida.parquet', index=False, compression=None)segundos = perf_counter() - inicio
print('Tiempo de escritura:', segundos, 's')print('Tamaño:', Path('medida.parquet').stat().st_size / 1024**2, 'MiB')python medir_escritura.pyperf_counter mide duraciones. La resta da el tiempo transcurrido durante la escritura. stat().st_size devuelve bytes y dividimos entre 1024**2 para obtener MiB. Mantén fuera del cronómetro la descarga, la carga inicial y la preparación de la tabla que vayas a escribir.
Los metadatos pueden hacer que un formato binario ocupe más que el texto con tres filas. Con datos mayores cambian las proporciones. Los tamaños y tiempos observados describen ese archivo, esas opciones y ese equipo.
Particionar para leer solo el período necesario
Section titled “Particionar para leer solo el período necesario”Supongamos que guardamos ventas de muchos días y por lo general, las consulta suelen ser de un determinado día. Existe la posibilidad de agrupar los archivos por fecha en vez de colocar todo el histórico en una única carpeta.
Esta organización puede utilizarse con CSV, JSONL, Avro, Parquet u ORC. El particionado organiza un conjunto de archivos en carpetas según un criterio, independientemente del formato de cada archivo. En el dibujo usamos Parquet, pero las carpetas también podrían contener archivos .csv, .jsonl, .avro o .orc.
historico/├── fecha=2026-04-19/│ └── part-000.parquet├── fecha=2026-04-20/│ └── part-000.parquet└── fecha=2026-04-21/ └── part-000.parquetCada carpeta contiene las ventas de su fecha. Esta organización se llama particionado. Un motor configurado para reconocer esas particiones puede resolver el filtro de fecha leyendo la carpeta fecha=2026-04-20 y omitiendo las otras dos.
El siguiente fragmento ilustra la consulta de un conjunto ya particionado, como el del dibujo. Requiere import pyarrow.dataset as ds y que destino identifique su carpeta. El conjunto del ejemplo debe contener fecha, producto e importe.
# Leer por lotes únicamente la fecha solicitadafecha_elegida = '2026-04-20'historico = ds.dataset(destino, format='parquet', partitioning='hive')
def leer_seleccion(): consulta = historico.scanner( filter=ds.field('fecha') == fecha_elegida, columns=['fecha', 'producto', 'importe'], batch_size=50000, use_threads=False, ) for lote in consulta.to_batches(): for venta in lote.to_pylist(): assert venta['fecha'] == fecha_elegida yield ventaM01-FMT-01 — Convertir y comparar el archivo de ventas Aules
Section titled “M01-FMT-01 — Convertir y comparar el archivo de ventas Aules”Queremos comprobar qué cambia cuando pasamos de tres registros a millones de ventas. Vamos a medir cuánto ocupa cada formato, cuánto tarda en generarse y cuánto tarda en leerse. También compararemos leer todos los campos con leer únicamente la tienda y el importe.
0. Qué vamos a medir
Section titled “0. Qué vamos a medir”Para cada archivo anota su tamaño y el tiempo de conversión. Inicia el cronómetro antes de abrir el original y detenlo después de cerrar el escritor. El tiempo incluye descompresión, lectura de las ventas y escritura del nuevo formato. Cada archivo se genera una vez.
Después mide tres lecturas completas de cada archivo y calcula la mediana de sus duraciones. En los tres archivos de seis columnas, mide también tres lecturas solicitando únicamente tienda e importe. Los archivos reducidos ya contienen esos dos campos y solo necesitan la prueba de lectura completa.
El siguiente fragmento muestra cómo medir una lectura Parquet por lotes. Añádelo a tu script para utilizarlo después de crear ventas.parquet. Para los demás formatos adapta la función de lectura usando los lectores de la sección Procesar el archivo completo por bloques.
# Fragmento para medir la lectura de un Parquet ya generadofrom statistics import medianfrom time import perf_counterimport pyarrow.parquet as pq
def leer_archivo(ruta, columnas=None): cantidad = 0 with pq.ParquetFile(ruta) as archivo: for lote in archivo.iter_batches(batch_size=50000, columns=columnas): cantidad += lote.num_rows return cantidad
tiempos = []for repeticion in range(3): inicio = perf_counter() cantidad = leer_archivo('ventas.parquet') tiempos.append(perf_counter() - inicio)
print('Ventas leídas:', cantidad)print('Tiempos:', tiempos)print('Mediana:', median(tiempos), 'segundos')En def leer_archivo(ruta, columnas=None), ruta recibe el nombre del archivo. columnas=None establece que se leerán todas las columnas cuando no indiquemos una selección. iter_batches entrega grupos de filas y lote.num_rows permite contarlas. return cantidad devuelve el recuento cuando termina el recorrido.
tiempos.append(...) añade cada duración a la lista. median(tiempos) obtiene su valor central una vez ordenadas; por ejemplo, para 0,8, 0,6 y 0,7 segundos, la mediana es 0,7 segundos.
La función debe recorrer el archivo hasta el final dentro de la zona cronometrada. Crear un generador sin recorrerlo no mide la lectura. Cada repetición abre de nuevo el archivo e inicia el contador desde cero. median obtiene el valor central de las tres duraciones; conserva también los valores individuales. Las lecturas pueden aprovechar datos en caché de ejecuciones anteriores.
Para la prueba de dos campos, llama al lector con columnas=['tienda', 'importe']. En ORC indica esa selección en read_stripe. El tamaño se obtiene con Path(ruta).stat().st_size / 1024**2, como en el ejemplo de medición anterior.
1. Trabajar con el archivo original
Section titled “1. Trabajar con el archivo original”Dentro del contenedor, desde /lab/trabajo/formatos2, descarga el archivo si todavía no lo tienes disponible.
curl -fL -o purchases.txt.gz https://github.com/josepgarcia/datos/raw/refs/heads/main/purchases.txt.gzPandas puede leer el archivo comprimido directamente. Crea comparar_formatos.py y utiliza esta carga inicial. Ejecuta esta primera carga y observa su comportamiento. Después desarrolla las conversiones en ese mismo script siguiendo las instrucciones del apartado 2.
import pandas as pd
columnas = ['fecha', 'hora', 'tienda', 'categoria', 'importe', 'pago']ventas = pd.read_csv( 'purchases.txt.gz', sep='\t', header=None, names=columnas, keep_default_na=False, dtype={'fecha': str, 'hora': str, 'tienda': str, 'categoria': str, 'importe': 'float64', 'pago': str},)print(ventas.shape)print(ventas.head())print(ventas.dtypes)sep='\t' indica tabuladores. El original carece de cabecera, por eso indicamos header=None y sus nombres mediante names. shape muestra filas y columnas. El archivo original utilizado en el ensayo contiene 4.138.476 ventas y seis columnas. head() muestra las primeras filas y dtypes permite comprobar los tipos.
Cada fila representa una venta. Conserva fecha, hora, tienda, categoría y pago como texto, e importe como float64. Para esta demostración se utilizan números de coma flotante; la precisión contable requiere otra elección de tipos.
Antes de continuar, ejecuta tu programa y observa su resultado. Si no termina como esperabas, conserva los mensajes o el comportamiento observado, investiga la causa y documenta qué has cambiado para poder continuar. Puedes consultar los procedimientos de la sección Procesar el archivo completo por bloques.
2. Crear cinco archivos
Section titled “2. Crear cinco archivos”La sección Procesar el archivo completo por bloques contiene las operaciones de lectura y escritura necesarias para completar este apartado.
Para las conversiones, crea un lector nuevo que entregue una venta cada vez, siguiendo leer_compras() de esa sección. La primera prueba con pandas sirve para observar y documentar el comportamiento de la carga. El lector que utilizarás en las cinco conversiones debe leer el comprimido y convertir importe a float, conservando los otros cinco campos como texto.
Conserva los campos en este orden.
| Posición | Campo | Tipo que debes conservar |
|---|---|---|
| 1 | fecha | texto |
| 2 | hora | texto |
| 3 | tienda | texto |
| 4 | categoria | texto |
| 5 | importe | número de doble precisión |
| 6 | pago | texto |
Debes crear los siguientes archivos:
| Archivo que debes crear | Datos que contiene | Compresión |
|---|---|---|
ventas.parquet |
Todas las ventas y sus seis columnas | Ninguna |
ventas.orc |
Las mismas ventas y seis columnas | Ninguna |
ventas_snappy.orc |
Las mismas ventas y seis columnas | Snappy |
ventas_reducidas.avro |
Las mismas ventas, solo tienda e importe | Ninguna |
ventas_reducidas.parquet |
Las mismas ventas, solo tienda e importe | Ninguna |
Para Parquet y ORC, forma bloques de hasta 50.000 registros y utiliza los escritores de esa sección. Cada archivo debe recibir todas las ventas. Inicia un recorrido nuevo del original para cada producto.
Para el Parquet reducido, usa tabla.select(['tienda', 'importe']) sobre la tabla del bloque actual y declara un esquema con esos dos campos. Para Avro, prepara el esquema de dos campos y entrega un generador al escritor, como en esa sección.
Estas son las opciones de compresión de los escritores que vas a utilizar.
| Producto | Opción del escritor |
|---|---|
| Parquet completo y reducido | ParquetWriter(..., compression='NONE') |
| ORC sin compresión | ORCWriter(..., compression='uncompressed') |
| ORC con Snappy | ORCWriter(..., compression='snappy') |
| Avro reducido | writer(..., codec='null') |
Los esquemas Arrow incluyen los campos de datos que has declarado. Al crearlos desde registros, no se añade el índice de pandas que evitábamos con index=False en los primeros ejemplos.
3. Comprobar los archivos y compararlos
Section titled “3. Comprobar los archivos y compararlos”Vuelve a abrir los archivos con sus lectores. Comprueba que cada uno contiene los 4.138.476 registros del original y las columnas que le corresponden. En Avro puedes contarlos con un bucle sobre reader, como en el ejemplo. Contrasta algunas ventas con las filas iniciales del original.
Completa esta tabla con tus medidas. En los archivos reducidos escribe «No procede» en la última columna, porque su lectura completa ya recupera únicamente tienda e importe. Haz la lectura de comprobación después de detener el cronómetro de conversión. Recorre Parquet por lotes, ORC por stripes y Avro por registros, como se describe en la sección Procesar el archivo completo por bloques.
| Archivo | Filas | Columnas | Compresión | Tamaño MiB | Conversión s | Lectura completa, mediana s | Tienda e importe, mediana s |
|---|---|---|---|---|---|---|---|
| ventas.parquet | |||||||
| ventas.orc | |||||||
| ventas_snappy.orc | |||||||
| ventas_reducidas.avro | No procede | ||||||
| ventas_reducidas.parquet | No procede |
Responde brevemente estas preguntas.
- ¿Qué cambia al pasar de ORC sin compresión a ORC con Snappy? Compara tamaño y tiempos de conversión y lectura.
- Entre Avro y Parquet con tienda e importe, ¿qué diferencias observas? Ambos contienen los mismos datos y están sin compresión.
- ¿Por qué el tamaño de un archivo con dos columnas no permite demostrar por sí solo que su formato sea mejor que otro con seis columnas?
- En los archivos de seis columnas, ¿qué cambia al leer únicamente tienda e importe? Relaciónalo con la organización columnar.
Entrega en Aules
Section titled “Entrega en Aules”Entrega un ZIP con tu script y un informe Markdown que incluya la tabla, los tres tiempos individuales de cada prueba de lectura, las respuestas, cualquier incidencia observada y las versiones de Python, pandas, PyArrow y fastavro utilizadas. Los archivos de datos generados se conservan en tu equipo. Consulta las versiones dentro del contenedor con estas órdenes.
python --versionpython -c "import pandas, pyarrow, fastavro; print(pandas.__version__, pyarrow.__version__, fastavro.__version__)"Procesar el archivo completo por bloques
Section titled “Procesar el archivo completo por bloques”Esta sección contiene los lectores y escritores que necesitas en los apartados 2 y 3 de la práctica M01-FMT-01.
Cuando los datos no caben en memoria
Section titled “Cuando los datos no caben en memoria”Al pasar de los ejemplos pequeños al archivo original hay que considerar la memoria que ocuparán los datos y las copias intermedias del programa, además de su tamaño en disco.
Si el conjunto supera la memoria disponible, se procesa de forma incremental. El programa lee un registro o un bloque, realiza su trabajo, guarda el resultado y continúa con el siguiente. Los acumuladores necesarios se conservan entre pasos, sin reunir todos los registros en una lista.
La unidad de lectura depende del formato.
| Formato | Unidad de lectura | Herramienta |
|---|---|---|
| CSV y JSONL | Un registro de texto | Para CSV, el lector csv de Python, que interpreta delimitadores y campos entrecomillados |
| Avro | Un registro binario | fastavro.reader |
| Parquet | Un lote de filas | ParquetFile.iter_batches |
| ORC | Un stripe | ORCFile.read_stripe |
Pandas también permite leer CSV por bloques mediante el parámetro chunksize. Cada bloque es un DataFrame que debe procesarse antes de solicitar el siguiente. El tamaño del bloque debe ajustarse a los datos y a los recursos del equipo. Procesamiento de datos mayores que la memoria con pandas
Ejemplo con el archivo completo y bloques de ventas
Section titled “Ejemplo con el archivo completo y bloques de ventas”El archivo original de la práctica contiene 4.138.476 ventas. Su carga y las conversiones adicionales pueden necesitar mucha más memoria que los aproximadamente 211 MB del texto descomprimido.
¿Por qué ocupa más en memoria que en disco? En el archivo, cada venta es una línea de texto compacta. En memoria, Python y pandas crean un objeto para cada texto, con información adicional sobre su tipo y su longitud, y algunas operaciones generan copias intermedias de los datos. Por eso la memoria necesaria puede multiplicar varias veces el tamaño del archivo.
Vamos a convertirlo completo con una lectura incremental. Construiremos el script compras_por_bloques.py en tres pasos y lo ejecutaremos al terminar cada uno. Créalo en /lab/trabajo/formatos2, donde guardaste la descarga.
Paso 1. Un lector que entrega una venta cada vez
Section titled “Paso 1. Un lector que entrega una venta cada vez”Empieza el script con este código. La prueba final muestra solo las tres primeras ventas.
# compras_por_bloques.py (paso 1)import csvimport gzipfrom itertools import islice
campos = ['fecha', 'hora', 'tienda', 'categoria', 'importe', 'pago']
# Leer el comprimido y entregar una venta cada vezdef leer_compras(): with gzip.open('purchases.txt.gz', 'rt', encoding='utf-8', newline='') as entrada: for venta in csv.DictReader(entrada, fieldnames=campos, delimiter='\t'): venta['importe'] = float(venta['importe']) yield venta
# Prueba: mostrar las tres primeras ventasfor venta in islice(leer_compras(), 3): print(venta)python compras_por_bloques.pyDebes ver tres diccionarios con las seis claves de campos. El importe aparece como número, sin comillas.
gzip.open(..., 'rt'). Abre el archivo comprimido y entrega su contenido como texto a medida que se lee. No hace falta descomprimirlo previamente en otro archivo.csv.DictReader(..., fieldnames=campos, delimiter='\t'). Lee los registros separados por tabuladores. Como el original no tiene cabecera,fieldnamesaporta los nombres que tendrá cada diccionario.yield venta. Entrega el diccionario de una venta y dejaleer_comprasen pausa en ese punto. Cuando se pide otra venta, la función continúa con la siguiente vuelta del bucle. De ese modo va entregando registros sin guardarlos todos en una lista. Una función que utilizayieldse llama generador.islice(leer_compras(), 3). Pide al generador solo las tres primeras ventas. El resto del archivo no llega a leerse.
Paso 2. Escribir el Parquet por bloques
Section titled “Paso 2. Escribir el Parquet por bloques”Añade estos import al principio del script.
import pyarrow as paimport pyarrow.parquet as pqDespués, sustituye la prueba del paso 1 por este código.
esquema = pa.schema([ ('fecha', pa.string()), ('hora', pa.string()), ('tienda', pa.string()), ('categoria', pa.string()), ('importe', pa.float64()), ('pago', pa.string()),])
# Crear el Parquet completo escribiendo bloques limitadosentrada = leer_compras()escritas = 0with pq.ParquetWriter('ventas.parquet', esquema, compression='NONE') as escritor: while True: bloque = list(islice(entrada, 50000)) if not bloque: break tabla = pa.Table.from_pylist(bloque, schema=esquema) escritor.write_table(tabla) escritas += len(bloque)
print('Ventas escritas:', escritas)pa.schema(...). Declara los nombres y tipos de los campos de la tabla.pa.string()representa texto ypa.float64()un número de coma flotante de doble precisión.entrada = leer_compras(). Crea el generador. La lectura empieza cuando otra operación le pide registros. Cada nueva llamada aleer_compras()permite iniciar un recorrido desde el principio del archivo.pq.ParquetWriter(...). Abre el archivo de salida. Al terminar el bloquewith, el escritor cierra el archivo y completa sus metadatos.list(islice(entrada, 50000)). Pide al generador hasta 50.000 ventas y las reúne en una lista que representa el bloque actual. Al agotarse la entrada, se obtiene una lista vacía;if not bloquedetecta esa situación ybreaktermina el bucle.pa.Table.from_pylist(bloque, schema=esquema). Convierte los diccionarios del bloque actual en una tabla Arrow con esos tipos.escritor.write_table(tabla). Añade la tabla del bloque al archivo.escritasacumula cuántas ventas se han escrito.
Paso 3. Releer el Parquet y comprobar el recuento
Section titled “Paso 3. Releer el Parquet y comprobar el recuento”Añade este código al final del script y ejecútalo de nuevo.
# Leer el Parquet por lotes y comprobar el número de ventasleidas = 0with pq.ParquetFile('ventas.parquet') as archivo: for lote in archivo.iter_batches(batch_size=50000): leidas += lote.num_rows
assert leidas == escritasprint('Ventas escritas y leídas:', leidas)archivo.iter_batches(batch_size=50000). Relee el Parquet entregando lotes de hasta 50.000 filas.lote.num_rowsindica la cantidad de filas del lote actual.assert leidas == escritas. Comprueba que ambos recuentos coinciden. Si difieren, detiene el programa con un error para que revisemos el resultado.
Con la descarga utilizada en los ensayos, la salida termina así.
Ventas escritas: 4138476Ventas escritas y leídas: 4138476Memoria. El programa solo conserva el bloque actual y los búferes del lector y del escritor. El último bloque puede tener menos de 50.000 ventas. Si el equipo va justo de memoria, reduce el tamaño de bloque.
Alcance de la comprobación. assert compara recuentos; no revisa los valores de cada campo.
Aplicar el patrón a los otros archivos del ejercicio
Section titled “Aplicar el patrón a los otros archivos del ejercicio”El mismo lector leer_compras() sirve para todos los archivos de la práctica; también podría alimentar un escritor CSV o JSONL registro a registro. Genera un archivo cada vez: vuelve a llamar a leer_compras() para empezar un recorrido nuevo del original y mantén el escritor abierto hasta escribir el último bloque.
| Archivo | Escritor | Qué cambia respecto al ejemplo |
|---|---|---|
ventas.orc y ventas_snappy.orc |
orc.ORCWriter |
Cada tabla del bloque se escribe con escritor.write(tabla) |
ventas_reducidas.parquet |
pq.ParquetWriter |
Esquema de dos campos; cada tabla se reduce con tabla.select(...) |
ventas_reducidas.avro |
fastavro.writer |
No forma bloques: recibe un generador de diccionarios de dos campos |
Reutiliza el bucle que forma bloques y convierte cada bloque en tabla. Estas son las llamadas del escritor, mostradas como fragmento para adaptar tu script.
# Fragmento de escritura ORCimport pyarrow.orc as orc
with orc.ORCWriter('ventas_snappy.orc', compression='snappy', stripe_size=8 * 1024 * 1024) as escritor: # Aquí se repite el bucle de bloques del ejemplo # Cada tabla creada se escribe con esta operación escritor.write(tabla)escritor.write(tabla) añade al ORC la tabla que has creado con el bloque actual. El bucle de bloques debe situarse dentro del with para que todas las tablas se escriban antes de cerrar el archivo.
La versión sin compresión usa compression='uncompressed' y otro nombre de archivo. stripe_size configura un tamaño objetivo de stripe; ese tamaño no es una cota exacta de memoria.
Para comprobar un ORC, recorre sus stripes y suma sus filas.
# Fragmento para contar las ventas de un ORCimport pyarrow.orc as orc
leidas = 0with open('ventas_snappy.orc', 'rb') as entrada: archivo = orc.ORCFile(entrada) for numero in range(archivo.nstripes): stripe = archivo.read_stripe(numero) leidas += stripe.num_rows
print('Ventas leídas:', leidas)nstripes indica cuántos stripes hay. read_stripe(numero) recupera uno como lote Arrow y num_rows indica sus filas. Para leer solo dos campos, usa read_stripe(numero, columns=['tienda', 'importe']).
Para ver algunas ventas, convierte a diccionarios solo un tramo pequeño. Un stripe puede contener muchas ventas.
# Fragmento para mostrar las tres primeras ventas de un ORCwith open('ventas_snappy.orc', 'rb') as entrada: archivo = orc.ORCFile(entrada) primeras = archivo.read_stripe(0).slice(0, 3) for venta in primeras.to_pylist(): print(venta)slice(0, 3) selecciona las filas desde la posición 0 y en cantidad de 3. to_pylist() convierte ese tramo en una lista de diccionarios.
Parquet reducido
Section titled “Parquet reducido”Declara un esquema con los dos campos y reduce cada tabla del bloque antes de escribirla.
# Fragmento de escritura del Parquet reducidoesquema_dos_columnas = pa.schema([ ('tienda', pa.string()), ('importe', pa.float64()),])
with pq.ParquetWriter('ventas_reducidas.parquet', esquema_dos_columnas, compression='NONE') as escritor: # Aquí se repite el bucle de bloques del ejemplo # Cada tabla creada se reduce a dos columnas antes de escribirla escritor.write_table(tabla.select(['tienda', 'importe']))La tabla del bloque se sigue creando con el esquema completo de seis campos. tabla.select(['tienda', 'importe']) devuelve una tabla nueva con esas dos columnas, que coincide con esquema_dos_columnas. Para comprobarlo, usa la lectura por lotes del paso 3.
Avro reducido
Section titled “Avro reducido”Podemos entregar directamente un generador al escritor. Este fragmento utiliza el lector del ejemplo y el esquema de dos campos que debes preparar para el ejercicio.
# Fragmento de escritura Avro sin lista globalfrom fastavro import writer
def leer_reducidas(): for venta in leer_compras(): yield {'tienda': venta['tienda'], 'importe': venta['importe']}
with open('ventas_reducidas.avro', 'wb') as salida: writer(salida, esquema_reducido, leer_reducidas(), codec='null')En leer_reducidas, cada vuelta recibe una venta del generador original. La expresión {'tienda': venta['tienda'], 'importe': venta['importe']} crea un diccionario con solo esos dos campos. yield entrega ese diccionario y deja la función en pausa.
writer(..., leer_reducidas(), ...) recibe el generador y solicita sus registros a medida que los escribe. Esa llamada utiliza el esquema Avro de dos campos preparado para el ejercicio.
Para comprobarlo, recorre reader(entrada) con un bucle y cuenta los registros, sin reunirlos en una lista.
# Fragmento para contar las ventas de un Avrofrom fastavro import reader
leidas = 0with open('ventas_reducidas.avro', 'rb') as entrada: for venta in reader(entrada): leidas += 1
print('Ventas leídas:', leidas)M01-SPL-01 — ¿Se puede repartir la lectura? (práctica guiada)
Section titled “M01-SPL-01 — ¿Se puede repartir la lectura? (práctica guiada)”En el bloque 04 vimos que Hadoop reparte la entrada de un trabajo en splits y asigna cada uno a una tarea map. También vimos que la relación entre bloques y splits depende del formato y de la compresión. En esta práctica guiada lo comprobaremos con el mismo archivo de ventas en dos versiones: texto sin comprimir y comprimido con gzip. Es opcional y no forma parte de la entrega de M01-FMT-01.
Un archivo gzip es un flujo continuo: para llegar a la venta 2.000.000 hay que descomprimir todo lo anterior. Por eso Hadoop no puede empezar a leerlo por la mitad y lo asigna completo a un único split, aunque ocupe varios bloques. Un archivo de texto sin comprimir, en cambio, se puede empezar a leer en cualquier bloque, buscando el siguiente salto de línea.
Parquet y ORC resuelven este problema de otra forma. Se dividen en unidades independientes, grupos de filas y stripes, y sus metadatos indican dónde empieza cada una. Así, varias tareas pueden leer unidades distintas del mismo archivo a la vez, aunque estén comprimidas.
Cada paso indica dónde se ejecuta:
- [Contenedor de formatos]: la terminal de la práctica M01-FMT-01.
- [Anfitrión]: la terminal de tu equipo, fuera de cualquier contenedor.
- [master]: la terminal del nodo
masterdel clúster, como usuariohadoop.
1. Contar las unidades de los archivos columnares
Section titled “1. Contar las unidades de los archivos columnares”[Contenedor de formatos] En /lab/trabajo/formatos2, donde están los archivos de M01-FMT-01, crea este script y ejecútalo.
import pyarrow.parquet as pqimport pyarrow.orc as orc
for ruta in ['ventas.parquet', 'ventas_reducidas.parquet']: print(ruta, 'grupos de filas:', pq.ParquetFile(ruta).metadata.num_row_groups)
for ruta in ['ventas.orc', 'ventas_snappy.orc']: print(ruta, 'stripes:', orc.ORCFile(ruta).nstripes)python contar_unidades.pymetadata.num_row_groups lee de los metadatos del Parquet cuántos grupos de filas contiene. nstripes hace lo mismo con los stripes de un ORC. Ninguno de los dos recorre las ventas.
Con los archivos generados en los ensayos, la salida es esta.
ventas.parquet grupos de filas: 83ventas_reducidas.parquet grupos de filas: 83ventas.orc stripes: 28ventas_snappy.orc stripes: 7Los 83 grupos de filas proceden de los bloques de 50.000 ventas: cada llamada a write_table ha creado uno. En ORC, el número de stripes depende de stripe_size y de la compresión: con Snappy los datos ocupan menos y caben en menos stripes.
Sal del contenedor.
exit2. Arrancar el clúster y localizar el archivo de ventas
Section titled “2. Arrancar el clúster y localizar el archivo de ventas”[Anfitrión] En BigDataAplicadoLab_2627/modulo01/, arranca el clúster del bloque 03 y entra en master.
make upmake shellSi tu equipo tiene poca memoria, usa make up-mini. El número de splits que vamos a observar no depende de cuántos trabajadores haya.
[master] Entra en el directorio de la práctica del bloque 04, donde descargaste y descomprimiste las ventas.
cd /home/hadoop/ventas-por-tiendals -l purchases.txt purchases.txt.gzDeben aparecer los dos archivos: unos 211 MB el texto y unos 38 MB el comprimido. Si falta alguno, descárgalo y descomprímelo de nuevo.
curl -fL -o purchases.txt.gz https://github.com/josepgarcia/datos/raw/refs/heads/main/purchases.txt.gzgzip -dk purchases.txt.gz3. Subir los dos archivos con bloques pequeños
Section titled “3. Subir los dos archivos con bloques pequeños”El archivo comprimido ocupa unos 38 MB y cabría en un único bloque de 128 MB, el tamaño por defecto. Con un solo bloque daría un único split de todos modos y no podríamos ver la diferencia. Por eso subiremos ambos archivos con bloques de 16 MB.
[master]
hdfs dfs -mkdir -p /datos/m01-05/splitshdfs dfs -D dfs.blocksize=16m -put purchases.txt /datos/m01-05/splits/hdfs dfs -D dfs.blocksize=16m -put purchases.txt.gz /datos/m01-05/splits/hdfs fsck /datos/m01-05/splits -files -blocks-D dfs.blocksize=16m cambia el tamaño de bloque solo para los archivos de esa orden; el resto de HDFS conserva los 128 MB. Ya usamos -D con hadoop jar en el bloque 04 para indicar una opción de un trabajo.
En la salida de fsck, busca la línea de cada archivo. Debes ver 13 bloques para purchases.txt y 3 bloques para purchases.txt.gz.
4. Contar las tareas map con el texto sin comprimir
Section titled “4. Contar las tareas map con el texto sin comprimir”Lanzaremos un trabajo de Streaming solo con fase map. Cada tarea ejecutará wc -l, que cuenta las líneas que recibe. Así, cada archivo de salida indicará cuántas ventas ha leído una tarea.
[master]
hadoop jar /opt/hadoop/share/hadoop/tools/lib/hadoop-streaming-3.4.1.jar \ -D mapreduce.job.reduces=0 \ -input /datos/m01-05/splits/purchases.txt \ -output /datos/m01-05/salidas/texto \ -mapper 'wc -l'-D mapreduce.job.reduces=0. El trabajo no tiene fase reduce. Cada tarea map escribe directamente su propio archivo de salida.-mapper 'wc -l'. Cada tarea recibe las líneas de su split por la entrada estándar y escribe cuántas ha recibido. No necesitamos ningún script de Python.
Durante la ejecución, Hadoop escribe muchas líneas de registro. Busca estas dos.
INFO JobSubmitter:... - number of splits:13 Launched map tasks=13number of splits indica en cuántos splits se ha dividido la entrada y Launched map tasks, cuántas tareas map se han lanzado. Junto a esta última aparece Data-local map tasks: las tareas que se han ejecutado en un nodo que ya tenía su bloque.
Ahora mira los resultados de cada tarea.
hdfs dfs -ls /datos/m01-05/salidas/textohdfs dfs -cat /datos/m01-05/salidas/texto/part-*hdfs dfs -cat /datos/m01-05/salidas/texto/part-* | awk '{s += $1} END {print s}'Hay 13 archivos part-…, uno por tarea. Cada uno contiene unas 328.000 ventas, salvo el último, más pequeño porque su bloque no está completo. awk suma la primera columna de todas las líneas; el total debe ser 4138476.
5. Repetir con el archivo comprimido
Section titled “5. Repetir con el archivo comprimido”[master] Lanza el mismo trabajo sobre el archivo gzip, con otra carpeta de salida.
hadoop jar /opt/hadoop/share/hadoop/tools/lib/hadoop-streaming-3.4.1.jar \ -D mapreduce.job.reduces=0 \ -input /datos/m01-05/splits/purchases.txt.gz \ -output /datos/m01-05/salidas/gzip \ -mapper 'wc -l'Esta vez debes encontrar number of splits:1 y Launched map tasks=1.
hdfs dfs -ls /datos/m01-05/salidas/gziphdfs dfs -cat /datos/m01-05/salidas/gzip/part-*Hay un único archivo part-00000 con 4138476 ventas. Hadoop reconoce la extensión .gz y descomprime el archivo antes de entregar las líneas a wc -l, así que el recuento coincide. La diferencia está en el reparto: una sola tarea ha leído los tres bloques.
Si repites un trabajo, borra antes su carpeta de salida con hdfs dfs -rm -r o elige otra, porque Hadoop no sobrescribe una salida existente.
6. Anotar y reflexionar
Section titled “6. Anotar y reflexionar”Completa esta tabla con tus resultados.
| Archivo | Tamaño | Bloques HDFS | Splits y tareas map | Ventas contadas |
|---|---|---|---|---|
purchases.txt |
||||
purchases.txt.gz |
Responde brevemente.
- El archivo comprimido ocupa tres bloques, pero se ha leído con una sola tarea. ¿Por qué no se ha repartido?
ventas.parquetocupa un tamaño parecido al depurchases.txt.gzy tiene 83 grupos de filas. ¿Qué ventaja tendría para repartir su lectura entre varias tareas?
7. Limpiar
Section titled “7. Limpiar”[master] Borra los datos de esta práctica en HDFS y sal de master.
hdfs dfs -rm -r /datos/m01-05exit[Anfitrión] Si no vas a seguir trabajando con el clúster, detenlo. make down conserva el resto de los datos de HDFS.
make downCon lo visto hasta ahora, Hadoop Streaming entrega texto línea a línea y no lee Parquet ni ORC directamente. En M03 trabajaremos con Spark, que reparte el procesamiento en particiones y aprovecha precisamente los grupos de filas de Parquet para leer un archivo con varias tareas a la vez. Los procedimientos de esta sección permiten adaptar las operaciones de la práctica al volumen de sus datos.
Del sistema de ficheros al almacenamiento de objetos
Section titled “Del sistema de ficheros al almacenamiento de objetos”Hasta ahora hemos elegido cómo guardar los datos dentro de un archivo. La siguiente decisión es dónde guardar esos archivos para que otros procesos puedan utilizarlos.
En HDFS trabajamos con rutas y archivos cuyos bloques se reparten entre DataNodes. En un servicio de objetos, como Amazon S3 o MinIO, guardamos cada archivo como un objeto dentro de un bucket.
- El bucket es el contenedor que agrupa los objetos.
- La clave es el nombre completo que identifica un objeto dentro del bucket.
- Los metadatos describen propiedades como su tamaño o tipo de contenido.
Por ejemplo, podemos subir ventas.parquet al bucket datos-prueba con la clave ventas/ventas.parquet. La barra forma parte de la clave. La consola puede mostrarla como una carpeta, pero no necesitamos crear un directorio ventas antes de subir el objeto.
| Aspecto | HDFS | S3 o MinIO |
|---|---|---|
| Organización | Archivos y directorios | Buckets y claves de objetos |
| Acceso habitual | Cliente Hadoop, como hdfs dfs |
API S3 o cliente compatible, como mc |
| Distribución | Bloques repartidos entre DataNodes | Gestionada por el servicio según su despliegue |
| Uso en este curso | Comprender almacenamiento distribuido y su operación | Compartir archivos entre procesos mediante almacenamiento de objetos |
MinIO permite practicar una API compatible con S3 en nuestro equipo. La protección de los datos depende del despliegue; un único contenedor local permite estudiar las operaciones, pero no demuestra tolerancia a fallos del equipo físico.
M01-OBJ-01 — Subir y recuperar un objeto con MinIO
Section titled “M01-OBJ-01 — Subir y recuperar un objeto con MinIO”Este recorrido continúa el bloque después de la práctica de formatos. Utilizaremos el Parquet pequeño de los ejemplos para reconocer bucket, clave y metadatos.
Cada paso indica dónde se ejecuta:
- [Contenedor de formatos]: la terminal que has usado en la práctica.
- [Anfitrión]: la terminal de tu equipo, fuera de cualquier contenedor.
- [Cliente mc]: la terminal dentro del contenedor cliente de MinIO.
Preparar el archivo y arrancar MinIO
Section titled “Preparar el archivo y arrancar MinIO”1. [Contenedor de formatos] Sal del contenedor.
exit2. [Anfitrión] Sitúate en la raíz de los apuntes BDA_2627/, donde están la carpeta M01/ y el archivo M01.md. Crea la carpeta que compartiremos con el cliente MinIO.
mkdir -p trabajo-m01/formatos3. [Anfitrión] Con el explorador de archivos, copia el Parquet del ejemplo a la carpeta nueva y cámbiale el nombre a ventas.parquet. El origen y el destino están en dos proyectos diferentes.
BigDataAplicadoLab_2627/ ← proyecto del laboratorio de formatos└── modulo01/trabajo/formatos2/ └── ventas_ejemplo.parquet ← origen, creado con ejemplo_parquet.py
BDA_2627/ ← raíz de los apuntes├── M01/laboratorio-minio/docker-compose.yml└── trabajo-m01/formatos/ └── ventas.parquet ← destino, copia renombrada4. [Anfitrión] Desde la raíz de los apuntes, arranca el laboratorio ya preparado.
docker compose -f M01/laboratorio-minio/docker-compose.yml up -d --wait minioLa API queda en http://localhost:9002 y la consola web en http://localhost:9001. Puedes entrar en la consola con minioadmin y contraseña minioadmin, credenciales del laboratorio local.
Cargar, consultar y descargar
Section titled “Cargar, consultar y descargar”5. [Anfitrión] Abre la terminal del cliente desde la misma carpeta.
docker compose -f M01/laboratorio-minio/docker-compose.yml run --rm mcmc es MinIO Client. El laboratorio utiliza la imagen fijada minio/minio:RELEASE.2025-07-18T21-56-31Z, que ya incluye ese programa. Ahora estás dentro del contenedor cliente, donde trabajo-m01/ del anfitrión aparece como /trabajo.
6. [Cliente mc] Ejecuta estas órdenes.
mc alias set m01local http://minio:9000 minioadmin minioadminmc mb --ignore-existing m01local/datos-pruebamc cp /trabajo/formatos/ventas.parquet m01local/datos-prueba/ventas/ventas.parquetmc ls --recursive m01local/datos-prueba/mc stat m01local/datos-prueba/ventas/ventas.parquetmc cp m01local/datos-prueba/ventas/ventas.parquet /trabajo/formatos/ventas-recuperadas.parquetsha256sum /trabajo/formatos/ventas.parquet /trabajo/formatos/ventas-recuperadas.parquetexit| Orden | Qué hace | Operación parecida en HDFS |
|---|---|---|
mc alias set m01local ... |
Guarda la dirección y las credenciales del servidor con el nombre m01local |
El cliente Hadoop toma la dirección de su configuración |
mc mb --ignore-existing ... |
Crea el bucket; si ya existe, continúa sin error | hdfs dfs -mkdir -p crea un directorio |
mc cp origen destino |
Sube un objeto o lo descarga, según el sentido de la copia | hdfs dfs -put y hdfs dfs -get |
mc ls --recursive ... |
Lista los objetos del bucket con sus claves | hdfs dfs -ls -R |
mc stat ... |
Muestra los metadatos del objeto, como su tamaño y tipo | — |
sha256sum archivo1 archivo2 |
Calcula una huella del contenido de cada archivo | No es de MinIO ni de HDFS: es una orden de Linux |
Dentro de la red Docker, el cliente encuentra el servidor como minio:9000; la dirección localhost:9002 se utiliza desde el anfitrión.
La primera copia sube el archivo y la segunda lo recupera con otro nombre. Las dos líneas de sha256sum deben mostrar la misma huella. Los archivos recuperados se conservan en el anfitrión gracias al montaje.
Anota estos datos.
| Dato | Tu resultado |
|---|---|
| Bucket | |
| Clave del objeto | |
Tamaño mostrado por mc stat |
|
| ¿Coinciden las dos huellas SHA-256? | |
Qué cambia respecto a subir el archivo con hdfs dfs -put (dos o tres frases) |
7. [Anfitrión] Detén el laboratorio.
docker compose -f M01/laboratorio-minio/docker-compose.yml downEsta parada conserva los objetos en el volumen del servidor. Si abres otra sesión del cliente, tendrás que volver a ejecutar mc alias set para definir la conexión.
Data lake y lakehouse
Section titled “Data lake y lakehouse”Un data lake reúne datos originales y procesados para que distintos motores puedan analizarlos. Por ejemplo, un bucket puede conservar eventos JSONL y un histórico Parquet. El almacenamiento de objetos aporta el lugar donde guardarlos; los formatos indican cómo están representados.
Cuando varios procesos escriben y consultan esos archivos, necesitamos controlar qué archivos forman una tabla y qué versión deben leer las consultas. Un lakehouse incorpora esa gestión de tablas sobre el almacenamiento del data lake.
| Capa | Ejemplo | Función |
|---|---|---|
| Almacenamiento | S3 o MinIO | Guarda los objetos |
| Formato de archivo | Parquet | Guarda los datos y sus tipos |
| Formato de tabla | Apache Iceberg | Organiza los archivos y las versiones de una tabla |
Apache Iceberg — gestionar una tabla formada por archivos
Section titled “Apache Iceberg — gestionar una tabla formada por archivos”Imagina que una tabla de ventas está repartida entre varios Parquet. Iceberg registra cuáles pertenecen a cada estado de la tabla. Así, una consulta puede leer una versión coherente aunque otro proceso esté preparando una actualización.
- Un snapshot identifica una versión de la tabla. Las versiones anteriores se pueden consultar mientras se conserven sus metadatos y los archivos necesarios.
- Los metadatos describen los archivos que forman esa versión y sus características.
- El catálogo permite localizar la referencia actual de la tabla.
Por ejemplo, una tabla de ventas recibe un archivo nuevo cada mes.
| Snapshot | Archivos que forman la tabla | Qué ha ocurrido |
|---|---|---|
| 1 | enero.parquet |
Carga inicial |
| 2 | enero.parquet y febrero.parquet |
Carga de febrero |
Una consulta que empezó con el snapshot 1 mientras se cargaba febrero sigue viendo solo enero hasta terminar. Las consultas nuevas leen el snapshot 2. Mientras se conserven sus metadatos y archivos, también se puede volver a consultar el snapshot 1.
Iceberg también facilita cambios compatibles de esquema y de particionado. En este bloque nos interesa distinguir archivo, almacenamiento y tabla. La práctica anterior ya nos ha permitido trabajar con Parquet y con objetos; estos conceptos explican qué capa adicional aporta un lakehouse.
M01-FMT-02 — Elegir las capas necesarias
Section titled “M01-FMT-02 — Elegir las capas necesarias”Para cada caso, propón un formato de archivo, un lugar donde guardarlo y si añadirías una tabla Iceberg. Justifica la decisión con una propiedad que hemos explicado. La primera fila es un ejemplo resuelto.
| Necesidad | Formato de archivo | Almacenamiento | ¿Añadirías Iceberg? ¿Por qué? |
|---|---|---|---|
| Ejemplo: guardar durante un año los registros de acceso de un servidor para auditorías puntuales | JSONL comprimido con gzip: cada acceso es un registro completo y el texto se puede revisar | Bucket de MinIO o S3 | No: solo se añaden archivos nuevos y nadie actualiza la tabla ni consulta versiones anteriores |
| Compartir 200 ventas para revisarlas manualmente | |||
| Intercambiar eventos que pueden añadir campos opcionales | |||
| Consultar un histórico por fecha y categoría, con cargas nocturnas completas | |||
| Actualizar un histórico desde varios procesos y consultar versiones anteriores |
Referencias docentes
Section titled “Referencias docentes”La secuencia de explicación, ejemplos y actividad recupera los materiales del curso anterior. Para ampliar conceptos y consultar otros ejemplos, utiliza Formatos de datos en Big Data, de Aitor Medrano. Las interfaces utilizadas se documentan en pandas 2.2, PyArrow 18.1 y fastavro.