Spaces:
Runtime error
Runtime error
Merge branch 'main' of https://github.com/Diaure/Futurisys_ML_API
Browse files- .github/workflows/ci.yml +9 -10
- .gitignore +1 -1
- App/database.py +30 -0
- App/main.py +4 -0
- App/model.py +51 -0
- App/model/variables_entree.json +0 -1
- App/predict.py +57 -8
- App/schemas.py +0 -1
- Dockerfile +13 -0
- README.md +181 -48
- poetry.lock +0 -0
- pyproject.toml +10 -2
- requirements.txt +8 -1
- scripts/create_tables.py +6 -0
- scripts/dataset_final.csv +0 -0
- scripts/insert_dataset.py +25 -0
.github/workflows/ci.yml
CHANGED
|
@@ -16,16 +16,15 @@ jobs:
|
|
| 16 |
steps:
|
| 17 |
- uses: actions/setup-python@v5
|
| 18 |
with:
|
| 19 |
-
python-version: "3.11"
|
| 20 |
|
| 21 |
- uses: actions/checkout@v4
|
| 22 |
|
| 23 |
-
- name: Install
|
| 24 |
-
run:
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
- name: Run tests
|
| 30 |
-
run: pytest
|
| 31 |
-
|
|
|
|
| 16 |
steps:
|
| 17 |
- uses: actions/setup-python@v5
|
| 18 |
with:
|
| 19 |
+
python-version: "3.11.9"
|
| 20 |
|
| 21 |
- uses: actions/checkout@v4
|
| 22 |
|
| 23 |
+
- name: Install Poetry
|
| 24 |
+
run: pip install poetry
|
| 25 |
+
|
| 26 |
+
- name: Install dependencies with Poetry
|
| 27 |
+
run: poetry install --no-interaction --no-root
|
| 28 |
+
|
| 29 |
+
- name: Run tests
|
| 30 |
+
run: poetry run pytest
|
|
|
.gitignore
CHANGED
|
@@ -7,4 +7,4 @@ venv/
|
|
| 7 |
App/model/
|
| 8 |
*.joblib
|
| 9 |
*.json
|
| 10 |
-
App/model/modele_final_xgb.joblib
|
|
|
|
| 7 |
App/model/
|
| 8 |
*.joblib
|
| 9 |
*.json
|
| 10 |
+
App/model/modele_final_xgb.joblib
|
App/database.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from dotenv import load_dotenv
|
| 3 |
+
from sqlalchemy import create_engine
|
| 4 |
+
from sqlalchemy.orm import sessionmaker, declarative_base
|
| 5 |
+
|
| 6 |
+
load_dotenv()
|
| 7 |
+
|
| 8 |
+
# Détection si on est en CI (GitHub Actions) ou en test
|
| 9 |
+
IS_CI = os.getenv("CI") == "true"
|
| 10 |
+
IS_PYTEST = "pytest" in os.getenv("PYTHONPATH", "") or os.getenv("PYTEST_CURRENT_TEST") is not None
|
| 11 |
+
|
| 12 |
+
SKIP_DB = IS_CI or IS_PYTEST
|
| 13 |
+
|
| 14 |
+
DB_USER = os.getenv("DB_USER", "postgres")
|
| 15 |
+
DB_PASSWORD = os.getenv("DB_PASSWORD", "password")
|
| 16 |
+
DB_HOST = os.getenv("DB_HOST", "localhost")
|
| 17 |
+
DB_PORT = os.getenv("DB_PORT", "5432")
|
| 18 |
+
DB_NAME = os.getenv("DB_NAME", "test_db")
|
| 19 |
+
|
| 20 |
+
DATABASE_URL = (f"postgresql+psycopg2://{DB_USER}:{DB_PASSWORD}"f"@{DB_HOST}:{DB_PORT}/{DB_NAME}")
|
| 21 |
+
|
| 22 |
+
Base = declarative_base()
|
| 23 |
+
|
| 24 |
+
if not SKIP_DB:
|
| 25 |
+
engine = create_engine(DATABASE_URL)
|
| 26 |
+
SessionLocal = sessionmaker(autocommit = False, autoflush = False, bind = engine)
|
| 27 |
+
|
| 28 |
+
else:
|
| 29 |
+
engine = None
|
| 30 |
+
SessionLocal = None
|
App/main.py
CHANGED
|
@@ -8,6 +8,10 @@ app = FastAPI(
|
|
| 8 |
version="0.1.0"
|
| 9 |
)
|
| 10 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
@app.post("/predict")
|
| 12 |
def predict(data: EmployeeFeatures):
|
| 13 |
"""
|
|
|
|
| 8 |
version="0.1.0"
|
| 9 |
)
|
| 10 |
|
| 11 |
+
@app.get("/")
|
| 12 |
+
def root():
|
| 13 |
+
return {"status": "API OK"}
|
| 14 |
+
|
| 15 |
@app.post("/predict")
|
| 16 |
def predict(data: EmployeeFeatures):
|
| 17 |
"""
|
App/model.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sqlalchemy import Column, Integer, String, Float, Boolean, DateTime, ForeignKey
|
| 2 |
+
from sqlalchemy.sql import func
|
| 3 |
+
from App.database import Base
|
| 4 |
+
|
| 5 |
+
class Input(Base):
|
| 6 |
+
__tablename__ = "inputs"
|
| 7 |
+
|
| 8 |
+
id = Column(Integer, primary_key=True, index=True)
|
| 9 |
+
genre = Column(String)
|
| 10 |
+
statut_marital = Column(String)
|
| 11 |
+
departement = Column(String)
|
| 12 |
+
poste = Column(String)
|
| 13 |
+
domaine_etude = Column(String)
|
| 14 |
+
frequence_deplacement = Column(String)
|
| 15 |
+
heure_supplementaires = Column(Boolean)
|
| 16 |
+
evolution_cat_evol = Column(String)
|
| 17 |
+
categorie_employe = Column(String)
|
| 18 |
+
satisfaction_employee_nature_travail = Column(Integer)
|
| 19 |
+
nombre_participation_pee = Column(Integer)
|
| 20 |
+
ecart_note_evaluation = Column(Integer)
|
| 21 |
+
revenu_mensuel = Column(Integer)
|
| 22 |
+
distance_domicile_travail = Column(Integer)
|
| 23 |
+
satisfaction_globale = Column(Float)
|
| 24 |
+
niveau_education = Column(Integer)
|
| 25 |
+
note_evaluation_actuelle = Column(Integer)
|
| 26 |
+
satisfaction_employee_equipe = Column(Integer)
|
| 27 |
+
age = Column(Integer)
|
| 28 |
+
revenu_par_annee_experience_interne = Column(Integer)
|
| 29 |
+
satisfaction_employee_equilibre_pro_perso = Column(Integer)
|
| 30 |
+
nombre_experiences_precedentes = Column(Integer)
|
| 31 |
+
annees_dans_l_entreprise = Column(Integer)
|
| 32 |
+
nb_formations_suivies = Column(Integer)
|
| 33 |
+
revenu_par_annee_experience_totale = Column(Integer)
|
| 34 |
+
ratio_sans_promotion = Column(Integer)
|
| 35 |
+
satisfaction_employee_environnement = Column(Integer)
|
| 36 |
+
exp_hors_entreprise = Column(Integer)
|
| 37 |
+
mobilite_promotion = Column(Integer)
|
| 38 |
+
annees_depuis_la_derniere_promotion = Column(Integer)
|
| 39 |
+
|
| 40 |
+
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
| 41 |
+
|
| 42 |
+
class Predictions(Base):
|
| 43 |
+
__tablename__ = "predictions"
|
| 44 |
+
id = Column(Integer, primary_key=True, index=True)
|
| 45 |
+
input_id = Column(Integer, ForeignKey("inputs.id"))
|
| 46 |
+
|
| 47 |
+
prediction_label = Column(String)
|
| 48 |
+
prediction_proba = Column(Float)
|
| 49 |
+
model_version = Column(String)
|
| 50 |
+
|
| 51 |
+
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
App/model/variables_entree.json
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
["genre", "statut_marital", "departement", "poste", "domaine_etude", "frequence_deplacement", "heure_supplementaires", "evolution_cat_evol", "categorie_employe", "satisfaction_employee_nature_travail", "nombre_participation_pee", "ecart_note_evaluation", "revenu_mensuel", "distance_domicile_travail", "satisfaction_globale", "niveau_education", "note_evaluation_actuelle", "satisfaction_employee_equipe", "age", "revenu_par_annee_experience_interne", "satisfaction_employee_equilibre_pro_perso", "nombre_experiences_precedentes", "annees_dans_l_entreprise", "nb_formations_suivies", "revenu_par_annee_experience_totale", "ratio_sans_promotion", "satisfaction_employee_environnement", "exp_hors_entreprise", "mobilite_promotion", "annees_depuis_la_derniere_promotion"]
|
|
|
|
|
|
App/predict.py
CHANGED
|
@@ -2,16 +2,43 @@ import joblib
|
|
| 2 |
import pandas as pd
|
| 3 |
from App.schemas import EmployeeFeatures
|
| 4 |
import json
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
|
|
|
|
| 6 |
|
| 7 |
-
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
-
FEATURES = list(EmployeeFeatures.model_fields.keys())
|
| 10 |
-
with open("App/model/mapping_classes.json") as f:
|
| 11 |
-
CLASS_MAPPING = json.load(f)
|
| 12 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
def predict_employee(data: dict):
|
| 14 |
-
|
|
|
|
|
|
|
| 15 |
|
| 16 |
print("Colonnes API :", df.columns.tolist())
|
| 17 |
print("Nombre colonnes API :", len(df.columns))
|
|
@@ -19,7 +46,29 @@ def predict_employee(data: dict):
|
|
| 19 |
pred = model.predict(df)[0]
|
| 20 |
proba = model.predict_proba(df)[0][1]
|
| 21 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
return {
|
| 23 |
-
"Prediction":
|
| 24 |
-
"Probabilite_depart": float(proba)
|
| 25 |
-
}
|
|
|
|
| 2 |
import pandas as pd
|
| 3 |
from App.schemas import EmployeeFeatures
|
| 4 |
import json
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from huggingface_hub import hf_hub_download
|
| 7 |
+
from sqlalchemy.orm import Session
|
| 8 |
+
from App.database import SessionLocal
|
| 9 |
+
from App.model import Input, Predictions
|
| 10 |
|
| 11 |
+
MODEL_REPO = "Diaure/xgb_model"
|
| 12 |
|
| 13 |
+
# Variables chargées
|
| 14 |
+
model = None
|
| 15 |
+
classes_mapping = None
|
| 16 |
+
Features = list(EmployeeFeatures.model_fields.keys())
|
| 17 |
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
+
|
| 20 |
+
# Chargement des fichiers: fonction pour charger le modèle, le mapping afin de permettre à l'API de démarrer m^me si les éléments ne sont pas présents
|
| 21 |
+
def files_load():
|
| 22 |
+
global model, classes_mapping
|
| 23 |
+
|
| 24 |
+
if model is None:
|
| 25 |
+
chemin_model = Path(hf_hub_download(repo_id=MODEL_REPO, filename="modele_final_xgb.joblib"))
|
| 26 |
+
# if not chemin_model.exists():
|
| 27 |
+
# raise RuntimeError("Eléments du modèle introuvable.")
|
| 28 |
+
model =joblib.load(chemin_model)
|
| 29 |
+
|
| 30 |
+
if classes_mapping is None:
|
| 31 |
+
chemin_mapping = Path(hf_hub_download(repo_id=MODEL_REPO, filename="mapping_classes.json"))
|
| 32 |
+
# if not chemin_mapping.exists():
|
| 33 |
+
# raise RuntimeError("Mapping des classes introuvable.")
|
| 34 |
+
with open(chemin_mapping) as f:
|
| 35 |
+
classes_mapping = json.load(f)
|
| 36 |
+
|
| 37 |
+
# Fonction prédiction
|
| 38 |
def predict_employee(data: dict):
|
| 39 |
+
files_load()
|
| 40 |
+
|
| 41 |
+
df = pd.DataFrame([data])[Features]
|
| 42 |
|
| 43 |
print("Colonnes API :", df.columns.tolist())
|
| 44 |
print("Nombre colonnes API :", len(df.columns))
|
|
|
|
| 46 |
pred = model.predict(df)[0]
|
| 47 |
proba = model.predict_proba(df)[0][1]
|
| 48 |
|
| 49 |
+
db: Session = SessionLocal() if SessionLocal is not None else None
|
| 50 |
+
|
| 51 |
+
if db is not None:
|
| 52 |
+
try:
|
| 53 |
+
# enregistrer les inputs: à chaque appel de POST/predict, on stocke d'abord les entrées de l'utilisateur
|
| 54 |
+
input_row = Input(**data)
|
| 55 |
+
db.add(input_row)
|
| 56 |
+
db.commit()
|
| 57 |
+
db.refresh(input_row)
|
| 58 |
+
|
| 59 |
+
# puis on récupère les ids générés automatiquement et enregistre les prédictions liés aux ids
|
| 60 |
+
pred_row = Predictions(input_id = input_row.id, prediction_label = classes_mapping[str(pred)], prediction_proba = float(proba), model_version = "v1")
|
| 61 |
+
db.add(pred_row)
|
| 62 |
+
db.commit()
|
| 63 |
+
|
| 64 |
+
except Exception as e:
|
| 65 |
+
print("🔥 ERREUR DB :", e)
|
| 66 |
+
raise e
|
| 67 |
+
|
| 68 |
+
finally:
|
| 69 |
+
db.close()
|
| 70 |
+
|
| 71 |
+
# puis on renvoie la réponse API
|
| 72 |
return {
|
| 73 |
+
"Prediction": classes_mapping[str(pred)],
|
| 74 |
+
"Probabilite_depart": float(proba)}
|
|
|
App/schemas.py
CHANGED
|
@@ -10,7 +10,6 @@ class EmployeeFeatures(BaseModel):
|
|
| 10 |
heure_supplementaires: bool
|
| 11 |
evolution_cat_evol: str
|
| 12 |
categorie_employe: str
|
| 13 |
-
|
| 14 |
satisfaction_employee_nature_travail: int
|
| 15 |
nombre_participation_pee: int
|
| 16 |
ecart_note_evaluation: int
|
|
|
|
| 10 |
heure_supplementaires: bool
|
| 11 |
evolution_cat_evol: str
|
| 12 |
categorie_employe: str
|
|
|
|
| 13 |
satisfaction_employee_nature_travail: int
|
| 14 |
nombre_participation_pee: int
|
| 15 |
ecart_note_evaluation: int
|
Dockerfile
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# force rebuild
|
| 2 |
+
FROM python:3.11
|
| 3 |
+
|
| 4 |
+
WORKDIR /code
|
| 5 |
+
|
| 6 |
+
COPY requirements.txt .
|
| 7 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 8 |
+
|
| 9 |
+
COPY . .
|
| 10 |
+
|
| 11 |
+
EXPOSE 7860
|
| 12 |
+
|
| 13 |
+
CMD ["uvicorn", "App.main:app", "--host", "0.0.0.0", "--port", "7860"]
|
README.md
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
# Futurisys – Déploiement d’un modèle de Machine Learning via API
|
| 2 |
|
| 3 |
## Contexte
|
|
@@ -5,9 +15,7 @@
|
|
| 5 |
opérationnels et accessibles via une API performante.
|
| 6 |
|
| 7 |
Ce projet correspond à un **Proof of Concept (POC)** visant à déployer un modèle de machine
|
| 8 |
-
learning en production en appliquant les bonnes pratiques d’ingénierie logicielle
|
| 9 |
-
versionnage, tests, base de données et automatisation.
|
| 10 |
-
|
| 11 |
|
| 12 |
|
| 13 |
## Objectifs du projet
|
|
@@ -19,7 +27,7 @@ versionnage, tests, base de données et automatisation.
|
|
| 19 |
|
| 20 |
|
| 21 |
## Périmètre fonctionnel
|
| 22 |
-
Le projet inclut
|
| 23 |
- Une API développée avec **FastAPI**
|
| 24 |
- L’exposition d’un modèle de machine learning via des endpoints REST
|
| 25 |
- Une base de données **PostgreSQL** pour stocker les entrées/sorties du modèle
|
|
@@ -27,53 +35,75 @@ Le projet inclut :
|
|
| 27 |
- Un pipeline **CI/CD** pour automatiser les tests et le déploiement
|
| 28 |
- Une documentation technique claire
|
| 29 |
|
| 30 |
-
## CI/CD et
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
|
| 32 |
-
|
| 33 |
|
| 34 |
-
À chaque push sur les branches de travail et à chaque pull request vers
|
| 35 |
le pipeline exécute automatiquement les étapes suivantes :
|
| 36 |
- installation d’un environnement Python 3.11 isolé
|
| 37 |
- installation des dépendances définies dans le projet
|
| 38 |
-
- exécution des tests
|
| 39 |
|
| 40 |
-
L’objectif est de
|
| 41 |
-
- le projet
|
| 42 |
-
-
|
| 43 |
-
-
|
|
|
|
| 44 |
|
| 45 |
-
|
| 46 |
|
| 47 |
-
|
| 48 |
-
- un schéma d’entrée validé avec **Pydantic**
|
| 49 |
-
- un préprocesseur entraîné et sauvegardé
|
| 50 |
-
- un modèle de machine learning sérialisé avec **joblib**
|
| 51 |
|
| 52 |
-
|
| 53 |
-
-
|
| 54 |
-
-
|
| 55 |
-
- `mapping_classes.json`
|
| 56 |
|
| 57 |
-
|
| 58 |
|
| 59 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
|
| 61 |
-
```bash
|
| 62 |
-
uvicorn App.main:app --reload --log-level debug
|
| 63 |
-
```
|
| 64 |
-
L’API est alors accessible à l’adresse http://127.0.0.1:8000/
|
| 65 |
|
| 66 |
-
|
| 67 |
|
| 68 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
`POST /predict`
|
| 70 |
|
| 71 |
-
Cet endpoint reçoit les caractéristiques d’un
|
| 72 |
|
| 73 |
- une prédiction lisible ("Reste" ou "Part")
|
| 74 |
- la probabilité associée au départ
|
| 75 |
|
| 76 |
-
Exemple de réponse
|
| 77 |
```json
|
| 78 |
{
|
| 79 |
"Prediction": "Part",
|
|
@@ -83,26 +113,121 @@ Exemple de réponse :
|
|
| 83 |
Les données d’entrée sont validées automatiquement avant l’appel au modèle,
|
| 84 |
garantissant la cohérence avec les variables utilisées lors de l’entraînement.
|
| 85 |
|
| 86 |
-
|
| 87 |
|
| 88 |
L’API expose un endpoint principal de prédiction.
|
| 89 |
|
| 90 |
**POST /predict**
|
| 91 |
- Description : retourne une prédiction de départ d’un employé
|
| 92 |
-
- Validation des données
|
| 93 |
-
- Réponses possibles
|
| 94 |
-
- 200
|
| 95 |
-
- 422
|
| 96 |
|
| 97 |
-
##
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
-
|
| 102 |
-
-
|
| 103 |
-
-
|
| 104 |
-
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
|
| 107 |
|
| 108 |
## Structure du projet
|
|
@@ -111,7 +236,9 @@ futurisys_ml-api/
|
|
| 111 |
├── github/workflows
|
| 112 |
│ ├── ci.yml # Description des évènement déclenchants des tests
|
| 113 |
├── app/ # Code applicatif principal
|
|
|
|
| 114 |
│ ├── main.py # Point d’entrée de l’API
|
|
|
|
| 115 |
│ ├── predict.py # Application du modèle
|
| 116 |
│ ├── schemas.py # Validation des données (Pydantic)
|
| 117 |
│ ── model/ # Elements du modèle
|
|
@@ -119,12 +246,18 @@ futurisys_ml-api/
|
|
| 119 |
│ ├── modele_final_xgb.joblib # Modèle final avec hyperparamètres
|
| 120 |
│ ├── preprocesseur_fitted.joblib # Pipeline entrainé
|
| 121 |
|
|
| 122 |
-
├── scripts/
|
| 123 |
-
├──
|
|
|
|
|
|
|
|
|
|
| 124 |
│ ├── test_api.py # Test automatisé de l'API via Pytest
|
| 125 |
|
|
|
|
|
| 126 |
├── .gitignore # Nettoyage du dépôt
|
| 127 |
-
├──
|
|
|
|
|
|
|
| 128 |
├── README.md # Présentation du projet
|
| 129 |
-
└── requirements.txt # Librairies
|
| 130 |
```
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Futurisys ML API
|
| 3 |
+
emoji: 🚀
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: green
|
| 6 |
+
sdk: docker
|
| 7 |
+
pinned: false
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
|
| 11 |
# Futurisys – Déploiement d’un modèle de Machine Learning via API
|
| 12 |
|
| 13 |
## Contexte
|
|
|
|
| 15 |
opérationnels et accessibles via une API performante.
|
| 16 |
|
| 17 |
Ce projet correspond à un **Proof of Concept (POC)** visant à déployer un modèle de machine
|
| 18 |
+
learning en production en appliquant les bonnes pratiques d’ingénierie logicielle: versionnage, tests, base de données et automatisation.
|
|
|
|
|
|
|
| 19 |
|
| 20 |
|
| 21 |
## Objectifs du projet
|
|
|
|
| 27 |
|
| 28 |
|
| 29 |
## Périmètre fonctionnel
|
| 30 |
+
Le projet inclut:
|
| 31 |
- Une API développée avec **FastAPI**
|
| 32 |
- L’exposition d’un modèle de machine learning via des endpoints REST
|
| 33 |
- Une base de données **PostgreSQL** pour stocker les entrées/sorties du modèle
|
|
|
|
| 35 |
- Un pipeline **CI/CD** pour automatiser les tests et le déploiement
|
| 36 |
- Une documentation technique claire
|
| 37 |
|
| 38 |
+
## CI/CD et Déploiement
|
| 39 |
+
|
| 40 |
+
Ce projet met en œuvre une approche CI/CD complète, séparant:
|
| 41 |
+
- l’intégration continue (**CI**): garantir la qualité du code
|
| 42 |
+
- le déploiement continu (**CD**): rendre l’API accessible publiquement
|
| 43 |
+
|
| 44 |
+
### `Intégration Continue (CI) – GitHub Actions`
|
| 45 |
|
| 46 |
+
L’intégration continue est assurée via GitHub Actions.
|
| 47 |
|
| 48 |
+
À chaque **push** sur les branches de travail et à chaque **pull request** vers **`develop`**,
|
| 49 |
le pipeline exécute automatiquement les étapes suivantes :
|
| 50 |
- installation d’un environnement Python 3.11 isolé
|
| 51 |
- installation des dépendances définies dans le projet
|
| 52 |
+
- exécution des tests automatisés avec Pytest
|
| 53 |
|
| 54 |
+
L’objectif est de:
|
| 55 |
+
- vérifier que le projet est installable
|
| 56 |
+
- garantir que l’API démarre correctement
|
| 57 |
+
- valider le chargement du modèle et le endpoint /*`predict`*
|
| 58 |
+
- éviter toute régression avant fusion vers **`develop`**.
|
| 59 |
|
| 60 |
+
### `Déploiement Continu (CD) – Hugging Face Spaces`
|
| 61 |
|
| 62 |
+
Le déploiement de l’API est réalisé sur Hugging Face Spaces qui permet:
|
|
|
|
|
|
|
|
|
|
| 63 |
|
| 64 |
+
- d’héberger gratuitement des applications ML
|
| 65 |
+
- de déployer une API Dockerisée
|
| 66 |
+
- d’exposer un service accessible publiquement sans gérer de serveur
|
|
|
|
| 67 |
|
| 68 |
+
Dans ce projet, Hugging Face est utilisé comme plateforme de démonstration et de mise à disposition de l’API.
|
| 69 |
|
| 70 |
+
Le déploiement repose sur un Dockerfile, qui définit :
|
| 71 |
+
- l’image Python utilisée (Python 3.11)
|
| 72 |
+
- l’installation des dépendances
|
| 73 |
+
- le lancement de l’API avec Uvicorn
|
| 74 |
+
|
| 75 |
+
Il garantit la reproductibilité de l'environnement lors de l'exécution de l'API.
|
| 76 |
+
|
| 77 |
+
A noter que les ***fichiers binaires*** ne sont pas stochés dans le dépôt GiHub principal pour les raisons suivantes:
|
| 78 |
+
- Hugging Face bloque les push Git contenant des fichiers binaires lourds
|
| 79 |
+
- Git n’est pas conçu pour versionner des artefacts ML volumineux.
|
| 80 |
+
|
| 81 |
+
Pour contourner la situation, dans le projet, les artefacts sont stockés dans un Space Hugging Face dédié, séparé du code. Lors du démarrage de lAPI:
|
| 82 |
+
- le code télécharge dynamiquement les artefacts via huggingface_hub
|
| 83 |
+
- l’API peut démarrer même si les fichiers ne sont pas présents localement
|
| 84 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
|
| 86 |
+
### `Lancer l’API en local`
|
| 87 |
|
| 88 |
+
L’API est déployée publiquement sur Hugging Face Spaces.
|
| 89 |
+
|
| 90 |
+
- URL de l’API :
|
| 91 |
+
https://diaure-futurisys-ml-api.hf.space
|
| 92 |
+
- Documentation interactive (Swagger UI):
|
| 93 |
+
https://diaure-futurisys-ml-api.hf.space/docs. Ele permet de:
|
| 94 |
+
- visualiser les endpoints
|
| 95 |
+
- tester directement l’endpoint `/predict`
|
| 96 |
+
- voir les schémas d’entrée et de sortie.
|
| 97 |
+
|
| 98 |
+
### `Endpoint principal`
|
| 99 |
`POST /predict`
|
| 100 |
|
| 101 |
+
Cet endpoint reçoit les caractéristiques d’un employ�� et retourne:
|
| 102 |
|
| 103 |
- une prédiction lisible ("Reste" ou "Part")
|
| 104 |
- la probabilité associée au départ
|
| 105 |
|
| 106 |
+
Exemple de réponse:
|
| 107 |
```json
|
| 108 |
{
|
| 109 |
"Prediction": "Part",
|
|
|
|
| 113 |
Les données d’entrée sont validées automatiquement avant l’appel au modèle,
|
| 114 |
garantissant la cohérence avec les variables utilisées lors de l’entraînement.
|
| 115 |
|
| 116 |
+
### `Documentation des endpoints`
|
| 117 |
|
| 118 |
L’API expose un endpoint principal de prédiction.
|
| 119 |
|
| 120 |
**POST /predict**
|
| 121 |
- Description : retourne une prédiction de départ d’un employé
|
| 122 |
+
- Validation des données: Pydantic
|
| 123 |
+
- Réponses possibles:
|
| 124 |
+
- 200: prédiction valide
|
| 125 |
+
- 422: données invalides
|
| 126 |
|
| 127 |
+
## Base de données et traçabilité des prédictions
|
| 128 |
+
### `Objectifs`
|
| 129 |
+
|
| 130 |
+
L’intégration d’une base de données PostgreSQL permet d’inscrire le projet dans une logique MLOps et de répondre à plusieurs objectifs clés:
|
| 131 |
+
- assurer la traçabilité complète des prédictions du modèle
|
| 132 |
+
- conserver l’historique des données d’entrée utilisateur
|
| 133 |
+
- stocker les résultats de prédiction (label, probabilité, version du modèle)
|
| 134 |
+
- préparer une architecture compatible avec un déploiement en production.
|
| 135 |
+
|
| 136 |
+
### `Méthodologie utilisée`
|
| 137 |
+
- **PostgreSQL** a été retenu pour:
|
| 138 |
+
- sa robustesse et sa fiabilité
|
| 139 |
+
- sa compatibilité native avec SQLAlchemy
|
| 140 |
+
- son usage courant en environnement professionnel
|
| 141 |
+
|
| 142 |
+
- **SQLAlchemy** est utilisé comme couche d’abstraction:
|
| 143 |
+
- gestion centralisée de la connexion à la base
|
| 144 |
+
- cohérence entre le schéma Python et la base SQL
|
| 145 |
+
|
| 146 |
+
Les identifiants de connexion sont stockés dans des variables d’environnement (`.env`) afin d’éviter toute exposition de secrets dans le dépôt Git.
|
| 147 |
+
|
| 148 |
+
### `Modélisation de la base de données`
|
| 149 |
+
La base de données repose sur trois tables distinctes, chacune ayant un rôle précis.
|
| 150 |
+
1. `employees_dataset - Dataset de référence`
|
| 151 |
+
Il contient le dataset final nettoyé et préparé lors de l'entraînement du modèle en incluant l'ensemble des **32 deatures** du modèle. Il sert de:
|
| 152 |
+
- référence de schéma
|
| 153 |
+
- source de validation
|
| 154 |
+
- base documentaire du modèle
|
| 155 |
|
| 156 |
+
C'est une table qui n'est jamais alimentée par l'utilisateur.
|
| 157 |
+
|
| 158 |
+
```python
|
| 159 |
+
load_dotenv()
|
| 160 |
+
|
| 161 |
+
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 162 |
+
csv_path = os.path.join(BASE_DIR, "dataset_final.csv")
|
| 163 |
+
|
| 164 |
+
df = pd.read_csv(csv_path, encoding="latin-1")
|
| 165 |
+
|
| 166 |
+
DB_USER = os.getenv("DB_USER")
|
| 167 |
+
DB_PASSWORD = os.getenv("DB_PASSWORD")
|
| 168 |
+
DB_HOST = os.getenv("DB_HOST")
|
| 169 |
+
DB_PORT = os.getenv("DB_PORT")
|
| 170 |
+
DB_NAME = os.getenv("DB_NAME")
|
| 171 |
+
|
| 172 |
+
DATABASE_URL = (f"postgresql+psycopg2://{DB_USER}:{DB_PASSWORD}"f"@{DB_HOST}:{DB_PORT}/{DB_NAME}")
|
| 173 |
+
|
| 174 |
+
engine = create_engine(DATABASE_URL)
|
| 175 |
+
|
| 176 |
+
df.to_sql("employees_dataset", engine, if_exists="replace", index=False)
|
| 177 |
+
```
|
| 178 |
+
|
| 179 |
+
2. `inputs - Entrées utilisateur`
|
| 180 |
+
- Enregistre chaque requête utilisateur envoyée à l'endpoint `/predict`
|
| 181 |
+
- Contient exactement les features attendues par le modèle
|
| 182 |
+
- Structure strictement alignée avec le schéma Pydandic(`EmployeeFeatures`)
|
| 183 |
+
- Permet:
|
| 184 |
+
- l'audit des predictions
|
| 185 |
+
- l'analyse à posteriori
|
| 186 |
+
- la reproductibilité des résultats.
|
| 187 |
+
```python
|
| 188 |
+
class Input(Base):
|
| 189 |
+
__tablename__ = "inputs"
|
| 190 |
+
|
| 191 |
+
id = Column(Integer, primary_key=True, index=True)
|
| 192 |
+
genre = Column(String)
|
| 193 |
+
statut_marital = Column(String)
|
| 194 |
+
departement = Column(String)
|
| 195 |
+
poste = Column(String)
|
| 196 |
+
```
|
| 197 |
+
|
| 198 |
+
3. `predictions - Résultats du modèle`
|
| 199 |
+
- Continet:
|
| 200 |
+
- le label de prédiction
|
| 201 |
+
- la probabilité associée
|
| 202 |
+
- Reliée à `inputs` via une clé étrangère
|
| 203 |
+
- Garantit une trçabilité complète.
|
| 204 |
+
```python
|
| 205 |
+
class Predictions(Base):
|
| 206 |
+
__tablename__ = "predictions"
|
| 207 |
+
id = Column(Integer, primary_key=True, index=True)
|
| 208 |
+
input_id = Column(Integer, ForeignKey("inputs.id"))
|
| 209 |
+
|
| 210 |
+
prediction_label = Column(String)
|
| 211 |
+
prediction_proba = Column(Float)
|
| 212 |
+
model_version = Column(String)
|
| 213 |
+
```
|
| 214 |
+
|
| 215 |
+
### `Interaction API <> Base de données`
|
| 216 |
+
Lors d’un appel à l’endpoint `POST /predict`:
|
| 217 |
+
- les données utilisateur sont validées via **Pydantic**
|
| 218 |
+
- les entrées sont enregistrées dans la table **inputs**
|
| 219 |
+
- le modèle est exécuté
|
| 220 |
+
- la prédiction est enregistrée dans la table **predictions**
|
| 221 |
+
- la réponse est retournée à l’utilisateur.
|
| 222 |
+
|
| 223 |
+
## Stack technique
|
| 224 |
+
- **Langage**: Python
|
| 225 |
+
- **API**: FastAPI
|
| 226 |
+
- **Machine Learning**: scikit-learn
|
| 227 |
+
- **Base de données**: PostgreSQL
|
| 228 |
+
- **Tests**: Pytest, pytest-cov
|
| 229 |
+
- **CI/CD**: GitHub Actions
|
| 230 |
+
- **Versionnage**: Git / GitHub
|
| 231 |
|
| 232 |
|
| 233 |
## Structure du projet
|
|
|
|
| 236 |
├── github/workflows
|
| 237 |
│ ├── ci.yml # Description des évènement déclenchants des tests
|
| 238 |
├── app/ # Code applicatif principal
|
| 239 |
+
│ ├── database.py # Point de connexion à la base PostgreSQL
|
| 240 |
│ ├── main.py # Point d’entrée de l’API
|
| 241 |
+
│ ├── model.py # Définition des tables de la database
|
| 242 |
│ ├── predict.py # Application du modèle
|
| 243 |
│ ├── schemas.py # Validation des données (Pydantic)
|
| 244 |
│ ── model/ # Elements du modèle
|
|
|
|
| 246 |
│ ├── modele_final_xgb.joblib # Modèle final avec hyperparamètres
|
| 247 |
│ ├── preprocesseur_fitted.joblib # Pipeline entrainé
|
| 248 |
|
|
| 249 |
+
├── scripts/ # Scripts bd (BD, données)
|
| 250 |
+
│ ├── create_tables.py # Créaton des tables définies dans model.py
|
| 251 |
+
│ ├── dataset_final.csv # Data final
|
| 252 |
+
│ ├── insert_dataset.py # Code chargement de la table dataset_final
|
| 253 |
+
├── tests/ # Tests unitaires, fonctionnels
|
| 254 |
│ ├── test_api.py # Test automatisé de l'API via Pytest
|
| 255 |
|
|
| 256 |
+
├── .env # Stockage des variables sensibles et de configuration
|
| 257 |
├── .gitignore # Nettoyage du dépôt
|
| 258 |
+
├── Dockerfile # Reproduction du dépôt
|
| 259 |
+
├── poetry.lock # Nettoyage du dépôt
|
| 260 |
+
├── pyproject.toml # Librairies dépendances ML
|
| 261 |
├── README.md # Présentation du projet
|
| 262 |
+
└── requirements.txt # Librairies dépendances API
|
| 263 |
```
|
poetry.lock
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
pyproject.toml
CHANGED
|
@@ -21,11 +21,19 @@ dependencies = [
|
|
| 21 |
"catboost ==1.2.7",
|
| 22 |
"numba ==0.59.1",
|
| 23 |
"llvmlite ==0.42.0",
|
| 24 |
-
"ipykernel>=6.25,<7.0"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
]
|
| 26 |
|
| 27 |
-
|
| 28 |
[build-system]
|
| 29 |
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
| 30 |
build-backend = "poetry.core.masonry.api"
|
| 31 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
"catboost ==1.2.7",
|
| 22 |
"numba ==0.59.1",
|
| 23 |
"llvmlite ==0.42.0",
|
| 24 |
+
"ipykernel>=6.25,<7.0",
|
| 25 |
+
"huggingface-hub ==1.3.1",
|
| 26 |
+
"fastapi ==0.115.0",
|
| 27 |
+
"uvicorn ==0.30.1",
|
| 28 |
+
"python-dotenv ==1.2.1",
|
| 29 |
+
"psycopg2-binary ==2.9.11"
|
| 30 |
]
|
| 31 |
|
|
|
|
| 32 |
[build-system]
|
| 33 |
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
| 34 |
build-backend = "poetry.core.masonry.api"
|
| 35 |
|
| 36 |
+
[tool.poetry.group.dev.dependencies]
|
| 37 |
+
pytest = "9.0.2"
|
| 38 |
+
|
| 39 |
+
|
requirements.txt
CHANGED
|
@@ -6,4 +6,11 @@ Pygments==2.19.2
|
|
| 6 |
pytest==9.0.2
|
| 7 |
fastapi==0.115.0
|
| 8 |
uvicorn==0.30.1
|
| 9 |
-
httpx==0.27.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
pytest==9.0.2
|
| 7 |
fastapi==0.115.0
|
| 8 |
uvicorn==0.30.1
|
| 9 |
+
httpx==0.27.0
|
| 10 |
+
huggingface-hub==1.3.1
|
| 11 |
+
joblib==1.4.2
|
| 12 |
+
pandas==2.2.2
|
| 13 |
+
scikit-learn==1.4.2
|
| 14 |
+
xgboost ==2.0.3
|
| 15 |
+
huggingface-hub ==1.3.1
|
| 16 |
+
python-dotenv ==1.2.1
|
scripts/create_tables.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from App.database import engine
|
| 2 |
+
from App.database import Base
|
| 3 |
+
|
| 4 |
+
Base.metadata.create_all(bind=engine)
|
| 5 |
+
|
| 6 |
+
print("Tables créées avec succès")
|
scripts/dataset_final.csv
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
scripts/insert_dataset.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import pandas as pd
|
| 3 |
+
from dotenv import load_dotenv
|
| 4 |
+
from sqlalchemy import create_engine
|
| 5 |
+
|
| 6 |
+
load_dotenv()
|
| 7 |
+
|
| 8 |
+
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 9 |
+
csv_path = os.path.join(BASE_DIR, "dataset_final.csv")
|
| 10 |
+
|
| 11 |
+
df = pd.read_csv(csv_path, encoding="latin-1")
|
| 12 |
+
|
| 13 |
+
DB_USER = os.getenv("DB_USER")
|
| 14 |
+
DB_PASSWORD = os.getenv("DB_PASSWORD")
|
| 15 |
+
DB_HOST = os.getenv("DB_HOST")
|
| 16 |
+
DB_PORT = os.getenv("DB_PORT")
|
| 17 |
+
DB_NAME = os.getenv("DB_NAME")
|
| 18 |
+
|
| 19 |
+
DATABASE_URL = (f"postgresql+psycopg2://{DB_USER}:{DB_PASSWORD}"f"@{DB_HOST}:{DB_PORT}/{DB_NAME}")
|
| 20 |
+
|
| 21 |
+
engine = create_engine(DATABASE_URL)
|
| 22 |
+
|
| 23 |
+
df.to_sql("employees_dataset", engine, if_exists="replace", index=False)
|
| 24 |
+
|
| 25 |
+
print("Dataset inséré dans PostgreSQL")
|