Módulo 7: Messy Domains And Medallion At Depth

Múltiples hechos, un solo calendario conformado

Descripción

La lección 2 de este módulo midió que dim_date ya sirve a dos hechos —fact_orders y, en espíritu, fact_sessions— pero nunca completó la unión real con un JOIN explícito más allá de fact_orders. Esta lección cierra esa deuda: une los tres hechos de Kiosko —fact_orders, fact_sessions, fact_store_activity— contra el mismo dim_date, y construye una sola tabla resumen que responde, día por día, tres preguntas de negocio distintas a la vez: cuánto vendió Kiosko, cuántas sesiones de navegación hubo, y cuántas tiendas tuvieron actividad — sin que ninguna de las tres tablas de hechos haya necesitado cambiar su forma para lograrlo.

Conexión con el módulo. Esta lección demuestra, con evidencia ejecutada, la pieza central del título del módulo: "múltiples hechos, un solo calendario conformado". Retoma dim_date exactamente como la dejó el módulo 2 —treinta y un filas, agosto de 2026 completo, generada de forma independiente a cualquier hecho— y confirma que esa independencia fue, precisamente, lo que le permitió servir a tres procesos de negocio sin que ninguno de los tres tuviera que negociar con los otros dos.

Una analogía: el mismo calendario de pared, tres agendas distintas colgadas debajo

Retoma la analogía del módulo 2: un calendario de pared, impreso una sola vez, antes de que exista ningún evento que anotar en él. Ahora imagina tres agendas distintas colgadas debajo de ese mismo calendario —una de ventas, una de visitas de clientes, una de actividad operativa de cada sucursal—, cada una anotando sus propios eventos en las mismas fechas del calendario compartido. El gerente de ventas puede consultar solo su agenda; el gerente de operaciones, solo la suya; pero cualquiera de los dos puede, si lo necesita, comparar ambas agendas contra el mismo día del mismo calendario, sin ninguna traducción de fechas entre ellas —el 8 de agosto es el 8 de agosto para las tres agendas, sin ambigüedad—.

Esta lección cuelga, literalmente, las tres agendas de Kiosko —fact_orders, fact_sessions, fact_store_activity— debajo del mismo calendario —dim_date—, y las lee juntas por primera vez.

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.

Ejemplo trabajado: los tres hechos, contra el mismo dim_date

Con el warehouse reconstruido —fact_orders, dim_date, fact_sessions, fact_store_activity, exactamente como quedaron en los módulos 1 a 6, ningún cambio—, la primera unión: fact_orders resumido por día, contra dim_date.

# shared_calendar.py -- continua sobre con, con las 4 tablas heredadas ya reconstruidas
print("=== fact_orders resumido por dia, unido contra dim_date ===")
print(con.sql("""
    SELECT d.calendar_date, d.day_of_week, COUNT(*) AS orders, ROUND(SUM(o.revenue), 2) AS revenue
    FROM dim_date d JOIN fact_orders o ON CAST(o.order_ts AS DATE) = d.calendar_date
    GROUP BY d.calendar_date, d.day_of_week
    ORDER BY d.calendar_date
"""))

Qué esperar.

=== fact_orders resumido por dia, unido contra dim_date ===
┌───────────────┬─────────────┬────────┬─────────┐
│ calendar_date │ day_of_week │ orders │ revenue │
│     date      │   varchar   │ int64  │ double  │
├───────────────┼─────────────┼────────┼─────────┤
│ 2026-08-03    │ Monday      │      8 │   15.85 │
│ 2026-08-04    │ Tuesday     │      6 │   15.85 │
│ 2026-08-05    │ Wednesday   │      2 │    9.55 │
│ 2026-08-06    │ Thursday    │      5 │   11.05 │
│ 2026-08-07    │ Friday      │      7 │   18.05 │
│ 2026-08-08    │ Saturday    │      9 │   31.85 │
│ 2026-08-09    │ Sunday      │      3 │    3.95 │
└───────────────┴─────────────┴────────┴─────────┘

Ocho, seis, dos, cinco, siete, nueve, tres — los mismos conteos diarios de la semana de Kiosko que conoces desde el módulo 1, esta vez agrupados a través de dim_date en vez de directamente sobre order_ts. Ahora, la unión que ningún módulo anterior hizo: fact_sessions.session_date contra el mismo dim_date.calendar_date.

