GAA1104 · Big Data e Inteligencia de Negocios · Clase 04

De dónde sale el dato y dónde se rompe

Sabes qué publican la CMF, el Banco Central, el SII, el INE y la Bolsa de Santiago —en qué formato y con qué frecuencia—, validas un RUT con dígito verificador antes de salir a buscarlo afuera, lees CSV, JSON y XLSX con SQL sin abrir Excel, y operas fechas, monedas y cartera sin corromper el análisis.

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

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

Clase 04 · el gráfico de hoy

Línea de tiempo (tipo de cambio) y barras (DSO por segmento)

Dos preguntas distintas, dos gráficos. La primera es cambio en el tiempo; la segunda, magnitud comparada. Juntarlas en dos ejes sería el error que la clase 18 desarma.

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

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

Tres preguntas, en silencio, 90 segundos (espaciado 1–3–7–21):

Apertura — Active recall · 8 min
  1. (clase 03) El JOIN devolvió 6.195 filas y el origen tenía 6.000. ¿Qué le pasó al cliente Minera Punta Verde y por qué?
  2. (clase 02) En el top 10 de las líneas más grandes, Ferretería Los Andes apareció escrita de tres formas distintas. ¿Por qué ninguna consulta lo avisó, y qué tuvo que pasar para que lo viéramos?
  3. (clase 03) ¿Por qué se agrupa por RUT normalizado y no por el nombre del cliente? Da el par de números.
Apertura — Active recall · 8 min

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

Devolución de los C1 (2 min de los 8): cada uno lee su corrección en silencio. Nombrar en voz alta los dos ítems que más se fallaron y no más — la retro individual va escrita, no dictada.

La pregunta colgada se abre, no se cierra todavía: «"¿De dónde saco lo que falta?" tiene dos respuestas y hoy vemos las dos. La primera: de adentro, preguntándole a quien mantiene el maestro. La segunda: de afuera. Y afuera, en Chile, hay más de lo que creen.»

Dónde vive el dato: las cinco fuentes chilenas, y valida el RUT antes de salir · 22 min

Bloque comprimido: el mapa de fuentes, el recorrido de las cinco instituciones y el módulo 11, en 22 minutos. El recorrido va a 3 minutos por fuente, cronometrado. Lo que no cabe en pantalla va a la ficha del sitio.

Dónde vive el dato: las cinco fuentes chilenas, y valida el RUT antes de salir · 22 min

El mapa de fuentes (lámina única, se copia — 3 min)

TipoQuién la generaEjemplos en este casoCómo falla
Internatu propia empresalibro de ventas, maestro de clientes, plan de cuentas, contabilidad y los registros de eventos del ERPmal tipeada, sin gobierno, sin diccionario — lo que vimos las tres semanas pasadas
Externa de negocioun tercero con el que operascartola del banco, liquidación del factoring, estado de cuenta del proveedorllega en el formato que a ellos les acomoda, y cambia sin avisarte
Abiertauna institución pública o un mercadoCMF, Banco Central, SII, INE, Bolsa de Santiagoestá bien hecha y no es tuya: tiene su definición, su desfase y su metodología, y tú te adaptas
Dónde vive el dato: las cinco fuentes chilenas, y valida el RUT antes de salir · 22 min

El tipo interno que casi nadie nombra, y va en la lámina: los registros de eventos —el log del ERP: quién creó la factura, quién la modificó, a qué hora—. Es la única fuente que contesta «¿quién cambió este precio, y cuándo?». Existe siempre y nadie la pide hasta el día que la necesita.

«La diferencia práctica: en la interna puedes llamar por teléfono y corregir el origen. En la abierta no. En la abierta, tu única defensa es leer la metodología antes de usar el número.»
Dónde vive el dato: las cinco fuentes chilenas, y valida el RUT antes de salir · 22 min

Regla del bloque, dicha antes de abrir el primer navegador: no se dicta ninguna URL profunda. Se llega desde la portada de cada institución. Es lento a propósito: así es como se llega cuando la estructura del sitio cambia, y así es como se detecta que la URL que te dio la IA no existe.

Dónde vive el dato: las cinco fuentes chilenas, y valida el RUT antes de salir · 22 min

Las cinco fuentes (3 min cada una, con reloj — 12 min)

1 · CMF (cmfchile.cl). Estados financieros de emisores inscritos, hechos esenciales, información de bancos y seguros. Formato: ficha por entidad, EEFF descargables (XBRL y planillas). Frecuencia: EEFF de emisores trimestral, información bancaria mensual. Para qué sirve aquí: es la única fuente donde ves el estado de resultados de tu cliente. Si vas a dar crédito a 90 días, ahí está el balance de quien te va a deber.

Dónde vive el dato: las cinco fuentes chilenas, y valida el RUT antes de salir · 22 min

2 · Banco Central (bcentral.cl, Base de Datos Estadísticos). Dólar observado, TPM, UF, IPC, IMACEC, cuentas nacionales. Formato: consulta en línea y descarga a planilla; hay API con credenciales. Frecuencia: el dólar observado es de día hábil; la TPM cambia en las reuniones de política monetaria; el IMACEC es mensual.

Dónde vive el dato: las cinco fuentes chilenas, y valida el RUT antes de salir · 22 min
El detalle que hay que decir en voz alta, porque cuesta plata: el dólar observado que se publica hoy corresponde a las transacciones del día hábil anterior. No es "el dólar de hoy". Cuando alguien dice "usamos el tipo de cambio del día", hay que preguntar de qué día.

