GAA1104 · Big Data e Inteligencia de Negocios · Clase 12

Las tres vistas, y el notebook como entregable

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

Clase 12 · el gráfico de hoy

Línea de tiempo, tres granos

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.

Apertura — Active recall · 8 min

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?»

🔨 Actividad — Las tres del recall

Entregas: las tres respuestas escritas en tu cuaderno. 90 segundos en silencio, 2 minutos en pares, 3 de puesta en común.

Apertura — Active recall · 8 min

Contéstalas de memoria, sin abrir apuntes ni el notebook de la clase anterior (espaciado 1–3–7–21):

  1. Sesión 17 · ¿Qué va en dim_fecha y por qué se construye una sola vez?
  2. Sesión 15 · ¿Cuáles son las cuatro verificaciones del modelo de Austral?
  3. Sesión 11 · ¿Cuál es el orden de ataque frente a un archivo nuevo, en sus cuatro pasos?

Par (2 min) → plenaria (3 min).

Apertura — Active recall · 8 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.

El requisito, leído como lo escribe el parcial · 10 min

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).»

🔨 Actividad — Desarma el requisito en sus cuatro decisiones

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.

El requisito, leído como lo escribe el parcial · 10 min

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:

El requisito, leído como lo escribe el parcial · 10 min
  1. Filtro desde 2018 → ¿sobre el hecho o sobre cada vista? (sobre el hecho, una sola vez: si se filtra en cada vista, tarde o temprano una se te queda sin filtro)
  2. Fecha dd-mm-yyyy → ¿al agrupar o al exportar? (al exportar: si agrupas por texto, "01-02-2018" y "01-12-2018" se ordenan mal)
  3. Tres granularidades → la estructura es idéntica; lo único que cambia es el DATE_TRUNC.
  4. Cantidades: SUM. Precios: AVG. Y ojo: "promediar precios" no es una sola cosa, como se va a ver a las 21:40.
El requisito, leído como lo escribe el parcial · 10 min
«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.»
Reconstruir el modelo de ayer · 2 min

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
Las tres vistas, una a una · 25 min

🔨 Actividad — Construye las tres vistas en tu máquina

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?»

Las tres vistas, una a una · 25 min
-- 🔨 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;
Las tres vistas, una a una · 25 min
«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;
Las tres vistas, una a una · 25 min

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.)

Respiro · 5 min
La prueba de suma cruzada · 20 min

🔨 Actividad — Prueba que las tres vistas dicen lo mismo

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.

La prueba de suma cruzada · 20 min
-- 🔨 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:

La prueba de suma cruzada · 20 min
-- 🔨 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
La prueba de suma cruzada · 20 min
«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.

La prueba de suma cruzada · 20 min
# 👀 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()
La prueba de suma cruzada · 20 min

Qué se mira en el resultado:

La prueba de suma cruzada · 20 min
  1. Que las tres líneas viven en la misma banda. Tienen que: es el mismo dato. Si una se despega, la vista está mal construida y la suma cruzada lo habría dicho antes.
  2. Las tres torres de la línea diaria. El 19-07-2018, el 12-06-2019 y el 17-10-2020. La línea semanal las convierte en un bulto y la mensual las hace desaparecer. Ninguna de las tres vistas miente: cada una responde una pregunta distinta.
  3. Y por eso la pregunta que hay que dejar colgada: si el informe mensual es el que sube a la gerencia, ¿quién se enteró de esos tres días? Guárdenlos — el martes que viene son la clase entera.
  4. El eje vertical dice «unidades por día», no «unidades». Ese rótulo es la razón por la que las tres pueden estar en el mismo gráfico. Sin él, el gráfico es una trampa.
La prueba de suma cruzada · 20 min

👀 Se mira — el error, provocado a propósito

-- 👀 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
La prueba de suma cruzada · 20 min
«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.

"Promediar los precios" no es una sola cosa · 15 min

👀 Se mira — tres promedios del mismo precio

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:

"Promediar los precios" no es una sola cosa · 15 min
"Promediar los precios" no es una sola cosa · 15 min

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
"Promediar los precios" no es una sola cosa · 15 min
«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.»
dd-mm-yyyy y exportación · 10 min

🔨 Actividad — Exporta el CSV que la plataforma pide

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.

dd-mm-yyyy y exportación · 10 min
-- 🔨 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.

dd-mm-yyyy y exportación · 10 min
«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 negocio interno, trabajo guiado y cierre · 25 min

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:

El experto de negocio interno, trabajo guiado y cierre · 25 min
Semana desde el lunesSemana desde el domingo
Semanas en el período157157
Total de unidades1.009.1041.009.104
Primer período del reporte2018-01-01 · 5.568 u2017-12-31 · 4.998 u
El experto de negocio interno, trabajo guiado y cierre · 25 min
«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".»
El experto de negocio interno, trabajo guiado y cierre · 25 min

🔨 Actividad — Trabajo guiado: las tres consultas que escribes tú

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.

El experto de negocio interno, trabajo guiado y cierre · 25 min
  1. Vista trimestral —la cuarta granularidad, para probar que el patrón se repite: DATE_TRUNC('quarter', ...). Debe dar 12 trimestres y sumar 1.009.104.
  2. Demanda diaria promedio por producto (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.
  3. Bonus: ¿qué día de la semana se vende más? (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.
El experto de negocio interno, trabajo guiado y cierre · 25 min
# 🔨 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)
El experto de negocio interno, trabajo guiado y cierre · 25 min

Síntesis, construida con ellos:

  1. «¿Cómo pruebas que tus vistas están bien?» → suma cruzada y conteo de períodos.
  2. «¿Dónde va el filtro?» → en la base, una sola vez.
  3. «¿Cuándo se formatea la fecha?» → al final, después de ordenar.
  4. «¿Cuál promedio?» → el que responda tu pregunta, declarado.
El experto de negocio interno, trabajo guiado y cierre · 25 min

🎫 Se lleva — Ticket T18

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.)

El experto de negocio interno, trabajo guiado y cierre · 25 min

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?
Clase 12 · cierre

Ticket de salida

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

← → avanzar · P proyector · F pantalla completa
GAA1104 · C12 · Las tres vistas, y el notebook como entregable 20:30 1 / 1