Módulo 2: The Star Schema And Conformed Dimensions
Construyendo dim_date: el calendario de Kiosko
Descripción
Esta es la lección que cierra la deuda que el módulo 1 nombró explícitamente en su primer párrafo: "no existe dim_date". En esta lección la construyes de verdad — una función Python pura, generate_date_dim(start_date, end_date), que produce una fila por cada día del calendario dentro de un rango fijo, con columnas listas para responder cualquier pregunta de negocio por día, semana, mes o fin de semana, sin volver a calcular nada dos veces.
Conexión con el módulo. Esta lección construye la cuarta tabla del star —la única que faltaba por completo—. Junto con store_key y product_key de la lección anterior, dim_date deja las tres dimensiones listas para el ensamblaje final de la lección 7.
Una analogía: el calendario de pared, impreso una sola vez
Piensa en un calendario de pared de esos que se cuelgan en una oficina el primero de enero: doce meses, cada día con su casilla, su nombre de día de la semana ya impreso, los fines de semana ya marcados en otro color. Nadie construye ese calendario reaccionando a los eventos que van a ocurrir — se imprime completo, de una vez, antes de que exista ningún evento que registrar en él. El calendario es útil precisamente porque es independiente: sirve para anotar una reunión, un cumpleaños o un feriado, sin que ninguno de esos eventos haya tenido que "generar" el día en el que ocurre.
dim_date es ese calendario de pared. Se genera una vez, para un rango de fechas completo, sin mirar ni una fila de fact_orders — de hecho, el ejemplo de esta lección va a generar un mes entero de agosto de 2026, no solo los siete días en los que Kiosko tuvo ventas. Esa independencia es la propiedad más importante de una dimensión de calendario bien construida: cualquier hecho nuevo que Kiosko agregue en el futuro —sesiones de navegación, actividad por tienda, lo que sea— puede unirse contra el mismo dim_date, sin que nadie tenga que regenerarlo ni ajustarlo.
Ejemplo trabajado: generate_date_dim(), en Python puro
La función central de esta lección no usa ninguna librería externa —solo datetime.date y datetime.timedelta de la librería estándar de Python—, y no depende de ninguna fecha del sistema: recibe un rango fijo, start_date y end_date, y produce siempre la misma lista de filas para ese mismo rango, sin importar cuándo la corras.
# dim_date.py
from datetime import date, timedelta
import duckdb
DAY_NAMES = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
def generate_date_dim(start_date: str, end_date: str) -> list[dict]:
"""Genera filas fijas de dim_date entre start_date y end_date (ambos incluidos, formato YYYY-MM-DD).
Puro Python, sin datetime.now() ni ninguna fuente de no-determinismo: el mismo
start_date/end_date siempre produce, byte a byte, la misma lista de filas.
"""
start = date.fromisoformat(start_date)
end = date.fromisoformat(end_date)
if end < start:
raise ValueError(f"end_date ({end_date}) es anterior a start_date ({start_date})")
rows = []
current = start
while current <= end:
weekday_index = current.weekday() # 0=Monday ... 6=Sunday, sin depender del locale del sistema
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
dim_date_rows = generate_date_dim("2026-08-01", "2026-08-31")
print(f"Total de filas generadas para agosto 2026: {len(dim_date_rows)}\n")
print("=== Primeras 3 filas ===")
for row in dim_date_rows[:3]:
print(row)
print("\n=== Ultimas 3 filas ===")
for row in dim_date_rows[-3:]:
print(row)
Qué esperar. Al correr python3 dim_date.py, la salida es exactamente esta:
Total de filas generadas para agosto 2026: 31
=== Primeras 3 filas ===
{'date_key': 20260801, 'calendar_date': datetime.date(2026, 8, 1), 'day_of_week': 'Saturday', 'month': 8, 'quarter': 3, 'year': 2026, 'is_weekend': True}
{'date_key': 20260802, 'calendar_date': datetime.date(2026, 8, 2), 'day_of_week': 'Sunday', 'month': 8, 'quarter': 3, 'year': 2026, 'is_weekend': True}
{'date_key': 20260803, 'calendar_date': datetime.date(2026, 8, 3), 'day_of_week': 'Monday', 'month': 8, 'quarter': 3, 'year': 2026, 'is_weekend': False}
=== Ultimas 3 filas ===
{'date_key': 20260829, 'calendar_date': datetime.date(2026, 8, 29), 'day_of_week': 'Saturday', 'month': 8, 'quarter': 3, 'year': 2026, 'is_weekend': True}
{'date_key': 20260830, 'calendar_date': datetime.date(2026, 8, 30), 'day_of_week': 'Sunday', 'month': 8, 'quarter': 3, 'year': 2026, 'is_weekend': True}
{'date_key': 20260831, 'calendar_date': datetime.date(2026, 8, 31), 'day_of_week': 'Monday', 'month': 8, 'quarter': 3, 'year': 2026, 'is_weekend': False}
Treinta y un filas — un día por cada día de agosto de 2026, ni uno más ni uno menos —. Fíjate en algo deliberado del rango elegido: cubre por completo la semana de órdenes de Kiosko (3 al 9 de agosto), pero va mucho más allá — el 1 y 2 de agosto (sábado y domingo, sin ninguna orden registrada) también tienen su fila, igual que el resto del mes. Esa es, precisamente, la independencia de la que habla la analogía del calendario de pared: dim_date no se generó a partir de fact_orders — se generó sola, completa, lista para cualquier fecha del mes que alguien necesite consultar, tenga o no tenga ventas ese día.
Ahora, carga esas filas en DuckDB con CREATE TABLE dim_date AS SELECT * FROM dim_date_rows y verifica la semana de Kiosko dentro de ese calendario completo:
# dim_date_load.py -- continua sobre dim_date_rows generado arriba
con = duckdb.connect()
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],
)
print("=== dim_date cargada en DuckDB: conteo total ===")
print(con.sql("SELECT COUNT(*) AS total_dates FROM dim_date"))
print("=== dim_date: solo la semana de ordenes de Kiosko (2026-08-03 a 2026-08-09) ===")
print(con.sql("""
SELECT date_key, calendar_date, day_of_week, is_weekend
FROM dim_date
WHERE calendar_date BETWEEN '2026-08-03' AND '2026-08-09'
ORDER BY calendar_date
"""))
print("=== dim_date: cuantos dias son fin de semana en agosto 2026 ===")
print(con.sql("SELECT is_weekend, COUNT(*) AS days FROM dim_date GROUP BY is_weekend ORDER BY is_weekend"))
Qué esperar. Al correr python3 dim_date_load.py, la salida es exactamente esta:
=== dim_date cargada en DuckDB: conteo total ===
┌─────────────┐
│ total_dates │
│ int64 │
├─────────────┤
│ 31 │
└─────────────┘
=== dim_date: solo la semana de ordenes de Kiosko (2026-08-03 a 2026-08-09) ===
┌──────────┬───────────────┬─────────────┬────────────┐
│ date_key │ calendar_date │ day_of_week │ is_weekend │
│ int32 │ date │ varchar │ boolean │
├──────────┼───────────────┼─────────────┼────────────┤
│ 20260803 │ 2026-08-03 │ Monday │ false │
│ 20260804 │ 2026-08-04 │ Tuesday │ false │
│ 20260805 │ 2026-08-05 │ Wednesday │ false │
│ 20260806 │ 2026-08-06 │ Thursday │ false │
│ 20260807 │ 2026-08-07 │ Friday │ false │
│ 20260808 │ 2026-08-08 │ Saturday │ true │
│ 20260809 │ 2026-08-09 │ Sunday │ true │
└──────────┴───────────────┴─────────────┴────────────┘
=== dim_date: cuantos dias son fin de semana en agosto 2026 ===
┌────────────┬───────┐
│ is_weekend │ days │
│ boolean │ int64 │
├────────────┼───────┤
│ false │ 21 │
│ true │ 10 │
└────────────┴───────┘
Detente en la primera tabla de la semana de Kiosko: los comentarios de raw_orders.py, desde el módulo 1, ya llamaban al 8 de agosto "Sábado" y al 9 de agosto "Domingo" — pero esa era una anotación humana, escrita a mano en un comentario de código, nunca verificada. Ahora dim_date lo confirma de forma independiente, calculado, no copiado: 2026-08-08 es efectivamente Saturday, 2026-08-09 es efectivamente Sunday, con is_weekend = true en ambos. Esto no es casualidad — es la primera vez que esta guía verifica, con código, algo que hasta ahora solo existía como comentario humano.
Diagrama: las siete columnas de dim_date
┌──────────────────────────────────────────────────────────────────────┐
│ dim_date │
├──────────────┬───────────────────────────────────────────────────────┤
│ date_key │ INTEGER, llave sustituta -- YYYYMMDD (ej. 20260803) │
│ calendar_date│ DATE -- la fecha real (ej. 2026-08-03) │
│ day_of_week │ VARCHAR -- 'Monday'...'Sunday', calculado sin locale │
│ month │ INTEGER -- 1 a 12 │
│ quarter │ INTEGER -- 1 a 4, derivado de month │
│ year │ INTEGER -- ej. 2026 │
│ is_weekend │ BOOLEAN -- true si Saturday o Sunday │
└──────────────┴───────────────────────────────────────────────────────┘
Profundización: date_key es una excepción deliberada a la regla de la lección 3
En la lección anterior aprendiste que una llave sustituta "no tiene ningún significado de negocio" — store_key = 1 no te dice nada sobre la tienda hasta que la unes contra dim_store. date_key, tal como lo construyó esta lección, rompe esa regla a propósito: date_key = 20260803 sí tiene significado — cualquiera que lo vea reconoce que es "3 de agosto de 2026", sin necesitar ningún JOIN.
Esto es exactamente lo que Kimball llama, en la práctica de la industria, una llave inteligente (smart key), y es la única excepción ampliamente aceptada a la regla de "la llave sustituta no debe tener significado". La razón por la que se acepta específicamente para dim_date: el calendario es, de todas las dimensiones posibles, la más estable y universalmente entendida que existe — un día no cambia de significado nunca, no se renombra, no se fusiona con otro día. Esa estabilidad absoluta es lo que hace seguro codificar el significado directamente en la llave, algo que sería mucho más riesgoso hacer con, por ejemplo, un store_key basado en el nombre de la tienda (que sí puede cambiar, como viste en el ejercicio 3 de la lección anterior).
Una segunda decisión que vale la pena explicar: day_of_week se calculó con una lista fija en Python (DAY_NAMES), indexada por date.weekday(), en vez de usar una función de formato de fecha que dependiera del idioma configurado en el sistema operativo donde corre el script. Esta es una decisión de reproducibilidad, no de estilo: una función de fecha sensible al idioma del sistema podría devolver "lunes" en una máquina configurada en español y "Monday" en otra configurada en inglés, para exactamente la misma fecha — rompiendo la garantía de que este código produce, siempre, la misma salida byte a byte, sin importar en qué computadora corra.
Errores comunes
Generar dim_date únicamente para el rango de fechas que ya tiene fact_orders. Qué pasa: alguien, al ver que Kiosko solo tiene órdenes entre el 3 y el 9 de agosto, genera dim_date con exactamente ese rango de siete días — ni uno más. Por qué pasa: parece más eficiente generar solo lo que "se va a usar hoy", evitando filas que hoy no tienen ninguna orden asociada. Cómo detectarlo: si tu dim_date no tiene ninguna fila para fechas sin ventas —como el 1 y 2 de agosto de esta lección—, cualquier pregunta futura de negocio ("¿cuántos días sin ventas tuvo Kiosko este mes?") se vuelve imposible de responder, porque esos días ni siquiera existen en tu calendario. Cómo corregirlo: dim_date se genera para el rango de calendario completo que el negocio necesita —típicamente meses o años, mucho más allá de lo que el dato actual cubre—, exactamente como hizo esta lección con el mes completo de agosto en vez de solo la semana con ventas.
Calcular el día de la semana con una función dependiente del sistema operativo. Qué pasa: alguien usa current.strftime("%A") (el nombre completo del día, según el locale) en vez de la lista fija DAY_NAMES de esta lección. Por qué pasa: strftime("%A") parece más simple —una línea menos de código—, y funciona correctamente en la máquina donde se prueba por primera vez. Cómo detectarlo: si corres el mismo script en dos computadoras con configuración de idioma distinta y obtienes nombres de día diferentes ("Monday" en una, "lunes" en otra), tu código no es reproducible — viola directamente la regla de esta guía de que cualquier "Qué esperar" debe ser idéntico, byte a byte, sin importar dónde se ejecute. Cómo corregirlo: usa siempre una lista fija indexada por date.weekday() (un entero de 0 a 6, que no depende de ningún locale), como hace generate_date_dim() en esta lección.
Confundir quarter calculado con quarter fiscal. Qué pasa: alguien asume que quarter = (month - 1) // 3 + 1 siempre corresponde al trimestre fiscal de cualquier empresa, sin verificar que Kiosko —o cualquier negocio real— use el calendario estándar (enero-marzo, abril-junio...) para sus reportes financieros. Por qué pasa: el cálculo de trimestre calendario es tan común que es fácil asumir que es universal. Cómo detectarlo: si un reporte de "ventas del Q1" no coincide con lo que el equipo financiero de una empresa real reporta como su Q1, es probable que esa empresa use un año fiscal desplazado (empezando en abril, julio, u octubre, una práctica común en muchas industrias). Cómo corregirlo: para Kiosko, esta guía asume calendario estándar sin año fiscal desplazado —una simplificación razonable para el alcance de este caso—, pero en un warehouse de producción real, quarter (y year) fiscal, cuando difiere del calendario, se agregan como columnas adicionales explícitas (fiscal_quarter, fiscal_year), nunca sobrescribiendo el quarter calendario que dim_date ya calcula.
Ejercicios
Ejercicio 1 — Genera dim_date para todo el tercer trimestre de 2026. Modifica la llamada a generate_date_dim() para cubrir el rango completo "2026-07-01" a "2026-09-30" (julio, agosto y septiembre), y confirma cuántas filas totales genera y cuántas de esas filas son fin de semana.
Ver solución
q3_rows = generate_date_dim("2026-07-01", "2026-09-30")
print(f"Total filas Q3 2026: {len(q3_rows)}")
weekend_q3 = sum(1 for r in q3_rows if r["is_weekend"])
print(f"Dias de fin de semana en Q3 2026: {weekend_q3}")
Salida esperada:
Total filas Q3 2026: 92
Dias de fin de semana en Q3 2026: 26
Julio (31 días) + agosto (31 días) + septiembre (30 días) = 92 días totales — y de esos, 26 caen en sábado o domingo. Fíjate en que este cálculo no necesitó ninguna consulta a fact_orders: es exactamente la independencia de dim_date que esta lección explicó — el calendario existe por sí solo, sin depender de ningún dato de ventas.
Ejercicio 2 — Verifica que cada date_key corresponde exactamente a su calendar_date. Usando dim_date ya cargada en DuckDB, escribe una consulta que confirme que, para cada fila, date_key (como texto) coincide con calendar_date formateada como YYYYMMDD — una verificación de que la llave inteligente nunca se desincroniza de la fecha real que representa.
Ver solución
print(con.sql("""
SELECT COUNT(*) AS filas_desincronizadas
FROM dim_date
WHERE CAST(date_key AS VARCHAR) != strftime(calendar_date, '%Y%m%d')
"""))
Salida esperada:
┌───────────────────────┐
│ filas_desincronizadas │
│ int64 │
├───────────────────────┤
│ 0 │
└───────────────────────┘
Cero filas desincronizadas — confirma, con evidencia y no con intuición, que date_key siempre codifica exactamente la fecha de calendar_date en la misma fila, para las 31 filas de agosto de 2026. Esta es la misma disciplina de verificación de grano y de llaves que ya viste en las lecciones anteriores, aplicada ahora a la llave inteligente de dim_date.
Ejercicio 3 — Explica en tus propias palabras por qué date_key es una excepción, no una regla general. Usando la profundización de esta lección, explica en 2-3 frases por qué sería una mala idea aplicar el mismo patrón de "llave con significado" a store_key, codificando por ejemplo el nombre de la ciudad dentro del número.
Ver solución
date_key funciona como llave inteligente porque el calendario es completamente estable — un día nunca cambia de significado, nunca se renombra—, así que codificar la fecha dentro de la llave nunca queda "desactualizado". Una tienda, en cambio, sí puede cambiar de nombre, de ciudad (si Kiosko la reubicara) o incluso cerrar y reabrir con otro identificador — si store_key codificara, por ejemplo, la ciudad actual de la tienda, un cambio de ciudad dejaría la llave con información falsa, sin ninguna forma limpia de corregirla sin romper referencias existentes. Por eso store_key sigue siendo un entero sin significado (la regla general de la lección 3), y date_key es la excepción deliberada — la estabilidad de lo que describe la dimensión es lo que determina si una llave inteligente es segura o no.
Resumen y siguiente paso
En esta lección construiste dim_date de punta a punta: generate_date_dim(start_date, end_date) en Python puro, sin ninguna fuente de no determinismo, generando 31 filas para agosto de 2026 —un mes completo, no solo los siete días con órdenes—, cargadas en DuckDB con CREATE TABLE dim_date AS SELECT * FROM dim_date_rows. Verificaste, con una consulta real, que el 8 y 9 de agosto son efectivamente fin de semana —confirmando, por primera vez con código, algo que hasta ahora solo era un comentario humano en raw_orders.py—. También aprendiste que date_key es la única excepción aceptada a la regla de "llave sustituta sin significado", precisamente porque el calendario es la dimensión más estable que existe.
Antes de avanzar deberías poder: escribir de memoria la firma de generate_date_dim() y sus siete columnas de salida; explicar por qué dim_date se genera independientemente de fact_orders, no a partir de él; y justificar por qué date_key puede ser una llave inteligente cuando store_key no debería serlo.
Con las tres dimensiones completas —dim_store, dim_product con llave sustituta, dim_date recién construida—, la lección 5 da un paso conceptual: qué significa que una dimensión sea conformada, y por qué dim_store y dim_date, tal como quedaron en este módulo, están listas para servir a un segundo proceso de negocio que todavía no existe.
Recursos
- Kimball Group — "Star Schema / OLAP Cube" — la fuente que documenta
dim_datecomo la dimensión conformada más universal de cualquier warehouse dimensional. kimballgroup.com/data-warehouse-business-intelligence-resources/kimball-techniques/dimensional-modeling-techniques/star-schema-olap-cube. En inglés. - Python — documentación oficial de
datetime, la única librería usada paragenerate_date_dim(): fechas siempre fijas, nuncadatetime.now(). docs.python.org/3/library/datetime.html. En inglés. - DuckDB — documentación de funciones de formato de fecha (
strftime), usada para construirdate_keyy para verificar su sincronización concalendar_dateen esta lección. duckdb.org/docs/current/sql/functions/dateformat. En inglés.