🦆 Module 15 — Pandas approfondi + DuckDB
Bootcamp Data Analyst — From Zero to Hero | Niveau Intermédiaire · Module 15
🎯 Ce que tu seras capable de faire à la fin de ce module
- Distinguer
transform/mutate(garde une ligne par observation) deagg/summarise(résume par groupe) - Découper une variable continue en tranches (
pd.cut/cut()) - Écrire une analyse lisible en chaînant les étapes (method chaining / pipe
%>%) - Interroger un DataFrame directement en SQL avec DuckDB, en Python et en R
- Savoir quand utiliser DuckDB plutôt qu'une vraie base de données (Module 16)
⏱️ Durée estimée : 2 heures 🛠️ Outils : Python (
pandas,duckdb) · R (dplyr,duckdb) 📌 Prérequis : Modules 10-12 (Niveau Débutant) · Module 14
1. Pourquoi aller au-delà du groupby/group_by simple
Au Niveau Débutant, tu as appris à grouper et résumer : moyenne par département, comptage par catégorie. En situation réelle, trois besoins reviennent sans cesse et méritent des outils dédiés :
- Comparer une valeur à la moyenne de son groupe, sans perdre le détail ligne par ligne (ex : "de combien ce salaire s'écarte-t-il de la moyenne de son département ?")
- Découper une variable continue en catégories lisibles (ex : ancienneté en "Junior / Confirmé / Senior" plutôt qu'un nombre d'années brut)
- Interroger rapidement un DataFrame avec du SQL, sans monter une base de données, quand tu préfères la syntaxe
SELECTàgroupby
Le dataset — on retrouve celui du Module 11
Même jeu de données RH de Bamba & Associés qu'au Module 11 (15 employés) — mais cette fois avec des techniques plus avancées.
| Nom | Département | Contrat | Salaire (FCFA) | Ancienneté (ans) |
|---|---|---|---|---|
| Kouassi Ama | RH | CDI | 450 000 | 5 |
| Kouamé Jean | Finance | CDI | 620 000 | 8 |
| Traoré Seydou | Commercial | CDI | 380 000 | 3 |
| Diabaté Fatou | RH | CDD | 310 000 | 1 |
| Bamba Koné | Direction | CDI | 1 800 000 | 12 |
| Ouédraogo Moussa | IT | CDI | 580 000 | 6 |
| Soro Aminata | Commercial | Stage | 180 000 | 0 |
| Koné Ibrahim | Finance | CDI | 540 000 | 7 |
| Yao Adjoua | IT | CDI | 610 000 | 9 |
| Diallo Mamadou | Commercial | CDD | 340 000 | 2 |
| Coulibaly Assata | RH | CDI | 420 000 | 4 |
| N'Guessan Éric | Finance | CDI | 590 000 | 7 |
| Touré Mariam | IT | Stage | 200 000 | 0 |
| Keita Souleymane | Commercial | CDI | 360 000 | 3 |
| Fofana Raïssa | Direction | CDI | 950 000 | 10 |
2. transform/mutate vs agg/summarise
C'est la distinction la plus utile de ce module : est-ce que tu veux garder une ligne par employé (avec une info de groupe ajoutée), ou résumer en une ligne par groupe ?
Python (transform garde les 15 lignes) :
pythondf["salaire_moyen_dept"] = df.groupby("departement")["salaire"].transform("mean").round(0)
print(df[["nom", "departement", "salaire", "salaire_moyen_dept"]].head(4))
# nom departement salaire salaire_moyen_dept
# 0 Kouassi Ama RH 450000 393333.0
# 1 Kouamé Jean Finance 620000 583333.0
# 2 Traoré Seydou Commercial 380000 315000.0
# 3 Diabaté Fatou RH 310000 393333.0Python (agg, collapse à 5 lignes — une par département) :
pythonprint(df.groupby("departement")["salaire"].mean().round(0))
# departement
# Commercial 315000.0
# Direction 1375000.0
# Finance 583333.0
# IT 463333.0
# RH 393333.0R (mutate, équivalent de transform — garde les 15 lignes) :
rlibrary(dplyr)
df <- df %>%
group_by(departement) %>%
mutate(salaire_moyen_dept = round(mean(salaire), 0)) %>%
ungroup()
df %>% select(nom, departement, salaire, salaire_moyen_dept) %>% head(4)R (summarise, équivalent de agg — collapse par groupe) :
rdf %>%
group_by(departement) %>%
summarise(salaire_moyen = round(mean(salaire), 0))🔗 Pont :
transform(Python) =mutate(R) → une ligne par observation, une colonne de groupe en plus.agg(Python) =summarise(R) → une ligne par groupe, le détail individuel disparaît.
À quoi ça sert concrètement
pythondf["ecart_moyenne"] = (df["salaire"] - df["salaire_moyen_dept"]).round(0)
print(df[["nom", "departement", "ecart_moyenne"]].sort_values("ecart_moyenne", ascending=False).head(3))
# nom departement ecart_moyenne
# 4 Bamba Koné Direction 425000.0
# 8 Yao Adjoua IT 146667.0
# 5 Ouédraogo Moussa IT 116667.0Bamba Koné gagne 425 000 FCFA de plus que la moyenne de son département — une info impossible à obtenir avec un simple agg, puisqu'elle nécessite de garder chaque ligne individuelle et la moyenne de son groupe côte à côte.
3. Découper une variable continue en tranches
Un nombre d'années brut ("5", "8", "0"...) est moins lisible dans un rapport que des catégories métier.
Python (pd.cut) :
pythonimport numpy as np
df["tranche_anciennete"] = pd.cut(
df["anciennete"],
bins=[-1, 2, 5, np.inf],
labels=["Junior (0-2 ans)", "Confirmé (3-5 ans)", "Senior (6+ ans)"]
)
print(df["tranche_anciennete"].value_counts())
# tranche_anciennete
# Senior (6+ ans) 7
# Junior (0-2 ans) 4
# Confirmé (3-5 ans) 4R (cut(), même logique de bornes) :
rdf$tranche_anciennete <- cut(
df$anciennete,
breaks = c(-1, 2, 5, Inf),
labels = c("Junior (0-2 ans)", "Confirmé (3-5 ans)", "Senior (6+ ans)")
)
table(df$tranche_anciennete)💡 Les bornes sont exclues à gauche, incluses à droite par défaut dans les deux langages :
(-1, 2]inclut 0, 1 et 2, mais pas -1. Vérifie toujours un cas limite (ici, quelqu'un avec exactement 2 ans d'ancienneté) pour confirmer de quel côté il tombe.
Alternative R plus flexible — case_when (utile si la logique de découpage n'est pas une simple suite de bornes) :
rdf <- df %>%
mutate(tranche_anciennete = case_when(
anciennete <= 2 ~ "Junior (0-2 ans)",
anciennete <= 5 ~ "Confirmé (3-5 ans)",
TRUE ~ "Senior (6+ ans)"
))4. Écrire une analyse lisible — method chaining / pipe
Plutôt que d'empiler des variables intermédiaires (etape1 = ..., etape2 = ...), on chaîne les opérations pour lire l'analyse comme une phrase.
Python :
pythonresultat = (
df.groupby("departement")["salaire"]
.agg(["mean", "count"])
.round(0)
.rename(columns={"mean": "salaire_moyen", "count": "effectif"})
.sort_values("salaire_moyen", ascending=False)
)
print(resultat)
# salaire_moyen effectif
# departement
# Direction 1375000.0 2
# Finance 583333.0 3
# IT 463333.0 3
# RH 393333.0 3
# Commercial 315000.0 4R (avec le pipe %>%, identique dans l'esprit) :
rresultat <- df %>%
group_by(departement) %>%
summarise(salaire_moyen = round(mean(salaire), 0), effectif = n()) %>%
arrange(desc(salaire_moyen))
print(resultat)🔗 Pont : les parenthèses
(...)en Python et le pipe%>%en R jouent le même rôle — enchaîner les étapes sans variables intermédiaires jetables. Le R moderne propose aussi le pipe natif|>(équivalent à%>%pour un usage simple).
5. DuckDB — interroger un DataFrame en SQL
DuckDB est un moteur SQL "embarqué" (in-process) — pas de serveur à lancer, pas de connexion réseau. Il s'installe comme une librairie et peut interroger directement un DataFrame Python/R avec la syntaxe SQL que tu maîtrises depuis les Modules 06-09.
Python
pythonimport duckdb
resultat = duckdb.sql("""
SELECT departement, ROUND(AVG(salaire),0) AS salaire_moyen, COUNT(*) AS effectif
FROM df
WHERE contrat = 'CDI'
GROUP BY departement
ORDER BY salaire_moyen DESC
""").df()
print(resultat)
# departement salaire_moyen effectif
# 0 Direction 1375000.0 2
# 1 IT 595000.0 2
# 2 Finance 583333.0 3
# 3 RH 435000.0 2
# 4 Commercial 370000.0 2DuckDB reconnaît automatiquement df comme une table — pas besoin de l'importer ou de le déclarer, tant que la variable existe dans ton environnement Python.
R
rlibrary(duckdb)
library(DBI)
con <- dbConnect(duckdb())
duckdb_register(con, "df", df)
resultat <- dbGetQuery(con, "
SELECT departement, ROUND(AVG(salaire),0) AS salaire_moyen, COUNT(*) AS effectif
FROM df
WHERE contrat = 'CDI'
GROUP BY departement
ORDER BY salaire_moyen DESC
")
print(resultat)
dbDisconnect(con, shutdown = TRUE)En R, il faut explicitement enregistrer le DataFrame (duckdb_register) avant de pouvoir le requêter, et fermer proprement la connexion (dbDisconnect(..., shutdown = TRUE)) à la fin — Python gère ça de façon plus transparente.
💡 Pourquoi ne pas juste utiliser
groupby/group_by? Parce que si tu es plus à l'aise en SQL, ou que ta logique d'analyse est déjà écrite en SQL (par exemple copiée depuis le Module 16), DuckDB te permet de la réutiliser telle quelle sur un DataFrame local — sans base de données, sans jointure de schéma à gérer.
6. Pont DuckDB → PostgreSQL
Tu vas rencontrer un vrai PostgreSQL hébergé au Module 16. Ne confonds pas les deux :
| DuckDB (ce module) | PostgreSQL (Module 16) | |
|---|---|---|
| Où ça tourne | En local, dans ton processus Python/R | Sur un serveur distant |
| Pour quoi faire | Prototypage rapide, analyse solo sur des fichiers/DataFrames | Stockage centralisé, partagé, auquel Power BI se connecte (Module 17) |
| Persistance | Éphémère (le temps du script), ou un simple fichier .duckdb |
Persistant, multi-utilisateurs, accessible à toute l'équipe |
| Installation | pip install duckdb / install.packages("duckdb") — zéro serveur |
Compte hébergé (Neon/Supabase) — zéro install non plus, mais un vrai serveur existe derrière |
| Concurrence | Un seul processus à la fois | Plusieurs utilisateurs/applications en simultané |
🔗 Retiens l'essentiel : DuckDB pour l'analyse locale ultra-rapide et le prototypage, PostgreSQL pour le stockage persistant et centralisé de l'entreprise. Ce n'est pas "lequel est meilleur", mais "lequel correspond à ce que tu es en train de faire" — un∙e DA utilise souvent les deux dans la même semaine.
✅ Résumé du module
| Concept | Python | R |
|---|---|---|
| Ajouter une stat de groupe en gardant chaque ligne | .groupby(...).transform(...) |
group_by(...) %>% mutate(...) |
| Résumer une ligne par groupe | .groupby(...).agg(...) |
group_by(...) %>% summarise(...) |
| Découper en tranches | pd.cut(col, bins=..., labels=...) |
cut(col, breaks=..., labels=...) |
| Découpage conditionnel flexible | np.select / conditions imbriquées |
case_when(...) |
| Chaîner des étapes | Parenthèses (...) + méthodes . |
Pipe %>% ou ` |
| SQL sur un DataFrame | duckdb.sql("... FROM df") |
dbGetQuery(con, "... FROM df") après duckdb_register |
| DuckDB vs PostgreSQL | Local, prototypage | Distant, centralisé, multi-utilisateurs |
🧠 Quiz — Vérifie ta compréhension
Q1. Tu veux ajouter une colonne "salaire moyen du département" à ton DataFrame, en gardant les 15 lignes individuelles. Tu utilises...
- a)
.groupby("departement")["salaire"].agg("mean") - b)
.groupby("departement")["salaire"].transform("mean") - c)
.groupby("departement")["salaire"].sum()
👉 Voir la réponse
✅ b) — transform renvoie un résultat de la même taille que la colonne d'origine (une valeur par ligne), donc tu peux l'assigner directement comme nouvelle colonne. agg (a) collapse en une ligne par groupe — impossible à réassigner ligne par ligne sans jointure supplémentaire.
Q2. En R, quel est l'équivalent de .groupby(...).transform(...) ?
- a)
group_by(...) %>% summarise(...) - b)
group_by(...) %>% mutate(...) - c)
group_by(...) %>% filter(...)
👉 Voir la réponse
✅ b) — mutate ajoute une colonne en gardant une ligne par observation, exactement comme transform en Python. summarise (a) est l'équivalent de agg (collapse par groupe).
Q3. Pourquoi choisir DuckDB plutôt qu'un vrai PostgreSQL pour analyser un fichier CSV que tu viens de télécharger ?
- a) DuckDB est plus rapide dans tous les cas, il faut toujours le préférer
- b) Le fichier est local et l'analyse est ponctuelle — pas besoin d'un serveur centralisé et partagé
- c) PostgreSQL ne sait pas faire de
GROUP BY
👉 Voir la réponse
✅ b) — DuckDB convient au prototypage local sur un fichier que toi seul∙e utilises. Dès que plusieurs personnes ou outils (comme Power BI) doivent accéder aux mêmes données en continu, il faut un serveur centralisé comme PostgreSQL.
➡️ Module suivant
Tu sais maintenant manipuler des DataFrames en profondeur et les interroger en SQL local. Direction le Module 16, où tu montes un vrai PostgreSQL hébergé et écris des requêtes avancées.
→ Module 16 — SQL avancé & vues optimisées