Módulo 1: From File Format To Table Format
Verificando el mismo total de 106.15
Descripción
Cargar cuarenta filas no es, por sí solo, evidencia de que la migración a Iceberg fue correcta — podría haber cuarenta filas con datos corruptos, con columnas mezcladas, o con un revenue calculado mal, y el conteo seguiría diciendo 40. Esta lección hace la verificación que de verdad importa: leer kiosko.fact_orders desde Iceberg y confirmar, con el mismo revenue por tienda y el mismo total (106.15) que ya confirmaron seis motores distintos en las seis guías anteriores, que la migración preservó el dato exacto, no solo el conteo de filas.
Conexión con el módulo. Esta lección cierra el arco de las lecciones 4, 5 y 6 —instalar, crear, cargar— con la pregunta que en realidad importa: ¿el dato sigue siendo correcto? Es la misma disciplina de verificación que ya viste en cada guía anterior del ecosistema, ahora aplicada por primera vez a una tabla Iceberg.
Una analogía: contar las fotos, y después mirar si son las fotos correctas
Un archivero descuidado, al recibir un álbum nuevo, podría conformarse con contar las páginas: "cuarenta fotos, coincide con lo que me dijeron, listo". Un archivero riguroso hace algo más: además de contar, mira una muestra de las fotos y confirma que son, de verdad, las fotos correctas —no cuarenta fotos cualquiera, sino exactamente las cuarenta órdenes de la semana del 3 al 9 de agosto, con el revenue correcto en cada una—. Esta lección es ese segundo paso: no te conformas con len(table.scan().to_arrow()) == 40 —eso ya lo confirmaste en la lección 6—; vas a sumar el revenue, desglosado por tienda, y comparar ese número contra el mismo que ya verificaron seis motores distintos antes que Iceberg.
Ejemplo trabajado: la misma verificación de siempre, ahora sobre Iceberg
# verify_fact_orders_iceberg.py
import os
from pyiceberg.catalog import load_catalog
DIM_STORE = [
{"store_id": "S01", "store_name": "Kiosko Centro", "city": "Bogota"},
{"store_id": "S02", "store_name": "Kiosko Norte", "city": "Lima"},
{"store_id": "S03", "store_name": "Kiosko Sur", "city": "Santiago"},
]
store_names = {s["store_id"]: s["store_name"] for s in DIM_STORE}
warehouse_path = os.path.abspath("kiosko_warehouse")
catalog_db_path = os.path.abspath("kiosko_catalog.db")
catalog = load_catalog(
"kiosko", type="sql",
uri=f"sqlite:///{catalog_db_path}", warehouse=f"file://{warehouse_path}",
)
table = catalog.load_table("kiosko.fact_orders")
scanned = table.scan().to_arrow()
total_rows = scanned.num_rows
total_revenue = round(sum(scanned.column("revenue").to_pylist()), 2)
print("=== kiosko.fact_orders (Iceberg) ===\n")
print(f"len(table.scan().to_arrow()) = {total_rows}")
by_store = scanned.group_by("store_id").aggregate([("revenue", "sum")])
print("\n=== Revenue by store ===")
for row in sorted(by_store.to_pylist(), key=lambda r: r["store_id"]):
sid = row["store_id"]
print(f"{sid} {store_names[sid]:<14}: revenue={round(row['revenue_sum'], 2)}")
print(f"\nTotal week revenue: {total_revenue}")
Qué esperar (verificado corriendo el script real, sobre la tabla cargada en la lección 6):
=== kiosko.fact_orders (Iceberg) ===
len(table.scan().to_arrow()) = 40
=== Revenue by store ===
S01 Kiosko Centro : revenue=38.3
S02 Kiosko Norte : revenue=38.8
S03 Kiosko Sur : revenue=29.05
Total week revenue: 106.15
Detente en estos cinco números, porque son exactamente los mismos que verificaron data-engineering-foundations-guide, python-for-data-engineering-guide, data-modeling-for-analytics-guide, dbt-analytics-engineering-guide, airflow-and-declarative-orchestration-guide y spark-and-distributed-processing-guide — seis guías, seis motores distintos (Python puro con CSV, un paquete instalado con uv, DuckDB, DuckDB vía dbt, Airflow orquestando el mismo paquete, y un DataFrame de PySpark), y ahora una séptima confirmación, leyendo desde una tabla Iceberg real por primera vez. S01=38.3, S02=38.8, S03=29.05, total 106.15 — el mismo dato, sin ninguna desviación, después de haber pasado por siete implementaciones completamente distintas.
Verificación adicional: el grano se sostiene también sobre Iceberg
data-modeling-for-analytics-guide (módulo 1) declaró el grano de fact_orders como "una línea de orden", verificado con COUNT(*) contra COUNT(DISTINCT order_id || '-' || product_id). Vale la pena repetir esa misma verificación aquí, ahora con pyarrow.compute sobre los datos leídos desde Iceberg, para confirmar que la migración no introdujo ninguna fila duplicada:
import pyarrow.compute as pc
keys = pc.binary_join_element_wise(scanned.column("order_id"), scanned.column("product_id"), "-")
distinct_keys = pc.count_distinct(keys)
print("total_rows =", scanned.num_rows)
print("distinct_order_product_lines =", distinct_keys.as_py())
qty_invalid = scanned.filter(pc.field("quantity") <= 0).num_rows
print("filas con quantity invalida =", qty_invalid)
Qué esperar (verificado corriendo el script real):
total_rows = 40
distinct_order_product_lines = 40
filas con quantity invalida = 0
40 contra 40 — el grano declarado en data-modeling-for-analytics-guide se sostiene exactamente igual sobre la tabla Iceberg, sin ninguna fila duplicada introducida por la migración. Y cero filas con quantity inválida, la misma verificación de calidad que validate_orders() ya garantizó en data-engineering-foundations-guide, mucho antes de que este dato llegara a Iceberg.
Diagrama: siete motores, un mismo número
flowchart LR
A["foundations\nPython + CSV"] --> G["106.15"]
B["python-for-data-engineering\nkiosko_pipeline (uv)"] --> G
C["data-modeling\nDuckDB"] --> G
D["dbt\nDuckDB via dbt"] --> G
E["airflow\nkiosko_pipeline orquestado"] --> G
F["spark\nPySpark DataFrame"] --> G
H["esta guia\nPyIceberg + Iceberg"] --> G
Profundización: por qué esta verificación es más fuerte que "confiar en la migración"
Es tentador pensar que, si table.append() no lanzó ningún error en la lección 6, la migración necesariamente fue correcta — al fin y al cabo, Iceberg valida el esquema en cada escritura, tal como viste en los errores comunes de esa lección. Pero validar el esquema (tipos y nombres de columna correctos) no es lo mismo que validar el valor de cada dato — un bug en build_fact_orders_parquet.py que calculara revenue = quantity + unit_price en vez de quantity * unit_price habría producido cuarenta filas con el esquema perfectamente válido, sin ningún error de Iceberg, y con un total completamente distinto de 106.15. Esta lección es la que atrapa ese tipo de error — no confía en que "si no hubo excepción, está bien"; recalcula el número de negocio desde cero, y lo compara contra un valor externo, ya conocido, verificado independientemente seis veces antes. Esta es, con precisión, la misma disciplina que cada guía anterior de este ecosistema exigió antes de dar por buena cualquier tabla nueva — Iceberg no cambia esa disciplina, solo cambia dónde vive el dato que se está verificando.
Errores comunes
Confiar solo en el conteo de filas, sin sumar el revenue. Qué pasa: alguien, al ver len(table.scan().to_arrow()) == 40 en la lección 6, considera la migración terminada, sin correr la verificación de revenue de esta lección. Por qué pasa: el conteo de filas es la verificación más simple posible, y se siente como suficiente evidencia. Cómo detectarlo: si tu proceso de verificación se detiene en "cuarenta filas, listo", revisa la Profundización de esta lección — cuarenta filas con datos corruptos siguen siendo cuarenta filas. Cómo corregirlo: siempre compara un número de negocio (revenue, en este caso) contra un valor externo ya conocido — la tabla de esta lección con las seis guías anteriores es exactamente ese valor externo para el caso de Kiosko.
Sumar revenue directamente en Python con sum() sobre una lista larga, y encontrar diferencias de redondeo minúsculas. Qué pasa: alguien suma manualmente los valores de revenue con aritmética de punto flotante estándar, y obtiene algo como 106.14999999999999 en vez de 106.15 exacto, y se preocupa pensando que hay un error de datos. Por qué pasa: la aritmética de punto flotante binario (IEEE 754), la misma que usa Python, DuckDB y Spark, no representa exactamente números decimales como 0.55 o 1.20 — es una limitación conocida de cualquier lenguaje que use double/float64, no un bug de esta guía ni de Iceberg. Cómo detectarlo: si tu total tiene más de dos decimales visibles y varios 9 seguidos cerca del final, es redondeo de punto flotante, no un error de datos. Cómo corregirlo: el ejemplo trabajado de esta lección usa round(total_revenue, 2) exactamente por esto — redondear a dos decimales al final del cálculo es la práctica estándar para presentar montos de dinero, y es la misma técnica que ya usaron, sin excepción, las seis guías anteriores de este ecosistema.
Ejercicios
Ejercicio 1 — Reproduce ambas verificaciones tú mismo. Con kiosko.fact_orders ya cargada (lección 6), corre el script de revenue por tienda y el script del grano de esta lección. Confirma que obtienes 106.15 de total y 40 == 40 en la verificación de grano.
Ver solución
Si la lección 6 se completó sin errores, ambos scripts de esta lección deberían reproducir exactamente los números mostrados aquí: S01=38.3, S02=38.8, S03=29.05, total 106.15, y total_rows == distinct_order_product_lines == 40. Si algún número no coincide, revisa primero si corriste table.append() más de una vez en la lección 6 (el primer error común de esa lección) — un revenue duplicado (212.3, el doble de 106.15) es la señal más clara de ese problema específico.
Ejercicio 2 — Calcula el revenue por producto, no solo por tienda. Usando scanned (el resultado de table.scan().to_arrow() del ejemplo trabajado de esta lección), escribe el código que agrupa por product_id en vez de store_id, y confirma cuál de los cuatro productos de Kiosko generó más revenue en la semana.
Ver solución
by_product = scanned.group_by("product_id").aggregate([("revenue", "sum")])
for row in sorted(by_product.to_pylist(), key=lambda r: r["product_id"]):
print(f"{row['product_id']}: revenue={round(row['revenue_sum'], 2)}")
El patrón es idéntico al de group_by("store_id") del ejemplo trabajado — solo cambia la columna de agrupación. P001 Bottled Water 600ml, con el mayor volumen de unidades vendidas de la semana (visible al revisar RAW_ORDERS de la lección 6), es el producto con más revenue total — la verificación exacta, corrida sobre tus propios datos, es el objetivo real de este ejercicio, no memorizar el resultado.
Ejercicio 3 — Explica por qué esta verificación no depende de Iceberg en absoluto. En 2-3 frases, explica por qué el código de esta lección —sumar revenue, agrupar por store_id, comparar contra un valor externo— sería exactamente el mismo, línea por línea después de table.scan().to_arrow(), si los datos vinieran de un pandas.DataFrame leído directamente de un CSV.
Ver solución
table.scan().to_arrow() devuelve un pyarrow.Table normal — el mismo tipo de objeto que obtendrías leyendo cualquier Parquet o CSV con pyarrow, sin ninguna dependencia de Iceberg más allá de ese único paso de lectura. Todo lo que pasa después —group_by(), aggregate(), sumar y redondear— es código de pyarrow puro, completamente ajeno a si los datos vinieron de un archivo suelto o de una tabla Iceberg. Esto tiene sentido con la lección 3 de este módulo: Iceberg agrega capacidades de tabla (historia, transacciones, esquema evolutivo) por encima de Parquet, pero una vez que los datos ya están en tus manos como un pyarrow.Table, se comportan exactamente igual sin importar de dónde vinieron.
Resumen y siguiente paso
En esta lección verificaste, con el mismo revenue por tienda (S01=38.3, S02=38.8, S03=29.05) y el mismo total (106.15) que ya confirmaron seis motores distintos en las seis guías anteriores, que la migración de fact_orders a Iceberg preservó el dato exacto — no solo el conteo de filas. Y repetiste la verificación de grano de data-modeling-for-analytics-guide (40 == 40, sin duplicados) sobre los datos leídos desde la tabla Iceberg.
Antes de avanzar deberías poder: explicar por qué contar filas no es suficiente evidencia de una migración correcta; reproducir ambas verificaciones de esta lección en tu propia máquina; y recitar de memoria el revenue por tienda de Kiosko (38.3/38.8/29.05, total 106.15) — un número que vas a seguir viendo durante el resto de esta guía.
Con la primera tabla Iceberg de Kiosko cargada y verificada, la lección 8 —el proyecto de cierre de este módulo— junta las lecciones 4 a 7 en un solo flujo de punta a punta, y cierra formalmente la promesa que abrió este módulo en la lección 1.
Recursos
- PyIceberg — referencia de API,
table.scan()y su interoperabilidad directa conpyarrow.Table. py.iceberg.apache.org/api. En inglés. - Apache Arrow — documentación oficial de
pyarrow.compute, funciones de agregación (group_by,aggregate) usadas en esta lección. arrow.apache.org/docs/python/compute.html. En inglés. - DISEÑO de
data-modeling-for-analytics-guide— fuente de la declaración de grano (COUNT(*)vsCOUNT(DISTINCT order_id || '-' || product_id)) que esta lección repite sobre Iceberg.src/guides/data-modeling-for-analytics-guide/DISENO.md. En español. - DISEÑO de esta guía — el número canónico de Kiosko (
106.15,S01=38.3/S02=38.8/S03=29.05) verificado, por séptima vez, en esta lección.src/guides/lakehouse-and-iceberg-guide/DISENO.md. En español.