Spaces:
Sleeping
Sleeping
| """Banco Ripley – Morosidad Predictor API. | |
| FastAPI service that exposes the trained LightGBM inference pipeline. | |
| Accepts raw financial / demographic features (same schema as data/raw/data.csv) | |
| and returns a binary prediction plus probability of morosidad. | |
| Target convention → 1 = Malo/Moroso | 0 = Bueno | |
| """ | |
| import os | |
| import pickle | |
| import time | |
| from pathlib import Path | |
| from typing import Any, Optional | |
| import numpy as np | |
| import pandas as pd | |
| from fastapi import FastAPI, HTTPException | |
| from fastapi.responses import JSONResponse | |
| from pydantic import BaseModel, Field | |
| # ── Paths ───────────────────────────────────────────────────────────────────── | |
| BASE_DIR = Path(__file__).parent | |
| MODEL_PATH = BASE_DIR / "model.pkl" | |
| # ── Load model once at startup ──────────────────────────────────────────────── | |
| print(f"[startup] Loading pipeline from {MODEL_PATH} …") | |
| with open(MODEL_PATH, "rb") as f: | |
| PIPELINE = pickle.load(f) | |
| print("[startup] Pipeline loaded ✓") | |
| # ── FastAPI app ─────────────────────────────────────────────────────────────── | |
| app = FastAPI( | |
| title="Banco Ripley – Modelo Predictiva Early", | |
| description=( | |
| "API de inferencia para el modelo de morosidad temprana de Banco Ripley. " | |
| "El pipeline acepta datos financieros / demográficos crudos y devuelve " | |
| "la predicción binaria junto con la probabilidad de morosidad.\n\n" | |
| "**Convención del target:** `1 = Malo/Moroso` | `0 = Bueno`" | |
| ), | |
| version="1.0.0", | |
| docs_url="/docs", | |
| redoc_url="/redoc", | |
| ) | |
| # ── Schema ──────────────────────────────────────────────────────────────────── | |
| class ClientRecord(BaseModel): | |
| """Raw feature record – misma estructura que data/raw/data.csv. | |
| Los campos ``serie`` y ``PERIODO`` son **ignorados** por el pipeline | |
| (el ColumnSelector los descarta antes del modelo); no es necesario enviarlos. | |
| """ | |
| PERIODO: Optional[Any] = Field(None, description="Ignorado por el pipeline – no requerido") | |
| serie: Optional[Any] = Field(None, description="Ignorado por el pipeline – no requerido") | |
| MESES_ANT_RCC: Optional[Any] = None | |
| EDAD: Optional[Any] = None | |
| GENERO: Optional[str] = None | |
| FLAG_ENTIDAD_PRINCIPAL: Optional[Any] = None | |
| GRADO_INSTRUCCION: Optional[str] = None | |
| DEPARTAMENTO: Optional[str] = None | |
| PROVINCIA: Optional[str] = None | |
| DISTRITO: Optional[str] = None | |
| FLAG_TC_MODELOS: Optional[Any] = None | |
| FLAG_MES: Optional[Any] = None | |
| CONTAR_COMP: Optional[Any] = None | |
| MARCA_HP: Optional[Any] = None | |
| MARCA_SEG_VIDA: Optional[Any] = None | |
| MARCA_DIF: Optional[Any] = None | |
| MARCA_CONV: Optional[Any] = None | |
| DMAX_SIN_HP: Optional[Any] = None | |
| DOTROS_DTOTAL: Optional[Any] = None | |
| DMES_DTOTAL: Optional[Any] = None | |
| LINEA_BASE: Optional[Any] = None | |
| RATIO_CONS: Optional[Any] = None | |
| UTIL_TARJ: Optional[Any] = None | |
| UTIL_EFEC: Optional[Any] = None | |
| UTIL_COMP: Optional[Any] = None | |
| RATIO_PRES: Optional[Any] = None | |
| INT_SALDO: Optional[Any] = None | |
| GARAN_SALDO: Optional[Any] = None | |
| CONV_PREST: Optional[Any] = None | |
| CONV_DEUDA: Optional[Any] = None | |
| MAX_CALIF3: Optional[Any] = None | |
| MAX_ATRASO3: Optional[Any] = None | |
| MAX_KTOT_M3: Optional[Any] = None | |
| MAX_CONV_PREST3: Optional[Any] = None | |
| PROM_UTIL_TARJ3: Optional[Any] = None | |
| PROM_UTIL_COMP3: Optional[Any] = None | |
| PROM_RATIO_PRES3: Optional[Any] = None | |
| PROM_UTIL_EFEC3: Optional[Any] = None | |
| MAX_INT_SALDO3: Optional[Any] = None | |
| MARCA_DIF3: Optional[Any] = None | |
| MARCA_SEG_VIDA3: Optional[Any] = None | |
| MARCA_GAR3: Optional[Any] = None | |
| DIF_BUE_MAL3: Optional[Any] = None | |
| DIF_BUE_MAL100_3: Optional[Any] = None | |
| PROM_DOTROS_DTOTAL3: Optional[Any] = None | |
| MAX_DMES_DTOTAL3: Optional[Any] = None | |
| PROM_DMES_DTOTAL3: Optional[Any] = None | |
| MAX_DOTROS_DTOTAL3: Optional[Any] = None | |
| MAX_LINEA3: Optional[Any] = None | |
| PROM_LINEA3: Optional[Any] = None | |
| MAX_CALIF6: Optional[Any] = None | |
| MAX_KTOT_M6: Optional[Any] = None | |
| PROM_UTIL_TARJ6: Optional[Any] = None | |
| PROM_UTIL_COMP6: Optional[Any] = None | |
| PROM_RATIO_PRES6: Optional[Any] = None | |
| PROM_UTIL_EFEC6: Optional[Any] = None | |
| MAX_INT_SALDO6: Optional[Any] = None | |
| MARCA_DIF6: Optional[Any] = None | |
| MARCA_SEG_VIDA6: Optional[Any] = None | |
| MARCA_GAR6: Optional[Any] = None | |
| MARCA_HIP6: Optional[Any] = None | |
| DIF_BUE_MAL6: Optional[Any] = None | |
| DIF_BUE_MAL100_6: Optional[Any] = None | |
| PROM_DOTROS_DTOTAL6: Optional[Any] = None | |
| MAX_DMES_DTOTAL6: Optional[Any] = None | |
| PROM_DMES_DTOTAL6: Optional[Any] = None | |
| MAX_DOTROS_DTOTAL6: Optional[Any] = None | |
| MAX_LINEA6: Optional[Any] = None | |
| PROM_LINEA6: Optional[Any] = None | |
| MAX_KTOT_M12: Optional[Any] = None | |
| PROM_UTIL_TARJ12: Optional[Any] = None | |
| PROM_UTIL_COMP12: Optional[Any] = None | |
| PROM_RATIO_PRES12: Optional[Any] = None | |
| PROM_UTIL_EFEC12: Optional[Any] = None | |
| MAX_INT_SALDO12: Optional[Any] = None | |
| MARCA_DIF12: Optional[Any] = None | |
| MARCA_SEG_VIDA12: Optional[Any] = None | |
| MARCA_GAR12: Optional[Any] = None | |
| MARCA_HIP12: Optional[Any] = None | |
| DIF_BUE_MAL12: Optional[Any] = None | |
| DIF_BUE_MAL100_12: Optional[Any] = None | |
| PROM_DOTROS_DTOTAL12: Optional[Any] = None | |
| MAX_DMES_DTOTAL12: Optional[Any] = None | |
| PROM_DMES_DTOTAL12: Optional[Any] = None | |
| MAX_DOTROS_DTOTAL12: Optional[Any] = None | |
| MAX_LINEA12: Optional[Any] = None | |
| PROM_LINEA12: Optional[Any] = None | |
| D12_MAXD: Optional[Any] = None | |
| D12_MAXD6: Optional[Any] = None | |
| D12_MAXD3: Optional[Any] = None | |
| PROM3_PROM12: Optional[Any] = None | |
| PROM3_PROM6: Optional[Any] = None | |
| PROM6_PROM12: Optional[Any] = None | |
| Max_AumKP: Optional[Any] = None | |
| Max_AumMORA: Optional[Any] = None | |
| Max_AumKT: Optional[Any] = None | |
| Max_DismDTOTAL: Optional[Any] = None | |
| Max_AumMES: Optional[Any] = None | |
| Max_DismMES: Optional[Any] = None | |
| Max_DismCONS: Optional[Any] = None | |
| Max_AumCONS: Optional[Any] = None | |
| Max_AumDTOTAL: Optional[Any] = None | |
| Max_DismKP: Optional[Any] = None | |
| Max_DismKT: Optional[Any] = None | |
| NMES_UMORA: Optional[Any] = None | |
| PROMDIR_IND12: Optional[Any] = None | |
| PROMDIR_IND6: Optional[Any] = None | |
| PROMDIR_IND3: Optional[Any] = None | |
| RATIO_CONS12: Optional[Any] = None | |
| LINT_PROM12: Optional[Any] = None | |
| MAX_PORC_ACR12: Optional[Any] = None | |
| MAX_CALIF12: Optional[Any] = None | |
| MARCA_CONV3: Optional[Any] = None | |
| MAX_CONV_DEUDA3: Optional[Any] = None | |
| MAX_ENT_REP3: Optional[Any] = None | |
| MARCA_HIP3: Optional[Any] = None | |
| MAX_PORC_ACR3: Optional[Any] = None | |
| MAX_RATIO_PRES3: Optional[Any] = None | |
| MAX_UTIL_COMP3: Optional[Any] = None | |
| MAX_UTIL_EFEC3: Optional[Any] = None | |
| MAX_UTIL_TARJ3: Optional[Any] = None | |
| RATIOS_PROM_COMP_DEUDA3: Optional[Any] = None | |
| RATIOS_PROM_EFEC_DEUDA3: Optional[Any] = None | |
| MAX_ENT_ACR3: Optional[Any] = None | |
| MAX_GARAN_SALDO3: Optional[Any] = None | |
| MARCA_CONV6: Optional[Any] = None | |
| MAX_CONV_DEUDA6: Optional[Any] = None | |
| MAX_ENT_REP6: Optional[Any] = None | |
| MAX_PORC_ACR6: Optional[Any] = None | |
| MAX_RATIO_PRES6: Optional[Any] = None | |
| MAX_UTIL_COMP6: Optional[Any] = None | |
| MAX_UTIL_EFEC6: Optional[Any] = None | |
| MAX_UTIL_TARJ6: Optional[Any] = None | |
| RATIOS_PROM_COMP_DEUDA6: Optional[Any] = None | |
| RATIOS_PROM_EFEC_DEUDA6: Optional[Any] = None | |
| MAX_ENT_ACR6: Optional[Any] = None | |
| MAX_GARAN_SALDO6: Optional[Any] = None | |
| MAX_ATRASO6: Optional[Any] = None | |
| MAX_CONV_PREST6: Optional[Any] = None | |
| MARCA_CONV12: Optional[Any] = None | |
| MAX_CONV_DEUDA12: Optional[Any] = None | |
| MAX_ENT_REP12: Optional[Any] = None | |
| MAX_RATIO_PRES12: Optional[Any] = None | |
| MAX_UTIL_COMP12: Optional[Any] = None | |
| MAX_UTIL_EFEC12: Optional[Any] = None | |
| MAX_UTIL_TARJ12: Optional[Any] = None | |
| RATIOS_PROM_COMP_DEUDA12: Optional[Any] = None | |
| RATIOS_PROM_EFEC_DEUDA12: Optional[Any] = None | |
| MAX_ENT_ACR12: Optional[Any] = None | |
| MAX_GARAN_SALDO12: Optional[Any] = None | |
| MAX_ATRASO12: Optional[Any] = None | |
| MAX_CONV_PREST12: Optional[Any] = None | |
| MARCA_GAR: Optional[Any] = None | |
| FLAG_TENENCIA_VEHICULAR: Optional[Any] = None | |
| SITUACION_LABORAL: Optional[str] = None | |
| ESTADO_CIVIL: Optional[str] = None | |
| class Config: | |
| extra = "allow" | |
| class PredictRequest(BaseModel): | |
| records: list[ClientRecord] | |
| class PredictionResult(BaseModel): | |
| prediction: int = Field(..., description="0=Bueno | 1=Malo/Moroso") | |
| probability_malo: float = Field(..., description="Probabilidad de morosidad (clase 1)") | |
| label: str = Field(..., description="Etiqueta legible: Bueno | Malo") | |
| class PredictResponse(BaseModel): | |
| predictions: list[PredictionResult] | |
| model_version: str = "lgbm-inference-pipeline-v1" | |
| run_id: str = "8967c637226f47209eeb1dba83e7519e" | |
| # ── Helpers ─────────────────────────────────────────────────────────────────── | |
| def _predict(records: list[dict]) -> list[PredictionResult]: | |
| df = pd.DataFrame(records) | |
| probas = PIPELINE.predict_proba(df)[:, 1] | |
| preds = (probas >= 0.5).astype(int) | |
| return [ | |
| PredictionResult( | |
| prediction=int(p), | |
| probability_malo=round(float(pr), 6), | |
| label="Malo" if p == 1 else "Bueno", | |
| ) | |
| for p, pr in zip(preds, probas) | |
| ] | |
| # ── Endpoints ───────────────────────────────────────────────────────────────── | |
| def root(): | |
| return { | |
| "service": "predictiva_early", | |
| "description": "Modelo de morosidad temprana – Banco Ripley", | |
| "target_convention": {"0": "Bueno", "1": "Malo/Moroso"}, | |
| "docs": "/docs", | |
| } | |
| def health(): | |
| return {"status": "ok"} | |
| def predict_single(record: ClientRecord): | |
| """Predice morosidad para **un único cliente**. | |
| Envía un registro con la misma estructura que `data/raw/data.csv`. | |
| Devuelve la clase predicha (`0=Bueno`, `1=Malo/Moroso`) y la probabilidad. | |
| """ | |
| try: | |
| results = _predict([record.model_dump()]) | |
| except Exception as exc: | |
| raise HTTPException(status_code=422, detail=str(exc)) | |
| return PredictResponse(predictions=results) | |
| def predict_batch(request: PredictRequest): | |
| """Predice morosidad para un **lote de clientes** (máximo 500 registros). | |
| Envía una lista de registros bajo la clave `records`. | |
| """ | |
| if len(request.records) > 500: | |
| raise HTTPException( | |
| status_code=413, | |
| detail="Batch demasiado grande. Máximo 500 registros por llamada.", | |
| ) | |
| try: | |
| results = _predict([r.model_dump() for r in request.records]) | |
| except Exception as exc: | |
| raise HTTPException(status_code=422, detail=str(exc)) | |
| return PredictResponse(predictions=results) | |