14  Comunicación técnica de sistemas inteligentes

mlops
comunicacion
dashboards
Autor/a

Wilder Ramírez Delgado

Fecha de publicación

12 de septiembre de 2026

Serie MLOps y Producción · de la ingeniería de datos al monitoreo.

Módulo V — Monitoreo, interpretabilidad y comunicación

El curso entrenó, orquestó, publicó y vigiló. Nada de eso decide un presupuesto si el informe dice “F1 = 0.63” y el lector no sabe si debe esperar, reentrenar o apagar.

Este artículo cierra la serie: atar un número a una acción y a un riesgo. No levantamos Streamlit. El tablero es Matplotlib —las figuras que un Streamlit solo coloca—. El reporte es Markdown generado por código.

Objetivos

  1. Elegir métricas según la decisión, no según el print de sklearn.
  2. Armar un tablero mínimo (tendencia + semáforo).
  3. Escribir el riesgo en tres audiencias.
  4. Generar un reporte automático a partir de números, no de memoria.

Qué no cubre

  • Reentrenar (sesión 6) ni PSI (sesión 12): aquí se consumen.

Ruta

  1. Una métrica no es una decisión.
  2. Serie ya observada.
  3. Semáforo y tablero.
  4. Tres audiencias.
  5. Reporte automático.
  6. Taller de cierre.
from pathlib import Path
from datetime import date
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

RUTA = Path("artefactos_sesion14")
RUTA.mkdir(exist_ok=True)
np.random.seed(42)
print("Entorno listo. Esta sesión no entrena: redacta.")
Entorno listo. Esta sesión no entrena: redacta.

1) Una métrica no es una decisión

En una frase: comunicar técnicamente es atar un número a una acción posible y a un riesgo si esa acción falla.

“F1 = 0.63” no dice qué hacer. “F1 de la semana 7 cayó bajo el umbral 0.55: no promover; abrir reentrenamiento” sí.

Error Por qué falla Alternativa
Tirar todas las métricas Nadie sabe cuál manda Una de decisión + dos de contexto
Accuracy en desbalance Felicita al que niega a todos F1, recall o costo
Ocultar el umbral Nadie puede discrepar El umbral se escribe antes

2) La serie que ‘ya observó’ monitoreo

semanas = pd.DataFrame({
    "semana": np.arange(1, 9),
    "f1":     [0.62, 0.61, 0.63, 0.60, 0.59, 0.57, 0.48, 0.46],
    "psi":    [0.04, 0.05, 0.06, 0.08, 0.11, 0.18, 0.31, 0.34],
    "recall": [0.58, 0.57, 0.59, 0.55, 0.54, 0.50, 0.41, 0.38],
    "cerrada": [True]*6 + [True, True],
})
UMBRAL_F1 = 0.55
UMBRAL_PSI = 0.25
print(semanas.to_string(index=False))
print("Umbrales acordados ANTES: F1 ≥", UMBRAL_F1, "| PSI ≤", UMBRAL_PSI)
 semana   f1  psi  recall  cerrada
      1 0.62 0.04    0.58     True
      2 0.61 0.05    0.57     True
      3 0.63 0.06    0.59     True
      4 0.60 0.08    0.55     True
      5 0.59 0.11    0.54     True
      6 0.57 0.18    0.50     True
      7 0.48 0.31    0.41     True
      8 0.46 0.34    0.38     True
Umbrales acordados ANTES: F1 ≥ 0.55 | PSI ≤ 0.25

Estos números son el input de la comunicación, no un resultado nuevo de sklearn. Inventarlos a mano en un informe real sería fraude; aquí simulan la salida de la sesión 12.

3) Semáforo: tres estados, tres acciones

def semaforo(fila):
    if (not fila["cerrada"]):
        return "ámbar", "semana abierta: solo vigilar PSI"
    if fila["f1"] < UMBRAL_F1 and fila["psi"] > UMBRAL_PSI:
        return "rojo", "reentrenar y no promover"
    if fila["f1"] < UMBRAL_F1:
        return "rojo", "rollback y diagnóstico (posible concept drift)"
    if fila["psi"] > UMBRAL_PSI:
        return "ámbar", "vigilar: foto de x cambió, F1 aún sostiene"
    return "verde", "mantener production"

estados = semanas.apply(semaforo, axis=1, result_type="expand")
semanas["estado"] = estados[0]
semanas["accion"] = estados[1]
print(semanas[["semana", "f1", "psi", "estado", "accion"]].to_string(index=False))
 semana   f1  psi estado                   accion
      1 0.62 0.04  verde      mantener production
      2 0.61 0.05  verde      mantener production
      3 0.63 0.06  verde      mantener production
      4 0.60 0.08  verde      mantener production
      5 0.59 0.11  verde      mantener production
      6 0.57 0.18  verde      mantener production
      7 0.48 0.31   rojo reentrenar y no promover
      8 0.46 0.34   rojo reentrenar y no promover

