PE – Python numérique – S4

pandas 2 – trier, regrouper, croiser, combiner

S4 · mardi 6 octobre 2026

Raphael Prasquier · cours d'Aurélien Noce – UE12 Python numérique

mardi 6 octobre 2026
PE – Python numérique – S4

Au programme aujourd'hui 🎯

Quiz 3 (10 min, papier) sur la séance 3 et le devoir D3, puis correction flash.

Notebooks :

  • 2-05 trier : sort_values, sort_index
  • 2-07 regrouper : groupby, multi-critères, pd.cut, pivot_table
  • 2-08 créer une DataFrame à la main
  • 2-09 combiner : concat et merge

TP : exercice Titanic (groupby / pivot_table) puis TP « collages »

Devoir : D4 « l'énergie par région », pour le dimanche 11 octobre, 23 h 59

mardi 6 octobre 2026
PE – Python numérique – S4

Retour sur le quiz 3

  • loc inclut la borne. df.loc[0:2] → 3 lignes. Et après un filtre, df.loc[0]
    peut lever KeyError : l'étiquette 0 n'est plus là, iloc[0] si.
  • == np.nan est toujours faux. Les manquants se trouvent avec isna(), se comptent avec
    isna().sum().
  • df[mask]["col"] = v n'écrit rien. La bonne forme est df.loc[mask, "col"] = v.

Pour D3 : convertir un temps "2:05:42" se fait avec .str.split(":") puis astype(int) — pas avec une boucle sur les lignes.

mardi 6 octobre 2026
PE – Python numérique – S4

Trier : sort_values

df.sort_values("Age")                                      # croissant, NaN à la fin
df.sort_values(["Pclass", "Age"], ascending=[True, False])  # par classe, puis âge décroissant
df.nlargest(3, "Fare")                                     # les 3 plus chers, sans tout trier

Le tri préserve l'index : les étiquettes suivent leurs lignes.
reset_index(drop=True) renumérote de 0 à n-1 après coup.

df.sort_values("Age").index[:2]                     # [804, 756] — les PassengerId
df.sort_values("Age").reset_index(drop=True).index  # 0, 1, 2, …

En effet, une ligne ne perd pas son identité parce qu'on l'a déplacée.

mardi 6 octobre 2026
PE – Python numérique – S4

groupby : split → apply → combine

Dès qu'une question porte sur des sous-groupes, c'est un groupby qu'il faut.

df.groupby("Sex")["Age"].mean()
# Sex
# female    27.9
# male      30.7
  1. split — pandas découpe la table en groupes (un par valeur de Sex) ;
  2. apply — applique l'agrégation à chaque groupe (mean, sum, size, count…) ;
  3. combine — recolle les résultats en une Series indexée par la clé.

Vous n'écrivez que la ligne du milieu ; pandas fait les deux autres.

mardi 6 octobre 2026
PE – Python numérique – S4

groupby : agréger, inspecter

g = df.groupby("Sex")
g.size()                          # effectif de chaque groupe : female 314, male 577
g["Survived"].mean()              # taux de survie par sexe
g["Age"].agg(["mean", "count"])   # plusieurs agrégats d'un coup → une DataFrame
g.get_group("female")             # la sous-table, pour vérifier

df.groupby("Pclass")["Survived"].mean()   # 1 → 0.63, 2 → 0.47, 3 → 0.24

Piège de version : g.mean() sans choisir de colonne échoue (TypeError) dès qu'une colonne
est du texte. Le remède : g.mean(numeric_only=True) — ou mieux, nommez la colonne.

Notez la nuance : size compte les lignes, count compte les valeurs non manquantes d'une colonne.

mardi 6 octobre 2026
PE – Python numérique – S4

Plusieurs clés → un MultiIndex

m = df.groupby(["Pclass", "Sex"])["Survived"].mean()
# Pclass  Sex
# 1       female    0.97
#         male      0.37
# 2       female    0.92
#         male      0.16
# 3       female    0.50
#         male      0.14
m.loc[(1, "female")]      # 0.97 : la clé est un TUPLE
m.index[:2]               # [(1, 'female'), (1, 'male')]
m.unstack()               # le 2e niveau passe en colonnes → table 3 × 2