Y la palabra que hay que saber decir: vigencia. Un dato puede estar perfectamente bien medido y aun así no servir hoy. El dólar observado del viernes es correcto, y el lunes ya es un dato obsoleto para valorizar una factura. La pregunta de vigencia —«¿de qué día es este número y hasta cuándo sirve?»— se le hace a toda fuente, interna o abierta, y es exactamente lo que el Parcial 1 pide cuando exige que el dato sea preciso y oportuno.
Dónde vive el dato: las cinco fuentes chilenas, y valida el RUT antes de salir · 22 min

3 · SII (sii.cl). Consulta de situación tributaria de un tercero por RUT, estadísticas de empresas por rubro, tamaño y comuna, valores y fechas de referencia (UF, UTM). Formato: consulta en pantalla; estadísticas en planilla. Frecuencia: consulta en línea, estadísticas anuales. Para qué sirve aquí: es donde se verifica que el RUT al que le vas a dar crédito existe y está activo.

4 · Bolsa de Santiago (bolsadesantiago.com). Precios y volúmenes de acciones, índices (IPSA, IGPA), renta fija. Frecuencia diaria. Para qué sirve aquí: si el cliente o el competidor cotiza, el mercado le pone precio todos los días. Es la segunda opinión sobre lo que dice el balance.

Dónde vive el dato: las cinco fuentes chilenas, y valida el RUT antes de salir · 22 min

5 · INE (ine.gob.cl). IPC, empleo, remuneraciones, índices de producción. Planillas y microdatos, mensual para IPC y empleo. Para qué sirve aquí: sin IPC, "crecimos 8 %" no significa nada.

«Cinco instituciones. Ninguna les va a mandar el archivo por correo: hay que ir a buscarlo. Y las cinco tienen algo que su ERP no tiene: una metodología escrita. Cuando el número no calza, en la interna llamas por teléfono; en la abierta, lees el documento.»
Dónde vive el dato: las cinco fuentes chilenas, y valida el RUT antes de salir · 22 min

Valida el RUT antes de salir a buscarlo: módulo 11 (7 min)

El razonamiento primero. «Van a ir al SII a preguntar por 18 RUT. Cada consulta toma tiempo. ¿Y si algunos ni siquiera son RUT válidos? El dígito verificador es un control que se calcula: si no calza, el RUT está mal escrito y no hace falta salir a buscarlo.»

La regla, dicha en 40 segundos: se toman los dígitos del cuerpo de derecha a izquierda, se multiplican por 2, 3, 4, 5, 6, 7 y se vuelve a empezar en 2; se suman; se calcula 11 − (suma mod 11); si da 11 el dígito es 0, si da 10 es K, si no, es el número.

Dónde vive el dato: las cinco fuentes chilenas, y valida el RUT antes de salir · 22 min
CREATE OR REPLACE TABLE ventas AS
SELECT * FROM read_csv_auto(
  'datos/finanzas/ventas_extracto.csv');
Dónde vive el dato: las cinco fuentes chilenas, y valida el RUT antes de salir · 22 min
WITH ruts AS (
  SELECT DISTINCT upper(replace(replace(replace(rut_cliente,'.',''),'-',''),' ','')) AS r
  FROM ventas
),
partes AS (
  SELECT r, substr(r, 1, length(r) - 1) AS cuerpo, substr(r, length(r), 1) AS dv FROM ruts
),
suma AS (
  SELECT p.cuerpo, p.dv,
         SUM(CAST(substr(p.cuerpo, length(p.cuerpo) - i + 1, 1) AS INTEGER)
             * (CASE ((i - 1) % 6) WHEN 0 THEN 2 WHEN 1 THEN 3 WHEN 2 THEN 4
                                   WHEN 3 THEN 5 WHEN 4 THEN 6 ELSE 7 END)) AS s
  FROM partes p, generate_series(1, 8) g(i)
  WHERE i <= length(p.cuerpo)
  GROUP BY p.cuerpo, p.dv
)
SELECT COUNT(*) AS ruts_distintos,
       COUNT(*) FILTER (
         WHERE dv = CASE 11 - (s % 11) WHEN 11 THEN '0' WHEN 10 THEN 'K'
                                       ELSE CAST(11 - (s % 11) AS VARCHAR) END
       ) AS pasan_modulo_11
FROM suma;
-- 18 | 3
Dónde vive el dato: las cinco fuentes chilenas, y valida el RUT antes de salir · 22 min

El momento de honestidad del bloque.

Dónde vive el dato: las cinco fuentes chilenas, y valida el RUT antes de salir · 22 min
«Tres de dieciocho. Y es culpa mía. Este dataset es sintético, se los dije la primera clase: yo inventé los RUT y no me molesté en calcularles el dígito verificador. Acaban de encontrar, con una consulta de veinte líneas, el defecto de fábrica del archivo con el que llevan tres semanas trabajando.

Eso es exactamente lo que quiero que sepan hacer: el archivo que te llega no es sagrado, y hay controles que lo prueban en dos minutos. En un archivo real, un RUT que no pasa módulo 11 es un error de digitación o un dato inventado, y en los dos casos hay que devolverlo antes de darle crédito a esa empresa.»
Dónde vive el dato: las cinco fuentes chilenas, y valida el RUT antes de salir · 22 min

Se anota como hallazgo del informe de calidad (que se redacta en la clase 06): 18 RUT distintos, 3 pasan el control de dígito verificador; el resto no se puede validar contra el SII sin corregirse primero.

CSV, JSON y XLSX: los tres se leen con SQL · 14 min