El semáforo es un contrato de lenguaje. Verde no significa “el modelo es justo” (sesión 13). Significa “no se dispara la acción de esta política”.

4) Tablero mínimo: tendencia + umbral visible

fig, axes = plt.subplots(2, 1, figsize=(8.6, 6.2), sharex=True)
axes[0].plot(semanas["semana"], semanas["f1"], marker="o")
axes[0].axhline(UMBRAL_F1, color="tomato", ls="--", label="umbral F1")
axes[0].set_ylabel("F1")
axes[0].legend()
axes[1].plot(semanas["semana"], semanas["psi"], marker="s", color="slategray")
axes[1].axhline(UMBRAL_PSI, color="tomato", ls="--", label="umbral PSI")
axes[1].set_ylabel("PSI")
axes[1].set_xlabel("semana")
axes[1].legend()
axes[0].set_title("Tablero de decisión (umbrales pintados, no 'implícitos')")
plt.tight_layout()
fig.savefig(RUTA / "tablero.png", dpi=120)
plt.show()
print("Figura guardada: tablero.png")

Figura guardada: tablero.png

Un Streamlit solo colocaría esta figura junto a un selector de semana. La sustancia ya está aquí. El GIF del dashboard no es el entregable.

5) Última semana: la frase que manda

ultima = semanas.iloc[-1]
print("Estado:", ultima["estado"])
print("Acción:", ultima["accion"])
print(f"F1={ultima['f1']:.2f} (umbral {UMBRAL_F1}) | PSI={ultima['psi']:.2f} (umbral {UMBRAL_PSI})")
Estado: rojo
Acción: reentrenar y no promover
F1=0.46 (umbral 0.55) | PSI=0.34 (umbral 0.25)

6) Tres audiencias, tres textos

El mismo hecho (rojo, semana 8) no se narra igual al comité, al ingeniero on-call y al regulador. Mentir por omisión en una audiencia es tan grave como inventar el F1.

def textos(fila):
    return {
        "comite": (
            f"Recomendación: no promover y abrir reentrenamiento. "
            f"El F1 semanal ({fila['f1']:.2f}) quedó bajo el mínimo acordado ({UMBRAL_F1}) "
            f"y la foto de los datos (PSI={fila['psi']:.2f}) también superó su umbral."
        ),
        "oncall": (
            f"SEMAFORO=rojo semana={int(fila['semana'])} f1={fila['f1']:.2f} psi={fila['psi']:.2f}. "
            f"Acción: rollback al run_id previo (sesión 8/11) y disparar job de train."
        ),
        "regulador": (
            f"El sistema permanece en production bajo política de umbrales prepublicados. "
            f"Se registra desviación de desempeño y de estabilidad poblacional. "
            f"La explicación caso a caso (sesión 13) no se sustituye por este agregado."
        ),
    }

for k, v in textos(ultima).items():
    print("===", k, "===")
    print(v)
    print()
=== comite ===
Recomendación: no promover y abrir reentrenamiento. El F1 semanal (0.46) quedó bajo el mínimo acordado (0.55) y la foto de los datos (PSI=0.34) también superó su umbral.

=== oncall ===
SEMAFORO=rojo semana=8 f1=0.46 psi=0.34. Acción: rollback al run_id previo (sesión 8/11) y disparar job de train.

=== regulador ===
El sistema permanece en production bajo política de umbrales prepublicados. Se registra desviación de desempeño y de estabilidad poblacional. La explicación caso a caso (sesión 13) no se sustituye por este agregado.

7) Riesgos que hay que decir en voz alta

Si hacemos Riesgo Si no hacemos
Reentrenar ya Hornear un drift de captura Seguir decidiendo mal
Rollback Volver a un modelo viejo Dejar el degradado
Esperar otra semana Más casos mal etiquetados Reaccionar a un falso positivo de PSI

La comunicación técnica enumera ambos lados. Un informe que solo lista beneficios es publicidad.

8) Reporte automático: el Markdown no se escribe de memoria

