Pipeline · 7 de 10
API de inferencia
Fastify
Servidor HTTP del proyecto para el serving — liviano, con buen soporte de
TypeScript y hooks (onResponse) que después se reusan directo para
instrumentar métricas de Prometheus.
onnxruntime-node
Corre el modelo ONNX in-process, sin depender de un runtime de Python en
producción. Carga los dos modelos al arrancar; operation_type decide
cuál usar (no es un input del modelo en sí).
El request tipa district, surface, property_type y operation_type
directo — se codifican con el mismo mapeo de categorías exportado desde
Python en el paso anterior.
De un input de usuario a un precio, paso a paso
Un requisito no negociable: la misma fila (district, surface,
property_type) tiene que convertirse en el mismo vector de números
tanto al entrenar en Python como al servir en Node. Si difieren en el
orden de las columnas o en qué entero le toca a cada categoría, el modelo
predice sobre datos que nunca vio en entrenamiento. Así se garantiza,
archivo por archivo:
-
ml/training/train.py→to_categorical. Sobre el fold de train,districtyproperty_typese castean acategoryde pandas. El orden en que pandas asigna esas categorías (train_df[c].cat.categories) es lo que define el código numérico de cada valor — esa lista es la fuente de verdad, no una convención documentada aparte. -
ml/training/export_onnx.py→to_onnx_input. Para validar el modelo exportado, convierte esas columnas a.cat.codes(el entero interno de pandas) — código-1(categoría no vista) se reemplaza porNaN. Es literalmente cómo XGBoost ve sus propias categóricas. -
export_onnx.py→save_category_mapping. Vuelca esa misma lista ordenada de categorías — no los códigos, la lista, en orden — a{op}_categories.json. La posición de un valor en esa lista es su código, el mismo que asignóto_categoricalen train:{ "district": ["Alto Selva Alegre", "Cayma", "..."], "property_type": ["Casa", "Departamento", "..."] } -
api/src/encode.js→categoryCode. Node no tiene pandas, así que reimplementa la misma regla a mano —indexOfsobre la lista del JSON,-1mapeado aNaN:function categoryCode(value, categories) { const idx = categories.indexOf(value); return idx === -1 ? NaN : idx; }Como opera sobre la misma lista exacta que exportó Python, el resultado es el mismo código — dos implementaciones distintas (pandas vs.
indexOf), un solo mapeo, sin margen para que diverjan. -
encode.js→encodeInput. Arma el vector final en el mismo orden queFEATURE_COLSdetrain.py—district,surface,property_type(operation_typeno es input del modelo, solo elige cuál de los dos modelos llamar):export function encodeInput({ district, surface, property_type }, categories) { return Float32Array.from([ categoryCode(district, categories.district), surface, categoryCode(property_type, categories.property_type), ]); } -
ONNX Runtime. Corre la inferencia sobre ese vector. Un distrito nunca visto en train produce
NaNen su posición — el mismo “missing” que XGBoost ya rutea nativamente, sin código especial en el handler. Probado de verdad: la misma predicción comparada entreonnxruntimePython yonnxruntime-nodepara un distrito no visto (12.144119vs.12.144122, en escala log) — y víaPOST /predictreal. -
server.js→Math.exp(). El modelo predice en escalalog(price_usd)(la decisión tomada en Baseline model) — la API deshace esa transformación antes de responder:Math.exp(output.variable.data[0]).
Trazabilidad: cada predicción queda en Postgres
pg
Driver de Postgres para Node — cada request exitoso inserta una fila en
prediction_logs (input, predicción, versión de modelo, latencia,
timestamp), pensado desde el principio para lo que necesitan el dashboard
y el reporte de drift. Un 400 de validación correctamente no genera
una fila.
La versión de modelo servida se resuelve contra el Model Registry real de MLflow al arrancar y se cachea en memoria, no por request.
Un gotcha real: MLflow desde otro contenedor
El contenedor de la API golpeó 403 Invalid Host header al llamar a
MLflow (POST /registered-models/get-latest-versions) para resolver la
versión servida. El Host header interno (mlflow:5000, nombre de
servicio de Compose) no matcheaba el allowlist default de MLflow
(localhost + IPs privadas) — quedó sin activar en la etapa de
infraestructura porque nada llamaba a MLflow desde otro contenedor
todavía. Fix: --allowed-hosts '*' en el comando de mlflow — aceptable
acá porque ese puerto nunca se expone más allá de localhost y la red
interna de Compose.
Artefactos de este paso
| Artefacto | Tipo | Qué es |
|---|---|---|
api/src/server.js | Código | App de Fastify: /health, /metrics, POST /predict. |
api/src/encode.js | Código | categoryCode + encodeInput — la codificación de categorías, espejo exacto de train.py. |
api/src/models.js | Código | Carga los dos modelos ONNX + sus categories.json al arrancar. |
api/src/mlflow.js | Código | Resuelve la versión de modelo servida contra el Model Registry, cacheada en memoria. |
api/src/db.js | Código | logPrediction — inserta una fila en prediction_logs por request exitoso. |
api/Dockerfile | Infra | node:22-slim (no alpine — el binario nativo de onnxruntime-node necesita glibc). |