ASI-Engineer commited on
Commit
abd571c
·
verified ·
1 Parent(s): 38aa465

Upload folder using huggingface_hub

Browse files
Files changed (6) hide show
  1. README.md +259 -34
  2. README_HF.md +33 -9
  3. app.py +158 -3
  4. src/config.py +1 -1
  5. src/preprocessing.py +227 -16
  6. src/schemas.py +49 -0
README.md CHANGED
@@ -1,49 +1,274 @@
1
- ---
2
- title: Employee Turnover Prediction API
3
- emoji: 👔
4
- colorFrom: blue
5
- colorTo: purple
6
- sdk: docker
7
- pinned: true
8
- license: mit
9
- app_port: 7860
10
- ---
11
 
12
- # Employee Turnover Prediction API 🚀
13
 
14
- API de prédiction du turnover des employés avec XGBoost + SMOTE.
15
 
16
- ## 🎯 Fonctionnalités
 
 
 
17
 
18
- - ✅ Prédiction de turnover (0 = reste, 1 = part)
19
- - 📊 Probabilités et niveau de risque (Low/Medium/High)
 
 
20
  - 🔐 Authentification API Key
21
- - 📝 Logs structurés JSON
22
- - 🛡️ Rate limiting (20 req/min)
23
- - 📚 Documentation OpenAPI/Swagger
24
 
25
- ## 🔗 Endpoints
26
 
27
- - **Docs** : `/docs` - Documentation interactive
28
- - **Health** : `/health` - Status de l'API
29
- - **Predict** : `/predict` - Prédiction de turnover
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
 
31
- ## 🚀 Utilisation
 
 
 
 
 
32
 
33
  ```bash
34
- # Health check
35
- curl https://asi-engineer-employee-turnover-api.hf.space/health
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
 
37
- # Prédiction
38
- curl -X POST https://asi-engineer-employee-turnover-api.hf.space/predict \
 
 
 
 
 
 
 
 
 
39
  -H "Content-Type: application/json" \
40
- -d '{
41
- "satisfaction_employee_environnement": 3,
42
- "satisfaction_employee_nature_travail": 4,
43
- ...
44
- }'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  ```
46
 
47
- ## 📚 Documentation complète
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
 