reporte = f'''# Reporte semanal de modelo — semana {int(ultima["semana"])}
**Fecha:** {date.today().isoformat()}
**Estado:** {ultima["estado"]}
**Acción recomendada:** {ultima["accion"]}

## Métricas de decisión
- F1: {ultima["f1"]:.2f} (umbral {UMBRAL_F1})
- PSI: {ultima["psi"]:.2f} (umbral {UMBRAL_PSI})
- Recall (contexto): {ultima["recall"]:.2f}

## Lectura para comité
{textos(ultima)["comite"]}

## Lectura para operación
{textos(ultima)["oncall"]}

## Nota de gobernanza
{textos(ultima)["regulador"]}

## Qué no dice este reporte
- No certifica equidad (sesión 13).
- No sustituye el hash de datos (sesión 8).
- No autoriza a bajar el umbral para poner verde (sesión 11).
'''
(RUTA / "reporte.md").write_text(reporte, encoding="utf-8")
print(reporte)
# Reporte semanal de modelo — semana 8
**Fecha:** 2026-09-26
**Estado:** rojo
**Acción recomendada:** reentrenar y no promover

## Métricas de decisión
- F1: 0.46 (umbral 0.55)
- PSI: 0.34 (umbral 0.25)
- Recall (contexto): 0.38

## Lectura para comité
Recomendación: no promover y abrir reentrenamiento. El F1 semanal (0.46) quedó bajo el mínimo acordado (0.55) y la foto de los datos (PSI=0.34) también superó su umbral.

## Lectura para operación
SEMAFORO=rojo semana=8 f1=0.46 psi=0.34. Acción: rollback al run_id previo (sesión 8/11) y disparar job de train.

## Nota de gobernanza
El sistema permanece en production bajo política de umbrales prepublicados. Se registra desviación de desempeño y de estabilidad poblacional. La explicación caso a caso (sesión 13) no se sustituye por este agregado.

## Qué no dice este reporte
- No certifica equidad (sesión 13).
- No sustituye el hash de datos (sesión 8).
- No autoriza a bajar el umbral para poner verde (sesión 11).

Si el número cambia y el Markdown no se regenera, el informe es un souvenir. El hábito: el reporte lo emite el job de la sesión 7, no un copy-paste del viernes.

9) Métrica de contexto: recall no desempata el semáforo

Recall está en el reporte porque el daño de un falso negativo puede importar. No está en la función semaforo de este laboratorio. Si negocio quiere que mande, se cambia la política en un commit, no en una reunión verbal.

print("¿El recall de la semana 8 cambiaría el rojo?:", ultima["recall"] < 0.45)
print("Hoy: no. La política publicada usa F1 y PSI. Punto.")
¿El recall de la semana 8 cambiaría el rojo?: True
Hoy: no. La política publicada usa F1 y PSI. Punto.

10) Cierre de la serie: mapa de una frase

Sesión Frase que este reporte hereda
4–5 \(T\) y \(f\) viajan juntos
6 el F1 no es un torneo sobre test
7 el reporte es un nodo, no un rito
8 cada cifra cita un hash
9–10 se sirve un contrato empaquetado
11 el umbral no se rebaja para el verde
12 PSI y F1 son radares distintos
13 el agregado no explica esta persona

14b) Anti-reporte: lo que no se entrega

anti = [
    "un screenshot de 14 métricas sin umbral",
    "accuracy como titular",
    "el F1 del diálogo 0.91 (sesión 6)",
    "un GIF de Streamlit sin acción",
]
print("\n".join(f"- {a}" for a in anti))
- un screenshot de 14 métricas sin umbral
- accuracy como titular
- el F1 del diálogo 0.91 (sesión 6)
- un GIF de Streamlit sin acción

14c) El semáforo de la semana 6 (ámbar de PSI)

s6 = semanas.iloc[5]
print(s6[["semana", "f1", "psi", "estado", "accion"]].to_string())
print("Útil para enseñar: ámbar no es 'todo bien' ni 'apaga'.")
semana                      6
f1                       0.57
psi                      0.18
estado                  verde
accion    mantener production
Útil para enseñar: ámbar no es 'todo bien' ni 'apaga'.

14d) Una frase de examen

“El entregable no es el F1; es una acción con umbral publicado, un riesgo si se toma y un riesgo si no se toma, en el idioma de quien decide.”

14e) One-pager de 90 segundos

Un comité no lee ocho semanas. El one-pager: estado, una cifra, una acción, un riesgo. El anexo lleva la tabla. Quien invierte el orden (anexo primero) pierde la sala.

print(f"ONE-PAGER: {ultima['estado'].upper()} | F1 {ultima['f1']:.2f} | {ultima['accion']}")
ONE-PAGER: ROJO | F1 0.46 | reentrenar y no promover

14f) RACI mínimo

Acción Responsible Accountable Consulted
Rollback on-call dueño del servicio ciencia de datos
Bajar umbral — producto + riesgo no el on-call solo
Reentrenar MLOps ciencia de datos negocio

14g) Glosario para no especialistas

