Employee-Churn-Prediction / src /utils /transformer.py
Alexis-Ravet's picture
Upload folder using huggingface_hub
b666236 verified
Raw
History Blame Contribute Delete
10.8 kB
"""
Module de transformation des données pour le modèle de prédiction du churn employé.
Ce code reproduit les transformations faites dans le notebook Jupyter.
Les données sont d'abord validées par Pydantic (dans l'API), puis transformées ici.
"""
import pandas as pd
def _get_ordinal_mappings() -> dict:
"""
Reproduit les mappings du notebook pour l'OrdinalEncoder.
"""
return {
# experience_categorie: Junior=1, Confirmé=2, Senior=3, Expert=4
"experience_categorie": {"Junior": 1, "Confirmé": 2, "Senior": 3, "Expert": 4},
# ecart_revenu_categorie: Revenu bien inférieur=1 à Revenu bien supérieur=4
"ecart_revenu_categorie": {
"Revenu bien inférieur à la médiane": 1,
"Revenu inférieur à la médiane": 2,
"Revenu supérieur à la médiane": 3,
"Revenu bien supérieur à la médiane": 4,
},
# age_categorie: Jeune=1, Jeune adulte=2, Adulte=3, Mature=4
"age_categorie": {"Jeune": 1, "Jeune adulte": 2, "Adulte": 3, "Mature": 4},
# poste: hiérarchie de 1 à 9
"poste": {
"Représentant Commercial": 1,
"Ressources Humaines": 2,
"Assistant de Direction": 3,
"Consultant": 4,
"Manager": 5,
"Cadre Commercial": 6,
"Tech Lead": 7,
"Senior Manager": 8,
"Directeur Technique": 9,
},
# frequence_deplacement: Aucun=1, Occasionnel=2, Frequent=3
"frequence_deplacement": {"Aucun": 1, "Occasionnel": 2, "Frequent": 3},
}
def _get_bins_and_labels() -> dict:
"""
Reproduit les seuils du notebook pour les bins et labels des catégories calculées.
"""
return {
# Quartiles de annee_experience_totale (min, Q1=6, Médiane=10, Q3=15, max)
"experience_categorie_bins": {
"bins": [0, 6, 10, 15, 47],
"labels": ["Junior", "Confirmé", "Senior", "Expert"],
},
# Quartiles de ecart_revenu (min, Q1=-744, Médiane=0, Q3=1027.25, max)
"ecart_revenu_categorie_bins": {
"bins": [-20000, -744, 0, 1027.25, 20000],
"labels": [
"Revenu bien inférieur à la médiane",
"Revenu inférieur à la médiane",
"Revenu supérieur à la médiane",
"Revenu bien supérieur à la médiane",
],
},
# Quartiles de age (min, Q1=30, Médiane=36, Q3=43, max)
"age_categorie_bins": {
"bins": [0, 30, 36, 43, 65],
"labels": ["Jeune", "Jeune adulte", "Adulte", "Mature"],
},
}
def transformer_donnees(donnees_employe: dict) -> pd.DataFrame:
"""
Transforme les données d'un employé pour le modèle.
Cette fonction reproduit les étapes du notebook :
1. Calculs préliminaires : diff_note_evaluation, ratio_experience
2. Création des catégories (bins)
3. Suppression des colonnes brutes utilisées pour les calculs
4. Application des OrdinalEncoder
5. Binarization pour genre et heure_supplementaires
6. OneHotEncoder pour statut_marital et departement
7. Ordonnancement final des colonnes
8. Contrôle de sécurité
Args:
donnees_employe: Dict avec les données de l'employé (validées par Pydantic)
Returns:
DataFrame prétraité avec les 24 features exactes pour le modèle
"""
# Créer un DataFrame pandas
df = pd.DataFrame([donnees_employe])
# Supprimer l'ID avant toute transformation (non utilisé par le modèle)
df = df.drop(columns=["id_employee"], errors="ignore")
# ÉTAPE 1: CALCULS PRÉLIMINAIRES
# Ces calculs utilisent les colonnes brutes avant suppression
# diff_note_evaluation = note_evaluation_actuelle - note_evaluation_precedente
df["diff_note_evaluation"] = (
df["note_evaluation_actuelle"] - df["note_evaluation_precedente"]
)
# ratio_experience = annee_experience_totale / (annees_dans_l_entreprise + 1)
df["ratio_experience"] = round(
df["annee_experience_totale"] / (df["annees_dans_l_entreprise"] + 1), 2
)
# ÉTAPE 2: CRÉATION DES CATÉGORIES (BINS)
bins_config = _get_bins_and_labels()
# experience_categorie: Junior/Confirmé/Senior/Expert
df["experience_categorie"] = pd.cut(
df["annee_experience_totale"],
bins=bins_config["experience_categorie_bins"]["bins"],
labels=bins_config["experience_categorie_bins"]["labels"],
include_lowest=True,
)
# age_categorie: Jeune/Jeune adulte/Adulte/Mature
df["age_categorie"] = pd.cut(
df["age"],
bins=bins_config["age_categorie_bins"]["bins"],
labels=bins_config["age_categorie_bins"]["labels"],
include_lowest=True,
)
# ecart_revenu: différence avec le revenu médian par poste et expérience
df["ecart_revenu"] = df["revenu_mensuel"] - df.groupby(
["poste", "experience_categorie"]
)["revenu_mensuel"].transform("median")
# ecart_revenu_categorie
df["ecart_revenu_categorie"] = pd.cut(
df["ecart_revenu"],
bins=bins_config["ecart_revenu_categorie_bins"]["bins"],
labels=bins_config["ecart_revenu_categorie_bins"]["labels"],
include_lowest=True,
)
# ÉTAPE 3: SUPPRESSION DES COLONNES BRUTES
# Colonnes utilisées pour les calculs, désormais inutiles
df = df.drop(
columns=[
"note_evaluation_actuelle",
"note_evaluation_precedente",
"annee_experience_totale",
"revenu_mensuel",
"age",
"ecart_revenu", # colonne temporaire
],
errors="ignore",
)
# ÉTAPE 4: ORDINAL ENCODING
mappings = _get_ordinal_mappings()
# experience_categorie: Junior=1, Confirmé=2, Senior=3, Expert=4
df["experience_categorie"] = df["experience_categorie"].map(
mappings["experience_categorie"]
)
# age_categorie: Jeune=1, Jeune adulte=2, Adulte=3, Mature=4
df["age_categorie"] = df["age_categorie"].map(mappings["age_categorie"])
# ecart_revenu_categorie: 1 à 4
df["ecart_revenu_categorie"] = df["ecart_revenu_categorie"].map(
mappings["ecart_revenu_categorie"]
)
# poste: 1 à 9 (hiérarchie)
df["poste"] = df["poste"].map(mappings["poste"])
# frequence_deplacement: Aucun=1, Occasionnel=2, Frequent=3
df["frequence_deplacement"] = df["frequence_deplacement"].map(
mappings["frequence_deplacement"]
)
# ÉTAPE 5: BINARIZATION (TRANSFORMATIONS SIMPLES)
# genre: M=0, F=1
df["genre"] = df["genre"].replace({"M": 0, "F": 1})
# heure_supplementaires: Non=0, Oui=1
df["heure_supplementaires"] = df["heure_supplementaires"].replace(
{"Non": 0, "Oui": 1}
)
# ÉTAPE 6: ONE HOT ENCODING
# Utilisation de pd.Categorical pour forcer toutes les catégories
# Cela garantit que toutes les colonnes sont créées même avec une seule ligne
# statut_marital: drop_first=True (Célibataire supprimé)
if "statut_marital" in df.columns:
# Définir toutes les catégories pour drop_first=True
statut_cat = pd.Categorical(
df["statut_marital"], categories=["Célibataire", "Divorcé(e)", "Marié(e)"]
)
df_statut = pd.get_dummies(
statut_cat, prefix="statut_marital", drop_first=True, dtype=int
)
df = pd.concat([df.drop(columns=["statut_marital"]), df_statut], axis=1)
# departement: drop_first=True (Consulting supprimé)
if "departement" in df.columns:
dept_cat = pd.Categorical(
df["departement"],
categories=["Consulting", "Commercial", "Ressources Humaines"],
)
df_dept = pd.get_dummies(
dept_cat, prefix="departement", drop_first=True, dtype=int
)
df = pd.concat([df.drop(columns=["departement"]), df_dept], axis=1)
# ÉTAPE 7: ORDONNANCEMENT FINAL DES COLONNES
# Les 24 features finales telles qu'ordonnées dans le notebook
colonnes_finales = [
"genre",
"poste",
"annees_dans_l_entreprise",
"satisfaction_employee_environnement",
"satisfaction_employee_nature_travail",
"satisfaction_employee_equipe",
"satisfaction_employee_equilibre_pro_perso",
"heure_supplementaires",
"augementation_salaire_precedente",
"nombre_participation_pee",
"nb_formations_suivies",
"distance_domicile_travail",
"niveau_education",
"frequence_deplacement",
"annees_depuis_la_derniere_promotion",
"diff_note_evaluation",
"ratio_experience",
"experience_categorie",
"ecart_revenu_categorie",
"age_categorie",
"statut_marital_Divorcé(e)",
"statut_marital_Marié(e)",
"departement_Commercial",
"departement_Ressources Humaines",
]
# Vérification que toutes les colonnes existent
for col in colonnes_finales:
if col not in df.columns:
# Ajouter la colonne avec des zéros si pas présente (pour OneHot manquant)
df.loc[:, col] = 0
# Garder seulement les colonnes finales dans le bon ordre
df = df.loc[:, colonnes_finales]
# ÉTAPE 8: CONTRÔLE DE SÉCURITÉ
# Convertir les colonnes catégorielles en nombres
# (pd.cut retourne un dtype category → cette boucle convertit chaque catégorie en son indice numérique)
for col in df.columns:
if df[col].dtype.name == "category":
df.loc[:, col] = pd.Categorical(df[col]).codes
# Remplir les valeurs NaN par 0
df = df.fillna(0)
# S'assurer que tout est de type numérique
df = df.astype(float)
return df
def get_liste_features() -> list:
"""
Retourne la liste des 24 features attendues par le modèle.
"""
return [
"genre",
"poste",
"annees_dans_l_entreprise",
"satisfaction_employee_environnement",
"satisfaction_employee_nature_travail",
"satisfaction_employee_equipe",
"satisfaction_employee_equilibre_pro_perso",
"heure_supplementaires",
"augementation_salaire_precedente",
"nombre_participation_pee",
"nb_formations_suivies",
"distance_domicile_travail",
"niveau_education",
"frequence_deplacement",
"annees_depuis_la_derniere_promotion",
"diff_note_evaluation",
"ratio_experience",
"experience_categorie",
"ecart_revenu_categorie",
"age_categorie",
"statut_marital_Divorcé(e)",
"statut_marital_Marié(e)",
"departement_Commercial",
"departement_Ressources Humaines",
]