print("\n=== fact_sessions resumido por dia, unido contra dim_date ===")
print(con.sql("""
    SELECT d.calendar_date, d.day_of_week, COUNT(*) AS sessions
    FROM dim_date d JOIN fact_sessions s ON s.session_date = d.calendar_date
    GROUP BY d.calendar_date, d.day_of_week
    ORDER BY d.calendar_date
"""))

Qué esperar.

=== fact_sessions resumido por dia, unido contra dim_date ===
┌───────────────┬─────────────┬──────────┐
│ calendar_date │ day_of_week │ sessions │
│     date      │   varchar   │  int64   │
├───────────────┼─────────────┼──────────┤
│ 2026-08-03    │ Monday      │        2 │
│ 2026-08-04    │ Tuesday     │        3 │
│ 2026-08-05    │ Wednesday   │        2 │
│ 2026-08-06    │ Thursday    │        2 │
│ 2026-08-07    │ Friday      │        3 │
│ 2026-08-08    │ Saturday    │        3 │
│ 2026-08-09    │ Sunday      │        2 │
└───────────────┴─────────────┴──────────┘

Fíjate en algo importante que este JOIN no necesitó: ninguna conversión de tipo, ningún strftime, ningún truco. fact_orders.order_ts es un TIMESTAMP —necesita CAST(... AS DATE) para encontrar su fecha—, pero fact_sessions.session_date ya es un DATE desde que el módulo 6 lo calculó con MIN(CAST(event_ts AS DATE)). Las dos formas de unir son válidas; la diferencia está en qué tan cerca del tipo de dim_date.calendar_date (también DATE) llega cada columna del hecho.

Las tres agendas, colgadas del mismo calendario

Ahora, la tabla que resume el negocio completo de Kiosko día por día, con los tres hechos unidos contra dim_date a la vez —y, deliberadamente, extendida un día antes y un día después de la semana con datos, para confirmar que dim_date sigue siendo independiente de cualquier hecho, exactamente como demostró el módulo 2—.

# three_facts_one_calendar.py -- continua sobre con
print("\n=== Los 3 hechos, un solo calendario conformado (incluye dias sin actividad) ===")
print(con.sql("""
    SELECT d.calendar_date, d.day_of_week, d.is_weekend,
           COALESCE(o.orders, 0) AS orders,
           COALESCE(o.revenue, 0) AS revenue,
           COALESCE(s.sessions, 0) AS sessions,
           COALESCE(a.stores_active, 0) AS stores_active
    FROM dim_date d
    LEFT JOIN (
        SELECT CAST(order_ts AS DATE) AS d, COUNT(*) AS orders, ROUND(SUM(revenue), 2) AS revenue
        FROM fact_orders GROUP BY 1
    ) o ON o.d = d.calendar_date
    LEFT JOIN (
        SELECT session_date AS d, COUNT(*) AS sessions FROM fact_sessions GROUP BY 1
    ) s ON s.d = d.calendar_date
    LEFT JOIN (
        SELECT activity_date AS d, COUNT(DISTINCT store_id) AS stores_active
        FROM fact_store_activity WHERE daily_revenue > 0 GROUP BY 1
    ) a ON a.d = d.calendar_date
    WHERE d.calendar_date BETWEEN '2026-08-01' AND '2026-08-10'
    ORDER BY d.calendar_date
"""))

Qué esperar.

=== Los 3 hechos, un solo calendario conformado (incluye dias sin actividad) ===
┌───────────────┬─────────────┬────────────┬────────┬─────────┬──────────┬───────────────┐
│ calendar_date │ day_of_week │ is_weekend │ orders │ revenue │ sessions │ stores_active │
│     date      │   varchar   │  boolean   │ int64  │ double  │  int64   │     int64     │
├───────────────┼─────────────┼────────────┼────────┼─────────┼──────────┼───────────────┤
│ 2026-08-01    │ Saturday    │ true       │      0 │     0.0 │        0 │             0 │
│ 2026-08-02    │ Sunday      │ true       │      0 │     0.0 │        0 │             0 │
│ 2026-08-03    │ Monday      │ false      │      8 │   15.85 │        2 │             3 │
│ 2026-08-04    │ Tuesday     │ false      │      6 │   15.85 │        3 │             3 │
│ 2026-08-05    │ Wednesday   │ false      │      2 │    9.55 │        2 │             2 │
│ 2026-08-06    │ Thursday    │ false      │      5 │   11.05 │        2 │             3 │
│ 2026-08-07    │ Friday      │ false      │      7 │   18.05 │        3 │             3 │
│ 2026-08-08    │ Saturday    │ true       │      9 │   31.85 │        3 │             3 │
│ 2026-08-09    │ Sunday      │ true       │      3 │    3.95 │        2 │             3 │
│ 2026-08-10    │ Monday      │ false      │      0 │     0.0 │        0 │             0 │
└───────────────┴─────────────┴────────────┴────────┴─────────┴──────────┴───────────────┘
  10 rows                                                                      7 columns

