Upload folder using huggingface_hub
Browse files- .dockerignore +1 -0
- README.md +1 -1
- pyproject.toml +1 -0
- src/api/main.py +28 -0
- src/api/schemas.py +3 -1
- src/db/__init__.py +0 -0
- src/db/create_db.py +181 -0
- src/db/database.py +60 -0
- src/db/logger_db.py +180 -0
- src/db/models.py +110 -0
- src/db/schema.md +76 -0
- src/utils/transformer.py +3 -0
- uv.lock +43 -0
.dockerignore
CHANGED
|
@@ -9,6 +9,7 @@ tests/
|
|
| 9 |
*.md
|
| 10 |
*.txt
|
| 11 |
notebooks/
|
|
|
|
| 12 |
.env
|
| 13 |
*.csv
|
| 14 |
.gitignore
|
|
|
|
| 9 |
*.md
|
| 10 |
*.txt
|
| 11 |
notebooks/
|
| 12 |
+
data/
|
| 13 |
.env
|
| 14 |
*.csv
|
| 15 |
.gitignore
|
README.md
CHANGED
|
@@ -186,7 +186,7 @@ Response:
|
|
| 186 |
- [x] CI/CD pipeline with GitLab
|
| 187 |
- [x] Automated deployment to Hugging Face Spaces
|
| 188 |
- [x] Test coverage reporting with pytest-cov and Cobertura
|
| 189 |
-
- [
|
| 190 |
- [ ] Add a Gradio or Streamlit user interface for non-technical users
|
| 191 |
- [ ] Implement model versioning and A/B testing
|
| 192 |
|
|
|
|
| 186 |
- [x] CI/CD pipeline with GitLab
|
| 187 |
- [x] Automated deployment to Hugging Face Spaces
|
| 188 |
- [x] Test coverage reporting with pytest-cov and Cobertura
|
| 189 |
+
- [x] Add a database layer (Alembic + SQLAlchemy) for logging predictions
|
| 190 |
- [ ] Add a Gradio or Streamlit user interface for non-technical users
|
| 191 |
- [ ] Implement model versioning and A/B testing
|
| 192 |
|
pyproject.toml
CHANGED
|
@@ -12,6 +12,7 @@ dependencies = [
|
|
| 12 |
"joblib==1.5.3",
|
| 13 |
"numpy==2.3.5",
|
| 14 |
"pandas==2.3.3",
|
|
|
|
| 15 |
"pydantic==2.12.5",
|
| 16 |
"scikit-learn==1.8.0",
|
| 17 |
"sqlalchemy==2.0.46",
|
|
|
|
| 12 |
"joblib==1.5.3",
|
| 13 |
"numpy==2.3.5",
|
| 14 |
"pandas==2.3.3",
|
| 15 |
+
"psycopg2-binary==2.9.12",
|
| 16 |
"pydantic==2.12.5",
|
| 17 |
"scikit-learn==1.8.0",
|
| 18 |
"sqlalchemy==2.0.46",
|
src/api/main.py
CHANGED
|
@@ -8,13 +8,25 @@ Contenu:
|
|
| 8 |
- Point d'entrée de l'application
|
| 9 |
"""
|
| 10 |
|
|
|
|
|
|
|
| 11 |
from fastapi import FastAPI, HTTPException
|
| 12 |
from fastapi.responses import Response
|
| 13 |
|
| 14 |
from src.api.schemas import DonneesEmploye, ResultatPrediction
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
from src.utils.model_loader import get_modele
|
| 16 |
from src.utils.transformer import transformer_donnees
|
| 17 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
# APPLICATION FASTAPI
|
| 19 |
|
| 20 |
tags_metadata = [
|
|
@@ -145,6 +157,22 @@ def predire(employe: DonneesEmploye):
|
|
| 145 |
"""
|
| 146 |
donnees = employe.model_dump(exclude_none=True)
|
| 147 |
resultat = predire_churn(donnees)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
return ResultatPrediction(**resultat)
|
| 149 |
|
| 150 |
|
|
|
|
| 8 |
- Point d'entrée de l'application
|
| 9 |
"""
|
| 10 |
|
| 11 |
+
import logging
|
| 12 |
+
|
| 13 |
from fastapi import FastAPI, HTTPException
|
| 14 |
from fastapi.responses import Response
|
| 15 |
|
| 16 |
from src.api.schemas import DonneesEmploye, ResultatPrediction
|
| 17 |
+
from src.db.logger_db import (
|
| 18 |
+
log_api_operation,
|
| 19 |
+
log_prediction_input,
|
| 20 |
+
log_prediction_output,
|
| 21 |
+
)
|
| 22 |
from src.utils.model_loader import get_modele
|
| 23 |
from src.utils.transformer import transformer_donnees
|
| 24 |
|
| 25 |
+
# CONFIGURATION DU LOGGER
|
| 26 |
+
logging.basicConfig(level=logging.INFO)
|
| 27 |
+
logger = logging.getLogger(__name__)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
# APPLICATION FASTAPI
|
| 31 |
|
| 32 |
tags_metadata = [
|
|
|
|
| 157 |
"""
|
| 158 |
donnees = employe.model_dump(exclude_none=True)
|
| 159 |
resultat = predire_churn(donnees)
|
| 160 |
+
|
| 161 |
+
# Logging dans la base de données (optionnel, ne bloque pas l'API)
|
| 162 |
+
input_id = log_prediction_input(donnees)
|
| 163 |
+
log_prediction_output(
|
| 164 |
+
input_id=input_id,
|
| 165 |
+
prediction=resultat["prediction"],
|
| 166 |
+
probabilite=resultat["probabilite"],
|
| 167 |
+
classe=resultat["classe"],
|
| 168 |
+
)
|
| 169 |
+
log_api_operation(
|
| 170 |
+
operation="PREDICT",
|
| 171 |
+
table_cible="prediction_inputs",
|
| 172 |
+
details=f"id_employee={donnees.get('id_employee')}, prediction={resultat['prediction']}",
|
| 173 |
+
statut="SUCCESS",
|
| 174 |
+
)
|
| 175 |
+
|
| 176 |
return ResultatPrediction(**resultat)
|
| 177 |
|
| 178 |
|
src/api/schemas.py
CHANGED
|
@@ -21,13 +21,15 @@ class DonneesEmploye(BaseModel):
|
|
| 21 |
|
| 22 |
# INFORMATIONS PERSONNELLES
|
| 23 |
|
|
|
|
|
|
|
| 24 |
age: int = Field(
|
| 25 |
..., ge=18, le=65, description="Âge de l'employé (entre 18 et 65 ans)"
|
| 26 |
)
|
| 27 |
|
| 28 |
genre: Literal["M", "F"] = Field(..., description="Genre: M=Homme, F=Femme")
|
| 29 |
|
| 30 |
-
revenu_mensuel:
|
| 31 |
..., gt=0, lt=20000, description="Salaire mensuel en euros"
|
| 32 |
)
|
| 33 |
|
|
|
|
| 21 |
|
| 22 |
# INFORMATIONS PERSONNELLES
|
| 23 |
|
| 24 |
+
id_employee: int = Field(..., ge=1, description="ID unique de l'employé")
|
| 25 |
+
|
| 26 |
age: int = Field(
|
| 27 |
..., ge=18, le=65, description="Âge de l'employé (entre 18 et 65 ans)"
|
| 28 |
)
|
| 29 |
|
| 30 |
genre: Literal["M", "F"] = Field(..., description="Genre: M=Homme, F=Femme")
|
| 31 |
|
| 32 |
+
revenu_mensuel: int = Field(
|
| 33 |
..., gt=0, lt=20000, description="Salaire mensuel en euros"
|
| 34 |
)
|
| 35 |
|
src/db/__init__.py
ADDED
|
File without changes
|
src/db/create_db.py
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Script de création de la base de données PostgreSQL et d'insertion du dataset.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
import pandas as pd
|
| 8 |
+
from sqlalchemy.exc import IntegrityError
|
| 9 |
+
|
| 10 |
+
from src.db.database import SessionLocal, engine
|
| 11 |
+
from src.db.models import Base, Employee
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def racine_projet() -> Path:
|
| 15 |
+
"""Remonte l'arborescence jusqu'à trouver la racine du projet."""
|
| 16 |
+
courant = Path(__file__).resolve()
|
| 17 |
+
for parent in courant.parents:
|
| 18 |
+
if (parent / "pyproject.toml").exists():
|
| 19 |
+
return parent
|
| 20 |
+
raise FileNotFoundError("Racine du projet non trouvée")
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
PROJECT_ROOT = racine_projet()
|
| 24 |
+
RAW_DIR = PROJECT_ROOT / "data" / "raw"
|
| 25 |
+
PROCESSED_DIR = PROJECT_ROOT / "data" / "processed"
|
| 26 |
+
|
| 27 |
+
CSV_SIRH = RAW_DIR / "extrait_sirh.csv"
|
| 28 |
+
CSV_EVAL = RAW_DIR / "extrait_eval.csv"
|
| 29 |
+
CSV_SONDAGE = RAW_DIR / "extrait_sondage.csv"
|
| 30 |
+
CSV_EMPLOYES = PROCESSED_DIR / "employees.csv"
|
| 31 |
+
|
| 32 |
+
COLONNES_EMPLOYEES = [
|
| 33 |
+
"id_employee",
|
| 34 |
+
"age",
|
| 35 |
+
"genre",
|
| 36 |
+
"revenu_mensuel",
|
| 37 |
+
"statut_marital",
|
| 38 |
+
"departement",
|
| 39 |
+
"poste",
|
| 40 |
+
"annee_experience_totale",
|
| 41 |
+
"annees_dans_l_entreprise",
|
| 42 |
+
"satisfaction_employee_environnement",
|
| 43 |
+
"note_evaluation_precedente",
|
| 44 |
+
"satisfaction_employee_nature_travail",
|
| 45 |
+
"satisfaction_employee_equipe",
|
| 46 |
+
"satisfaction_employee_equilibre_pro_perso",
|
| 47 |
+
"note_evaluation_actuelle",
|
| 48 |
+
"heure_supplementaires",
|
| 49 |
+
"augementation_salaire_precedente",
|
| 50 |
+
"nombre_participation_pee",
|
| 51 |
+
"nb_formations_suivies",
|
| 52 |
+
"distance_domicile_travail",
|
| 53 |
+
"niveau_education",
|
| 54 |
+
"frequence_deplacement",
|
| 55 |
+
"annees_depuis_la_derniere_promotion",
|
| 56 |
+
"a_quitte_l_entreprise",
|
| 57 |
+
]
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def fusionner_csv() -> pd.DataFrame:
|
| 61 |
+
"""
|
| 62 |
+
Charge les 3 CSV et les fusionne sur la colonne id_employee.
|
| 63 |
+
|
| 64 |
+
Reproduit les étapes du notebook :
|
| 65 |
+
1. Renomme eval_number en id_employee dans df_eval
|
| 66 |
+
2. Retire le préfixe 'E_' et convertit en int
|
| 67 |
+
3. Renomme code_sondage en id_employee dans df_sondage
|
| 68 |
+
4. Inner merge des 3 DataFrames
|
| 69 |
+
|
| 70 |
+
Returns:
|
| 71 |
+
DataFrame fusionné (32 colonnes, 1470 lignes)
|
| 72 |
+
"""
|
| 73 |
+
df_sirh = pd.read_csv(CSV_SIRH)
|
| 74 |
+
df_eval = pd.read_csv(CSV_EVAL)
|
| 75 |
+
df_sondage = pd.read_csv(CSV_SONDAGE)
|
| 76 |
+
|
| 77 |
+
df_eval = df_eval.rename(columns={"eval_number": "id_employee"})
|
| 78 |
+
df_eval["id_employee"] = df_eval["id_employee"].str[2:].astype("int64")
|
| 79 |
+
|
| 80 |
+
df_sondage = df_sondage.rename(columns={"code_sondage": "id_employee"})
|
| 81 |
+
|
| 82 |
+
df_central = pd.merge(df_sirh, df_eval, on="id_employee", how="inner")
|
| 83 |
+
df_central = pd.merge(df_central, df_sondage, on="id_employee", how="inner")
|
| 84 |
+
|
| 85 |
+
print(
|
| 86 |
+
f"DataFrame fusionné : {df_central.shape[0]} lignes, {df_central.shape[1]} colonnes"
|
| 87 |
+
)
|
| 88 |
+
return df_central
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def nettoyer_dataframe(df_central: pd.DataFrame) -> pd.DataFrame:
|
| 92 |
+
"""
|
| 93 |
+
Nettoie le DataFrame fusionné pour ne garder que les colonnes utilisées par le modèle.
|
| 94 |
+
|
| 95 |
+
Étapes :
|
| 96 |
+
1. Retire le '%' de augementation_salaire_precedente et convertit en int
|
| 97 |
+
2. Sélectionne uniquement les 23 colonnes du modèle
|
| 98 |
+
|
| 99 |
+
Args:
|
| 100 |
+
df_central: DataFrame fusionné (32 colonnes)
|
| 101 |
+
|
| 102 |
+
Returns:
|
| 103 |
+
DataFrame nettoyé (23 colonnes)
|
| 104 |
+
"""
|
| 105 |
+
df_central["augementation_salaire_precedente"] = (
|
| 106 |
+
df_central["augementation_salaire_precedente"].str[:-2].astype("int64")
|
| 107 |
+
)
|
| 108 |
+
|
| 109 |
+
df_employees = df_central.loc[:, COLONNES_EMPLOYEES].copy()
|
| 110 |
+
|
| 111 |
+
print(
|
| 112 |
+
f"DataFrame nettoyé : {df_employees.shape[0]} lignes, {df_employees.shape[1]} colonnes"
|
| 113 |
+
)
|
| 114 |
+
return df_employees
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
def sauvegarder_csv(df_employees: pd.DataFrame) -> None:
|
| 118 |
+
"""
|
| 119 |
+
Sauvegarde le DataFrame nettoyé au format CSV.
|
| 120 |
+
|
| 121 |
+
Args:
|
| 122 |
+
df_employees: DataFrame nettoyé (23 colonnes)
|
| 123 |
+
"""
|
| 124 |
+
PROCESSED_DIR.mkdir(parents=True, exist_ok=True)
|
| 125 |
+
df_employees.to_csv(CSV_EMPLOYES, index=False)
|
| 126 |
+
print(f"CSV sauvegardé : {CSV_EMPLOYES}")
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def creer_tables() -> None:
|
| 130 |
+
"""Crée les tables dans PostgreSQL à partir des modèles ORM."""
|
| 131 |
+
Base.metadata.create_all(engine)
|
| 132 |
+
print("Tables créées avec succès")
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def inserer_donnees(df_employees: pd.DataFrame) -> None:
|
| 136 |
+
"""
|
| 137 |
+
Insère les données du DataFrame dans la table employees.
|
| 138 |
+
|
| 139 |
+
Args:
|
| 140 |
+
df_employees: DataFrame nettoyé (23 colonnes, 1470 lignes)
|
| 141 |
+
"""
|
| 142 |
+
session = SessionLocal()
|
| 143 |
+
try:
|
| 144 |
+
employees = [Employee(**row.to_dict()) for _, row in df_employees.iterrows()]
|
| 145 |
+
session.add_all(employees)
|
| 146 |
+
session.commit()
|
| 147 |
+
print(f"{len(employees)} employés insérés avec succès")
|
| 148 |
+
except IntegrityError as e:
|
| 149 |
+
session.rollback()
|
| 150 |
+
print(f"Erreur d'intégrité : {e}")
|
| 151 |
+
print("Les données existent peut-être déjà. Vider la table avant de réinsérer.")
|
| 152 |
+
except Exception as e:
|
| 153 |
+
session.rollback()
|
| 154 |
+
print(f"Erreur lors de l'insertion : {e}")
|
| 155 |
+
finally:
|
| 156 |
+
session.close()
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
def main() -> None:
|
| 160 |
+
"""Point d'entrée principal du script."""
|
| 161 |
+
|
| 162 |
+
# 1. Chargement et fusion des CSV
|
| 163 |
+
df_central = fusionner_csv()
|
| 164 |
+
|
| 165 |
+
# 2. Nettoyage du DataFrame"
|
| 166 |
+
df_employees = nettoyer_dataframe(df_central)
|
| 167 |
+
|
| 168 |
+
# 3. Sauvegarde du CSV nettoyé
|
| 169 |
+
sauvegarder_csv(df_employees)
|
| 170 |
+
|
| 171 |
+
# 4. Création des tables
|
| 172 |
+
creer_tables()
|
| 173 |
+
|
| 174 |
+
# 5. Insertion des données
|
| 175 |
+
inserer_donnees(df_employees)
|
| 176 |
+
|
| 177 |
+
print("Base de données créée avec succès")
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
if __name__ == "__main__":
|
| 181 |
+
main()
|
src/db/database.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Configuration de la connexion à la base de données PostgreSQL.
|
| 3 |
+
Utilise SQLAlchemy comme ORM et lit l'URL depuis le fichier .env.
|
| 4 |
+
|
| 5 |
+
Si DATABASE_URL n'est pas définie (ex: conteneur Docker sans PostgreSQL),
|
| 6 |
+
le module ne génère pas d'erreur. Les fonctions de logging détecteront
|
| 7 |
+
l'absence de DB et fonctionneront en mode dégradé.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import logging
|
| 11 |
+
import os
|
| 12 |
+
|
| 13 |
+
from dotenv import load_dotenv
|
| 14 |
+
from sqlalchemy import create_engine
|
| 15 |
+
from sqlalchemy.orm import DeclarativeBase, sessionmaker
|
| 16 |
+
|
| 17 |
+
logger = logging.getLogger(__name__)
|
| 18 |
+
|
| 19 |
+
# Charge les variables d'environnement depuis le fichier .env
|
| 20 |
+
load_dotenv()
|
| 21 |
+
|
| 22 |
+
# URL de connexion à la base de données
|
| 23 |
+
DATABASE_URL = os.getenv("DATABASE_URL")
|
| 24 |
+
|
| 25 |
+
if DATABASE_URL is None:
|
| 26 |
+
logger.warning(
|
| 27 |
+
"DATABASE_URL n'est pas définie. "
|
| 28 |
+
"La base de données ne sera pas disponible. "
|
| 29 |
+
"L'API fonctionnera sans logging en base de données."
|
| 30 |
+
)
|
| 31 |
+
engine = None
|
| 32 |
+
SessionLocal = None
|
| 33 |
+
else:
|
| 34 |
+
# Engine SQLAlchemy = connexion vers PostgreSQL
|
| 35 |
+
engine = create_engine(DATABASE_URL)
|
| 36 |
+
# SessionLocal = fabrique de sessions (une session = une transaction DB)
|
| 37 |
+
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
# Base = classe de base pour tous les modèles ORM
|
| 41 |
+
class Base(DeclarativeBase):
|
| 42 |
+
pass
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def get_db():
|
| 46 |
+
"""
|
| 47 |
+
Générateur qui fournit une session de base de données.
|
| 48 |
+
|
| 49 |
+
Yields:
|
| 50 |
+
Session SQLAlchemy ou None si la DB n'est pas configurée
|
| 51 |
+
"""
|
| 52 |
+
if SessionLocal is None:
|
| 53 |
+
yield None
|
| 54 |
+
return
|
| 55 |
+
|
| 56 |
+
db = SessionLocal()
|
| 57 |
+
try:
|
| 58 |
+
yield db
|
| 59 |
+
finally:
|
| 60 |
+
db.close()
|
src/db/logger_db.py
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Module de logging des prédictions dans la base de données PostgreSQL.
|
| 3 |
+
|
| 4 |
+
Si la base de données n'est pas disponible (ex: conteneur Docker sans PostgreSQL),
|
| 5 |
+
les fonctions ne lèvent pas d'erreur et laissent un avertissement dans les logs.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import logging
|
| 9 |
+
import os
|
| 10 |
+
|
| 11 |
+
from dotenv import load_dotenv
|
| 12 |
+
|
| 13 |
+
logger = logging.getLogger(__name__)
|
| 14 |
+
|
| 15 |
+
# Vérifie si la DB est configurée
|
| 16 |
+
load_dotenv()
|
| 17 |
+
DATABASE_URL = os.getenv("DATABASE_URL")
|
| 18 |
+
DB_AVAILABLE = DATABASE_URL is not None
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def _get_session():
|
| 22 |
+
"""
|
| 23 |
+
Ouvre une session SQLAlchemy si la DB est disponible.
|
| 24 |
+
|
| 25 |
+
Returns:
|
| 26 |
+
Session SQLAlchemy ou None si la DB n'est pas configurée
|
| 27 |
+
"""
|
| 28 |
+
if not DB_AVAILABLE:
|
| 29 |
+
return None
|
| 30 |
+
|
| 31 |
+
try:
|
| 32 |
+
from src.db.database import SessionLocal
|
| 33 |
+
|
| 34 |
+
return SessionLocal()
|
| 35 |
+
except Exception as e:
|
| 36 |
+
logger.warning(f"Impossible de se connecter à la base de données : {e}")
|
| 37 |
+
return None
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def log_prediction_input(donnees: dict) -> int | None:
|
| 41 |
+
"""
|
| 42 |
+
Enregistre les données brutes d'un employé dans prediction_inputs.
|
| 43 |
+
|
| 44 |
+
Args:
|
| 45 |
+
donnees: Dict avec les données de l'employé (validées par Pydantic)
|
| 46 |
+
|
| 47 |
+
Returns:
|
| 48 |
+
ID de l'input inséré, ou None si la DB n'est pas disponible
|
| 49 |
+
"""
|
| 50 |
+
if not DB_AVAILABLE:
|
| 51 |
+
logger.info("DB non configurée : log_prediction_input ignoré")
|
| 52 |
+
return None
|
| 53 |
+
|
| 54 |
+
try:
|
| 55 |
+
from src.db.models import PredictionInput
|
| 56 |
+
except Exception:
|
| 57 |
+
logger.warning("Impossible d'importer les modèles DB")
|
| 58 |
+
return None
|
| 59 |
+
|
| 60 |
+
session = _get_session()
|
| 61 |
+
if session is None:
|
| 62 |
+
return None
|
| 63 |
+
|
| 64 |
+
try:
|
| 65 |
+
prediction_input = PredictionInput(**donnees)
|
| 66 |
+
session.add(prediction_input)
|
| 67 |
+
session.commit()
|
| 68 |
+
session.refresh(prediction_input)
|
| 69 |
+
input_id: int = prediction_input.id # type: ignore[assignment]
|
| 70 |
+
logger.info(f"Input inséré en DB (id={input_id})")
|
| 71 |
+
return input_id
|
| 72 |
+
except Exception as e:
|
| 73 |
+
session.rollback()
|
| 74 |
+
logger.warning(f"Erreur lors de l'insertion input : {e}")
|
| 75 |
+
return None
|
| 76 |
+
finally:
|
| 77 |
+
session.close()
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def log_prediction_output(
|
| 81 |
+
input_id: int | None, prediction: str, probabilite: float, classe: int
|
| 82 |
+
) -> bool:
|
| 83 |
+
"""
|
| 84 |
+
Enregistre le résultat d'une prédiction dans prediction_outputs.
|
| 85 |
+
|
| 86 |
+
Args:
|
| 87 |
+
input_id: ID de l'input dans prediction_inputs
|
| 88 |
+
prediction: "Oui" ou "Non"
|
| 89 |
+
probabilite: Probabilité de départ (entre 0 et 1)
|
| 90 |
+
classe: 0 ou 1
|
| 91 |
+
|
| 92 |
+
Returns:
|
| 93 |
+
True si l'insertion a réussi, False sinon
|
| 94 |
+
"""
|
| 95 |
+
if not DB_AVAILABLE or input_id is None:
|
| 96 |
+
logger.info(
|
| 97 |
+
"DB non configurée ou input_id manquant : log_prediction_output ignoré"
|
| 98 |
+
)
|
| 99 |
+
return False
|
| 100 |
+
|
| 101 |
+
try:
|
| 102 |
+
from src.db.models import PredictionOutput
|
| 103 |
+
except Exception:
|
| 104 |
+
logger.warning("Impossible d'importer les modèles DB")
|
| 105 |
+
return False
|
| 106 |
+
|
| 107 |
+
session = _get_session()
|
| 108 |
+
if session is None:
|
| 109 |
+
return False
|
| 110 |
+
|
| 111 |
+
try:
|
| 112 |
+
prediction_output = PredictionOutput(
|
| 113 |
+
input_id=input_id,
|
| 114 |
+
prediction=prediction,
|
| 115 |
+
probabilite=probabilite,
|
| 116 |
+
classe=classe,
|
| 117 |
+
)
|
| 118 |
+
session.add(prediction_output)
|
| 119 |
+
session.commit()
|
| 120 |
+
logger.info(f"Output inséré en DB (input_id={input_id})")
|
| 121 |
+
return True
|
| 122 |
+
except Exception as e:
|
| 123 |
+
session.rollback()
|
| 124 |
+
logger.warning(f"Erreur lors de l'insertion output : {e}")
|
| 125 |
+
return False
|
| 126 |
+
finally:
|
| 127 |
+
session.close()
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def log_api_operation(
|
| 131 |
+
operation: str,
|
| 132 |
+
table_cible: str,
|
| 133 |
+
details: str | None = None,
|
| 134 |
+
statut: str = "SUCCESS",
|
| 135 |
+
) -> bool:
|
| 136 |
+
"""
|
| 137 |
+
Enregistre une opération API dans api_logs.
|
| 138 |
+
|
| 139 |
+
Args:
|
| 140 |
+
operation: Type d'opération (ex: "INSERT", "SELECT")
|
| 141 |
+
table_cible: Table concernée (ex: "prediction_inputs")
|
| 142 |
+
details: Détails optionnels de l'opération
|
| 143 |
+
statut: "SUCCESS" ou "ERROR"
|
| 144 |
+
|
| 145 |
+
Returns:
|
| 146 |
+
True si l'insertion a réussi, False sinon
|
| 147 |
+
"""
|
| 148 |
+
if not DB_AVAILABLE:
|
| 149 |
+
logger.info(
|
| 150 |
+
f"DB non configurée : log_api_operation ignoré ({operation} {table_cible})"
|
| 151 |
+
)
|
| 152 |
+
return False
|
| 153 |
+
|
| 154 |
+
try:
|
| 155 |
+
from src.db.models import ApiLog
|
| 156 |
+
except Exception:
|
| 157 |
+
logger.warning("Impossible d'importer les modèles DB")
|
| 158 |
+
return False
|
| 159 |
+
|
| 160 |
+
session = _get_session()
|
| 161 |
+
if session is None:
|
| 162 |
+
return False
|
| 163 |
+
|
| 164 |
+
try:
|
| 165 |
+
api_log = ApiLog(
|
| 166 |
+
operation=operation,
|
| 167 |
+
table_cible=table_cible,
|
| 168 |
+
details=details,
|
| 169 |
+
statut=statut,
|
| 170 |
+
)
|
| 171 |
+
session.add(api_log)
|
| 172 |
+
session.commit()
|
| 173 |
+
logger.info(f"Log API inséré : {operation} {table_cible} ({statut})")
|
| 174 |
+
return True
|
| 175 |
+
except Exception as e:
|
| 176 |
+
session.rollback()
|
| 177 |
+
logger.warning(f"Erreur lors de l'insertion log API : {e}")
|
| 178 |
+
return False
|
| 179 |
+
finally:
|
| 180 |
+
session.close()
|
src/db/models.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Modèles ORM SQLAlchemy pour la base de données PostgreSQL.
|
| 3 |
+
|
| 4 |
+
Contient les 4 tables du schéma :
|
| 5 |
+
- employees : dataset brut d'entraînement (1470 lignes)
|
| 6 |
+
- prediction_inputs : données brutes envoyées à l'API
|
| 7 |
+
- prediction_outputs : résultat de la prédiction
|
| 8 |
+
- api_logs : traçabilité des échanges API ↔ DB
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
from datetime import datetime
|
| 12 |
+
|
| 13 |
+
from sqlalchemy import Column, DateTime, Float, ForeignKey, Integer, String
|
| 14 |
+
from sqlalchemy.orm import relationship
|
| 15 |
+
|
| 16 |
+
from src.db.database import Base
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class Employee(Base):
|
| 20 |
+
"""Table du dataset brut (1470 employés)."""
|
| 21 |
+
|
| 22 |
+
__tablename__ = "employees"
|
| 23 |
+
|
| 24 |
+
id_employee = Column(Integer, primary_key=True)
|
| 25 |
+
age = Column(Integer, nullable=False)
|
| 26 |
+
genre = Column(String(1), nullable=False)
|
| 27 |
+
revenu_mensuel = Column(Integer, nullable=False)
|
| 28 |
+
statut_marital = Column(String(20), nullable=False)
|
| 29 |
+
departement = Column(String(20), nullable=False)
|
| 30 |
+
poste = Column(String(30), nullable=False)
|
| 31 |
+
annee_experience_totale = Column(Integer, nullable=False)
|
| 32 |
+
annees_dans_l_entreprise = Column(Integer, nullable=False)
|
| 33 |
+
satisfaction_employee_environnement = Column(Integer, nullable=False)
|
| 34 |
+
note_evaluation_precedente = Column(Integer, nullable=False)
|
| 35 |
+
satisfaction_employee_nature_travail = Column(Integer, nullable=False)
|
| 36 |
+
satisfaction_employee_equipe = Column(Integer, nullable=False)
|
| 37 |
+
satisfaction_employee_equilibre_pro_perso = Column(Integer, nullable=False)
|
| 38 |
+
note_evaluation_actuelle = Column(Integer, nullable=False)
|
| 39 |
+
heure_supplementaires = Column(String(5), nullable=False)
|
| 40 |
+
augementation_salaire_precedente = Column(Integer, nullable=False)
|
| 41 |
+
nombre_participation_pee = Column(Integer, nullable=False)
|
| 42 |
+
nb_formations_suivies = Column(Integer, nullable=False)
|
| 43 |
+
distance_domicile_travail = Column(Integer, nullable=False)
|
| 44 |
+
niveau_education = Column(Integer, nullable=False)
|
| 45 |
+
frequence_deplacement = Column(String(20), nullable=False)
|
| 46 |
+
annees_depuis_la_derniere_promotion = Column(Integer, nullable=False)
|
| 47 |
+
a_quitte_l_entreprise = Column(String(5), nullable=False)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
class PredictionInput(Base):
|
| 51 |
+
"""Données brutes envoyées à l'API (avant transformation)."""
|
| 52 |
+
|
| 53 |
+
__tablename__ = "prediction_inputs"
|
| 54 |
+
|
| 55 |
+
id = Column(Integer, primary_key=True, autoincrement=True)
|
| 56 |
+
id_employee = Column(Integer, nullable=False)
|
| 57 |
+
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
| 58 |
+
age = Column(Integer, nullable=False)
|
| 59 |
+
genre = Column(String(1), nullable=False)
|
| 60 |
+
revenu_mensuel = Column(Integer, nullable=False)
|
| 61 |
+
statut_marital = Column(String(20), nullable=False)
|
| 62 |
+
departement = Column(String(20), nullable=False)
|
| 63 |
+
poste = Column(String(30), nullable=False)
|
| 64 |
+
annee_experience_totale = Column(Integer, nullable=False)
|
| 65 |
+
annees_dans_l_entreprise = Column(Integer, nullable=False)
|
| 66 |
+
satisfaction_employee_environnement = Column(Integer, nullable=False)
|
| 67 |
+
note_evaluation_precedente = Column(Integer, nullable=False)
|
| 68 |
+
satisfaction_employee_nature_travail = Column(Integer, nullable=False)
|
| 69 |
+
satisfaction_employee_equipe = Column(Integer, nullable=False)
|
| 70 |
+
satisfaction_employee_equilibre_pro_perso = Column(Integer, nullable=False)
|
| 71 |
+
note_evaluation_actuelle = Column(Integer, nullable=False)
|
| 72 |
+
heure_supplementaires = Column(String(5), nullable=False)
|
| 73 |
+
augementation_salaire_precedente = Column(Integer, nullable=False)
|
| 74 |
+
nombre_participation_pee = Column(Integer, nullable=False)
|
| 75 |
+
nb_formations_suivies = Column(Integer, nullable=False)
|
| 76 |
+
distance_domicile_travail = Column(Integer, nullable=False)
|
| 77 |
+
niveau_education = Column(Integer, nullable=False)
|
| 78 |
+
frequence_deplacement = Column(String(20), nullable=False)
|
| 79 |
+
annees_depuis_la_derniere_promotion = Column(Integer, nullable=False)
|
| 80 |
+
|
| 81 |
+
output = relationship("PredictionOutput", back_populates="input", uselist=False)
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
class PredictionOutput(Base):
|
| 85 |
+
"""Résultat de la prédiction du modèle."""
|
| 86 |
+
|
| 87 |
+
__tablename__ = "prediction_outputs"
|
| 88 |
+
|
| 89 |
+
id = Column(Integer, primary_key=True, autoincrement=True)
|
| 90 |
+
input_id = Column(
|
| 91 |
+
Integer, ForeignKey("prediction_inputs.id"), nullable=False, unique=True
|
| 92 |
+
)
|
| 93 |
+
prediction = Column(String(5), nullable=False)
|
| 94 |
+
probabilite = Column(Float, nullable=False)
|
| 95 |
+
classe = Column(Integer, nullable=False)
|
| 96 |
+
|
| 97 |
+
input = relationship("PredictionInput", back_populates="output")
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
class ApiLog(Base):
|
| 101 |
+
"""Traçabilité des échanges entre l'API et la base de données."""
|
| 102 |
+
|
| 103 |
+
__tablename__ = "api_logs"
|
| 104 |
+
|
| 105 |
+
id = Column(Integer, primary_key=True, autoincrement=True)
|
| 106 |
+
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
| 107 |
+
operation = Column(String(30), nullable=False)
|
| 108 |
+
table_cible = Column(String(20), nullable=False)
|
| 109 |
+
details = Column(String(255), nullable=True)
|
| 110 |
+
statut = Column(String(10), nullable=False)
|
src/db/schema.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
```mermaid
|
| 2 |
+
erDiagram
|
| 3 |
+
employees {
|
| 4 |
+
int id_employee PK
|
| 5 |
+
int age
|
| 6 |
+
varchar genre
|
| 7 |
+
int revenu_mensuel
|
| 8 |
+
varchar statut_marital
|
| 9 |
+
varchar departement
|
| 10 |
+
varchar poste
|
| 11 |
+
int annee_experience_totale
|
| 12 |
+
int annees_dans_l_entreprise
|
| 13 |
+
int satisfaction_employee_environnement
|
| 14 |
+
int note_evaluation_precedente
|
| 15 |
+
int satisfaction_employee_nature_travail
|
| 16 |
+
int satisfaction_employee_equipe
|
| 17 |
+
int satisfaction_employee_equilibre_pro_perso
|
| 18 |
+
int note_evaluation_actuelle
|
| 19 |
+
varchar heure_supplementaires
|
| 20 |
+
int augementation_salaire_precedente
|
| 21 |
+
int nombre_participation_pee
|
| 22 |
+
int nb_formations_suivies
|
| 23 |
+
int distance_domicile_travail
|
| 24 |
+
int niveau_education
|
| 25 |
+
varchar frequence_deplacement
|
| 26 |
+
int annees_depuis_la_derniere_promotion
|
| 27 |
+
varchar a_quitte_l_entreprise
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
prediction_inputs {
|
| 31 |
+
int id PK
|
| 32 |
+
timestamp created_at
|
| 33 |
+
int id_employee
|
| 34 |
+
int age
|
| 35 |
+
varchar genre
|
| 36 |
+
int revenu_mensuel
|
| 37 |
+
varchar statut_marital
|
| 38 |
+
varchar departement
|
| 39 |
+
varchar poste
|
| 40 |
+
int annee_experience_totale
|
| 41 |
+
int annees_dans_l_entreprise
|
| 42 |
+
int satisfaction_employee_environnement
|
| 43 |
+
int note_evaluation_precedente
|
| 44 |
+
int satisfaction_employee_nature_travail
|
| 45 |
+
int satisfaction_employee_equipe
|
| 46 |
+
int satisfaction_employee_equilibre_pro_perso
|
| 47 |
+
int note_evaluation_actuelle
|
| 48 |
+
varchar heure_supplementaires
|
| 49 |
+
int augementation_salaire_precedente
|
| 50 |
+
int nombre_participation_pee
|
| 51 |
+
int nb_formations_suivies
|
| 52 |
+
int distance_domicile_travail
|
| 53 |
+
int niveau_education
|
| 54 |
+
varchar frequence_deplacement
|
| 55 |
+
int annees_depuis_la_derniere_promotion
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
prediction_inputs ||--|| prediction_outputs : "1:1"
|
| 59 |
+
|
| 60 |
+
prediction_outputs {
|
| 61 |
+
int id PK
|
| 62 |
+
int input_id FK
|
| 63 |
+
varchar prediction
|
| 64 |
+
float probabilite
|
| 65 |
+
int classe
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
api_logs {
|
| 69 |
+
int id PK
|
| 70 |
+
timestamp created_at
|
| 71 |
+
varchar operation
|
| 72 |
+
varchar table_cible
|
| 73 |
+
varchar details
|
| 74 |
+
varchar statut
|
| 75 |
+
}
|
| 76 |
+
```
|
src/utils/transformer.py
CHANGED
|
@@ -92,6 +92,9 @@ def transformer_donnees(donnees_employe: dict) -> pd.DataFrame:
|
|
| 92 |
# Créer un DataFrame pandas
|
| 93 |
df = pd.DataFrame([donnees_employe])
|
| 94 |
|
|
|
|
|
|
|
|
|
|
| 95 |
# ÉTAPE 1: CALCULS PRÉLIMINAIRES
|
| 96 |
# Ces calculs utilisent les colonnes brutes avant suppression
|
| 97 |
|
|
|
|
| 92 |
# Créer un DataFrame pandas
|
| 93 |
df = pd.DataFrame([donnees_employe])
|
| 94 |
|
| 95 |
+
# Supprimer l'ID avant toute transformation (non utilisé par le modèle)
|
| 96 |
+
df = df.drop(columns=["id_employee"], errors="ignore")
|
| 97 |
+
|
| 98 |
# ÉTAPE 1: CALCULS PRÉLIMINAIRES
|
| 99 |
# Ces calculs utilisent les colonnes brutes avant suppression
|
| 100 |
|
uv.lock
CHANGED
|
@@ -2012,6 +2012,7 @@ dependencies = [
|
|
| 2012 |
{ name = "joblib" },
|
| 2013 |
{ name = "numpy" },
|
| 2014 |
{ name = "pandas" },
|
|
|
|
| 2015 |
{ name = "pydantic" },
|
| 2016 |
{ name = "scikit-learn" },
|
| 2017 |
{ name = "sqlalchemy" },
|
|
@@ -2041,6 +2042,7 @@ requires-dist = [
|
|
| 2041 |
{ name = "joblib", specifier = "==1.5.3" },
|
| 2042 |
{ name = "numpy", specifier = "==2.3.5" },
|
| 2043 |
{ name = "pandas", specifier = "==2.3.3" },
|
|
|
|
| 2044 |
{ name = "pydantic", specifier = "==2.12.5" },
|
| 2045 |
{ name = "scikit-learn", specifier = "==1.8.0" },
|
| 2046 |
{ name = "sqlalchemy", specifier = "==2.0.46" },
|
|
@@ -2308,6 +2310,47 @@ wheels = [
|
|
| 2308 |
{ url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" },
|
| 2309 |
]
|
| 2310 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2311 |
[[package]]
|
| 2312 |
name = "ptyprocess"
|
| 2313 |
version = "0.7.0"
|
|
|
|
| 2012 |
{ name = "joblib" },
|
| 2013 |
{ name = "numpy" },
|
| 2014 |
{ name = "pandas" },
|
| 2015 |
+
{ name = "psycopg2-binary" },
|
| 2016 |
{ name = "pydantic" },
|
| 2017 |
{ name = "scikit-learn" },
|
| 2018 |
{ name = "sqlalchemy" },
|
|
|
|
| 2042 |
{ name = "joblib", specifier = "==1.5.3" },
|
| 2043 |
{ name = "numpy", specifier = "==2.3.5" },
|
| 2044 |
{ name = "pandas", specifier = "==2.3.3" },
|
| 2045 |
+
{ name = "psycopg2-binary", specifier = "==2.9.12" },
|
| 2046 |
{ name = "pydantic", specifier = "==2.12.5" },
|
| 2047 |
{ name = "scikit-learn", specifier = "==1.8.0" },
|
| 2048 |
{ name = "sqlalchemy", specifier = "==2.0.46" },
|
|
|
|
| 2310 |
{ url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" },
|
| 2311 |
]
|
| 2312 |
|
| 2313 |
+
[[package]]
|
| 2314 |
+
name = "psycopg2-binary"
|
| 2315 |
+
version = "2.9.12"
|
| 2316 |
+
source = { registry = "https://pypi.org/simple" }
|
| 2317 |
+
sdist = { url = "https://files.pythonhosted.org/packages/2a/60/a3624f79acea344c16fbef3a94d28b89a8042ddfb8f3e4ca83f538671409/psycopg2_binary-2.9.12.tar.gz", hash = "sha256:5ac9444edc768c02a6b6a591f070b8aae28ff3a99be57560ac996001580f294c", size = 379686, upload-time = "2026-04-21T09:40:34.304Z" }
|
| 2318 |
+
wheels = [
|
| 2319 |
+
{ url = "https://files.pythonhosted.org/packages/e2/9f/ef4ef3c8e15083df90ca35265cfd1a081a2f0cc07bb229c6314c6af817f4/psycopg2_binary-2.9.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5cdc05117180c5fa9c40eea8ea559ce64d73824c39d928b7da9fb5f6a9392433", size = 3712459, upload-time = "2026-04-20T23:34:30.549Z" },
|
| 2320 |
+
{ url = "https://files.pythonhosted.org/packages/b5/01/3dd14e46ba48c1e1a6ec58ee599fa1b5efa00c246d5046cd903d0eeb1af1/psycopg2_binary-2.9.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d3227a3bc228c10d21011a99245edca923e4e8bf461857e869a507d9a41fe9f6", size = 3822936, upload-time = "2026-04-20T23:34:32.77Z" },
|
| 2321 |
+
{ url = "https://files.pythonhosted.org/packages/a6/f7/0640e4901119d8a9f7a1784b927f494e2198e213ceb593753d1f2c8b1b30/psycopg2_binary-2.9.12-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:995ce929eede89db6254b50827e2b7fd61e50d11f0b116b29fffe4a2e53c4580", size = 4578676, upload-time = "2026-04-20T23:34:35.18Z" },
|
| 2322 |
+
{ url = "https://files.pythonhosted.org/packages/b0/55/44df3965b5f297c50cc0b1b594a31c67d6127a9d133045b8a66611b14dfb/psycopg2_binary-2.9.12-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9fe06d93e72f1c048e731a2e3e7854a5bfaa58fc736068df90b352cefe66f03f", size = 4274917, upload-time = "2026-04-20T23:34:37.982Z" },
|
| 2323 |
+
{ url = "https://files.pythonhosted.org/packages/b0/4b/74535248b1eac0c9336862e8617c765ac94dac76f9e25d7c4a79588c8907/psycopg2_binary-2.9.12-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40e7b28b63aaf737cb3a1edc3a9bbc9a9f4ad3dcb7152e8c1130e4050eddcb7d", size = 5894843, upload-time = "2026-04-20T23:34:40.856Z" },
|
| 2324 |
+
{ url = "https://files.pythonhosted.org/packages/f2/ba/f1bf8d2ae71868ad800b661099086ee52bc0f8d9f05be1acd8ebb06757cc/psycopg2_binary-2.9.12-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:89d19a9f7899e8eb0656a2b3a08e0da04c720a06db6e0033eab5928aabe60fa9", size = 4110556, upload-time = "2026-04-20T23:34:44.016Z" },
|
| 2325 |
+
{ url = "https://files.pythonhosted.org/packages/45/46/c15706c338403b7c420bcc0c2905aad116cc064545686d8bf85f1999ea00/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:612b965daee295ae2da8f8218ce1d274645dc76ef3f1abf6a0a94fd57eff876d", size = 3655714, upload-time = "2026-04-20T23:34:46.233Z" },
|
| 2326 |
+
{ url = "https://files.pythonhosted.org/packages/b3/7c/a2d5dc09b64a4564db242a0fe418fde7d33f6f8259dd2c5b9d7def00fb5a/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b9a339b79d37c1b45f3235265f07cdeb0cb5ad7acd2ac7720a5920989c17c24e", size = 3301154, upload-time = "2026-04-20T23:34:49.528Z" },
|
| 2327 |
+
{ url = "https://files.pythonhosted.org/packages/c0/e8/cc8c9a4ce71461f9ec548d38cadc41dc184b34c73e6455450775a9334ccd/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:3471336e1acfd9c7fe507b8bad5af9317b6a89294f9eb37bd9a030bb7bebcdc6", size = 3048882, upload-time = "2026-04-20T23:34:51.86Z" },
|
| 2328 |
+
{ url = "https://files.pythonhosted.org/packages/19/6a/31e2296bc0787c5ab75d3d118e40b239db8151b5192b90b77c72bc9256e9/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7af18183109e23502c8b2ae7f6926c0882766f35b5175a4cd737ad825e4d7a1b", size = 3351298, upload-time = "2026-04-20T23:34:54.124Z" },
|
| 2329 |
+
{ url = "https://files.pythonhosted.org/packages/5f/a8/75f4e3e11203b590150abed2cf7794b9c9c9f7eceddae955191138b44dde/psycopg2_binary-2.9.12-cp312-cp312-win_amd64.whl", hash = "sha256:398fcd4db988c7d7d3713e2b8e18939776fd3fb447052daae4f24fa39daede4c", size = 2757230, upload-time = "2026-04-20T23:34:56.242Z" },
|
| 2330 |
+
{ url = "https://files.pythonhosted.org/packages/91/bb/4608c96f970f6e0c56572e87027ef4404f709382a3503e9934526d7ba051/psycopg2_binary-2.9.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7c729a73c7b1b84de3582f73cdd27d905121dc2c531f3d9a3c32a3011033b965", size = 3712419, upload-time = "2026-04-20T23:34:58.754Z" },
|
| 2331 |
+
{ url = "https://files.pythonhosted.org/packages/5e/af/48f76af9d50d61cf390f8cd657b503168b089e2e9298e48465d029fcc713/psycopg2_binary-2.9.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4413d0caef93c5cf50b96863df4c2efe8c269bf2267df353225595e7e15e8df7", size = 3822990, upload-time = "2026-04-20T23:35:00.821Z" },
|
| 2332 |
+
{ url = "https://files.pythonhosted.org/packages/7a/df/aba0f99397cd811d32e06fc0cc781f1f3ce98bc0e729cb423925085d781a/psycopg2_binary-2.9.12-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4dfcf8e45ebb0c663be34a3442f65e17311f3367089cd4e5e3a3e8e62c978777", size = 4578696, upload-time = "2026-04-20T23:35:03.409Z" },
|
| 2333 |
+
{ url = "https://files.pythonhosted.org/packages/95/9c/eaa74021ac4e4d5c2f83d82fc6615a63f4fe6c94dc4e94c3990427053f67/psycopg2_binary-2.9.12-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c41321a14dd74aceb6a9a643b9253a334521babfa763fa873e33d89cfa122fb5", size = 4274982, upload-time = "2026-04-20T23:35:05.583Z" },
|
| 2334 |
+
{ url = "https://files.pythonhosted.org/packages/35/ed/c25deff98bd26187ba48b3b250a3ffc3037c46c5b89362534a15d200e0db/psycopg2_binary-2.9.12-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83946ba43979ebfdc99a3cd0ee775c89f221df026984ba19d46133d8d75d3cd9", size = 5894867, upload-time = "2026-04-20T23:35:07.902Z" },
|
| 2335 |
+
{ url = "https://files.pythonhosted.org/packages/9a/81/8d0e21ca77373c6c9589e5c4528f6e8f0c08c62cafc76fb0bddb7a2cee22/psycopg2_binary-2.9.12-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:411e85815652d13560fbe731878daa5d92378c4995a22302071890ec3397d019", size = 4110578, upload-time = "2026-04-20T23:35:10.149Z" },
|
| 2336 |
+
{ url = "https://files.pythonhosted.org/packages/00/fc/f481e2435bd8f742d0123309174aae4165160ad3ef17c1b99c3622c241d2/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c8ad4c08e00f7679559eaed7aff1edfffc60c086b976f93972f686384a95e2c", size = 3655816, upload-time = "2026-04-20T23:35:12.56Z" },
|
| 2337 |
+
{ url = "https://files.pythonhosted.org/packages/53/79/b9f46466bdbe9f239c96cde8be33c1aace4842f06013b47b730dc9759187/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:00814e40fa23c2b37ef0a1e3c749d89982c73a9cb5046137f0752a22d432e82f", size = 3301307, upload-time = "2026-04-20T23:35:15.029Z" },
|
| 2338 |
+
{ url = "https://files.pythonhosted.org/packages/3f/19/7dc003b32fe35024df89b658104f7c8538a8b2dcbde7a4e746ce929742e7/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:98062447aebc20ed20add1f547a364fd0ef8933640d5372ff1873f8deb9b61be", size = 3048968, upload-time = "2026-04-20T23:35:16.757Z" },
|
| 2339 |
+
{ url = "https://files.pythonhosted.org/packages/91/58/2dbd7db5c604d45f4950d988506aae672a14126ec22998ced5021cbb76bb/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:66a7685d7e548f10fb4ce32fb01a7b7f4aa702134de92a292c7bd9e0d3dbd290", size = 3351369, upload-time = "2026-04-20T23:35:18.933Z" },
|
| 2340 |
+
{ url = "https://files.pythonhosted.org/packages/42/ee/dee8dcaad07f735824de3d6563bc67119fa6c28257b17977a8d624f02fab/psycopg2_binary-2.9.12-cp313-cp313-win_amd64.whl", hash = "sha256:b6937f5fe4e180aeee87de907a2fa982ded6f7f15d7218f78a083e4e1d68f2a0", size = 2757347, upload-time = "2026-04-20T23:35:21.283Z" },
|
| 2341 |
+
{ url = "https://files.pythonhosted.org/packages/13/1b/708c0dca874acfad6d65314271859899a79007686f3a1f74e82a2ed4b645/psycopg2_binary-2.9.12-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6f3b3de8a74ef8db215f22edffb19e32dc6fa41340456de7ec99efdc8a7b3ec2", size = 3712428, upload-time = "2026-04-20T23:35:23.453Z" },
|
| 2342 |
+
{ url = "https://files.pythonhosted.org/packages/d6/39/ddbea9d4b4de6aca9431b6ed253f530f8a02d3b8f9bcfd0dbfe2b3de6fe4/psycopg2_binary-2.9.12-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1006fb62f0f0bc5ce256a832356c6262e91be43f5e4eb15b5eaf38079464caf2", size = 3823184, upload-time = "2026-04-20T23:35:25.92Z" },
|
| 2343 |
+
{ url = "https://files.pythonhosted.org/packages/bf/a0/bc2fef74b106fa345567122a0659e6d94512ed7dc0131ec44c9e5aba3725/psycopg2_binary-2.9.12-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:840066105706cd2eb29b9a1c2329620056582a4bf3e8169dec5c447042d0869f", size = 4579157, upload-time = "2026-04-20T23:35:28.542Z" },
|
| 2344 |
+
{ url = "https://files.pythonhosted.org/packages/57/d7/d4e3b2005d3de607ca4fbb0e8742e248056e52184a6b94ebda3c1c2c329b/psycopg2_binary-2.9.12-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:863f5d12241ebe1c76a72a04c2113b6dc905f90b9cef0e9be0efd994affd9354", size = 4274970, upload-time = "2026-04-20T23:35:30.418Z" },
|
| 2345 |
+
{ url = "https://files.pythonhosted.org/packages/2e/42/c9853f8db3967fe08bcde11f53d53b85d351750cae726ce001cb68afa9c1/psycopg2_binary-2.9.12-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a99eaab34a9010f1a086b126de467466620a750634d114d20455f3a824aae033", size = 5895175, upload-time = "2026-04-20T23:35:33.584Z" },
|
| 2346 |
+
{ url = "https://files.pythonhosted.org/packages/eb/fd/b82b5601a97630308bef079f545ffec481bbbc795c2ba5ec416a01d03f60/psycopg2_binary-2.9.12-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ffdd7dc5463ccd61845ac37b7012d0f35a1548df9febe14f8dd549be4a0bc81e", size = 4110658, upload-time = "2026-04-20T23:35:35.638Z" },
|
| 2347 |
+
{ url = "https://files.pythonhosted.org/packages/62/8c/32ca69b0389ef25dd22937bf9e8fbe2ce27aea20b05ded48c4ce4cb42475/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:54a0dfecab1b48731f934e06139dfe11e24219fb6d0ceb32177cf0375f14c7b5", size = 3656251, upload-time = "2026-04-20T23:35:37.854Z" },
|
| 2348 |
+
{ url = "https://files.pythonhosted.org/packages/c4/29/96992a2b59e3b9d730fcf9612d0a387305025dc867a9fc490a9e496e074e/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:96937c9c5d891f772430f418a7a8b4691a90c3e6b93cf72b5bd7cad8cbca32a5", size = 3301810, upload-time = "2026-04-20T23:35:39.927Z" },
|
| 2349 |
+
{ url = "https://files.pythonhosted.org/packages/56/ad/44b06659949b243ae10112cd3b20a197f9bf3e81d5651379b9eb889bfaad/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:77b348775efd4cdab410ec6609d81ccecd1139c90265fa583a7255c8064bc03d", size = 3048977, upload-time = "2026-04-20T23:35:41.806Z" },
|
| 2350 |
+
{ url = "https://files.pythonhosted.org/packages/1d/f2/10a1bcebadb6aa55e280e1f58975c36a7b560ea525184c7aa4064c466633/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:527e6342b3e44c2f0544f6b8e927d60de7f163f5723b8f1dfa7d2a84298738cd", size = 3351466, upload-time = "2026-04-20T23:35:43.993Z" },
|
| 2351 |
+
{ url = "https://files.pythonhosted.org/packages/20/be/b732c8418ffa5bcfda002890f5dc4c869fc17db66ff11f53b17cfe44afc0/psycopg2_binary-2.9.12-cp314-cp314-win_amd64.whl", hash = "sha256:f12ae41fcafadb39b2785e64a40f9db05d6de2ac114077457e0e7c597f3af980", size = 2848762, upload-time = "2026-04-20T23:35:46.421Z" },
|
| 2352 |
+
]
|
| 2353 |
+
|
| 2354 |
[[package]]
|
| 2355 |
name = "ptyprocess"
|
| 2356 |
version = "0.7.0"
|