PE – Python numérique – S3

pandas 1 – tables étiquetées

S3 · mardi 29 septembre 2026

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

mardi 29 septembre 2026
PE – Python numérique – S3

Au programme aujourd'hui 🎯

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

Notebooks :

  • 2-01 read_csv, DataFrame, Series, premier coup d'œil
  • 2-02 conditions et masques sur une table
  • 2-03 indexation et slicing : loc, iloc, index vs indice

TP : 2-04 TP « on the moon » — nettoyer une vraie table en 16 étapes

Devoir : D3 « TP marathon », pour le dimanche 4 octobre, 23 h 59

mardi 29 septembre 2026
PE – Python numérique – S3

Retour sur le quiz 2

  • axis = la dimension consommée. tab.sum(axis=0) sur (3, 4) → 4 valeurs (une par colonne).
    Beaucoup ont répondu « 3 ».
  • Broadcasting par la droite. (3, 5) + (3,) → erreur ; (3, 5) + (3, 1) → ok.
    Le [:, None] n'est pas décoratif.
  • tab[mask] est une copie, tab[mask] = 0 écrit dans tab. Les deux phrases sont vraies,
    et la question demandait de les distinguer.

Et pour D2 : la boucle sur les itérations de Mandelbrot est normale ; celle sur les pixels, non.

mardi 29 septembre 2026
PE – Python numérique – S3

Pourquoi pandas ? Garder le lien entre les observations

Une observation, c'est un animal avec son nom et sa vitesse. Avec NumPy seul,
on range ces informations dans deux tableaux séparés, liés uniquement par la position :

import numpy as np

noms     = np.array(["cochon", "escargot", "elephant"])
vitesses = np.array([17.5, 0.1, 40])

vitesses.sort()             # [0.1, 17.5, 40] … mais noms n'a pas bougé
noms[vitesses > 10]         # ['escargot', 'elephant'] : faux, et aucune erreur !

Chaque tri, filtre ou suppression doit être répété à l'identique sur tous les tableaux ;
un seul oubli, et les noms ne correspondent plus aux vitesses.

Ce que nous voulons : une ligne = une observation, que tout le monde déplace d'un seul bloc.

mardi 29 septembre 2026
PE – Python numérique – S3

DataFrame et Series

Une observation par ligne, des colonnes de types différents – là où NumPy exige un seul dtype.

import pandas as pd

animaux = pd.DataFrame({
    "nom":     ["cochon", "escargot", "elephant"],
    "vitesse": [17.5, 0.1, 40],
})
animaux.dtypes                     # nom → str, vitesse → float64 : UN dtype PAR COLONNE
animaux.sort_values("vitesse")     # les lignes bougent entières
animaux[animaux["vitesse"] > 10]   # cochon, elephant : le nom suit sa vitesse
  • une DataFrame : la table, deux dimensions, colonnes nommées ;
  • une Series : une colonne (ou une ligne) isolée — un tableau NumPy plus un index.

En clair, pandas est NumPy plus des étiquettes.

mardi 29 septembre 2026
PE – Python numérique – S3

Series et ndarray : même moteur, alignement différent

Une Series, c'est un tableau NumPy 1D (les valeurs) plus un index (les étiquettes) et un nom.

s = pd.Series([17.5, 0.1, 40], index=["cochon", "escargot", "elephant"], name="vitesse")
s.to_numpy()      # array([17.5,  0.1, 40. ]) : les valeurs, sans l'index
np.sqrt(s)        # les ufunc NumPy marchent… et renvoient une Series, index compris
s[s > 10]         # masques booléens, comme en NumPy

La différence : les opérations s'alignent sur les étiquettes, pas sur les positions.

a = pd.Series([1, 2],   index=["x", "y"])
b = pd.Series([10, 20], index=["y", "z"])
a + b                          # x NaN, y 12, z NaN
a.to_numpy() + b.to_numpy()    # [11, 22] : NumPy apparie par position

Seules les colonnes numériques sont des ndarray en dessous : une colonne str est stockée autrement par pandas.

