GAA1104 · Big Data e Inteligencia de Negocios · Clase 03

Agregar y unir (+ Control C1)

Respondes preguntas de negocio con GROUP BY, descubres que el archivo escribe 100 nombres de cliente donde hay 18 clientes y que no se arregla solo, y lo enriqueces con un JOIN contra dos catálogos que tampoco están limpios.

El notebook de esta clase: https://gaa1104.vercel.app/notebooks/clase-03.ipynb

Ing. en Administración · mención Finanzas · Prof. Tomás Fernández

Clase 03 · el gráfico de hoy

Barras horizontales, ordenadas de mayor a menor

La pregunta es de magnitud comparada entre categorías (venta por segmento). Horizontales porque las etiquetas son nombres largos, y ordenadas porque el orden alfabético no dice nada.

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 clase anterior (se proyecta la pregunta, NO la respuesta):

«Si el segmento está escrito de 23 formas, ¿de cuántas formas estará escrito el nombre del cliente? ¿Y el RUT?»

Tres preguntas, en silencio, 90 segundos (espaciado 1–3–7–21). Nadie habla todavía:

Apertura — Active recall · 8 min
  1. (clase 02) Filtramos WHERE segmento = 'Minería' y salieron CLP 980.768.001. Con las cinco escrituras salieron 1.360.516.886. ¿Por qué la primera consulta no dio error?
  2. (clase 01) La pregunta del gerente de Administración y Finanzas tenía dos partes. ¿Cuáles eran?
  3. (clase 01, contrato de IA) Le pides a la IA que te escriba un GROUP BY, funciona y lo entregas. ¿En qué nivel del contrato estás y qué tienes que hacer antes de entregarlo?
Apertura — Active recall · 8 min

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

La pregunta colgada NO se responde acá con el número: se responde a las 21:00, corriendo la consulta. Hoy la apertura sólo deja el pronóstico anotado: pedir en voz alta «¿cuántas formas creen que hay del nombre del cliente? escríbanlo en el cuaderno».

Las agregaciones del oficio, y HAVING · 22 min

La analogía, dicha una sola vez: tomas las 6.000 filas, las repartes en cajones —un cajón por segmento— y pegas un resumen en la tapa de cada cajón. Eso es GROUP BY. Lo que va en la tapa lo eliges tú: SUM, COUNT, AVG, MIN, MAX.

Las agregaciones del oficio, y HAVING · 22 min
CREATE OR REPLACE TABLE ventas AS
SELECT * FROM read_csv_auto(
  'datos/finanzas/ventas_extracto.csv');

SELECT segmento,
       COUNT(*) AS lineas,
       ROUND(SUM(cantidad * precio_unitario)) AS venta_clp
FROM ventas
WHERE moneda = 'CLP'
GROUP BY segmento
ORDER BY venta_clp DESC;
Las agregaciones del oficio, y HAVING · 22 min
segmentolineasventa_clp
Construcción9771.782.114.743
Retail9881.762.573.723
Industrial1.0081.451.896.518
Minería664980.768.001
INDUSTRIAL284530.292.913
RETAIL270417.771.014
CONSTRUCCIÓN217398.467.114
Público265395.441.830
Las agregaciones del oficio, y HAVING · 22 min

Pregunta a la sala: «¿Cuántos segmentos comerciales tiene esta empresa?»

«El GROUP BY no se rompió. Hizo exactamente lo que le pediste: juntar lo que está escrito igual. INDUSTRIAL no está escrito igual que Industrial. Para SQL son dos cajones. Para el negocio son el mismo cliente pagando la misma factura.»

Primer auxilio, y su límite:

Las agregaciones del oficio, y HAVING · 22 min
SELECT upper(strip_accents(trim(segmento))) AS seg,
       COUNT(*) AS lineas,
       ROUND(SUM(cantidad * precio_unitario)) AS venta_clp
FROM ventas
WHERE moneda = 'CLP'
GROUP BY 1
ORDER BY venta_clp DESC;
Las agregaciones del oficio, y HAVING · 22 min
seglineasventa_clp
CONSTRUCCION1.4642.599.966.121
RETAIL1.4002.439.292.126
INDUSTRIAL1.4542.197.864.560
MINERIA9111.360.516.886
PUBLICO376531.366.093
Las agregaciones del oficio, y HAVING · 22 min

