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 verificadoResuelto — módulo 1
Llaves sustitutas, dim_date, dimensiones conformadasResuelto — módulo 2
Snowflake vs tabla anchaResuelto — módulo 3
Historización de una dimensión que cambia (SCD)Resuelto — módulo 4
Join punto-en-el-tiempo, deduplicaciónResuelto — módulo 5
Accumulating snapshot, cumulative designResuelto — módulo 6
Dimensión junk, más de un hechoResuelto — 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