Djohell commited on
Commit
9d18580
·
1 Parent(s): 9b067c8

Deploy API connected to MLflow

Browse files
Files changed (5) hide show
  1. .gitignore +16 -0
  2. Dockerfile +29 -0
  3. app.py +129 -0
  4. processing.py +87 -0
  5. requirements.txt +6 -0
.gitignore ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Ignorer le dossier de cache Python
2
+ __pycache__/
3
+ *.pyc
4
+
5
+ # Ignorer les modèles locaux
6
+ models/
7
+ *.json
8
+ *.xgb
9
+
10
+ # Ignorer l'environnement virtuel
11
+ venv/
12
+ env/
13
+ .env/
14
+
15
+ # Ignorer les secrets locaux
16
+ .env
Dockerfile ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 1. Utiliser une image Python légère
2
+ FROM python:3.9-slim
3
+
4
+ # 2. Définir le répertoire de travail
5
+ WORKDIR /app
6
+
7
+ # 3. Installer les dépendances système nécessaires
8
+ RUN apt-get update && apt-get install -y \
9
+ libgomp1 \
10
+ && rm -rf /var/lib/apt/lib/lists/*
11
+
12
+ # 4. Créer un utilisateur non-root
13
+ RUN useradd -m -u 1000 user
14
+ USER user
15
+ ENV PATH="/home/user/.local/bin:$PATH"
16
+
17
+ # 5. Copier les fichiers de dépendances et installer
18
+ COPY --chown=user requirements.txt .
19
+ RUN pip install --no-cache-dir --upgrade -r requirements.txt
20
+
21
+ # 6. Copier tout le reste du code et les modèles
22
+ COPY --chown=user . .
23
+
24
+ # 7. Exposer le port par défaut de Hugging Face
25
+ EXPOSE 7860
26
+
27
+ # 8. Lancer l'application avec Uvicorn
28
+
29
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
app.py ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import numpy as np
4
+ import xgboost as xgb
5
+ import pandas as pd
6
+ import mlflow.xgboost
7
+ from fastapi import FastAPI, HTTPException, Body
8
+ from dotenv import load_dotenv
9
+ from processing import prepare_input, calculate_survival_risk, map_statut_expert, get_sigma
10
+
11
+ # --- 1. CONFIGURATION MLFLOW ---
12
+ load_dotenv()
13
+
14
+ # MLflow utilise automatiquement MLFLOW_TRACKING_USERNAME et MLFLOW_TRACKING_PASSWORD
15
+ mlflow.set_tracking_uri(os.getenv("MLFLOW_TRACKING_URI"))
16
+
17
+ RUN_ID = "674d07aab0b0493a838310da47c71a95"
18
+ MODEL_URI = f"runs:/{RUN_ID}/model"
19
+
20
+ # --- 2. INITIALISATION DE L'API ---
21
+ app = FastAPI(
22
+ title="Business Risk API - Test MLflow Remote",
23
+ description="API de simulation utilisant un modèle stocké sur un serveur MLflow distant.",
24
+ version="3.5.0"
25
+ )
26
+
27
+ # Variables globales pour le modèle et Sigma
28
+ model = None
29
+ SIGMA = None
30
+
31
+ # Dans ton bloc startup, ajoute ces prints pour débugger :
32
+ @app.on_event("startup")
33
+ async def load_model():
34
+ global model, SIGMA
35
+ try:
36
+ print(f"🚀 Connexion à MLflow : {os.getenv('MLFLOW_TRACKING_URI')}")
37
+
38
+ # 1. On charge l'objet
39
+ loaded_model = mlflow.xgboost.load_model(MODEL_URI)
40
+
41
+ # 2. CORRECTION ICI :
42
+ # Si c'est déjà un Booster, on l'utilise directement.
43
+ # Si c'est un wrapper XGBModel, on appelle get_booster().
44
+ if isinstance(loaded_model, xgb.Booster):
45
+ model = loaded_model
46
+ else:
47
+ model = loaded_model.get_booster()
48
+
49
+ # 3. On extrait Sigma
50
+ SIGMA = get_sigma(model)
51
+
52
+ print(f"✅ Modèle chargé avec succès (Sigma: {round(SIGMA, 4)})")
53
+ except Exception as e:
54
+ print(f"❌ Erreur lors du chargement : {e}")
55
+
56
+ # --- 3. ROUTES ---
57
+
58
+ @app.get("/")
59
+ def home():
60
+ return {
61
+ "status": "online",
62
+ "model_source": "MLflow Remote",
63
+ "run_id": RUN_ID
64
+ }
65
+
66
+ @app.post("/predict")
67
+ async def predict(
68
+ data: dict = Body(..., example={
69
+ "age_estime": 4.5,
70
+ "Tranche_effectif_num": 3,
71
+ "code_departement": "26",
72
+ "code_ape": "43",
73
+ "categorie_juridique": "5499",
74
+ "is_ess": 0
75
+ })
76
+ ):
77
+ """
78
+ Simule le risque de fermeture d'une entreprise.
79
+
80
+ Exemple fourni :
81
+ - Age : 4.5 ans
82
+ - Effectif : Tranche 3
83
+ - Localisation : Drôme (26)
84
+ - Secteur : Construction (43)
85
+ """
86
+ if model is None:
87
+ raise HTTPException(status_code=503, detail="Modèle non chargé")
88
+
89
+ try:
90
+ # 1. Préparation des données (Mapping APE 2 chiffres inclus)
91
+ dmatrix = prepare_input(data)
92
+
93
+ # 2. Inférence (Score MU)
94
+ mu = float(model.predict(dmatrix)[0])
95
+
96
+ # 3. Calcul des probabilités de fermeture aux horizons 1, 2 et 3 ans
97
+ p1 = calculate_survival_risk(mu, 1, SIGMA)
98
+ p2 = calculate_survival_risk(mu, 2, SIGMA)
99
+ p3 = calculate_survival_risk(mu, 3, SIGMA)
100
+
101
+ return {
102
+ "diagnostic": {
103
+ "profil_global": map_statut_expert(p2),
104
+ "indice_confiance_mu": round(mu, 4)
105
+ },
106
+ "probabilites_fermeture": {
107
+ "1_an": f"{p1}%",
108
+ "2_ans": f"{p2}%",
109
+ "3_ans": f"{p3}%"
110
+ },
111
+ "entrees_reçues": {
112
+ "division_ape": data.get("code_ape"),
113
+ "departement": data.get("code_departement")
114
+ },
115
+ "metadonnees": {
116
+ "run_id": RUN_ID,
117
+ "sigma_utilise": round(SIGMA, 6)
118
+ }
119
+ }
120
+
121
+ except Exception as e:
122
+ raise HTTPException(
123
+ status_code=500,
124
+ detail=f"Erreur interne lors du calcul : {str(e)}"
125
+ )
126
+
127
+ if __name__ == "__main__":
128
+ import uvicorn
129
+ uvicorn.run(app, host="0.0.0.0", port=7860)
processing.py ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import numpy as np
3
+ import xgboost as xgb
4
+ import json
5
+
6
+ # --- 1. DICTIONNAIRE DE MAPPING DIVISION (2 chiffres) -> SECTION (Ton Modèle) ---
7
+ # Ce dictionnaire permet à l'utilisateur de saisir "56" et au modèle de recevoir "Hébergement et restauration"
8
+ NAF_TO_SECTION = {
9
+ "01": "Agriculture, sylviculture et pêche", "02": "Agriculture, sylviculture et pêche", "03": "Agriculture, sylviculture et pêche",
10
+ "10": "Industrie manufacturière", "11": "Industrie manufacturière", "12": "Industrie manufacturière",
11
+ "41": "Construction", "42": "Construction", "43": "Travaux de construction spécialisés",
12
+ "45": "Commerce", "46": "Commerce", "47": "Commerce de détail, à l’exception des automobi...",
13
+ "49": "Transports et entreposage", "55": "Hébergement et restauration", "56": "Hébergement et restauration",
14
+ "58": "Information et communication", "61": "Information et communication",
15
+ "64": "Activités financières et d'assurance", "66": "Activités auxiliaires de services financiers e...",
16
+ "68": "Activités immobilières",
17
+ "69": "Activités juridiques, comptables, de gestion, d'études de conseil",
18
+ "71": "Activités d'architecture et d'ingénierie",
19
+ "85": "Enseignement",
20
+ "86": "Santé humaine et action sociale",
21
+ "96": "Autres services personnels"
22
+ }
23
+
24
+ # Chargement des colonnes exactes du modèle
25
+ with open('models/features_config.json', 'r') as f:
26
+ FEATURES = json.load(f)
27
+
28
+ def get_sigma(model):
29
+ """Extrait le paramètre scale (sigma) du JSON du modèle XGBoost"""
30
+ config = json.loads(model.save_config())
31
+ def find_key(obj, key):
32
+ if isinstance(obj, dict):
33
+ for k, v in obj.items():
34
+ if k == key: return v
35
+ res = find_key(v, key)
36
+ if res is not None: return res
37
+ elif isinstance(obj, list):
38
+ for item in obj:
39
+ res = find_key(item, key)
40
+ if res is not None: return res
41
+ return None
42
+ scale = find_key(config, 'aft_loss_distribution_scale')
43
+ return float(scale) if scale else 0.8
44
+
45
+ def calculate_survival_risk(mu, horizon, s):
46
+ """Formule de survie pour distribution Logistique (AFT)"""
47
+ z = (np.log(horizon) - mu) / s
48
+ z = np.clip(z, -50, 50)
49
+ return round((1 / (1 + np.exp(-z))) * 100, 2)
50
+
51
+ def map_statut_expert(p2):
52
+ """Traduction de la probabilité à 2 ans en libellé métier"""
53
+ if p2 > 20: return '🔴 CRITIQUE'
54
+ if p2 > 10: return '🟠 VIGILANCE'
55
+ if p2 > 5: return '🟡 OBSERVATION'
56
+ return '🟢 SAIN'
57
+
58
+ def prepare_input(data):
59
+ """Prépare le DMatrix avec mapping automatique des codes APE et CJ"""
60
+ df = pd.DataFrame(0.0, index=[0], columns=FEATURES)
61
+
62
+ # 1. Variables numériques directes
63
+ df['age_au_diagnostic'] = float(data.get('age_estime', 0))
64
+ df['Tranche_effectif_num'] = float(data.get('Tranche_effectif_num', 0))
65
+ df['risque_departemental'] = float(data.get('code_departement', 0))
66
+ df['is_ess'] = int(data.get('is_ess', 0))
67
+
68
+ # 2. Mapping APE (Division -> Section)
69
+ code_ape_2 = str(data.get('code_ape', ''))[:2]
70
+ section_name = NAF_TO_SECTION.get(code_ape_2)
71
+
72
+ if section_name:
73
+ col_ape = f"APE_{section_name}"
74
+ if col_ape in df.columns:
75
+ df[col_ape] = 1.0
76
+ elif 'APE_Autres_Secteurs' in df.columns:
77
+ df['APE_Autres_Secteurs'] = 1.0
78
+
79
+ # 3. Mapping Catégorie Juridique (CJ_Prefix)
80
+ cj_prefix = str(data.get('categorie_juridique', ''))[:4]
81
+ col_cj = f"CJ_{cj_prefix}"
82
+ if col_cj in df.columns:
83
+ df[col_cj] = 1.0
84
+ elif 'CJ_Autres_Status' in df.columns:
85
+ df['CJ_Autres_Status'] = 1.0
86
+
87
+ return xgb.DMatrix(df)
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
+ xgboost
4
+ pandas
5
+ numpy
6
+ python-multipart