Modéliser l'équité en planification du personnel avec CP-SAT
Contraintes d'équité, équilibrage min-max et poids configurables
Pourquoi l'équité compte
L'équité en planification est une question de perception et de mathématiques. Quand les employés sentent que le planning est injuste, cela érode la confiance, augmente le turnover et génère des réclamations qui consomment du temps de management. Le problème est que la planification manuelle crée des biais systématiques, non par intention, mais parce que les planificateurs humains tendent à reproduire des schémas familiers. L'employé qui est "bon le matin" a toujours les matinées. Celui qui ne se plaint jamais reçoit les pires shifts.
Ces biais se cumulent sur des mois. À la fin d'un trimestre, certains employés ont travaillé 10% de jours de plus que leurs collègues du même groupe. D'autres ont été affectés exclusivement à une seule fonction malgré trois qualifications. Personne ne l'a planifié ainsi. C'est simplement arrivé parce qu'aucun humain ne peut suivre les totaux cumulatifs de 70 employés sur 30 jours tout en assurant simultanément la couverture, les repos et la rotation des weekends.
Un solveur CP-SAT le peut. L'équité devient un objectif mathématique : minimiser l'écart entre l'employé qui travaille le plus et celui qui travaille le moins. Le solveur suit chaque affectation, chaque fonction, chaque groupe, simultanément. L'équité n'est pas une réflexion après coup. Elle est intégrée dans la fonction objectif.
Équité de charge : équilibrer les jours travaillés
La première dimension de l'équité est simple : au sein de chaque groupe fonctionnel, chaque employé devrait travailler approximativement le même nombre de jours par mois. Si un groupe compte 7 employés et que l'opération nécessite 4 personnes par jour pendant 23 jours ouvrables, cela fait 92 personnes-jours. Divise par 7, chaque employé devrait travailler environ 13 jours.
En pratique, le solveur ne peut pas toujours atteindre une répartition parfaite. Mais il peut minimiser l'écart entre le maximum et le minimum. La pénalité pour l'inéquité de charge est définie par groupe :
# Pour chaque groupe g :
gap = max_jours_travailles[g] - min_jours_travailles[g]
penalty += gap * WEIGHT_WORKLOAD_EQUITY
Le poids de 50 par unité d'écart signifie que chaque jour supplémentaire de déséquilibre coûte 50 points. Un écart de 2 coûte 100. Un écart de 0 ne coûte rien. Le solveur pousse naturellement vers le plus petit écart réalisable.
C'est une contrainte souple, pas dure. Si le solveur doit choisir entre laisser un shift non couvert (pénalité 10 000) ou créer un écart de charge de 1 jour (pénalité 50), il choisira toujours l'écart. La couverture gagne. Mais quand la couverture est réalisable, le solveur distribue le travail aussi équitablement que possible.
Équité par qualification : équilibrer entre fonctions
L'équité de charge seule ne suffit pas. Prenons un employé qualifié pour la sécurité (AVSEC) et la logistique (CARGO). Si le solveur l'affecte exclusivement à des shifts sécurité pendant tout le mois, il travaille le même nombre de jours que ses collègues, mais il ne touche jamais à la logistique. Pendant ce temps, un autre employé reçoit tous les shifts logistique.
Cela crée deux problèmes. D'abord, c'est perçu comme injuste : certaines fonctions sont considérées comme plus ou moins souhaitables. Ensuite, cela crée une perte de compétence : les employés qui ne pratiquent jamais une qualification perdent en maîtrise. L'équité par qualification répond aux deux.
# Pour chaque groupe g, pour chaque fonction f :
gap = max_shifts_dans_f[g] - min_shifts_dans_f[g]
penalty += gap * WEIGHT_QUALIF_EQUITY
Le poids de 500 par unité est 10 fois supérieur à l'équité de charge. Cela reflète un choix de conception délibéré : à charge totale égale, le solveur devrait fortement préférer une distribution équilibrée entre les fonctions. Un employé qui travaille 14 jours répartis 7/7 entre deux fonctions est un meilleur résultat qu'un qui travaille 14 jours entièrement dans une seule fonction, même si le total de jours est identique.
L'approche min-max dans CP-SAT
Les deux objectifs d'équité utilisent le même patron de modélisation : minimiser la différence entre les valeurs maximale et minimale au sein d'un groupe. Dans CP-SAT, cela s'exprime avec des variables entières et des contraintes d'inégalité.
# Creer des IntVar pour le max et le min dans un groupe
max_days = model.NewIntVar(0, 30, 'max_days_g')
min_days = model.NewIntVar(0, 30, 'min_days_g')
# Contraindre max >= le compte de chaque employe, min <= le compte de chaque employe
for e in group:
model.Add(max_days >= worked_days[e])
model.Add(min_days <= worked_days[e])
# Definir l'ecart
gap = model.NewIntVar(0, 30, 'gap_g')
model.Add(gap == max_days - min_days)
# Ajouter a l'objectif
model.Minimize(gap * PENALTY_WEIGHT)
Le solveur assigne des valeurs à max_days et min_days qui satisfont toutes les contraintes d'inégalité. Comme on minimise l'écart, le solveur est incité à pousser max_days vers le bas et min_days vers le haut, ce qui revient à distribuer le travail aussi uniformément que possible dans le groupe.
Ce patron est répété pour chaque groupe (équité de charge) et pour chaque paire groupe-fonction (équité par qualification). Pour une opération avec 8 groupes et 6 fonctions, cela crée 8 écarts de charge et jusqu'à 48 écarts par qualification, chacun contribuant indépendamment à la pénalité totale.
Pourquoi min-max et pas l'écart-type ?
L'écart-type est une mesure naturelle de dispersion, mais il est quadratique et ne se décompose pas proprement dans un modèle CP linéaire. L'écart min-max est linéaire, facile à exprimer en contraintes CP-SAT, et directement interprétable : un écart de 2 signifie que l'employé le plus charge a travaillé exactement 2 jours de plus que le moins charge. Pas d'abstraction statistique nécessaire.
Exclure les absences du calcul d'équité
Un employé en vacances pendant 10 jours sur un mois de 30 jours peut travailler au maximum 20 jours. Si le reste de son groupe travaille 22-23 jours, l'écart est d'au moins 2-3 jours, et le solveur ne peut pas le combler. Inclure cet employé dans le calcul d'équité pénalise le solveur pour quelque chose qu'il ne contrôle pas.
La solution est directe : les employés avec des contraintes HOLIDAY ou VACATION sont exclus du calcul d'équité pour le groupe concerné. Le solveur ne mesure l'écart que parmi les employés pleinement disponibles (ou au moins également contraints).
# Construire le groupe d'equite en excluant les absents
equity_group = [e for e in group if not has_long_absence(e)]
# Calculer le min-max uniquement sur equity_group
for e in equity_group:
model.Add(max_days >= worked_days[e])
model.Add(min_days <= worked_days[e])
De même, les employés de type "étudiant" sont exclus des calculs d'équité weekend. Les étudiants ont des schémas de disponibilité différents et des obligations contractuelles différentes. Les inclure dans la garantie de weekend pénaliserait le solveur injustement ou forcerait le solveur à donner aux étudiants des weekends dont ils n'ont pas besoin, au détriment des employés à temps plein qui en ont besoin.
L'équité parfaite est impossible
Prenons un exemple concret. Un groupe de 7 employés, 23 jours ouvrables dans le mois, besoin journalier de 4 employés. Cela fait 92 personnes-jours pour 7 personnes. 92 / 7 = 13,14 jours chacun. Comme on ne peut pas travailler une fraction de jour, la meilleure répartition possible est :
- 4 employés travaillent 13 jours
- 3 employés travaillent 14 jours
Total : (4 × 13) + (3 × 14) = 52 + 42 = 94. En fait, ce total de 94 ne correspond pas exactement à 92. La répartition réelle dépend des besoins de couverture exacts et des autres contraintes (repos, weekends, qualifications). Le point important est qu'un écart de 0 est mathématiquement impossible quand le total ne se divise pas exactement. Un écart de 1 est le minimum théorique.
Le solveur trouve cela automatiquement. Il n'a pas besoin qu'on lui dise qu'un écart de 1 est acceptable. Il minimise simplement l'écart, atteint le plancher mathématique, et passe à l'amélioration des autres objectifs. C'est l'un des atouts d'une approche par pénalités : le solveur découvre l'optimum réalisable sans qu'on lui donne une cible explicite.
Quand l'écart est plus grand que prévu
Si le solveur produit un écart de 3 ou plus dans un groupe, cela signifie que d'autres contraintes interfèrent. Causes fréquentes :
- Trop peu d'employés qualifiés pour une fonction spécifique, forçant les mêmes personnes à la couvrir de manière répétée
- Les contraintes de repos (règle des 11 heures) créant des effets en chaîne qui limitent la flexibilité de planification
- Des absences fixes (FIXED_OFF) réparties de manière inégale dans le groupe
La décomposition des pénalités rend ces causes visibles. Une pénalité d'équité élevée est un signal de diagnostic, pas un échec du solveur.
Poids configurables
Les poids par défaut (50 pour l'équité de charge, 500 pour l'équité par qualification) reflètent une priorité équilibrée où l'équité compte mais la couverture gagne toujours. Ces poids sont configurables dans les paramètres du solveur.
| Objectif | Poids par défaut | Effet d'une augmentation |
|---|---|---|
| Équité de charge (jours) | Très faible | Équilibrage plus strict des jours travaillés par groupe |
| Équité par qualification (shifts/fonction) | Faible | Équilibrage plus strict des types de shifts entre fonctions |
| Couverture manquante | Très élevé | Préférence encore plus forte pour la couverture complète |
| Jours de repos manquants | Moyen | Garantie plus forte des jours de repos contractuels |
Augmenter le poids de l'équité de charge de 50 à 500 fait que le solveur traite un écart de 1 jour aussi sérieusement qu'un jour de repos manquant. Cela produit des plannings plus équilibrés mais peut dégrader la couverture dans les situations tendues. Le solveur peut laisser un shift non couvert plutôt que de créer un déséquilibre de charge.
L'insight clé est que le réglage des poids est un compromis, pas une amélioration gratuite. Des poids d'équité plus élevés ne créent pas plus d'employés. Ils redistribuent la même force de travail plus uniformément, potentiellement au détriment d'autres objectifs. Un planificateur qui met l'équité à 10 000 (égal à la couverture) dit au solveur : "je préfère un shift non couvert à un planning inéquitable." C'est peut-être le bon choix pour certaines opérations, mais cela doit être un choix délibéré.
Mesurer l'équité
Après l'exécution du solveur, la décomposition des pénalités montre l'écart d'équité par groupe. C'est la métrique principale d'équité. Voici ce que différentes valeurs d'écart signifient en pratique :
| Écart (jours) | Évaluation | Cause typique |
|---|---|---|
| 0 | Équité parfaite | La taille du groupe divise exactement le total de personnes-jours |
| 1 | Excellent | Arrondi normal (résultat le plus fréquent) |
| 2 | Bon | Interférence mineure de contraintes (repos, weekends) |
| 3 | Acceptable | Goulot d'étranglement de qualification ou absences inégales |
| 4+ | À investiguer | Déséquilibre structurel : trop peu d'employés qualifiés, ou contraintes dures en conflit |
Pour l'équité par qualification, la même logique s'applique par fonction. Un écart de 0-1 shifts par fonction est excellent. Un écart de 3+ dans une fonction spécifique suggère que trop peu d'employés dans le groupe détiennent cette qualification, forçant le solveur à sur-affecter ceux qui la possèdent.
Équité entre les mois
Le solveur optimise un mois à la fois. Il ne reporte pas les données d'équité des mois précédents. Si un employé a travaillé plus en janvier, le solveur de février ne le sait pas. L'équité inter-mensuelle nécessite soit un ajustement manuel des données d'entrée (par exemple ajuster les cibles en fonction du mois précédent), soit une couche méta qui suit l'équité cumulative. C'est une décision opérationnelle, pas une limitation du solveur.
Rapporter l'équité aux planificateurs
Le rapport du solveur inclut des statistiques d'équité par groupe : le min, le max et l'écart pour les métriques de charge et de qualification. Un planificateur peut parcourir le rapport et identifier immédiatement quels groupes sont bien équilibrés et lesquels nécessitent une attention. Si un groupe affiche systématiquement un écart de 4+, la solution est généralement structurelle : ajouter un employé avec la qualification manquante, ou diviser le groupe en unités plus petites et plus homogènes.
Voir les métriques d'équité sur vos données ?
Envoyez-nous votre liste d'employés et votre matrice de qualifications. Nous lancerons le solveur et vous montrerons la décomposition d'équité par groupe.
Nous contacter