Cinco filas. Y ahí está otra vez el 1.360.516.886 de la clase pasada — el número correcto de Minería, ahora sin escribir las cinco variantes a mano.

«Tres funciones de texto —mayúsculas, sin tilde, sin espacios sobrantes— y los 23 cajones se volvieron 5. Guarden esa idea: **normalizar es agrupar por lo que el dato significa, no por cómo se escribió.**»
Las agregaciones del oficio, y HAVING · 22 min

Y ahora se mira. Las dos consultas ya dieron el número: el gráfico no lo reemplaza, lo hace evidente. Es el primer gráfico del semestre y trae escrita la regla que se usa hasta el examen — la forma la decide el trabajo que hace el dato. Acá el trabajo es comparar magnitudes entre categorías con nombres largos, así que son barras horizontales ordenadas por valor, nunca alfabéticas: el orden alfabético no dice nada.

Las agregaciones del oficio, y HAVING · 22 min
import matplotlib.pyplot as plt

# La paleta del curso, y no cambia en todo el semestre: gris para todo,
# UN color para lo único que se quiere mirar.
GRIS, TINTA, ALERTA = "#b8b8b8", "#102a38", "#8c2b20"

sucio = bd.sql("""
  SELECT segmento AS etiqueta,
         upper(strip_accents(trim(segmento))) AS clave,
         SUM(cantidad * precio_unitario) / 1e6 AS venta_mm
  FROM ventas WHERE moneda = 'CLP'
  GROUP BY 1, 2 ORDER BY venta_mm""")

limpio = bd.sql("""
  SELECT upper(strip_accents(trim(segmento))) AS etiqueta,
         upper(strip_accents(trim(segmento))) AS clave,
         SUM(cantidad * precio_unitario) / 1e6 AS venta_mm
  FROM ventas WHERE moneda = 'CLP'
  GROUP BY 1, 2 ORDER BY venta_mm""")

fig, ejes = plt.subplots(1, 2, figsize=(12, 6.4), sharex=True)
for eje, d, rotulo, grosor in ((ejes[0], sucio,  "Como lo escribió el ERP", 0.72),
                               (ejes[1], limpio, "Como se llama el negocio", 0.45)):
    eje.barh(range(len(d)), d.venta_mm, height=grosor,
             color=[ALERTA if c == "MINERIA" else GRIS for c in d.clave])
    eje.set_yticks(range(len(d)))
    eje.set_yticklabels([f"«{e}»" for e in d.etiqueta], fontsize=8.5)
    eje.set_title(f"{rotulo}: {len(d)} barras", fontsize=12, color=TINTA, loc="left")
    eje.set_xlabel("venta en millones de CLP", fontsize=9)
    for lado in ("top", "right", "left"):
        eje.spines[lado].set_visible(False)
    eje.tick_params(length=0)

for y, v in enumerate(limpio.venta_mm):
    ejes[1].text(v + 30, y, f"{v:,.0f}".replace(",", "."), va="center",
                 fontsize=9, color=TINTA)

fig.suptitle("Los 23 cajones de la izquierda son 5 segmentos:\n"
             "en color, las cinco escrituras de Minería que a la derecha son una sola barra",
             fontsize=13, color=TINTA, x=0.02, ha="left")
fig.tight_layout()
plt.show()
Las agregaciones del oficio, y HAVING · 22 min

Qué se mira en el resultado, en este orden y sin apurarse:

Las agregaciones del oficio, y HAVING · 22 min
  1. Cuántas barras hay a cada lado. Veintitrés contra cinco. Es el mismo archivo, la misma plata y la misma consulta: lo único que cambia son tres funciones de texto.
  2. Las barras de color. A la izquierda son cinco —Minería, MINERIA, MINERÍA, minería y una que además trae un espacio al final—; a la derecha son una sola, y mide exactamente lo que miden las cinco juntas.
  3. Las comillas de las etiquetas. Están puestas a propósito: son la única manera de ver el espacio en blanco de «Construcción ». Un carácter invisible partió un segmento en dos.
  4. El eje horizontal es el mismo en los dos lados. Si no lo fuera, la comparación no valdría nada. Ésa es la primera regla de lectura de un gráfico y hoy se dice por primera vez: antes de leer las barras, se lee el eje.
Las agregaciones del oficio, y HAVING · 22 min

Ticket promedio por canal — y la primera lección de grano del semestre:

Las agregaciones del oficio, y HAVING · 22 min
SELECT COUNT(*)                       AS lineas,
       COUNT(DISTINCT numero_factura) AS facturas,
       ROUND(COUNT(*)::DOUBLE
             / COUNT(DISTINCT numero_factura), 3)
         AS lineas_por_factura
FROM ventas;
-- 6000 | 2951 | 2.033
Las agregaciones del oficio, y HAVING · 22 min

Detenerse aquí. «6.000 líneas, 2.951 facturas. 2,03 líneas por factura. Anoten ese número: explica un crimen que vamos a resolver en la clase 09 y que cuesta 9,7 puntos de margen.»

Las agregaciones del oficio, y HAVING · 22 min
WITH factura AS (
  SELECT numero_factura,
         ANY_VALUE(canal) AS canal,
         SUM(cantidad * precio_unitario) AS bruto
  FROM ventas
  WHERE moneda = 'CLP'
  GROUP BY numero_factura
)
SELECT canal,
       COUNT(*)          AS facturas,
       ROUND(AVG(bruto)) AS ticket_promedio_clp
FROM factura
GROUP BY canal
ORDER BY ticket_promedio_clp DESC;
Las agregaciones del oficio, y HAVING · 22 min
canalfacturasticket_promedio_clp
Distribuidor6093.562.330
Directo1.3693.327.875
E-commerce863.137.598
Licitación6943.074.717
Las agregaciones del oficio, y HAVING · 22 min
«Fíjense en lo que hicimos: primero agrupamos por factura, y después promediamos. Si promedian directo sobre las líneas, están promediando pedazos de factura y el "ticket" no es el ticket. Se llama grano, es la palabra más importante del semestre y vuelve en la clase 09.»

HAVING: filtrar después de agregar. WHERE filtra filas antes de repartirlas en cajones; HAVING filtra cajones ya armados.

Las agregaciones del oficio, y HAVING · 22 min
SELECT upper(strip_accents(trim(region_cliente))) AS region,
       COUNT(*) AS lineas,
       ROUND(SUM(cantidad * precio_unitario)) AS venta_clp
FROM ventas
WHERE moneda = 'CLP'
GROUP BY 1
HAVING COUNT(*) > 500
ORDER BY venta_clp DESC;
-- METROPOLITANA 1446 | 2.290.179.576
-- VALPARAISO     742 | 1.306.465.271
-- ANTOFAGASTA    658 |   985.557.458
Las agregaciones del oficio, y HAVING · 22 min

Error clásico, provocado a propósito: escribir WHERE COUNT(*) > 500. Leer el mensaje de DuckDB en pantalla y traducirlo al español con ellos. El error es material didáctico, no un accidente.

Y la primera pasada al margen —la mitad de la pregunta del gerente:

Las agregaciones del oficio, y HAVING · 22 min
SELECT linea_negocio,
       ROUND(100.0 * (SUM(cantidad * precio_unitario)
                    - SUM(cantidad * costo_unitario)
                    - SUM(descuento_factura)
                    - SUM(flete_despacho))
             / SUM(cantidad * precio_unitario), 2)
         AS margen_aparente_pct
FROM ventas
GROUP BY linea_negocio
ORDER BY margen_aparente_pct DESC;
Las agregaciones del oficio, y HAVING · 22 min
linea_negociomargen_aparente_pct
Herramientas21,76
Seguridad20,79
Eléctrico8,66
Fijaciones−13,22
«Según esto, cada peso que vendemos en Fijaciones nos hace perder trece centavos. ¿Qué haría un gerente con esta lámina?»
Las agregaciones del oficio, y HAVING · 22 min
«Esta consulta corrió sin error. Sumó cuatro columnas que existen. Y es la lámina con la que se cierran negocios rentables. Lo llamo el margen aparente y lo vamos a desarmar en la clase 09. Hoy sólo quiero que sepan que existe.»

Hilo 3 — salir de Excel: «Esto en Excel era una tabla dinámica. Y la tabla dinámica te habría dado las mismas 23 filas, con la misma cara de seguridad. El problema no es la lentitud de Excel: es que ninguna de las dos herramientas sabe que INDUSTRIAL e Industrial son lo mismo.»

El primer insight: 100 formas, 18 clientes · 14 min

Aquí se responde la pregunta colgada, corriéndola con ellos. Antes de correr, cobrar el pronóstico que anotaron a las 20:30.