Hilo 3 — salir de Excel: «Las fuentes de arriba entregan planillas. Y ustedes no van a abrir ninguna en Excel: las van a leer con la misma frase que usan hace tres semanas.»

-- CSV: el formato de las transacciones
SELECT * FROM read_csv_auto('datos/tc_diario.csv') LIMIT 3;
CSV, JSON y XLSX: los tres se leen con SQL · 14 min
-- JSON: el formato de los catálogos y de casi toda API
SELECT razon_social, segmento, limite_credito_clp
FROM (SELECT unnest(clientes, recursive := true)
      FROM read_json_auto('datos/finanzas/catalogo_clientes.json'))
ORDER BY limite_credito_clp DESC
LIMIT 5;
-- Puerto Austral S.A.                | Industrial   | 241.000.000
-- Constructora Vilches S.A.          | Construcción | 236.000.000
-- Ferretería El Roble Ltda.          | Retail       | 234.000.000
-- Comercial Ñuble SpA                | Retail       | 213.000.000
-- Servicios Eléctricos Cauquenes SpA | Industrial   | 199.000.000
CSV, JSON y XLSX: los tres se leen con SQL · 14 min
-- XLSX: el formato en el que te van a mandar todo,
-- te guste o no. Se fabrica uno en vivo para tenerlo
-- que leer, y así nadie depende de traer un archivo.
INSTALL excel; LOAD excel;

COPY (SELECT * FROM read_csv_auto('datos/tc_diario.csv')
        LIMIT 10)
  TO 'salidas/tc_prueba.xlsx'
  WITH (FORMAT xlsx, HEADER true);

SELECT * FROM read_xlsx('salidas/tc_prueba.xlsx') LIMIT 3;
-- 2022-01-01 | 837.67
-- 2022-01-02 | 839.70
-- 2022-01-03 | 844.93
CSV, JSON y XLSX: los tres se leen con SQL · 14 min
«Tres formatos, tres funciones, la misma frase después. Abrir Excel para mirar un archivo es una decisión, no una obligación — y es la decisión que te impide reproducir lo que hiciste.»

Por qué JSON para catálogos y CSV para transacciones (pregunta dirigida, que la contesten ellos): el CSV es plano, masivo y barato; el JSON permite jerarquía y campos opcionales — por eso el catálogo de condiciones pudo traer vigente y costo_financiero_anual_pct sin ensuciar cada fila.

CSV, JSON y XLSX: los tres se leen con SQL · 14 min

Y el punto que cuesta caro: un CSV no trae tipos.

DESCRIBE ventas;
column_namecolumn_type
fecha_emisionVARCHAR
fecha_vencimientoTIMESTAMP
fecha_pagoTIMESTAMP
CSV, JSON y XLSX: los tres se leen con SQL · 14 min
«Tres columnas de fecha. DuckDB tipó dos como fecha y la tercera la dejó como texto. No preguntó, no avisó, no falló. Adivinó, y en una de las tres se rindió. ¿Por qué justo en fecha_emision? Guarden la pregunta veinte minutos: la respuesta cuesta 21 líneas.»

Y la cobertura del tipo de cambio, para dejar la mesa puesta antes del respiro:

CSV, JSON y XLSX: los tres se leen con SQL · 14 min
WITH calendario AS (
  SELECT UNNEST(generate_series(
           DATE '2022-01-01', DATE '2024-12-31',
           INTERVAL 1 DAY))::DATE AS d
),
tc AS (
  SELECT fecha FROM read_csv_auto('datos/tc_diario.csv')
)
SELECT COUNT(*) AS dias_del_periodo,
       COUNT(*) FILTER (WHERE tc.fecha IS NULL)
         AS dias_sin_tipo_de_cambio
FROM calendario LEFT JOIN tc ON calendario.d = tc.fecha;
-- 1096 | 15
CSV, JSON y XLSX: los tres se leen con SQL · 14 min
«El archivo de tipo de cambio que acabamos de bajar del Banco Central tiene 15 días sin dato en tres años. Anoten el 15.»

Y se mira, porque una serie de tiempo en una tabla no es una serie de tiempo. La consulta ya contó los huecos: quince. El gráfico dice dónde están, que es la pregunta que sigue. Y la forma se elige, no se hereda: acá el dato es cambio en el tiempo, así que es una línea. Si fuera magnitud comparada entre categorías serían barras, y no es lo mismo.

CSV, JSON y XLSX: los tres se leen con SQL · 14 min
import matplotlib.pyplot as plt

GRIS, TINTA, ALERTA = "#b8b8b8", "#102a38", "#8c2b20"

serie = bd.sql("SELECT fecha, tc_usd_clp FROM tc ORDER BY fecha")

huecos = bd.sql("""
  WITH calendario AS (
    SELECT UNNEST(generate_series(DATE '2022-01-01', DATE '2024-12-31',
                                  INTERVAL 1 DAY))::DATE AS d)
  SELECT c.d AS fecha,
         (SELECT t.tc_usd_clp FROM tc t
          WHERE t.fecha < c.d ORDER BY t.fecha DESC LIMIT 1) AS ultimo_conocido
  FROM calendario c LEFT JOIN tc ON c.d = tc.fecha
  WHERE tc.fecha IS NULL ORDER BY 1""")

fig, eje = plt.subplots(figsize=(11, 4.6))
eje.plot(serie.fecha, serie.tc_usd_clp, color=GRIS, linewidth=1.4)
eje.scatter(huecos.fecha, huecos.ultimo_conocido, s=42, color=ALERTA, zorder=3)

