oc_p5-dev / src /preprocessing.py
ASI-Engineer's picture
Upload folder using huggingface_hub
abd571c verified
#!/usr/bin/env python3
"""
Module de preprocessing pour transformer les données d'entrée avant prédiction.
Ce module applique les mêmes transformations que le pipeline d'entraînement :
- Feature engineering (ratios, moyennes)
- Encoding (OneHot, Ordinal)
- Scaling (StandardScaler avec paramètres sauvegardés)
"""
import numpy as np
import pandas as pd
from sklearn.preprocessing import OneHotEncoder, OrdinalEncoder
from src.schemas import EmployeeInput
# Paramètres du scaler sauvegardés depuis l'entraînement
# Ces valeurs doivent correspondre exactement à celles utilisées lors du training
SCALER_PARAMS = {
"columns": [
"nombre_participation_pee",
"nb_formations_suivies",
"nombre_employee_sous_responsabilite",
"distance_domicile_travail",
"niveau_education",
"annees_depuis_la_derniere_promotion",
"annes_sous_responsable_actuel",
"satisfaction_employee_environnement",
"note_evaluation_precedente",
"niveau_hierarchique_poste",
"satisfaction_employee_nature_travail",
"satisfaction_employee_equipe",
"satisfaction_employee_equilibre_pro_perso",
"note_evaluation_actuelle",
"augementation_salaire_precedente",
"age",
"revenu_mensuel",
"nombre_experiences_precedentes",
"nombre_heures_travailless",
"annee_experience_totale",
"annees_dans_l_entreprise",
"annees_dans_le_poste_actuel",
"revenu_par_anciennete",
"experience_par_anciennete",
"satisfaction_moyenne",
"promo_par_anciennete",
"frequence_deplacement",
],
"mean": [
0.7938775510204081,
2.7993197278911564,
1.0,
9.19251700680272,
2.912925170068027,
2.1789115646258503,
4.102721088435374,
2.721768707482993,
2.7299319727891156,
2.0639455782312925,
2.7285714285714286,
2.7122448979591836,
2.7612244897959184,
3.1537414965986397,
15.209523809523809,
36.923809523809524,
6502.931292517007,
2.6931972789115646,
80.0,
11.268707482993197,
6.980272108843537,
4.214965986394557,
1170.0019803036198,
1.9285635921785853,
2.730952380952381,
0.23624418065415922,
1.0863945578231293,
],
"scale": [
0.8517867966287158,
1.2888320187689346,
1.0,
8.104106529671768,
1.0238165299102608,
3.1873417003246085,
3.502524756587405,
1.0927103547111134,
0.7113190741884202,
1.1065633247112856,
1.1024709415085499,
1.0808410657505316,
0.7062354909319911,
0.3607007746349458,
3.658692627979528,
9.132265690615387,
4706.355164823003,
2.497159198593844,
1.0,
7.7078836108215345,
6.0028580432875085,
3.575242796407657,
1353.331540788815,
2.2050718706188372,
0.5056427624070211,
0.2687717006578023,
0.5319888822661019,
],
}
def create_input_dataframe(employee: EmployeeInput) -> pd.DataFrame:
"""
Convertit un objet EmployeeInput Pydantic en DataFrame pandas.
Args:
employee: Données validées d'un employé.
Returns:
DataFrame avec une seule ligne contenant toutes les features.
"""
data = {
# SONDAGE
"nombre_participation_pee": [employee.nombre_participation_pee],
"nb_formations_suivies": [employee.nb_formations_suivies],
"nombre_employee_sous_responsabilite": [
employee.nombre_employee_sous_responsabilite
],
"distance_domicile_travail": [employee.distance_domicile_travail],
"niveau_education": [employee.niveau_education],
"domaine_etude": [employee.domaine_etude],
"ayant_enfants": [employee.ayant_enfants],
"frequence_deplacement": [employee.frequence_deplacement],
"annees_depuis_la_derniere_promotion": [
employee.annees_depuis_la_derniere_promotion
],
"annes_sous_responsable_actuel": [employee.annes_sous_responsable_actuel],
# EVALUATION
"satisfaction_employee_environnement": [
employee.satisfaction_employee_environnement
],
"note_evaluation_precedente": [employee.note_evaluation_precedente],
"niveau_hierarchique_poste": [employee.niveau_hierarchique_poste],
"satisfaction_employee_nature_travail": [
employee.satisfaction_employee_nature_travail
],
"satisfaction_employee_equipe": [employee.satisfaction_employee_equipe],
"satisfaction_employee_equilibre_pro_perso": [
employee.satisfaction_employee_equilibre_pro_perso
],
"note_evaluation_actuelle": [employee.note_evaluation_actuelle],
"heure_supplementaires": [employee.heure_supplementaires],
"augementation_salaire_precedente": [employee.augementation_salaire_precedente],
# SIRH
"age": [employee.age],
"genre": [employee.genre],
"revenu_mensuel": [employee.revenu_mensuel],
"statut_marital": [employee.statut_marital],
"departement": [employee.departement],
"poste": [employee.poste],
"nombre_experiences_precedentes": [employee.nombre_experiences_precedentes],
"nombre_heures_travailless": [employee.nombre_heures_travailless],
"annee_experience_totale": [employee.annee_experience_totale],
"annees_dans_l_entreprise": [employee.annees_dans_l_entreprise],
"annees_dans_le_poste_actuel": [employee.annees_dans_le_poste_actuel],
}
return pd.DataFrame(data)
def engineer_features(df: pd.DataFrame) -> pd.DataFrame:
"""
Applique le feature engineering (mêmes transformations que l'entraînement).
Args:
df: DataFrame avec les colonnes brutes.
Returns:
DataFrame avec les features engineered ajoutées.
"""
df = df.copy()
# Ratios (+ 1 pour éviter division par zéro)
df["revenu_par_anciennete"] = df["revenu_mensuel"] / (
df["annees_dans_l_entreprise"] + 1
)
df["experience_par_anciennete"] = df["annee_experience_totale"] / (
df["annees_dans_l_entreprise"] + 1
)
df["promo_par_anciennete"] = df["annees_depuis_la_derniere_promotion"] / (
df["annees_dans_l_entreprise"] + 1
)
# Moyenne de satisfaction
df["satisfaction_moyenne"] = df[
[
"satisfaction_employee_environnement",
"satisfaction_employee_nature_travail",
"satisfaction_employee_equipe",
"satisfaction_employee_equilibre_pro_perso",
]
].mean(axis=1)
return df
def encode_and_scale(df: pd.DataFrame) -> pd.DataFrame:
"""
Encode les variables catégorielles et scale les numériques.
IMPORTANT: Doit correspondre EXACTEMENT au pipeline d'entraînement.
Args:
df: DataFrame avec features engineered.
Returns:
DataFrame transformé avec 50 colonnes dans l'ordre exact du modèle.
"""
df = df.copy()
# === ENCODING ===
# NOTE: ayant_enfants et heure_supplementaires sont SUPPRIMÉS
# (ne font pas partie des features du modèle d'entraînement)
cols_to_drop = ["ayant_enfants", "heure_supplementaires"]
df = df.drop(columns=[col for col in cols_to_drop if col in df.columns])
# OneHot pour variables catégorielles non-ordonnées
# IMPORTANT: Utiliser les mêmes catégories que lors de l'entraînement
cat_non_ord = ["genre", "statut_marital", "departement", "poste", "domaine_etude"]
# Définir toutes les catégories possibles (depuis training data)
categories_dict = {
"genre": ["F", "M"],
"statut_marital": ["Célibataire", "Divorcé(e)", "Marié(e)"],
"departement": ["Commercial", "Consulting", "Ressources Humaines"],
"poste": [
"Assistant de Direction",
"Cadre Commercial",
"Consultant",
"Directeur Technique",
"Manager",
"Représentant Commercial",
"Ressources Humaines",
"Senior Manager",
"Tech Lead",
],
"domaine_etude": [
"Autre",
"Entrepreunariat",
"Infra & Cloud",
"Marketing",
"Ressources Humaines",
"Transformation Digitale",
],
}
onehot = OneHotEncoder(
sparse_output=False,
handle_unknown="ignore",
categories=[categories_dict[col] for col in cat_non_ord],
)
encoded_non_ord = pd.DataFrame(
onehot.fit_transform(df[cat_non_ord]),
columns=onehot.get_feature_names_out(cat_non_ord),
index=df.index,
)
# Ordinal pour fréquence déplacement
ordinal = OrdinalEncoder(categories=[["Aucun", "Occasionnel", "Frequent"]])
df["frequence_deplacement"] = ordinal.fit_transform(
df[["frequence_deplacement"]]
).flatten()
# Supprimer les colonnes catégorielles originales
df = df.drop(columns=cat_non_ord)
# Concaténer les encodages OneHot
df = pd.concat([df, encoded_non_ord], axis=1)
# === RÉORDONNER LES COLONNES SELON L'ORDRE DU MODÈLE ===
# Ordre exact des features attendues par le modèle (50 colonnes)
expected_columns = [
"nombre_participation_pee",
"nb_formations_suivies",
"nombre_employee_sous_responsabilite",
"distance_domicile_travail",
"niveau_education",
"annees_depuis_la_derniere_promotion",
"annes_sous_responsable_actuel",
"satisfaction_employee_environnement",
"note_evaluation_precedente",
"niveau_hierarchique_poste",
"satisfaction_employee_nature_travail",
"satisfaction_employee_equipe",
"satisfaction_employee_equilibre_pro_perso",
"note_evaluation_actuelle",
"augementation_salaire_precedente",
"age",
"revenu_mensuel",
"nombre_experiences_precedentes",
"nombre_heures_travailless",
"annee_experience_totale",
"annees_dans_l_entreprise",
"annees_dans_le_poste_actuel",
"revenu_par_anciennete",
"experience_par_anciennete",
"satisfaction_moyenne",
"promo_par_anciennete",
"genre_F",
"genre_M",
"statut_marital_Célibataire",
"statut_marital_Divorcé(e)",
"statut_marital_Marié(e)",
"departement_Commercial",
"departement_Consulting",
"departement_Ressources Humaines",
"poste_Assistant de Direction",
"poste_Cadre Commercial",
"poste_Consultant",
"poste_Directeur Technique",
"poste_Manager",
"poste_Représentant Commercial",
"poste_Ressources Humaines",
"poste_Senior Manager",
"poste_Tech Lead",
"domaine_etude_Autre",
"domaine_etude_Entrepreunariat",
"domaine_etude_Infra & Cloud",
"domaine_etude_Marketing",
"domaine_etude_Ressources Humaines",
"domaine_etude_Transformation Digitale",
"frequence_deplacement",
]
# Réordonner les colonnes
df = df[expected_columns]
# === SCALING ===
# Appliquer le StandardScaler avec les paramètres sauvegardés
for i, col in enumerate(SCALER_PARAMS["columns"]):
if col in df.columns:
mean = SCALER_PARAMS["mean"][i]
scale = SCALER_PARAMS["scale"][i]
df[col] = (df[col] - mean) / scale
return df
def preprocess_for_prediction(employee: EmployeeInput) -> np.ndarray:
"""
Pipeline complet de preprocessing pour une prédiction.
Args:
employee: Données validées d'un employé.
Returns:
Array numpy transformé prêt pour model.predict().
Examples:
>>> from src.schemas import EmployeeInput
>>> employee = EmployeeInput(...)
>>> X = preprocess_for_prediction(employee)
>>> prediction = model.predict(X)
"""
# 1. Créer DataFrame
df = create_input_dataframe(employee)
# 2. Feature engineering
df = engineer_features(df)
# 3. Encoding et scaling
df = encode_and_scale(df)
# 4. Convertir en numpy array (le modèle attend un array)
return df.values
def preprocess_dataframe_for_prediction(df: pd.DataFrame) -> pd.DataFrame:
"""
Préprocess un DataFrame complet (issu de CSV fusionnés) pour prédiction batch.
Args:
df: DataFrame avec toutes les colonnes nécessaires.
Returns:
DataFrame transformé prêt pour model.predict().
"""
# Feature engineering
df_processed = engineer_features(df)
# Encoding et scaling
df_processed = encode_and_scale(df_processed)
return df_processed
def merge_csv_dataframes(
sondage_df: pd.DataFrame,
eval_df: pd.DataFrame,
sirh_df: pd.DataFrame,
) -> pd.DataFrame:
"""
Fusionne les 3 DataFrames CSV comme lors de l'entraînement.
Args:
sondage_df: DataFrame du fichier sondage.
eval_df: DataFrame du fichier évaluation.
sirh_df: DataFrame du fichier SIRH.
Returns:
DataFrame fusionné avec toutes les colonnes.
"""
# Nettoyage de l'évaluation
eval_df = eval_df.copy()
eval_df["augementation_salaire_precedente"] = eval_df[
"augementation_salaire_precedente"
].apply(lambda x: float(str(x).replace(" %", "")) if isinstance(x, str) else x)
eval_df["employee_id"] = eval_df["eval_number"].apply(
lambda x: int(str(x).replace("E_", "")) if isinstance(x, str) else x
)
# Nettoyage du sondage
sondage_df = sondage_df.copy()
sondage_df["employee_id"] = sondage_df["code_sondage"].apply(
lambda x: int(x) if isinstance(x, (str, int)) else None
)
# Fusion
central_df = pd.merge(sondage_df, eval_df, on="employee_id", how="inner")
central_df = pd.merge(
central_df, sirh_df, left_on="employee_id", right_on="id_employee", how="inner"
)
# Conserver l'ID pour le retour
central_df["original_employee_id"] = central_df["employee_id"]
# Supprimer les colonnes de jointure
central_df.drop(
["code_sondage", "eval_number", "id_employee", "employee_id"],
axis=1,
inplace=True,
errors="ignore",
)
return central_df