glosario = {
    "F1": "equilibrio entre acertar los positivos y no inventarlos",
    "PSI": "qué tanto cambió la foto de los datos de entrada",
    "umbral": "número acordado *antes* para no discutir con el gráfico",
    "rojo": "hay que actuar, no 'el modelo es malo en abstracto'",
}
for k, v in glosario.items():
    print(f"{k}: {v}")
F1: equilibrio entre acertar los positivos y no inventarlos
PSI: qué tanto cambió la foto de los datos de entrada
umbral: número acordado *antes* para no discutir con el gráfico
rojo: hay que actuar, no 'el modelo es malo en abstracto'

14h) Agenda de la reunión de semáforo

  1. Estado (30 s). 2. Cifra y umbral (1 min). 3. Acción propuesta (1 min). 4. Riesgos (2 min). 5. Dueño y fecha (30 s). Quien empiece por la arquitectura del bosque ha perdido.

14i) Versión del reporte

print("reporte.md versionado con: fecha, umbrales, semana, estado")
print("un PDF sin esos cuatro campos es una foto, no un registro")
reporte.md versionado con: fecha, umbrales, semana, estado
un PDF sin esos cuatro campos es una foto, no un registro

14j) Traducir recall a costo

Si cada falso negativo cuesta \(c\) y cada falso positivo \(k\), el titular puede ser costo esperado, no F1. Entonces el semáforo cambia de métrica —en el repo, no en la pizarra.

c_fn, c_fp = 10, 1
print(f"si negocio firma c_fn={c_fn} x c_fp={c_fp}, el reporte titularía costo, no F1")
si negocio firma c_fn=10 x c_fp=1, el reporte titularía costo, no F1

14k) No usar jerga de las sesiones 4–5 en el comité

ColumnTransformer no se menciona. Se dice “el dato crudo entra igual que el día del entrenamiento”. La jerga queda en el anexo técnico.

14l) Pregunta que cierra la reunión

“¿Quién hace qué antes de qué fecha?” Si no hay respuesta, hubo teatro de métricas. El reporte automático deja huecos para dueño y fecha; el humano los llena.

print("dueño: ________  fecha: ________  (no lo inventa el Markdown)")
dueño: ________  fecha: ________  (no lo inventa el Markdown)

14m) Serie completa en una oración

Diseñamos \(T\), la encapsulamos, elegimos \(f\) sin pescar el test, orquestamos, nombramos la corrida, la servimos en una caja portable, la máquina pulsó el botón, vigilamos el mundo que se movió, abrimos un caso, y ahora alguien firma una acción.

14n) Lo que este cierre no promete

No promete que el F1 suba. Promete que la próxima decisión no se tomará de memoria. Esa es la diferencia entre un curso de modelos y un curso de producción.

15) Errores frecuentes

  1. Titular con accuracy.
  2. Ocultar el umbral.
  3. Un solo texto para tres audiencias.
  4. Regenerar el reporte a mano.
  5. Bajar el umbral en el acta, no en el repo.

16) Checklist

  1. ¿La métrica de decisión está declarada?
  2. ¿El umbral está pintado?
  3. ¿Hay acción, no solo color?
  4. ¿Hay riesgo de actuar y de no actuar?
  5. ¿El Markdown lo emite el código?

17) Conclusiones

Comunicar un sistema inteligente es el último contrato de la serie: el que une números a una firma. El tablero, el semáforo y el reporte automático no sustituyen las sesiones 6 a 13; las hacen decidibles. Si el lector no sabe qué hacer al terminar el informe, no hubo comunicación técnica: hubo un anexo de métricas.

18) Taller de cierre

A. Cambie UMBRAL_F1 a 0.47. ¿Sigue rojo la semana 8? ¿Debe cambiarse el umbral así?

B. Redacte el párrafo de comité para la semana 6 (ámbar).

C. Añada al reporte el run_id inventado run_prod_08.

D. Escriba el riesgo de esperar otra semana en una línea.

E. En 8 líneas, cuente la serie del curso a alguien que no la cursó, sin citar librerías.

print("Reporte:", (RUTA / "reporte.md").exists(), "| tablero:", (RUTA / "tablero.png").exists())
print("Estado final:", ultima["estado"], "→", ultima["accion"])
print("Taller A–E. Si el informe no recomienda, no cierra el curso.")
Reporte: True | tablero: True
Estado final: rojo → reentrenar y no promover
Taller A–E. Si el informe no recomienda, no cierra el curso.

💬 ¿Te sirvió?

Deja en los comentarios una duda o un caso donde aplicarías esto — respondo todos. Sígueme para no perderte el próximo artículo de la serie y comparte con alguien que esté aprendiendo arquitectura de IA.

👉 El código completo está disponible para ejecutar directamente.