alto = serie.loc[serie.tc_usd_clp.idxmax()]
eje.annotate(f"{alto.tc_usd_clp:,.0f}".replace(",", ".") + f"\n{alto.fecha:%d-%m-%Y}",
             xy=(alto.fecha, alto.tc_usd_clp), xytext=(-86, -6),
             textcoords="offset points", fontsize=9, color=TINTA)

eje.set_ylim(780, 1032)
eje.set_title("El dólar se movió entre 794 y 1.010 en tres años: una factura en USD\n"
              "sin tipo de cambio no es un detalle. Y en color, los 15 días que faltan",
              fontsize=13, color=TINTA, loc="left")
eje.set_ylabel("CLP por USD", fontsize=9)
for lado in ("top", "right"):
    eje.spines[lado].set_visible(False)
fig.tight_layout()
plt.show()
CSV, JSON y XLSX: los tres se leen con SQL · 14 min

Qué se mira en el resultado:

CSV, JSON y XLSX: los tres se leen con SQL · 14 min
  1. La línea, primero. Entre el mínimo (794, en febrero de 2022) y el máximo (1.010, el 29 de septiembre de 2022) hay 216 pesos. Una factura en dólares valorizada con el tipo de cambio equivocado no se equivoca «un poquito».
  2. Los puntos de color. Son los 15 días sin dato, puestos a la altura del último valor conocido — que es justamente lo que mañana se llama arrastre. No están agrupados en una semana ni en un feriado largo: caen sueltos, en cualquier parte, y por eso no se arreglan a ojo.
  3. Un solo eje, y una sola serie. La tentación de meter acá el número de facturas en USD se resiste: son otra unidad. Eso sería dos escalas en un gráfico, que es el error que la clase 18 desarma entera.
Respiro · 5 min

Pararse, estirar, subir la luz un segundo.

La bisagra · 4 min

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

«Bajaste el tipo de cambio del Banco Central. Sabes de qué día es, sabes que le faltan 15 días en tres años y sabes leerlo con SQL. Ahora mira lo que pasa cuando lo cruzas con tus facturas.»

Y la respuesta a la pregunta colgada, cerrada acá, en dos mitades:

La bisagra · 4 min
La bisagra · 4 min
«Y ahora la parte incómoda. En los próximos cincuenta minutos ninguna consulta va a dar error, y tres números que ya usamos van a resultar estar mal. La fuente externa no llega para arreglarte el archivo: llega para mostrarte dónde estaba roto.»
Fechas: las 21 líneas que un CAST se come en silencio · 20 min

Proyectar y mirar. Nadie ve nada:

SELECT fecha_emision, numero_factura, cliente
FROM ventas LIMIT 15;

Ahora buscarlas a propósito:

Fechas: las 21 líneas que un CAST se come en silencio · 20 min
SELECT COUNT(*) AS lineas_totales,
       COUNT(*) FILTER (WHERE fecha_emision LIKE '%/%')
         AS escritas_con_barra,
       ROUND(100.0 * COUNT(*) FILTER (
               WHERE fecha_emision LIKE '%/%')
             / COUNT(*), 2) AS pct
FROM ventas;
-- 6000 | 21 | 0.35
Fechas: las 21 líneas que un CAST se come en silencio · 20 min
SELECT fecha_emision, numero_factura, cliente
FROM ventas WHERE fecha_emision LIKE '%/%' LIMIT 6;
-- 16/05/2023 | F-2023-000059 | MINERA ALTOS DEL LOA SPA
-- 17/10/2024 | F-2024-000126 | Puerto Austral S.A.
-- 03/02/2023 | F-2023-000290 | Minera  Altos  del  Loa  SpA
-- 11/07/2023 | F-2023-000315 | Municipalidad de Quilpué
-- 15/04/2022 | F-2022-000360 | Minera Altos del Loa SpA
-- 07/04/2022 | F-2022-000399 | Ferretería Los Andes LTDA
Fechas: las 21 líneas que un CAST se come en silencio · 20 min

21 líneas de 6.000. El 0,35 %. Y ahora la parte que importa:

Fechas: las 21 líneas que un CAST se come en silencio · 20 min
SELECT COUNT(*) AS lineas,
       COUNT(TRY_CAST(fecha_emision AS DATE))
         AS parsean_como_iso,
       COUNT(*) - COUNT(TRY_CAST(fecha_emision AS DATE))
         AS se_vuelven_null,
       COUNT(COALESCE(
         TRY_CAST(fecha_emision AS DATE),
         TRY_STRPTIME(fecha_emision, '%d/%m/%Y')::DATE))
         AS parsean_con_las_dos
FROM ventas;
-- 6000 | 5979 | 21 | 6000
Fechas: las 21 líneas que un CAST se come en silencio · 20 min

El momento del bloque, escrito en la pizarra:

Un CAST convierte esas 21 líneas en NULL. No falla. No avisa. Se las come.

«Y ahí es donde esto deja de ser un detalle: esas 21 líneas desaparecen de todo lo que se agrupe por fecha. Del reporte trimestral, de la estacionalidad, del cálculo del DSO. La conversión que no falla es la más peligrosa que existe, porque no deja rastro.»
Fechas: las 21 líneas que un CAST se come en silencio · 20 min

Y así se cierra la pregunta que quedó abierta a las 21:00: read_csv_auto adivinó el tipo de las tres columnas de fecha. En dos acertó. En fecha_emision encontró 21 filas que no calzaban con ningún formato único y renunció: la dejó como texto. No es un defecto de DuckDB — es lo que hace cualquier herramienta que lee un CSV, porque un CSV no trae tipos.