El primer insight: 100 formas, 18 clientes · 14 min
SELECT COUNT(*)                       AS lineas,
       COUNT(DISTINCT cliente)        AS formas_cliente,
       COUNT(DISTINCT rut_cliente)    AS formas_rut,
       COUNT(DISTINCT segmento)       AS formas_segmento,
       COUNT(DISTINCT region_cliente) AS formas_region
FROM ventas;
-- 6000 | 100 | 74 | 23 | 62
El primer insight: 100 formas, 18 clientes · 14 min
«Cien. Y hay dieciocho clientes. El RUT, que es el número que el Estado usa para identificar a una empresa, viene escrito de 74 formas para 18 RUT. Eso es lo que hay adentro del archivo con que se arma el reporte de ventas.»

Ahora el mismo problema donde duele, cliente por cliente:

El primer insight: 100 formas, 18 clientes · 14 min
SELECT cliente, COUNT(*) AS lineas
FROM ventas
WHERE upper(strip_accents(cliente)) LIKE '%LOS ANDES%'
GROUP BY cliente
ORDER BY lineas DESC;
El primer insight: 100 formas, 18 clientes · 14 min
clientelineas
Ferretería Los Andes Ltda.235
FERRETERÍA LOS ANDES LTDA.69
Ferretería Los Andes Ltda. (espacio delante)59
Ferretería Los Andes LTDA58
Ferretería Los Andes Ltda.. (punto de más)35
El primer insight: 100 formas, 18 clientes · 14 min

Un cliente. Cinco filas en el reporte. Ninguna con el total.

Y el intento de arreglarlo sólo con texto, corrido en vivo:

El primer insight: 100 formas, 18 clientes · 14 min
SELECT COUNT(DISTINCT cliente) AS crudo,
       COUNT(DISTINCT upper(trim(cliente)))
         AS mas_caja_y_bordes,
       COUNT(DISTINCT upper(regexp_replace(
         trim(cliente), ' +', ' ', 'g')))
         AS mas_espacios_internos,
       COUNT(DISTINCT upper(replace(replace(replace(
         rut_cliente,'.',''),'-',''),' ','')))
         AS por_rut_normalizado
FROM ventas;
-- 100 | 64 | 46 | 18
El primer insight: 100 formas, 18 clientes · 14 min

La cascada, escrita en la pizarra: 100 → 64 → 46 → … y ahí se atasca.

«Mayúsculas y espacios te llevan de 100 a 46. De 46 a 18 no llegas limpiando texto: llegas adivinando si SA es lo mismo que S.A., si el punto de más es un error de dedo, si "Ferretería" con y sin tilde es la misma empresa. Cada adivinanza es una apuesta. Y hay una columna en el archivo que no requiere adivinar ninguna: el RUT. Normalizado, cierra en 18 de una sola pasada.»
El primer insight: 100 formas, 18 clientes · 14 min
SELECT COUNT(DISTINCT rut_cliente) AS formas_crudas,
       COUNT(DISTINCT upper(replace(replace(replace(
         rut_cliente,'.',''),'-',''),' ','')))
         AS ruts_reales
FROM ventas;
-- 74 | 18

Regla que se copia: para agrupar personas o empresas, agrupa por la llave, muestra el nombre. El nombre es para leer; el RUT es para contar.

Los cinco clientes más grandes, ya bien contados:

El primer insight: 100 formas, 18 clientes · 14 min
SELECT upper(replace(replace(replace(
         rut_cliente,'.',''),'-',''),' ',''))
         AS rut_norm,
       ANY_VALUE(cliente)      AS un_nombre_cualquiera,
       COUNT(DISTINCT cliente) AS formas_de_escritura,
       ROUND(SUM(cantidad * precio_unitario) / 1e6, 1)
         AS venta_MM_clp
FROM ventas
WHERE moneda = 'CLP'
GROUP BY 1
ORDER BY venta_MM_clp DESC
LIMIT 5;
El primer insight: 100 formas, 18 clientes · 14 min
rut_normun_nombre_cualquieraformasventa_MM_clp
764128859Constructora Vilches S.A.61.340,6
772039141Ferretería Los Andes LTDA61.012,4
96874230KMinera Altos del Loa SpA5985,6
781156602COMERCIAL ÑUBLE SPA5710,9
763304828CONSTRUCTORA AYSÉN LTDA.6683,9
El primer insight: 100 formas, 18 clientes · 14 min
«Ninguno de estos cinco clientes aparecía completo en el reporte anterior. El más grande de la cartera estaba partido en seis pedazos.»
Respiro · 5 min

