GiusMagi commited on
Commit
7f6e43f
Β·
verified Β·
1 Parent(s): 0c071a5

Update server.py

Browse files
Files changed (1) hide show
  1. server.py +168 -157
server.py CHANGED
@@ -4,214 +4,225 @@ import pandas as pd
4
  import cloudpickle as cp
5
  from fastapi import FastAPI, HTTPException
6
  from fastapi.middleware.cors import CORSMiddleware
7
- from fastapi.responses import JSONResponse
8
- from pydantic import BaseModel, Field
9
  import traceback
10
  import logging
 
11
 
12
- # Setup logging
13
- logging.basicConfig(level=logging.INFO)
 
 
 
 
14
  logger = logging.getLogger(__name__)
15
 
16
  app = FastAPI(
17
- title="Incassi API",
18
- description="API per predire incassi con hurdle logit + ordinal XGB",
19
- version="1.0",
20
- docs_url="/docs",
21
- redoc_url="/redoc",
22
  )
23
 
24
- # CORS
25
  app.add_middleware(
26
  CORSMiddleware,
27
- allow_origins=["*"],
28
  allow_credentials=True,
29
  allow_methods=["*"],
30
  allow_headers=["*"],
31
  )
32
 
33
- # Carica il modello con debug dettagliato
34
- try:
35
- logger.info("πŸ”§ Caricamento modello in corso...")
36
- with open("incassi_model.pkl", "rb") as f:
37
- mdl = cp.load(f)
38
- logger.info(f"βœ… Modello caricato - Versione: {getattr(mdl, 'model_version', 'unknown')}")
39
-
40
- # Test rapido del modello
41
- test_df = pd.DataFrame([{
42
- "Debitore_cluster": "A",
43
- "Importo iniziale outstanding": 10000.0,
44
- "giorni_da_iscrizione": 100,
45
- "giorni_da_cessione": 50
46
- }])
47
- test_result = mdl.predict(test_df)
48
- logger.info(f"🎯 Test modello riuscito - yhat: {test_result[2]:.2f}")
49
-
50
- except Exception as e:
51
- logger.error(f"❌ ERRORE CRITICO nel caricamento modello: {e}")
52
- traceback.print_exc()
53
- raise e
54
-
55
- # ===== PYDANTIC MODELS CON VALIDAZIONE MIGLIORATA =====
56
- class PredictIn(BaseModel):
57
- # Usa Field con alias per gestire nomi diversi
58
- Debitore_cluster: str | None = Field(default=None, description="Cluster del debitore")
59
- Stato_Giudizio: str | None = Field(default=None, description="Stato del giudizio")
60
- Cedente: str | None = Field(default=None, description="Cedente")
61
- Importo_iniziale_outstanding: float | None = Field(default=None, ge=0.0, description="Importo iniziale outstanding")
62
- Decreto_sospeso: str | int | None = Field(default=None, description="Decreto sospeso")
63
- Notifica_Decreto: str | int | None = Field(default=None, description="Notifica decreto")
64
- Opposizione_al_decreto_ingiuntivo: str | int | None = Field(default=None, description="Opposizione al decreto ingiuntivo")
65
- Ricorso_al_TAR: str | int | None = Field(default=None, description="Ricorso al TAR")
66
- Sentenza_TAR: str | int | None = Field(default=None, description="Sentenza TAR")
67
- Atto_di_Precetto: str | int | None = Field(default=None, description="Atto di precetto")
68
- Decreto_Ingiuntivo: str | int | None = Field(default=None, description="Decreto ingiuntivo")
69
- Sentenza_giudizio_opposizione: str | int | None = Field(default=None, description="Sentenza giudizio opposizione")
70
- giorni_da_iscrizione: int | None = Field(default=None, ge=0, description="Giorni da iscrizione")
71
- giorni_da_cessione: int | None = Field(default=None, ge=0, description="Giorni da cessione")
72
- Zona: str | None = Field(default=None, description="Zona")
73
-
74
- class CurveIn(PredictIn):
75
- iscr_start: int = Field(default=0, ge=0, description="Giorni da iscrizione - inizio")
76
- iscr_end: int = Field(default=1500, ge=0, description="Giorni da iscrizione - fine")
77
- step: int = Field(default=50, ge=1, description="Step per la curva")
78
 