La cura, que se copia al cuaderno y se usa el resto del semestre:

Fechas: las 21 líneas que un CAST se come en silencio · 20 min
CREATE OR REPLACE VIEW v AS
SELECT *,
       COALESCE(
         TRY_CAST(fecha_emision AS DATE),
         TRY_STRPTIME(fecha_emision, '%d/%m/%Y')::DATE)
         AS emision,
       TRY_CAST(fecha_vencimiento AS DATE) AS vencimiento,
       TRY_CAST(fecha_pago AS DATE)        AS pago
FROM ventas;
Fechas: las 21 líneas que un CAST se come en silencio · 20 min

Regla en dos frases: TRY_ delante de todo lo que convierta — devuelve NULL en vez de reventar. Y después cuenta los NULL: si conviertes y no cuentas, no convertiste, adivinaste.

Hilo 3 — salir de Excel: «Excel hace esto peor, y conviene saber exactamente por qué: no deja la fecha como texto y avisa — la convierte según la configuración regional del computador. En una máquina en español-Chile, 03/02/2023 es el 3 de febrero; en una en inglés-EE.UU., el 2 de marzo. El mismo archivo, abierto por dos personas del mismo equipo, produce dos análisis distintos y ninguno de los dos da error. En SQL la conversión es explícita y queda escrita en la consulta.»

Fechas: las 21 líneas que un CAST se come en silencio · 20 min
Nota para el Parcial 2: la "plataforma chilena" del enunciado oficial exige la salida en dd-mm-yyyy. Hoy aprendieron los dos sentidos: leer lo chileno y escribirlo. strftime(emision, '%d-%m-%Y') y listo. Guárdenlo.
Monedas: 89 de 395 líneas en USD, y los 15 días que faltan · 15 min
SELECT moneda,
       COUNT(*) AS lineas,
       COUNT(*) FILTER (WHERE tipo_cambio IS NULL)
         AS sin_tipo_de_cambio
FROM ventas
GROUP BY moneda ORDER BY moneda;
-- CLP | 5605 | 0
-- USD |  395 | 89
Monedas: 89 de 395 líneas en USD, y los 15 días que faltan · 15 min
«395 líneas facturadas en dólares —las mineras y parte del sector industrial. 89 de ellas vienen sin tipo de cambio. El 22,5 %.»

Y la pregunta incómoda, primero:

SELECT ROUND(SUM(cantidad * precio_unitario) / 1e6, 1)
         AS suma_ciega_MM
FROM ventas;
-- 9129.8
Monedas: 89 de 395 líneas en USD, y los 15 días que faltan · 15 min
«Este número lo hemos usado todas las clases y está mal. Suma 5.605 líneas en pesos y 395 en dólares como si fueran la misma unidad. Es sumar kilos con litros. La consulta corrió, el número salió, y nadie se quejó.»

Paso 1 — unir por fecha exacta contra el Banco Central:

Monedas: 89 de 395 líneas en USD, y los 15 días que faltan · 15 min
CREATE OR REPLACE TABLE tc AS
SELECT * FROM read_csv_auto('datos/tc_diario.csv');

SELECT COUNT(*) AS lineas_usd_sin_tc,
       COUNT(tc.tc_usd_clp) AS rescatadas,
       COUNT(*) - COUNT(tc.tc_usd_clp) AS siguen_sin_tc
FROM v LEFT JOIN tc ON v.emision = tc.fecha
WHERE v.moneda = 'USD' AND v.tipo_cambio IS NULL;
-- 89 | 87 | 2
Monedas: 89 de 395 líneas en USD, y los 15 días que faltan · 15 min

Detenerse. «87 rescatadas. Y dos que no. ¿Por qué dos?»

Monedas: 89 de 395 líneas en USD, y los 15 días que faltan · 15 min
SELECT v.numero_factura, v.emision,
       dayname(v.emision) AS dia, v.cliente
FROM v LEFT JOIN tc ON v.emision = tc.fecha
WHERE v.moneda = 'USD'
  AND v.tipo_cambio IS NULL AND tc.fecha IS NULL;
-- F-2022-001336 | 2022-06-06 | Monday | Agrícola Colchagua SpA
-- F-2022-001336 | 2022-06-06 | Monday | Agrícola Colchagua SpA
Monedas: 89 de 395 líneas en USD, y los 15 días que faltan · 15 min
«Una factura. Dos líneas. Emitida el lunes 6 de junio de 2022, que es uno de los 15 días que anotaron antes del respiro. El problema encadenado completo: una factura en dólares, sin tipo de cambio propio, emitida justo un día en que la fuente externa tampoco tiene dato.»

Paso 2 — la regla declarada. Preguntar antes de mostrar: «¿qué hacemos? ¿Inventamos el promedio del mes?» Recoger dos ideas, no más, y dar la regla del oficio:

Monedas: 89 de 395 líneas en USD, y los 15 días que faltan · 15 min
Nunca se inventa un valor intermedio. Se arrastra el último valor conocido —que es lo que hace la propia empresa cuando el banco no publica— y se declara por escrito que se arrastró. Un dato falso contamina en silencio; un arrastre declarado es una decisión auditable.

En SQL eso es un ASOF JOIN: "el registro más reciente que sea anterior o igual a esta fecha".

Monedas: 89 de 395 líneas en USD, y los 15 días que faltan · 15 min
SELECT COUNT(*) AS lineas_usd_sin_tc,
       COUNT(tc.tc_usd_clp) AS con_tc_tras_arrastre
