pandas pivot table & melt : remodeler large ↔︎ long
Donnez à vos données la forme qu’exige la question — une matrice de synthèse avec pivot_table, le format long rangé avec melt, et les remodelages de MultiIndex avec stack et unstack.
Remodelez un DataFrame pandas entre format large et format long : construisez une matrice de synthèse avec pivot_table (index, columns, values, aggfunc, margins), passez du large au long avec melt (id_vars, value_vars), étalez et repliez un MultiIndex avec stack et unstack, comptez des catégories avec crosstab, et comprenez pourquoi pivot_table est le choix sûr par défaut face à pivot.
Date de publication
17 juillet 2026
Modifié
18 juillet 2026
AstucePoints clés
pivot_table construit une matrice de synthèse — et agrège, donc elle ne bute jamais sur les doublons.pivot_table(index=, columns=, values=, aggfunc="mean") transforme des lignes en format long en une grille rows × columns d’un seul nombre récapitulatif par cellule ; margins=True ajoute les totaux de lignes et de colonnes.
pivot est pivot_table sans l’agrégation — et il lève une erreur sur les paires ligne/colonne en double. Ne l’employez que lorsque chaque cellule contient déjà exactement une valeur ; sinon pivot_table est le choix sûr par défaut.
melt passe du large au long — le format rangé (tidy) que réclament les tracés et le regroupement.df.melt(id_vars=, value_vars=) empile plusieurs colonnes en une seule paire variable/value ; c’est l’inverse de pivot, et c’est ce qui alimente seaborn et groupby.
stack/unstack remodèlent un MultiIndex. Après groupby([...]).mean(), unstack() étale le dernier niveau d’index en colonnes ; stack() le replie. C’est le passage large↔︎long rapide quand vos données portent déjà un MultiIndex.
crosstab compte des catégories.pd.crosstab(rows, cols) est un tableau de fréquences en un seul appel ; normalize= convertit les effectifs en proportions.
Introduction
Les mêmes données doivent prendre des formes différentes selon la tâche. Un tableau avec une ligne par manchot est parfait pour filtrer et fusionner, mais inadapté à un rapport — un lecteur veut une grille compacte espèce × sexe de masses moyennes, pas 344 lignes. Il est tout aussi inadapté pour tracer un diagramme en barres groupées — seaborn veut une unique colonne longue de valeurs, avec une colonne de regroupement à côté. Mêmes chiffres, trois formes différentes. Le remodelage permet de passer de l’une à l’autre, et sous pandas cela signifie pivot_table, melt, stack/unstack et crosstab.
Cette leçon est la boîte à outils du remodelage telle que vous vous en servez réellement : partez de lignes de manchots rangées (tidy), passez-les au pivot_table pour en faire une matrice de synthèse à glisser dans un rapport, ramenez un tableau large vers le long avec melt pour qu’un tracé puisse le regrouper, étalez et repliez un résultat de groupby avec unstack/stack, et comptez des catégories avec crosstab. Ici les tableaux imprimés sont les figures — chacun est petit et se lit à voix haute, de sorte que vous voyez exactement ce que chaque remodelage a fait à vos données.
Cette leçon s’appuie sur pandas DataFrames (chargement, groupby, agg — nous nous en servons sans les réexpliquer) et sur la leçon voisine Combiner : merge & join — on remodèle souvent un tableau juste après l’avoir fusionné. Les données des manchots sont fournies sous forme d’un petit CSV à côté de cette leçon (issu du jeu de données Palmer Station LTER / palmerpenguins), il n’y a donc rien à télécharger et aucun accès réseau au rendu. Chaque résultat ci-dessous a été produit par le code affiché, et les blocs se construisent dans l’ordre. Pour expérimenter, modifiez la cellule Essayez en direct à la fin et appuyez sur Run.
Le tableau rangé des manchots
Partez de la forme sous laquelle la plupart des données arrivent et restent : le format long (ou rangé, tidy) — une ligne par observation, une colonne par variable. Chaque ligne est un manchot ; les colonnes sont son espèce, son île, ses mesures et son sexe.
import pandas as pdpenguins = pd.read_csv("penguins.csv")print("shape:", penguins.shape)print(penguins[["species", "island", "sex", "body_mass_g"]].head())
shape: (344, 7)
species island sex body_mass_g
0 Adelie Torgersen Male 3750.0
1 Adelie Torgersen Female 3800.0
2 Adelie Torgersen Female 3250.0
3 Adelie Torgersen NaN NaN
4 Adelie Torgersen Female 3450.0
344 lignes, 7 colonnes — une ligne par oiseau. Cette forme est idéale pour filtrer, regrouper et fusionner, mais elle ne répond pas d’un coup d’œil à « quelle est la masse corporelle moyenne de chaque espèce et sexe ? ». Pour cela, il faut disposer les chiffres dans une grille. C’est le premier remodelage.
pivot_table : la matrice de synthèse
pivot_table est le cheval de bataille. Vous nommez trois choses — ce qui descend dans l’index (les lignes), ce qui s’étale sur les columns, et quelles values résumer — plus aggfunc, la fonction qui réduit toutes les lignes concordantes à un seul nombre par cellule. Ici : l’espèce sur le côté, le sexe en haut, la masse corporelle moyenne dans chaque cellule.
sex Female Male
species
Adelie 3369.0 4043.0
Chinstrap 3527.0 3939.0
Gentoo 4680.0 5485.0
Voilà un tableau de rapport. Chaque cellule est la masse corporelle moyenne des oiseaux de cette espèce et de ce sexe — les mâles Gentoo pèsent en moyenne 5485 g, les femelles Adelie 3369 g. pivot_table a pris les nombreuses lignes derrière chaque combinaison espèce/sexe et les a réduites à une seule moyenne, si bien que le résultat est une grille nette 3 × 2 quel que soit le nombre de lignes. C’est là tout l’intérêt d’aggfunc : pivot_table agrège toujours, ce qui explique précisément pourquoi c’est le choix sûr par défaut (voir plus bas).
Ajouter les totaux avec margins=True
Un rapport veut généralement aussi les totaux de lignes et de colonnes. margins=True ajoute une ligne et une colonne All — ici la moyenne globale par sexe, la moyenne globale par espèce, et la moyenne générale dans le coin :
sex Female Male All
species
Adelie 3369.0 4043.0 3706.0
Chinstrap 3527.0 3939.0 3733.0
Gentoo 4680.0 5485.0 5092.0
All 3862.0 4546.0 4207.0
La colonne All donne la moyenne par espèce sur les deux sexes (Gentoo 5092 g), la ligne All la moyenne par sexe sur toutes les espèces (mâles 4546 g, femelles 3862 g), et le coin en bas à droite est la moyenne générale, 4207 g. Notez que les marges sont des moyennes des lignes sous-jacentes, pas des cellules — margins réagrège les données brutes, de sorte qu’un groupe déséquilibré ne peut pas fausser le total.
Résumer de plusieurs façons : une liste d’aggfunc
Passez une liste à aggfunc pour calculer plusieurs résumés d’un coup. Ici la masse moyenne et la taille du groupe, côte à côte :
mean count
sex Female Male Female Male
species
Adelie 3369.0 4043.0 73 73
Chinstrap 3527.0 3939.0 34 34
Gentoo 4680.0 5485.0 58 61
Le résultat a maintenant des colonnes MultiIndex — un niveau supérieur (mean, count) au-dessus du niveau sex. Le bloc count confirme les tailles de groupe derrière chaque moyenne (73 Adelie de chaque sexe, 61 mâles Gentoo), ce qui permet de voir d’un coup d’œil quelles cellules reposent sur peu d’oiseaux. Atteignez un bloc précis avec un tuple : mean_and_n["count"]. (Les effectifs écartent les 11 manchots dont le sex est manquant, car pivot_table ignore les clés NaN par défaut.)
pivot vs pivot_table : pourquoi la version tableau est le choix par défaut
Il existe une fonction plus simple, pivot, qui remodèle du long vers le large sans agréger — elle se contente de placer chaque valeur dans sa cellule row × column. Cela ne fonctionne que si chaque cellule contient exactement une valeur. Nos données brutes comptent de nombreux manchots par espèce/sexe, donc pivot ne peut pas savoir quelle masse mettre dans la cellule Gentoo-mâle :
# pivot() refuses to guess — it raises when a cell would hold more than one valuetry: penguins.pivot(index="species", columns="sex", values="body_mass_g")exceptValueErroras e:print("ValueError:", e)
ValueError: Index contains duplicate entries, cannot reshape
Ce ValueError: Index contains duplicate entries, cannot reshape est l’erreur de remodelage la plus courante, et c’est toute la raison d’être de pivot_table : pivot_table = pivot + une étape d’agrégation, si bien qu’elle réduit les doublons au lieu d’échouer dessus. pivot n’est le bon outil que lorsque vos données sont déjà à une valeur par cellule — par exemple un tableau que vous venez de résumer. La grille des masses moyennes construite plus haut est exactement cela : une fois de retour au format long, pivot reconstruit proprement la grille large. Nous verrons cet aller-retour dans la section suivante.
Règle empirique : si vous remodelez des lignes brutes, utilisez pivot_table ; ne recourez à pivot que lorsque chaque cellule est garantie unique.
melt : du large au long (le format rangé)
melt est le mouvement inverse — il prend un tableau large et empile plusieurs colonnes en deux : l’une contenant les anciens noms de colonnes, l’autre les valeurs. C’est ainsi que l’on obtient des données longues/rangées, la forme que préfèrent groupby et toutes les bibliothèques de tracé. Prenez la matrice des masses moyennes, aplatissez-la de nouveau en un tableau simple, puis appliquez-lui melt :
wide = matrix.reset_index() # species back to a column, sex names as columnsprint("wide:\n", wide, "\n")long= wide.melt( id_vars="species", # keep this column as-is (the identifier) var_name="sex", # the melted column names go here value_name="mean_mass", # their values go here)print("long:\n", long)
wide:
sex species Female Male
0 Adelie 3369.0 4043.0
1 Chinstrap 3527.0 3939.0
2 Gentoo 4680.0 5485.0
long:
species sex mean_mass
0 Adelie Female 3369.0
1 Chinstrap Female 3527.0
2 Gentoo Female 4680.0
3 Adelie Male 4043.0
4 Chinstrap Male 3939.0
5 Gentoo Male 5485.0
id_vars nomme la ou les colonnes à conserver comme identifiants ; toute autre colonne (Female, Male) est dépivotée en lignes. La grille 3 × 2 est devenue 6 lignes longues — une par combinaison espèce-et-sexe — les anciens en-têtes de colonnes vivant désormais dans une colonne sex et les chiffres dans mean_mass. Ce sont des données rangées : une observation par ligne, chaque variable dans sa propre colonne. Et comme chaque paire species/sex n’apparaît maintenant qu’une seule fois, pivot l’inverse parfaitement — long.pivot(index="species", columns="sex", values="mean_mass") renvoie la grille large d’origine. melt et pivot sont inverses l’un de l’autre.
La raison de s’en donner la peine : le format long est ce que veulent les outils de tracé.
import matplotlib.pyplot as pltimport seaborn as snssns.set_style("whitegrid")palette = {"Female": "#3a86d4", "Male": "#f4a259"}fig, ax = plt.subplots(figsize=(7, 4.5))sns.barplot(data=long, x="species", y="mean_mass", hue="sex", palette=palette, ax=ax)ax.set_xlabel("Species")ax.set_ylabel("Mean body mass (g)")ax.set_title("Mean penguin body mass by species and sex")ax.legend(title="Sex", frameon=False)sns.despine()fig.tight_layout()plt.show()
Figure 1
Seaborn a lu la table longue directement : x="species" choisit les groupes, hue="sex" scinde chacun en deux barres — une ligne de la table longue par barre. La figure raconte clairement l’histoire que contenait la matrice : les Gentoo sont bien plus lourds que les Adelie et les Chinstrap, et au sein de chaque espèce les mâles pèsent plus que les femelles. Essayez de tracer cela à partir de la matrice large et vous devrez fournir les colonnes à la main au traceur ; à partir du format long, c’est un seul appel. C’est là le gain concret de melt — remodelez une fois, et le tracé en découle.
stack / unstack : remodeler un MultiIndex
Lorsque vos données portent déjà un MultiIndex — comme en produit un groupby sur plusieurs colonnes — stack et unstack sont le passage large↔︎long le plus rapide. Calculez la masse moyenne par espèce et par île et vous obtenez une Series longue à index sur deux niveaux :
species island
Adelie Biscoe 3710.0
Dream 3688.0
Torgersen 3706.0
Chinstrap Dream 3733.0
Gentoo Biscoe 5076.0
Name: body_mass_g, dtype: float64
C’est long : une valeur par espèce-et-île, indexée par les deux. unstack() étale le dernier niveau d’index (island) sur les colonnes, transformant la Series longue en une grille large :
wide_grid = grouped.unstack()print(wide_grid)
island Biscoe Dream Torgersen
species
Adelie 3710.0 3688.0 3706.0
Chinstrap NaN 3733.0 NaN
Gentoo 5076.0 NaN NaN
Maintenant les espèces descendent sur le côté et les îles s’alignent en haut — la même forme que donnerait pivot_table, mais directement à partir du groupby, sans redéfinir index/columns. Les cellules NaN sont honnêtes : les Chinstrap ne vivent que sur Dream, les Gentoo seulement sur Biscoe, donc ces combinaisons espèce/île n’ont tout simplement aucun oiseau. Pour revenir en arrière, stack() replie les colonnes en lignes et supprime les NaN :
print(wide_grid.stack())
species island
Adelie Biscoe 3710.0
Dream 3688.0
Torgersen 3706.0
Chinstrap Dream 3733.0
Gentoo Biscoe 5076.0
dtype: float64
Vous voilà de retour à la Series longue (moins les combinaisons vides). Utilisez unstack/stack quand les données sont déjà indexées selon l’axe sur lequel vous voulez pivoter ; recourez à pivot_table quand vous partez de colonnes plates et voulez agréger dans la même étape.
crosstab : compter des catégories
Souvent la question est simplement « combien dans chaque combinaison ? » — un tableau de fréquences (ou de contingence). pd.crosstab le fait en un seul appel : passez les catégories de lignes et les catégories de colonnes, et il compte :
island Biscoe Dream Torgersen
species
Adelie 44 56 52
Chinstrap 0 68 0
Gentoo 124 0 0
Chaque cellule est le nombre d’oiseaux de cette espèce sur cette île — les Gentoo sont 124 sur Biscoe et nulle part ailleurs, les Chinstrap 68 sur Dream uniquement, les Adelie répartis sur les trois (44 / 56 / 52). C’est le plan d’échantillonnage de l’étude, lu directement sur les données. crosstab est en réalité un pivot_table avec aggfunc="count" et une signature plus commode pour le cas courant du comptage.
Pour convertir les effectifs en proportions, passez normalize=. normalize="index" fait que chaque ligne somme à 1 — ici, la part de chaque espèce présente sur chaque île :
island Biscoe Dream Torgersen
species
Adelie 0.29 0.37 0.34
Chinstrap 0.00 1.00 0.00
Gentoo 1.00 0.00 0.00
Maintenant la lecture est au sein de chaque espèce : 100 % des Gentoo sont sur Biscoe, 100 % des Chinstrap sur Dream, tandis que les Adelie se répartissent à peu près également (29 % / 37 % / 34 %). Utilisez normalize="columns" pour les parts au sein de chaque île, ou normalize=True pour la part de chaque cellule dans le total général. Un effectif brut répond à « combien » ; un crosstab normalisé répond à « quelle fraction ».
Quel remodelage pour quelle question ?
Choisissez l’outil en fonction de ce que vous cherchez à produire, pas par habitude :
Votre objectif
L’outil
L’appel
Une grille de synthèse (lignes × cols) à partir de données brutes, en agrégeant
Effectifs/proportions de combinaisons de catégories
crosstab
pd.crosstab(rows, cols, normalize=)
En cas de doute : agréger des lignes brutes en une grille → pivot_table ; aplatir un tableau large pour un tracé → melt ; compter des catégories → crosstab. Et rappelez-vous que pivot échoue sur les doublons tandis que pivot_table les agrège — c’est cette différence qui fait trébucher la plupart des gens.
La même idée en R
Vous travaillez en R, ou dans les deux langages ? Les équivalents se trouvent dans la leçon Remodeler des données en R (tidyr) : le melt de pandas correspond au pivot_longer() de tidyr (large → long), et pivot_table/pivot à pivot_wider() (long → large). L’idée des données rangées — une observation par ligne — est la même dans les deux, et c’est pourquoi le format long est la forme que récompensent la plupart des outils.
Essayez en direct
Les blocs ci-dessus ont été exécutés au moment du rendu. Modifiez celui-ci et appuyez sur Run pour remodeler les manchots vous-même — il s’exécute dans votre navigateur via Pyodide (sans installation). Le premier Run télécharge pandas une fois (~20 Mo), après quoi il est mis en cache et instantané.
🟢 Avec un agent IA
Une table dans la mauvaise forme ? Collez un df.head() et dites à Prova la forme qu’il vous faut — « transforme cette table longue en une matrice espèce-par-année des ventes moyennes » ou « melt ces quatre colonnes de mesures en une seule pour tracer. » Elle choisit entre pivot_table, melt et crosstab selon votre objectif, écrit l’appel exact index=/columns=/id_vars=, et vous prévient quand pivot lèverait une erreur sur des doublons. Parce que le runtime est juste là, vous l’exécutez et lisez vous-même la table remodelée au lieu de faire confiance à un appel qui semble correct mais a silencieusement agrégé la mauvaise colonne. The runtime is the judge. Demander à Prova →
Problèmes fréquents
ValueError: Index contains duplicate entries, cannot reshape. Vous avez appelé pivot (et non pivot_table) sur des données comptant plus d’une ligne par paire index/columns, si bien que pandas ne peut pas décider quelle valeur va dans la cellule. Passez à pivot_table et fournissez un aggfunc ("mean", "sum", "count", …) pour réduire les doublons — ou, si chaque cellule doit vraiment être unique, repérez les coupables avec df.duplicated(subset=["index_col", "columns_col"]) et corrigez d’abord les données.
pivot_table n’a rien renvoyé (ou que des NaN). Presque toujours, la colonne values n’est pas numérique et l’aggfunc="mean" par défaut n’avait rien à moyenner — vérifiez le dtype de la colonne, et utilisez aggfunc="count"/"first" pour une colonne non numérique. Les lignes dont la clé index/columns est NaN sont écartées par défaut ; passez dropna=False pour les conserver.
Les colonnes MultiIndex sont malcommodes à sélectionner après un pivot multi-aggfunc. Passer une liste à aggfunc (ou plusieurs values) vous donne deux niveaux d’en-têtes de colonnes. Atteignez un bloc avec un tuple — result["mean"] ou result[("mean", "Male")] — ou aplatissez les en-têtes avec result.columns = ["_".join(c) for c in result.columns] si un tableau à un seul niveau est plus commode à manipuler ensuite.
melt a absorbé une colonne que vous vouliez conserver. Toute colonne non listée dans id_vars est dépivotée dans la colonne value. Si un identifiant a disparu dans le melt, ajoutez-le à id_vars ; pour ne remodeler que certaines colonnes et laisser le reste intact, nommez-les explicitement dans value_vars.
Questions fréquentes
NoteQuelle est la différence entre pivot et pivot_table dans pandas ?
pivot remodèle du long vers le large sans agréger — il faut que chaque paire index/columns corresponde à exactement une valeur, et il lève une ValueError en cas de doublons. pivot_table ajoute une étape d’agrégation (aggfunc, "mean" par défaut), si bien qu’il réduit plusieurs lignes par cellule à un seul nombre récapitulatif et n’échoue jamais sur les doublons. N’utilisez pivot que lorsque vos données sont déjà à une valeur par cellule ; recourez à pivot_table pour des données brutes — c’est le choix sûr par défaut.
NoteComment remodeler un DataFrame pandas du large au long ?
Utilisez df.melt() : nommez les colonnes à conserver comme identifiants avec id_vars=, et pandas empile toutes les autres colonnes en une paire variable/value (renommez-les avec var_name=/value_name=). Pour ne remodeler que certaines colonnes, listez-les dans value_vars=. Le format long — une observation par ligne — est la forme qu’attendent groupby et les bibliothèques de tracé comme seaborn.
NoteComment passer du long au large dans pandas ?
Si chaque cellule index/columns est déjà unique, utilisez df.pivot(index=, columns=, values=). Si plusieurs lignes partagent une cellule et que vous voulez les résumer, utilisez plutôt pd.pivot_table(df, index=, columns=, values=, aggfunc=) — il agrège les doublons. Lorsque les données portent déjà un MultiIndex (par exemple après groupby([...]).mean()), .unstack() étale le dernier niveau d’index en colonnes en un seul appel.
NoteQuelle est la différence entre stack et unstack dans pandas ?
Ce sont des inverses qui remodèlent un MultiIndex. unstack() fait pivoter un niveau d’index vers le haut dans les colonnes (long → large) ; stack() pousse un niveau de colonnes vers le bas dans l’index (large → long). Après df.groupby(["a", "b"]).mean(), .unstack() étale le niveau b en haut, et .stack() le replie — en supprimant les cellules NaN sans données.
NoteComment créer un tableau de fréquences ou de contingence dans pandas ?
Utilisez pd.crosstab(rows, cols) — passez les catégories de lignes et de colonnes et il compte chaque combinaison en un seul appel. Ajoutez normalize="index" (chaque ligne somme à 1), "columns", ou True (part du total général) pour obtenir des proportions plutôt que des effectifs bruts. C’est un pivot_table avec aggfunc="count" et une signature plus simple pour le cas du comptage.
Testez vos connaissances
ImportantÀ vous — remodeler les manchots
Utilisez la table des manchots de la cellule Essayez en direct ci-dessus (penguins = pd.read_csv("penguins.csv")).
Tâche 1. Construisez un pivot_table de la longueur moyenne des nageoires (flipper_length_mm) avec les espèces dans les lignes et les îles dans les colonnes. Quelles cellules espèce/île sont vides, et pourquoi ?
AstuceIndice
pd.pivot_table(penguins, index="species", columns="island", values="flipper_length_mm", aggfunc="mean"). Les cellules vides sont les combinaisons espèce/île sans aucun oiseau.
Les Chinstrap (Dream uniquement) et les Gentoo (Biscoe uniquement) laissent des NaN dans les îles où ils n’ont jamais été échantillonnés — seuls les Adelie apparaissent sur les trois. Les NaN ne sont pas des données manquantes ; ce sont des combinaisons qui n’existent pas.
Tâche 2. Réalisez un crosstab de espèce × sexe en proportions au sein de chaque espèce (normalize="index"). Quel partage attendez-vous en gros, et les données le montrent-elles ?
Chaque espèce est proche d’un partage 50/50 mâle/femelle — l’étude a échantillonné les sexes de façon à peu près égale au sein de chaque espèce, donc chaque ligne est proche de 0.50 / 0.50.
Vérification rapide. Vous appelez penguins.pivot(index="species", columns="sex", values="body_mass_g") sur la table brute et obtenez une ValueError au sujet d’entrées en double. Quel est le correctif ?
AstuceAfficher la réponse
Il y a de nombreux manchots par espèce/sexe, donc pivot ne peut pas mettre une seule valeur dans chaque cellule. Utilisez pivot_table avec un aggfunc (par exemple aggfunc="mean") — il agrège les lignes en double en un seul nombre par cellule au lieu de lever une erreur. pivot ne fonctionne que lorsque chaque paire index/columns est déjà unique.
Conclusion
Vous savez désormais donner à un DataFrame la forme qu’exige la tâche : pivot_table pour une matrice de synthèse (avec margins pour les totaux et une liste d’aggfunc pour plusieurs résumés à la fois), melt pour le format long rangé que veulent le tracé et le regroupement, stack/unstack pour le remodelage rapide d’un MultiIndex, et crosstab pour les effectifs et les proportions. Tout aussi important, vous connaissez le piège — pivot lève une erreur sur les cellules en double tandis que pivot_table les agrège — donc vous choisissez par défaut l’outil qui agrège pour des données brutes. Remodelez tôt, vérifiez que le tableau imprimé a bien la forme voulue, et le reste de l’analyse devient plus facile.
Leçons connexes
Approfondir à partir d’ici :Combiner : merge & join — on remodèle souvent un tableau juste après l’avoir fusionné · pandas DataFrames — le groupby/agg derrière pivot_table et unstack · Remodeler des données en R — les mêmes passages large↔︎long avec pivot_longer/pivot_wider de tidyr.
Prouvez que vous savez le faire. Maîtrisez toute la série Manipulation de données en Python — suivez votre parcours, construisez des projets et obtenez un certificat.