mardi 29 septembre 2026
PE – Python numérique – S3

Charger et regarder : read_csv

df = pd.read_csv("titanic.csv")          # sep=";", comment="#", index_col=... au besoin
df.shape          # (891, 12)
df.head()         # les 5 premières lignes
df.info()         # dtype et nombre de valeurs NON manquantes par colonne
df.describe()     # min / max / moyenne / quartiles des colonnes numériques
df.columns        # Index(['PassengerId', 'Survived', 'Pclass', 'Name', 'Sex', 'Age', ...])

Le réflexe, avant tout calcul : head() + info(). On y voit les types mal devinés
(un nombre lu comme du texte) et les trous.

info() est précieuse : c'est la seule vue qui montre à la fois le dtype et les manquants.

mardi 29 septembre 2026
PE – Python numérique – S3

Index et indice

L'indice est la position (0, 1, 2, …) ; l'index est l'étiquette de la ligne
— par défaut un RangeIndex, si bien que les deux coïncident par accident.

df = pd.read_csv("titanic.csv", index_col="PassengerId")   # ou df.set_index("PassengerId")
df.index          # les PassengerId, dans l'ordre du fichier (pas trié !)

df.loc[552]       # la ligne dont l'ÉTIQUETTE vaut 552
df.iloc[0]        # la ligne en POSITION 0   → ici la même, par hasard
df.loc[0]         # KeyError : aucun passager n'a l'étiquette 0

Les colonnes ont aussi un index : df.columns. Une DataFrame, c'est des données + deux index.

L'intérêt de distinguer les deux : après un tri, un filtre ou un groupby, les positions changent, les étiquettes restent.

mardi 29 septembre 2026
PE – Python numérique – S3

Les colonnes

Une DataFrame se comporte comme un dictionnaire de colonnes :

df["Age"]                  # une Series
df[["Name", "Age"]]        # une DataFrame (liste de colonnes → double crochet)
df.Age                     # raccourci, seulement si le nom est un identifiant valide

df["Decede"] = 1 - df["Survived"]   # nouvelle colonne, calcul vectorisé
df["Age"].mean(), df["Age"].max()   # les agrégations NumPy, qui IGNORENT les NaN
df["Sex"].value_counts()            # male 577, female 314
df["Embarked"].unique()             # ['S', 'Q', 'C', nan]

value_counts et unique d'abord : ce sont les deux premières questions à poser à toute colonne catégorielle.

mardi 29 septembre 2026
PE – Python numérique – S3

loc et iloc

Deux accesseurs [ligne, colonne] — toujours les lignes d'abord :

accesseur travaille sur borne haute d'une tranche
.loc les étiquettes incluse
.iloc les positions entières exclue (comme Python)
df = pd.read_csv("titanic.csv")       # RangeIndex 0..890
df.loc[0:2]          # 3 lignes : étiquettes 0, 1, 2
df.iloc[0:2]         # 2 lignes : positions 0, 1
df.loc[0, "Name"]    # une cellule
df.iloc[0, 3]        # la même, par position
df.loc[df["Sex"] == "female", ["Name", "Age"]]   # masque + liste de colonnes

Pourquoi loc inclut-il la borne ? Parce que df.loc["2020-01":"2020-03"] doit donner tout mars — nous y reviendrons en S5.

mardi 29 septembre 2026
PE – Python numérique – S3

Masques booléens

Comme en NumPy, une condition est vectorisée et renvoie une Series de booléens, alignée sur l'index :

mineurs_1re = (df["Age"] < 18) & (df["Pclass"] == 1)
mineurs_1re.sum()                   # 12
df[mineurs_1re]                     # les 12 lignes
df.loc[mineurs_1re, "Name"]

df["Age"].between(0, 1)             # bébés : bornes incluses
df["Embarked"].isin(["C", "Q"])     # appartenance à une liste

Mêmes règles qu'en NumPy : &, |, ~, parenthèses — jamais and, or, not :