FROM v ASOF LEFT JOIN tc ON v.emision >= tc.fecha
WHERE v.moneda = 'USD' AND v.tipo_cambio IS NULL;
-- 89 | 89

Paso 3 — el número que sí se puede mandar:

Monedas: 89 de 395 líneas en USD, y los 15 días que faltan · 15 min
WITH consolidado AS (
  SELECT v.*,
         COALESCE(v.tipo_cambio, tc.tc_usd_clp) AS tc_final
  FROM v ASOF LEFT JOIN tc ON v.emision >= tc.fecha
), lineas AS (
  SELECT cantidad * precio_unitario AS nominal,
         cantidad * precio_unitario
           * CASE WHEN moneda = 'USD' THEN tc_final
                  ELSE 1 END AS en_clp
  FROM consolidado
)
SELECT ROUND(SUM(nominal) / 1e6, 1) AS suma_ciega_MM,
       ROUND(SUM(en_clp)  / 1e6, 1) AS consolidada_clp_MM,
       ROUND((SUM(en_clp) - SUM(nominal)) / 1e6, 1)
         AS brecha_MM
FROM lineas;
-- 9129.8 | 9825.5 | 695.7
Monedas: 89 de 395 líneas en USD, y los 15 días que faltan · 15 min

El número del día, en la pizarra:

Monedas: 89 de 395 líneas en USD, y los 15 días que faltan · 15 min
CLP 695,7 millones de diferencia entre sumar la columna y consolidar la moneda.

«¿Un 7,6 %? Ése es el porcentaje sobre la suma ciega — o sea, dividido por el número que acabamos de declarar mal hace veinte minutos. Sobre la venta consolidada, que es la que sí se puede mandar, la brecha es 7,1 %. Fíjense: hasta el porcentaje cambia según el denominador que elijas, y elegir denominador es una decisión, no un detalle de formato.

Y ojo: no hay ninguna fila mala. Todas las cifras del archivo son correctas. El error está en la operación, no en el dato.»
Monedas: 89 de 395 líneas en USD, y los 15 días que faltan · 15 min

Hilo 2 — dónde vive esto: «Esto en una empresa se llama "el reporte de ventas no cuadra con el estado de resultados". Y la explicación, nueve de cada diez veces, es una de estas dos: el tipo de cambio, o el descuento repetido. La segunda la vemos en la clase 09.»

Cartera: 639 líneas sin fecha de pago y el DSO real por segmento · 15 min

El concepto en una frase de negocio: el DSO (days sales outstanding, días de cobro) es cuántos días pasan entre que emites la factura y te pagan. No es un indicador de contabilidad: es la respuesta a «¿cuánta plata mía está en la calle y por cuánto tiempo?»

Cartera: 639 líneas sin fecha de pago y el DSO real por segmento · 15 min
SELECT COUNT(*) FILTER (WHERE pago IS NULL)
         AS lineas_sin_pago,
       COUNT(DISTINCT numero_factura)
         FILTER (WHERE pago IS NULL)
         AS facturas_sin_pago,
       COUNT(*) FILTER (WHERE pago IS NULL AND cantidad > 0)
         AS sin_notas_de_credito
FROM v;
-- 639 | 324 | 530
Cartera: 639 líneas sin fecha de pago y el DSO real por segmento · 15 min

Y el DSO, por el camino ingenuo —el del analista que todavía no arregló las fechas, que es donde va a estar la mitad de la sala:

Cartera: 639 líneas sin fecha de pago y el DSO real por segmento · 15 min
SELECT upper(strip_accents(trim(segmento))) AS segmento,
       COUNT(*) FILTER (
         WHERE TRY_CAST(fecha_emision AS DATE) IS NOT NULL)
         AS lineas_pagadas,
       ROUND(AVG(TRY_CAST(fecha_pago AS DATE)
                 - TRY_CAST(fecha_emision AS DATE)), 1)
         AS dso_dias
FROM ventas
WHERE fecha_pago IS NOT NULL
GROUP BY 1
ORDER BY dso_dias DESC;
Cartera: 639 líneas sin fecha de pago y el DSO real por segmento · 15 min
segmentolineas_pagadasdso_dias
PUBLICO31791,6
CONSTRUCCION1.30872,5
MINERIA91160,6
INDUSTRIAL1.55156,2
RETAIL1.25455,7

Y la consulta que convierte el indicador en una conversación:

Cartera: 639 líneas sin fecha de pago y el DSO real por segmento · 15 min
SELECT upper(strip_accents(trim(segmento))) AS segmento,
       ROUND(AVG(condicion_pago_dias), 1)   AS dias_pactados,
       ROUND(AVG(pago - vencimiento), 1)    AS dias_de_mora
FROM v
WHERE pago IS NOT NULL
GROUP BY 1
ORDER BY dias_de_mora DESC;
Cartera: 639 líneas sin fecha de pago y el DSO real por segmento · 15 min
segmentodias_pactadosdias_de_mora
PUBLICO49,842,0
CONSTRUCCION50,621,9
MINERIA52,28,5
INDUSTRIAL51,15,1
RETAIL50,94,8
Cartera: 639 líneas sin fecha de pago y el DSO real por segmento · 15 min
«Miren la primera columna. A todos los segmentos les pactamos lo mismo: unos 50 días. La diferencia no está en lo que negociamos, está en lo que cobramos. Retail paga con 4,8 días de atraso; el sector Público, con 42.

Traducido a plata: la empresa le está prestando al sector Público, gratis, durante un mes y medio adicional al plazo que ella misma le dio. Eso no aparece en ninguna línea del estado de resultados. Aparece en la caja.»
Cartera: 639 líneas sin fecha de pago y el DSO real por segmento · 15 min