Pararse, estirar, subir la luz un segundo. Clase nocturna: el cuerpo también aprende.

La bisagra · 4 min

Éste es el bloque que convierte dos sesiones en una clase. Se dicta de pie, sin proyectar SQL.

«Entonces el archivo no se puede arreglar solo. Mayúsculas y espacios te dejan en 46 y ahí se atasca: para llegar a 18 tienes que salir del archivo. ¿Y si le pegamos el maestro de clientes?»

Recoger tres respuestas de la sala en 90 segundos, sin corregir ninguna. Después:

La bisagra · 4 min
«Es exactamente lo que haría cualquiera, y es lo que vamos a hacer en los próximos cuarenta minutos. Les adelanto una sola cosa, y quiero que la anoten antes de que la veamos: entran 6.000 líneas y salen 6.195. El resto de la clase es averiguar de dónde salieron las 195 que nadie vendió.»
El maestro de clientes y el primer JOIN · 16 min

Proyectar el JSON en un editor de texto, no en el navegador. Que vean la forma.

{ "rut": "76.412.885-9", "razon_social": "Constructora Vilches S.A.",
  "segmento": "Construcción", "region": "Metropolitana",
  "limite_credito_clp": 236000000 }
El maestro de clientes y el primer JOIN · 16 min
El maestro de clientes y el primer JOIN · 16 min

Leerlo con SQL, sin abrir nada más:

El maestro de clientes y el primer JOIN · 16 min
CREATE OR REPLACE VIEW maestro AS
SELECT * FROM (
  SELECT unnest(clientes, recursive := true)
  FROM read_json_auto(
    'datos/finanzas/catalogo_clientes.json'));

SELECT COUNT(*) AS entradas,
       COUNT(DISTINCT rut) AS ruts FROM maestro;
-- 18 | 17
El maestro de clientes y el primer JOIN · 16 min

Detenerse. «Dieciocho entradas. Diecisiete RUT. Antes de unir nada: ahí ya hay un problema, y está del lado del maestro, no del nuestro.»

El JOIN en una frase: pegar a cada línea de venta la información del cliente que la generó, usando el RUT como puente.

Primero, la versión ingenua —la que casi todo el mundo escribe:

El maestro de clientes y el primer JOIN · 16 min
SELECT COUNT(*) AS lineas_pegadas
FROM ventas v
JOIN maestro m ON v.rut_cliente = m.rut;
-- 3641
«3.641 de 6.000. El 60,7 %. Y la consulta corrió sin error, sin advertencia y en medio segundo. Si mandas ese reporte, le acabas de decir a Finanzas que la empresa vendió el 39 % menos de lo que vendió.»
El maestro de clientes y el primer JOIN · 16 min

Preguntar: «¿por qué no pegó?» Van a llegar solos: el RUT viene escrito de 74 formas y el maestro lo escribe de una.

La cura, con la misma normalización de recién aplicada a los dos lados:

El maestro de clientes y el primer JOIN · 16 min
CREATE OR REPLACE VIEW ventas_n AS
SELECT *, upper(replace(replace(replace(
         rut_cliente,'.',''),'-',''),' ','')) AS rut_norm
FROM ventas;

CREATE OR REPLACE VIEW maestro_n AS
SELECT *, upper(replace(replace(
         rut,'.',''),'-','')) AS rut_norm
FROM maestro;

SELECT COUNT(*)
FROM ventas_n JOIN maestro_n USING (rut_norm);
-- 6195
El maestro de clientes y el primer JOIN · 16 min

Escribir el número en la pizarra, grande:

Entraron 6.000 líneas. Salieron 6.195.

«El JOIN inventó 195 líneas de venta. No están en el archivo de ventas, no las facturó nadie, y aparecerían en el reporte con su venta, su costo y su margen. Un JOIN que cambia el conteo de filas sin que nadie lo note es la forma más común de mentir con datos sin querer.»
El maestro de clientes y el primer JOIN · 16 min

La consulta de control que hay que correr SIEMPRE después de un JOIN —se copia, se pega en el cuaderno, se usa el resto del semestre:

El maestro de clientes y el primer JOIN · 16 min
SELECT
  (SELECT COUNT(*) FROM ventas_n) AS lineas_origen,
  (SELECT COUNT(*) FROM ventas v JOIN maestro m
     ON v.rut_cliente = m.rut)    AS join_sin_normalizar,
  (SELECT COUNT(*) FROM ventas_n JOIN maestro_n
     USING (rut_norm))            AS join_normalizado,
  (SELECT COUNT(*) FROM ventas_n LEFT JOIN maestro_n
     USING (rut_norm)
     WHERE maestro_n.rut IS NULL) AS lineas_sin_maestro,
  (SELECT COUNT(*) FROM ventas_n JOIN maestro_n
     USING (rut_norm)
     WHERE maestro_n.region IS NULL) AS lineas_sin_region;
-- 6000 | 3641 | 6195 | 99 | 774
El maestro de clientes y el primer JOIN · 16 min

LEFT vs INNER, en lámina y en una sola idea:

Qué hace con lo que no calzaCuándo se usa
INNER JOINlo borra en silenciocuando ya verificaste que no falta nada
LEFT JOINlo conserva con NULL — lo VESsiempre, para diagnosticar
El maestro de clientes y el primer JOIN · 16 min
«Regla del curso: para diagnosticar, LEFT. Siempre. El INNER es una decisión que se toma después de mirar, no antes.»

Hilo 3 — salir de Excel: «Esto en Excel era un BUSCARV. Y hay una diferencia que hoy importa: BUSCARV devuelve la primera coincidencia y sigue. Con el cliente duplicado del maestro habrían obtenido 6.000 filas —el conteo cuadra, se ve perfecto— y a la mitad de las líneas de Punta Verde les habría pegado el segmento equivocado, sin duplicar nada y sin avisar nunca. El JOIN al menos te muestra el problema cambiando el conteo. Un error visible es infinitamente mejor que uno invisible.»

Las cuatro trampas del maestro, medidas una por una · 20 min

Cada trampa se corre en vivo, se mide y se le pone nombre. Es el corazón de la clase y no se salta: es el corazón de RA2.

Trampa 1 — El RUT sin normalizar: 3.641 de 6.000

Ya está medida arriba. Nombre: fallo de llave. Se ve porque el conteo baja.

Las cuatro trampas del maestro, medidas una por una · 20 min

Trampa 2 — El cliente que está dos veces con segmento contradictorio

SELECT rut, COUNT(*) AS entradas,
       string_agg(segmento, ' | ') AS segmentos
FROM maestro
GROUP BY rut
HAVING COUNT(*) > 1;
-- 96.701.443-5 | 2 | Minería | Industrial
Las cuatro trampas del maestro, medidas una por una · 20 min
SELECT
  (SELECT COUNT(*) FROM ventas_n
     WHERE rut_norm = '967014435')
     AS lineas_reales,
  (SELECT COUNT(*) FROM ventas_n
     JOIN maestro_n USING (rut_norm)
     WHERE rut_norm = '967014435')
     AS lineas_tras_join;
-- 294 | 588
Las cuatro trampas del maestro, medidas una por una · 20 min
«Minera Punta Verde tiene 294 líneas de venta. Después del JOIN tiene 588. El cliente factura el doble sin haber vendido una unidad más, porque alguien la cargó dos veces en el maestro: una como Minería y otra como Industrial. Ésas son las 195 líneas inventadas (294 duplicadas menos 99 que se pierden, y las 99 vienen ahora).»

Nombre: fan-out. Se ve porque el conteo sube.

Las cuatro trampas del maestro, medidas una por una · 20 min

Trampa 3 — El cliente que no está en el maestro

Ésta no se proyecta primero: la corren ellos. Son 3 minutos y salen del tiempo de la Trampa 4, que es la más corta.

«Antes de que yo lo proyecte: corran ustedes el LEFT JOIN contra el maestro con WHERE maestro_n.rut IS NULL y díganme cuántas líneas les da. La vista ventas_n y la vista maestro_n ya están creadas: es una consulta de cuatro líneas.»
Las cuatro trampas del maestro, medidas una por una · 20 min
SELECT ANY_VALUE(cliente) AS cliente,
       COUNT(*) AS lineas,
       COUNT(DISTINCT numero_factura) AS facturas,
       ROUND(SUM(cantidad * precio_unitario))
         AS venta_nominal_clp
