Jointures et agrégation de données
Python
Pierre Pudlo
2025-09-01
Table des matières
1 Jointures et agrégation de données avec Python 2
1.1 Objectifs de la séance . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
2 Introduction aux jointures avec pandas 2
2.1 Types de jointures . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
2.2 Syntaxe de base de merge() . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
3 Chargement et préparation des données 3
3.1 Import des tables principales . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
3.2 Chargement des données de temps dû . . . . . . . . . . . . . . . . . . . . . . 4
4 Jointures de base entre tables 5
4.1 Jointure contrats-personnes-emplois . . . . . . . . . . . . . . . . . . . . . . . 5
4.2 Jointure avec types d’absences . . . . . . . . . . . . . . . . . . . . . . . . . . 6
5 Jointures et agrégations complexes 8
5.1 Jointure absences et temps dû . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
5.2 Enrichissement de la table des plages horaires de temps dû . . . . . . . . . . 9
5.3 Préparation à l’agrégation pour analyse . . . . . . . . . . . . . . . . . . . . . 10
5.4 Agrégations finales pour l’analyse . . . . . . . . . . . . . . . . . . . . . . . . . 11
5.5 Un exemple de résultat . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
6 Exercice à rendre 12
7 Annexe 13
7.1 Concepts à maîtriser . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
7.2 Fonctions pandas essentielles à apprendre . . . . . . . . . . . . . . . . . . . . 13
7.3 Techniques d’analyse avancées . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
1
1 Jointures et agrégation de données avec Python
Ce document présente les techniques avancées de jointure et d’agrégation de données avec
pandas pour analyser les données RH hospitalières, en se concentrant sur l’analyse des
absences et du temps de travail.
La période d’étude s’étend du 1er septembre 2024 au 31 août 2025.
Cette session vous donne un exemple concret d’analyse efficace des données RH complexes
et de création de rapports insights pour la prise de décision managériale.
1.1 Objectifs de la séance
À la fin de cette séance, vous serez capable de :
1. Maîtriser les jointures avec pandas
2. Combiner efficacement plusieurs tables de données
3. Effectuer des agrégations complexes avec groupby
4. Créer des premiers rapports statistiques complets
2 Introduction aux jointures avec pandas
2.1 Types de jointures
pandas offre plusieurs types de jointures via la fonction merge() :
• inner: Garde uniquement les lignes présentes dans les deux tables
• left: Garde toutes les lignes de la table de gauche
• right: Garde toutes les lignes de la table de droite
• outer: Garde toutes les lignes des deux tables
Lorsqu’une ligne n’a pas de correspondance, pandas remplit les valeurs manquantes avec
NaN.
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
2.2 Syntaxe de base de merge()
Sous forme de fonction, la syntaxe de [Link]() est la suivante :
2
# Syntaxe générale
resultat = [Link](
df_gauche,
df_droite,
on='colonne_commune', # Colonne de jointure commune
how='inner', # Type de jointure
left_on='col_gauche', # Si noms différents
right_on='col_droite', # Si noms différents
suffixes=('_x', '_y') # Suffixes pour colonnes en conflit
)
Sous forme de méthode, on peut aussi écrire :
# Syntaxe méthode
resultat = df_gauche.merge(
df_droite,
on='colonne_commune',
how='inner',
left_on='col_gauche',
right_on='col_droite',
suffixes=('_x', '_y')
)
3 Chargement et préparation des données
3.1 Import des tables principales
Importer les tables personnes, contrats, emplois, absences, absences_types et
conges_rtt et convertir les dates au format datetime.
path="../data/"
# Charger les tables de base
personnes = pd.read_csv(f'{path}/[Link]')
contrats = pd.read_csv(f'{path}/[Link]')
emplois = pd.read_csv(f'{path}/[Link]')
absences = pd.read_csv(f'{path}/[Link]')
absences_types = pd.read_csv(f'{path}/absences_types.csv')
conges_rtt = pd.read_csv(f'{path}/conges_rtt.csv')
# Conversion des dates
contrats['debut'] = pd.to_datetime(contrats['debut'])
contrats['fin'] = pd.to_datetime(contrats['fin'])
absences['debut'] = pd.to_datetime(absences['debut'])
3
absences['fin'] = pd.to_datetime(absences['fin'])
conges_rtt['debut'] = pd.to_datetime(conges_rtt['debut'])
conges_rtt['fin'] = pd.to_datetime(conges_rtt['fin'])
print("Tables chargées :")
print(f"- Personnes: {len(personnes)} lignes")
print(f"- Contrats: {len(contrats)} lignes")
print(f"- Emplois: {len(emplois)} lignes")
print(f"- Absences: {len(absences)} lignes")
print(f"- Types d'absences: {len(absences_types)} lignes")
print(f"- Congés/RTT: {len(conges_rtt)} lignes")
3.2 Chargement des données de temps dû
Compléter la fonction ci-dessous qui charge un ou plusieurs fichiers de temps dû mensuels
et les combine en un seul DataFrame.
• En l’absence de données obtenues, retourner un DataFrame vide.
• Convertir les colonnes start et end en format datetime. Elles ont été enregistrées au
format ISO8601.
• Utiliser une structure de type try/except pour gérer les erreurs de chargement.
• Concaténer les DataFrames chargés avec [Link]().
import glob
import os
def charger_temps_du_mensuel(annee=None, mois=None, path="../data/temps_du_plages"):
"""
Charge les fichiers de temps dû mensuels.
Args:
annee: Année spécifique (None = toutes)
mois: Mois spécifique (None = tous)
path: Chemin des fichiers de temps dû (par défaut "../data/temps_du_plages")
Returns:
DataFrame: Données de temps dû consolidées
"""
# Construire le pattern de fichier
if annee and mois:
pattern = f"{path}/temps_du_{annee}_{mois:02d}.csv"
elif annee:
pattern = f"{path}/temps_du_{annee}_*.csv"
else:
4
pattern = f"{path}/temps_du_*.csv"
fichiers = [Link](pattern)
if not fichiers:
print(f"Aucun fichier trouvé avec le pattern: {pattern}")
return [Link]()
print(f"Chargement de {len(fichiers)} fichiers de temps dû...")
dataframes = []
for fichier in sorted(fichiers):
try:
# Compléter ici pour remplir la liste dataframes
except Exception as e:
print(f" KO Erreur avec {fichier}: {e}")
if dataframes:
temps_du_complet = [Link](dataframes, ignore_index=True)
print(f"Total: {len(temps_du_complet)} lignes de temps dû")
return temps_du_complet
else:
return [Link]()
# Charger quelques mois pour les exemples
temps_du = charger_temps_du_mensuel(2024, 10) # Octobre 2024 seulement
4 Jointures de base entre tables
4.1 Jointure contrats-personnes-emplois
À l’aide de la fonction [Link](), créer une table enrichie des contrats avec les informations
des personnes et des emplois.
• Les colonnes à garder sont : 'id', 'debut', 'fin', 'type', 'part_temps',
'nom', 'prénom', 'date de naissance', 'genre', 'libellé', 'catégorie',
'rythme', 'id_pers', 'id_emploi'
• Ajouter age, l’âge de la personne (en année, arrondi à l’unité) et anciennete_jours,
anciennete_annees l’ancienneté du contrat (en jours sans arrondi pour le premier ;
en années, arrondi à l’unité près pour le second). Ces quantités doivent être calculées
à la fin de la période d’étude, c’est-à-dire au 31 août 2025.
5
def creer_table_contrats_enrichie():
"""
Crée une table enrichie des contrats avec infos personnes et emplois.
Returns:
DataFrame: Contrats avec toutes les informations contextuelles
"""
# Jointure contrats -> personnes et emplois
# Compléter ici pour créer la table contrats_enrichis
# Nettoyer les colonnes en doublon
colonnes_a_garder = [
'id', 'debut', 'fin', 'type', 'part_temps',
'nom', 'prénom', 'date de naissance', 'genre',
'libellé', 'catégorie', 'rythme',
'id_pers', 'id_emploi'
]
contrats_enrichis = contrats_enrichis[colonnes_a_garder]
# Calculer l'âge et l'ancienneté
contrats_enrichis['age'] = 0 # Corriger ici
contrats_enrichis['anciennete_jours'] = 0 # Corriger ici
contrats_enrichis['anciennete_annees'] = 0 # Corriger ici
return contrats_enrichis
# Créer la table enrichie
contrats_enrichis = creer_table_contrats_enrichie()
print("Table contrats enrichie créée:")
print(f"Dimensions: {contrats_enrichis.shape}")
print("\nPremières lignes:")
print(contrats_enrichis[['nom', 'prénom', 'libellé', 'catégorie', 'type', 'age']].head())
4.2 Jointure avec types d’absences
Créer maintenant une fonction qui enrichit la table des absences avec les informations de
absences_types et les informations de contrat précédemment enrichies.
• Les colonnes à garder sont : 'id', 'id_contrat', 'debut', 'fin', 'jours_ouvres',
'code', 'libellé', 'libellé_contrat', 'famille', 'type', 'type_contrat',
'nom', 'prénom', 'catégorie', 'age'
6
• Renommer libellé en libellé_absence et type en type_absence
En utilisant les méthodes groupby, size, sort_values(ascending=False) et head, afficher
les 5 types d’absences les plus fréquents dans les lignes de votre tableau enrichi.
def enrichir_absences():
"""
Enrichit les absences avec les types et informations contrats.
Returns:
DataFrame: Absences avec descriptions complètes
"""
# Jointure absences -> types d'absences
absences_typees = [Link]() # Corriger ici
# Jointure avec la vue contrats enrichie
absences_enrichies = [Link]() # Corriger ici
# Sélectionner les colonnes utiles
colonnes_finales = [
'id', 'id_contrat', 'debut', 'fin', 'jours_ouvres',
'code', 'libellé', 'libellé_contrat', 'famille', 'type', 'type_contrat',
'nom', 'prénom', 'catégorie', 'age'
]
# Renommer pour clarifier
absences_enrichies = absences_enrichies[colonnes_finales].rename(columns={
'libellé': 'libellé_absence',
'type': 'type_absence'
})
return absences_enrichies
absences_enrichies = enrichir_absences()
print("Absences enrichies:")
print(f"Dimensions: {absences_enrichies.shape}")
print("\nExemple d'absences par type:")
print(absences_enrichies.groupby('libellé_absence').size().sort_values(ascending=False).he
7
5 Jointures et agrégations complexes
L’objectif est maintenant d’étudier le taux d’absence, c’est-à-dire
∑ durée de travail dû pendant absence
.
∑ durée de travail dû
Il faut donc donc faire le lien entre les tables de temps dû et la table des absences. Une
plage de temps dû est considérée comme absente si
(1) les identifiants de contrats correspondent et
(2) les périodes de temps se chevauchent.
La condition 1 revient à faire une jointure entre les tables de temps dû et d’absences sur
l’identifiant de contrat. Et la condition 2 est un filtre sur cette jointure.
ĺ Attention
Dans toute cette partie, si l’on s’y prend mal, on risque d’avoir des temps de calcul
très longs. Il est donc recommandé de procéder par étape.
• Ne charger qu’une seule table mensuelle de plages de temps dû dans votre Data-
Frame temps_du pendant que vous concevez et déboguez les différentes fonctions.
• Une fois toutes les fonctions conçues et vérifiées, vous pourrez charger l’ensemble
des tables mensuelles et réexécuter les calculs.
def ajouter_identifiant_unique(temps_du):
"""
Ajoute une colonne d'identifiant unique à la table des temps dû.
Args:
temps_du (DataFrame): Table des temps dû.
Returns:
DataFrame: Table des temps dû avec identifiant unique.
"""
temps_du['id_temps_du'] = 0 # Corriger ici
return temps_du
temps_du = ajouter_identifiant_unique(temps_du)
5.1 Jointure absences et temps dû
L’objectif ici est de créer une table de temps dû pendant les absences. On ne garde une ligne
que si elle est présente dans les deux tables. Ce qui revient à faire une jointure interne.
8
Écrire une fonction joindre_temps_absences qui, à partir des tables de temps dû et la
table absences non enrichies, effectue la jointure et le filtre.
def joindre_temps_absences(temps_du, absences):
"""
Jointure entre les temps dû et les absences.
Args:
temps_du (DataFrame): Table des temps dû.
absences (DataFrame): Table des absences.
Returns:
DataFrame: Table des temps dû pendant les absences.
"""
# Jointure temps_du et absences sur l'identifiant de contrat
jointure = [Link]() # Corriger ici
# Filtrer les périodes de temps se chevauchant
temps_absences = jointure[] # Corriger ici
return temps_absences
temps_absences = joindre_temps_absences(temps_du, absences)
5.2 Enrichissement de la table des plages horaires de temps dû
Écrire une fonction qui ajoute une colonne absence à la table temps_du. Cette colonne vaut
1 s’il s’agit d’une absence, 0 sinon.
def enrichir_temps_du(temps_du, temps_absences):
"""
Enrichit la table des temps dû avec une colonne d'absence.
Args:
temps_du (DataFrame): Table des temps dû.
temps_absences (DataFrame): Table des temps dû pendant les absences.
Returns:
DataFrame: Table des temps dû enrichie.
"""
temps_du['absence'] = [Link](len(temps_du)) # Corriger ici
return temps_du
temps_du = enrichir_temps_du(temps_du, temps_absences)
9
5.3 Préparation à l’agrégation pour analyse
Il reste à agréger cette table enrichies des plages de temps dû pour pouvoir procéder à
l’analyse. On veut pouvoir utiliser différents filtres dans l’analyse :
• la période (mensuelle, trimestrielle, annuelle)
• la famille et le type d’absence
• le type de contrat (apprenti, cdd, cdi)
• la catégorie de l’emploi (Médical, Encadrement, Laboratoire, Pharmacie, etc.)
• la tranche d’âge (moins de 20 ans, 20-30 ans, 30-40 ans, 40-50 ans, 50-60 ans, 60 ans
et plus)
N.B. L’âge est calculé à la date de fin de la période d’étude, c’est-à-dire au 31 août 2025.
Les variables d’agrégation sont donc : mois, famille, type_absence, type_contrat,
categorie, tranche_age.
Écrire une fonction qui, éventuellement à l’aide de jointure gauche, enrichit la table des
plages de temps dû avec les informations utiles et ajoute les colonnes nécessaires.
def continuer_enrichir_temps_du(temps_du, absences_enrichies, contrats_enrichis):
"""
Enrichit la table des temps dû avec des informations supplémentaires pour l'analyse.
Args:
temps_du (DataFrame): Table des temps dû avec colonne absence.
absences_enrichies (DataFrame): Table des absences enrichies.
contrats_enrichis (DataFrame): Table des contrats enrichis.
Returns:
DataFrame: Table des temps dû enrichie avec toutes les variables d'analyse.
"""
# Jointure gauche temps_du avec contrats pour récupérer type_contrat, catégorie et age
temps_du_enrichi = [Link]() # Corriger ici
# Ajouter la colonne mois
temps_du_enrichi['mois'] = [Link](len(temps_du_enrichi), dtype=int) # Corriger ici
# Ajouter la tranche d'âge
def calculer_tranche_age(age):
# Compléter et corriger ici
return 'Non défini'
temps_du_enrichi['tranche_age'] = temps_du_enrichi['age'].apply(calculer_tranche_age)
# Jointure avec absences enrichies pour récupérer famille et type_absence
temps_du_avec_absences = [Link]() # Corriger ici
10
# Filtrer pour ne garder famille et type_absence que si chevauchement temporel
return temps_du_avec_absences
temps_du = continuer_enrichir_temps_du(temps_du, absences_enrichies, contrats_enrichis)
5.4 Agrégations finales pour l’analyse
Il faut maintenant réaliser trois étapes :
• Agréger les plages de temps dû pour obtenir la somme des durées de ces plages par
valeurs des variables de groupby. Cela donne une table des dénominateur du taux
d’absentéisme.
• Réaliser la même agrégation, mais sur la table filtrée des plages avec absence. Cela
donne une table des numérateurs du taux d’absentéisme.
• Joindre ces deux tables pour avoir les numérateurs et les dénominateurs. S’assurer
qu’il n’y a pas de valeurs manquantes.
N.B. On ne calcule pas le rapport à ce stade car, si l’on regroupe deux groupes, par exemple
les catégories Encadrement et Direction, pour former un nouveau groupe, on doit som-
mer numérateurs et dénominateurs correspondant pour avoir le taux d’absentéisme de ce
nouveau groupe.
# Agrégation des dénominateurs (total temps dû)
variables_groupby = ['mois', 'famille', 'type_absence', 'type_contrat', 'catégorie', 'tran
# Remplacer les NaN par 'Aucune' pour les colonnes liées aux absences
temps_du_clean = temps_du.fillna({
'famille': 'Aucune',
'type_absence': 'Aucune'
})
denominateurs = 0 # Corriger ici
# Agrégation des numérateurs (temps dû pendant absences)
numerateurs = 0 # Corriger ici
# Jointure pour obtenir numérateurs et dénominateurs
taux_absenteisme_data = [Link]() # Corriger ici
print(f"Table d'analyse créée avec {len(taux_absenteisme_data)} lignes")
print("Premières lignes:")
print(taux_absenteisme_data.head())
11
5.5 Un exemple de résultat
Écrire une fonction qui, à partir de la dernière table agrégée produite calcule les taux
d’absentéisme par mois et par famille d’absence. On mettra les mois en ligne et les familles
d’absence en colonnes.
def calculer_taux_absenteisme_mois_famille(taux_absenteisme_data):
"""
Calcule les taux d'absentéisme par mois et famille d'absence.
Args:
taux_absenteisme_data (DataFrame): Données agrégées avec numérateurs et dénominate
Returns:
DataFrame: Tableau croisé dynamique avec taux d'absentéisme (%).
"""
# Agréger par mois et famille d'absence (sommer sur les autres dimensions)
resume_mois_famille = [Link]() # Corriger ici
# Calculer le taux d'absentéisme en pourcentage
resume_mois_famille['taux_absenteisme'] = [Link](len(resume_mois_famille)) # Corrig
# Créer un tableau croisé dynamique
tableau_croise = [Link]() # Corriger ici
return tableau_croise
# Exemple d'utilisation
exemple_taux = calculer_taux_absenteisme_mois_famille(taux_absenteisme_data)
print("Taux d'absentéisme par mois et famille d'absence (%):")
print(exemple_taux.round(2))
6 Exercice à rendre
Faire une analyse des périodes de vacances et de RTT des employés de l’hôpital Santé Bonne
Mère. L’objectif est de savoir à quel moments ces périodes se concentrent typiquement pour
éventuellement prévoir le recours à des interims. Les principales questions auxquelles ont
souhaiterait répondre par cette analyse sont donc :
• Sur quelles périodes de l’année se concentrent ces périodes de vacances et de RTT ?
• Y a-t-il des différences notables entre les différents types de contrats (apprenti, cdd,
cdi) ou les différentes catégories d’emploi ?
Il est inutile d’utiliser les plages de temps dû ici.
12
L’objectif du code est de créer une table préparant cette analyse.
Livrables
• le code commenté de création de la table à partir des fichiers CSV bruts
• une description courte des différentes étapes pour obtenir la table d’intérêt
• un exemple d’utilisation de cette table préparée pour commencer à répondre aux
questions d’intérêt
Éléments d’évaluation
• Décomposition en étapes claires et logiques
• Justesse du code proposé
• Utilisation de fonctions pour encapsuler les différentes étapes de création de la table
• Commentaires du code (ce qu’il faut, mais juste ce qu’il faut)
7 Annexe
7.1 Concepts à maîtriser
1. Jointures pandas : merge() avec différents types (inner, left, right, outer)
2. Agrégations complexes : groupby avec fonctions multiples et métriques dérivées
3. Analyse temporelle : Évolution des indicateurs dans le temps
4. Jointures multiples : Combinaison de plusieurs tables pour analyses croisées
5. Rapports de synthèse : Création de tableaux de bord automatisés
7.2 Fonctions pandas essentielles à apprendre
• [Link]() : Jointures entre DataFrames
• groupby().agg() : Agrégations avec fonctions multiples
• fillna() : Remplissage des valeurs manquantes
• pivot() : Reformatage de données tabulaires
• [Link]() : Concaténation de DataFrames
• .dt : Accesseur pour opérations sur dates
7.3 Techniques d’analyse avancées
• Jointures en cascade : Chaînage de plusieurs merge()
• Métriques dérivées : Calcul d’indicateurs à partir d’agrégations
• Analyse comparative : Confrontation de différentes sources
• Gestion des valeurs manquantes : fillna() dans les jointures
• Performance : Optimisation des requêtes sur gros volumes
13