Aller au contenu principal

Module 7 — Jointures, regroupements et tableaux croisés

Les données réelles arrivent en morceaux : les commandes dans une table, les clients dans une autre, les produits dans une troisième. Et les questions qui intéressent le métier sont des agrégats : chiffre d'affaires par segment, panier moyen par mois. Ce module couvre les deux mécanismes qui relient les morceaux aux réponses — et leurs pièges, qui produisent des chiffres faux sans lever la moindre erreur.

merge : joindre deux tables

commandes_enrichies = commandes.merge(
clients,
on="client_id", # la clé de jointure
how="left", # le type de jointure
)

Les quatre types, et quand les employer :

how=Ce qui est conservéUsage typique
innerLes clés présentes des deux côtésCroiser deux sources fiables
leftToute la table de gaucheEnrichir sans perdre de lignes — le défaut raisonnable
rightToute la table de droiteRare — réécrire en left inversé
outerTout, des deux côtésAudits de correspondance entre sources

Avec how="left", les commandes sans client correspondant reçoivent des NaN dans les colonnes clients — visible et traçable. Avec inner, elles disparaissent : un inner irréfléchi est la façon la plus discrète de perdre 8 % de son chiffre d'affaires dans une analyse.

Les deux vérifications non négociables

Compter avant et après. Un left doit rendre exactement le nombre de lignes de gauche ; s'il en rend plus, la clé de droite contenait des doublons et chaque ligne de gauche a été démultipliée — les sommes en aval seront gonflées en silence. Compter les non-appariés : resultat["colonne_de_droite"].isna().sum(). Ces quatre lignes de contrôle évitent les deux accidents de jointure les plus coûteux.

assert len(resultat) == len(commandes), "Démultiplication : clé non unique à droite"
print(f"Sans correspondance : {resultat['segment'].isna().mean():.1%}")

Quand les clés n'ont pas le même nom : left_on="client_id", right_on="id". Quand les colonnes homonymes coexistent : suffixes=("", "_client").

groupby : scinder, appliquer, combiner

Le patron conceptuel : scinder la table en groupes, appliquer une agrégation à chaque groupe, combiner les résultats.

df.groupby("segment")["montant"].sum()          # une agrégation
df.groupby("segment")["montant"].agg(["count", "mean", "sum"]) # plusieurs

# Plusieurs clés, agrégations nommées — la forme la plus lisible
recap = df.groupby(["segment", "pays"]).agg(
nb_commandes=("commande_id", "count"),
ca=("montant", "sum"),
panier_moyen=("montant", "mean"),
).reset_index()

reset_index() en sortie transforme le résultat indexé en table plate — presque toujours ce qu'on veut pour la suite (jointure, export, graphique).

Le groupby temporel mérite sa mention : combiné à pd.Grouper ou resample, il produit les séries mensuelles qui peuplent tous les rapports :

ca_mensuel = df.set_index("date")["montant"].resample("ME").sum()

Deux réflexes de fiabilité : vérifier les tailles de groupes (.size()) — une moyenne sur trois lignes n'a pas le poids d'une moyenne sur trente mille — et se rappeler que les NaN sont exclus des agrégations, ce qui peut faire diverger count d'une colonne à l'autre.

pivot_table : le tableau croisé

Pour présenter un agrégat en deux dimensions — lignes × colonnes — comme dans un tableur :

tcd = df.pivot_table(
values="montant",
index="segment", # les lignes
columns="annee", # les colonnes
aggfunc="sum",
fill_value=0,
margins=True, # ligne et colonne "All" de totaux
)

pivot_table est un groupby remis en forme : même moteur, présentation différente. Il agrège les doublons (contrairement à pivot, qui échoue s'il y en a) — c'est presque toujours lui qu'il faut. L'opération inverse, melt, déplie un tableau large en format long, celui que préfèrent Seaborn et la plupart des outils.

Composer : la requête d'analyse complète

Les trois outils s'enchaînent naturellement, dans le style chaîné du module 5 :

top_segments = (
commandes
.merge(clients[["client_id", "segment"]], on="client_id", how="left")
.groupby("segment")
.agg(ca=("montant", "sum"), clients=("client_id", "nunique"))
.assign(ca_par_client=lambda d: d["ca"] / d["clients"])
.sort_values("ca_par_client", ascending=False)
)

Joindre, grouper, dériver, trier : c'est l'équivalent pandas d'une requête SQL complète, et la forme que prendront la plupart de vos analyses. Chaque étape du enchaînement peut être exécutée seule pour inspection — c'est le grand avantage de ce style sur la variable écrasée vingt fois.

Ce qu'il faut retenir

  • merge avec how="left" par défaut ; toujours compter les lignes avant/après et le taux de non-appariés — la démultiplication par clé dupliquée est le bug le plus coûteux du module.
  • groupby = scinder-appliquer-combiner ; agrégations nommées pour la lisibilité, reset_index() en sortie, un œil sur les tailles de groupes.
  • resample pour les agrégats temporels ; pivot_table pour la présentation croisée ; melt pour revenir au format long.
  • Les analyses réelles composent les trois en chaîne inspectable étape par étape.

Au module suivant : la visualisation — transformer ces agrégats en graphiques qui montrent la distribution, la tendance et l'anomalie.