Serie MLOps y Producción · de la ingeniería de datos al monitoreo.
El problema que vas a resolver
Laura, del equipo de operaciones de la empresa de mensajería, lleva varias semanas recibiendo modelos para detectar envíos que podrían llegar tarde. El trabajo ya puede ejecutarse mediante las tareas del capítulo anterior. Ahora hay muchas carpetas, archivos y resultados. Laura pregunta cuál está usando, con qué datos se entrenó y si la propuesta nueva mejora la anterior.
Hoy tendrías que responder buscando correos y abriendo carpetas una por una. Vas a construir algo mejor: cada ejecución quedará registrada con sus datos, su procedimiento, sus ajustes y el archivo concreto que produjo. Así cada pregunta de Laura se contesta consultando un registro.
Ficha
En este capítulo
Pregunta
¿Cómo identificas, comparas y eliges versiones de los modelos de Laura?
Datos
Los mismos envíos sintéticos del capítulo 5, con su resultado de puntualidad
Herramientas
Python, pandas, NumPy, scikit-learn, joblib y matplotlib
Lo que produces
Fichas de corridas, comparación de validación, evaluación final y registro local de modelos
Objetivos de aprendizaje
Al terminar este capítulo podrás:
Usar la huella de un archivo para saber si dos tablas son realmente iguales.
Identificar cada corrida por sus datos, su procedimiento y el modelo que produjo.
Rechazar comparaciones entre corridas medidas en condiciones distintas.
Conservar las corridas terminadas sin sobrescribirlas.
Promover un modelo con una regla escrita y explicar por qué «latest» no basta.
Seguir el linaje desde la alerta de Laura hasta los datos que la produjeron.
Primero compararás tablas diminutas que cambian en un dígito. Después identificarás el conjunto de envíos y el procedimiento que ejecutas. Registrarás varias configuraciones sobre el mismo desarrollo, las compararás y evaluarás solamente la elegida en prueba. Finalmente actualizarás un registro local de demostración y usarás su referencia para predecir un envío de Laura.
TipLo que necesitas recordar
Concepto
En una línea
Dónde se explica
Huella SHA-256
Identifica el contenido de un archivo a partir de sus bytes.
Tienes tres envíos, A, B y C, con distancias de 100, 200 y 300 kilómetros. Una corrección cambia la distancia de B a 201. Puedes identificar la celda modificada a mano. El nombre del archivo sigue siendo envios.csv y la cifra sigue ocupando tres dígitos.
Guarda la primera tabla en la carpeta a y la corregida en b. Guarda además una copia exacta de la primera con otro nombre. Antes de ejecutar, anota qué esperas: las tablas de a y b tienen diferente contenido aunque compartan nombre y tamaño; la copia tiene otro nombre, pero conserva el contenido de a.
Dos archivos con igual nombre y tamaño contienen distancias distintas; una copia con otro nombre conserva la identidad de contenido.
El cálculo retoma la huella del capítulo 5. Vas a aplicarla a los bytes efectivamente escritos. Esos bytes incluyen nombres de columnas y separadores. En un archivo grande puedes mantener el mismo hábito sin inspeccionar cada celda a mano.
ESPACIO = ARTEFACTOS /"experimento-envios"shutil.rmtree(ESPACIO, ignore_errors=True) # cada ejecución del notebook parte de ceroESPACIO.mkdir()tabla_a = pd.DataFrame({"envio": ["A", "B", "C"], "distancia_km": [100, 200, 300]})tabla_b = tabla_a.copy()tabla_b.loc[1, "distancia_km"] =201rutas_juguete = [ESPACIO /"a/envios.csv", ESPACIO /"b/envios.csv", ESPACIO /"copia/otro.csv"]filas_juguete = []for ruta, tabla inzip(rutas_juguete, [tabla_a, tabla_b, tabla_a]): ruta.parent.mkdir(exist_ok=True) tabla.to_csv(ruta, index=False) filas_juguete.append({"ruta": ruta.relative_to(ESPACIO).as_posix(),"bytes": ruta.stat().st_size,"sha256": hashlib.sha256(ruta.read_bytes()).hexdigest()})display(tabla_a)display(tabla_b)display(pd.DataFrame(filas_juguete))print("A y B: mismo tamaño:", filas_juguete[0]["bytes"] == filas_juguete[1]["bytes"])print("A y B: misma huella:", filas_juguete[0]["sha256"] == filas_juguete[1]["sha256"])print("A y copia: misma huella:", filas_juguete[0]["sha256"] == filas_juguete[2]["sha256"])
envio
distancia_km
0
A
100
1
B
200
2
C
300
envio
distancia_km
0
A
100
1
B
201
2
C
300
ruta
bytes
sha256
0
a/envios.csv
37
2f62405b663353cdb795e5460cdf28b4f4c8a170acc2f7...
1
b/envios.csv
37
0e9010edcaa4a82c7c739cfc47bc3314a3f02a86414ddb...
2
copia/otro.csv
37
2f62405b663353cdb795e5460cdf28b4f4c8a170acc2f7...
A y B: mismo tamaño: True
A y B: misma huella: False
A y copia: misma huella: True
Los 3 archivos tienen 37 bytes. A y B comparten el nombre envios.csv, pero el cambio de 200 a 201 produce huellas diferentes: la comparación devuelve False. La copia otro.csv conserva la huella de A y devuelve True, aunque cambie el nombre. Puedes verificar el único dígito distinto en las 2 tablas mostradas.
El tamaño dice cuántos bytes hay, la ruta dice dónde están y la huella dice si son exactamente los que esperabas. Por eso la ficha guardará la ruta y la huella: una no reemplaza a la otra.
La huella que calculamos identifica el archivo exacto. Si cambias el orden de las filas o cómo representas los decimales, sus bytes pueden cambiar aunque estés describiendo los mismos envíos. Para este experimento conservaremos el CSV original y su orden. Eso hace explícita la unidad que estamos identificando.
2) Nombrar datos, procedimiento y modelo
NotaVersionamiento y corrida
El versionamiento consiste en guardar cada versión de algo que cambia (datos, código o modelos) con un nombre que la distinga y la información necesaria para recuperarla. Una corrida es una ejecución identificada de un procedimiento sobre datos concretos, con sus ajustes y resultados. Aquí cada corrida produce un pipeline y una ficha propia.
Piensa en una receta preparada varias veces. Necesitas saber qué ingredientes se usaron, qué instrucciones se siguieron y qué preparación terminó en el recipiente que vas a servir. En el modelo, esas preguntas corresponden a los datos, al procedimiento y al archivo ajustado. Conservar solamente la receta deja sin identificar los otros dos elementos.
Una corrida relaciona datos, procedimiento y modelo.
Identidad
Qué conservarás
Pregunta que permite responder
Datos
CSV y huella completa
¿Qué filas y valores recibió el experimento?
Procedimiento
Código, bibliotecas, semilla y descripción de la medición
¿Qué se hizo y cómo se evaluó?
Modelo
Pipeline ajustado, ruta y huella
¿Qué archivo concreto genera esta predicción?
Una nueva configuración produce otra corrida aunque los datos sean iguales. Es precisamente lo que quieres comparar. La semilla, la separación y los pliegues también quedan registrados, porque determinan qué ejemplos intervienen en cada medición. Así puedes distinguir cambiar un ajuste de cambiar el examen.
Volver al mismo historial de envíos
El capítulo puede ejecutarse solo. Genera otra vez los envíos con el mecanismo y la semilla del capítulo 5 y escribe un CSV. A partir de ese momento lees el archivo conservado para entrenar: la huella registrada corresponde a la fuente que realmente utilizas.
El historial vuelve a contener 600 envíos, 67 pesos faltantes y 24.3 % de tardíos. La separación deja 450 filas de desarrollo y 150 reservadas para prueba. Cada configuración usará esas mismas posiciones, en vez de volver a sortear un examen distinto.
La partición se mantiene mientras comparas configuraciones. Guardarás incluso los índices de desarrollo, prueba y pliegues. En este CSV, el índice corresponde a la posición de cada fila conservada. Si alguien reordena la fuente, cambia la huella y ya tienes una señal de que debe revisar la comparación.
Dejar disponible el procedimiento
NotaProtocolo de comparación
Un protocolo de comparación describe las condiciones que mantienes iguales al medir candidatos: datos, partición, pliegues, métrica, categoría positiva y umbral. Los hiperparámetros de cada candidato se registran aparte. Esto permite cambiar el ajuste conservando la misma manera de medir.
Usaremos el archivo pequeño procedimiento_envios.py, ubicado junto al notebook. Contiene el pipeline y la función que calcula F1 en validación cruzada antes de ajustarlo con todo desarrollo. Su código completo aparece al final del capítulo. El experimento conserva una copia exacta del archivo y su huella.
El JSON del protocolo añade el contexto de ejecución que el archivo recibe: columnas, semilla, versiones y las posiciones exactas de cada partición. La huella del código identifica el archivo; el protocolo identifica la medición. Si cambias alguno, el registro puede mostrar qué parte cambió.
# El módulo junto al notebook contiene el código completo que vamos a ejecutar.from procedimiento_envios import ajustar_configuraciondef huella(ruta):"""Recibe una ruta existente y devuelve el SHA-256 completo de sus bytes."""return hashlib.sha256(ruta.read_bytes()).hexdigest()def escribir_json_nuevo(ruta, contenido):"""Recibe ruta y objeto JSON; crea el archivo sin sobrescribir otro."""with ruta.open("x", encoding="utf-8") as archivo: json.dump(contenido, archivo, indent=2, ensure_ascii=False)def leer_json(ruta):"""Recibe una ruta JSON y devuelve el objeto que contiene."""return json.loads(ruta.read_text(encoding="utf-8"))codigo = Path("procedimiento_envios.py").read_bytes()ruta_codigo = ESPACIO /"procedimiento_envios.py"_ = ruta_codigo.write_bytes(codigo)protocolo = {"version": "comparacion-envios-v1", "semilla": SEMILLA,"datos": huella(RUTA_DATOS), "codigo": huella(ruta_codigo),"columnas": X.columns.to_list(), "objetivo": "llega_tarde","indices_desarrollo": X_ent.index.to_list(), "indices_prueba": X_pru.index.to_list(),"pliegues": [{"ajuste": ajuste.tolist(), "validacion": validacion.tolist()}for ajuste, validacion in cv.split(X_ent, y_ent)],"metrica": "f1", "positivo": 1, "umbral_comparacion": 0.50,"versiones": versiones,}RUTA_PROTOCOLO = ESPACIO /"protocolo.json"escribir_json_nuevo(RUTA_PROTOCOLO, protocolo)print("Huella de datos:", huella(RUTA_DATOS))print("Huella de código:", huella(ruta_codigo))print("Huella de protocolo:", huella(RUTA_PROTOCOLO))
Huella de datos: e30b9e443e968d0f01c04535685598a204bacc2ca0e6114d2a939f36dcb156e8
Huella de código: 6624ee23e2b133d138a8512201943f3acdea229937633225cd454aa1863a2741
Huella de protocolo: 62e1892f2736c6c7ce62e19e1c0dbca48d8b42b5e695af670ceb98c1a8badf74
La salida muestra 3 huellas completas: datos, código y protocolo. La de datos comienza con e30b9e443e96, igual que la fuente conservada en el capítulo 7. Cada identificador responde a una comprobación distinta: recuperar el mismo CSV, el mismo archivo o la misma receta de evaluación.
Aquí el código de preparación y ajuste está en el archivo, mientras el notebook organiza los registros y la selección final. Guardar el archivo y la receta hace visible esa separación. Para reproducir el recorrido completo también conservas el notebook y sus salidas, donde está escrita la política posterior de umbral y promoción.
3) Convertir los resultados en una tabla de corridas
NotaRegistro de corridas e inmutabilidad
El registro de corridas es una colección de fichas que puedes consultar como una tabla: cada fila representa una corrida. La inmutabilidad significa que una ficha completada se conserva como quedó. Si necesitas corregir el procedimiento o un resultado, creas otra corrida que deja identificable el cambio.
El capítulo 5 guardó un registro_corrida.json. Ahora amplías esa idea para varias ejecuciones. Cada carpeta contendrá el pipeline ajustado y su ficha. Los datos y el procedimiento se conservan una vez en el espacio del experimento, y cada ficha apunta a ellos con sus huellas.
Antes de entrenar, el registro exige un identificador nuevo. Así evita que una llamada repetida sustituya silenciosamente el modelo de una corrida anterior. La tabla se construye a partir de fichas terminadas; una carpeta que quedara incompleta tras una interrupción necesitaría investigarse antes de considerarla una corrida registrada.
Tres configuraciones, una forma de medir
La primera configuración es la base del capítulo 5. La segunda retoma los ajustes elegidos en el capítulo 6: C igual a 10, pesos equilibrados y mediana. La tercera prueba una regularización mayor con C igual a 0.01, pesos equilibrados y media. El propósito aquí es registrar configuraciones conocidas, conservando un ejemplo pequeño que puedas seguir.
Las tres reciben los mismos pliegues y se comparan mediante F1 con el corte predeterminado de 0.50. La elección de un umbral para el candidato ganador vendrá después. Mantener separadas estas dos decisiones permite leer qué significa cada métrica de la ficha.
CORRIDAS = ESPACIO /"corridas"CORRIDAS.mkdir()def registrar_corrida(identificador, configuracion):"""Recibe ID y ajustes; guarda una corrida nueva y devuelve su ficha.""" carpeta = CORRIDAS / identificadorif carpeta.exists():raiseValueError("El identificador ya existe: crea otra corrida") carpeta.mkdir() modelo, puntajes = ajustar_configuracion(X_ent, y_ent, configuracion, cv) ruta_modelo = carpeta /"modelo.joblib" _ = joblib.dump(modelo, ruta_modelo) registro = {"corrida": identificador, "fecha_utc": datetime.now(timezone.utc).isoformat(),"datos": RUTA_DATOS.as_posix(), "huella_datos": huella(RUTA_DATOS),"codigo": ruta_codigo.as_posix(), "huella_codigo": huella(ruta_codigo),"protocolo": RUTA_PROTOCOLO.as_posix(),"huella_protocolo": huella(RUTA_PROTOCOLO),"configuracion": configuracion, "f1_pliegues": puntajes.tolist(),"f1_cv": float(puntajes.mean()), "desviacion_cv": float(puntajes.std()),"modelo": ruta_modelo.as_posix(), "huella_modelo": huella(ruta_modelo),"desarrollo": len(X_ent), "prueba_reservada": len(X_pru), "versiones": versiones, } escribir_json_nuevo(carpeta /"registro_corrida.json", registro)return registroconfiguraciones = [ ("corrida_01", {"modelo__C": 1.0, "modelo__class_weight": None,"preparar__numericas__rellenar__strategy": "mean"}), ("corrida_02", {"modelo__C": 10.0, "modelo__class_weight": "balanced","preparar__numericas__rellenar__strategy": "median"}), ("corrida_03", {"modelo__C": 0.01, "modelo__class_weight": "balanced","preparar__numericas__rellenar__strategy": "mean"}),]registros = [registrar_corrida(nombre, ajustes) for nombre, ajustes in configuraciones]tablero = pd.DataFrame(registros)display(tablero[["corrida", "f1_cv", "desviacion_cv", "desarrollo", "prueba_reservada"]])display(pd.DataFrame([{"corrida": r["corrida"], **r["configuracion"]} for r in registros]))
corrida
f1_cv
desviacion_cv
desarrollo
prueba_reservada
0
corrida_01
0.384560
0.058823
450
150
1
corrida_02
0.518076
0.044456
450
150
2
corrida_03
0.501623
0.033360
450
150
corrida
modelo__C
modelo__class_weight
preparar__numericas__rellenar__strategy
0
corrida_01
1.00
NaN
mean
1
corrida_02
10.00
balanced
median
2
corrida_03
0.01
balanced
mean
La tabla registra 3 corridas. La 01 obtiene F1 de validación de 0.384560; la 02, 0.518076; y la 03, 0.501623. Sus desviaciones entre pliegues son 0.058823, 0.044456 y 0.033360, respectivamente. Todas usan 450 filas de desarrollo y conservan 150 para prueba. La configuración 02 encabeza esta comparación y la 03 queda por debajo pese a haberse creado después.
Cada F1 de la tabla es un promedio de validación. El pipeline guardado se ajustó después con todo desarrollo, siguiendo la misma configuración. La ficha guarda tanto el promedio como los valores de cada pliegue; puedes volver a observar la variación en lugar de conservar solamente la cifra que más te gustó.
La columna prueba reservada informa qué parte quedó fuera. Las corridas todavía carecen de una métrica final de prueba porque ese conjunto no intervino en la comparación. El archivo del modelo existe para cada configuración, pero la disponibilidad de un archivo y la aceptación operativa de una versión se registran en lugares diferentes.
Leer una fila sin abrir el modelo
Una ficha permite localizar datos, código, protocolo y pipeline. También conserva versiones y ajustes. Puedes contestar qué se usó sin cargar un objeto de scikit-learn. Si quieres comprobar que encontraste el archivo correcto, comparas su huella con la guardada. Si quieres generar predicciones, cargas después el pipeline correspondiente.
Esto ayuda al trabajo en equipo: Laura puede citar un identificador y quien investiga puede encontrar una fila concreta. Un mensaje como «revisa corrida_02» tiene un significado estable dentro de este experimento. Si necesitas transportar la carpeta, conserva su estructura y sus rutas relativas para mantener esos enlaces.
4) Comparar solamente bajo las mismas condiciones
La comparación comprueba los datos y la medición antes de calcular diferencias.
La primera barrera es la huella de los datos. Si comparas modelos evaluados sobre envíos diferentes, una cifra mayor puede corresponder a un conjunto más sencillo. El segundo control es el protocolo: dos corridas sobre el mismo CSV pueden reservar otras filas o aplicar otro corte. Entonces también cambió lo que representa el F1.
La función comparar exige ambas coincidencias antes de restar. Vas a provocar dos rechazos. En el primero corriges una distancia y construyes una ficha ficticia con esa nueva huella. En el segundo mantienes los datos, pero alteras el identificador del protocolo. Son pruebas de la barrera de comparación, sin entrenamientos adicionales ni métricas atribuidas a esos ejemplos ficticios.
def comparar(actual, candidata):"""Recibe dos fichas comparables y devuelve su diferencia de F1 CV."""if actual["huella_datos"] != candidata["huella_datos"]:raiseValueError("Comparación rechazada: las corridas usan datos distintos")if actual["huella_protocolo"] != candidata["huella_protocolo"]:raiseValueError("Comparación rechazada: cambió la forma de medir")return candidata["f1_cv"] - actual["f1_cv"]delta = comparar(registros[0], registros[1])print("Diferencia F1 CV, 02 menos 01:", round(delta, 6))datos_cambiados = datos_leidos.copy()datos_cambiados.loc[0, "distancia_km"] +=0.1ruta_cambiada = ESPACIO /"envios_corregidos.csv"datos_cambiados.to_csv(ruta_cambiada, index=False)# Ficha ficticia para probar la barrera; no constituye una corrida entrenada.ficha_tramposa = {**registros[1], "huella_datos": huella(ruta_cambiada)}try: _ = comparar(registros[0], ficha_tramposa)exceptValueErroras error:print("ValueError:", str(error).splitlines()[0])ficha_otro_corte = {**registros[1], "huella_protocolo": "protocolo-alterado"}try: _ = comparar(registros[0], ficha_otro_corte)exceptValueErroras error:print("ValueError:", str(error).splitlines()[0])
Diferencia F1 CV, 02 menos 01: 0.133516
ValueError: Comparación rechazada: las corridas usan datos distintos
ValueError: Comparación rechazada: cambió la forma de medir
La diferencia válida de F1 de validación, corrida 02 menos corrida 01, es 0.133516. El primer intento alterado devuelve ValueError: Comparación rechazada: las corridas usan datos distintos. El segundo devuelve ValueError: Comparación rechazada: cambió la forma de medir. La función rechaza ambos antes de devolver una diferencia.
La diferencia válida describe lo observado bajo un protocolo común. Puedes usarla dentro de una regla declarada. Interpretarla como una mejora estable requiere más evidencia que este conjunto sintético; reuniremos los límites de la demostración al final del recorrido.
También puedes registrar corridas sobre datos nuevos. El control exige formar otra comparación apropiada: por ejemplo, evaluar los candidatos sobre un conjunto nuevo compartido y documentar ese experimento. Conservar una historia de conjuntos distintos es útil; mezclarlos automáticamente en una sola clasificación por F1 cambia la pregunta que estás respondiendo.
Ver los pliegues detrás del promedio
La gráfica conserva cada valor de validación. Los candidatos se enfrentan al mismo turno reservado en cada posición del eje horizontal. Mira si la diferencia es parecida entre turnos o si depende especialmente de uno. La tabla del registro y la gráfica muestran dos vistas del mismo resultado.
Figura 8.1: Tres configuraciones medidas en los mismos pliegues
Los 5 valores de cada línea corresponden a los mismos pliegues. La corrida 02 alcanza el mayor promedio, 0.518076, mientras la 03 tiene menor desviación entre turnos, 0.033360. Esas 2 características responden a preguntas distintas: nivel medio de F1 y variación observada de la medición.
5) Conservar la ficha cuando llega una corrección
Una corrida documenta una ejecución terminada. Si cambias su configuración dentro del JSON, la ficha dejaría de describir el archivo que produjo. Si corriges el F1 sin registrar por qué, tampoco podrías reconstruir cómo se obtuvo la primera cifra. Por eso el cambio se representa con otra identidad.
Prueba ahora a registrar otra configuración bajo corrida_01. La función comprobará la existencia del identificador antes de ajustar. Después compararás la huella de la ficha original para confirmar que el rechazo ocurrió sin modificarla.
huellas_registros = {r["corrida"]: huella( CORRIDAS / r["corrida"] /"registro_corrida.json") for r in registros}try: _ = registrar_corrida("corrida_01", configuraciones[1][1])exceptValueErroras error:print("ValueError:", str(error).splitlines()[0])print("Corridas completas:", len(list(CORRIDAS.glob("*/registro_corrida.json"))))print("Ficha 01 intacta:", huellas_registros["corrida_01"] == huella( CORRIDAS /"corrida_01/registro_corrida.json"))
ValueError: El identificador ya existe: crea otra corrida
Corridas completas: 3
Ficha 01 intacta: True
El intento repetido devuelve ValueError: El identificador ya existe: crea otra corrida. Siguen existiendo 3 fichas completas y la comparación de la ficha 01 devuelve True. El registro conserva la configuración original sin ejecutar un ajuste que la sustituya.
Esta protección corresponde a nuestra función de registro. En un equipo puedes reforzarla mediante permisos y almacenamiento con historial. Aquí el comportamiento es visible y pequeño: la ruta ya ocupada detiene el intento. Es una forma de practicar la decisión de crear un registro nuevo cuando cambió el experimento.
La información nueva relacionada con una corrida también puede vivir en otro documento que cite su identificador. Harás eso con la evaluación final: añadirá una observación posterior sobre el ganador sin reescribir las fichas usadas para compararlo. Así conservarás lo que sabías durante la selección y lo que mediste después.
6) Fijar la decisión y abrir prueba una sola vez
Elige el mayor F1 de validación entre las configuraciones registradas. Después repite la política de umbral del capítulo 6 usando predicciones de desarrollo obtenidas fuera de pliegue. El objetivo interno pide sensibilidad de al menos 0.70; entre los cortes que cumplen, eliges el mayor F1 y resuelves empates con el corte más alto.
La regla de promoción se declara antes de seleccionar y medir. Exigirá una mejora de F1 de validación de al menos 0.02 respecto de la base y un F1 de validación de al menos 0.50. Además, el candidato elegido deberá alcanzar F1 de prueba de al menos 0.50 y sensibilidad de prueba de al menos 0.60. Son valores ilustrativos para la entrega local, con la misma aceptación del capítulo 7.
Estos límites representan decisiones diferentes. La mejora de validación justifica considerar el cambio de configuración; la prueba observa el candidato fijado; el umbral describe cómo se emitirán las alertas. Escribirlos por separado ayuda a evitar que el significado de «mejor» cambie mientras miras resultados.
# Criterios fijados antes de elegir y de abrir la prueba reservada.REGLA = {"mejora_cv_minima": 0.02, "f1_cv_minimo": 0.50,"f1_prueba_minimo": 0.50, "sensibilidad_prueba_minima": 0.60}ganadora =max(registros, key=lambda registro: registro["f1_cv"])modelo_ganador = joblib.load(ganadora["modelo"])prob_fuera = cross_val_predict( modelo_ganador, X_ent, y_ent, cv=cv, method="predict_proba")[:, 1]opciones = []for corte in np.arange(0.15, 0.751, 0.05): corte =round(float(corte), 2) prediccion = (prob_fuera >= corte).astype(int) opciones.append({"umbral": corte, "f1": f1_score(y_ent, prediccion),"sensibilidad": recall_score(y_ent, prediccion)})aceptables = pd.DataFrame(opciones).query("sensibilidad >= 0.70")if aceptables.empty:raiseValueError("No hay umbral admisible en desarrollo")elegida = aceptables.sort_values(["f1", "umbral"], ascending=False).iloc[0]umbral =float(elegida.umbral)# Única predicción de prueba; todas las métricas usan este mismo vector.prob_prueba = modelo_ganador.predict_proba(X_pru)[:, 1]prediccion = (prob_prueba >= umbral).astype(int)evaluacion = {"corrida": ganadora["corrida"], "huella_modelo": ganadora["huella_modelo"],"huella_datos": ganadora["huella_datos"], "umbral": umbral,"politica_umbral": "OOF: sensibilidad >= 0.70; mayor F1; empate: mayor corte","f1_prueba": f1_score(y_pru, prediccion),"sensibilidad_prueba": recall_score(y_pru, prediccion),"precision_prueba": precision_score(y_pru, prediccion),"regla": REGLA,}RUTA_EVALUACION = ESPACIO /"evaluacion_final.json"escribir_json_nuevo(RUTA_EVALUACION, evaluacion)display(pd.DataFrame([evaluacion]).drop(columns=["huella_modelo", "huella_datos"]))print("Regla de promoción:", REGLA)
La evaluación cita corrida_02 y el umbral de 0.45. Obtiene F1 de prueba de 0.527473, sensibilidad de 0.666667 y precisión de 0.436364. La mejora previa de validación fue 0.133516 y supera el mínimo de 0.02; el F1 de validación supera 0.50. Las métricas finales también superan sus mínimos de 0.50 y 0.60. La sensibilidad de prueba queda por debajo del objetivo interno de 0.70 que se usó para elegir el corte con desarrollo, igual que en el capítulo anterior.
El documento evaluacion_final.json cita la corrida, el modelo, los datos y el umbral. Las fichas previas conservan su F1 de comparación con corte de 0.50. La evaluación final se refiere a la política seleccionada con desarrollo. Al consultar una cifra, puedes distinguir qué decisión y qué conjunto la produjeron.
Si el candidato quedara retenido, conservarías esa evaluación y diseñarías otro experimento. Consultar la misma prueba repetidamente para ajustar convertiría la medición final en parte de la selección que ya estudiaste. La organización de archivos acompaña esa separación estadística.
7) Mantener un registro de modelos con una regla de promoción
NotaRegistro de modelos y promoción
Un registro de modelos, o registry, relaciona versiones identificadas con su estado de uso. Una promoción cambia la referencia elegida después de comprobar criterios escritos. Usaremos tres etiquetas locales: candidato, en producción y retirado. En este notebook describen una simulación en disco; el servicio real se construirá en otro capítulo.
La ficha de una corrida cuenta cómo se produjo un resultado. El registro de modelos responde cuál representa la entrega actual. En pocas palabras: las fichas no cambian nunca; la referencia de cuál se usa sí puede cambiar, y cada cambio queda anotado.
Imita una situación inicial en la que la configuración base representa una entrega anterior para Laura. La marcarás «en producción» dentro del registro de demostración. Esta condición inicial se declara como simulada; no ejecuta un servicio ni certifica una nueva evaluación de esa base. El candidato elegido tendrá que superar la regla antes de sustituir esa referencia local.
La promoción retira la referencia anterior y selecciona la nueva conservando las corridas.
La función comprueba que los datos y el protocolo coinciden con los de la referencia anterior. Revisa que la evaluación corresponde a la corrida y a sus huellas. También vuelve a calcular las huellas de los archivos de la candidata. Solamente después aplica la regla de mejora y aceptación, cambia las etapas y guarda una entrada en la historia.
REGISTRO = ESPACIO /"registro_modelos.json"# Situación inicial simulada: la configuración base representa la entrega anterior.registro_modelos = {"actual": "corrida_01", "simulacion_local": True,"etapas": {"corrida_01": "en producción", "corrida_02": "candidato","corrida_03": "candidato"},"historia": [{"accion": "inicio simulado", "corrida": "corrida_01"}],}escribir_json_nuevo(REGISTRO, registro_modelos)def promover(candidata, ruta_evaluacion):"""Recibe ficha y evaluación; verifica criterios y actualiza el registro local.""" registro = leer_json(REGISTRO) actual =next(r for r in registros if r["corrida"] == registro["actual"]) mejora = comparar(actual, candidata) comprobacion = leer_json(ruta_evaluacion)if comprobacion["corrida"] != candidata["corrida"]:raiseValueError("La evaluación pertenece a otra corrida")if comprobacion["huella_modelo"] != candidata["huella_modelo"]:raiseValueError("La evaluación pertenece a otro modelo")if comprobacion["huella_datos"] != candidata["huella_datos"]:raiseValueError("La evaluación pertenece a otros datos")for campo in ["datos", "codigo", "protocolo", "modelo"]:if huella(Path(candidata[campo])) != candidata["huella_"+ campo]:raiseValueError("La integridad del artefacto cambió: "+ campo) cumple = ( mejora >= REGLA["mejora_cv_minima"]and candidata["f1_cv"] >= REGLA["f1_cv_minimo"]and comprobacion["f1_prueba"] >= REGLA["f1_prueba_minimo"]and comprobacion["sensibilidad_prueba"] >= REGLA["sensibilidad_prueba_minima"] )ifnot cumple:raiseValueError("Promoción rechazada: no se cumplen los criterios registrados") registro["etapas"][registro["actual"]] ="retirado" registro["etapas"][candidata["corrida"]] ="en producción" registro["actual"] = candidata["corrida"] registro["evaluacion"] = ruta_evaluacion.as_posix() registro["huella_evaluacion"] = huella(ruta_evaluacion) registro["historia"].append({"accion": "promocion local", "corrida": candidata["corrida"],"mejora_cv": mejora, "regla": REGLA}) provisional = REGISTRO.with_suffix(".tmp") escribir_json_nuevo(provisional, registro) _ = provisional.replace(REGISTRO)return registroregistro_final = promover(ganadora, RUTA_EVALUACION)display(pd.DataFrame({"corrida": registro_final["etapas"].keys(),"etapa": registro_final["etapas"].values()}))print("Referencia actual:", registro_final["actual"])print("Movimientos de historia:", len(registro_final["historia"]))
corrida
etapa
0
corrida_01
retirado
1
corrida_02
en producción
2
corrida_03
candidato
Referencia actual: corrida_02
Movimientos de historia: 2
El registro deja corrida_01 como retirado, corrida_02 como en producción y corrida_03 como candidato. La referencia actual es corrida_02. La historia contiene 2 movimientos: el inicio simulado y la promoción local. Las etiquetas describen el registro de demostración y todos los archivos siguen en su carpeta.
Retirado significa que esa corrida dejó de representar la referencia actual. Su carpeta permanece disponible para investigar y comparar. La historia conserva el origen simulado y el movimiento de promoción con su regla. Puedes reconstruir por qué cambió la referencia sin modificar los experimentos que la sustentan.
Por qué «latest» deja preguntas abiertas
Una etiqueta latest significa «la última», pero necesita precisar última según qué: creación, copia, modificación o aprobación. La corrida creada más recientemente puede tener un F1 menor. Si el sistema carga lo último que encuentra por nombre, el criterio de selección queda sustituido por un accidente del orden de los archivos.
Un alias es una referencia con nombre que apunta a una versión concreta y puede cambiar de destino. Puede resultar útil si guardas a qué versión apuntaba y por qué cambió. Aquí la referencia actual cita un identificador y la historia explica su actualización. Para reproducir una predicción necesitas resolver esa referencia y conservar la versión resultante.
8) Seguir el linaje hasta la predicción de Laura
NotaLinaje
El linaje es la relación que permite recorrer cómo se produjo y eligió un resultado: datos, procedimiento, corrida, modelo y decisión de uso. Cada enlace conserva identificadores que te permiten seguir la cadena tanto hacia adelante como hacia atrás.
El registro actual puede rastrearse hasta los datos y el procedimiento originales.
Comienza por actual. Busca su ficha, localiza la evaluación final y verifica las huellas antes de cargar el pipeline. Luego utiliza el umbral guardado para decidir la alerta. Es el mismo envío de Laura de los capítulos anteriores: distancia de 520 kilómetros, peso sin registrar y servicio económico.
Última corrida creada: corrida_03 F1 CV: 0.501623
Corrida elegida: corrida_02 F1 CV: 0.518076
Fichas originales intactas: True
La consulta resuelve corrida_02. Para el envío de Laura obtiene una probabilidad de 0.762617, usa el umbral 0.45 y devuelve alerta True. La última corrida creada es la 03, con F1 de validación de 0.501623; la elegida conserva 0.518076. La comprobación de las 3 fichas originales devuelve True: la promoción y la evaluación posterior mantuvieron su contenido.
La pregunta de Laura ahora puede responderse con una ruta de consulta. Sabes qué referencia está elegida, qué modelo contiene, con qué contenido se entrenó y qué evaluación acompaña su política de alerta. Si alguien presenta otra cifra, puedes pedir la corrida y el documento donde se calculó.
La verificación de huellas controla que los archivos coinciden con los registrados. El origen de los archivos y los permisos del registro siguen formando parte de la operación. Conservar una identidad de contenido y controlar quién puede cambiar los registros son responsabilidades que se complementan.
9) Qué añade una herramienta como MLflow
El seguimiento de experimentos reúne parámetros, métricas y archivos para consultarlos. El registro de modelos organiza versiones y referencias de uso. Construiste una versión pequeña con carpetas para comprender las relaciones; puedes reconocer esas mismas necesidades cuando abras una herramienta de equipo.
Necesidad del ejemplo
Qué aporta MLflow según su documentación
Consultar fichas de corridas
Seguimiento de parámetros, versiones de código, métricas y archivos mediante interfaces y una vista web
Agrupar trabajo relacionado
Experimentos que reúnen corridas para una tarea
Encontrar versiones de modelos
Registro central con versiones, metadatos y relación con su corrida
Mantener referencias de uso
Alias y etiquetas vinculados a versiones concretas
Las etiquetas candidato, en producción y retirado pertenecen al diseño local de este capítulo. La documentación de MLflow ofrece alias y etiquetas para organizar versiones y referencias; puedes modelar decisiones de tu equipo con esas capacidades. Sus mecanismos de consulta facilitan encontrar la información que aquí recorriste mediante JSON.
La regla de comparación y la política de aceptación siguen siendo decisiones del equipo. Al pasar a una herramienta, conserva los identificadores de datos, el protocolo y la separación de prueba. Así cada resultado que consultes mantiene el significado que aprendiste a revisar.
NotaAlcance del experimento
Los envíos son sintéticos y las reglas de promoción son ilustrativas. La diferencia de F1 observada describe estos pliegues y requiere evidencia adicional para valorar una mejora estable. Las configuraciones provienen del recorrido didáctico del libro; la prueba compartida mantiene continuidad y una evaluación operativa necesita datos nuevos. La huella identifica bytes: orden y formato del CSV importan. Conservamos el archivo de ajuste y la receta; reproducir todo el recorrido requiere además este notebook y las versiones indicadas. El registro tiene un solo escritor local y las etapas de producción son simuladas. Carga archivos joblib de origen confiable y en un entorno compatible, como indica la documentación de persistencia de scikit-learn.
Errores frecuentes
Lo que ves
Qué pasó
Cómo se arregla
Igual nombre y tamaño, distinta huella
Cambió un dato conservando la longitud del archivo.
Identifica el contenido y conserva ambas fuentes.
Comparación rechazada por datos
Las métricas provienen de conjuntos diferentes.
Forma una comparación sobre datos compartidos.
Comparación rechazada por protocolo
Cambió la partición, la métrica u otra condición de medición.
Declara otro experimento o conserva el protocolo común.
Identificador ya existente
Intentaste sustituir una corrida terminada.
Crea otra identidad y documenta el cambio.
La evaluación pertenece a otra corrida
Se mezclaron fichas y resultados.
Recupera el documento que cita el modelo elegido.
La última corrida rinde menos
El orden de creación no representa aprobación.
Resuelve la referencia explícita del registro.
Un archivo no coincide con su huella
Se modificó o se recuperó otro contenido.
Investiga y restaura la versión identificada.
Lista de verificación
Cada cifra permite encontrar una corrida concreta.
Cada corrida conserva datos, código, protocolo, ajustes, versiones y modelo.
Las comparaciones exigen datos y protocolo comunes.
Las fichas completadas conservan su contenido.
El umbral y la evaluación final quedan relacionados con el ganador.
La promoción verifica identidad, integridad y criterios declarados.
La referencia actual puede resolverse hasta un modelo específico.
La historia conserva qué versión dejó de usarse y por qué cambió la referencia.
Ejercicios
Cambia solamente el nombre. Copia el CSV de juguete a otra ruta sin volver a serializarlo. Predice qué ocurrirá con el tamaño y la huella, y comprueba la comparación.
Añade una configuración. Propón otra C manteniendo datos y protocolo. Explica qué registro nuevo crearías y qué métricas de desarrollo usarías antes de considerar una evaluación futura.
Cambia el examen. Conserva el mismo CSV, pero propone otra partición. Indica qué campo del registro permitiría detectar la diferencia aunque los hiperparámetros permanezcan iguales.
Reconstruye la entrega. Partiendo únicamente del registro de modelos y la carpeta completa del experimento, enumera los archivos necesarios para responder las preguntas de Laura.
TipOrientaciones
Leer y escribir los mismos bytes conserva tamaño y huella. La ruta cambia porque representa otra ubicación. Si vuelves a guardar mediante otra herramienta, revisa también separadores y finales de línea antes de esperar bytes idénticos.
Usa un identificador nuevo y conserva las condiciones de los pliegues. Guarda los valores por turno y el promedio. La prueba utilizada en este recorrido ya fue consultada; planifica nuevos datos de evaluación para otro ciclo de selección.
Cambian los índices del protocolo y su huella. El control de comparación debe rechazar la mezcla. Una semilla idéntica ayuda a repetir, mientras los índices guardados permiten comprobar exactamente qué filas se utilizaron.
Resuelve actual, abre la ficha correspondiente y la evaluación citada. Verifica el modelo, los datos, el código y el protocolo mediante sus huellas. Recupera el umbral de la evaluación final para convertir la probabilidad en alerta.
Glosario
Término
Significado en este capítulo
Versionamiento
Guardar cada versión con un nombre que la distinga y permita recuperarla.
Corrida
Ejecución identificada con datos, procedimiento, ajustes y resultados.
Protocolo de comparación
Condiciones compartidas para medir candidatos.
Registro de corridas
Colección de fichas consultable como una tabla.
Inmutabilidad
Conservación de una ficha completada sin sustituir su contenido.
Registro de modelos o registry
Relación de versiones con su estado de uso.
Promoción
Cambio de referencia después de comprobar criterios escritos.
Candidato
Versión disponible para evaluación antes de elegirla como actual.
En producción
Etiqueta de la versión actual dentro de la simulación local.
Retirado
Versión que dejó de representar la referencia actual.
Latest
Referencia ambigua a lo último según algún orden.
Alias
Nombre que apunta a una versión concreta y puede actualizarse.
Linaje
Relaciones que permiten reconstruir el origen y la elección de un resultado.
Qué aprendiste
Partiste de una celda que cambiaba en un dígito y terminaste con una cadena consultable de datos, código, protocolo y modelo. Registraste configuraciones bajo condiciones compartidas, rechazaste comparaciones alteradas y conservaste las fichas al añadir una evaluación final. La promoción cambió la referencia local y mantuvo disponibles las corridas anteriores.
Laura ya puede identificar qué versión representa la entrega y cómo se eligió. En el capítulo 9, Despliegue de modelos como servicios, el siguiente paso será cargar una versión identificada y ofrecer sus predicciones mediante una interfaz de servicio.
Código del procedimiento
El archivo usa la misma preparación base del capítulo 5 y recibe los ajustes de cada corrida. Puedes comprobar qué recibe y devuelve cada función antes de modificarlo. El archivo ejecutado y la copia conservada quedan identificados por la huella registrada.
TipVer el código de procedimiento_envios.py
"""Pipeline de envíos y evaluación de una configuración, sin acceso a prueba.El notebook conserva este archivo junto con los datos y registra su SHA-256.Las funciones reciben desarrollo y pliegues explícitos y no escriben archivos."""from sklearn.compose import ColumnTransformerfrom sklearn.impute import SimpleImputerfrom sklearn.linear_model import LogisticRegressionfrom sklearn.model_selection import cross_val_scorefrom sklearn.pipeline import Pipelinefrom sklearn.preprocessing import OneHotEncoder, StandardScalerdef crear_pipeline():"""Devuelve el pipeline de envíos sin ajustar, con los pasos del capítulo 5.""" numericas = Pipeline([ ("rellenar", SimpleImputer(strategy="mean")), ("escalar", StandardScaler()), ]) preparar = ColumnTransformer([ ("numericas", numericas, ["distancia_km", "peso_kg"]), ("categoricas", OneHotEncoder(handle_unknown="ignore"), ["servicio"]), ])return Pipeline([ ("preparar", preparar), ("modelo", LogisticRegression(max_iter=1000)), ])def ajustar_configuracion(X, y, configuracion, cv):"""Recibe desarrollo, ajustes y pliegues; devuelve pipeline y F1 por pliegue.""" pipeline = crear_pipeline().set_params(**configuracion) puntajes = cross_val_score(pipeline, X, y, cv=cv, scoring="f1") _ = pipeline.fit(X, y)return pipeline, puntajes