Generar las tres vistas que exige la "plataforma chilena" del Parcial 2 —desde 2018, fechas dd-mm-yyyy, cantidades sumadas y precios promediados— y probar que las tres dicen la misma verdad con la prueba de suma cruzada.
El notebook de esta clase: https://gaa1104.vercel.app/notebooks/clase-12.ipynb
Ing. en Administración · mención Finanzas · Prof. Tomás Fernández
El mismo dato diario, semanal y mensual. El punto de la clase es que el grano cambia la historia, y eso sólo se ve superponiendo.
La forma la decide el trabajo que hace el dato —magnitud, identidad, polaridad, cambio en el tiempo, distribución—, no el gusto. Regla del curso: un solo eje.
Lo que quedó planteado la sesión anterior (se proyecta la pregunta, NO la respuesta):
«El modelo está cargado y los conteos cuadran. ¿Cómo pruebas que lo que SALGA de él también cuadra?»
Entregas: las tres respuestas escritas en tu cuaderno. 90 segundos en silencio, 2 minutos en pares, 3 de puesta en común.
Contéstalas de memoria, sin abrir apuntes ni el notebook de la clase anterior (espaciado 1–3–7–21):
dim_fecha y por qué se construye una sola vez?Par (2 min) → plenaria (3 min).
La respuesta a la pregunta colgada, dicha por ellos y escrita en la pizarra:
La prueba de suma cruzada. Si tres vistas del mismo hecho dan tres totales distintos, una está mal —y es la que te va a explotar en la reunión. El control no es opcional: es lo que separa un entregable de un borrador.
Preguntas que quedan planteadas hoy: al cierre.
Se proyecta textual, como llega:
«Filtra desde 2018 en adelante, reformatea la fecha a dd-mm-yyyy y construye tres vistas con granularidad diaria, semanal y mensual (promediando los precios y sumando las cantidades).»
Entregas: las cuatro decisiones escondidas del requisito, escritas en tu cuaderno con la respuesta que elegiste y por qué. 10 minutos, entre todos y en la pizarra.
Esto es exactamente lo que vas a tener que hacer solo en tres semanas. Antes de escribir una línea de SQL, contesta las cuatro:
DATE_TRUNC.SUM. Precios: AVG. Y ojo: "promediar precios" no es una sola cosa, como se va a ver a las 21:40.«El requisito tiene cuatro líneas y cuatro decisiones escondidas. En la prueba, el que responde las cuatro sin darse cuenta de que eran decisiones, saca 4. El que las declara, saca 7.»
Es otra noche: la base parte vacía. Antes de correr nada, el recall real —preguntarles a ellos, no mostrarlo—: «¿qué tablas dejamos ayer y cuál era el grano del hecho?» Recoger las cinco en voz alta y recién entonces reconstruirlas, en silencio, en dos segundos:
duckdb -init herramientas/cargar.sql
.read herramientas/modelo-alimentaria.sql
-- ven_limpia 15.355 · dim_producto 10 · dim_proveedor 4 · dim_fecha 1.461 · fact_ventas 15.355
Entregas: fact_2018 y las tres vistas —diaria, semanal y mensual— creadas en tu máquina, y la inspección visual de la mensual hecha. 25 minutos. Las tres vistas son entregable de la etapa 1 del Parcial 2.
Las celdas vienen escritas, pero las vistas tienen que quedar creadas en tu máquina: es lo que se evalúa. Antes de correr cada una, la misma pregunta: «¿qué cambia respecto de la anterior?»
-- 🔨 ACTIVIDAD · córrela: la base filtrada y las tres vistas -- la base filtrada, UNA sola vez (regla de negocio: 2017 no se usa) CREATE VIEW fact_2018 AS SELECT * FROM fact_ventas WHERE EXTRACT(year FROM fecha_key) >= 2018; -- 1) diaria CREATE VIEW vista_diaria AS SELECT fecha_key AS periodo, SUM(cantidad) AS cantidad, ROUND(AVG(precio_unitario), 1) AS precio_prom FROM fact_2018 GROUP BY 1; -- 2) semanal CREATE VIEW vista_semanal AS SELECT DATE_TRUNC('week', fecha_key) AS periodo, SUM(cantidad) AS cantidad, ROUND(AVG(precio_unitario), 1) AS precio_prom FROM fact_2018 GROUP BY 1; -- 3) mensual CREATE VIEW vista_mensual AS SELECT DATE_TRUNC('month', fecha_key) AS periodo, SUM(cantidad) AS cantidad, ROUND(AVG(precio_unitario), 1) AS precio_prom FROM fact_2018 GROUP BY 1;
«Tres vistas, una sola idea. La granularidad no es una tabla distinta: es una decisión de agrupación. El que entiende eso escribe la cuarta en veinte segundos.»
Inspección visual, obligatoria antes de creerle a nada:
-- 🔨 ACTIVIDAD · córrela y mírala: ¿está el año completo? SELECT * FROM vista_mensual ORDER BY periodo LIMIT 12;
Dos preguntas dirigidas sobre lo que aparece en pantalla: ¿está el año completo? ¿Y por qué las cantidades se mueven mucho y los precios casi nada? (Guardarse esa segunda observación: es la semilla de la sesión 21.)
Entregas: las dos consultas corridas y sus resultados anotados —1.009.104 en las tres vistas, y 1.096 · 157 · 36 períodos—. 20 minutos. Sin esto, las vistas son un borrador, no un entregable.
Corres tú las dos: primero la suma, después el conteo de períodos.
-- 🔨 ACTIVIDAD · córrela tú: las tres vistas tienen que sumar lo mismo SELECT (SELECT SUM(cantidad) FROM vista_diaria) AS diaria, (SELECT SUM(cantidad) FROM vista_semanal) AS semanal, (SELECT SUM(cantidad) FROM vista_mensual) AS mensual; -- 1.009.104 · 1.009.104 · 1.009.104
Las tres iguales, y el número es el de ayer. Y los conteos de filas, que también se comparan:
-- 🔨 ACTIVIDAD · córrela tú: los períodos también tienen que cuadrar SELECT (SELECT COUNT(*) FROM vista_diaria) AS dias, (SELECT COUNT(*) FROM vista_semanal) AS semanas, (SELECT COUNT(*) FROM vista_mensual) AS meses; -- 1.096 · 157 · 36
«1.096 días son exactamente tres años (2018, 2019 y 2020, con el 29 de febrero de 2020 adentro). 36 meses son exactamente tres años. Si te da 35, te falta un mes; y ese mes tiene ventas.»
La suma cruzada ya dijo que las tres vistas son el mismo dato. Ahora se mira si cuentan la misma historia. Para poder superponerlas hay un paso que no se salta: las tres se llevan a unidades por día. Un total mensual y un total diario no caben en el mismo eje —son escalas distintas— y meterlos igual sería el error que la clase 18 desarma. Con la misma unidad, la superposición es legítima.
# 👀 SE MIRA · el gráfico ya viene escrito: se corre y se mira import matplotlib.pyplot as plt GRIS, TINTA, ALERTA = "#b8b8b8", "#102a38", "#8c2b20" # Las tres, en la MISMA unidad: unidades por día. Sin esto no se pueden superponer. dia = bd.sql("SELECT periodo, cantidad AS por_dia FROM vista_diaria ORDER BY 1") sem = bd.sql("""SELECT DATE_TRUNC('week', periodo) AS periodo, SUM(cantidad) / COUNT(*) AS por_dia FROM vista_diaria GROUP BY 1 ORDER BY 1""") mes = bd.sql("""SELECT DATE_TRUNC('month', periodo) AS periodo, SUM(cantidad) / COUNT(*) AS por_dia FROM vista_diaria GROUP BY 1 ORDER BY 1""") fig, eje = plt.subplots(figsize=(11, 5)) eje.plot(dia.periodo, dia.por_dia, color="#d8d8d8", linewidth=0.6, label=f"diaria · {len(dia)} puntos") eje.plot(sem.periodo, sem.por_dia, color=GRIS, linewidth=1.3, label=f"semanal · {len(sem)} puntos") eje.plot(mes.periodo, mes.por_dia, color=ALERTA, linewidth=2.4, label=f"mensual · {len(mes)} puntos") for _, f in dia.nlargest(3, "por_dia").iterrows(): eje.annotate(f"{f.periodo:%d-%m-%Y}\n{f.por_dia:,.0f}".replace(",", "."), xy=(f.periodo, f.por_dia), xytext=(6, -4), textcoords="offset points", fontsize=8, color=TINTA) eje.set_title("Las tres vistas suman las mismas 1.009.104 unidades.\n" "El día tiene tres torres que el mes borra sin avisar", fontsize=13, color=TINTA, loc="left") eje.set_ylabel("unidades por día", fontsize=9) eje.legend(frameon=False, fontsize=9, loc="upper right") for lado in ("top", "right"): eje.spines[lado].set_visible(False) fig.tight_layout() plt.show()
Qué se mira en el resultado:
-- 👀 SE MIRA · una vista sin filtro, y un número que no se ve raro -- una vista mal hecha: agrega ANTES de filtrar SELECT SUM(cantidad) AS total_sin_filtro FROM (SELECT fecha_key, SUM(cantidad) AS cantidad FROM fact_ventas GROUP BY 1); -- 1.316.435
«307.331 unidades de más. Son las 3.814 filas de 2017 que se colaron por una vista a la que se le olvidó el filtro. Y fíjense en lo peligroso: el número no se ve raro. Es plausible, es del mismo orden de magnitud, y un gráfico con él se ve perfecto. Sólo la suma cruzada lo delata.»
La regla del bloque, para el Parcial 2: las tres vistas se construyen sobre la misma base filtrada. Un filtro escrito tres veces es un filtro que algún día va a estar escrito de dos formas.
Viene resuelta: tres promedios del mismo precio, en una sola consulta.
-- 👀 SE MIRA · tres promedios del mismo precio, uno al lado del otro SELECT ROUND((SELECT AVG(precio_prom) FROM vista_mensual), 2) AS promedio_de_promedios, ROUND((SELECT AVG(precio_unitario) FROM fact_2018), 2) AS promedio_simple, ROUND((SELECT SUM(cantidad * precio_unitario) / SUM(cantidad) FROM fact_2018), 2) AS promedio_ponderado; -- 2.349,85 · 2.349,86 · 1.817,93
Se lee de izquierda a derecha y se discute:
Y el detalle que hay que mostrar porque va a aparecer en el parcial:
-- 👀 SE MIRA · los precios nulos que AVG ignora en silencio SELECT COUNT(*) AS precios_nulos FROM fact_2018 WHERE precio_unitario IS NULL; -- 10
«AVG ignora los nulos en silencio: esas 10 filas no están en el denominador y nadie te avisó. Son 10 de 11.541, así que no mueve la aguja —pero tú tienes que saberlo antes de que te lo pregunten, no después.»
Entregas: vista_diaria_ddmmyyyy.csv generado y abierto en un editor de texto, con la primera línea 01-01-2018,852,2210.6. 10 minutos. Este archivo es entregable de la etapa 1 del Parcial 2.
-- 🔨 ACTIVIDAD · córrela tú y después abre el archivo que sale COPY ( SELECT strftime(periodo, '%d-%m-%Y') AS periodo, cantidad, precio_prom FROM vista_diaria ORDER BY periodo ) TO 'salidas/vista_diaria_ddmmyyyy.csv' (HEADER, DELIMITER ',');
Se abre el CSV resultante en un editor de texto y se mira la primera línea: 01-01-2018,852,2210.6.
«La plataforma ya puede comer. Y este archivo es un entregable de la etapa 1 del parcial.»
El detalle que cuesta puntos: el ORDER BY periodo se hace sobre la fecha, no sobre el texto. Se demuestra en dos segundos —si se ordena el texto, 01-02-2018 va antes que 01-01-2019 y el archivo sale desordenado. La fecha se formatea al final, nunca antes de ordenar.
El experto de esta sesión — Control de Gestión:
«DATE_TRUNC('week', ...) en DuckDB parte las semanas el lunes. Es una decisión que tomó la herramienta por ti, y tú la vas a firmar. Entonces: ¿la semana del reporte parte el lunes o el domingo? ¿Y el mes cierra el 30 o el último día hábil?»
Se muestra, medido, lo que cambia:
| Semana desde el lunes | Semana desde el domingo | |
|---|---|---|
| Semanas en el período | 157 | 157 |
| Total de unidades | 1.009.104 | 1.009.104 |
| Primer período del reporte | 2018-01-01 · 5.568 u | 2017-12-31 · 4.998 u |
«El total no se mueve un gramo. Pero cada comparación semanal sí, y con el criterio domingo aparece en tu reporte "desde 2018" un período etiquetado 2017. Imagínate explicando eso en un comité.
La respuesta no está en la documentación de DuckDB: está en Control de Gestión, porque tiene que calzar con el resto de los informes de la empresa. Y si no puedes preguntar, se declara: "semanas ISO, de lunes a domingo".»
Entregas: las dos primeras consultas escritas y corriendo, con el número que tienen que dar. La tercera es bonus. 12 minutos; el docente circula.
Acá no hay solución escrita: la consulta la escribes tú y el número que aparece abajo es para que sepas si te quedó bien.
DATE_TRUNC('quarter', ...). Debe dar 12 trimestres y sumar 1.009.104.JOIN con dim_producto): la Harina de trigo lidera con 152,0 unidades/día y el Cacao cierra con 44,3. Es el germen del requisito 3.2 del parcial.JOIN con dim_fecha.dia_sem). El fin de semana vende casi la mitad que un martes: 94.206 unidades el domingo contra 169.764 el martes. ¿Eso es demanda o es que la bodega no despacha domingo? —otra pregunta para el experto.# 🔨 ACTIVIDAD — escribe TÚ estas consultas. Acá no viene la respuesta. # Descomenta la llamada a bd.sql() de cada una cuando la tengas escrita. vista_trimestral = """ -- 1) La cuarta granularidad: DATE_TRUNC('quarter', ...) sobre fact_2018. -- Tiene que dar 12 trimestres y sumar 1.009.104. """ # bd.sql(vista_trimestral) demanda_por_producto = """ -- 2) Demanda diaria promedio por producto: JOIN de fact_2018 con dim_producto. -- La Harina de trigo tiene que salir primera, con 152,0 unidades por día. """ # bd.sql(demanda_por_producto) dia_de_semana = """ -- 3) BONUS: ¿qué día de la semana se vende más? JOIN con dim_fecha.dia_sem. -- El domingo tiene que dar casi la mitad que el martes. """ # bd.sql(dia_de_semana)
Síntesis, construida con ellos:
Entregas: media hoja con las tres respuestas, antes de salir. 3 minutos, sin nota. Las mismas van en la celda final del notebook, que se descarga y se sube a klearning.
a) La regla de las tres vistas: ¿qué se promedia y qué se suma? b) Las vistas diaria, semanal y mensual suman distinto: ¿qué revisas primero? c) Del 1 al 5, ¿dónde te sientes hoy y qué te falta?
(En la ruta de 24 clases este ticket es el TC12 y agrega una cosa al ítem c: si ya corriste el notebook espejo de la sesión 19.)
Anki S7–S9: el tramo de modelamiento completo entra al mazo hoy.
Preguntas que quedan planteadas (se escriben en la pizarra y no se responden):
→ Las tres vistas cuadran en SQL. Si mañana el equipo trabaja en Python, ¿te va a dar el mismo número?
→ Y si no da lo mismo, ¿qué revisas primero: la herramienta, o la decisión que declaraste?
Tres minutos. No suma nota. Sirve para armar la apertura de la próxima clase.
El notebook de esta clase:
https://gaa1104.vercel.app/notebooks/clase-12.ipynb