Tableau croisé dynamique Pandas : résumer et restructurer les données comme dans Excel (Guide)
Updated on
Tous les analystes ayant travaillé avec Excel connaissent les tableaux croisés dynamiques. Vous faites glisser un champ dans la zone des lignes, un autre dans la zone des colonnes, choisissez une fonction de synthèse, et une feuille dense de transactions brutes se transforme en un résumé clair montrant des totaux par catégorie, des moyennes par région ou des décomptes par mois. Maintenant, vous devez faire la même chose en Python, mais écrire des appels groupby imbriqués avec unstack paraît maladroit et difficile à lire.
La vraie frustration arrive quand vos données ont plusieurs niveaux de regroupement, que vous avez besoin de sous-totaux, ou que vous voulez appliquer plusieurs fonctions d’agrégation à différentes colonnes en même temps. Enchaîner groupby, agg et unstack produit un code fragile qui casse dès que la forme des données change.
La fonction pandas pivot_table apporte toute la puissance des tableaux croisés dynamiques façon Excel dans Python, avec une API propre et déclarative. Un seul appel de fonction gère le regroupement, l’agrégation, l’indexation multi-niveaux, les sous-totaux et le traitement des valeurs manquantes. Ce guide couvre chaque paramètre, parcourt des exemples pratiques et compare pivot_table avec groupby, pivot et crosstab pour que vous sachiez exactement quel outil utiliser.
Ce que fait pd.pivot_table()
pd.pivot_table() crée un tableau de synthèse de type tableur à partir d’un DataFrame. Vous indiquez quelles colonnes utiliser comme étiquettes de lignes (index), lesquelles utiliser comme en-têtes de colonnes, quelles valeurs agréger et quelle fonction d’agrégation appliquer. Le résultat est un nouveau DataFrame où chaque cellule contient une statistique de synthèse.
Voici un simple avant/après :
Données brutes :
| region | product | revenue |
|---|---|---|
| North | Widget | 1200 |
| North | Gadget | 800 |
| South | Widget | 1500 |
| South | Gadget | 950 |
| North | Widget | 1400 |
| South | Widget | 1600 |
Tableau croisé (somme du revenue par région et produit) :
| region | Gadget | Widget |
|---|---|---|
| North | 800 | 2600 |
| South | 950 | 3100 |
Chaque cellule contient la somme du revenue pour la combinaison région-produit correspondante.
Syntaxe et paramètres de pd.pivot_table()
pd.pivot_table(data, values=None, index=None, columns=None, aggfunc='mean',
fill_value=None, margins=False, dropna=True, margins_name='All',
observed=False, sort=True)Référence des paramètres
| Paramètre | Description | Par défaut |
|---|---|---|
data | Le DataFrame à synthétiser | Obligatoire |
values | Colonne(s) à agréger | None (toutes les colonnes numériques) |
index | Colonne(s) à utiliser comme étiquettes de lignes | None |
columns | Colonne(s) à utiliser comme en-têtes de colonnes | None |
aggfunc | Fonction(s) d’agrégation : 'mean', 'sum', 'count', 'min', 'max', ou une fonction/un dict/une liste | 'mean' |
fill_value | Valeur pour remplacer les NaN dans le résultat | None |
margins | Ajouter les totaux lignes/colonnes (sous-totaux) | False |
margins_name | Nom de la ligne/colonne de totaux | 'All' |
dropna | Exclure les colonnes dont toutes les entrées sont NaN | True |
observed | N’afficher que les catégories observées pour les colonnes catégorielles | False |
sort | Trier le résultat | True |
Données d’exemple pour tous les exemples
Chaque exemple ci-dessous utilise ce dataset de ventes :
import pandas as pd
import numpy as np
sales = pd.DataFrame({
'date': pd.to_datetime(['2025-01-15', '2025-01-20', '2025-02-10', '2025-02-18',
'2025-01-12', '2025-02-22', '2025-01-25', '2025-02-14',
'2025-01-30', '2025-02-05', '2025-01-18', '2025-02-28']),
'region': ['North', 'North', 'North', 'North', 'South', 'South',
'South', 'South', 'East', 'East', 'East', 'East'],
'product': ['Widget', 'Gadget', 'Widget', 'Gadget',
'Widget', 'Gadget', 'Widget', 'Gadget',
'Widget', 'Gadget', 'Widget', 'Gadget'],
'salesperson': ['Alice', 'Alice', 'Bob', 'Bob',
'Charlie', 'Charlie', 'Diana', 'Diana',
'Eve', 'Eve', 'Frank', 'Frank'],
'revenue': [1200, 800, 1400, 850, 1500, 950, 1100, 780, 1300, 900, 1250, 870],
'units': [10, 8, 12, 9, 15, 10, 11, 8, 13, 9, 12, 9]
})
print(sales)Tableau croisé de base : somme du revenue par région
table = pd.pivot_table(sales, values='revenue', index='region', aggfunc='sum')
print(table)Sortie :
revenue
region
East 4320
North 4250
South 4330C’est équivalent à sales.groupby('region')['revenue'].sum(), mais renvoie un DataFrame au lieu d’une Series.
Ajouter des en-têtes de colonnes
Ajoutez le paramètre columns pour ventiler le résumé par une seconde variable :
table = pd.pivot_table(sales, values='revenue', index='region',
columns='product', aggfunc='sum')
print(table)Sortie :
product Gadget Widget
region
East 1770 2550
North 1650 2600
South 1730 2600Désormais, chaque cellule montre le revenue total pour une paire région-produit spécifique.
Fonctions d’agrégation multiples
Passez une liste de fonctions à aggfunc pour calculer plusieurs statistiques à la fois :
table = pd.pivot_table(sales, values='revenue', index='region',
columns='product', aggfunc=['sum', 'mean', 'count'])
print(table)Sortie :
sum mean count
product Gadget Widget Gadget Widget Gadget Widget
region
East 1770 2550 885.0 1275.0 2 2
North 1650 2600 825.0 1300.0 2 2
South 1730 2600 865.0 1300.0 2 2Le résultat a un MultiIndex sur les colonnes, avec la fonction d’agrégation au premier niveau et le produit au second niveau.
Fonctions d’agrégation différentes par colonne
Utilisez un dictionnaire pour aggfunc afin d’appliquer différentes fonctions à différentes colonnes de valeurs :
table = pd.pivot_table(sales, values=['revenue', 'units'], index='region',
columns='product',
aggfunc={'revenue': 'sum', 'units': 'mean'})
print(table)Sortie :
revenue units
product Gadget Widget Gadget Widget
region
East 1770 2550 9.0 12.5
North 1650 2600 8.5 11.0
South 1730 2600 9.0 13.0Le revenue est additionné tandis que les units sont moyennées — exactement ce que vous feriez dans un tableau croisé dynamique Excel avec des fonctions de synthèse différentes selon le champ.
Ajouter des sous-totaux avec margins
Le paramètre margins ajoute une ligne et une colonne montrant les totaux généraux :
table = pd.pivot_table(sales, values='revenue', index='region',
columns='product', aggfunc='sum', margins=True)
print(table)Sortie :
product Gadget Widget All
region
East 1770 2550 4320
North 1650 2600 4250
South 1730 2600 4330
All 5150 7750 12900La ligne All montre le revenue total par produit. La colonne All montre le revenue total par région. La cellule en bas à droite est le total général.
Vous pouvez personnaliser l’étiquette avec margins_name :
table = pd.pivot_table(sales, values='revenue', index='region',
columns='product', aggfunc='sum',
margins=True, margins_name='Total')
print(table)Gérer les valeurs manquantes avec fill_value
Quand certaines combinaisons n’existent pas dans vos données, le tableau croisé contient des NaN. Utilisez fill_value pour les remplacer :
# Remove one row to create a missing combination
sales_missing = sales.drop(index=0)
table = pd.pivot_table(sales_missing, values='revenue', index='region',
columns='product', aggfunc='sum', fill_value=0)
print(table)Cela remplace les NaN par 0, ce qui est plus propre à l’affichage et évite des problèmes dans les calculs en aval.
Index multi-niveaux (regroupement de lignes)
Passez une liste de colonnes à index pour créer des étiquettes de lignes hiérarchiques :
table = pd.pivot_table(sales, values='revenue',
index=['region', 'salesperson'],
columns='product', aggfunc='sum')
print(table)Sortie :
product Gadget Widget
region salesperson
East Eve 900.0 1300.0
Frank 870.0 1250.0
North Alice 800.0 1200.0
Bob 850.0 1400.0
South Charlie 950.0 1500.0
Diana 780.0 1100.0Chaque salesperson est imbriqué sous sa région, donnant une vue “drill-down” des données.
Colonnes multi-niveaux
De la même manière, passez une liste à columns pour obtenir des en-têtes de colonnes hiérarchiques :
sales['month'] = sales['date'].dt.month_name()
table = pd.pivot_table(sales, values='revenue', index='region',
columns=['product', 'month'], aggfunc='sum', fill_value=0)
print(table)Cela crée un en-tête de colonnes à deux niveaux, avec le produit au premier niveau et le mois au second.
Utiliser des fonctions d’agrégation personnalisées
Vous pouvez passer n’importe quel callable à aggfunc, y compris des fonctions lambda et des fonctions NumPy :
# Range (max - min) of revenue by region
table = pd.pivot_table(sales, values='revenue', index='region',
columns='product', aggfunc=lambda x: x.max() - x.min())
print(table)Sortie :
product Gadget Widget
region
East 30 50
North 50 200
South 170 400D’autres agrégations personnalisées utiles :
# Coefficient of variation
table = pd.pivot_table(sales, values='revenue', index='region',
aggfunc=lambda x: x.std() / x.mean() * 100)Exemple concret : analyse des notes d’étudiants
students = pd.DataFrame({
'student': ['Alice', 'Alice', 'Alice', 'Bob', 'Bob', 'Bob',
'Charlie', 'Charlie', 'Charlie', 'Diana', 'Diana', 'Diana'],
'subject': ['Math', 'Science', 'English'] * 4,
'semester': ['Fall', 'Fall', 'Fall', 'Fall', 'Fall', 'Fall',
'Spring', 'Spring', 'Spring', 'Spring', 'Spring', 'Spring'],
'score': [92, 88, 95, 78, 85, 72, 90, 93, 88, 85, 79, 91]
})
# Average score by subject and semester
table = pd.pivot_table(students, values='score', index='subject',
columns='semester', aggfunc='mean', margins=True)
print(table.round(1))Sortie :
semester Fall Spring All
subject
English 83.5 89.5 86.5
Math 85.0 87.5 86.2
Science 86.5 86.0 86.2
All 85.0 87.7 86.3On voit immédiatement que les moyennes du semestre Spring sont légèrement plus élevées au global, et que English montre la plus forte progression.
Exemple concret : rapport mensuel des ventes
# Create a monthly sales summary
sales['month'] = sales['date'].dt.strftime('%Y-%m')
report = pd.pivot_table(sales, values=['revenue', 'units'],
index='region', columns='month',
aggfunc={'revenue': 'sum', 'units': 'sum'},
margins=True, fill_value=0)
print(report)Cela produit le même type de rapport de synthèse mensuel que vous créeriez dans Excel, avec des totaux pour chaque région et chaque mois.
pivot_table vs pivot vs groupby vs crosstab
Ces quatre fonctions se recoupent partiellement, mais chacune a un cas d’usage distinct :
| Fonctionnalité | pivot_table() | pivot() | groupby() | crosstab() |
|---|---|---|---|---|
| Agrégation | Oui (toute fonction) | Non (erreur en cas de doublons) | Oui (toute fonction) | Oui (limité) |
| Gère les doublons | Oui, via aggfunc | Non | Oui | Oui |
| Sous-totaux (margins) | Oui | Non | Non (manuel) | Oui |
| Remplir les valeurs manquantes | Oui (fill_value) | Non | Non | Oui (fill_value) |
| Entrée | DataFrame | DataFrame | DataFrame | Series/tableaux |
| Sortie | DataFrame | DataFrame | DataFrame/Series | DataFrame |
| Index multi-niveaux | Oui | Oui | Oui | Oui |
| Idéal pour | Résumer des données avec agrégation | Restructurer des données clé-valeur à clés uniques | Analyse groupée flexible | Tables de fréquence |
| Fonction par défaut | mean | N/A | N/A | count |
Quand utiliser chacune
Utilisez pivot_table() quand vous avez besoin de résumés façon Excel avec agrégation, sous-totaux ou plusieurs fonctions d’agrégation. C’est l’option la plus puissante et la plus lisible pour créer des tables de synthèse.
Utilisez pivot() lorsque vos données ont des combinaisons uniques de valeurs d’index et de colonnes, et que vous souhaitez simplement les restructurer sans agrégation. C’est plus rapide que pivot_table() car cela saute l’étape d’agrégation.
Utilisez groupby() lorsque vous avez besoin de calculs par groupes mais sans résultat au format “wide”. groupby renvoie par défaut une sortie au format “long”. Vous pouvez obtenir un résultat similaire à pivot_table via groupby().unstack(), mais pivot_table est plus lisible.
Utilisez crosstab() lorsque vous calculez des tables de fréquence ou des tableaux croisés de variables catégorielles. crosstab() accepte directement des Series ou des tableaux (pas uniquement des DataFrames) et, par défaut, compte les occurrences.
Exemple d’équivalence
Ces trois lignes produisent le même résultat :
# pivot_table approach
result1 = pd.pivot_table(sales, values='revenue', index='region',
columns='product', aggfunc='sum')
# groupby + unstack approach
result2 = sales.groupby(['region', 'product'])['revenue'].sum().unstack()
# Both produce the same table
print(result1.equals(result2)) # TrueLa version pivot_table est plus lisible, surtout lorsque vous ajoutez des totaux, des valeurs de remplissage, ou plusieurs fonctions d’agrégation.
Aplatir des colonnes MultiIndex
Après avoir créé un tableau croisé avec plusieurs fonctions d’agrégation, vous obtenez souvent des colonnes MultiIndex difficiles à manipuler :
table = pd.pivot_table(sales, values='revenue', index='region',
columns='product', aggfunc=['sum', 'mean'])
# Flatten the column MultiIndex
table.columns = ['_'.join(col).strip() for col in table.columns.values]
print(table)Sortie :
sum_Gadget sum_Widget mean_Gadget mean_Widget
region
East 1770 2550 885.0 1275.0
North 1650 2600 825.0 1300.0
South 1730 2600 865.0 1300.0Désormais, les colonnes sont des chaînes “plates”, plus simples à référencer.
Trier et filtrer des tableaux croisés
Les tableaux croisés sont des DataFrames ordinaires : vous pouvez donc les trier et les filtrer :
table = pd.pivot_table(sales, values='revenue', index='region',
columns='product', aggfunc='sum', margins=True)
# Sort by total revenue (All column), descending
sorted_table = table.sort_values('All', ascending=False)
print(sorted_table)
# Filter to show only regions with Widget revenue > 2500
filtered = table[table['Widget'] > 2500]
print(filtered)Exporter des tableaux croisés
Enregistrez votre tableau croisé vers Excel (là où les parties prenantes l’attendent) ou vers CSV :
table = pd.pivot_table(sales, values='revenue', index='region',
columns='product', aggfunc='sum', margins=True)
# Export to Excel
table.to_excel('sales_pivot.xlsx', sheet_name='Revenue Summary')
# Export to CSV
table.to_csv('sales_pivot.csv')Conseils de performance
pivot_table() appelle groupby en interne, donc les performances sont similaires. Pour de gros datasets :
| Taille du dataset | Temps attendu |
|---|---|
| 100K lignes, 2 colonnes de group | ~5 ms |
| 1M lignes, 2 colonnes de group | ~50 ms |
| 10M lignes, 3 colonnes de group | ~500 ms |
Stratégies d’optimisation :
- Réduire les données avant de pivoter — filtrez les lignes et ne gardez que les colonnes nécessaires avant d’appeler
pivot_table(). - Utiliser des dtypes
category— convertissez les colonnes de chaînes en dtypecategorypour accélérer le regroupement. - Éviter les aggfuncs en lambda — les noms intégrés (
'sum','mean') utilisent du code C optimisé. Les fonctions lambda retombent sur des boucles Python plus lentes.
# Faster: use categorical dtypes
sales['region'] = sales['region'].astype('category')
sales['product'] = sales['product'].astype('category')
# Faster: use string name instead of lambda
table = pd.pivot_table(sales, values='revenue', index='region',
columns='product', aggfunc='sum') # 'sum' is optimizedVisualiser des tableaux croisés avec PyGWalker
Même si pd.pivot_table() est excellent pour les synthèses numériques, il arrive que vous deviez visualiser les tendances de manière interactive. PyGWalker (opens in a new tab) est une bibliothèque Python open-source qui vous permet de créer des tableaux croisés, des bar charts, des heatmaps, et plus encore via une interface visuelle de type drag-and-drop — aucun code requis après la configuration initiale.
import pandas as pd
import pygwalker as pyg
sales = pd.DataFrame({
'region': ['North', 'North', 'South', 'South', 'East', 'East'] * 2,
'product': ['Widget', 'Gadget'] * 6,
'revenue': [1200, 800, 1500, 950, 1300, 900, 1400, 850, 1100, 780, 1250, 870],
'units': [10, 8, 15, 10, 13, 9, 12, 9, 11, 8, 12, 9]
})
# Launch interactive pivot table and visualization
walker = pyg.walk(sales)PyGWalker vous offre une interface à la Tableau où vous pouvez faire glisser region vers les lignes, product vers les colonnes et revenue vers les valeurs pour créer un tableau croisé visuellement. Vous pouvez passer instantanément de la vue tableau à la vue graphique, tester différentes fonctions d’agrégation et exporter le résultat — le tout sans écrire de code supplémentaire.
Essayez PyGWalker dans Google Colab (opens in a new tab), Kaggle (opens in a new tab), ou installez avec
pip install pygwalker.
FAQ
Quelle est la différence entre pivot et pivot_table dans pandas ?
pivot() restructure les données sans agrégation — il exige des combinaisons uniques de valeurs d’index et de colonnes et lève une erreur si des doublons existent. pivot_table() gère les doublons en les agrégeant via une fonction comme sum ou mean. Utilisez pivot() pour un simple reshape et pivot_table() lorsque vous avez besoin d’agrégation ou de sous-totaux.
Comment ajouter des totaux (margins) à un pivot table pandas ?
Définissez margins=True dans l’appel à pivot_table() : pd.pivot_table(df, values='revenue', index='region', columns='product', aggfunc='sum', margins=True). Cela ajoute une ligne et une colonne All avec des sous-totaux. Personnalisez le libellé avec margins_name='Total'.
Puis-je utiliser plusieurs fonctions d’agrégation dans un pivot table ?
Oui. Passez une liste à aggfunc : aggfunc=['sum', 'mean', 'count']. Cela crée un en-tête de colonnes MultiIndex avec un niveau pour la fonction et un niveau pour les colonnes de valeurs. Vous pouvez aussi passer un dictionnaire pour appliquer des fonctions différentes selon la colonne : aggfunc={'revenue': 'sum', 'units': 'mean'}.
En quoi pivot_table est-il différent de groupby dans pandas ?
Les deux agrègent des données, mais pivot_table() produit un résultat au format “wide” (avec des en-têtes de colonnes issus d’une des variables de regroupement), tandis que groupby() produit par défaut un résultat au format “long”. pivot_table() prend aussi en charge nativement les totals (margins) et le remplissage de valeurs. En interne, pivot_table() utilise groupby().
Comment gérer les valeurs NaN dans un pivot table ?
Utilisez le paramètre fill_value pour remplacer les NaN par une valeur spécifique : pd.pivot_table(df, ..., fill_value=0). Le paramètre dropna=True (par défaut) exclut les colonnes dont toutes les entrées sont NaN.
Puis-je créer un pivot table avec des pourcentages ?
Oui. Créez d’abord le pivot table avec des comptes ou des sommes, puis divisez par le total. Vous pouvez aussi utiliser normalize dans pd.crosstab() pour des tableaux croisés en pourcentage. Pour les pivot tables, calculez manuellement les pourcentages : table = table.div(table.sum(axis=1), axis=0) * 100.
Conclusion
La fonction pivot_table() de pandas est l’outil le plus polyvalent pour créer des tables de synthèse en Python. Voici les points clés à retenir :
- Utilisez
values,indexetcolumnspour définir quoi résumer, comment regrouper les lignes et comment regrouper les colonnes. - Utilisez
aggfuncpour spécifier la fonction d’agrégation. Passez une liste pour plusieurs fonctions, ou un dictionnaire pour des fonctions par colonne. - Utilisez
margins=Truepour ajouter des sous-totaux — l’équivalent de “Grand Total” dans Excel. - Utilisez
fill_valuepour remplacer les combinaisons manquantes par une valeur par défaut (souvent 0). - Préférez
pivot_table()àgroupby().unstack()pour la lisibilité, surtout quand vous avez besoin de totals ou de valeurs de remplissage. - Préférez
pivot()àpivot_table()lorsque vos données ont des combinaisons de clés uniques et que vous n’avez pas besoin d’agrégation. - Préférez
crosstab()pour de simples tables de fréquence sur des variables catégorielles.
Pour explorer vos tableaux croisés de façon interactive, PyGWalker (opens in a new tab) fournit une interface visuelle drag-and-drop qui reproduit l’expérience des tableaux croisés dynamiques Excel dans Jupyter Notebook.