df[df["Age"] > 30 and df["Sex"] == "female"]   # ValueError: The truth value of a Series is ambiguous
mardi 29 septembre 2026
PE – Python numérique – S3

Valeurs manquantes : NaN

Une valeur absente est notée NaN (Not a Number). Elle contamine les calculs élément par élément, mais les agrégations l'ignorent.

df["Age"].isna().sum()       # 177 âges manquants
df.isna().sum()              # le compte, colonne par colonne
df["Age"].mean()             # 29.7 — calculée sur les 714 valeurs présentes

df.dropna(subset=["Age"])              # retirer les lignes sans âge
df["Age"].fillna(df["Age"].median())   # ou remplacer

Premier piège : on ne teste pas un NaN avec ==.

df["Age"] == np.nan          # toujours False ! NaN n'est égal à rien → utilisez isna()
mardi 29 septembre 2026
PE – Python numérique – S3

isna() renvoie un masque

isna() ne répond pas « oui / non » : elle renvoie une Series de booléens, une par ligne,
que l'on utilise comme n'importe quel masque.

sans_age = df["Age"].isna()     # Series de bool : False, False, False, True, …
sans_age.sum()                  # 177 : True compte pour 1

df[sans_age]                    # les 177 passagers sans âge
df[~sans_age]                   # les 714 autres (≡ df[df["Age"].notna()])
df.loc[sans_age, "Name"]        # seulement leurs noms
df[sans_age & (df["Sex"] == "female")]   # 53 femmes sans âge

df[sans_age]["Survived"].mean()   # 0.29 contre 0.41 pour les autres

Le dernier résultat est une vraie question : les manquants ne sont pas forcément tirés au hasard.
Avant de les supprimer, regardez qui ils sont.

mardi 29 septembre 2026
PE – Python numérique – S3

Second piège : un trou change le dtype

NaN est un flottant (type(np.nan) → float), et un tableau NumPy d'entiers ne sait pas
représenter une case vide. Pour loger le trou, pandas convertit donc toute la colonne en float64.

pd.Series([1, 2, 3]).dtype      # int64
pd.Series([1, 2, None]).dtype   # float64 : [1.0, 2.0, NaN]

C'est ce qui arrive à la lecture d'un CSV : une seule cellule vide dans une colonne d'entiers,
et vous obtenez 3.0, NaN, 5.0. Revenir aux entiers échoue tant que le trou est là :

df["nb"].astype(int)            # IntCastingNaNError : impossible de convertir NaN en entier
df["nb"].astype("Int64")        # [3, <NA>, 5] : entiers « nullables » de pandas (I majuscule)
mardi 29 septembre 2026
PE – Python numérique – S3

Nettoyer : drop, astype, to_numeric, replace

df = df.drop(columns=["Cabin", "Ticket"])       # colonnes inutiles (ou trop trouées)

s = pd.Series(["1", "2", "x"])
pd.to_numeric(s, errors="coerce")               # [1.0, 2.0, NaN] : l'intrus devient NaN
df["Sex"].replace({"male": "H", "female": "F"})

La plupart des méthodes renvoient une nouvelle table et laissent l'original intact :
pensez à réaffecter (df = df.drop(...)).

L'intérêt de errors="coerce" : un fichier réel a toujours une case « n/a » ou « ? », et on préfère un NaN qu'on sait compter à une exception.

mardi 29 septembre 2026
PE – Python numérique – S3

Le type category : un nombre n'est pas toujours une quantité

Pclass vaut 1, 2 ou 3 : pandas y voit des entiers, et calcule volontiers des absurdités.

df["Pclass"].mean()          # 2.31 : la « classe moyenne » du Titanic ?!

Le type category déclare une colonne à valeurs dans un ensemble fini :

df["Pclass"] = df["Pclass"].astype("category")
df["Pclass"].cat.categories  # [1, 2, 3] : les valeurs possibles
df["Pclass"].mean()          # TypeError : pandas refuse l'opération absurde
df["Pclass"].describe()      # count 891, unique 3, top 3 : un résumé qui a du sens
  • le sens : les calculs absurdes sont refusés ;
  • la mémoire : un petit code par ligne (Embarked : 44 ko → 1 ko) ;
  • l'ordre : on peut en déclarer un (faible < moyen < fort) pour trier et comparer.
