Projet : Identifiez les causes d'attrition au sein d'une ESN

Bouzouita Hayette

Contexte : ESN (spécialisée dans la transformation digitaele + vente d'applications SaaS) avec une hausse de taux de démission.
But coté RH : Objectiver la situation via les données, identifier les causes/segments à risque ainsi que les leviers actionnables pour y remédier.

Mission 1 en 2 notebooks :

  • EDA multi-fichiers (3 datasets) -> comparer les employés partants vs restants / faire ressortir les différences clés / formaliser des insights RH .
  • Modèle de classification : Scorer la probabilité de démission par employé, puis expliquer les drivers avec SHAP (mais seulement après avoir stabilisé le pipeline de modélisation).

-> Présentation à l'issu de la mission

Mission 2 (plus tard) :

  • Préparer le déploiement via PostgreSQL + requêtes SQL de test/validation.
  • Déployer le modèle de ML.

-> Présentation à l'issu de la mission

Notebook 1 : EDA

Section 1 : Importation et Compréhension des données

In [1]:
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns 
import numpy as np

from pathlib import Path


pd.set_option("display.max_columns",200)
pd.set_option("display.max_rows",200)
In [2]:
#Chargement des fichiers csv 

DATA_PATH = Path("..")/"data"/"raw"
sirh = pd.read_csv(DATA_PATH/"extrait_sirh.csv")
evals = pd.read_csv(DATA_PATH/"extrait_eval.csv")
sond = pd.read_csv(DATA_PATH/"extrait_sondage.csv")

Nous récupérons 3 fichiers csv:

In [3]:
#check rapide des 3 datasets : taille, typage, NaN 
def quick_profile(df, name):
    print(f"\n=== {name} ===")
    print("shape:", df.shape)
    print("\nDtypes:\n", df.dtypes)
    na = (df.isna().mean().sort_values(ascending=False) * 100).round(1)
    print("\n% NA (top 15):\n", na.head(15))

quick_profile(sirh, "SIRH")
quick_profile(evals, "EVALS")
quick_profile(sond, "SONDAGE")
=== SIRH ===
shape: (1470, 12)

Dtypes:
 id_employee                       int64
age                               int64
genre                               str
revenu_mensuel                    int64
statut_marital                      str
departement                         str
poste                               str
nombre_experiences_precedentes    int64
nombre_heures_travailless         int64
annee_experience_totale           int64
annees_dans_l_entreprise          int64
annees_dans_le_poste_actuel       int64
dtype: object

% NA (top 15):
 id_employee                       0.0
age                               0.0
genre                             0.0
revenu_mensuel                    0.0
statut_marital                    0.0
departement                       0.0
poste                             0.0
nombre_experiences_precedentes    0.0
nombre_heures_travailless         0.0
annee_experience_totale           0.0
annees_dans_l_entreprise          0.0
annees_dans_le_poste_actuel       0.0
dtype: float64

=== EVALS ===
shape: (1470, 10)

Dtypes:
 satisfaction_employee_environnement          int64
note_evaluation_precedente                   int64
niveau_hierarchique_poste                    int64
satisfaction_employee_nature_travail         int64
satisfaction_employee_equipe                 int64
satisfaction_employee_equilibre_pro_perso    int64
eval_number                                    str
note_evaluation_actuelle                     int64
heure_supplementaires                          str
augementation_salaire_precedente               str
dtype: object

% NA (top 15):
 satisfaction_employee_environnement          0.0
note_evaluation_precedente                   0.0
niveau_hierarchique_poste                    0.0
satisfaction_employee_nature_travail         0.0
satisfaction_employee_equipe                 0.0
satisfaction_employee_equilibre_pro_perso    0.0
eval_number                                  0.0
note_evaluation_actuelle                     0.0
heure_supplementaires                        0.0
augementation_salaire_precedente             0.0
dtype: float64

=== SONDAGE ===
shape: (1470, 12)

Dtypes:
 a_quitte_l_entreprise                    str
nombre_participation_pee               int64
nb_formations_suivies                  int64
nombre_employee_sous_responsabilite    int64
code_sondage                           int64
distance_domicile_travail              int64
niveau_education                       int64
domaine_etude                            str
ayant_enfants                            str
frequence_deplacement                    str
annees_depuis_la_derniere_promotion    int64
annes_sous_responsable_actuel          int64
dtype: object

% NA (top 15):
 a_quitte_l_entreprise                  0.0
nombre_participation_pee               0.0
nb_formations_suivies                  0.0
nombre_employee_sous_responsabilite    0.0
code_sondage                           0.0
distance_domicile_travail              0.0
niveau_education                       0.0
domaine_etude                          0.0
ayant_enfants                          0.0
frequence_deplacement                  0.0
annees_depuis_la_derniere_promotion    0.0
annes_sous_responsable_actuel          0.0
dtype: float64

Les 3 datasets contiennent 1470 lignes. Une ligne = un employé.
Il n'y a pas de id_employee dans les datasets evals et sond. Il faudra donc vérifier si les 3 identifiants de chaque dataset correspondent entre eux.
Il n'y a pas de valeurs manquantes explicites dans les jeux de données.
Il y a des incohérences de typages que nous devront traiter dans la section de nettoyage et de préparation des données.
Il y a des erreurs de saisies dans les noms des colonnes, il faudra également vérifier la qualité des variables catégorielles.
La variable a_quitte_l_entreprise est la target (variable cible). Elle devra être convertie en variable numérique binaire (0/1).

In [4]:
sirh.head()
Out[4]:
id_employee age genre revenu_mensuel statut_marital departement poste nombre_experiences_precedentes nombre_heures_travailless annee_experience_totale annees_dans_l_entreprise annees_dans_le_poste_actuel
0 1 41 F 5993 Célibataire Commercial Cadre Commercial 8 80 8 6 4
1 2 49 M 5130 Marié(e) Consulting Assistant de Direction 1 80 10 10 7
2 4 37 M 2090 Célibataire Consulting Consultant 6 80 7 0 0
3 5 33 F 2909 Marié(e) Consulting Assistant de Direction 1 80 8 8 7
4 7 27 M 3468 Marié(e) Consulting Consultant 9 80 6 2 2
In [5]:
evals.head()
Out[5]:
satisfaction_employee_environnement note_evaluation_precedente niveau_hierarchique_poste satisfaction_employee_nature_travail satisfaction_employee_equipe satisfaction_employee_equilibre_pro_perso eval_number note_evaluation_actuelle heure_supplementaires augementation_salaire_precedente
0 2 3 2 4 1 1 E_1 3 Oui 11 %
1 3 2 2 2 4 3 E_2 4 Non 23 %
2 4 2 1 3 2 3 E_4 3 Oui 15 %
3 4 3 1 3 3 3 E_5 3 Oui 11 %
4 1 3 1 2 4 3 E_7 3 Non 12 %
In [6]:
evals['eval_number'].value_counts()
Out[6]:
eval_number
E_1       1
E_2       1
E_4       1
E_5       1
E_7       1
         ..
E_2061    1
E_2062    1
E_2064    1
E_2065    1
E_2068    1
Name: count, Length: 1470, dtype: int64
In [7]:
sond.head()
Out[7]:
a_quitte_l_entreprise nombre_participation_pee nb_formations_suivies nombre_employee_sous_responsabilite code_sondage distance_domicile_travail niveau_education domaine_etude ayant_enfants frequence_deplacement annees_depuis_la_derniere_promotion annes_sous_responsable_actuel
0 Oui 0 0 1 1 1 2 Infra & Cloud Y Occasionnel 0 5
1 Non 1 3 1 2 8 1 Infra & Cloud Y Frequent 1 7
2 Oui 0 3 1 4 2 2 Autre Y Occasionnel 0 0
3 Non 0 3 1 5 3 4 Infra & Cloud Y Frequent 3 0
4 Non 1 3 1 7 2 1 Transformation Digitale Y Occasionnel 2 2
In [8]:
sond["code_sondage"].value_counts()
Out[8]:
code_sondage
1       1
2       1
4       1
5       1
7       1
       ..
2061    1
2062    1
2064    1
2065    1
2068    1
Name: count, Length: 1470, dtype: int64

Section 2 : Nettoyage et Préparation des données

Typage incohérent :¶

  • La variable cible a_quitte_l_entreprise dans sond (str -> int 0/1)
In [9]:
sond['a_quitte_l_entreprise'].value_counts()
Out[9]:
a_quitte_l_entreprise
Non    1233
Oui     237
Name: count, dtype: int64
In [10]:
sond['a_quitte_l_entreprise'] = sond['a_quitte_l_entreprise'].map({
    "Oui": 1,
    "Non": 0
})
In [11]:
sond["a_quitte_l_entreprise"].value_counts(normalize=True)
Out[11]:
a_quitte_l_entreprise
0    0.838776
1    0.161224
Name: proportion, dtype: float64

Si l'employé a quitté l'entreprise alors a_quitte_l_entreprise prend la valeur 1, sinon 0.

  • ayant_enfants dans sond
In [12]:
sond["ayant_enfants"].value_counts()
Out[12]:
ayant_enfants
Y    1470
Name: count, dtype: int64
In [13]:
sond = sond.drop(columns=["ayant_enfants"], errors="ignore")
sond.shape
Out[13]:
(1470, 11)

La variable ayant_enfants présente une variance nulle (100 % des employés ont la même modalité) et ne peut donc pas expliquer les différences de comportement de démission.

  • augementation_salaire_precedente (erreur de saissie dans le nom de la variable + erreur de typage) dans la table evals
In [14]:
evals['augementation_salaire_precedente'].value_counts()
Out[14]:
augementation_salaire_precedente
11 %    210
13 %    209
14 %    201
12 %    198
15 %    101
18 %     89
17 %     82
16 %     78
19 %     76
22 %     56
20 %     55
21 %     48
23 %     28
24 %     21
25 %     18
Name: count, dtype: int64
In [15]:
evals["augmentation_salaire_precedente_pct"] = (
    evals["augementation_salaire_precedente"]
    .str.replace(" %", "", regex=False)
    .astype(int)
)

evals = evals.drop(columns=["augementation_salaire_precedente"])
In [16]:
evals["augmentation_salaire_precedente_pct"].value_counts()
Out[16]:
augmentation_salaire_precedente_pct
11    210
13    209
14    201
12    198
15    101
18     89
17     82
16     78
19     76
22     56
20     55
21     48
23     28
24     21
25     18
Name: count, dtype: int64

La variable a été renommé augmentation_salaire_precedente_pct une fois corrigée et convertie en variable numérique.

  • heure_supplementaires (str -> en binaire 0/1 serait plus pertinent) + rennomer colonne
In [17]:
evals["heure_supplementaires"].value_counts()
Out[17]:
heure_supplementaires
Non    1054
Oui     416
Name: count, dtype: int64
In [18]:
evals["heures_supplementaires"] = evals["heure_supplementaires"].map({
    "Oui": 1,
    "Non": 0
})

Si l'employé effectue des heures supplémentaire alors heures_supplementaires prend la valeur 1, sinon 0.

In [19]:
evals["heures_supplementaires_version_2"] = evals["heure_supplementaires"].apply(
    lambda x: 1 if x == "Oui" else 0
)

evals = evals.drop(columns=["heures_supplementaires_version_2"])
In [20]:
evals = evals.drop(columns=["heure_supplementaires"])

Vérification des identifiants¶

Nous avons remarqué que id_employee est présente dans la table sirh mais ne l'est pas dans evals ni dans sond.
Deux hypothèses :
1- Les lignes sont déjà dans le même ordre pour les 3 tables.
2- Ordre non garanti : il manque une clé explicite.

Nous remarquons d'autre part que eval_number semble être une clé de la table evals.
De même, code_sondage pour sond.

On observe => id_employee : 2068 / eval_number : E_2068 / code_sondage : 2068
Il semblerait qu'un même identifiant employé soit stocké différemment selon les systèmes. Vérifions ainsi la correspondance entre ces 3 clés.

In [21]:
#normalisation des identifiants, création d'une clé commune temporaire

# SIRH
sirh["employee_key"] = sirh["id_employee"].astype(str)

# EVALS
evals["employee_key"] = evals["eval_number"].str.replace("E_", "", regex=False)

# SONDAGE
sond["employee_key"] = sond["code_sondage"].astype(str)
In [22]:
#vérification de l'alignement réel 

#Unicité (il faut 1470 partout -> ok)
sirh["employee_key"].nunique(), evals["employee_key"].nunique(), sond["employee_key"].nunique()

#Correspondance des sets
set(sirh["employee_key"]) == set(evals["employee_key"]) == set(sond["employee_key"])
Out[22]:
True

Une clé employé commune a été reconstruite à partir des identifiants techniques propres à chaque système afin de garantir une jointure fiable entre les sources.

In [23]:
df = (
    sirh
    .merge(evals, on="employee_key", how="inner")
    .merge(sond, on="employee_key", how="inner")
)
In [24]:
df = df.drop(columns=["id_employee", "eval_number", "code_sondage", "employee_key"]) 
In [25]:
df
Out[25]:
age genre revenu_mensuel statut_marital departement poste nombre_experiences_precedentes nombre_heures_travailless annee_experience_totale annees_dans_l_entreprise annees_dans_le_poste_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 augmentation_salaire_precedente_pct heures_supplementaires a_quitte_l_entreprise nombre_participation_pee nb_formations_suivies nombre_employee_sous_responsabilite distance_domicile_travail niveau_education domaine_etude frequence_deplacement annees_depuis_la_derniere_promotion annes_sous_responsable_actuel
0 41 F 5993 Célibataire Commercial Cadre Commercial 8 80 8 6 4 2 3 2 4 1 1 3 11 1 1 0 0 1 1 2 Infra & Cloud Occasionnel 0 5
1 49 M 5130 Marié(e) Consulting Assistant de Direction 1 80 10 10 7 3 2 2 2 4 3 4 23 0 0 1 3 1 8 1 Infra & Cloud Frequent 1 7
2 37 M 2090 Célibataire Consulting Consultant 6 80 7 0 0 4 2 1 3 2 3 3 15 1 1 0 3 1 2 2 Autre Occasionnel 0 0
3 33 F 2909 Marié(e) Consulting Assistant de Direction 1 80 8 8 7 4 3 1 3 3 3 3 11 1 0 0 3 1 3 4 Infra & Cloud Frequent 3 0
4 27 M 3468 Marié(e) Consulting Consultant 9 80 6 2 2 1 3 1 2 4 3 3 12 0 0 1 3 1 2 1 Transformation Digitale Occasionnel 2 2
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
1465 36 M 2571 Marié(e) Consulting Consultant 4 80 17 5 2 3 4 2 4 3 3 3 17 0 0 1 3 1 23 2 Transformation Digitale Frequent 0 3
1466 39 M 9991 Marié(e) Consulting Manager 4 80 9 7 7 4 2 3 1 1 3 3 15 0 0 1 5 1 6 1 Transformation Digitale Occasionnel 1 7
1467 27 M 6142 Marié(e) Consulting Tech Lead 1 80 6 6 2 2 4 2 2 2 3 4 20 1 0 1 0 1 4 3 Infra & Cloud Occasionnel 0 3
1468 49 M 5390 Marié(e) Commercial Cadre Commercial 2 80 17 9 6 4 2 2 2 4 2 3 14 0 0 0 3 1 2 3 Transformation Digitale Frequent 0 8
1469 34 M 4404 Marié(e) Consulting Consultant 2 80 6 4 3 2 4 2 3 1 4 3 12 0 0 0 3 1 8 3 Transformation Digitale Occasionnel 1 2

1470 rows × 30 columns

Afin de garantir la cohérence des analyses et la disponibilité de l’ensemble des variables explicatives et de la cible d’attrition, seules les observations présentes dans l’ensemble des sources de données ont été conservées via une jointure interne.
Nous supprimons ensuite les identifiants qui ne serviront pas à l'analyse et à la modélisation.
Nous obtenons bien, après jointure, 1470 employés décrit par 30 variables.

Vérification de la qualité des variables catégorielles¶

In [26]:
#identification des variables catégorielles
cat_cols = df.select_dtypes(include="str").columns
cat_cols
Out[26]:
Index(['genre', 'statut_marital', 'departement', 'poste', 'domaine_etude',
       'frequence_deplacement'],
      dtype='str')
In [27]:
for col in cat_cols:
    print("\n", col)
    print(df[col].value_counts())
 genre
genre
M    882
F    588
Name: count, dtype: int64

 statut_marital
statut_marital
Marié(e)       673
Célibataire    470
Divorcé(e)     327
Name: count, dtype: int64

 departement
departement
Consulting             961
Commercial             446
Ressources Humaines     63
Name: count, dtype: int64

 poste
poste
Cadre Commercial           326
Assistant de Direction     292
Consultant                 259
Tech Lead                  145
Manager                    131
Senior Manager             102
Représentant Commercial     83
Directeur Technique         80
Ressources Humaines         52
Name: count, dtype: int64

 domaine_etude
domaine_etude
Infra & Cloud              606
Transformation Digitale    464
Marketing                  159
Entrepreunariat            132
Autre                       82
Ressources Humaines         27
Name: count, dtype: int64

 frequence_deplacement
frequence_deplacement
Occasionnel    1043
Frequent        277
Aucun           150
Name: count, dtype: int64

Les variables catégorielles présentent une structure propre et cohérente.
Aucune correction syntaxique ou regroupement n’a été appliqué à ce stade.
Les éventuelles redondances sémantiques entre certaines variables (poste, département, domaine d’étude) seront analysées lors de l’analyse exploratoire et de la phase de modélisation.

Verification de la qualité des variables numériques¶

In [28]:
#identification des variables numériques
num_cols = df.select_dtypes(include="number").columns
num_cols
Out[28]:
Index(['age', 'revenu_mensuel', 'nombre_experiences_precedentes',
       'nombre_heures_travailless', 'annee_experience_totale',
       'annees_dans_l_entreprise', 'annees_dans_le_poste_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',
       'augmentation_salaire_precedente_pct', 'heures_supplementaires',
       'a_quitte_l_entreprise', 'nombre_participation_pee',
       'nb_formations_suivies', 'nombre_employee_sous_responsabilite',
       'distance_domicile_travail', 'niveau_education',
       'annees_depuis_la_derniere_promotion', 'annes_sous_responsable_actuel'],
      dtype='str')
In [29]:
df[num_cols].describe().T
Out[29]:
count mean std min 25% 50% 75% max
age 1470.0 36.923810 9.135373 18.0 30.0 36.0 43.0 60.0
revenu_mensuel 1470.0 6502.931293 4707.956783 1009.0 2911.0 4919.0 8379.0 19999.0
nombre_experiences_precedentes 1470.0 2.693197 2.498009 0.0 1.0 2.0 4.0 9.0
nombre_heures_travailless 1470.0 80.000000 0.000000 80.0 80.0 80.0 80.0 80.0
annee_experience_totale 1470.0 11.279592 7.780782 0.0 6.0 10.0 15.0 40.0
annees_dans_l_entreprise 1470.0 7.008163 6.126525 0.0 3.0 5.0 9.0 40.0
annees_dans_le_poste_actuel 1470.0 4.229252 3.623137 0.0 2.0 3.0 7.0 18.0
satisfaction_employee_environnement 1470.0 2.721769 1.093082 1.0 2.0 3.0 4.0 4.0
note_evaluation_precedente 1470.0 2.729932 0.711561 1.0 2.0 3.0 3.0 4.0
niveau_hierarchique_poste 1470.0 2.063946 1.106940 1.0 1.0 2.0 3.0 5.0
satisfaction_employee_nature_travail 1470.0 2.728571 1.102846 1.0 2.0 3.0 4.0 4.0
satisfaction_employee_equipe 1470.0 2.712245 1.081209 1.0 2.0 3.0 4.0 4.0
satisfaction_employee_equilibre_pro_perso 1470.0 2.761224 0.706476 1.0 2.0 3.0 3.0 4.0
note_evaluation_actuelle 1470.0 3.153741 0.360824 3.0 3.0 3.0 3.0 4.0
augmentation_salaire_precedente_pct 1470.0 15.209524 3.659938 11.0 12.0 14.0 18.0 25.0
heures_supplementaires 1470.0 0.282993 0.450606 0.0 0.0 0.0 1.0 1.0
a_quitte_l_entreprise 1470.0 0.161224 0.367863 0.0 0.0 0.0 0.0 1.0
nombre_participation_pee 1470.0 0.793878 0.852077 0.0 0.0 1.0 1.0 3.0
nb_formations_suivies 1470.0 2.799320 1.289271 0.0 2.0 3.0 3.0 6.0
nombre_employee_sous_responsabilite 1470.0 1.000000 0.000000 1.0 1.0 1.0 1.0 1.0
distance_domicile_travail 1470.0 9.192517 8.106864 1.0 2.0 7.0 14.0 29.0
niveau_education 1470.0 2.912925 1.024165 1.0 2.0 3.0 4.0 5.0
annees_depuis_la_derniere_promotion 1470.0 2.187755 3.222430 0.0 0.0 1.0 3.0 15.0
annes_sous_responsable_actuel 1470.0 4.123129 3.568136 0.0 2.0 3.0 7.0 17.0
In [30]:
df = df.drop(columns=[
    "nombre_heures_travailless",
    "nombre_employee_sous_responsabilite"
])
In [31]:
(df["annees_dans_le_poste_actuel"] > df["annees_dans_l_entreprise"]).sum()

#(df["annees_dans_l_entreprise"] > df["annee_experience_totale"]).sum()
#etc. 

#ok cohérent
Out[31]:
np.int64(0)

Les variables numériques ont été contrôlées selon des règles métier simples.
Aucune incohérence majeure n’a été détectée.
Deux variables constantes ont été supprimées (nombre_employee_sous_responsabilite et nombre_heures_travailless).
Le jeu de données est désormais considéré comme prêt pour l’analyse exploratoire.

Section 3 : Analyse exploratoire (RH - oriented)

Objectif EDA : Identifier les différences clés entre les employés partis et restants.

In [32]:
sns.set_theme(
    style="whitegrid",
    palette=["#6ca57a"],
    context="talk"
)

Photographie globale de l'attrition¶

Objectif : Comprendre la structure générale de la cible avant toute analyse détaillée.

In [33]:
attrition_rate = df["a_quitte_l_entreprise"].mean()
n_total = len(df)
n_depart = df["a_quitte_l_entreprise"].sum()
n_restant = n_total - n_depart

print(f"Le taux d'attrition global : {attrition_rate*100:.2f} %")
print(f"Sur un total de {n_total} employés, {n_depart} ont quitté l’entreprise.")
Le taux d'attrition global : 16.12 %
Sur un total de 1470 employés, 237 ont quitté l’entreprise.
In [34]:
plt.figure(figsize=(8,8))

ax = sns.countplot(
    data=df,
    x="a_quitte_l_entreprise"
)

ax.set_xticklabels(["Restés", "Partis"])

# Ajouter les pourcentages
for i, count in enumerate([n_restant, n_depart]):
    percentage = count / n_total * 100
    ax.text(i, count + 10, f"{percentage:.2f}%", ha="center")

plt.title("Répartition globale des employés\n")
plt.xlabel("")
plt.ylabel("Nombre d'employés")

plt.show()
C:\Users\bouzo\AppData\Local\Temp\ipykernel_6572\3709298785.py:8: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator.
  ax.set_xticklabels(["Restés", "Partis"])
No description has been provided for this image

Environ 16 % des employés ont quitté l’entreprise, ce qui confirme une situation d’attrition significative.
Notons que le jeu de données présente un désequilibre des classes.

Nous allons procéder à une EDA selon plusieurs blocs :

  • Profil de l'employé
  • Parcours académique & développement
  • Expérience & trajectoire interne
  • Satisfaction & engagement des employés
  • Rémunération & performance
  • Organisation & structure
  • Charge & mobilité

Bloc 1 : Profil de l'employé¶

Variables : age, genre, statut_marital
Objectif : Identifier si le profil sociodémographique distingue les employés partis des employés restés.

  • Âge — Les partants sont-ils plus jeunes ou plus âgés ?
In [35]:
df.groupby("a_quitte_l_entreprise")["age"].describe()
Out[35]:
count mean std min 25% 50% 75% max
a_quitte_l_entreprise
0 1233.0 37.561233 8.88836 18.0 31.0 36.0 43.0 60.0
1 237.0 33.607595 9.68935 18.0 28.0 32.0 39.0 58.0
In [36]:
plt.figure(figsize=(6,5))

sns.boxplot(
    data=df,
    x="a_quitte_l_entreprise",
    y="age"
)

plt.xticks([0,1], ["Restés", "Partis"])
plt.title("Distribution de l'âge selon le statut de départ\n")
plt.xlabel("")
plt.ylabel("Âge")

plt.show()
No description has been provided for this image

La différence est visible mais les distributions se chevauchent largement. L'âge ne semble pas être un déterminant seul. De plus, l'âge pourrait être un proxy de salaire/d'ancienneté/de niveau hierarchique.

In [37]:
df["age_groupe"] = df["age"].apply(
    lambda x: "jeune" if x < 39 else "senior"
)
In [38]:
groupe_age_attrition = (
    df.groupby("age_groupe")["a_quitte_l_entreprise"]
    .mean()
    .reset_index()
)

groupe_age_attrition
Out[38]:
age_groupe a_quitte_l_entreprise
0 jeune 0.192053
1 senior 0.111702
In [39]:
df.drop(columns="age_groupe", inplace=True)
  • Genre — Y a-t-il une différence réelle ?
In [40]:
genre_attrition = (
    df.groupby("genre")["a_quitte_l_entreprise"]
    .mean()
    .reset_index()
)

genre_attrition
Out[40]:
genre a_quitte_l_entreprise
0 F 0.147959
1 M 0.170068
In [41]:
plt.figure(figsize=(6,5))

sns.barplot(
    data=genre_attrition,
    x="genre",
    y="a_quitte_l_entreprise"
)

plt.title("Taux d'attrition selon le genre\n")
plt.xlabel("Genre")
plt.ylabel("Taux d'attrition")

plt.show()
No description has been provided for this image

Le taux d’attrition est légèrement plus élevé chez les hommes (17,0 %) que chez les femmes (14,8 %).
Toutefois, l’écart reste modéré et ne suggère pas un déséquilibre majeur lié au genre.

  • Statut marital — Le statut marital influence-t-il l'attrition ?
In [42]:
statut_attrition = (
    df.groupby("statut_marital")["a_quitte_l_entreprise"]
    .mean()
    .sort_values(ascending=False)
    .reset_index()
)

statut_attrition
Out[42]:
statut_marital a_quitte_l_entreprise
0 Célibataire 0.255319
1 Marié(e) 0.124814
2 Divorcé(e) 0.100917
In [43]:
plt.figure(figsize=(7,5))

sns.barplot(
    data=statut_attrition,
    x="a_quitte_l_entreprise",
    y="statut_marital"
)

plt.title("Taux d'attrition selon le statut marital\n")
plt.xlabel("Taux d'attrition")
plt.ylabel("")

plt.show()
No description has been provided for this image

Les employés célibataires présentent un taux d’attrition significativement plus élevé (25,5 %) que les employés mariés (12,5 %) ou divorcés (10,1 %).

Bloc 2 : Parcours académique & développement¶

Variables : niveau_education,domaine_etude,nb_formations_suivies , a_suivi_formation (nouvelle variable)
Objectif : évaluer dans quelle mesure le parcours académique et les dispositifs de développement des compétences constituent des facteurs de rétention, ou au contraire, de vulnérabilité au turnover.

  • Niveau éducation
In [44]:
df["niveau_education"].value_counts(normalize=True).sort_index()
Out[44]:
niveau_education
1    0.115646
2    0.191837
3    0.389116
4    0.270748
5    0.032653
Name: proportion, dtype: float64
In [45]:
attrition_edu = (
    df.groupby("niveau_education")["a_quitte_l_entreprise"]
    .mean()
    .reset_index()
)

attrition_edu
Out[45]:
niveau_education a_quitte_l_entreprise
0 1 0.182353
1 2 0.156028
2 3 0.173077
3 4 0.145729
4 5 0.104167
In [46]:
plt.figure(figsize=(8,5))

sns.barplot(
    data=attrition_edu,
    x="niveau_education",
    y="a_quitte_l_entreprise"
)

plt.title("Taux d'attrition selon le niveau d'éducation")
plt.xlabel("Niveau d'éducation")
plt.ylabel("Taux d'attrition")
plt.show()
No description has been provided for this image

Bien que l’effet soit modéré, le niveau d’éducation semble associé à une plus grande stabilité professionnelle.

  • Domaine d'études
In [47]:
df["domaine_etude"].value_counts(normalize=True)
Out[47]:
domaine_etude
Infra & Cloud              0.412245
Transformation Digitale    0.315646
Marketing                  0.108163
Entrepreunariat            0.089796
Autre                      0.055782
Ressources Humaines        0.018367
Name: proportion, dtype: float64
In [48]:
attrition_domain = (
    df.groupby("domaine_etude")["a_quitte_l_entreprise"]
    .mean()
    .sort_values(ascending=False)
    .reset_index()
)

attrition_domain
Out[48]:
domaine_etude a_quitte_l_entreprise
0 Ressources Humaines 0.259259
1 Entrepreunariat 0.242424
2 Marketing 0.220126
3 Infra & Cloud 0.146865
4 Transformation Digitale 0.135776
5 Autre 0.134146
In [49]:
plt.figure(figsize=(10,6))

sns.barplot(
    data=attrition_domain,
    y="domaine_etude",
    x="a_quitte_l_entreprise"
)

plt.title("Taux d'attrition par domaine d'étude")
plt.xlabel("Taux d'attrition")
plt.ylabel("Domaine d'étude")
plt.show()
No description has been provided for this image
In [50]:
df.groupby("domaine_etude")["a_quitte_l_entreprise"].agg(["count","sum"])
Out[50]:
count sum
domaine_etude
Autre 82 11
Entrepreunariat 132 32
Infra & Cloud 606 89
Marketing 159 35
Ressources Humaines 27 7
Transformation Digitale 464 63

Le taux d’attrition varie selon le domaine d’étude.
Les profils issus du marketing, de l’entrepreneuriat et des ressources humaines présentent des taux d’attrition plus élevés, tandis que les profils techniques (Infra & Cloud, Transformation Digitale) apparaissent plus stables.
Toutefois, certaines catégories étant faiblement représentées (notamment RH), ces résultats doivent être interprétés avec prudence.

  • Montée en compétence/ formation
In [51]:
df.groupby("a_quitte_l_entreprise")["nb_formations_suivies"].describe()
Out[51]:
count mean std min 25% 50% 75% max
a_quitte_l_entreprise
0 1233.0 2.832928 1.293585 0.0 2.0 3.0 3.0 6.0
1 237.0 2.624473 1.254784 0.0 2.0 2.0 3.0 6.0
In [52]:
plt.figure(figsize=(8,5))

sns.boxplot(
    data=df,
    x="a_quitte_l_entreprise",
    y="nb_formations_suivies"
)

plt.title("Nombre de formations suivies selon le statut de départ")
plt.xlabel("Statut de départ")
plt.ylabel("Nombre de formations")
plt.show()
No description has been provided for this image

La variable nb_formations_suivies peu informative. Création d'une nouvelle variable (binaire) :

In [53]:
df["a_suivi_formation"] = (df["nb_formations_suivies"] > 0).astype(int)

df.groupby("a_suivi_formation")["a_quitte_l_entreprise"].mean()
Out[53]:
a_suivi_formation
0    0.277778
1    0.156780
Name: a_quitte_l_entreprise, dtype: float64
In [54]:
plt.figure(figsize=(6,4))

sns.barplot(
    data=df,
    x="a_suivi_formation",
    y="a_quitte_l_entreprise"
)

plt.xticks([0,1], ["Aucune formation", "A suivi formation"])
plt.title("Taux d'attrition selon la participation aux formations")
plt.ylabel("Taux d'attrition")
plt.xlabel("")
plt.show()
No description has been provided for this image

Les employés n’ayant suivi aucune formation présentent un taux d’attrition de 27,8 %, contre 15,7 % pour ceux ayant bénéficié d’au moins une formation.
L’investissement en formation apparaît ainsi comme un levier potentiel de fidélisation.

Bloc 3 : Expérience & trajectoire (externe et interne)¶

Variables : nombre_experiences_precedentes, annee_experience_avant_entreprise (nouvelle variable), annees_dans_l_entreprise, mobilité_interne (nouvelle variable).
Objectif : Comprendre si le parcours professionnel (expérience passée et trajcetoire interne) distingue les employés partis des restants.

  • Feature Engineering
In [55]:
#Création de la variable "annees_experience_avant_entreprise"
df["annee_experience_avant_entreprise"] = (
    df["annee_experience_totale"] - df["annees_dans_l_entreprise"]
)

#Création de la variable "annees_interne_avant_poste_actuel"
df["annees_interne_avant_poste_actuel"] = (
     df["annees_dans_l_entreprise"] - df["annees_dans_le_poste_actuel"]
)

#Création de la variable "mobilite_interne" 
# 0 = jamais changé de poste ; 1 = a déjà évolué en interne
df["mobilite_interne"] = (df["annees_interne_avant_poste_actuel"] > 0).astype(int)
In [56]:
df
Out[56]:
age genre revenu_mensuel statut_marital departement poste nombre_experiences_precedentes annee_experience_totale annees_dans_l_entreprise annees_dans_le_poste_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 augmentation_salaire_precedente_pct heures_supplementaires a_quitte_l_entreprise nombre_participation_pee nb_formations_suivies distance_domicile_travail niveau_education domaine_etude frequence_deplacement annees_depuis_la_derniere_promotion annes_sous_responsable_actuel a_suivi_formation annee_experience_avant_entreprise annees_interne_avant_poste_actuel mobilite_interne
0 41 F 5993 Célibataire Commercial Cadre Commercial 8 8 6 4 2 3 2 4 1 1 3 11 1 1 0 0 1 2 Infra & Cloud Occasionnel 0 5 0 2 2 1
1 49 M 5130 Marié(e) Consulting Assistant de Direction 1 10 10 7 3 2 2 2 4 3 4 23 0 0 1 3 8 1 Infra & Cloud Frequent 1 7 1 0 3 1
2 37 M 2090 Célibataire Consulting Consultant 6 7 0 0 4 2 1 3 2 3 3 15 1 1 0 3 2 2 Autre Occasionnel 0 0 1 7 0 0
3 33 F 2909 Marié(e) Consulting Assistant de Direction 1 8 8 7 4 3 1 3 3 3 3 11 1 0 0 3 3 4 Infra & Cloud Frequent 3 0 1 0 1 1
4 27 M 3468 Marié(e) Consulting Consultant 9 6 2 2 1 3 1 2 4 3 3 12 0 0 1 3 2 1 Transformation Digitale Occasionnel 2 2 1 4 0 0
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
1465 36 M 2571 Marié(e) Consulting Consultant 4 17 5 2 3 4 2 4 3 3 3 17 0 0 1 3 23 2 Transformation Digitale Frequent 0 3 1 12 3 1
1466 39 M 9991 Marié(e) Consulting Manager 4 9 7 7 4 2 3 1 1 3 3 15 0 0 1 5 6 1 Transformation Digitale Occasionnel 1 7 1 2 0 0
1467 27 M 6142 Marié(e) Consulting Tech Lead 1 6 6 2 2 4 2 2 2 3 4 20 1 0 1 0 4 3 Infra & Cloud Occasionnel 0 3 0 0 4 1
1468 49 M 5390 Marié(e) Commercial Cadre Commercial 2 17 9 6 4 2 2 2 4 2 3 14 0 0 0 3 2 3 Transformation Digitale Frequent 0 8 1 8 3 1
1469 34 M 4404 Marié(e) Consulting Consultant 2 6 4 3 2 4 2 3 1 4 3 12 0 0 0 3 8 3 Transformation Digitale Occasionnel 1 2 1 2 1 1

1470 rows × 32 columns

  • Expérience externe (avant l'entreprise)
In [57]:
df.groupby("a_quitte_l_entreprise")["nombre_experiences_precedentes"].describe()
Out[57]:
count mean std min 25% 50% 75% max
a_quitte_l_entreprise
0 1233.0 2.645580 2.460090 0.0 1.0 2.0 4.0 9.0
1 237.0 2.940928 2.678519 0.0 1.0 1.0 5.0 9.0
In [58]:
plt.figure(figsize=(6,5))

sns.boxplot(
    data=df,
    x="a_quitte_l_entreprise",
    y="nombre_experiences_precedentes"
)

plt.xticks([0,1], ["Restés", "Partis"])
plt.title("Nombre d'expériences précédentes selon le statut de départ")
plt.xlabel("")
plt.ylabel("Nombre d'expériences")

plt.show()
No description has been provided for this image

La distribution est globalement similaire entre les deux groupes.

In [59]:
df.groupby("a_quitte_l_entreprise")["annee_experience_avant_entreprise"].describe()
Out[59]:
count mean std min 25% 50% 75% max
a_quitte_l_entreprise
0 1233.0 4.493917 6.414283 0.0 0.0 2.0 6.0 33.0
1 237.0 3.113924 4.618462 0.0 0.0 1.0 4.0 22.0
In [60]:
plt.figure(figsize=(6,5))

sns.boxplot(
    data=df,
    x="a_quitte_l_entreprise",
    y="annee_experience_avant_entreprise"
)

plt.xticks([0,1], ["Restés", "Partis"])
plt.title("Expérience avant l'entreprise selon le statut de départ")
plt.xlabel("")
plt.ylabel("Années")

plt.show()
No description has been provided for this image
  • Expérience interne
In [61]:
df.groupby("a_quitte_l_entreprise")["annees_dans_l_entreprise"].describe()
Out[61]:
count mean std min 25% 50% 75% max
a_quitte_l_entreprise
0 1233.0 7.369019 6.096298 0.0 3.0 6.0 10.0 37.0
1 237.0 5.130802 5.949984 0.0 1.0 3.0 7.0 40.0
In [62]:
sns.violinplot(
    data=df,
    x="a_quitte_l_entreprise",
    y="annees_dans_l_entreprise",
    inner="quartile"
)

plt.xticks([0, 1], ["Restés", "Partis"])
plt.title("Ancienneté dans l'entreprise selon le statut de départ")
plt.xlabel("")
plt.ylabel("Années dans l'entreprise")

plt.show()
No description has been provided for this image

Le risque de départ est fortement concentré dans les premières années.

In [63]:
df.groupby("mobilite_interne")["a_quitte_l_entreprise"].mean()
Out[63]:
mobilite_interne
0    0.227848
1    0.148418
Name: a_quitte_l_entreprise, dtype: float64
In [64]:
plt.figure(figsize=(6,5))

sns.barplot(
    data=df,
    x="mobilite_interne",
    y="a_quitte_l_entreprise"
)

plt.xticks([0,1], ["Pas de mobilité", "Mobilité interne"])
plt.title("Taux d'attrition selon la mobilité interne")
plt.xlabel("")
plt.ylabel("Taux d'attrition")

plt.show()
No description has been provided for this image

Les employés n’ayant jamais changé de poste présentent un taux d’attrition significativement plus élevé. La mobilité interne semble associée à une meilleure rétention des collaborateurs.

Bloc 4 : Satisfaction & engagement des employés¶

Variables : satisfaction_employee_environnement,satisfaction_employee_nature_travail,satisfaction_employee_equipe,satifaction_employee_equilibre_pro_perso
Objectif : Identifier si le ressenti des employés est associé au départ.
En effet, les variables de ce bloc reposent sur des évaluations déclaratives. Elles traduisent le ressenti des employés et peuvent refléter leur niveau d’engagement perçu.

In [65]:
satisfaction_cols = [
    "satisfaction_employee_environnement",
    "satisfaction_employee_nature_travail",
    "satisfaction_employee_equipe",
    "satisfaction_employee_equilibre_pro_perso"
]

# Moyennes par groupe
sat = df.groupby("a_quitte_l_entreprise")[satisfaction_cols].mean().T
sat.columns = ["Restés", "Partis"]

# Écarts
sat["Écart (Partis - Restés)"] = sat["Partis"] - sat["Restés"]
sat["% Écart"] = sat["Écart (Partis - Restés)"] / sat["Restés"] * 100

# Labels plus courts 
rename_map = {
    "satisfaction_employee_environnement": "Environnement",
    "satisfaction_employee_nature_travail": "Nature du travail",
    "satisfaction_employee_equipe": "Équipe",
    "satisfaction_employee_equilibre_pro_perso": "Équilibre pro/perso",
}
sat = sat.rename(index=rename_map)

sat = sat.round(3)
sat
Out[65]:
Restés Partis Écart (Partis - Restés) % Écart
Environnement 2.771 2.464 -0.307 -11.083
Nature du travail 2.779 2.468 -0.310 -11.165
Équipe 2.734 2.599 -0.135 -4.931
Équilibre pro/perso 2.781 2.658 -0.123 -4.415

Les employés ayant quitté l’entreprise présentent des niveaux de satisfaction systématiquement plus faibles sur l’ensemble des dimensions analysées.

In [66]:
# Trier par écart (du plus négatif au moins négatif)
plot_df = sat[["Écart (Partis - Restés)"]].sort_values("Écart (Partis - Restés)")

plt.figure(figsize=(16, 6))
ax = sns.barplot(
    x="Écart (Partis - Restés)",
    y=plot_df.index,
    data=plot_df.reset_index().rename(columns={"index": "Dimension"}),
    orient="h"
)

ax.axvline(0, linewidth=1)  # ligne de référence
ax.set_title("Écart de satisfaction (Partis - Restés) par dimension")
ax.set_xlabel("Écart moyen (points sur échelle 1–4)")
ax.set_ylabel("")

# Ajouter les valeurs sur les barres
for container in ax.containers:
    ax.bar_label(container, fmt="%.2f", padding=3)

plt.tight_layout()
plt.show()
No description has been provided for this image
In [67]:
cohen_d = {}

for col in satisfaction_cols:
    group1 = df[df["a_quitte_l_entreprise"] == 0][col]  # Restés
    group2 = df[df["a_quitte_l_entreprise"] == 1][col]  # Partis
    
    mean_diff = group2.mean() - group1.mean()
    
    pooled_std = np.sqrt(
        ((group1.std() ** 2) + (group2.std() ** 2)) / 2
    )
    
    cohen_d[col] = mean_diff / pooled_std

cohen_d
Out[67]:
{'satisfaction_employee_environnement': np.float64(-0.27386678816210647),
 'satisfaction_employee_nature_travail': np.float64(-0.2805679349258372),
 'satisfaction_employee_equipe': np.float64(-0.12269740817644381),
 'satisfaction_employee_equilibre_pro_perso': np.float64(-0.1632478303798571)}

Les écarts les plus marqués concernent :

  • La nature du travail (écart moyen = -0.31 ; Cohen’s d ≈ -0.28)

  • L’environnement de travail (écart moyen = -0.31 ; Cohen’s d ≈ -0.27)

Ces effets, bien que modérés, suggèrent que le contenu du travail et le cadre professionnel constituent des facteurs différenciants dans les départs.

À l’inverse, les dimensions liées à l’équipe et à l’équilibre vie pro-perso présentent des écarts plus faibles (Cohen’s d < 0.2), indiquant un pouvoir explicatif plus limité.

Ces résultats suggèrent que l’attrition semble davantage associée à l’expérience professionnelle vécue qu’aux relations humaines ou à l’équilibre personnel.

Bloc 5 : Performance, rémunération & évolution¶

Variables : note_evaluation_actuelle, evolution_note (nouvelle variable), variation_note_cat (nouvelle variable juste pour EDA), revenu_mensuel, augmentation_salaire_precedente_pct , nombre_participation_pee, utilisation_pee (nouvelle variable juste pour EDA), annees_depuis_la_dernière_promotion
Objectif : Evaluer si la reconnaissance, la progression et la rémunération sont associées aux départs.

  • Performance
In [68]:
df.groupby("a_quitte_l_entreprise")["note_evaluation_actuelle"].describe()
Out[68]:
count mean std min 25% 50% 75% max
a_quitte_l_entreprise
0 1233.0 3.153285 0.360408 3.0 3.0 3.0 3.0 4.0
1 237.0 3.156118 0.363735 3.0 3.0 3.0 3.0 4.0

L’analyse de la note d’évaluation actuelle ne révèle aucune différence significative entre les employés restés et ceux ayant quitté l’entreprise (moyenne ≈ 3.15 dans les deux groupes).

La performance actuelle ne semble donc pas constituer un facteur explicatif direct des départs.

In [69]:
df["evolution_note"] = (
    df["note_evaluation_actuelle"] -
    df["note_evaluation_precedente"]
)
In [70]:
df.groupby("a_quitte_l_entreprise")["evolution_note"].describe()
Out[70]:
count mean std min 25% 50% 75% max
a_quitte_l_entreprise
0 1233.0 0.382806 0.789190 -1.0 0.0 0.0 1.0 3.0
1 237.0 0.637131 0.865519 -1.0 0.0 0.0 1.0 3.0

En revanche, l’évolution de la note entre deux évaluations apporte un éclairage intéressant. Les employés ayant quitté l’entreprise présentent en moyenne une progression plus importante de leur note (+0.64 contre +0.38).

In [71]:
df["variation_note_cat"] = pd.cut(
    df["evolution_note"],
    bins=[-10, -0.1, 0.1, 10],
    labels=["Baisse", "Stable", "Hausse"]
)
In [72]:
pd.crosstab(
    df["variation_note_cat"],
    df["a_quitte_l_entreprise"],
    normalize="index"
)
Out[72]:
a_quitte_l_entreprise 0 1
variation_note_cat
Baisse 0.903226 0.096774
Stable 0.858653 0.141347
Hausse 0.799660 0.200340
In [73]:
attrition_by_variation = (
    df.groupby("variation_note_cat")["a_quitte_l_entreprise"]
    .mean()
    .reset_index()
)

sns.barplot(
    data=attrition_by_variation,
    x="variation_note_cat",
    y="a_quitte_l_entreprise"
)

plt.title("Taux d'attrition selon l'évolution de la note")
plt.ylabel("Taux d'attrition")
plt.xlabel("Variation de la note")
plt.show()
No description has been provided for this image

L’analyse catégorielle confirme ce phénomène : le taux d’attrition atteint environ 20 % chez les employés dont la note augmente, contre moins de 10 % chez ceux dont la note diminue.

Ces résultats suggèrent que les départs pourraient concerner des profils en progression, potentiellement plus attractifs sur le marché du travail. L’attrition ne semble donc pas uniquement liée à une sous-performance, mais pourrait également toucher des employés performants.

  • Rémunération
In [74]:
df.groupby("a_quitte_l_entreprise")["revenu_mensuel"].median()
Out[74]:
a_quitte_l_entreprise
0    5204.0
1    3202.0
Name: revenu_mensuel, dtype: float64
In [75]:
sns.boxplot(
    data=df,
    x="a_quitte_l_entreprise",
    y="revenu_mensuel"
)
Out[75]:
<Axes: xlabel='a_quitte_l_entreprise', ylabel='revenu_mensuel'>
No description has been provided for this image

L’analyse du revenu mensuel met en évidence une différence marquée entre les employés restés et ceux ayant quitté l’entreprise.

La médiane du revenu mensuel s’élève à 5 204 € chez les employés restés contre 3 202 € chez les employés partis, traduisant un écart significatif de niveau de rémunération.

La distribution confirme que les départs concernent majoritairement des salariés situés dans les niveaux de rémunération les plus faibles.

In [76]:
df.groupby("a_quitte_l_entreprise")[
    "augmentation_salaire_precedente_pct"
].mean()
Out[76]:
a_quitte_l_entreprise
0    15.231144
1    15.097046
Name: augmentation_salaire_precedente_pct, dtype: float64
In [77]:
sns.boxplot(
    data=df,
    x="a_quitte_l_entreprise",
    y="augmentation_salaire_precedente_pct"
)
Out[77]:
<Axes: xlabel='a_quitte_l_entreprise', ylabel='augmentation_salaire_precedente_pct'>
No description has been provided for this image

L’augmentation salariale précédente ne présente pas de différence notable entre les deux groupes (≈ 15 % dans les deux cas). Cela suggère que ce n’est pas l’évolution récente du salaire qui influence le départ, mais plutôt le niveau de rémunération global.

  • Evolution & engagement
In [78]:
df.groupby("a_quitte_l_entreprise")["annees_depuis_la_derniere_promotion"].describe()
Out[78]:
count mean std min 25% 50% 75% max
a_quitte_l_entreprise
0 1233.0 2.234388 3.234762 0.0 0.0 1.0 3.0 15.0
1 237.0 1.945148 3.153077 0.0 0.0 1.0 2.0 15.0
In [79]:
sns.boxplot(
    data=df,
    x="a_quitte_l_entreprise",
    y="annees_depuis_la_derniere_promotion"
)

plt.title("Ancienneté depuis la dernière promotion selon le statut de départ")
plt.xlabel("")
plt.ylabel("Années depuis la dernière promotion")
plt.show()
No description has been provided for this image

L’analyse des années écoulées depuis la dernière promotion ne révèle pas de différence notable entre les employés restés et ceux ayant quitté l’entreprise.
La médiane est identique (1 an) et les distributions apparaissent similaires, suggérant que la stagnation promotionnelle ne constitue pas un facteur déterminant dans les départs.

In [80]:
df.groupby("a_quitte_l_entreprise")[
    "nombre_participation_pee"
].mean()
Out[80]:
a_quitte_l_entreprise
0    0.845093
1    0.527426
Name: nombre_participation_pee, dtype: float64
In [81]:
df["utilisation_pee"] = (
    df["nombre_participation_pee"] > 0
).astype(int)
In [82]:
df.groupby("utilisation_pee")[
    "a_quitte_l_entreprise"
].mean()
Out[82]:
utilisation_pee
0    0.244057
1    0.098927
Name: a_quitte_l_entreprise, dtype: float64
In [83]:
attrition_pee = (
    df.groupby("utilisation_pee")["a_quitte_l_entreprise"]
    .mean()
    .reset_index()
)

sns.barplot(
    data=attrition_pee,
    x="utilisation_pee",
    y="a_quitte_l_entreprise"
)

plt.xticks([0,1], ["Pas de PEE", "Utilise le PEE"])
plt.ylabel("Taux d'attrition")
plt.title("Taux d'attrition selon l'utilisation du PEE")
plt.show()
No description has been provided for this image

En revanche, l’utilisation du Plan d’Épargne Entreprise (PEE) présente un effet marqué. Le taux d’attrition atteint environ 24 % chez les employés ne participant pas au PEE, contre moins de 10 % chez ceux qui y contribuent.

Ces résultats indiquent qu’un engagement financier à long terme au sein de l’entreprise semble fortement associé à une plus grande rétention.

Bloc 6 : Organisation & Structure du poste¶

Variables : department, poste, annees_dans_le_poste_actuel, niveau_hierarchique_poste, annees_sous_responsable_actuel
Objectif : Identifier si la position hiérarchique, le département ou encore la stabilité managériale influencent l’attrition.

  • Département
In [84]:
attrition_dept = (
    df.groupby("departement")["a_quitte_l_entreprise"]
    .mean()
    .sort_values(ascending=False)
    .reset_index()
)

attrition_dept
Out[84]:
departement a_quitte_l_entreprise
0 Commercial 0.206278
1 Ressources Humaines 0.190476
2 Consulting 0.138398
In [85]:
sns.barplot(
    data=attrition_dept,
    x="a_quitte_l_entreprise",
    y="departement"
)

plt.title("Taux d'attrition par département")
plt.xlabel("Taux d'attrition")
plt.ylabel("")
plt.xlim(0, attrition_dept["a_quitte_l_entreprise"].max() * 1.1)

plt.show()
No description has been provided for this image
In [86]:
df["departement"].value_counts(normalize=True)
Out[86]:
departement
Consulting             0.653741
Commercial             0.303401
Ressources Humaines    0.042857
Name: proportion, dtype: float64

Le département Commercial semble particulièrement exposé à l’attrition, possiblement en raison des spécificités du métier (pression commerciale, mobilité externe, objectifs de performance).

Le département Consulting apparaît quant à lui plus stable.

Le département Ressources Humaines représente une part limitée de l’effectif total (≈ 4 %), ce qui peut rendre son taux d’attrition plus sensible aux variations individuelles.

  • Poste
In [87]:
attrition_poste = (
    df.groupby("poste")["a_quitte_l_entreprise"]
    .mean()
    .sort_values(ascending=False)
    .reset_index()
)


plt.figure(figsize=(12, 6))

sns.barplot(
    data=attrition_poste,
    x="a_quitte_l_entreprise",
    y="poste"
)

plt.title("Taux d'attrition par poste")
plt.xlabel("Taux d'attrition")
plt.ylabel("")

plt.show()
No description has been provided for this image

L’analyse par poste met en évidence des disparités importantes d’attrition. Les fonctions commerciales, en particulier le poste de Représentant Commercial, présentent les taux de départ les plus élevés.

In [88]:
df.groupby("niveau_hierarchique_poste")[
    "a_quitte_l_entreprise"
].mean()
Out[88]:
niveau_hierarchique_poste
1    0.263352
2    0.097378
3    0.146789
4    0.047170
5    0.072464
Name: a_quitte_l_entreprise, dtype: float64
In [89]:
sns.barplot(
    data=df,
    x="niveau_hierarchique_poste",
    y="a_quitte_l_entreprise",
    estimator="mean",
    errorbar=None
)

plt.title("Taux d'attrition par niveau hiérarchique")
plt.ylabel("Taux d'attrition")
plt.xlabel("Niveau hiérarchique")
plt.show()
No description has been provided for this image

Ces résultats suggèrent que les positions les moins élevées dans la hiérarchie sont les plus exposées au risque de départ.

In [90]:
plt.figure(figsize=(8, 6))

sns.violinplot(
    data=df,
    x="a_quitte_l_entreprise",
    y="annees_dans_le_poste_actuel",
    inner="quartile"
)

plt.xticks([0, 1], ["Restés", "Partis"])
plt.title("Ancienneté dans le poste actuel")
plt.xlabel("")
plt.ylabel("Années")

plt.show()
No description has been provided for this image

Les départs sont plus fréquents chez les employés récemment positionnés dans leur poste.

  • Stabilité managériale
In [91]:
sns.boxplot(
    data=df,
    x="a_quitte_l_entreprise",
    y="annes_sous_responsable_actuel"
)
Out[91]:
<Axes: xlabel='a_quitte_l_entreprise', ylabel='annes_sous_responsable_actuel'>
No description has been provided for this image

Les employés partis ont généralement une relation managériale plus récente.
Ces résultats suggèrent que les périodes de transition – prise de poste ou changement de manager – peuvent constituer des phases de vulnérabilité en matière de rétention.

Bloc 7 : Conditions de travail & contraintes professionnelles¶

Variabales : heures_supplementaires, distance_domicile_travail, distance_cat (nouvelle variable), frequence_deplacement
Objectif : Évaluer si les contraintes opérationnelles (charge, mobilité, déplacements) influencent l’attrition.

  • Charge de travail
In [92]:
df.groupby("heures_supplementaires")[
    "a_quitte_l_entreprise"
].mean()
Out[92]:
heures_supplementaires
0    0.104364
1    0.305288
Name: a_quitte_l_entreprise, dtype: float64
In [93]:
attrition_heures = (
    df.groupby("heures_supplementaires")["a_quitte_l_entreprise"]
    .mean()
    .reset_index()
)

sns.barplot(
    data=attrition_heures,
    x="heures_supplementaires",
    y="a_quitte_l_entreprise"
)

plt.xticks([0,1], ["Pas d'heures supp", "Heures supp"])
plt.ylabel("Taux d'attrition")
plt.title("Taux d'attrition selon les heures supplémentaires")
plt.show()
No description has been provided for this image

L’analyse des heures supplémentaires révèle un écart marqué. Le taux d’attrition atteint 30,5 % chez les employés déclarant effectuer des heures supplémentaires, contre seulement 10,4 % chez ceux n’en effectuant pas.

Cette différence significative suggère que la charge de travail constitue un facteur majeur d’attrition. Les contraintes opérationnelles et le surcroît d’activité pourraient contribuer à un désengagement progressif ou à une recherche d’alternatives professionnelles.

In [94]:
df["heures_supplementaires"].value_counts(normalize=True)
Out[94]:
heures_supplementaires
0    0.717007
1    0.282993
Name: proportion, dtype: float64

28 % des employés font des heures supplémentaires (= presque 1 employé sur 3 est concerné)
Et leur taux d’attrition est 3 fois plus élevé

  • Mobilité
In [95]:
df.groupby("a_quitte_l_entreprise")[
    "distance_domicile_travail"
].describe()
Out[95]:
count mean std min 25% 50% 75% max
a_quitte_l_entreprise
0 1233.0 8.915653 8.012633 1.0 2.0 7.0 13.0 29.0
1 237.0 10.632911 8.452525 1.0 3.0 9.0 17.0 29.0
In [96]:
plt.figure(figsize=(8, 6))

sns.boxplot(
    data=df,
    x="a_quitte_l_entreprise",
    y="distance_domicile_travail"
)

plt.xticks([0, 1], ["Restés", "Partis"])
plt.title("Distance domicile-travail selon le statut de départ")
plt.xlabel("")
plt.ylabel("Distance")

plt.show()
No description has been provided for this image
In [97]:
plt.figure(figsize=(10, 6))

sns.histplot(
    data=df,
    x="distance_domicile_travail",
    hue="a_quitte_l_entreprise",
    element="step",
    stat="density",
    common_norm=False
)

plt.title("Distribution de la distance domicile-travail")
plt.xlabel("Distance")
plt.ylabel("Densité")

plt.show()
No description has been provided for this image

Les employés ayant quitté l’entreprise habitent en moyenne plus loin (10,6 vs 8,9). La médiane est également plus élevée (9 vs 7), suggérant un effet réel bien que modéré. La distance domicile-travail semble donc être un facteur contributif, mais non déterminant à elle seule.

In [98]:
df["distance_cat"] = pd.cut(
    df["distance_domicile_travail"],
    bins=[0,5,10,20,30],
    labels=["Proche","Moyen","Loin","Très loin"]
)
In [99]:
df.groupby("distance_cat")["a_quitte_l_entreprise"].mean()
Out[99]:
distance_cat
Proche       0.137658
Moyen        0.144670
Loin         0.200000
Très loin    0.220588
Name: a_quitte_l_entreprise, dtype: float64
In [100]:
sns.barplot(
    data=df,
    x="distance_cat",
    y="a_quitte_l_entreprise",
    estimator="mean",
    errorbar=None
)

plt.title("Taux d'attrition par catégorie de distance")
plt.ylabel("Taux d'attrition")
plt.xlabel("Distance_cat")
plt.show()
No description has been provided for this image

Le taux d’attrition augmente progressivement avec la distance domicile-travail.
La distance apparaît donc comme un facteur de risque croissant, avec un effet particulièrement marqué au-delà de 10 km.

In [101]:
attrition_deplacement = (
    df.groupby("frequence_deplacement")["a_quitte_l_entreprise"]
    .mean()
    .sort_values(ascending=False)
    .reset_index()
)
In [102]:
attrition_deplacement = (
    df.groupby("frequence_deplacement")["a_quitte_l_entreprise"]
    .mean()
    .sort_values(ascending=False)
    .reset_index()
)

plt.figure(figsize=(8, 5))

sns.barplot(
    data= attrition_deplacement,
    x="a_quitte_l_entreprise",
    y="frequence_deplacement"
)

plt.title("Taux d'attrition selon la fréquence des déplacements")
plt.xlabel("Taux d'attrition")
plt.ylabel("")

plt.show()
No description has been provided for this image

Le taux d’attrition augmente fortement avec la fréquence des déplacements professionnels. Les employés effectuant des déplacements fréquents présentent un taux d’attrition proche de 25 %, contre seulement 8 % pour ceux n’effectuant aucun déplacement. La mobilité professionnelle apparaît comme un facteur de risque majeur d’attrition.

Bilan EDA

L'attrition semble principalement liée à :

  • Un manque d'investissement en formation
  • Une absence de mobilité interne
  • Une charge de travail élevée (heures supplémentaires, fréquence des déplacements)
  • Une rémunération moins attractive
  • Un positionnement hiérarchique peu élevé et une ancienneté faible dans le poste
  • Une progression de performance non accompagnée
  • Une faible inscription dans les dispositifs d'engagement long terme (ex: PEE)

Les leviers prioritaires seraient :

  • Formation : renforcer l’accès à la formation + rendre les parcours de montée en compétences visibles.
  • Mobilité interne : formaliser des trajectoires, ouvrir des passerelles, et définir des jalons d’évolution plus rapides.
  • Charge de travail : encadrer les heures supplémentaires (pilotage, staffing, alerte managériale, prévention surcharge) ou encore les fréquence des déplacements.
  • Rémunération : améliorer la compétitivité et l’équité (revue des salaires, correction des écarts, ajustements ciblés sur populations à risque / métiers en tension).
  • Début de parcours : renforcer onboarding + points de suivi 30/60/90 jours + accompagnement managérial des nouveaux.
  • Profils performants : mettre en place un dispositif “rétention talents” (reconnaissance, missions, évolution, package).
  • Engagement long terme : augmenter l’adhésion aux dispositifs (PEE) via pédagogie, incitations, et communication RH.

Les constats précédents reposent sur une analyse descriptive des données. Ils mettent en évidence des tendances et des associations statistiques, sans permettre d’établir de relations causales.
Ces éléments constituent donc des signaux exploratoires et des hypothèses de travail.

Afin d’aller au-delà de cette lecture descriptive, une approche de modélisation prédictive sera mise en place. Un modèle de classification sera entraîné afin d’estimer, pour chaque employé, la probabilité de démission.

Cette démarche permettra de hiérarchiser les variables selon leur contribution réelle au risque d’attrition et de distinguer les facteurs structurants des simples corrélations observées.
L’interprétation des contributions des variables sera réalisée à l’aide de méthodes d’explicabilité du modèle, afin de garantir une lecture transparente et exploitable des résultats.

Exportation des données pour modélisation

  • Suppression des variables redondantes :
In [103]:
df
Out[103]:
age genre revenu_mensuel statut_marital departement poste nombre_experiences_precedentes annee_experience_totale annees_dans_l_entreprise annees_dans_le_poste_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 augmentation_salaire_precedente_pct heures_supplementaires a_quitte_l_entreprise nombre_participation_pee nb_formations_suivies distance_domicile_travail niveau_education domaine_etude frequence_deplacement annees_depuis_la_derniere_promotion annes_sous_responsable_actuel a_suivi_formation annee_experience_avant_entreprise annees_interne_avant_poste_actuel mobilite_interne evolution_note variation_note_cat utilisation_pee distance_cat
0 41 F 5993 Célibataire Commercial Cadre Commercial 8 8 6 4 2 3 2 4 1 1 3 11 1 1 0 0 1 2 Infra & Cloud Occasionnel 0 5 0 2 2 1 0 Stable 0 Proche
1 49 M 5130 Marié(e) Consulting Assistant de Direction 1 10 10 7 3 2 2 2 4 3 4 23 0 0 1 3 8 1 Infra & Cloud Frequent 1 7 1 0 3 1 2 Hausse 1 Moyen
2 37 M 2090 Célibataire Consulting Consultant 6 7 0 0 4 2 1 3 2 3 3 15 1 1 0 3 2 2 Autre Occasionnel 0 0 1 7 0 0 1 Hausse 0 Proche
3 33 F 2909 Marié(e) Consulting Assistant de Direction 1 8 8 7 4 3 1 3 3 3 3 11 1 0 0 3 3 4 Infra & Cloud Frequent 3 0 1 0 1 1 0 Stable 0 Proche
4 27 M 3468 Marié(e) Consulting Consultant 9 6 2 2 1 3 1 2 4 3 3 12 0 0 1 3 2 1 Transformation Digitale Occasionnel 2 2 1 4 0 0 0 Stable 1 Proche
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
1465 36 M 2571 Marié(e) Consulting Consultant 4 17 5 2 3 4 2 4 3 3 3 17 0 0 1 3 23 2 Transformation Digitale Frequent 0 3 1 12 3 1 -1 Baisse 1 Très loin
1466 39 M 9991 Marié(e) Consulting Manager 4 9 7 7 4 2 3 1 1 3 3 15 0 0 1 5 6 1 Transformation Digitale Occasionnel 1 7 1 2 0 0 1 Hausse 1 Moyen
1467 27 M 6142 Marié(e) Consulting Tech Lead 1 6 6 2 2 4 2 2 2 3 4 20 1 0 1 0 4 3 Infra & Cloud Occasionnel 0 3 0 0 4 1 0 Stable 1 Proche
1468 49 M 5390 Marié(e) Commercial Cadre Commercial 2 17 9 6 4 2 2 2 4 2 3 14 0 0 0 3 2 3 Transformation Digitale Frequent 0 8 1 8 3 1 1 Hausse 0 Proche
1469 34 M 4404 Marié(e) Consulting Consultant 2 6 4 3 2 4 2 3 1 4 3 12 0 0 0 3 8 3 Transformation Digitale Occasionnel 1 2 1 2 1 1 -1 Baisse 0 Moyen

1470 rows × 36 columns

In [104]:
df_model = df.copy()
In [105]:
cols_to_drop = [
    "nb_formations_suivies", #on a a_suivi_formation
    "annee_experience_totale", #annee_experience_avant_entreprise + annee_dans_l_entreprise
    "annees_interne_avant_poste_actuel", #utilisé uniquement pour créer mobilite_interne
    "note_evaluation_precedente", #on a evolution_note + note_evolution_actuelle
    "nombre_participation_pee", #on a utilisation_pee
    "variation_note_cat", #on a evolution_note
    "distance_cat" #on a distance_domicile_travail 
]

df_model = df_model.drop(columns=cols_to_drop)
In [106]:
df_model
Out[106]:
age genre revenu_mensuel statut_marital departement poste nombre_experiences_precedentes annees_dans_l_entreprise annees_dans_le_poste_actuel satisfaction_employee_environnement niveau_hierarchique_poste satisfaction_employee_nature_travail satisfaction_employee_equipe satisfaction_employee_equilibre_pro_perso note_evaluation_actuelle augmentation_salaire_precedente_pct heures_supplementaires a_quitte_l_entreprise distance_domicile_travail niveau_education domaine_etude frequence_deplacement annees_depuis_la_derniere_promotion annes_sous_responsable_actuel a_suivi_formation annee_experience_avant_entreprise mobilite_interne evolution_note utilisation_pee
0 41 F 5993 Célibataire Commercial Cadre Commercial 8 6 4 2 2 4 1 1 3 11 1 1 1 2 Infra & Cloud Occasionnel 0 5 0 2 1 0 0
1 49 M 5130 Marié(e) Consulting Assistant de Direction 1 10 7 3 2 2 4 3 4 23 0 0 8 1 Infra & Cloud Frequent 1 7 1 0 1 2 1
2 37 M 2090 Célibataire Consulting Consultant 6 0 0 4 1 3 2 3 3 15 1 1 2 2 Autre Occasionnel 0 0 1 7 0 1 0
3 33 F 2909 Marié(e) Consulting Assistant de Direction 1 8 7 4 1 3 3 3 3 11 1 0 3 4 Infra & Cloud Frequent 3 0 1 0 1 0 0
4 27 M 3468 Marié(e) Consulting Consultant 9 2 2 1 1 2 4 3 3 12 0 0 2 1 Transformation Digitale Occasionnel 2 2 1 4 0 0 1
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
1465 36 M 2571 Marié(e) Consulting Consultant 4 5 2 3 2 4 3 3 3 17 0 0 23 2 Transformation Digitale Frequent 0 3 1 12 1 -1 1
1466 39 M 9991 Marié(e) Consulting Manager 4 7 7 4 3 1 1 3 3 15 0 0 6 1 Transformation Digitale Occasionnel 1 7 1 2 0 1 1
1467 27 M 6142 Marié(e) Consulting Tech Lead 1 6 2 2 2 2 2 3 4 20 1 0 4 3 Infra & Cloud Occasionnel 0 3 0 0 1 0 1
1468 49 M 5390 Marié(e) Commercial Cadre Commercial 2 9 6 4 2 2 4 2 3 14 0 0 2 3 Transformation Digitale Frequent 0 8 1 8 1 1 0
1469 34 M 4404 Marié(e) Consulting Consultant 2 4 3 2 2 3 1 4 3 12 0 0 8 3 Transformation Digitale Occasionnel 1 2 1 2 1 -1 0

1470 rows × 29 columns

  • Exportation des données :
In [107]:
df_model.to_csv("../data/processed/df_model_attrition.csv", index=False)