En effet, le résultat est une Series (une valeur par couple), pas une table ; unstack() la remet à plat quand on veut la lire ou la tracer.

mardi 6 octobre 2026
PE – Python numérique – S4

pivot_table : le tableau croisé en un appel 💡

Quand la question porte sur deux axes (« par classe ET par sexe »), pivot_table fait d'un
coup le groupby à deux clés et le unstack :

df.pivot_table(index="Pclass", columns="Sex", values="Survived", aggfunc="mean")
# Sex     female  male
# Pclass
# 1         0.97  0.37
# 2         0.92  0.16
# 3         0.50  0.14
  • index → les lignes · columns → les colonnes · values → ce qu'on agrège · aggfunc → comment ("mean" par défaut) ;
  • aggfunc="count" ou "size" donne les effectifs croisés ; fill_value=0 pour les cases vides ;
  • lecture : une cellule = l'agrégat pour ce couple (classe, sexe).
mardi 6 octobre 2026
PE – Python numérique – S4

⚠️ Croiser avant de conclure · Titanic 1/5

Embarquer à Cherbourg sauvait-il des vies ?

df.groupby("Embarked")["Survived"].mean()
mardi 6 octobre 2026
PE – Python numérique – S4

⚠️ Croiser avant de conclure · Titanic 2/5

55 % contre 34 % : on pourrait le croire

port passagers survie
C (Cherbourg) 168 55 %
Q (Queenstown) 77 39 %
S (Southampton) 644 34 %

Parmi les passagers de Cherbourg, 55 % ont survécu, contre 34 % à Southampton.

Mais qui embarquait où ?

mardi 6 octobre 2026
PE – Python numérique – S4

⚠️ Croiser avant de conclure · Titanic 3/5

Qui embarquait où ? Compter avec crosstab

Pour savoir qui embarquait où, on compte les passagers pour chaque couple (port, classe) : c'est un tableau croisé d'effectifs.

pd.crosstab(df["Embarked"], df["Pclass"])
# Pclass      1    2    3
# Embarked
# C          85   17   66
# Q           2    3   72
# S         127  164  353

crosstab(lignes, colonnes) : une ligne par port, une colonne par classe, et dans chaque case le nombre de passagers.

C'est exactement la pivot_table de tout à l'heure avec aggfunc="size" — en plus court quand on veut seulement compter.

Or 85 passagers sur 168 ne se compare pas facilement à 127 sur 644 : passons en proportions.

mardi 6 octobre 2026
PE – Python numérique – S4

⚠️ Croiser avant de conclure · Titanic 4/5

Cherbourg : la moitié des passagers en 1re classe

port passagers survie
C (Cherbourg) 168 55 %
Q (Queenstown) 77 39 %
S (Southampton) 644 34 %

Classes par port : pd.crosstab(..., normalize="index"), chaque ligne ramenée à 100 %

port 1re 2e 3e
C 51 % 10 % 39 %
Q 3 % 4 % 94 %
S 20 % 25 % 55 %

À Southampton, au contraire, plus de la moitié voyageait en 3e. Or la 1re classe a bien mieux survécu.

mardi 6 octobre 2026
PE – Python numérique – S4

⚠️ Croiser avant de conclure · Titanic 5/5

À classe égale, l'écart se réduit nettement

Classes par port

port 1re 2e 3e
C 51 % 10 % 39 %
Q 3 % 4 % 94 %
S 20 % 25 % 55 %

Survie par port et par classe : df.pivot_table(index="Embarked", columns="Pclass", values="Survived")

port 1re 2e 3e
C 69 % 53 % 38 %
Q 50 % 67 % 38 %
S 58 % 46 % 19 %

En 1re, 69 % contre 58 % : c'est d'abord la classe qui jouait, pas le port. Le port était une variable confondante.

mardi 6 octobre 2026
PE – Python numérique – S4

⚠️ Croiser avant de conclure · le paradoxe de Simpson 1/3

Calculs rénaux : quel traitement choisir ?

Une étude compare deux traitements sur 700 patients (Charig et al., BMJ, 1986).