Diez filas, tres hechos, un solo LEFT JOIN por cada uno, y dos días —2026-08-01 y 2026-08-10— donde las tres columnas de negocio caen en cero de forma consistente: dim_date no necesitó que ningún hecho tuviera datos ese día para incluirlo en el resultado, porque su calendario existe de forma completamente independiente, exactamente como el módulo 2 lo diseñó. Esta es la prueba final de que dim_date es conformada en el sentido completo: no solo la comparten tres hechos —eso ya lo midió la lección 2—, sino que ninguno de los tres tuvo que alterar su forma, su grano, ni su lógica de construcción para hacerlo.

Verificación cruzada: las sumas de esta tabla igualan los totales ya conocidos

totals = con.sql("""
    SELECT SUM(orders) AS total_orders, ROUND(SUM(revenue), 2) AS total_revenue, SUM(sessions) AS total_sessions
    FROM (
        SELECT COALESCE(o.orders, 0) AS orders, COALESCE(o.revenue, 0) AS revenue, COALESCE(s.sessions, 0) AS sessions
        FROM dim_date d
        LEFT JOIN (SELECT CAST(order_ts AS DATE) AS d, COUNT(*) AS orders, SUM(revenue) AS revenue FROM fact_orders GROUP BY 1) o ON o.d = d.calendar_date
        LEFT JOIN (SELECT session_date AS d, COUNT(*) AS sessions FROM fact_sessions GROUP BY 1) s ON s.d = d.calendar_date
    )
""").fetchone()

total_orders, total_revenue, total_sessions = totals
print(f"total_orders={total_orders}, total_revenue={total_revenue}, total_sessions={total_sessions}")
assert (total_orders, total_revenue, total_sessions) == (40, 106.15, 17)
print("Verificacion OK: los totales via dim_date coinciden con los ya conocidos (M1: 40/106.15, M6: 17)")

Qué esperar.

total_orders=40, total_revenue=106.15, total_sessions=17
Verificacion OK: los totales via dim_date coinciden con los ya conocidos (M1: 40/106.15, M6: 17)

Sumar la tabla de treinta y un días completos de dim_date —incluyendo los veinticuatro días sin ninguna actividad— produce, exactamente, los mismos totales que ya conocías: cuarenta órdenes y 106.15 de revenue desde el módulo 1, diecisiete sesiones desde el módulo 6. Unir contra un calendario completo, en vez de solo contra los días con datos, no cambia ningún total de negocio — solo hace visibles los días sin actividad, algo que unir directamente fact_orders contra fact_sessions (sin pasar por dim_date) nunca podría mostrar.

Diagrama: tres hechos, un calendario, sin negociación entre ellos

flowchart TD
    D["dim_date\n31 filas, agosto 2026 completo\nGENERADO SIN MIRAR NINGUN HECHO (M2)"]
    D -->|"CAST(order_ts AS DATE)\n= calendar_date"| FO["fact_orders\n40 filas, sin cambios"]
    D -->|"session_date\n= calendar_date"| FS["fact_sessions\n17 filas, sin cambios"]
    D -->|"activity_date\n= calendar_date"| FA["fact_store_activity\n21 filas, sin cambios"]

Profundización: por qué esto no fue posible antes de este módulo

Vale la pena preguntarse por qué esta unión de tres hechos vive aquí, en el módulo 7, y no antes. La respuesta no es técnica —el JOIN de esta lección no usa ninguna sintaxis nueva, nada que el módulo 2 no hubiera enseñado ya—; es de secuencia: fact_sessions y fact_store_activity no existían hasta el módulo 6. La unión de tres hechos contra un calendario compartido solo tiene sentido una vez que los tres hechos existen, verificados, con sus propios grano ya declarados y correctos. Este módulo es, literalmente, el primer punto de la guía donde esa unión es posible — y por eso su título nombra explícitamente "múltiples hechos, un solo calendario conformado" como parte del "dominio feo" que solo aparece cuando el negocio crece más allá de un proceso.