Y el cierre del bloque, con la cifra fina —la prueba de que las 21 líneas de recién importan:

Cartera: 639 líneas sin fecha de pago y el DSO real por segmento · 15 min
SELECT upper(strip_accents(trim(segmento))) AS segmento,
       ROUND(AVG(pago - TRY_CAST(fecha_emision AS DATE)), 1)
         AS dso_solo_iso,
       ROUND(AVG(pago - emision), 1)
         AS dso_rescatando_las_21
FROM v WHERE pago IS NOT NULL
GROUP BY 1 ORDER BY dso_rescatando_las_21 DESC;
-- PUBLICO      | 91.6 | 91.8
-- CONSTRUCCION | 72.5 | 72.5
-- MINERIA      | 60.6 | 60.7
-- INDUSTRIAL   | 56.2 | 56.2
-- RETAIL       | 55.7 | 55.7
Cartera: 639 líneas sin fecha de pago y el DSO real por segmento · 15 min
«Ahí está la prueba. Veintiuna filas de seis mil mueven el DSO del cliente que peor paga: de 91,6 a 91,8. Dos décimas de día. Parece nada. Y es exactamente el tipo de diferencia que hace que tu número no cuadre con el de Contabilidad y que te pases una mañana buscando por qué. La versión de la derecha es la correcta: la de la izquierda es la que perdió 21 facturas sin decirlo.»
Cartera: 639 líneas sin fecha de pago y el DSO real por segmento · 15 min

Y el bloque cierra con el gráfico, que es donde el DSO deja de ser una columna y pasa a ser una conversación. Las dos tablas ya calcularon los días; la barra sólo los ordena y los pone contra el plazo que la propia empresa pactó.

Cartera: 639 líneas sin fecha de pago y el DSO real por segmento · 15 min
import matplotlib.pyplot as plt

GRIS, TINTA, ALERTA = "#b8b8b8", "#102a38", "#8c2b20"

d = bd.sql("""
  SELECT upper(strip_accents(trim(segmento))) AS segmento,
         ROUND(AVG(pago - emision), 1)        AS dso_dias,
         ROUND(AVG(condicion_pago_dias), 1)   AS dias_pactados
  FROM v WHERE pago IS NOT NULL
  GROUP BY 1 ORDER BY dso_dias""")

fig, eje = plt.subplots(figsize=(10, 4.2))
eje.barh(d.segmento, d.dso_dias, height=0.62,
         color=[ALERTA if s == "PUBLICO" else GRIS for s in d.segmento])
eje.axvline(d.dias_pactados.mean(), color=TINTA, linestyle="--", linewidth=1)
eje.text(d.dias_pactados.mean() + 1.5, len(d) - 0.45,
         f"plazo pactado: {d.dias_pactados.mean():.0f} días, el mismo para los cinco",
         fontsize=9, color=TINTA)
eje.set_ylim(-0.6, len(d) - 0.15)

for y, valor in enumerate(d.dso_dias):
    eje.text(valor + 1, y, f"{valor:.1f}".replace(".", ","), va="center",
             fontsize=9, color=TINTA)

eje.set_title("A todos les pactamos unos 50 días. El sector Público paga a 91,8:\n"
              "42 días de caja de la empresa financiando gratis al Estado",
              fontsize=13, color=TINTA, loc="left")
eje.set_xlabel("días entre la emisión de la factura y el pago", fontsize=9)
eje.set_xlim(0, 105)
for lado in ("top", "right", "left"):
    eje.spines[lado].set_visible(False)
eje.tick_params(length=0)
fig.tight_layout()
plt.show()
Cartera: 639 líneas sin fecha de pago y el DSO real por segmento · 15 min

Qué se mira en el resultado:

Cartera: 639 líneas sin fecha de pago y el DSO real por segmento · 15 min
  1. La línea de puntos. Es el plazo pactado, y es prácticamente el mismo para los cinco segmentos. Todo lo que sobresale a su derecha es plata que la empresa ya no tiene y todavía no cobró.
  2. La barra de color. El sector Público, y sólo el sector Público. Ésa es la regla de estética funcional que el curso enseña en la clase 18 y que acá se está usando desde el primer día: gris para todo, un color para lo que decide.
  3. El orden. De mayor a menor, no alfabético. Un ranking ordenado por nombre obliga a leer los cinco números para saber quién es el peor; ordenado por valor, el peor es el de arriba y no hay que leer nada.
  4. **Y lo que este gráfico no hace:** poner los días pactados como una segunda serie con su propia escala. Van en la misma escala porque son la misma unidad —días—, y por eso pueden convivir. Dos unidades distintas habrían sido dos gráficos.
El experto de negocio interno: Control de Gestión y Tesorería · 8 min

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

El experto de negocio interno: Control de Gestión y Tesorería · 8 min
Control de Gestión: «El Banco Central publica el dólar observado y el ERP guarda su propio tipo_cambio en cada factura. Los dos números existen y no son iguales. ¿Cuál es la fuente oficial de tipo de cambio para nuestro cierre?»

Tesorería: «El sector Público paga a 91,6 días cuando le pactamos 50. ¿Por qué el sector Público paga a 90 días y qué hacemos mientras tanto?»
El experto de negocio interno: Control de Gestión y Tesorería · 8 min
«Puede ser. Y también puede que la empresa tenga una política escrita que diga otra cosa: el tipo de cambio de la fecha de emisión, o el del último día del mes, o el pactado en el contrato con el cliente minero. Las tres son defendibles y sólo una es la de tu empresa. El que lo sabe es Control de Gestión, que arma el cierre mensual. Si eliges tú, tu estado de resultados no va a cuadrar con el de Contabilidad, y la reunión de las 9 de la mañana la vas a perder.»