traitement patients succès
A (chirurgie ouverte) 350 78 %
B (percutanée, moins lourde) 350 83 %

Au total, B réussit mieux : 83 % contre 78 %.

Faut-il donc préférer B ?

mardi 6 octobre 2026
PE – Python numérique – S4

⚠️ Croiser avant de conclure · le paradoxe de Simpson 2/3

Par taille de calcul : A gagne partout !

A B
petits calculs 93 % 87 %
gros calculs 73 % 69 %
total 78 % 83 %

Sur les petits calculs, A fait mieux (93 % contre 87 %) ; sur les gros aussi (73 % contre 69 %).

Et pourtant, au total, c'est B qui l'emporte : c'est le paradoxe de Simpson.

mardi 6 octobre 2026
PE – Python numérique – S4

⚠️ Croiser avant de conclure · le paradoxe de Simpson 3/3

Tout dépend de qui a reçu quel traitement

Nombre de patients : pd.crosstab(df["taille"], df["traitement"])

A B
petits calculs 87 270
gros calculs 263 80
total 350 350

Les médecins réservaient B, moins lourd, aux petits calculs, les cas faciles ; A recevait surtout les gros.

B affiche donc de meilleurs résultats parce qu'il a reçu des patients plus faciles, pas parce qu'il soigne mieux.

Le réflexe : regardez la composition des groupes, puis refaites le calcul à l'intérieur de chaque sous-groupe.

Même piège, célèbre, à Berkeley en 1973 : au total, les femmes étaient moins admises que les hommes ; dans la plupart des départements, elles l'étaient autant ou plus. Elles candidataient simplement dans les départements les plus sélectifs.

mardi 6 octobre 2026
PE – Python numérique – S4

pd.cut : des intervalles → des catégories

Pour grouper une colonne continue, on la découpe d'abord en classes :

df["classe_age"] = pd.cut(df["Age"],
                          bins=[0, 12, 19, 55, 100],
                          labels=["enfant", "ado", "adulte", "senior"])
df["classe_age"].dtype          # category — ordonnée
df["classe_age"].value_counts() # adulte 510, ado 95, enfant 69, senior 40

df.groupby("classe_age")["Survived"].mean()
# enfant 0.58, ado 0.41, adulte 0.39, senior 0.30

Les bornes sont (a, b] : ouvertes à gauche, fermées à droite. Les NaN restent NaN.

En effet, « le taux de survie par âge » n'a pas de sens valeur par valeur ; par tranche, si.

mardi 6 octobre 2026
PE – Python numérique – S4

Le piège : l'index après groupby

df avait une colonne Sex ; le résultat du groupby, lui, n'en a plus : Sex est passé dans l'index.

g = df.groupby("Sex")["Age"].mean()
g
# Sex            ← nom de l'index
# female    27.9
# male      30.7
# Name: Age

g est une Series : female et male sont ses étiquettes.
g["female"] → 27.9 ; g["Sex"] → KeyError.

g.reset_index()
#       Sex   Age
# 0  female  27.9
# 1    male  30.7

reset_index() remet Sex en colonne : une vraie table, indexée 0, 1.
D'emblée : groupby("Sex", as_index=False).

À l'usage : on garde la clé en index pour lire une valeur, on la remet en colonne pour continuer — merge, export CSV, tracé.

mardi 6 octobre 2026
PE – Python numérique – S4

Créer une DataFrame à la main

Trois formes, selon comment les données se présentent :

pd.DataFrame({"nom": ["alice", "bob"], "age": [30, 25]})        # dict de listes : les COLONNES
pd.DataFrame([{"nom": "alice", "age": 30},
              {"nom": "bob",   "age": 25}])                        # liste de dicts : les LIGNES
pd.DataFrame(np.arange(6).reshape(2, 3),
             index=["a", "b"], columns=["x", "y", "z"])            # tableau numpy + étiquettes

Et dans l'autre sens : df.to_numpy() rend le tableau nu, df.to_dict() / s.tolist() les structures Python.

À l'usage, cela sert surtout à tester une idée sur 4 lignes avant de la lancer sur 40 000.