Errores comunes

Unir fact_orders y fact_sessions directamente entre sí, en vez de cada uno contra dim_date. Qué pasa: alguien, buscando comparar ventas y sesiones por día, escribe fact_orders f JOIN fact_sessions s ON CAST(f.order_ts AS DATE) = s.session_date, uniendo los dos hechos entre sí sin pasar por dim_date. Por qué pasa: si ambos ya tienen una columna de fecha, parece innecesario introducir una tercera tabla en el medio. Cómo detectarlo: si tu resultado tiene menos de diez filas para la ventana 2026-08-01 a 2026-08-10, o si pierdes los días sin sesiones o sin órdenes, tu JOIN directo entre hechos se comporta como un INNER JOIN implícito que descarta cualquier día donde uno de los dos hechos no tenga filas. Cómo corregirlo: cada hecho se une contra dim_date, con LEFT JOIN desde el calendario —nunca los hechos entre sí—, exactamente como hizo esta lección. Esa es, precisamente, la definición de "calendario conformado": el punto de encuentro es la dimensión compartida, no un hecho contra otro.

Olvidar el LEFT JOIN y perder los días sin actividad. Qué pasa: alguien usa JOIN (equivalente a INNER JOIN) en vez de LEFT JOIN desde dim_date hacia cada subconsulta agregada, y el resultado final solo muestra los días donde los tres hechos tuvieron actividad simultáneamente. Por qué pasa: JOIN es la palabra más corta y la que más se escribe por costumbre. Cómo detectarlo: si tu tabla final de diez días —2026-08-01 a 2026-08-10— muestra menos de diez filas, perdiste días donde algún hecho no tenía actividad. Cómo corregirlo: dim_date siempre debe ser el lado izquierdo de un LEFT JOIN cuando el objetivo es mostrar el calendario completo —incluyendo días sin ningún evento—; usa COALESCE(..., 0) para que esos días se vean como cero, no como filas faltantes.

Asumir que dim_date necesita una columna distinta por cada hecho que la use. Qué pasa: alguien, al ver que fact_orders usa order_ts (TIMESTAMP) y fact_sessions usa session_date (DATE), concluye que dim_date necesitaría columnas separadas —como date_key_for_orders, date_key_for_sessions— para servir a cada uno. Por qué pasa: cada hecho llega a la fecha con un tipo de columna ligeramente distinto, y parece que eso exige tratamiento distinto en la dimensión. Cómo detectarlo: si duplicas columnas en dim_date para "servir mejor" a cada hecho, perdiste el punto central de una dimensión conformada — su forma es una sola, y es cada hecho el que se adapta a ella (con un CAST si hace falta, como en fact_orders), no al revés. Cómo corregirlo: dim_date mantiene sus siete columnas originales, sin cambios, sin importar cuántos hechos la consulten — la adaptación de tipo, cuando hace falta, vive en la consulta que une el hecho contra la dimensión, nunca en la dimensión misma.

Ejercicios

Ejercicio 1 — Calcula qué día tuvo la mejor combinación de ventas y sesiones. Usando la tabla de diez días del ejemplo trabajado, identifica el día con mayor revenue y mayor sessions al mismo tiempo, si existe alguno.

Ver solución
print(con.sql("""
    SELECT d.calendar_date, d.day_of_week,
           COALESCE(o.orders, 0) AS orders, COALESCE(o.revenue, 0) AS revenue, COALESCE(s.sessions, 0) AS sessions
    FROM dim_date d
    LEFT JOIN (SELECT CAST(order_ts AS DATE) AS d, COUNT(*) AS orders, ROUND(SUM(revenue), 2) AS revenue FROM fact_orders GROUP BY 1) o ON o.d = d.calendar_date
    LEFT JOIN (SELECT session_date AS d, COUNT(*) AS sessions FROM fact_sessions GROUP BY 1) s ON s.d = d.calendar_date
    WHERE d.calendar_date BETWEEN '2026-08-03' AND '2026-08-09'
    ORDER BY o.revenue DESC, s.sessions DESC
    LIMIT 1
"""))

Salida esperada:

┌───────────────┬─────────────┬────────┬─────────┬──────────┐
│ calendar_date │ day_of_week │ orders │ revenue │ sessions │
│     date      │   varchar   │ int64  │ double  │  int64   │
├───────────────┼─────────────┼────────┼─────────┼──────────┤
│ 2026-08-08    │ Saturday    │      9 │   31.85 │        3 │
└───────────────┴─────────────┴────────┴─────────┴──────────┘

2026-08-08 (sábado) tiene el mayor revenue (31.85) de toda la semana y empata en el número máximo de sesiones (3, junto con martes y viernes) — el mejor día combinado de Kiosko, según ambos hechos a la vez, algo que solo se puede responder con los dos unidos contra el mismo calendario.

Ejercicio 2 — Confirma que ningún día de agosto 2026 tiene más sesiones que órdenes de las que tuvo esa semana. Sin usar la tabla completa de diez días, escribe una consulta que compare, para cada día con datos, sessions contra orders, y cuenta cuántos días tuvieron más sesiones que órdenes.

Ver solución
result = con.sql("""
    SELECT COUNT(*) AS days_with_more_sessions_than_orders
    FROM (
        SELECT COALESCE(o.orders, 0) AS orders, COALESCE(s.sessions, 0) AS sessions
        FROM dim_date d
        LEFT JOIN (SELECT CAST(order_ts AS DATE) AS d, COUNT(*) AS orders FROM fact_orders GROUP BY 1) o ON o.d = d.calendar_date
        LEFT JOIN (SELECT session_date AS d, COUNT(*) AS sessions FROM fact_sessions GROUP BY 1) s ON s.d = d.calendar_date
        WHERE d.calendar_date BETWEEN '2026-08-03' AND '2026-08-09'
    )
    WHERE sessions > orders
""").fetchone()[0]
print(f"dias con mas sesiones que ordenes: {result}")

Salida esperada:

dias con mas sesiones que ordenes: 0

Cero días — en cada uno de los siete días de la semana de Kiosko, el número de órdenes fue igual o mayor al número de sesiones registradas, algo consistente con el hecho de que las sesiones son solo una fuente entre varias de las ventas de Kiosko (la mayoría de las ventas registradas en fact_orders provienen del punto de venta físico, no solo del canal digital que events describe).

Ejercicio 3 — Explica, de memoria, por qué is_weekend (una columna de dim_date) puede responder preguntas sobre los tres hechos a la vez, sin que ninguno de los tres la almacene. En 2-3 frases, explica cómo fact_orders, fact_sessions y fact_store_activity pueden filtrarse o agruparse por fin de semana sin que ninguna de las tres tenga una columna is_weekend propia.

Ver solución

is_weekend vive una sola vez, en dim_date, calculada de forma independiente a cualquier hecho —weekday_index >= 5, según el módulo 2—; cualquier hecho que se una contra dim_date por su columna de fecha hereda automáticamente esa clasificación, sin necesitar su propia copia. Esto es, precisamente, el beneficio central de una dimensión conformada: un atributo calculado una sola vez —is_weekend— queda disponible para agrupar o filtrar cualquier número de hechos presentes o futuros, sin que cada uno tenga que recalcularlo ni almacenarlo por separado. Si fact_store_activity necesitara mañana filtrar por fin de semana, le bastaría unirse contra dim_date — no necesitaría ningún cambio de esquema propio.

Resumen y siguiente paso

Esta lección unió, por primera vez en esta guía, los tres hechos de Kiosko —fact_orders, fact_sessions, fact_store_activity— contra el mismo dim_date, con LEFT JOIN desde el calendario hacia cada uno, incluyendo días sin actividad para demostrar que la independencia de dim_date (diseñada desde el módulo 2) se sostiene incluso sirviendo a tres procesos de negocio a la vez. La verificación cruzada confirmó que sumar a través del calendario completo produce exactamente los mismos totales ya conocidos: cuarenta órdenes, 106.15 de revenue, diecisiete sesiones.

Antes de avanzar deberías poder: escribir de memoria el patrón LEFT JOIN desde dim_date hacia una subconsulta agregada por hecho; explicar por qué los hechos nunca se unen directamente entre sí, sino cada uno contra la dimensión compartida; y justificar por qué is_weekend no necesita existir en ninguna tabla de hechos.

La lección 7 cierra el argumento técnico del módulo con la pregunta que todavía falta: si el esquema de una tabla gold necesita cambiar de verdad —no un accidente, sino una evolución real del negocio—, ¿cómo se hace sin romper el contrato que validate_gold_schema() ya protege?

Recursos