File size: 10,801 Bytes
02f31d3 b666236 02f31d3 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 | """
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",
]
|