Serie MLOps y Producción · de la ingeniería de datos al monitoreo.
Módulo I — Fundamentos y arquitectura de sistemas de IA
Un diálogo frecuente al cierre de un curso de machine learning:
— ¿Ya está en producción?
— Sí. El notebook corre de punta a punta y el F1 es 0.87.
Esa respuesta confunde un experimento con un sistema. El notebook demuestra que existe una función \(\hat{y}=f(x)\) que, sobre un recorte de datos, produce un número. Un sistema de IA es otra cosa: un conjunto de contratos —de dato, de modelo, de tiempo y de responsabilidad— que debe seguir produciendo \(\hat{y}\) cuando cambia la fuente, el calendario, el responsable o la máquina.
Esta sesión no enseña a limpiar nulos (sesión 3), ni a diseñar \(T(x)\) (sesión 4), ni a orquestar un DAG (sesión 7), ni a exponer HTTP (sesión 9). Enseña el mapa: qué piezas existen, en qué orden se tocan y qué falla cuando se omiten. Quien no tiene mapa trata cada síntoma como un algoritmo distinto. Quien lo tiene, sabe en qué capa buscar.
MLOps no es “poner el .pkl en un servidor”. Es impedir que el camino del dato al veredicto sea un relato oral.
Objetivos de aprendizaje
Al finalizar, el estudiante estará en capacidad de:
Distinguir un experimento (notebook) de un sistema (contratos, artefactos, dueños).
Recorrer el ciclo de vida del aprendizaje automático como secuencia de etapas con entradas y salidas, no como lista de herramientas.
Separar el pipeline de datos del pipeline de modelo: distintos contratos, distintos fallos.
Argumentar cuándo basta un monolito y cuándo hace falta descomponer por interfaces.
Contrastar batch, online y near real-time por latencia, frescura y costo —sin implementar un servidor.
Trazar un flujo extremo a extremo mínimo (evento → persistencia → puntuación → registro) y situar cada sesión posterior en ese mapa.
1) Qué es MLOps — y qué no es
MLOps (Machine Learning Operations) nombra el conjunto de prácticas que hacen repetible, auditable y operable el camino desde un dato crudo hasta una decisión que alguien usa. No es un producto. No es un cargo. No es “DevOps con un modelo en el medio”, aunque toma de DevOps la idea de que lo que no está escrito no existe.
Tres definiciones útiles, de más estrecha a más amplia:
Alcance
Afirmación
Riesgo si se toma como única
Herramientas
“Usamos un registro, un orquestador y un monitor.”
Se compra software y se sigue operando a mano.
Proceso
“Cada cambio de dato, código o modelo deja traza.”
Lo que MLOps no resuelve por sí solo: un problema mal planteado, una etiqueta podrida, un sesgo de captura. Esas fallas son de diseño del problema. MLOps las hace visibles más rápido; no las convierte en aciertos.
1.1 El objeto de estudio no es el algoritmo
En un curso clásico el objeto es \(f\): árbol, red, \(k\)-NN. En un sistema el objeto es la composición
donde \(E\) extrae, \(T\) representa, \(f\) puntúa y \(A\) convierte el número en una decisión (cobrar, alertar, no hacer nada). Cada flecha tiene un dueño, un artefacto y un modo de fallar. Si solo se versiona \(f\), las otras tres flechas viven en la cabeza de quien “sí se acuerda cómo se filtraba”.
La sesión 2 desarrolla \(E\) (ETL). La 3, la calidad de \(x\). La 4, el diseño de \(T\). La 5, \(T\) y \(f\) como un contrato de ejecución. Aquí basta con nombrar las flechas.
1.2 El notebook como anti-patrón de producción
Un notebook es un medio excelente para descubrir. Es un medio pésimo para operar, porque mezcla en un solo archivo:
la pregunta de negocio,
la extracción,
la limpieza improvisada,
el ajuste de \(f\),
la cifra que se reporta,
y, a veces, el envío de un correo.
Esa mezcla produce tres deudas. Deuda de orden: las celdas se ejecutan a mano y el resultado depende de qué se corrió anoche. Deuda de frontera: no hay interfaz entre “dato listo” y “modelo listo”. Deuda de persona: si el autor sale, el sistema sale con él.
El criterio de esta sesión: si no se puede señalar el artefacto de cada flecha, no hay sistema. Hay un relato.
from pathlib import Pathimport jsonfrom datetime import datetime, timezoneimport pandas as pdimport numpy as npRUTA = Path("artefactos_sesion1")RUTA.mkdir(exist_ok=True)# Inventario de un "sistema" que vive solo en un notebook.deuda = pd.DataFrame([ {"pieza": "pregunta de negocio", "esta_escrita": False, "dueño": "oral"}, {"pieza": "esquema de entrada", "esta_escrita": False, "dueño": "la celda 7"}, {"pieza": "regla de limpieza", "esta_escrita": False, "dueño": "un dropna suelto"}, {"pieza": "función de puntuación", "esta_escrita": True, "dueño": "la última celda"}, {"pieza": "umbral de decisión", "esta_escrita": False, "dueño": "0.5 por costumbre"}, {"pieza": "registro de lo puntuado", "esta_escrita": False, "dueño": "nadie"},])deuda["hueco"] =~deuda["esta_escrita"]print(deuda.to_string(index=False))print("Huecos:", int(deuda["hueco"].sum()), "de", len(deuda))
pieza esta_escrita dueño hueco
pregunta de negocio False oral True
esquema de entrada False la celda 7 True
regla de limpieza False un dropna suelto True
función de puntuación True la última celda False
umbral de decisión False 0.5 por costumbre True
registro de lo puntuado False nadie True
Huecos: 5 de 6
2) El ciclo de vida no es un afiche: es un grafo de artefactos
Las etapas clásicas —problema, dato, entrenamiento, evaluación, despliegue, monitoreo— son correctas y, a la vez, engañosas: sugieren una línea. En la práctica hay ciclos. Se reentrena porque el dato cambió. Se vuelve al problema porque la métrica no era la decisión. Se revierte un despliegue porque el monitor (sesión 12) marcó deriva.
Para no perderse, cada etapa se define por qué entra, qué sale y quién firma:
Etapa
Entra
Sale
Pregunta que cierra
Problema
dolor de negocio
métrica de decisión, no solo F1
¿Qué acción cambia si \(\hat{y}\) acierta?
Dato
fuentes
tabla con esquema y hash
¿Se puede reconstruir esta foto?
Representación
tabla
\(z=T(x)\)
¿Qué se le permite ver a \(f\)?
Aprendizaje
\(z\) etiquetada
\(f\) y protocolo
¿El test permaneció ciego?
Evaluación
\(f\) + holdout
cifra una vez
¿Esa cifra es estimador o trofeo?
Despliegue
\(f\) + contrato
servicio o lote
¿Qué rompe si cambia una columna?
Monitoreo
tráfico real
semáforo y acción
¿Se distingue foto de \(x\) de F1 tardío?
El ciclo de vida, entonces, no se “cumple” comprando un orquestador. Se cumple si cada fila de esa tabla tiene un artefacto en disco —un JSON, un CSV, un manifiesto— y no solo una diapositiva.
ciclo = pd.DataFrame([ {"etapa": "problema", "artefacto": "metrica_decision.json", "depende_de": ""}, {"etapa": "dato", "artefacto": "foto.parquet + hash", "depende_de": "problema"}, {"etapa": "representacion", "artefacto": "T_fit.joblib", "depende_de": "dato"}, {"etapa": "aprendizaje", "artefacto": "f.joblib + protocolo", "depende_de": "representacion"}, {"etapa": "evaluacion", "artefacto": "holdout_una_vez.json", "depende_de": "aprendizaje"}, {"etapa": "despliegue", "artefacto": "contrato_entrada.json", "depende_de": "evaluacion"}, {"etapa": "monitoreo", "artefacto": "semaforo.json", "depende_de": "despliegue"},])orden = {e: i for i, e inenumerate(ciclo["etapa"])}def viola_orden(fila): dep = fila["depende_de"]if dep =="":returnFalsereturn orden[dep] > orden[fila["etapa"]]ciclo["orden_roto"] = ciclo.apply(viola_orden, axis=1)print(ciclo.to_string(index=False))print("Etapas que dependen de algo posterior:", int(ciclo["orden_roto"].sum()))
etapa artefacto depende_de orden_roto
problema metrica_decision.json False
dato foto.parquet + hash problema False
representacion T_fit.joblib dato False
aprendizaje f.joblib + protocolo representacion False
evaluacion holdout_una_vez.json aprendizaje False
despliegue contrato_entrada.json evaluacion False
monitoreo semaforo.json despliegue False
Etapas que dependen de algo posterior: 0
2.1 Por qué el orden no es pedantería
Si se puntúa antes de fijar el esquema, \(f\) memoriza un accidente de captura. Si se evalúa después de haber mirado el test para elegir \(f\) (sesión 6), la cifra no estima nada. Si se despliega sin contrato de entrada, el primer cambio de nombre de columna es un incidente. El orden es la forma barata de no pagar esos costos.
Un comprobador mínimo: ninguna etapa consume un artefacto que todavía no existe. No es un orquestador (eso es la sesión 7). Es una invariante de diseño.
3) Pipeline de datos frente a pipeline de modelo
Confundirlos es el error de arquitectura más caro del módulo I. Un pipeline de datos mueve y estabiliza \(x\): extrae, tipa, integra, carga. Su éxito se mide en completitud, puntualidad, esquema. Un pipeline de modelo consume \(x\)ya estabilizada y produce \(\hat{y}\) (o una \(f\) nueva). Su éxito se mide en métrica de decisión y costo de error.
Si se mezclan en una sola función run(), ocurre lo siguiente: un fallo de red en la extracción se reporta como “el modelo falló”; un cambio de umbral se “arregla” tocando el SQL; nadie sabe qué reejecutar.
Contrato mínimo, en palabras:
Datos. “Dada una fuente y una marca de agua, entrego una tabla con columnas \(C\) y un hash \(h\).”
Modelo. “Dada una tabla con columnas \(C\) (las mismas), entrego \(\hat{y}\) y no leo la fuente original.”
La sesión 2 escribe el primer contrato. La 5 escribe el segundo como fit/transform/predict. Aquí solo se demuestra que son dos funciones.
def pipeline_datos(eventos: pd.DataFrame, columnas: list[str]) ->dict:# Mueve y recorta. No puntúa. tabla = eventos.loc[:, columnas].copy() payload = tabla.to_csv(index=False).encode("utf-8")import hashlibreturn {"tabla": tabla, "n": len(tabla), "hash": hashlib.sha256(payload).hexdigest()[:12]}def pipeline_modelo(tabla: pd.DataFrame, umbral: float) -> pd.Series:# Puntúa. No extrae. Regla deliberadamente tonta: no es la sesión 6. puntaje = (tabla["monto"] / tabla["monto"].median()).clip(0, 5)return (puntaje >= umbral).astype(int)rng = np.random.default_rng(1)eventos = pd.DataFrame({"id": np.arange(8),"monto": rng.integers(10, 90, size=8),"canal": rng.choice(["app", "web"], size=8),"ruido_operativo": rng.normal(0, 1, size=8),})foto = pipeline_datos(eventos, ["id", "monto", "canal"])yhat = pipeline_modelo(foto["tabla"], umbral=1.1)print("hash de la foto:", foto["hash"], "| n =", foto["n"])print("predicciones :", yhat.tolist())print("El modelo no vio 'ruido_operativo'. Ese es el contrato.")
hash de la foto: 08fe058f4417 | n = 8
predicciones : [0, 0, 1, 1, 0, 0, 1, 1]
El modelo no vio 'ruido_operativo'. Ese es el contrato.
3.1 Una fuga temprana, solo para nombrarla
Si el pipeline de modelo calcula una estadística —la mediana del monto— sobre todo el histórico, incluido lo que más tarde se llamará test, la cifra de evaluación ya no es independiente. No se resuelve aquí (sesiones 5 y 6). Se deja el síntoma a la vista: la frontera dato/modelo también es una frontera de información.
monto = pd.Series([10, 12, 11, 80, 9, 13, 10, 12], name="monto")mediana_todo = monto.median()mediana_train = monto.iloc[:6].median()print(f"mediana(todo)={mediana_todo:.1f} mediana(train)={mediana_train:.1f}")print("Si T usa la primera, el test ya participó en la representación.")
mediana(todo)=11.5 mediana(train)=11.5
Si T usa la primera, el test ya participó en la representación.
4) Monolito frente a servicios
Un monolito es un único proceso (a menudo un único repositorio y un único despliegue) que extrae, transforma, puntúa y escribe. Un sistema por servicios parte esas responsabilidades detrás de interfaces: el extractor no importa el clasificador; el clasificador no abre la base transaccional.
Ninguno de los dos es moralmente superior. El monolito gana cuando el equipo es pequeño, el flujo es batch y el esquema es estable. Los servicios ganan cuando distintas cadencias conviven: el dato llega cada hora, el modelo se reentrena cada mes, la consulta online pide 50 ms.
El criterio de corte no es “microservicios porque es moderno”. Es ¿quién debe poder cambiar sin avisar a quién?. Si cambiar el SQL de extracción obliga a reentrenar, la frontera está mal dibujada —o el esquema no es un contrato. Si cambiar el umbral obliga a redeployar el extractor, también.
# Acoplamiento: el monolito lee una columna por posición.# El servicio lee un contrato (nombres). Un rename rompe uno y no el otro.def puntuar_monolito(fila: list) ->int:returnint(fila[1] >=50) # "la segunda columna es el monto"... hasta que no lo esdef puntuar_contrato(fila: dict, contrato: set[str]) ->int: faltan = contrato -set(fila)if faltan:raiseKeyError(f"contrato roto: {sorted(faltan)}")returnint(fila["monto"] >=50)fila_ok = {"id": 1, "monto": 72, "canal": "app"}fila_rename = {"id": 1, "importe": 72, "canal": "app"} # alguien "mejoró" el nombrecontrato = {"id", "monto", "canal"}print("monolito, orden original :", puntuar_monolito([1, 72, "app"]))print("monolito, columnas al revés:", puntuar_monolito([72, 1, "app"]), "← silencio incorrecto")print("contrato, fila ok :", puntuar_contrato(fila_ok, contrato))try: puntuar_contrato(fila_rename, contrato)exceptKeyErroras e:print("contrato, rename :", e)
monolito, orden original : 1
monolito, columnas al revés: 0 ← silencio incorrecto
contrato, fila ok : 1
contrato, rename : "contrato roto: ['monto']"
4.1 Cuándo no partir
Partir de más produce un sistema que nadie puede ejecutar en un portátil y, por tanto, nadie puede razonar. Una regla prudente para este curso: un proceso hasta que una cadencia o un dueño lo obliguen a ser dos. La sesión 9 introduce el servicio HTTP; la 10, la imagen. Aquí solo se fija el criterio.
5) Batch, online y la mentira del “tiempo real”
Tres modos, tres presupuestos:
Modo
Unidad
Latencia típica
Frescura de \(x\)
Costo relativo
Batch
lote (hora, día)
minutos–horas
la del cierre de lote
bajo
Online
una petición
decenas de ms
la de esa petición
medio–alto
Streaming / near-RT
evento o micro-lote
segundos
continua
alto de operación
“Tiempo real” en un slide suele significar online o near-RT. Casi nunca significa “el modelo se reentrena en 10 ms”. Puntuar rápido y aprender rápido son problemas distintos. Mezclarlos produce la arquitectura más cara: un reentrenamiento continuo que nadie puede auditar.
La pregunta de diseño no es “¿cuál es más avanzado?”. Es: ¿la decisión tolera esperar al próximo lote? Cobrar un fraude en la transacción no. Recalcular un score de morosidad mensual, sí. Quien elige online para un problema batch paga latencia, disponibilidad y versionado que no usa.
# Misma regla, dos cadencias. No hay servidor: hay un reloj.eventos = pd.DataFrame({"id": [1, 2, 3, 4],"t_evento": pd.to_datetime(["2026-09-12 08:01", "2026-09-12 08:04","2026-09-12 09:10", "2026-09-12 09:40"]),"monto": [20, 80, 15, 95],})cierre_lote = pd.Timestamp("2026-09-12 09:00")eventos["modo_batch"] = np.where(eventos["t_evento"] <= cierre_lote, "entra_al_lote_09", "espera_lote_10")eventos["modo_online"] ="puntua_al_llegar"print(eventos.to_string(index=False))print("Batch pospone el 09:10 y el 09:40. Online no. Esa es la única diferencia de arquitectura aquí.")
id t_evento monto modo_batch modo_online
1 2026-09-12 08:01:00 20 entra_al_lote_09 puntua_al_llegar
2 2026-09-12 08:04:00 80 entra_al_lote_09 puntua_al_llegar
3 2026-09-12 09:10:00 15 espera_lote_10 puntua_al_llegar
4 2026-09-12 09:40:00 95 espera_lote_10 puntua_al_llegar
Batch pospone el 09:10 y el 09:40. Online no. Esa es la única diferencia de arquitectura aquí.
5.1 Frescura: el dato también tiene fecha de caducidad
Sea \(t_{\text{evento}}\) el instante del hecho y \(t_{\text{score}}\) el instante de la predicción. La antigüedad\(\delta = t_{\text{score}}-t_{\text{evento}}\) es una propiedad del sistema, no del algoritmo. Un modelo excelente alimentado con \(x\) de ayer es un sistema tardío. La sesión 12 vigilará deriva; aquí solo se mide \(\delta\).
evento_id t_evento monto canal notas_libres
e000 2026-09-12 08:00:00 113 tienda revisar
e001 2026-09-12 08:07:00 78 app ok
e002 2026-09-12 08:14:00 84 web revisar
e003 2026-09-12 08:21:00 108 tienda
crudo.csv: 506 B
El crudo se guarda entero, incluidas columnas que el modelo no verá. Borrar “lo que no sirve” en la captura es perder la posibilidad de auditar. La sesión 2 llamará a esto capa bronce.
ESQUEMA = ["evento_id", "t_evento", "monto", "canal"]def extraer_foto(crudo: pd.DataFrame, esquema: list[str]) -> pd.DataFrame: faltan = [c for c in esquema if c notin crudo.columns]if faltan:raiseValueError(f"esquema incompleto: {faltan}")return crudo.loc[:, esquema].copy()foto = extraer_foto(crudo, ESQUEMA)foto.to_csv(RUTA /"foto.csv", index=False)print("columnas de la foto:", list(foto.columns))print("columnas que el modelo no ve:", [c for c in crudo.columns if c notin ESQUEMA])
columnas de la foto: ['evento_id', 't_evento', 'monto', 'canal']
columnas que el modelo no ve: ['notas_libres']
El registro de predicciones es el primer objeto que un auditor pediría: qué se decidió, con qué umbral, sobre qué foto, a qué hora. Sin ese archivo, “el modelo dijo que no” es una frase, no un hecho.
manifiesto = {"sesion": 1,"t_score": str(t_score),"n_crudo": int(len(crudo)),"n_foto": int(len(foto)),"umbral": 60,"esquema": ESQUEMA,"archivos": sorted(p.name for p in RUTA.iterdir()),}(RUTA /"manifiesto.json").write_text(json.dumps(manifiesto, indent=2, ensure_ascii=False), encoding="utf-8")print(json.dumps(manifiesto, indent=2, ensure_ascii=False))
Dado un evento_id, debe ser posible ir del registro a la fila cruda. Si no se puede, el sistema no es auditable. No hace falta un feature store (fuera de alcance) para exigir esa línea de vista.
Un mapa sin firmas es un organigrama decorativo. En un equipo pequeño una persona ocupa varios roles; eso no autoriza a no nombrarlos.
Rol
Firma
Lo que no firma
Dueño del problema
métrica de decisión, costo de FN/FP
el algoritmo
Ingeniero de datos
esquema, puntualidad, hash de la foto
el umbral de \(f\)
Científico de datos
\(T\), \(f\), protocolo de evaluación
el SQL de extracción “porque ya lo tenía”
Operación / MLOps
despliegue, rollback, semáforo
cambiar la etiqueta a mano
Auditor / riesgo
trazas y veto
reentrenar
La sesión 14 volverá sobre esto como comunicación. Aquí el punto es arquitectónico: una frontera de sistema que no tiene dueño se vuelve una frontera oral.
7.1 Ambientes: no son carpetas, son permisos
Dev, staging y producción no se distinguen por el color del logo. Se distinguen por quién puede escribir y por qué dato se ve. En desarrollo se experimenta. En staging se ensaya el contrato con un recorte realista. En producción se escribe el registro que un cliente o un regulador puede pedir.
Promover \(f\) de staging a producción sin promover el esquema y el umbral es el clásico “funciona en QA”. La sesión 8 formaliza el registro; aquí solo se nombra la regla: se promueve un paquete, no un archivo suelto.
ambiente escribe_predicciones_reales dato puede_romper
dev False muestra o sintético True
staging False foto reciente anonimizada False
prod True tráfico vivo False
8) Tres fallos de sistema que parecen fallos de modelo
Sesgo de entrenamiento–servicio. Se puntúa en producción una \(x\) que \(T\) nunca vio en entrenamiento: otra unidad, otro huso, otra columna derivada “que alguien agregó en el SQL del batch”. El F1 cae y se culpa a \(f\).
Cadencia cruzada. El lote de datos cierra a las 09:00; el modelo se reentrena a las 08:50 con la foto de ayer; a las 09:05 alguien compara cifras y declara deriva.
Decisión sin registro. El umbral se cambió “un poco” en una celda. No hay umbral en el manifiesto. No hay incidente: hay amnesia.
Ninguno se arregla eligiendo XGBoost.
# El mismo umbral, dos fotos: cambia n y cambia la tasa de positivos.# Quien solo mira la tasa dirá "el modelo se degradó".def tasa(df, umbral):returnfloat((df["monto"] >= umbral).mean())foto_lunes = foto.copy()foto_martes = foto.copy()foto_martes.loc[0:3, "monto"] = foto_martes.loc[0:3, "monto"] *2# otra captura, no otro fprint(f"tasa lunes (umbral 60): {tasa(foto_lunes, 60):.2f}")print(f"tasa martes (umbral 60): {tasa(foto_martes, 60):.2f}")print("f no cambió. Cambió la foto. El diagnóstico es de dato o de captura, no de algoritmo.")
tasa lunes (umbral 60): 0.67
tasa martes (umbral 60): 0.67
f no cambió. Cambió la foto. El diagnóstico es de dato o de captura, no de algoritmo.
8.1 Reproducir el sistema, no el número
Reproducir un F1 de un artículo exige semilla, datos y código. Reproducir un sistema exige, además, el manifiesto: esquema, umbral, \(t_{ ext{score}}\), hash de la foto. Un número que no apunta a esos cuatro objetos es una anécdota.
9) Dónde se sienta cada sesión posterior
Este curso recorre el mapa en orden de dependencia conceptual, no de moda industrial.
Sesión
Capa del mapa
Lo que no se pide todavía
2 ETL
\(E\): extraer, integrar, cargar
calidad profunda ni \(T(x)\)
3 Calidad
diagnóstico de \(x\)
monitoreo en producción (3a, 12)
4 Representación
diseñar \(T(x)\)
el Pipeline como contrato
5 Contrato de ejecución
fit/transform/predict
la búsqueda de hiperparámetros
6 Entrenamiento
protocolo honesto de \(f\)
el DAG
7 Orquestación
orden y reintentos
el registro de experimentos
8 Experimentos
run_id, staging/prod
el HTTP
9 Servicio
contrato 200/422/500
la imagen
10 Contenedor
lock y receta
el CI
11 CI/CD
puertas de métrica
el semáforo de deriva
12 Monitoreo
PSI/KS y frescura
SHAP
13 Interpretabilidad
explicar un caso
el informe de presupuesto
14 Comunicación
decidir con el titular
—
Quien salte de un accuracy de notebook a “montemos Kubernetes” está saltando las filas 2–8. El síntoma habitual es un servicio rápido que puntúa basura con gran disponibilidad.
9.1 Lo que esta sesión se niega a hacer
No limpia nulos ni discute MCAR (sesión 3).
No diseña encodings ni escaladores (sesión 4).
No serializa un Pipeline de scikit-learn (sesión 5).
No busca \(C\) ni declara un test ciego (sesión 6).
No implementa un orquestador ni HTTP ni Docker.
Si un ejercicio pide eso, está mal enunciado para este cuaderno. El ejercicio correcto pide nombrar la capa y el artefacto.
10) Inventario: el sistema cabe en una carpeta
Una arquitectura didáctica no se demuestra con un diagrama de cajas. Se demuestra listando archivos que un tercero puede leer sin el autor.
archivo bytes capa
crudo.csv 506 dato / bronce
foto.csv 444 dato / esquema
manifiesto.json 274 sistema / contrato
predicciones.csv 764 modelo / registro
10.1 Idempotencia de la puntuación batch
Volver a puntuar la misma foto con el mismo umbral debe producir el mismo yhat. Si no, hay estado escondido (una semilla no fijada, un now() dentro de \(f\), una lectura a una API). La sesión 7 formalizará reintentos; aquí se comprueba la invariante.
otra = puntuar(foto, umbral=60, t_score=t_score)igual = scored["yhat"].equals(otra["yhat"])print("misma foto + mismo umbral => mismo yhat:", igual)
misma foto + mismo umbral => mismo yhat: True
11) Reloj y calendario como entradas de primera clase
\(t_{\text{evento}}\) y \(t_{\text{score}}\) no son metadatos decorativos. Deciden si un lote es comparable con otro, si una etiqueta ya “cerró” (sesión 12) y si un reentrenamiento usó información del futuro. Un sistema que no persiste relojes no puede explicar una discrepancia el viernes.
relojes = scored[["evento_id", "t_evento", "t_score"]].copy()relojes["delta_min"] = (relojes["t_score"] - relojes["t_evento"]).dt.total_seconds() /60print(relojes.to_string(index=False))print("si t_score < t_evento en alguna fila, el sistema viaja al pasado.")
Muchos sistemas no cierran en \(\hat{y}\): cierran en una cola para un analista. Eso también es arquitectura. Cambia el SLA (el humano no responde en 40 ms), cambia el registro (hay que guardar la decisión humana) y cambia la métrica (ya no basta el F1 de \(f\); importa el F1 de \(f\)más el override).
Ignorarlo produce tableros que celebran un modelo que en la práctica nadie usa, o un modelo que se usa en contra de su umbral.
scored["override"] = np.where(scored["monto"] >=100, "revisar", "auto")print(scored.groupby("override")["yhat"].agg(["count", "mean"]).rename(columns={"mean": "tasa_yhat"}))print("La cola 'revisar' es una etapa. Tiene dueño. No es un print.")
count tasa_yhat
override
auto 8 0.5
revisar 4 1.0
La cola 'revisar' es una etapa. Tiene dueño. No es un print.
12.1 Sesgo entrenamiento–servicio, visto como arquitectura
No es un bug de scikit-learn. Es dos construcciones de \(x\) que no comparten receta: el batch de entrenamiento aplica un CAST y un filtro; el camino online no. El síntoma es un F1 que “se cae al desplegar”. El arreglo no es reentrenar: es una sola función \(T\) (sesión 5) invocada en ambos caminos.
def T_train(df):return df.loc[df["canal"] !="tienda", ["monto"]]def T_servicio(df):return df[["monto"]] # olvidó el filtroprint("n train :", len(T_train(foto)))print("n serv. :", len(T_servicio(foto)))print("mismas columnas, distinta población: el sistema tiene dos T.")
n train : 8
n serv. : 12
mismas columnas, distinta población: el sistema tiene dos T.
12.2 Contrato de tipos, no solo de nombres
Un rename se caza con un KeyError. Un monto que llega como texto ("1.200") pasa el contrato de nombres y rompe la regla numérica —o, peor, se interpreta mal. El manifiesto debe listar tipos, no solo etiquetas. La sesión 2 lo implementa al extraer; aquí se deja el hueco visible.
contrato_tipos = {"evento_id": "string", "monto": "int64", "canal": "string"}foto_mala = foto.copy()foto_mala["monto"] = foto_mala["monto"].astype(str) +".0"rotos = {c: str(foto_mala[c].dtype) for c, t in contrato_tipos.items()if c in foto_mala and t notinstr(foto_mala[c].dtype)}print("tipos esperados:", contrato_tipos)print("desajustes :", rotos or"ninguno")
Si \(\hat{y}\) se publica sin un kill switch (volver al lote anterior, o a una regla), el primer incidente se gestiona por WhatsApp. No hace falta Kubernetes: hace falta un puntero a la versión vigente y una forma de moverlo. La sesión 8 construye el registro; aquí se nombra el requisito.
producción apunta a: regla_umbral_60
tras rollback : regla_umbral_50
12.4 Registro de predicciones frente a monitoreo
Escribir predicciones.csv es registro. Comparar esa foto con una referencia y encender un semáforo es monitoreo (sesión 12). Quien tiene lo primero y llama a eso “ya monitoreamos” no tiene alerta: tiene un archivo que nadie mira.
print("registro :", (RUTA /"predicciones.csv").exists())print("monitor :", (RUTA /"semaforo.json").exists(), "← no existe, y no debe existir aún")
registro : True
monitor : False ← no existe, y no debe existir aún
12.5 Enrutamiento: más de una \(f\) no es “ya somos microservicios”
Un umbral para app y otro para tienda es política, no topología. Puede vivir en una tabla de reglas. Convertirlo en dos servicios solo se justifica si tienen cadencias o dueños distintos. Mientras tanto, es un if versionado.
El primer día no hay histórico de predicciones, no hay semáforo y a veces no hay etiqueta. Un sistema que exige tres meses de monitor para arrancar no arranca. El mapa debe admitir un modo degradado: regla + registro, y el resto de capas se encienden cuando hay con qué compararlos.
modo ="degradado"ifnot (RUTA /"semaforo.json").exists() else"pleno"print("modo de arranque:", modo)print("obligatorio en frío: crudo, foto, predicciones, manifiesto")print("aplazable : semáforo, registro de experimentos, imagen")
modo de arranque: degradado
obligatorio en frío: crudo, foto, predicciones, manifiesto
aplazable : semáforo, registro de experimentos, imagen
13) Errores frecuentes
Llamar “producción” a un notebook que corre en el portátil del autor.
Versionar \(f\) y no el esquema ni el umbral.
Medir el éxito del sistema solo con F1 de un holdout de hace tres meses.
Elegir online porque “suena a tiempo real” cuando el lote diario basta.
Partir en servicios antes de tener un contrato de columnas.
Borrar el crudo para “dejar limpio el proyecto”.
Diagnosticar como overfitting un cambio de captura.
14) Checklist
¿Hay manifiesto con esquema, umbral, \(n\) y hora?
¿El pipeline de datos y el de modelo son dos funciones?
¿Se puede trazar un evento_id hasta el crudo?
¿Está escrito el modo (batch / online) y su justificación?
¿Cada etapa del ciclo tiene artefacto o una razón explícita para no tenerlo todavía?
¿Se nombró dueño de la métrica de decisión?
15) Conclusiones
Un sistema de IA es una composición de contratos, no un algoritmo con un logo. Esta sesión dejó un mapa —ciclo de vida, dos pipelines, dos topologías, tres cadencias— y un contraejemplo operable: crudo, foto, predicciones, manifiesto. Las sesiones 2 y 3 bajan a la primera flecha (\(E\) y la calidad de \(x\)). Quien avance al modelo sin esos contratos estará, otra vez, en el diálogo del inicio: el notebook corre.
16) Taller
A. Añada al manifiesto el hash de foto.csv. ¿Cambia el hash si reordena las filas? ¿Debería?
B. Cambie el nombre monto → importe solo en el crudo. ¿En qué función debe fallar? ¿En el monolito fallaría en silencio?
C. Justifique en cinco líneas si el problema de su dominio pide batch u online. La latencia de \(f\) no es un argumento suficiente.
D. Señale, en la tabla de la sección 9, qué sesión firmaría el artefacto que usted no tiene hoy en un proyecto real.
E. Introduzca un override humano en tres filas y calcule la tasa de \(\hat{y}\)solo en la parte automática. Documente ambos números.
print("Artefactos de la sesión 1:")for p insorted(RUTA.iterdir()):print(f" {p.name:20s}{p.stat().st_size:6d} B")print("Taller A–E. Si la respuesta no nombra una capa, vuelva a la sección 9.")
Artefactos de la sesión 1:
crudo.csv 506 B
foto.csv 444 B
manifiesto.json 274 B
predicciones.csv 764 B
Taller A–E. Si la respuesta no nombra una capa, vuelva a la sección 9.
💬 ¿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.