49
- Voir [GitHub Repository](https://github.com/chaton59/OC_P5) pour la documentation complète.
 
 
1
+ # 🚀 Employee Turnover Prediction API - v2.2.0
 
 
 
 
 
 
 
 
 
2
 
3
+ ## 📊 Vue d'ensemble
4
 
5
+ API REST de prédiction du turnover des employés basée sur un modèle XGBoost avec SMOTE.
6
 
7
+ **✨ Nouveautés v2.2.0** :
8
+ - 📦 **Endpoint batch CSV** : Envoyez directement vos 3 fichiers CSV bruts
9
+ - 🔧 Correction du preprocessing (scaling + ordre des colonnes)
10
+ - 📊 Prédictions plus précises (~90% accuracy)
11
 
12
+ **✨ v2.1.0** :
13
+ - 📝 Logging structuré JSON
14
+ - 🛡️ Rate limiting (20 req/min par IP)
15
+ - ⚡ Gestion d'erreurs améliorée
16
  - 🔐 Authentification API Key
 
 
 
17
 
18
+ ## 🏗️ Architecture
19
 
20
+ ```
21
+ OC_P5/
22
+ ├── app.py # Point d'entrée FastAPI
23
+ ├── src/
24
+ │ ├── auth.py # Authentification API Key
25
+ │ ├── config.py # Configuration centralisée
26
+ │ ├── logger.py # Logging structuré (NOUVEAU)
27
+ │ ├── models.py # Chargement modèle HF Hub
28
+ │ ├── preprocessing.py # Pipeline preprocessing
29
+ │ ├── rate_limit.py # Rate limiting (NOUVEAU)
30
+ │ └── schemas.py # Validation Pydantic
31
+ ├── tests/ # Suite pytest (33 tests, 88% couverture)
32
+ ├── logs/ # Logs JSON (NOUVEAU)
33
+ │ ├── api.log # Tous les logs
34
+ │ └── error.log # Erreurs uniquement
35
+ ├── docs/ # Documentation
36
+ ├── ml_model/ # Scripts training
37
+ └── data/ # Données sources
38
+ ```
39
+
40
+ ## 🚀 Installation
41
 
42
+ ### Prérequis
43
+ - Python 3.12+
44
+ - Poetry 1.7+
45
+ - Git
46
+
47
+ ### Setup rapide
48
 
49
  ```bash
50
+ # 1. Cloner le repo
51
+ git clone https://github.com/chaton59/OC_P5.git
52
+ cd OC_P5
53
+
54
+ # 2. Installer les dépendances
55
+ poetry install
56
+
57
+ # 3. Configurer l'environnement
58
+ cp .env.example .env
59
+ # Éditer .env avec vos valeurs
60
+
61
+ # 4. Lancer l'API
62
+ poetry run uvicorn app:app --reload
63
+
64
+ # 5. Accéder à la documentation
65
+ # http://localhost:8000/docs
66
+ ```
67
+
68
+ ## 📝 Configuration (.env)
69
+
70
+ ```bash
71
+ # Mode développement (désactive auth + active logs détaillés)
72
+ DEBUG=true
73
+
74
+ # API Key (requis en production)
75
+ API_KEY=your-secret-key-here
76
+
77
+ # Logging (DEBUG, INFO, WARNING, ERROR, CRITICAL)
78
+ LOG_LEVEL=INFO
79
+
80
+ # HuggingFace Model
81
+ HF_MODEL_REPO=ASI-Engineer/employee-turnover-model
82
+ MODEL_FILENAME=model/model.pkl
83
+ ```
84
+
85
+ ## 🔒 Authentification
86
 
87
+ ### Mode DEBUG (développement)
88
+ ```bash
89
+ # L'API Key n'est PAS requise
90
+ curl http://localhost:8000/predict -H "Content-Type: application/json" -d '{...}'
91
+ ```
92
+
93
+ ### Mode PRODUCTION
94
+ ```bash
95
+ # L'API Key est REQUISE
96
+ curl http://localhost:8000/predict \
97
+ -H "X-API-Key: your-secret-key" \
98
  -H "Content-Type: application/json" \
99
+ -d '{...}'
100
+ ```
101
+
102
+ ## 📡 Endpoints
103
+
104
+ ### 🏥 Health Check
105
+ ```bash
106
+ GET /health
107
+
108
+ # Réponse
109
+ {
110
+ "status": "healthy",
111
+ "model_loaded": true,
112
+ "model_type": "Pipeline",
113
+ "version": "2.2.0"
114
+ }
115
+ ```
116
+
117
+ ### 🔮 Prédiction unitaire
118
+ ```bash
119
+ POST /predict
120
+ Content-Type: application/json
121
+ X-API-Key: your-key (en production)
122
+
123
+ # Payload (tous les champs d'un employé)
124
+ {
125
+ "nombre_participation_pee": 0,
126
+ "nb_formations_suivies": 2,
127
+ "satisfaction_employee_environnement": 3,
128
+ ...
129
+ }
130
+
131
+ # Réponse
132
+ {
133
+ "prediction": 0, # 0 = reste, 1 = part
134
+ "probability_0": 0.85, # Probabilité de rester
135
+ "probability_1": 0.15, # Probabilité de partir
136
+ "risk_level": "Low" # Low, Medium, High
137
+ }
138
+ ```
139
+
140
+ ### 📦 Prédiction batch (NOUVEAU)
141
+ ```bash
142
+ POST /predict/batch
143
+ X-API-Key: your-key (en production)
144
+
145
+ # Envoi des 3 fichiers CSV bruts
146
+ curl -X POST "http://localhost:8000/predict/batch" \
147
+ -H "X-API-Key: your-key" \
148
+ -F "sondage_file=@data/extrait_sondage.csv" \
149
+ -F "eval_file=@data/extrait_eval.csv" \
150
+ -F "sirh_file=@data/extrait_sirh.csv"
151
+
152
+ # Réponse
153
+ {
154
+ "total_employees": 1470,
155
+ "predictions": [
156
+ {"employee_id": 1, "prediction": 1, "probability_leave": 0.84, "risk_level": "High"},
157
+ {"employee_id": 2, "prediction": 0, "probability_leave": 0.11, "risk_level": "Low"}
158
+ ],
159
+ "summary": {
160
+ "total_stay": 1169,
161
+ "total_leave": 301,
162
+ "high_risk_count": 222,
163
+ "medium_risk_count": 233,
164
+ "low_risk_count": 1015
165
+ }
166
+ }
167
  ```
168
 
169
+ ## 📊 Logging
170
+
171
+ ### Logs structurés JSON
172
+
173
+ **Fichiers** :
174
+ - `logs/api.log` : Tous les logs
175
+ - `logs/error.log` : Erreurs uniquement
176
+
177
+ **Format** :
178
+ ```json
179
+ {
180
+ "timestamp": "2025-12-26T10:30:45",
181
+ "level": "INFO",
182
+ "logger": "employee_turnover_api",
183
+ "message": "Request POST /predict",
184
+ "method": "POST",
185
+ "path": "/predict",
186
+ "status_code": 200,
187
+ "duration_ms": 23.45,
188
+ "client_host": "127.0.0.1"
189
+ }
190
+ ```
191
+
192
+ ## 🛡️ Rate Limiting
193
+
194
+ **Configuration** :
195
+ - **Développement** : Désactivé (DEBUG=true)
196
+ - **Production** : 20 requêtes/minute par IP ou API Key
197
+
198
+ **En cas de dépassement** :
199
+ ```json
200
+ {
201
+ "error": "Rate limit exceeded",
202
+ "message": "20 per 1 minute"
203
+ }
204
+ ```
205
+
206
+ ## ✅ Tests
207
+
208
+ ```bash
209
+ # Tous les tests
210
+ poetry run pytest tests/ -v
211
+
212
+ # Avec couverture
213
+ poetry run pytest tests/ --cov --cov-report=html
214
+
215
+ # Voir rapport HTML
216
+ open htmlcov/index.html
217
+ ```
218
+
219
+ **Résultats** :
220
+ - ✅ 33 tests passés
221
+ - 📊 88% de couverture globale
222
+
223
+ ## 🚀 Déploiement
224
+
225
+ ### Variables d'environnement requises
226
+ ```bash
227
+ DEBUG=false
228
+ API_KEY=<votre-clé-sécurisée>
229
+ LOG_LEVEL=INFO
230
+ ```
231
+
232
+ ### HuggingFace Spaces
233
+ Prêt pour déploiement avec `app.py` et `requirements.txt`
234
+
235
+ ## 📚 Documentation
236
+
237
+ - **API Interactive** : http://localhost:8000/docs
238
+ - **ReDoc** : http://localhost:8000/redoc
239
+ - **Guide complet** : [docs/API_GUIDE.md](docs/API_GUIDE.md)
240
+ - **Standards** : [docs/standards.md](docs/standards.md)
241
+ - **Couverture tests** : [docs/TEST_COVERAGE.md](docs/TEST_COVERAGE.md)
242
+
243
+ ## 📦 Dépendances principales
244
+
245
+ - **FastAPI** 0.115.14 : Framework web
246
+ - **Pydantic** 2.12.5 : Validation données
247
+ - **XGBoost** 2.1.3 : Modèle ML
248
+ - **SlowAPI** 0.1.9 : Rate limiting
249
+ - **python-json-logger** 4.0.0 : Logs structurés
250
+ - **pytest** 9.0.2 : Tests
251
+
252
+ ## 🔄 Changelog
253
+
254
+ ### v2.2.0 (27 décembre 2025)
255
+ - 📦 Nouvel endpoint `/predict/batch` pour traitement CSV direct
256
+ - 🔧 Fix preprocessing : ajout du scaling des features
257
+ - 🔧 Fix preprocessing : correction de l'ordre des colonnes
258
+ - 📊 Amélioration précision des prédictions (~90%)
259
+
260
+ ### v2.1.0 (26 décembre 2025)
261
+ - ✨ Système de logging structuré JSON
262
+ - 🛡️ Rate limiting avec SlowAPI
263
+ - ⚡ Amélioration gestion d'erreurs
264
+ - 📊 Monitoring des performances
265
+
266
+ ### v2.0.0 (26 décembre 2025)
267
+ - ✅ Suite de tests complète (36 tests)
268
+ - 🔐 Authentification API Key
269
+ - 📊 88% de couverture de code
270
+
271
+ ## 👥 Auteurs
272
 
273
+ - **Projet** : OpenClassrooms P5
274
+ - **Repo** : [github.com/chaton59/OC_P5](https://github.com/chaton59/OC_P5)
README_HF.md CHANGED
@@ -16,6 +16,7 @@ API de prédiction du turnover des employés avec XGBoost + SMOTE.
16
  ## 🎯 Fonctionnalités
17
 
18
  - ✅ Prédiction de turnover (0 = reste, 1 = part)
 
19
  - 📊 Probabilités et niveau de risque (Low/Medium/High)
20
  - 🔐 Authentification API Key
21
  - 📝 Logs structurés JSON
@@ -24,26 +25,49 @@ API de prédiction du turnover des employés avec XGBoost + SMOTE.
24
 
25
  ## 🔗 Endpoints
26
 
27
- - **Docs** : `/docs` - Documentation interactive
28
- - **Health** : `/health` - Status de l'API
29
- - **Predict** : `/predict` - Prédiction de turnover
 
 
 
 
30
 
31
  ## 🚀 Utilisation
32
 
 
33
  ```bash
34
- # Health check
35
- curl https://asi-engineer-employee-turnover-api.hf.space/health
36
-
37
- # Prédiction
38
- curl -X POST https://asi-engineer-employee-turnover-api.hf.space/predict \
39
  -H "Content-Type: application/json" \
40
  -d '{
 
 
41
  "satisfaction_employee_environnement": 3,
42
- "satisfaction_employee_nature_travail": 4,
43
  ...
44
  }'
45
  ```
46
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  ## 📚 Documentation complète
48
 
49
  Voir [GitHub Repository](https://github.com/chaton59/OC_P5) pour la documentation complète.
 
16
  ## 🎯 Fonctionnalités
17
 
18
  - ✅ Prédiction de turnover (0 = reste, 1 = part)
19
+ - 📦 **Nouveau** : Endpoint batch pour traiter vos fichiers CSV directement
20
  - 📊 Probabilités et niveau de risque (Low/Medium/High)
21
  - 🔐 Authentification API Key
22
  - 📝 Logs structurés JSON
 
25
 
26
  ## 🔗 Endpoints
27
 
28
+ | Endpoint | Description |
29
+ |----------|-------------|
30
+ | `/docs` | Documentation interactive Swagger |
31
+ | `/health` | Status de l'API |
32
+ | `/ui` | Interface Gradio interactive |
33
+ | `/predict` | Prédiction unitaire (JSON) |
34
+ | `/predict/batch` | Prédiction batch (3 fichiers CSV) |
35
 
36
  ## 🚀 Utilisation
37
 
38
+ ### Prédiction unitaire
39
  ```bash
40
+ curl -X POST https://asi-engineer-oc-p5-dev.hf.space/predict \
 
 
 
 
41
  -H "Content-Type: application/json" \
42
  -d '{
43
+ "nombre_participation_pee": 0,
44
+ "nb_formations_suivies": 2,
45
  "satisfaction_employee_environnement": 3,
 
46
  ...
47
  }'
48
  ```
49
 
50
+ ### Prédiction batch (fichiers CSV)
51
+ ```bash
52
+ curl -X POST https://asi-engineer-oc-p5-dev.hf.space/predict/batch \
53
+ -F "sondage_file=@extrait_sondage.csv" \
54
+ -F "eval_file=@extrait_eval.csv" \
55
+ -F "sirh_file=@extrait_sirh.csv"
56
+ ```
57
+
58
+ **Réponse :**
59
+ ```json
60
+ {
61
+ "total_employees": 1470,
62
+ "predictions": [...],
63
+ "summary": {
64
+ "total_stay": 1169,
65
+ "total_leave": 301,
66
+ "high_risk_count": 222
67
+ }
68
+ }
69
+ ```
70
+
71
  ## 📚 Documentation complète
72
 
73
  Voir [GitHub Repository](https://github.com/chaton59/OC_P5) pour la documentation complète.
app.py CHANGED
@@ -8,12 +8,15 @@ Cette API expose le modèle de prédiction de départ des employés avec :
8
  - Health check pour monitoring
9
  - Documentation OpenAPI/Swagger automatique
10
  - Interface Gradio pour utilisation interactive
 
11
  """
 
12
  import time
13
  from contextlib import asynccontextmanager
14
 
15
  import gradio as gr
16
- from fastapi import Depends, FastAPI, HTTPException, Request
 
17
  from fastapi.middleware.cors import CORSMiddleware
18
  from slowapi import _rate_limit_exceeded_handler
19
  from slowapi.errors import RateLimitExceeded
@@ -23,9 +26,19 @@ from src.config import get_settings
23
  from src.gradio_ui import create_gradio_interface
24
  from src.logger import logger, log_model_load, log_request
25
  from src.models import get_model_info, load_model
26
- from src.preprocessing import preprocess_for_prediction
 
 
 
 
27
  from src.rate_limit import limiter
28
- from src.schemas import EmployeeInput, HealthCheck, PredictionOutput
 
 
 
 
 
 
29
 
30
  # Charger la configuration
31
  settings = get_settings()
@@ -240,6 +253,148 @@ async def predict(request: Request, employee: EmployeeInput):
240
  )
241
 
242
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
  # Monter l'interface Gradio sur /ui
244
  gradio_app = create_gradio_interface()
245
  app = gr.mount_gradio_app(app, gradio_app, path="/ui")
 
8
  - Health check pour monitoring
9
  - Documentation OpenAPI/Swagger automatique
10
  - Interface Gradio pour utilisation interactive
11
+ - Endpoint batch pour traitement de fichiers CSV
12
  """
13
+ import io
14
  import time
15
  from contextlib import asynccontextmanager
16
 
17
  import gradio as gr
18
+ import pandas as pd
19
+ from fastapi import Depends, FastAPI, File, HTTPException, Request, UploadFile
20
  from fastapi.middleware.cors import CORSMiddleware
21
  from slowapi import _rate_limit_exceeded_handler
22
  from slowapi.errors import RateLimitExceeded
 
26
  from src.gradio_ui import create_gradio_interface
27
  from src.logger import logger, log_model_load, log_request
28
  from src.models import get_model_info, load_model
29
+ from src.preprocessing import (
30
+ merge_csv_dataframes,
31
+ preprocess_dataframe_for_prediction,
32
+ preprocess_for_prediction,
33
+ )
34
  from src.rate_limit import limiter
35
+ from src.schemas import (
36
+ BatchPredictionOutput,
37
+ EmployeeInput,
38
+ EmployeePrediction,
39
+ HealthCheck,
40
+ PredictionOutput,
41
+ )
42
 
43
  # Charger la configuration
44
  settings = get_settings()
 
253
  )
254
 
255
 
256
+ @app.post(
257
+ "/predict/batch",
258
+ response_model=BatchPredictionOutput,
259
+ tags=["Prediction"],
260
+ dependencies=[Depends(verify_api_key)] if settings.is_api_key_required else [],
261
+ )
262
+ @limiter.limit("5/minute")
263
+ async def predict_batch(
264
+ request: Request,
265
+ sondage_file: UploadFile = File(..., description="Fichier CSV du sondage"),
266
+ eval_file: UploadFile = File(..., description="Fichier CSV des évaluations"),
267
+ sirh_file: UploadFile = File(..., description="Fichier CSV SIRH"),
268
+ ):
269
+ """
270
+ Endpoint de prédiction batch à partir de fichiers CSV.
271
+
272
+ **PROTÉGÉ PAR API KEY** : Requiert le header `X-API-Key` en production.
273
+
274
+ Prend en entrée les 3 fichiers CSV (sondage, évaluation, SIRH),
275
+ les fusionne, applique le preprocessing et retourne les prédictions
276
+ pour tous les employés.
277
+
278
+ Args:
279
+ sondage_file: Fichier CSV contenant les données de sondage.
280
+ eval_file: Fichier CSV contenant les données d'évaluation.
281
+ sirh_file: Fichier CSV contenant les données SIRH.
282
+
283
+ Returns:
284
+ BatchPredictionOutput: Prédictions pour tous les employés.
285
+
286
+ Raises:
287
+ HTTPException: 400 si les fichiers sont invalides.
288
+ HTTPException: 500 si erreur lors du traitement.
289
+ """
290
+ try:
291
+ # 1. Lire les fichiers CSV
292
+ sondage_content = await sondage_file.read()
293
+ eval_content = await eval_file.read()
294
+ sirh_content = await sirh_file.read()
295
+
296
+ sondage_df = pd.read_csv(io.BytesIO(sondage_content))
297
+ eval_df = pd.read_csv(io.BytesIO(eval_content))
298
+ sirh_df = pd.read_csv(io.BytesIO(sirh_content))
299
+
300
+ logger.info(
301
+ f"Fichiers CSV chargés: sondage={len(sondage_df)}, "
302
+ f"eval={len(eval_df)}, sirh={len(sirh_df)} lignes"
303
+ )
304
+
305
+ # 2. Fusionner les DataFrames
306
+ merged_df = merge_csv_dataframes(sondage_df, eval_df, sirh_df)
307
+ employee_ids = merged_df["original_employee_id"].tolist()
308
+ merged_df = merged_df.drop(columns=["original_employee_id"])
309
+
310
+ # Supprimer la colonne cible si présente
311
+ if "a_quitte_l_entreprise" in merged_df.columns:
312
+ merged_df = merged_df.drop(columns=["a_quitte_l_entreprise"])
313
+
314
+ logger.info(f"DataFrame fusionné: {len(merged_df)} employés")
315
+
316
+ # 3. Preprocessing
317
+ X = preprocess_dataframe_for_prediction(merged_df)
318
+
319
+ # 4. Charger le modèle et prédire
320
+ model = load_model()
321
+ predictions = model.predict(X.values)
322
+ probabilities = model.predict_proba(X.values)
323
+
324
+ # 5. Construire la réponse
325
+ results = []
326
+ risk_counts = {"Low": 0, "Medium": 0, "High": 0}
327
+ leave_count = 0
328
+
329
+ for i, emp_id in enumerate(employee_ids):
330
+ prob_stay = float(probabilities[i][0])
331
+ prob_leave = float(probabilities[i][1])
332
+ pred = int(predictions[i])
333
+
334
+ if prob_leave < 0.3:
335
+ risk = "Low"
336
+ elif prob_leave < 0.7:
337
+ risk = "Medium"
338
+ else:
339
+ risk = "High"
340
+
341
+ risk_counts[risk] += 1
342
+ if pred == 1:
343
+ leave_count += 1
344
+
345
+ results.append(
346
+ EmployeePrediction(
347
+ employee_id=int(emp_id),
348
+ prediction=pred,
349
+ probability_stay=prob_stay,
350
+ probability_leave=prob_leave,
351
+ risk_level=risk,
352
+ )
353
+ )
354
+
355
+ summary = {
356
+ "total_stay": len(results) - leave_count,
357
+ "total_leave": leave_count,
358
+ "high_risk_count": risk_counts["High"],
359
+ "medium_risk_count": risk_counts["Medium"],
360
+ "low_risk_count": risk_counts["Low"],
361
+ }
362
+
363
+ logger.info(f"Prédictions terminées: {summary}")
364
+
365
+ return BatchPredictionOutput(
366
+ total_employees=len(results),
367
+ predictions=results,
368
+ summary=summary,
369
+ )
370
+
371
+ except pd.errors.EmptyDataError:
372
+ raise HTTPException(
373
+ status_code=400,
374
+ detail={
375
+ "error": "Empty CSV file",
376
+ "message": "Un des fichiers CSV est vide.",
377
+ },
378
+ )
379
+ except KeyError as e:
380
+ raise HTTPException(
381
+ status_code=400,
382
+ detail={
383
+ "error": "Missing column",
384
+ "message": f"Colonne manquante dans les CSV: {e}",
385
+ },
386
+ )
387
+ except Exception as e:
388
+ logger.exception("Unexpected error during batch prediction")
389
+ raise HTTPException(
390
+ status_code=500,
391
+ detail={
392
+ "error": "Batch prediction failed",
393
+ "message": str(e),
394
+ },
395
+ )
396
+
397
+
398
  # Monter l'interface Gradio sur /ui
399
  gradio_app = create_gradio_interface()
400
  app = gr.mount_gradio_app(app, gradio_app, path="/ui")
src/config.py CHANGED
@@ -26,7 +26,7 @@ class Settings:
26
  API_KEY: str = os.getenv("API_KEY", "dev-key-change-me-in-production")
27
 
28
  # ===== API =====
29
- API_VERSION: str = os.getenv("API_VERSION", "1.0.0")
30
  API_HOST: str = os.getenv("API_HOST", "0.0.0.0")
31
  API_PORT: int = int(os.getenv("API_PORT", "8000"))
32
 
 
26
  API_KEY: str = os.getenv("API_KEY", "dev-key-change-me-in-production")
27
 
28
  # ===== API =====
29
+ API_VERSION: str = os.getenv("API_VERSION", "2.2.0")
30
  API_HOST: str = os.getenv("API_HOST", "0.0.0.0")
31
  API_PORT: int = int(os.getenv("API_PORT", "8000"))
32
 
src/preprocessing.py CHANGED
@@ -5,8 +5,7 @@ Module de preprocessing pour transformer les données d'entrée avant prédictio
5
  Ce module applique les mêmes transformations que le pipeline d'entraînement :
6
  - Feature engineering (ratios, moyennes)
7
  - Encoding (OneHot, Ordinal)
8
-
9
- Note: Pas de scaling car XGBoost est insensible à l'échelle des features.
10
  """
11
  import numpy as np
12
  import pandas as pd
@@ -14,6 +13,98 @@ from sklearn.preprocessing import OneHotEncoder, OrdinalEncoder
14
 
15
  from src.schemas import EmployeeInput
16
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
  def create_input_dataframe(employee: EmployeeInput) -> pd.DataFrame:
19
  """
@@ -119,7 +210,7 @@ def encode_and_scale(df: pd.DataFrame) -> pd.DataFrame:
119
  df: DataFrame avec features engineered.
120
 
121
  Returns:
122
- DataFrame transformé avec 50 colonnes (comme training).
123
  """
124
  df = df.copy()
125
 
@@ -184,10 +275,71 @@ def encode_and_scale(df: pd.DataFrame) -> pd.DataFrame:
184
  # Concaténer les encodages OneHot
185
  df = pd.concat([df, encoded_non_ord], axis=1)
186
 
187
- # NOTE: PAS de scaling !
188
- # XGBoost est un modèle basé sur des arbres, insensible à l'échelle.
189
- # Le scaling sur une seule observation causait des valeurs constantes
190
- # car StandardScaler.fit_transform() sur 1 ligne donne toujours 0.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
191
 
192
  return df
193
 
@@ -221,12 +373,71 @@ def preprocess_for_prediction(employee: EmployeeInput) -> np.ndarray:
221
  return df.values
222
 
223
 
224
- # TODO: Implémenter le chargement des artifacts sauvegardés
225
- # def load_preprocessing_artifacts(run_id: str) -> dict:
226
- # """
227
- # Charge les encoders et scaler depuis MLflow.
228
- #
229
- # Returns:
230
- # dict avec keys: 'onehot_encoder', 'ordinal_encoder', 'scaler'
231
- # """
232
- # pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  Ce module applique les mêmes transformations que le pipeline d'entraînement :
6
  - Feature engineering (ratios, moyennes)
7
  - Encoding (OneHot, Ordinal)
8
+ - Scaling (StandardScaler avec paramètres sauvegardés)
 
9
  """
10
  import numpy as np
11
  import pandas as pd
 
13
 
14
  from src.schemas import EmployeeInput
15
 
16
+ # Paramètres du scaler sauvegardés depuis l'entraînement
17
+ # Ces valeurs doivent correspondre exactement à celles utilisées lors du training
18
+ SCALER_PARAMS = {
19
+ "columns": [
20
+ "nombre_participation_pee",
21
+ "nb_formations_suivies",
22
+ "nombre_employee_sous_responsabilite",
23
+ "distance_domicile_travail",
24
+ "niveau_education",
25
+ "annees_depuis_la_derniere_promotion",
26
+ "annes_sous_responsable_actuel",
27
+ "satisfaction_employee_environnement",
28
+ "note_evaluation_precedente",
29
+ "niveau_hierarchique_poste",
30
+ "satisfaction_employee_nature_travail",
31
+ "satisfaction_employee_equipe",
32
+ "satisfaction_employee_equilibre_pro_perso",
33
+ "note_evaluation_actuelle",
34
+ "augementation_salaire_precedente",
35
+ "age",
36
+ "revenu_mensuel",
37
+ "nombre_experiences_precedentes",
38
+ "nombre_heures_travailless",
39
+ "annee_experience_totale",
40
+ "annees_dans_l_entreprise",
41
+ "annees_dans_le_poste_actuel",
42
+ "revenu_par_anciennete",
43
+ "experience_par_anciennete",
44
+ "satisfaction_moyenne",
45
+ "promo_par_anciennete",
46
+ "frequence_deplacement",
47
+ ],
48
+ "mean": [
49
+ 0.7938775510204081,
50
+ 2.7993197278911564,
51
+ 1.0,
52
+ 9.19251700680272,
53
+ 2.912925170068027,
54
+ 2.1789115646258503,
55
+ 4.102721088435374,
56
+ 2.721768707482993,
57
+ 2.7299319727891156,
58
+ 2.0639455782312925,
59
+ 2.7285714285714286,
60
+ 2.7122448979591836,
61
+ 2.7612244897959184,
62
+ 3.1537414965986397,
63
+ 15.209523809523809,
64
+ 36.923809523809524,
65
+ 6502.931292517007,
66
+ 2.6931972789115646,
67
+ 80.0,
68
+ 11.268707482993197,
69
+ 6.980272108843537,
70
+ 4.214965986394557,
71
+ 1170.0019803036198,
72
+ 1.9285635921785853,
73
+ 2.730952380952381,
74
+ 0.23624418065415922,
75
+ 1.0863945578231293,
76
+ ],
77
+ "scale": [
78
+ 0.8517867966287158,
79
+ 1.2888320187689346,
80
+ 1.0,
81
+ 8.104106529671768,
82
+ 1.0238165299102608,
83
+ 3.1873417003246085,
84
+ 3.502524756587405,
85
+ 1.0927103547111134,
86
+ 0.7113190741884202,
87
+ 1.1065633247112856,
88
+ 1.1024709415085499,
89
+ 1.0808410657505316,
90
+ 0.7062354909319911,
91
+ 0.3607007746349458,
92
+ 3.658692627979528,
93
+ 9.132265690615387,
94
+ 4706.355164823003,
95
+ 2.497159198593844,
96
+ 1.0,
97
+ 7.7078836108215345,
98
+ 6.0028580432875085,
99
+ 3.575242796407657,
100
+ 1353.331540788815,
101
+ 2.2050718706188372,
102
+ 0.5056427624070211,
103
+ 0.2687717006578023,
104
+ 0.5319888822661019,
105
+ ],
106
+ }
107
+
108
 
109
  def create_input_dataframe(employee: EmployeeInput) -> pd.DataFrame:
110
  """
 
210
  df: DataFrame avec features engineered.
211
 
212
  Returns:
213
+ DataFrame transformé avec 50 colonnes dans l'ordre exact du modèle.
214
  """
215
  df = df.copy()
216
 
 
275
  # Concaténer les encodages OneHot
276
  df = pd.concat([df, encoded_non_ord], axis=1)
277
 
278
+ # === RÉORDONNER LES COLONNES SELON L'ORDRE DU MODÈLE ===
279
+ # Ordre exact des features attendues par le modèle (50 colonnes)
280
+ expected_columns = [
281
+ "nombre_participation_pee",
282
+ "nb_formations_suivies",
283
+ "nombre_employee_sous_responsabilite",
284
+ "distance_domicile_travail",
285
+ "niveau_education",
286
+ "annees_depuis_la_derniere_promotion",
287
+ "annes_sous_responsable_actuel",
288
+ "satisfaction_employee_environnement",
289
+ "note_evaluation_precedente",
290
+ "niveau_hierarchique_poste",
291
+ "satisfaction_employee_nature_travail",
292
+ "satisfaction_employee_equipe",
293
+ "satisfaction_employee_equilibre_pro_perso",
294
+ "note_evaluation_actuelle",
295
+ "augementation_salaire_precedente",
296
+ "age",
297
+ "revenu_mensuel",
298
+ "nombre_experiences_precedentes",
299
+ "nombre_heures_travailless",
300
+ "annee_experience_totale",
301
+ "annees_dans_l_entreprise",
302
+ "annees_dans_le_poste_actuel",
303
+ "revenu_par_anciennete",
304
+ "experience_par_anciennete",
305
+ "satisfaction_moyenne",
306
+ "promo_par_anciennete",
307
+ "genre_F",
308
+ "genre_M",
309
+ "statut_marital_Célibataire",
310
+ "statut_marital_Divorcé(e)",
311
+ "statut_marital_Marié(e)",
312
+ "departement_Commercial",
313
+ "departement_Consulting",
314
+ "departement_Ressources Humaines",
315
+ "poste_Assistant de Direction",
316
+ "poste_Cadre Commercial",
317
+ "poste_Consultant",
318
+ "poste_Directeur Technique",
319
+ "poste_Manager",
320
+ "poste_Représentant Commercial",
321
+ "poste_Ressources Humaines",
322
+ "poste_Senior Manager",
323
+ "poste_Tech Lead",
324
+ "domaine_etude_Autre",
325
+ "domaine_etude_Entrepreunariat",
326
+ "domaine_etude_Infra & Cloud",
327
+ "domaine_etude_Marketing",
328
+ "domaine_etude_Ressources Humaines",
329
+ "domaine_etude_Transformation Digitale",
330
+ "frequence_deplacement",
331
+ ]
332
+
333
+ # Réordonner les colonnes
334
+ df = df[expected_columns]
335
+
336
+ # === SCALING ===
337
+ # Appliquer le StandardScaler avec les paramètres sauvegardés
338
+ for i, col in enumerate(SCALER_PARAMS["columns"]):
339
+ if col in df.columns:
340
+ mean = SCALER_PARAMS["mean"][i]
341
+ scale = SCALER_PARAMS["scale"][i]
342
+ df[col] = (df[col] - mean) / scale
343
 
344
  return df
345
 
 
373
  return df.values
374
 
375
 
376
+ def preprocess_dataframe_for_prediction(df: pd.DataFrame) -> pd.DataFrame:
377
+ """
378
+ Préprocess un DataFrame complet (issu de CSV fusionnés) pour prédiction batch.
379
+
380
+ Args:
381
+ df: DataFrame avec toutes les colonnes nécessaires.
382
+
383
+ Returns:
384
+ DataFrame transformé prêt pour model.predict().
385
+ """
386
+ # Feature engineering
387
+ df_processed = engineer_features(df)
388
+
389
+ # Encoding et scaling
390
+ df_processed = encode_and_scale(df_processed)
391
+
392
+ return df_processed
393
+
394
+
395
+ def merge_csv_dataframes(
396
+ sondage_df: pd.DataFrame,
397
+ eval_df: pd.DataFrame,
398
+ sirh_df: pd.DataFrame,
399
+ ) -> pd.DataFrame:
400
+ """
401
+ Fusionne les 3 DataFrames CSV comme lors de l'entraînement.
402
+
403
+ Args:
404
+ sondage_df: DataFrame du fichier sondage.
405
+ eval_df: DataFrame du fichier évaluation.
406
+ sirh_df: DataFrame du fichier SIRH.
407
+
408
+ Returns:
409
+ DataFrame fusionné avec toutes les colonnes.
410
+ """
411
+ # Nettoyage de l'évaluation
412
+ eval_df = eval_df.copy()
413
+ eval_df["augementation_salaire_precedente"] = eval_df[
414
+ "augementation_salaire_precedente"
415
+ ].apply(lambda x: float(str(x).replace(" %", "")) if isinstance(x, str) else x)
416
+ eval_df["employee_id"] = eval_df["eval_number"].apply(
417
+ lambda x: int(str(x).replace("E_", "")) if isinstance(x, str) else x
418
+ )
419
+
420
+ # Nettoyage du sondage
421
+ sondage_df = sondage_df.copy()
422
+ sondage_df["employee_id"] = sondage_df["code_sondage"].apply(
423
+ lambda x: int(x) if isinstance(x, (str, int)) else None
424
+ )
425
+
426
+ # Fusion
427
+ central_df = pd.merge(sondage_df, eval_df, on="employee_id", how="inner")
428
+ central_df = pd.merge(
429
+ central_df, sirh_df, left_on="employee_id", right_on="id_employee", how="inner"
430
+ )
431
+
432
+ # Conserver l'ID pour le retour
433
+ central_df["original_employee_id"] = central_df["employee_id"]
434
+
435
+ # Supprimer les colonnes de jointure
436
+ central_df.drop(
437
+ ["code_sondage", "eval_number", "id_employee", "employee_id"],
438
+ axis=1,
439
+ inplace=True,
440
+ errors="ignore",
441
+ )
442
+
443
+ return central_df
src/schemas.py CHANGED
@@ -248,3 +248,52 @@ class HealthCheck(BaseModel):
248
  "version": "1.0.0",
249
  }
250
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
248
  "version": "1.0.0",
249
  }
250
  }
251
+
252
+
253
+ class EmployeePrediction(BaseModel):
254
+ """Prédiction pour un employé dans le batch."""
255
+
256
+ employee_id: int = Field(..., description="ID de l'employé")
257
+ prediction: int = Field(..., description="Classe prédite (0=reste, 1=part)")
258
+ probability_stay: float = Field(
259
+ ..., ge=0, le=1, description="Probabilité de rester"
260
+ )
261
+ probability_leave: float = Field(
262
+ ..., ge=0, le=1, description="Probabilité de partir"
263
+ )
264
+ risk_level: str = Field(..., description="Niveau de risque (Low/Medium/High)")
265
+
266
+
267
+ class BatchPredictionOutput(BaseModel):
268
+ """Schéma de sortie pour les prédictions par lots (CSV)."""
269
+
270
+ total_employees: int = Field(..., description="Nombre total d'employés traités")
271
+ predictions: list[EmployeePrediction] = Field(
272
+ ..., description="Liste des prédictions"
273
+ )
274
+ summary: dict = Field(..., description="Résumé des prédictions")
275
+
276
+ class Config:
277
+ """Configuration Pydantic."""
278
+
279
+ json_schema_extra = {
280
+ "example": {
281
+ "total_employees": 100,
282
+ "predictions": [
283
+ {
284
+ "employee_id": 1,
285
+ "prediction": 0,
286
+ "probability_stay": 0.85,
287
+ "probability_leave": 0.15,
288
+ "risk_level": "Low",
289
+ }
290
+ ],
291
+ "summary": {
292
+ "total_stay": 80,
293
+ "total_leave": 20,
294
+ "high_risk_count": 15,
295
+ "medium_risk_count": 10,
296
+ "low_risk_count": 75,
297
+ },
298
+ }
299
+ }