79
- # ===== UTILITY FUNCTIONS =====
80
- def _to_model_format(d: dict) -> pd.DataFrame:
81
- """Converte input API -> formato atteso dal modello"""
82
- logger.info(f"πŸ”„ Conversione input: {d}")
83
-
84
  try:
85
- # Mapping esplicito API -> modello
86
- row = {
87
- "Debitore_cluster": d.get("Debitore_cluster"),
88
- "Stato_Giudizio": d.get("Stato_Giudizio"),
89
- "Cedente": d.get("Cedente"),
90
- "Importo iniziale outstanding": d.get("Importo_iniziale_outstanding"), # ⚠️ Nota l'underscore -> spazio
91
- "Decreto sospeso": d.get("Decreto_sospeso"),
92
- "Notifica Decreto": d.get("Notifica_Decreto"),
93
- "Opposizione al decreto ingiuntivo": d.get("Opposizione_al_decreto_ingiuntivo"),
94
- "Ricorso al TAR": d.get("Ricorso_al_TAR"),
95
- "Sentenza TAR": d.get("Sentenza_TAR"),
96
- "Atto di Precetto": d.get("Atto_di_Precetto"),
97
- "Decreto Ingiuntivo": d.get("Decreto_Ingiuntivo"),
98
- "Sentenza giudizio opposizione": d.get("Sentenza_giudizio_opposizione"),
99
- "giorni_da_iscrizione": d.get("giorni_da_iscrizione"),
100
- "giorni_da_cessione": d.get("giorni_da_cessione"),
101
- "Zona": d.get("Zona"),
102
- }
103
 
104
- df = pd.DataFrame([row])
105
- logger.info(f"βœ… Conversione completata - Shape: {df.shape}")
106
- return df
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
 
108
  except Exception as e:
109
- logger.error(f"❌ Errore nella conversione: {e}")
110
- raise HTTPException(status_code=400, detail=f"Errore nella conversione dati: {str(e)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
 
112
  # ===== ENDPOINTS =====
113
  @app.get("/")
114
  def root():
115
  return {
116
- "ok": True,
117
- "service": "incassi-api",
118
- "model_version": getattr(mdl, "model_version", "unknown")
 
119
  }
120
 
121
- @app.get("/version")
122
- def version():
123
- return {
124
- "model_version": getattr(mdl, "model_version", "unknown"),
125
- "p100_threshold": float(getattr(mdl, "p100_thr", 0.5)),
126
- "expected_features": len(getattr(mdl, "feat_cols_full", [])),
127
- }
128
-
129
- @app.get("/health")
130
- def health_check():
131
- """Health check endpoint"""
132
  try:
133
- # Quick model test
134
- test_df = pd.DataFrame([{"giorni_da_iscrizione": 100}])
135
- _ = mdl._ensure_raw_cols(test_df)
136
- return {"status": "healthy", "model": "loaded"}
 
 
 
 
 
137
  except Exception as e:
138
- raise HTTPException(status_code=503, detail=f"Model unhealthy: {str(e)}")
139
 
140
- @app.post("/predict")
141
- def predict(inp: PredictIn):
142
- """Predizione singola"""
143
  try:
144
- logger.info("πŸš€ /predict chiamato")
145
- logger.info(f"πŸ“₯ Input ricevuto: {inp.dict()}")
 
 
146
 
147
- # Converti input -> formato modello
148
  df = _to_model_format(inp.dict())
149
- logger.info(f"πŸ”„ DataFrame creato: {df.shape}")
150
 
151
- # Predizione
152
- logger.info("πŸ”§ Esecuzione predizione...")
153
- p100, prob_ord, yhat, final_class, _ = mdl.predict(df)
 
154
 
155
- result = {
156
- "p100": float(p100),
157
- "prob_ord": np.asarray(prob_ord, dtype=float).tolist(),
158
- "yhat": float(yhat),
159
- "final_class": str(final_class),
160
  }
161
 
162
- logger.info(f"βœ… Predizione completata: yhat={result['yhat']:.2f}")
163
- return result
164
-
165
- except HTTPException:
166
- raise # Re-raise HTTP exceptions
167
  except Exception as e:
168
- logger.error(f"❌ ERRORE in /predict: {e}")
169
  logger.error(f"❌ Traceback:\n{traceback.format_exc()}")
170
- raise HTTPException(status_code=500, detail=f"Errore interno: {str(e)}")
171
 
172
- @app.post("/curve_iscrizione")
173
- def curve_iscrizione(inp: CurveIn):
174
- """Curva di crescita per giorni da iscrizione"""
175
  try:
176
- logger.info("πŸš€ /curve_iscrizione chiamato")
177
 
178
- # Validazione parametri
179
- if inp.iscr_end <= inp.iscr_start:
180
- raise HTTPException(status_code=400, detail="iscr_end deve essere > iscr_start")
181
 
182
- if (inp.iscr_end - inp.iscr_start) / inp.step > 1000:
183
- raise HTTPException(status_code=400, detail="Troppi punti nella curva (max 1000)")
 
184
 
185
- # Converti input -> base row
186
- base_dict = inp.dict()
 
 
 
 
 
 
 
 
187
 
188
- # Esegui curva
189
- logger.info(f"πŸ”§ Calcolo curva: {inp.iscr_start}-{inp.iscr_end} step {inp.step}")
190
- out = mdl.curve_iscrizione(base_dict, inp.iscr_start, inp.iscr_end, inp.step)
191
 
192
- # JSON-safe conversion
193
- result = {
194
- "x": [int(v) for v in out.get("x", [])],
195
- "p100": [float(v) for v in out.get("p100", [])],
196
- "classes": {}
197
  }
198
 
199
- for k, arr in out.get("classes", {}).items():
200
- result["classes"][k] = [float(v) for v in arr]
201
-
202
- logger.info(f"βœ… Curva completata: {len(result['x'])} punti")
203
- return result
204
 
205
  except HTTPException:
206
  raise
207
  except Exception as e:
208
- logger.error(f"❌ ERRORE in /curve_iscrizione: {e}")
209
  logger.error(f"❌ Traceback:\n{traceback.format_exc()}")
210
- raise HTTPException(status_code=500, detail=f"Errore interno: {str(e)}")
211
 
212
- # ===== STARTUP EVENT =====
213
- @app.on_event("startup")
214
- async def startup_event():
215
- logger.info("πŸš€ API avviata con successo")
216
- logger.info(f"πŸ“‹ Modello version: {getattr(mdl, 'model_version', 'unknown')}")
217
- logger.info(f"🎯 Soglia p100: {getattr(mdl, 'p100_thr', 0.5):.3f}")
 
4
  import cloudpickle as cp
5
  from fastapi import FastAPI, HTTPException
6
  from fastapi.middleware.cors import CORSMiddleware
7
+ from pydantic import BaseModel
 
8
  import traceback
9
  import logging
10
+ import sys
11
 
12
+ # Setup logging piΓΉ dettagliato
13
+ logging.basicConfig(
14
+ level=logging.INFO,
15
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
16
+ handlers=[logging.StreamHandler(sys.stdout)]
17
+ )
18
  logger = logging.getLogger(__name__)
19
 
20
  app = FastAPI(
21
+ title="Incassi API - Debug Mode",
22
+ description="API per debug del modello incassi",
23
+ version="debug-1.0"
 
 
24
  )
25
 
 
26
  app.add_middleware(
27
  CORSMiddleware,
28
+ allow_origins=["*"],
29
  allow_credentials=True,
30
  allow_methods=["*"],
31
  allow_headers=["*"],
32
  )
33
 
34
+ # Variabile globale per il modello
35
+ mdl = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
 
37
+ # ===== CARICAMENTO MODELLO CON STEP-BY-STEP DEBUG =====
38
+ def load_model():
39
+ global mdl
 
 
40
  try:
41
+ logger.info("πŸ”§ STEP 1: Inizio caricamento modello...")
42
+
43
+ # Verifica esistenza file
44
+ import os
45
+ if not os.path.exists("incassi_model.pkl"):
46
+ raise FileNotFoundError("File incassi_model.pkl non trovato")
47
+
48
+ file_size = os.path.getsize("incassi_model.pkl")
49
+ logger.info(f"πŸ“ File trovato - Dimensione: {file_size} bytes")
 
 
 
 
 
 
 
 
 
50
 
51
+ # Carica il modello
52
+ logger.info("πŸ”§ STEP 2: Caricamento pickle...")
53
+ with open("incassi_model.pkl", "rb") as f:
54
+ mdl = cp.load(f)
55
+
56
+ logger.info("πŸ”§ STEP 3: Modello caricato in memoria")
57
+ logger.info(f"πŸ“‹ Tipo modello: {type(mdl)}")
58
+ logger.info(f"πŸ“‹ Versione: {getattr(mdl, 'model_version', 'N/A')}")
59
+
60
+ # Test attributi base (SENZA chiamare predict)
61
+ logger.info("πŸ”§ STEP 4: Verifica attributi modello...")
62
+ logger.info(f"πŸ“‹ p100_thr: {getattr(mdl, 'p100_thr', 'N/A')}")
63
+ logger.info(f"πŸ“‹ feat_cols_full length: {len(getattr(mdl, 'feat_cols_full', []))}")
64
+ logger.info(f"πŸ“‹ stage1 type: {type(getattr(mdl, 'stage1', None))}")
65
+ logger.info(f"πŸ“‹ stage2 type: {type(getattr(mdl, 'stage2', None))}")
66
+
67
+ logger.info("βœ… STEP 5: Modello caricato con successo (SENZA test predict)")
68
+ return True
69
 
70
  except Exception as e:
71
+ logger.error(f"❌ ERRORE nel caricamento modello: {e}")
72
+ logger.error(f"❌ Traceback completo:\n{traceback.format_exc()}")
73
+ return False
74
+
75
+ # Carica modello all'import
76
+ model_loaded = load_model()
77
+ if not model_loaded:
78
+ logger.error("❌ FATALE: Impossibile caricare il modello")
79
+
80
+ # ===== PYDANTIC MODELS SEMPLIFICATI =====
81
+ class PredictIn(BaseModel):
82
+ Debitore_cluster: str | None = None
83
+ Stato_Giudizio: str | None = None
84
+ Cedente: str | None = None
85
+ Importo_iniziale_outstanding: float | None = None
86
+ Decreto_sospeso: str | None = None
87
+ Notifica_Decreto: str | None = None
88
+ Opposizione_al_decreto_ingiuntivo: str | None = None
89
+ Ricorso_al_TAR: str | None = None
90
+ Sentenza_TAR: str | None = None
91
+ Atto_di_Precetto: str | None = None
92
+ Decreto_Ingiuntivo: str | None = None
93
+ Sentenza_giudizio_opposizione: str | None = None
94
+ giorni_da_iscrizione: int | None = None
95
+ giorni_da_cessione: int | None = None
96
+ Zona: str | None = None
97
+
98
+ def _to_model_format(d: dict) -> pd.DataFrame:
99
+ """Conversione input -> formato modello"""
100
+ logger.info("πŸ”„ Conversione input in corso...")
101
+
102
+ row = {
103
+ "Debitore_cluster": d.get("Debitore_cluster"),
104
+ "Stato_Giudizio": d.get("Stato_Giudizio"),
105
+ "Cedente": d.get("Cedente"),
106
+ "Importo iniziale outstanding": d.get("Importo_iniziale_outstanding"),
107
+ "Decreto sospeso": d.get("Decreto_sospeso"),
108
+ "Notifica Decreto": d.get("Notifica_Decreto"),
109
+ "Opposizione al decreto ingiuntivo": d.get("Opposizione_al_decreto_ingiuntivo"),
110
+ "Ricorso al TAR": d.get("Ricorso_al_TAR"),
111
+ "Sentenza TAR": d.get("Sentenza_TAR"),
112
+ "Atto di Precetto": d.get("Atto_di_Precetto"),
113
+ "Decreto Ingiuntivo": d.get("Decreto_Ingiuntivo"),
114
+ "Sentenza giudizio opposizione": d.get("Sentenza_giudizio_opposizione"),
115
+ "giorni_da_iscrizione": d.get("giorni_da_iscrizione"),
116
+ "giorni_da_cessione": d.get("giorni_da_cessione"),
117
+ "Zona": d.get("Zona"),
118
+ }
119
+
120
+ df = pd.DataFrame([row])
121
+ logger.info(f"βœ… DataFrame creato - Shape: {df.shape}")
122
+ return df
123
 
124
  # ===== ENDPOINTS =====
125
  @app.get("/")
126
  def root():
127
  return {
128
+ "ok": True,
129
+ "service": "incassi-api-debug",
130
+ "model_loaded": model_loaded,
131
+ "model_version": getattr(mdl, "model_version", "N/A") if mdl else "N/A"
132
  }
133
 
134
+ @app.get("/status")
135
+ def status():
136
+ """Status dettagliato del sistema"""
137
+ if not mdl:
138
+ return {"error": "Modello non caricato"}
139
+
 
 
 
 
 
140
  try:
141
+ return {
142
+ "model_loaded": True,
143
+ "model_type": str(type(mdl)),
144
+ "model_version": getattr(mdl, "model_version", "N/A"),
145
+ "p100_threshold": getattr(mdl, "p100_thr", "N/A"),
146
+ "features_count": len(getattr(mdl, "feat_cols_full", [])),
147
+ "stage1_loaded": hasattr(mdl, "stage1") and mdl.stage1 is not None,
148
+ "stage2_loaded": hasattr(mdl, "stage2") and mdl.stage2 is not None,
149
+ }
150
  except Exception as e:
151
+ return {"error": f"Errore status: {e}"}
152
 
153
+ @app.post("/test_simple")
154
+ def test_simple(inp: PredictIn):
155
+ """Test semplificato - solo conversione dati"""
156
  try:
157
+ logger.info("πŸ§ͺ Test semplice avviato...")
158
+
159
+ if not mdl:
160
+ raise HTTPException(status_code=503, detail="Modello non caricato")
161
 
162
+ # Solo conversione, SENZA predict
163
  df = _to_model_format(inp.dict())
164
+ logger.info(f"βœ… Conversione riuscita: {df.shape}")
165
 
166
+ # Test solo _ensure_raw_cols (metodo piΓΉ semplice)
167
+ logger.info("πŸ”§ Test _ensure_raw_cols...")
168
+ df_ensured = mdl._ensure_raw_cols(df)
169
+ logger.info(f"βœ… _ensure_raw_cols riuscito: {df_ensured.shape}")
170
 
171
+ return {
172
+ "status": "success",
173
+ "input_shape": df.shape,
174
+ "ensured_shape": df_ensured.shape,
175
+ "columns": list(df.columns),
176
  }
177
 
 
 
 
 
 
178
  except Exception as e:
179
+ logger.error(f"❌ Errore test_simple: {e}")
180
  logger.error(f"❌ Traceback:\n{traceback.format_exc()}")
181
+ raise HTTPException(status_code=500, detail=str(e))
182
 
183
+ @app.post("/predict")
184
+ def predict(inp: PredictIn):
185
+ """Predizione completa con debug step-by-step"""
186
  try:
187
+ logger.info("πŸš€ PREDICT chiamato...")
188
 
189
+ if not mdl:
190
+ raise HTTPException(status_code=503, detail="Modello non caricato")
 
191
 
192
+ # Step 1: Conversione
193
+ logger.info("πŸ”§ Step 1: Conversione input...")
194
+ df = _to_model_format(inp.dict())
195
 
196
+ # Step 2: Predict con cattura errore specifica
197
+ logger.info("πŸ”§ Step 2: Chiamata mdl.predict...")
198
+ try:
199
+ result = mdl.predict(df)
200
+ logger.info("βœ… mdl.predict completato")
201
+ except Exception as predict_error:
202
+ logger.error(f"❌ ERRORE SPECIFICO in mdl.predict: {predict_error}")
203
+ logger.error(f"❌ Tipo errore: {type(predict_error)}")
204
+ logger.error(f"❌ Traceback predict:\n{traceback.format_exc()}")
205
+ raise HTTPException(status_code=500, detail=f"Errore in predict: {predict_error}")
206
 
207
+ # Step 3: Formattazione output
208
+ logger.info("πŸ”§ Step 3: Formattazione output...")
209
+ p100, prob_ord, yhat, final_class, _ = result
210
 
211
+ response = {
212
+ "p100": float(p100),
213
+ "prob_ord": np.asarray(prob_ord, dtype=float).tolist(),
214
+ "yhat": float(yhat),
215
+ "final_class": str(final_class),
216
  }
217
 
218
+ logger.info(f"βœ… Predizione completata: yhat={response['yhat']:.2f}")
219
+ return response
 
 
 
220
 
221
  except HTTPException:
222
  raise
223
  except Exception as e:
224
+ logger.error(f"❌ ERRORE GENERALE in predict: {e}")
225
  logger.error(f"❌ Traceback:\n{traceback.format_exc()}")
226
+ raise HTTPException(status_code=500, detail=str(e))
227
 
228
+ logger.info("πŸš€ Server inizializzato - Modello caricato senza test predict")