mardi 29 septembre 2026
PE – Python numérique – S3

L'accesseur .str

Les colonnes de texte s'attaquent avec .str, qui vectorise les méthodes des chaînes :

df["Name"].str.contains("Miss").sum()    # 182
df["Name"].str.split(",").str[0]         # le nom de famille
df["Name"].str.len()
df["Name"].str.upper().str.strip()

Même idée pour les dates avec .dt (S5) et pour les catégories avec .cat.

Pourquoi un accesseur ? df["Name"].upper() n'existe pas, car une Series n'est pas une chaîne ; .str dit explicitement « applique à chaque élément ».

mardi 29 septembre 2026
PE – Python numérique – S3

Écrire dans une table ⚠️

df.loc[df["Age"] > 70, "Age"] = 70          # OUI : une seule opération [ligne, colonne]

df[df["Age"] > 70]["Age"] = 70              # NON : ChainedAssignmentError, RIEN n'est écrit

La 2e forme crée une copie intermédiaire (df[mask]) et écrit dedans.
Depuis pandas 3, c'est un avertissement explicite et df ne change pas ;
avant, ça marchait parfois — le pire des comportements !

La règle : pour lire, df[mask] va très bien ; pour écrire, toujours df.loc[mask, col] = ....

mardi 29 septembre 2026
PE – Python numérique – S3

Les 3 pièges du jour ⚠️

  1. loc inclut la borne haute, iloc l'exclut. df.loc[0:2] → 3 lignes, df.iloc[0:2] → 2.
    Et loc parle en étiquettes : après un filtre, df.loc[0] peut ne plus exister.

  2. NaN n'est égal à rien, même pas à lui-même. == np.nan est toujours faux →
    isna() / notna(). Et une colonne d'entiers avec un trou devient float64.

  3. df[mask][col] = v n'écrit nulle part. C'est df.loc[mask, col] = v.

Bonus : df["Age"] est une Series, df[["Age"]] une DataFrame. Le double crochet n'est pas une faute de frappe.

mardi 29 septembre 2026
PE – Python numérique – S3

TP du jour : « on the moon »

Notebook 2-04 sur https://numerique.info-mines.paris : les objets artificiels posés (ou écrasés) sur la Lune.

16 étapes courtes, chacune un geste : read_csv → drop → info → dropna → dtypes →
unique → to_numeric → replace → conversion → comptage par pays → total des masses →
.str sur les descriptions → tolist.

C'est le squelette de tout nettoyage : regarder, retirer, convertir, vérifier, compter.

À rendre : rien — mais c'est exactement ce que D3 vous demandera sur d'autres données.

Pour les rapides : exercices « pandas – bases » sur https://numerique-exos.info-mines.paris ;
2-15 tables interactives (itables) pour explorer confortablement.

Pour les très rapides 🗺️ : troisième niveau secret, le sujet bonus B3 – La France en 35 000 lignes.
Sur sa page ou celle des devoirs, tapez XYZZY — le mot magique de Colossal Cave Adventure (1976).

mardi 29 septembre 2026
PE – Python numérique – S3

Devoir pour la prochaine séance

D3 — TP marathon

Pour le dimanche 4 octobre, 23 h 59.

Les résultats d'un marathon, bruts : noms, pays, années, et des temps en texte ("2:05:42").

  • charger correctement (séparateur, en-tête, index_col) ;
  • convertir les temps en secondes (.str.split + astype), traiter les manquants ;
  • une dizaine de questions par masques et loc (le plus rapide, les Kényans, les femmes sous 2 h 30…) ;
  • une colonne calculée, un tri, un tolist().

Ce qu'on rend : le notebook exécuté, une réponse lisible sous chaque question.
Ce qui est noté : pas de boucle sur les lignes, loc/masques bien employés, NaN traités.

mardi 29 septembre 2026