mardi 6 octobre 2026
PE – Python numérique – S4

Combiner en hauteur : pd.concat

Empiler des tables de même structure (mêmes colonnes) :

d1 = pd.DataFrame({"nom": ["alice", "bob"],  "service":  ["R&D", "vente"]})
d2 = pd.DataFrame({"nom": ["bob", "carol"],  "embauche": [2019, 2022]})

pd.concat([d1, d2])                      # index 0, 1, 0, 1 : les doublons sont gardés !
pd.concat([d1, d2], ignore_index=True)   # index 0, 1, 2, 3

Les colonnes absentes d'un côté sont remplies de NaN — ici embauche pour d1, service pour d2 :
signe qu'on a empilé des choses qui n'allaient pas ensemble.

axis=1 concatène en largeur, par index — mais le plus souvent, ce qu'on voulait, c'est un merge.

mardi 6 octobre 2026
PE – Python numérique – S4

Combiner en largeur : merge

Fusionner sur une colonne commune, comme le JOIN de SQL :

d1.merge(d2, on="nom", how="inner")   # bob seulement        (1 ligne)
d1.merge(d2, on="nom", how="left")    # alice, bob           (2) — carol ignorée
d1.merge(d2, on="nom", how="right")   # bob, carol           (2)
d1.merge(d2, on="nom", how="outer")   # alice, bob, carol    (3) — avec des NaN
  • how="inner" est le défaut : on perd silencieusement les clés absentes d'un côté ;
  • clés de noms différents : left_on=, right_on= ; clé dans l'index : left_index=True ;
  • vérifiez toujours le shape avant / après — un merge qui grossit a trouvé des doublons.

Pourquoi ce paramètre how ? La question « que faire des lignes sans correspondance ? » n'a pas de réponse universelle : c'est à vous de trancher.

mardi 6 octobre 2026
PE – Python numérique – S4

Les 3 pièges du jour ⚠️

  1. Après groupby, la clé est dans l'index. g["Sex"] → KeyError.
    as_index=False ou reset_index() si vous la voulez en colonne.

  2. concat empile, merge aligne. Si vous écrivez pd.concat(..., axis=1)
    pour rapprocher deux tables par une clé, c'est un merge qu'il vous faut.

  3. merge est inner par défaut : les lignes sans correspondance disparaissent
    sans un mot. Comparez les shape.

Bonus : sort_values garde l'index ; pd.cut ferme ses intervalles à droite.

mardi 6 octobre 2026
PE – Python numérique – S4

TP du jour

1. Titanic, groupby / pivot_table (exercices en fin de 2-07, sur https://numerique.info-mines.paris) :
taux de survie par classe × sexe, par tranche d'âge, effectifs croisés, la même table par
groupby + unstack et par pivot_table — et vérifier qu'elles sont égales.

2. TP « collages » (collages1/2/3.csv, sur https://numerique-exos.info-mines.paris) :
trois fichiers partiels à empiler puis à joindre, avec des clés qui ne
correspondent pas toutes — choisir how= en connaissance de cause.

À rendre : rien — mais le TP collages est le brouillon de D4.

Pour les rapides : 2-15 itables · exercices « groupby » et « merge » sur numerique-exos ·
catalogue tp-optionnels/.

mardi 6 octobre 2026
PE – Python numérique – S4

Devoir pour la prochaine séance

D4 — L'énergie par région

Pour le dimanche 11 octobre, 23 h 59.

Une table région ; filière ; production (GWh), plus une table de population par région.

  • un tableau croisé région × filière (pivot_table, aggfunc="sum", fill_value=0) ;
  • le graphique en barres empilées correspondant (.plot.bar(stacked=True)) ;
  • un merge avec la population pour calculer une production par habitant, puis trier ;
  • deux ou trois phrases de lecture : quelle région, quelle filière, pourquoi.

Ce qu'on rend : le notebook exécuté, figures titrées et légendées.

Ce qui est noté : pivot_table et merge employés à bon escient (how= justifié),
pas de calcul « à la main » qu'un groupby ferait, lisibilité des figures.

mardi 6 octobre 2026