«Y con el sector Público, lo mismo por el otro lado: puede que el Estado pague así por diseño —hay procesos de recepción conforme, devengo y pago que toman ese tiempo y que ningún ejecutivo de cobranza acelera. Si es lo segundo, el DSO de 91,6 no es un problema de gestión: es un dato de entrada para el presupuesto de caja, y la decisión que sigue no es "llamar más", es "cuánto capital de trabajo necesito para vender al Estado, y si ese negocio paga ese costo". Quien lo sabe es Tesorería.»
El experto de negocio interno: Control de Gestión y Tesorería · 8 min

A quién se le pregunta, hoy:

El experto de negocio interno: Control de Gestión y Tesorería · 8 min
Duda que salió del datoEl rol que tiene la respuesta
¿Cuál es la fuente oficial de tipo de cambio del cierre: el Banco Central o el ERP?Control de Gestión
¿Qué hacemos con la factura del 6 de junio de 2022 que no tiene tipo de cambio ni en el ERP ni en el Banco Central?Control de Gestión (y la respuesta se escribe, no se recuerda)
¿Los 91,6 días del sector Público son mora o son el proceso?Tesorería
Las 639 líneas sin fecha de pago, ¿están impagas o falta cargar el pago?Cuentas por Cobrar
¿Nuestros RUT están validados contra el SII antes de dar crédito?Cuentas por Cobrar / Riesgo de crédito
El experto de negocio interno: Control de Gestión y Tesorería · 8 min

Hilo 2 — dónde vive esto en la organización: «En una empresa mediana, esta conversación es un correo de tres líneas. En una empresa grande existe un documento que se llama "política contable" y contesta las cinco. Preguntar por ese documento en tu primera semana te ahorra tres meses.»

Mapa de fuentes personal, síntesis y ticket · 9 min

Mapa de fuentes personal (media hoja, individual, 3 min). «Piensen en la empresa donde trabajan, o en una que conozcan —la de un familiar sirve.» Tres columnas:

¿Qué dato interno te falta para decidir mejor?¿Qué fuente abierta lo complementa?¿En qué formato vive cada uno?

Un voluntario lee el suyo. Conectar con: «eso es RA2, y el requisito 1 del Parcial 1 pregunta exactamente eso.» El resto se entrega la clase siguiente.

Síntesis, construida con ellos, con sus palabras:

Mapa de fuentes personal, síntesis y ticket · 9 min
  1. «Los tres tipos de fuente, con un ejemplo chileno cada uno» → interna / externa de negocio / abierta —y los registros de eventos.
  2. «El dólar observado que publican hoy, ¿de qué día es?» → del día hábil anterior. Eso se llama vigencia.
  3. «¿Qué hace TRY_ y qué hay que hacer después?» → devuelve NULL en vez de reventar; hay que contar los NULL.
  4. «¿Qué hacemos con un día sin tipo de cambio?» → arrastrar el último conocido y declararlo. Nunca inventar.
  5. «¿Cuál es la diferencia entre lo pactado y lo cobrado, y cómo se llama?» → 42 días en el sector Público; se llama capital de trabajo.
Mapa de fuentes personal, síntesis y ticket · 9 min

Y el nombre que hay que ponerle a todo esto, porque el enunciado lo va a usar: lo que llevamos tres semanas armando son los requisitos del dataframe final —la tabla que queda lista para analizar—, y son cinco: estructura (qué es una fila: una línea de factura, no una factura), formato (fecha ISO, decimal con punto, moneda declarada), tipos (fecha_emision es DATE y no VARCHAR), calidad (cuántos NULL quedan, cuáles se rescataron y cuáles se declararon) y clave de unión (el RUT normalizado, que es lo que pega el maestro).

Mapa de fuentes personal, síntesis y ticket · 9 min
«Los cinco se escriben antes de tocar una fila, y el quinto es el que más se olvida: si el dataframe final no trae la clave de unión, no se junta con nada — y ahí quedaste con dos archivos que no se hablan.»

Ticket TC04 (3 min, se proyecta): a) ¿Qué le exige una fuente externa a tus datos antes de poder cruzarla? Una frase. b) Cruzaste el tipo de cambio observado contra el libro de ventas: quedan 89 líneas en USD sin valorizar y 21 facturas que desaparecieron sin error. Di cuál falla es del archivo externo y cuál de tu columna de fecha. c) 1–5 y qué te falta.

Mapa de fuentes personal, síntesis y ticket · 9 min

Anki S3 disponible (tarjetas nuevas de fuentes, fechas, monedas y cartera; incluye las 4 tarjetas de llave natural vs surrogate que hoy no se dictan).

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

→ 639 líneas sin fecha de pago. ¿Es un error del archivo o es plata en la calle?

Puente a la clase 05:

Mapa de fuentes personal, síntesis y ticket · 9 min
«La próxima empezamos calidad en serio: cómo se decide si un vacío es un error o es información, y cómo se define qué es un valor "raro" sin que sea tu opinión. Con dos números que ya conocen: 639 y 89. Uno de los dos es un error de captura. El otro es el negocio funcionando — y confundirlos cuesta caro.»
Clase 04 · 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-04.ipynb

← → avanzar · P proyector · F pantalla completa
GAA1104 · C04 · De dónde sale el dato y dónde se rompe 20:30 1 / 1