FROM ventas_n LEFT JOIN maestro_n USING (rut_norm)
WHERE maestro_n.rut IS NULL
GROUP BY rut_norm;
-- Salmonera Chiloé SpA | 99 | 49 | 85.343.323
Las cuatro trampas del maestro, medidas una por una · 20 min
«Salmonera Chiloé SpA. 99 líneas, 49 facturas, 85,3 millones. No está en el maestro de clientes. El INNER JOIN la borra y nadie se entera. El LEFT JOIN la deja con segmento nulo y aparece en el reporte como "(en blanco)". Ninguna de las dos cosas es aceptable.»
Las cuatro trampas del maestro, medidas una por una · 20 min

Trampa 4 — Los campos vacíos del maestro

SELECT maestro_n.razon_social, COUNT(*) AS lineas
FROM ventas_n JOIN maestro_n USING (rut_norm)
WHERE maestro_n.region IS NULL
GROUP BY 1 ORDER BY 2 DESC;
-- Comercial Ñuble SpA      | 415
-- Constructora Aysén Ltda. | 359
Las cuatro trampas del maestro, medidas una por una · 20 min
«774 líneas. Cuando Finanzas abra la cartera por región, esas 774 líneas se caen del gráfico sin avisar. El total del gráfico no va a cuadrar con el total del estado de resultados, y nadie va a saber por qué.»
Las cuatro trampas del maestro, medidas una por una · 20 min
Hilo 2: «Este maestro es exactamente lo que exporta un ERP chileno. La entrada duplicada de Punta Verde no es un invento mío: es lo que pasa cuando un cliente cambia de clasificación y alguien crea una ficha nueva en vez de editar la vieja. Van a ver esto en su primera semana de trabajo.»
El segundo catálogo, el de Finanzas: condiciones de pago · 12 min

Este catálogo es más chico y es más grave: define cuánto crédito puede dar la empresa.

El segundo catálogo, el de Finanzas: condiciones de pago · 12 min
CREATE OR REPLACE VIEW condiciones AS
SELECT * FROM (
  SELECT unnest(condiciones, recursive := true)
  FROM read_json_auto(
    'datos/finanzas/catalogo_condiciones_pago.json'));

SELECT * FROM condiciones ORDER BY dias;
El segundo catálogo, el de Finanzas: condiciones de pago · 12 min
diasnombrevigentecosto_financiero_anual_pct
0Contadotrue0,0
30Crédito 30true9,6
45Crédito 45false10,2
60Crédito 60true10,8
90Crédito 90true12,4
El segundo catálogo, el de Finanzas: condiciones de pago · 12 min

Ahora, contra lo que el ERP realmente emitió:

El segundo catálogo, el de Finanzas: condiciones de pago · 12 min
SELECT v.condicion_pago_dias        AS dias,
       COUNT(*)                     AS lineas,
       ANY_VALUE(c.nombre)          AS nombre_en_catalogo,
       ANY_VALUE(c.vigente)         AS vigente
FROM ventas v
LEFT JOIN condiciones c ON v.condicion_pago_dias = c.dias
GROUP BY v.condicion_pago_dias
ORDER BY dias;
El segundo catálogo, el de Finanzas: condiciones de pago · 12 min
diaslineasnombre_en_catalogovigente
0598Contadotrue
301.961Crédito 30true
45233Crédito 45false
601.828Crédito 60true
901.245Crédito 90true
120135(null)(null)
El segundo catálogo, el de Finanzas: condiciones de pago · 12 min

368 líneas fuera del catálogo vigente. Y son dos fallas distintas —esto es lo que hay que dejar clavado:

El segundo catálogo, el de Finanzas: condiciones de pago · 12 min
El segundo catálogo, el de Finanzas: condiciones de pago · 12 min
«La primera es peor, porque no se cae. Un INNER JOIN te avisa cuando algo falta. Nada te avisa cuando algo calza pero ya no vale.»

Y la pregunta de negocio, que es de Finanzas y no de sintaxis:

El segundo catálogo, el de Finanzas: condiciones de pago · 12 min
SELECT condicion_pago_dias AS dias,
       COUNT(*) AS lineas,
       ROUND(SUM(cantidad * precio_unitario)) AS venta_clp
