Módulo 7: Messy Domains And Medallion At Depth
Mini-proyecto: la capa gold multi-hecho de Kiosko
Descripción
Este proyecto cierra el módulo integrando las seis piezas anteriores: el inventario del dominio con tres hechos y cuatro dimensiones (lección 2), la dimensión degenerada desarrollada a fondo (lección 3), dim_order_flags construida de punta a punta (lección 4), validate_gold_schema() probada y corrida (lección 5), los tres hechos unidos contra el mismo dim_date (lección 6), y la evolución de esquema segura vs insegura (lección 7). Lo que falta es reunir todo en un solo flujo: reconstruir el warehouse heredado sin ningún cambio, construir dim_order_flags desde cero, correr validate_gold_schema() sobre las cuatro tablas gold de la guía, y documentar el resultado completo en MEDALLION_SUMMARY — la estructura formal que el módulo 8, el capstone de toda la guía, va a heredar sin repetir el trabajo.
El proyecto tiene cinco partes. Primero, reconstruyes el warehouse heredado —fact_orders, dim_date, fact_sessions, fact_store_activity— exactamente como quedó desde los módulos 1 a 6. Segundo, construyes dim_order_flags y resuelves el flag_key de las cuarenta órdenes de Kiosko. Tercero, analizas revenue por forma de pago y canal, la primera pregunta de negocio real que la dimensión junk habilita. Cuarto, corres validate_gold_schema() sobre las cuatro tablas gold de la guía, con cero discrepancias como resultado. Quinto, documentas todo en MEDALLION_SUMMARY, la estructura formal que cierra el módulo.
Conexión con el módulo. Este proyecto no introduce ningún concepto nuevo — es la integración final de las siete lecciones anteriores, empaquetada como MEDALLION_SUMMARY, la estructura que el módulo 8 de esta guía puede citar sin volver a reconstruir la evidencia desde cero.
Una analogía: la auditoría de cierre de trimestre, no un reporte más
Cada lección de este módulo resolvió una pieza por separado: nombrar el dominio feo, desarrollar la dimensión degenerada, construir la dimensión junk, escribir el contrato de esquema, unir tres hechos contra un calendario compartido, y distinguir evolución segura de insegura. Este proyecto es la auditoría de cierre: todas esas piezas, verificadas juntas en un solo flujo, con cada número confirmado por un assert antes de pasar al siguiente — exactamente el rigor que un equipo de datos real aplicaría antes de declarar que su capa gold está lista para que otros equipos —BI, la próxima guía de esta serie— construyan sobre ella con confianza.
El material que necesitas
Necesitas, en la misma carpeta: kiosko.py, raw_orders.py y events.py (idénticos a los módulos anteriores). No necesitas ningún archivo adicional — dim_order_flags, order_flags_staging y validate_gold_schema() se definen directamente en el script de este proyecto, igual que en los proyectos anteriores.
La solución de referencia, verificada
Parte 1 — Reconstruir el warehouse heredado, sin cambios
# kiosko_medallion_project.py -- la capa gold multi-hecho de Kiosko, mini-proyecto de cierre del modulo 7
from datetime import date, datetime, timedelta
import duckdb
from kiosko import DIM_PRODUCT, DIM_STORE, Order, transform_fact_orders
from raw_orders import RAW_ORDERS
from events import RAW_EVENTS
DAY_NAMES = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
def generate_date_dim(start_date: str, end_date: str) -> list[dict]:
start = date.fromisoformat(start_date)
end = date.fromisoformat(end_date)
rows = []
current = start
while current <= end:
weekday_index = current.weekday()
rows.append({
"date_key": int(current.strftime("%Y%m%d")), "calendar_date": current,
"day_of_week": DAY_NAMES[weekday_index], "month": current.month,
"quarter": (current.month - 1) // 3 + 1, "year": current.year,
"is_weekend": weekday_index >= 5,
})
current += timedelta(days=1)
return rows
print("=== Kiosko: capa gold multi-hecho, entrega final del modulo 7 ===\n")
print(f"DuckDB version: {duckdb.__version__}\n")
con = duckdb.connect()
orders = [
Order(order_id=r[0], store_id=r[1], product_id=r[2], quantity=r[3],
unit_price=r[4], order_ts=datetime.fromisoformat(r[5]))
for r in RAW_ORDERS
]
fact_orders = transform_fact_orders(orders, DIM_STORE, DIM_PRODUCT)
con.execute("""
CREATE TABLE fact_orders (
order_id VARCHAR, store_id VARCHAR, product_id VARCHAR,
quantity INTEGER, unit_price DOUBLE, revenue DOUBLE, order_ts TIMESTAMP
)
""")
con.executemany("INSERT INTO fact_orders VALUES (?, ?, ?, ?, ?, ?, ?)",
[(r["order_id"], r["store_id"], r["product_id"], r["quantity"],
r["unit_price"], r["revenue"], r["order_ts"]) for r in fact_orders])
dim_date_rows = generate_date_dim("2026-08-01", "2026-08-31")
con.execute("""
CREATE TABLE dim_date (
date_key INTEGER, calendar_date DATE, day_of_week VARCHAR,
month INTEGER, quarter INTEGER, year INTEGER, is_weekend BOOLEAN
)
""")
con.executemany("INSERT INTO dim_date VALUES (?, ?, ?, ?, ?, ?, ?)",
[(r["date_key"], r["calendar_date"], r["day_of_week"], r["month"],
r["quarter"], r["year"], r["is_weekend"]) for r in dim_date_rows])
con.execute("CREATE TABLE events (event_id VARCHAR, event_type VARCHAR, session_id VARCHAR, event_ts TIMESTAMP)")
con.executemany("INSERT INTO events VALUES (?, ?, ?, ?)",
[(r[0], r[1], r[2], datetime.fromisoformat(r[3])) for r in RAW_EVENTS])
STORE_ROTATION = ["S01", "S02", "S03"]
def store_for_session(session_id: str) -> str:
session_number = int(session_id.split("-")[1])
return STORE_ROTATION[(session_number - 1) % 3]
ALL_SESSIONS = [f"SESS-{n:02d}" for n in range(1, 18)]
con.execute("CREATE TABLE session_store_map (session_id VARCHAR, store_id VARCHAR)")
con.executemany("INSERT INTO session_store_map VALUES (?, ?)",
[(sid, store_for_session(sid)) for sid in ALL_SESSIONS])
con.execute("""
CREATE TABLE fact_sessions AS
SELECT e.session_id, m.store_id, MIN(CAST(e.event_ts AS DATE)) AS session_date,
MAX(CASE WHEN e.event_type = 'page_view' THEN e.event_ts END) AS view_ts,
MAX(CASE WHEN e.event_type = 'add_to_cart' THEN e.event_ts END) AS add_to_cart_ts,
MAX(CASE WHEN e.event_type = 'purchase' THEN e.event_ts END) AS purchase_ts,
MAX(CASE WHEN e.event_type = 'purchase' THEN true ELSE false END) AS is_converted
FROM events e JOIN session_store_map m ON e.session_id = m.session_id
GROUP BY e.session_id, m.store_id
""")
con.execute("""
CREATE TABLE fact_store_activity (
store_id VARCHAR, activity_date DATE, daily_revenue DOUBLE,
revenue_array_7d DOUBLE[], active_days_7d INTEGER,
revenue_array_30d DOUBLE[], active_days_30d INTEGER
)
""")
DAYS = ["2026-08-03", "2026-08-04", "2026-08-05", "2026-08-06", "2026-08-07", "2026-08-08", "2026-08-09"]
for day in DAYS:
for store_id in STORE_ROTATION:
daily_revenue = con.sql(f"""
SELECT COALESCE(ROUND(SUM(revenue), 2), 0.0) FROM fact_orders
WHERE store_id = '{store_id}' AND CAST(order_ts AS DATE) = DATE '{day}'
""").fetchone()[0]
prev = con.sql(f"""
SELECT revenue_array_7d, revenue_array_30d FROM fact_store_activity
WHERE store_id = '{store_id}' ORDER BY activity_date DESC LIMIT 1
""").fetchone()
if prev is None:
new_7d, new_30d = [daily_revenue], [daily_revenue]
else:
new_7d = ([daily_revenue] + list(prev[0]))[:7]
new_30d = ([daily_revenue] + list(prev[1]))[:30]
active_7d = sum(1 for v in new_7d if v > 0)
active_30d = sum(1 for v in new_30d if v > 0)
con.execute("INSERT INTO fact_store_activity VALUES (?, ?, ?, ?, ?, ?, ?)",
(store_id, day, daily_revenue, new_7d, active_7d, new_30d, active_30d))
print("Parte 1 -- warehouse heredado, reconstruido sin cambios (M1-M6)")
for table in ["fact_orders", "dim_date", "fact_sessions", "fact_store_activity"]:
count = con.sql(f"SELECT COUNT(*) FROM {table}").fetchone()[0]
print(f" {table:20} {count:3} filas")
Qué esperar (Parte 1).
=== Kiosko: capa gold multi-hecho, entrega final del modulo 7 ===
DuckDB version: 1.5.5
Parte 1 -- warehouse heredado, reconstruido sin cambios (M1-M6)
fact_orders 40 filas
dim_date 31 filas
fact_sessions 17 filas
fact_store_activity 21 filas
Esta primera parte no construye nada nuevo — reconstruye, exactamente como en cada proyecto anterior de esta guía, las cuatro tablas heredadas que sostienen todo lo que sigue.
Parte 2 — Construir dim_order_flags y resolver las 40 órdenes
PAYMENT_METHODS = ["cash", "card", "wallet"]
CHANNELS = ["in_store", "app"]
def payment_method_for_order(order_index: int) -> str:
return PAYMENT_METHODS[(order_index - 1) % len(PAYMENT_METHODS)]
def channel_for_order(order_index: int) -> str:
return CHANNELS[(order_index - 1) % len(CHANNELS)]
order_flags_rows = [
(r[0], payment_method_for_order(i), channel_for_order(i))
for i, r in enumerate(RAW_ORDERS, start=1)
]
con.execute("CREATE TABLE order_flags_staging (order_id VARCHAR, payment_method VARCHAR, channel VARCHAR)")
con.executemany("INSERT INTO order_flags_staging VALUES (?, ?, ?)", order_flags_rows)
DIM_ORDER_FLAGS_ROWS = []
next_flag_key = 1
for payment_method in PAYMENT_METHODS:
for channel in CHANNELS:
DIM_ORDER_FLAGS_ROWS.append((next_flag_key, payment_method, channel))
next_flag_key += 1
con.execute("CREATE TABLE dim_order_flags (flag_key INTEGER, payment_method VARCHAR, channel VARCHAR)")
con.executemany("INSERT INTO dim_order_flags VALUES (?, ?, ?)", DIM_ORDER_FLAGS_ROWS)
flag_count = con.sql("SELECT COUNT(*) FROM dim_order_flags").fetchone()[0]
assert flag_count == 6, "dim_order_flags no tiene el producto cartesiano completo"
con.execute("""
CREATE TABLE order_flags_resolved AS
SELECT s.order_id, f.flag_key, s.payment_method, s.channel
FROM order_flags_staging s
JOIN dim_order_flags f ON s.payment_method = f.payment_method AND s.channel = f.channel
""")
resolved_count = con.sql("SELECT COUNT(*) FROM order_flags_resolved").fetchone()[0]
assert resolved_count == 40, "no todas las ordenes resolvieron su flag_key"
print("\nParte 2 -- dim_order_flags construida y las 40 ordenes resueltas")
print(f" dim_order_flags {flag_count:3} filas (3 payment_method x 2 channel)")
print(f" order_flags_resolved {resolved_count:3} filas (40 ordenes, cada una con su flag_key)")
Qué esperar (Parte 2).
Parte 2 -- dim_order_flags construida y las 40 ordenes resueltas
dim_order_flags 6 filas (3 payment_method x 2 channel)
order_flags_resolved 40 filas (40 ordenes, cada una con su flag_key)
Parte 3 — Revenue por forma de pago y canal
revenue_by_flag = con.sql("""
SELECT f.flag_key, f.payment_method, f.channel, COUNT(*) AS orders, ROUND(SUM(o.revenue), 2) AS revenue
FROM fact_orders o
JOIN order_flags_resolved r ON o.order_id = r.order_id
JOIN dim_order_flags f ON r.flag_key = f.flag_key
GROUP BY f.flag_key, f.payment_method, f.channel
ORDER BY f.flag_key
""").fetchall()
print("\nParte 3 -- revenue por forma de pago y canal")
for flag_key, payment_method, channel, orders_count, revenue in revenue_by_flag:
print(f" flag_key={flag_key} {payment_method:6} {channel:8} {orders_count:2} ordenes revenue={revenue}")
total_via_flags = round(sum(row[4] for row in revenue_by_flag), 2)
assert total_via_flags == 106.15, "el revenue desglosado por flag no coincide con el total conocido"
print(f" TOTAL (via flags): {total_via_flags}")
Qué esperar (Parte 3).
Parte 3 -- revenue por forma de pago y canal
flag_key=1 cash in_store 7 ordenes revenue=14.2
flag_key=2 cash app 7 ordenes revenue=14.8
flag_key=3 card in_store 6 ordenes revenue=15.7
flag_key=4 card app 7 ordenes revenue=23.8
flag_key=5 wallet in_store 7 ordenes revenue=19.55
flag_key=6 wallet app 6 ordenes revenue=18.1
TOTAL (via flags): 106.15
Parte 4 — validate_gold_schema() sobre las 4 tablas gold
def validate_gold_schema(con: duckdb.DuckDBPyConnection, table: str, expected_columns: dict[str, str]) -> list[str]:
actual_rows = con.sql(f"DESCRIBE {table}").fetchall()
actual_columns = {row[0]: row[1] for row in actual_rows}
discrepancies = []
for column_name, expected_type in expected_columns.items():
if column_name not in actual_columns:
discrepancies.append(f"{table}: falta la columna '{column_name}' (se esperaba tipo {expected_type})")
elif actual_columns[column_name] != expected_type:
discrepancies.append(
f"{table}: '{column_name}' tiene tipo {actual_columns[column_name]}, se esperaba {expected_type}"
)
for column_name in actual_columns:
if column_name not in expected_columns:
discrepancies.append(f"{table}: columna inesperada '{column_name}', no declarada en el contrato")
return discrepancies
GOLD_CONTRACTS = {
"fact_orders": {
"order_id": "VARCHAR", "store_id": "VARCHAR", "product_id": "VARCHAR",
"quantity": "INTEGER", "unit_price": "DOUBLE", "revenue": "DOUBLE", "order_ts": "TIMESTAMP",
},
"fact_sessions": {
"session_id": "VARCHAR", "store_id": "VARCHAR", "session_date": "DATE",
"view_ts": "TIMESTAMP", "add_to_cart_ts": "TIMESTAMP", "purchase_ts": "TIMESTAMP",
"is_converted": "BOOLEAN",
},
"fact_store_activity": {
"store_id": "VARCHAR", "activity_date": "DATE", "daily_revenue": "DOUBLE",
"revenue_array_7d": "DOUBLE[]", "active_days_7d": "INTEGER",
"revenue_array_30d": "DOUBLE[]", "active_days_30d": "INTEGER",
},
"dim_date": {
"date_key": "INTEGER", "calendar_date": "DATE", "day_of_week": "VARCHAR",
"month": "INTEGER", "quarter": "INTEGER", "year": "INTEGER", "is_weekend": "BOOLEAN",
},
}
print("\nParte 4 -- validate_gold_schema() sobre las 4 tablas gold de la guia")
total_discrepancies = 0
for table, expected_columns in GOLD_CONTRACTS.items():
discrepancies = validate_gold_schema(con, table, expected_columns)
total_discrepancies += len(discrepancies)
status = "OK, 0 discrepancias" if not discrepancies else f"{len(discrepancies)} discrepancias"
print(f" {table:20} {status}")
assert total_discrepancies == 0, "el contrato Medallion se rompio"
print(f" TOTAL: {len(GOLD_CONTRACTS)} tablas gold, {total_discrepancies} discrepancias -- OK")
Qué esperar (Parte 4).
Parte 4 -- validate_gold_schema() sobre las 4 tablas gold de la guia
fact_orders OK, 0 discrepancias
fact_sessions OK, 0 discrepancias
fact_store_activity OK, 0 discrepancias
dim_date OK, 0 discrepancias
TOTAL: 4 tablas gold, 0 discrepancias -- OK
Parte 5 — Documentar como una estructura formal
MEDALLION_SUMMARY = {
"warehouse_tables": {
"fact_orders": 40, "dim_date": 31, "fact_sessions": 17, "fact_store_activity": 21,
},
"degenerate_dimension": "order_id, vive dentro de fact_orders, sin tabla propia (modulos 1 y 7)",
"junk_dimension": {
"table": "dim_order_flags", "rows": flag_count,
"domains": {"payment_method": PAYMENT_METHODS, "channel": CHANNELS},
"orders_resolved": resolved_count,
"revenue_verified": total_via_flags,
},
"gold_contract": {
"tables_validated": list(GOLD_CONTRACTS.keys()),
"total_columns": sum(len(cols) for cols in GOLD_CONTRACTS.values()),
"discrepancies": total_discrepancies,
},
"conformed_dimensions": {"dim_store": 3, "dim_date": 3},
}
print("\nParte 5 -- la declaracion formal: MEDALLION_SUMMARY")
for key, value in MEDALLION_SUMMARY.items():
print(f" {key}: {value}")
Qué esperar. Al correr python3 kiosko_medallion_project.py completo (las cinco partes juntas), la salida termina exactamente así:
Parte 5 -- la declaracion formal: MEDALLION_SUMMARY
warehouse_tables: {'fact_orders': 40, 'dim_date': 31, 'fact_sessions': 17, 'fact_store_activity': 21}
degenerate_dimension: order_id, vive dentro de fact_orders, sin tabla propia (modulos 1 y 7)
junk_dimension: {'table': 'dim_order_flags', 'rows': 6, 'domains': {'payment_method': ['cash', 'card', 'wallet'], 'channel': ['in_store', 'app']}, 'orders_resolved': 40, 'revenue_verified': 106.15}
gold_contract: {'tables_validated': ['fact_orders', 'fact_sessions', 'fact_store_activity', 'dim_date'], 'total_columns': 28, 'discrepancies': 0}
conformed_dimensions: {'dim_store': 3, 'dim_date': 3}
Detente en gold_contract y en junk_dimension juntos, porque son los que resumen todo el módulo en una sola imagen. discrepancies: 0 sobre veintiocho columnas en cuatro tablas gold es la confirmación ejecutada de que el contrato Medallion de Kiosko se sostiene; revenue_verified: 106.15 es la prueba de que la dimensión junk nueva no alteró ni un centavo del negocio que ya conocías desde el módulo 1 — solo le agregó una forma nueva de desglosarlo. conformed_dimensions cierra con el número que la lección 2 empezó a medir y la lección 6 completó: tanto dim_store como dim_date sirven hoy a los tres hechos de Kiosko.
Diagrama: las siete piezas del módulo, cerradas con evidencia
flowchart TD
A["L2: Inventario del dominio\nVERIFICADO -- 3 hechos, 4 dimensiones"] --> B
B["L3: Dimension degenerada a fondo\nVERIFICADO -- dim_order_bad, 0 valor agregado"] --> C
C["L4: dim_order_flags\nVERIFICADO -- 6 filas, 40 ordenes resueltas"] --> D
D["L5: validate_gold_schema()\nVERIFICADO -- probada y con drift detectado"] --> E
E["L6: 3 hechos, 1 calendario\nVERIFICADO -- totales coinciden via dim_date"] --> F
F["L7: Evolucion segura vs insegura\nVERIFICADO -- aditivo primero"] --> G
G["MEDALLION_SUMMARY\nel contrato formal que este proyecto entrega"]
G --> H["Modulo 8: capstone,\nel primer warehouse completo de Kiosko"]
Cerrando el checklist de la lección 2 del módulo 1, pieza por pieza
| Pieza del checklist (lección 2, módulo 1) | Estado al cerrar este módulo |
|---|---|
Grano de fact_orders declarado y verificado | Resuelto — módulo 1 |
Llaves sustitutas, dim_date, dimensiones conformadas | Resuelto — módulo 2 |
| Snowflake vs tabla ancha | Resuelto — módulo 3 |
| Historización de una dimensión que cambia (SCD) | Resuelto — módulo 4 |
| Join punto-en-el-tiempo, deduplicación | Resuelto — módulo 5 |
| Accumulating snapshot, cumulative design | Resuelto — módulo 6 |
| Dimensión junk, más de un hecho | Resuelto — ESTE MÓDULO, MEDALLION_SUMMARY verificado: dim_order_flags (6 filas), contrato de 4 tablas gold (0 discrepancias) |
Las siete filas del checklist que abrió esta guía —en la lección 2 del módulo 1— quedan resueltas. El módulo 8, el capstone de toda la guía, no necesita resolver ninguna pieza nueva del modelado dimensional: necesita integrar las siete piezas que los módulos 1 a 7 ya construyeron y verificaron, en un solo warehouse de punta a punta, con un reporte final de negocio como salida.
Errores comunes
Entregar MEDALLION_SUMMARY sin los assert de las Partes 2 a 4. Qué pasa: alguien, apurado por mostrar la estructura de resumen como resultado final, la construye directamente después de correr las consultas, sin haber pasado por los assert que confirman cada número. Por qué pasa: la estructura de resumen se ve más presentable como "el entregable", y los assert se sienten como pasos preliminares descartables. Cómo detectarlo: si tu entrega final no incluye ninguna evidencia ejecutada de que dim_order_flags tiene 6 filas, que las 40 órdenes resolvieron su flag_key, y que las 4 tablas gold pasan el contrato con 0 discrepancias, estás documentando un proceso sin haber confirmado que funcionó. Cómo corregirlo: los assert de este proyecto no son opcionales — son la garantía que hace confiable todo lo que MEDALLION_SUMMARY documenta.
Pensar que dim_order_flags necesita agregarse a GOLD_CONTRACTS para que el proyecto esté completo. Qué pasa: alguien, después de construir dim_order_flags en la Parte 2, espera que la Parte 4 la incluya como una quinta tabla en GOLD_CONTRACTS, y se preocupa al ver que el proyecto solo valida cuatro. Por qué pasa: dim_order_flags es la tabla más nueva del módulo, y parece natural que también sea parte del contrato principal. Cómo detectarlo: si esperas ver dim_order_flags en la salida de la Parte 4, revisa el diseño de esta guía — las cuatro tablas gold verificadas explícitamente son fact_orders, fact_sessions, fact_store_activity y dim_date, con dim_order_flags como dimensión de apoyo. Cómo corregirlo: nada impide extender GOLD_CONTRACTS con dim_order_flags en un proyecto propio —el ejercicio 1 de la lección 5 ya lo hizo—, pero las cuatro tablas de este proyecto son, específicamente, las que el diseño de la guía nombra como "las 4 tablas gold de la guía".
Confundir el cierre de este módulo con el cierre de la guía completa. Qué pasa: alguien, al ver el checklist completo con las siete filas resueltas, concluye que ya no queda nada por construir en esta guía. Por qué pasa: un checklist completo se siente, naturalmente, como un final. Cómo detectarlo: si no puedes nombrar qué hace el módulo 8, perdiste de vista que el checklist de la lección 2 del módulo 1 fue una lista de conceptos pendientes —todos resueltos ahora—, no una lista de entregables del warehouse completo. Cómo corregirlo: el módulo 8 —el capstone— todavía tiene trabajo real: integrar las siete piezas en un solo flujo de punta a punta, reconstruyendo bronze y silver desde foundations, publicando el star con dim_product historizado, fact_sessions y fact_store_activity, y la tabla ancha para BI del módulo 3 — todo junto, por primera vez, en un solo script.
Ejercicios
Ejercicio 1 — Verifica que ninguna combinación de dim_order_flags quedó sin usar por ninguna orden. Usando order_flags_resolved y dim_order_flags, escribe una consulta con LEFT JOIN que confirme que las seis combinaciones precomputadas fueron usadas al menos una vez por las cuarenta órdenes.
Ver solución
unused_flags = con.sql("""
SELECT f.flag_key, f.payment_method, f.channel
FROM dim_order_flags f
LEFT JOIN order_flags_resolved r ON f.flag_key = r.flag_key
WHERE r.order_id IS NULL
""").fetchall()
print(f"Combinaciones de dim_order_flags sin usar: {len(unused_flags)}")
assert len(unused_flags) == 0
Salida esperada:
Combinaciones de dim_order_flags sin usar: 0
Cero combinaciones sin usar — con solo cuarenta órdenes repartidas entre seis combinaciones posibles, cada una recibió al menos seis órdenes (como ya viste en la lección 4), así que ninguna quedó vacía en esta semana específica de datos.
Ejercicio 2 — Extiende MEDALLION_SUMMARY con el desglose de revenue por payment_method solamente (sin channel). Agrega un campo revenue_by_payment_method que agrupe únicamente por forma de pago.
Ver solución
revenue_by_payment = con.sql("""
SELECT f.payment_method, ROUND(SUM(o.revenue), 2) AS revenue
FROM fact_orders o
JOIN order_flags_resolved r ON o.order_id = r.order_id
JOIN dim_order_flags f ON r.flag_key = f.flag_key
GROUP BY f.payment_method
ORDER BY f.payment_method
""").fetchall()
MEDALLION_SUMMARY["revenue_by_payment_method"] = {pm: rev for pm, rev in revenue_by_payment}
print(MEDALLION_SUMMARY["revenue_by_payment_method"])
Salida esperada:
{'card': 39.5, 'cash': 29.0, 'wallet': 37.65}
39.5 + 29.0 + 37.65 = 106.15, el mismo total de siempre. card es la forma de pago con mayor revenue, seguida de wallet y cash — un desglose que dim_order_flags habilita en una sola línea de GROUP BY, sin ningún cambio a fact_orders.
Ejercicio 3 — Explica, de memoria, qué necesita el módulo 8 de este proyecto para poder empezar. Sin mirar el diseño de la guía, describe en un párrafo de 4-6 frases qué piezas de MEDALLION_SUMMARY —y de las tablas construidas en este proyecto— va a necesitar el módulo 8 para construir el primer warehouse analítico completo de Kiosko.
Ver solución
El módulo 8 necesita, como base, exactamente las tablas que este proyecto dejó verificadas: fact_orders, dim_date, fact_sessions y fact_store_activity —las cuatro tablas gold con contrato confirmado (gold_contract.discrepancies: 0)—, más dim_order_flags y la dimensión degenerada order_id ya declarada dentro del propio hecho. También necesita, aunque este proyecto no las reconstruyó explícitamente, dim_store y dim_product_scd —la dimensión historizada del módulo 4—, porque el capstone integra el star completo, no solo las piezas nuevas de este módulo. Lo que el módulo 8 no necesita repetir es ninguna de las verificaciones que ya quedaron cerradas aquí: no vuelve a construir dim_order_flags desde cero, no vuelve a probar validate_gold_schema() contra un escenario de drift —esas evidencias ya existen, documentadas en MEDALLION_SUMMARY—. Lo que sí agrega el módulo 8, que ningún módulo anterior hizo, es reconstruir bronze y silver desde foundations (no solo asumir que fact_orders ya existe) y publicar mart_daily_sales_obt, la tabla ancha del módulo 3, como el entregable final para el equipo de BI.
Resumen y siguiente paso: el final del módulo 7
Con este mini-proyecto cierras el módulo 7 completo. Construiste dim_order_flags —seis filas, el producto cartesiano completo de tres formas de pago y dos canales—, resolviste las cuarenta órdenes de Kiosko a su flag_key correspondiente, y confirmaste que el revenue desglosado por forma de pago y canal sigue sumando 106.15, el mismo total conocido desde el módulo 1. Corriste validate_gold_schema() sobre las cuatro tablas gold de la guía —veintiocho columnas en total— con cero discrepancias, y documentaste todo en MEDALLION_SUMMARY, la estructura formal que resume, en una sola imagen, un dominio con tres hechos, una dimensión degenerada, una dimensión junk, y un contrato de esquema verificado.
Diste el séptimo paso de un camino de ocho módulos: Kiosko ya no es el fact_orders/dim_store/dim_product plano que dejó foundations — es un warehouse dimensional completo, con grano declarado, star schema, historia preservada, joins correctos en el tiempo, dos patrones de hecho no transaccionales, y ahora un dominio feo nombrado, con dimensiones no estándar y un contrato verificable entre bronze, silver y gold.
Hacia dónde sigues. El módulo 8 —project-kioskos-analytics-warehouse, el capstone de esta guía— integra las siete piezas de los módulos 1 a 7 en un solo flujo de punta a punta: bronze y silver reconstruidos desde foundations, el star completo con dim_product historizado, fact_sessions y fact_store_activity publicados, y mart_daily_sales_obt servida para el equipo de BI —el primer warehouse analítico real de Kiosko, cerrado con el mapa hacia las guías hermanas de este ecosistema.
Recursos
- Kimball Group — "Star Schema / OLAP Cube" — el vocabulario completo de dimensiones degeneradas y junk que este proyecto integró junto al resto del modelo dimensional. kimballgroup.com/data-warehouse-business-intelligence-resources/kimball-techniques/dimensional-modeling-techniques/star-schema-olap-cube. En inglés.
- Kimball Group — "Junk Dimensions" — la definición formal de
dim_order_flags, verificada de punta a punta en este proyecto. kimballgroup.com/data-warehouse-business-intelligence-resources/kimball-techniques/dimensional-modeling-techniques/junk-dimension. En inglés. - Databricks — "What is the medallion lakehouse architecture?" — el marco bronze/silver/gold que sostiene el contrato de esquema verificado en la Parte 4 de este proyecto. docs.databricks.com/aws/en/lakehouse/medallion. En inglés.
- DuckDB — documentación oficial del cliente Python, la interfaz que ejecutó cada verificación de este proyecto. duckdb.org/docs/current/clients/python/overview. En inglés.