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
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.
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):
JOIN devolvió 6.195 filas y el origen tenía 6.000. ¿Qué le pasó al cliente Minera Punta Verde y por qué?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.»
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.
| Tipo | Quién la genera | Ejemplos en este caso | Cómo falla |
|---|---|---|---|
| Interna | tu propia empresa | libro de ventas, maestro de clientes, plan de cuentas, contabilidad y los registros de eventos del ERP | mal tipeada, sin gobierno, sin diccionario — lo que vimos las tres semanas pasadas |
| Externa de negocio | un tercero con el que operas | cartola del banco, liquidación del factoring, estado de cuenta del proveedor | llega en el formato que a ellos les acomoda, y cambia sin avisarte |
| Abierta | una institución pública o un mercado | CMF, Banco Central, SII, INE, Bolsa de Santiago | está bien hecha y no es tuya: tiene su definición, su desfase y su metodología, y tú te adaptas |
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.»
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.
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.
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.
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.
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.
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.»
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.
CREATE OR REPLACE TABLE ventas AS SELECT * FROM read_csv_auto( 'datos/finanzas/ventas_extracto.csv');
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
El momento de honestidad del bloque.
«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.»
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.
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;
-- 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
-- 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
«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.
Y el punto que cuesta caro: un CSV no trae tipos.
DESCRIBE ventas;
| column_name | column_type |
|---|---|
fecha_emision | VARCHAR |
fecha_vencimiento | TIMESTAMP |
fecha_pago | TIMESTAMP |
«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:
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
«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.
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()
Qué se mira en el resultado:
Pararse, estirar, subir la luz un segundo.
É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:
«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.»
CAST se come en silencio · 20 minProyectar y mirar. Nadie ve nada:
SELECT fecha_emision, numero_factura, cliente FROM ventas LIMIT 15;
Ahora buscarlas a propósito:
CAST se come en silencio · 20 minSELECT 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
CAST se come en silencio · 20 minSELECT 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
CAST se come en silencio · 20 min21 líneas de 6.000. El 0,35 %. Y ahora la parte que importa:
CAST se come en silencio · 20 minSELECT 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
CAST se come en silencio · 20 minEl momento del bloque, escrito en la pizarra:
UnCASTconvierte 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.»
CAST se come en silencio · 20 minY 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:
CAST se come en silencio · 20 minCREATE 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;
CAST se come en silencio · 20 minRegla 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.»
CAST se come en silencio · 20 minNota para el Parcial 2: la "plataforma chilena" del enunciado oficial exige la salida endd-mm-yyyy. Hoy aprendieron los dos sentidos: leer lo chileno y escribirlo.strftime(emision, '%d-%m-%Y')y listo. Guárdenlo.
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
«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
«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:
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
Detenerse. «87 rescatadas. Y dos que no. ¿Por qué dos?»
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
«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:
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".
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:
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
El número del día, en la pizarra:
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.»
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.»
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?»
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
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:
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;
| segmento | lineas_pagadas | dso_dias |
|---|---|---|
| PUBLICO | 317 | 91,6 |
| CONSTRUCCION | 1.308 | 72,5 |
| MINERIA | 911 | 60,6 |
| INDUSTRIAL | 1.551 | 56,2 |
| RETAIL | 1.254 | 55,7 |
Y la consulta que convierte el indicador en una conversación:
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;
| segmento | dias_pactados | dias_de_mora |
|---|---|---|
| PUBLICO | 49,8 | 42,0 |
| CONSTRUCCION | 50,6 | 21,9 |
| MINERIA | 52,2 | 8,5 |
| INDUSTRIAL | 51,1 | 5,1 |
| RETAIL | 50,9 | 4,8 |
«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.»
Y el cierre del bloque, con la cifra fina —la prueba de que las 21 líneas de recién importan:
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
«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.»
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ó.
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()
Qué se mira en el resultado:
Los dos roles en la misma lámina. Las dudas concretas que salen del dato de hoy, escritas en la pizarra:
Control de Gestión: «El Banco Central publica el dólar observado y el ERP guarda su propiotipo_cambioen 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?»
«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.»
A quién se le pregunta, hoy:
| Duda que salió del dato | El 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 |
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 (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:
TRY_ y qué hay que hacer después?» → devuelve NULL en vez de reventar; hay que contar los NULL.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).
«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.
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:
«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.»
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