FROM ventas
WHERE condicion_pago_dias IN (45, 120) AND moneda = 'CLP'
GROUP BY 1 ORDER BY 1;
-- 45  | 218 | 277.352.802
-- 120 | 128 | 260.974.516
El segundo catálogo, el de Finanzas: condiciones de pago · 12 min
«538 millones de pesos de crédito que Finanzas nunca autorizó, dados a 45 y 120 días. Y el catálogo trae el costo_financiero_anual_pct justamente para poder preguntar: ¿cuánto nos cuesta el crédito que nadie aprobó? → eso se le pregunta a Tesorería y al Gerente de Administración y Finanzas.»
Control quincenal C1 · 10 min
Experto de negocio interno, síntesis y ticket · 9 min

Los dos roles en la misma lámina. Las dudas concretas que salen del dato de hoy:

Experto de negocio interno, síntesis y ticket · 9 min
KAM: «77.203.914-1 factura como Ferretería Los Andes Ltda. y también como Ferretería Los Andes LTDA. Mismo RUT, dos escrituras. ¿Es el mismo cliente? Y el RUT 96.701.443-5 está cargado dos veces: ¿es Minería o es Industrial?»

Administración de Ventas: «¿Por qué Salmonera Chiloé no está en el maestro, si nos compra hace dos años y le facturamos 85 millones?»
Experto de negocio interno, síntesis y ticket · 9 min
Duda que salió del dato de hoyEl rol que tiene la respuesta
¿Este cliente y este otro son el mismo, o son dos razones sociales?Ejecutivo comercial (KAM) de la cuenta
¿Punta Verde es Minería o Industrial?Ejecutivo comercial (KAM) de la cuenta
¿Por qué Salmonera Chiloé no está en el maestro?Administración de Ventas, dueña del maestro (y Cuentas por Cobrar, que le cobra todos los meses: sabe que existe)
¿Comercial Ñuble no tiene región o nadie la llenó?Administración de Ventas
Experto de negocio interno, síntesis y ticket · 9 min
«Cuatro preguntas. Ninguna se responde con SQL. Y las cuatro tienen que estar contestadas antes de que ese reporte salga de tu computador. Ninguna toma más de un correo.»

Y la regla del curso, que ya está en la pizarra desde la clase 02, se repite en voz alta:

Una consulta SQL sin una conversación previa es una opinión con formato de tabla.

Síntesis, construida con ellos, escrita en la pizarra con sus palabras:

Experto de negocio interno, síntesis y ticket · 9 min
  1. «¿Qué hace GROUP BY en una frase que entienda tu jefa?» → parte la tabla en cajones y resume cada cajón.
  2. «¿Con qué columna se cuentan clientes y por qué no con el nombre?» → con el RUT normalizado: 74 formas cierran en 18; el nombre se atasca en 46.
  3. «¿Qué se revisa siempre después de un JOIN?» → el conteo de filas: antes y después.
  4. «¿Cuáles son las dos fallas distintas del catálogo de condiciones?» → una existe pero no está vigente (45 días), la otra no existe (120 días).
Experto de negocio interno, síntesis y ticket · 9 min

Ticket TC03 (3 min, se proyecta): a) ¿Por qué pegar el maestro NO arregla las 100 formas de escribir 18 clientes? Una frase. b) Uniste por rut_cliente = rut sin normalizar y pegaron 3.641 de 6.000 líneas; el GROUP BY por cliente sigue dando 100 grupos. Escribe el arreglo que va en la fuente y el que va en el maestro, en ese orden. c) 1–5 y qué te falta.

Anki: se importa hoy el mazo S1–S2 (mostrar el import en 1 minuto, no más).

Pregunta que queda planteada (se escribe en la pizarra y no se responde hoy):

Experto de negocio interno, síntesis y ticket · 9 min
→ El RUT sin normalizar pega 3.641 de 6.000 líneas. ¿De dónde saco lo que falta, y con qué fuente lo valido?

Puente a la clase 04, corto:

«La próxima salimos del edificio: CMF, Banco Central, SII, INE, Bolsa de Santiago. Y van a aprender a validar un RUT antes de ir a buscarlo afuera, con una regla aritmética que se explica en dos frases.»
Clase 03 · 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-03.ipynb

← → avanzar · P proyector · F pantalla completa
GAA1104 · C03 · Agregar y unir (+ Control C1) 20:30 1 / 1