Des données qui concernent des personnes
Salaires, absences, évaluations, départs : l’analytics RH travaille sur les données les plus personnelles de l’entreprise. Une fuite ou une mauvaise diffusion peut nuire directement à un salarié, et la confiance des équipes dans le service RH en dépend. Ce cours commence donc par la protection des données, avant tout calcul.
Les données RH sont des données personnelles au sens du RGPD. Quelques principes établis guident toute analyse :
- finalité : on sait pourquoi on traite les données (piloter le turnover, mesurer l’égalité salariale) et on ne les réutilise pas pour autre chose ;
- minimisation : on ne garde que les colonnes nécessaires à cette finalité ;
- accès limité : seules les personnes habilitées voient les données individuelles ;
- conservation limitée et sécurité : fichiers protégés, pas de copie sur une clé USB ni dans un e-mail.
Certaines informations relèvent des catégories particulières de données (notamment la santé ou l’appartenance syndicale) et bénéficient d’une protection renforcée. Un motif d’absence médical en fait partie : on l’analyse uniquement agrégé, si c’est nécessaire, et on en parle avec le délégué à la protection des données (DPO) de l’entreprise.
Minimiser et pseudonymiser
Pour analyser des salaires par service, vous n’avez besoin ni du nom ni de l’adresse e-mail. Première étape : les supprimer. Deuxième étape : remplacer le matricule par un pseudonyme, pour pouvoir relier plusieurs fichiers sans exposer l’identifiant réel.
Un hachage simple (sha256 du matricule) ne suffit pas : les matricules sont peu nombreux et prévisibles, il suffit de hacher tous les numéros possibles pour retrouver la correspondance. On utilise une fonction à clé secrète, HMAC :
import hmac, hashlib
def pseudo(employee_id, secret):
digest = hmac.new(secret.encode(), str(employee_id).encode(), hashlib.sha256).hexdigest()
return digest[:12]
clean = raw.drop(columns=["name", "email"])
clean["pid"] = clean["employee_id"].map(lambda i: pseudo(i, SECRET))
clean = clean.drop(columns=["employee_id"])
Le même matricule donne toujours le même pseudonyme avec la même clé, ce qui permet les jointures. La clé est conservée séparément, par une personne habilitée, jamais dans le notebook ni dans le dépôt de code.
Pseudonymisation n’est pas anonymisation
- Pseudonymisé : la personne n’est plus nommée, mais on peut la retrouver (avec la clé, ou en croisant des colonnes). Les données restent personnelles et le RGPD continue de s’appliquer.
- Anonymisé : la réidentification est impossible, de façon irréversible, par quelque moyen raisonnable que ce soit. Les données sortent alors du champ du RGPD.
L’anonymisation réelle est difficile : dans une PME, « la seule femme cadre du service juridique de plus de 50 ans » est identifiable sans aucun nom. Le croisement de quelques colonnes (service, âge, sexe, date d’entrée) suffit souvent. Les techniques usuelles : généraliser (tranches d’âge plutôt qu’âge exact), supprimer les colonnes rares, et ne publier que des agrégats.
Le seuil de diffusion
Publier « salaire moyen du service juridique » quand ce service compte deux personnes revient presque à publier leurs salaires : chacun connaît le sien et déduit celui de l’autre. On applique une règle de seuil : tout indicateur calculé sur moins de N personnes (souvent 5, la valeur est une règle interne à fixer avec le DPO) est masqué.
def safe_aggregate(df, by, value, min_n=5):
out = df.groupby(by)[value].agg(n="size", mean_value="mean")
out["suppressed"] = out["n"] < min_n
out.loc[out["suppressed"], "mean_value"] = float("nan")
return out
Attention aux déductions par différence : si vous publiez le total de l’entreprise et tous les services sauf un, le service masqué se recalcule. Masquez alors un second groupe ou regroupez les petits services dans une catégorie « autres ».
À retenir
- Minimisez : ne gardez que les colonnes utiles à la finalité.
- Pseudonymisez avec une clé secrète conservée à part ; les données restent personnelles.
- Ne diffusez jamais un agrégat calculé sur un trop petit groupe.
Exercices
Les solutions et les tests restent sur le serveur. Dans l'espace étudiant, ton code est exécuté et corrigé, et le tuteur donne des indices avant la solution.
Pseudonymiser un extrait
Écrivez
pseudonymize(df, secret)qui renvoie une copie dedf:- sans les colonnes
nameetemail; - avec une colonne
pid= les 12 premiers caractères hexadécimaux du HMAC-SHA256 destr(employee_id)avec la clésecret; - sans la colonne
employee_id.
Ne modifiez pas
df.import pandas as pd raw = pd.DataFrame({ "employee_id": [1001, 1002, 1003, 1004], "name": ["Camille Roux", "Sacha Morel", "Lou Perrin", "Noa Lemaire"], "email": ["[email protected]", "[email protected]", "[email protected]", "[email protected]"], "department": ["Ventes", "Tech", "Tech", "Ventes"], "salary": [34000, 48000, 52000, 36000], }) import hmac import hashlib def pseudonymize(df, secret): ...- sans les colonnes
Agrégats avec seuil de diffusion
Écrivez
safe_aggregate(df, by, value, min_n=5)qui renvoie, par groupe,n(effectif),mean_value(moyenne devalue) etsuppressed(Truesin < min_n) ; la moyenne des groupes masqués est remplacée parNaN. Calculez ensuitereport = safe_aggregate(staff, "department", "salary")ethidden, la liste des groupes masqués.import pandas as pd staff = pd.DataFrame({ "department": ["Ventes"] * 6 + ["Tech"] * 7 + ["Juridique"] * 2, "salary": [32000, 34000, 36000, 38000, 40000, 42000, 45000, 47000, 50000, 52000, 55000, 58000, 60000, 48000, 70000], }) def safe_aggregate(df, by, value, min_n=5): ... # report = ... # hidden = ...Préparer un extrait diffusable dans un tableur
Saisissez à partir de A1 ce fichier fictif :
Matricule;Nom;Service;Age;Salaire 1001;Roux;Ventes;26;30000 1002;Morel;Ventes;34;32000 1003;Perrin;Ventes;41;34000 1004;Lemaire;Ventes;52;36000 1005;Garnier;Ventes;29;38000 1006;Blanc;Juridique;38;45000 1007;Faure;Juridique;47;50000 1008;Robin;Juridique;31;55000- Supprimez les colonnes Matricule et Nom.
- Ajoutez une colonne
Trancheavec la formule=SI(B2<30;"<30";SI(B2<45;"30-44";"45+"))(SI (IF)), recopiez-la, puis supprimez la colonne Age après l’avoir convertie en valeurs (collage spécial, valeurs). - Créez un tableau croisé dynamique par Service avec le nombre de lignes et la moyenne de Salaire.
- Appliquez la règle de seuil de 5 personnes : dans le tableau de synthèse à diffuser, remplacez la moyenne des services de moins de 5 personnes par « secret ».
- L'extrait ne contient plus ni nom, ni matricule, ni âge exact (colonnes Service, Salaire, Tranche)
- Les tranches comptent 2 personnes de moins de 30 ans, 4 de 30 à 44 ans et 2 de 45 ans et plus
- Le tableau de synthèse affiche Ventes : 5 personnes, moyenne 34 000
- La moyenne du service Juridique (3 personnes) est remplacée par « secret »
Quiz de fin de leçon
Un fichier où le nom a été remplacé par un code, avec une table de correspondance conservée par les RH, est :
- Anonymisé
- Pseudonymisé : les données restent personnelles
- Hors du champ du RGPD
Pourquoi utiliser HMAC plutôt qu’un simple sha256 du matricule ?
- C'est plus court
- Sans clé secrète, on retrouve un matricule en hachant tous les numéros possibles
- sha256 n'existe pas en Python
Pourquoi masquer le salaire moyen d’un service de deux personnes ?
- Parce que la moyenne est fausse
- Parce qu'elle permet de déduire un salaire individuel
- Parce que pandas ne sait pas la calculer
La suite du cours
- Effectifs et turnover18 min
- Absentéisme : taux, fréquence et durée15 min
- Recrutement : délais, tunnel de candidatures et sources15 min
- Égalité salariale : écart brut, écart à poste comparable et limites20 min