Tabla dinámica en Pandas: resume y reestructura datos como en Excel (Guía)
Updated on
Todo analista que haya trabajado con Excel conoce las tablas dinámicas. Arrastras un campo al área de filas, otro al área de columnas, eliges una función de resumen, y una hoja densa de transacciones en bruto se convierte en un resumen limpio que muestra totales por categoría, promedios por región o recuentos por mes. Ahora necesitas hacer lo mismo en Python, pero escribir llamadas groupby anidadas con unstack se siente torpe y difícil de leer.
La frustración real aparece cuando tus datos tienen múltiples niveles de agrupación, necesitas subtotales o quieres aplicar varias funciones de agregación a distintas columnas al mismo tiempo. Encadenar groupby, agg y unstack produce código frágil que se rompe cada vez que los datos cambian de forma.
La función pandas pivot_table trae todo el poder de las tablas dinámicas estilo Excel a Python con una API limpia y declarativa. Con una sola llamada maneja agrupación, agregación, indexación multinivel, subtotales y tratamiento de valores faltantes. Esta guía cubre cada parámetro, recorre ejemplos prácticos y compara pivot_table con groupby, pivot y crosstab para que sepas exactamente qué herramienta usar.
Qué hace pd.pivot_table()
pd.pivot_table() crea una tabla de resumen estilo hoja de cálculo a partir de un DataFrame. Indicas qué columnas usar como etiquetas de fila (index), cuáles como encabezados de columna, qué valores agregar y qué función de agregación aplicar. El resultado es un nuevo DataFrame donde cada celda contiene una estadística de resumen.
Aquí tienes un ejemplo simple del antes y el después:
Datos en bruto:
| region | product | revenue |
|---|---|---|
| North | Widget | 1200 |
| North | Gadget | 800 |
| South | Widget | 1500 |
| South | Gadget | 950 |
| North | Widget | 1400 |
| South | Widget | 1600 |
Tabla dinámica (suma de revenue por region y product):
| region | Gadget | Widget |
|---|---|---|
| North | 800 | 2600 |
| South | 950 | 3100 |
Cada celda contiene la suma de revenue para esa combinación región-producto.
Sintaxis y parámetros 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)Referencia de parámetros
| Parameter | Description | Default |
|---|---|---|
data | El DataFrame a resumir | Obligatorio |
values | Columna(s) a agregar | None (todas las columnas numéricas) |
index | Columna(s) a usar como etiquetas de fila | None |
columns | Columna(s) a usar como encabezados de columna | None |
aggfunc | Función(es) de agregación: 'mean', 'sum', 'count', 'min', 'max', o una función/dict/list | 'mean' |
fill_value | Valor para reemplazar NaN en el resultado | None |
margins | Añadir totales de fila/columna (subtotales) | False |
margins_name | Nombre para la fila/columna de totales | 'All' |
dropna | Excluir columnas cuyas entradas son todas NaN | True |
observed | Mostrar solo categorías observadas para columnas categóricas | False |
sort | Ordenar el resultado | True |
Datos de ejemplo para todos los ejemplos
Cada ejemplo de abajo usa este dataset de ventas:
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)Tabla dinámica básica: suma de revenue por región
table = pd.pivot_table(sales, values='revenue', index='region', aggfunc='sum')
print(table)Salida:
revenue
region
East 4320
North 4250
South 4330Esto es equivalente a sales.groupby('region')['revenue'].sum(), pero devuelve un DataFrame en lugar de una Series.
Añadir encabezados de columna
Agrega el parámetro columns para desglosar el resumen por una segunda variable:
table = pd.pivot_table(sales, values='revenue', index='region',
columns='product', aggfunc='sum')
print(table)Salida:
product Gadget Widget
region
East 1770 2550
North 1650 2600
South 1730 2600Ahora cada celda muestra el revenue total para un par región-producto específico.
Múltiples funciones de agregación
Pasa una lista de funciones a aggfunc para calcular varias estadísticas a la vez:
table = pd.pivot_table(sales, values='revenue', index='region',
columns='product', aggfunc=['sum', 'mean', 'count'])
print(table)Salida:
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 2El resultado tiene un MultiIndex en las columnas, con la función de agregación en el nivel superior y el producto en el segundo nivel.
Funciones de agregación diferentes por columna
Usa un diccionario en aggfunc para aplicar funciones distintas a diferentes columnas de valores:
table = pd.pivot_table(sales, values=['revenue', 'units'], index='region',
columns='product',
aggfunc={'revenue': 'sum', 'units': 'mean'})
print(table)Salida:
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.0Revenue se suma mientras que units se promedia; exactamente lo que harías en una tabla dinámica de Excel con funciones de resumen distintas por campo.
Añadir subtotales con margins
El parámetro margins añade una fila y una columna mostrando los totales generales:
table = pd.pivot_table(sales, values='revenue', index='region',
columns='product', aggfunc='sum', margins=True)
print(table)Salida:
product Gadget Widget All
region
East 1770 2550 4320
North 1650 2600 4250
South 1730 2600 4330
All 5150 7750 12900La fila All muestra el revenue total por producto. La columna All muestra el revenue total por región. La celda inferior derecha es el total general.
Puedes personalizar la etiqueta con margins_name:
table = pd.pivot_table(sales, values='revenue', index='region',
columns='product', aggfunc='sum',
margins=True, margins_name='Total')
print(table)Manejo de valores faltantes con fill_value
Cuando ciertas combinaciones no existen en tus datos, la tabla dinámica contiene NaN. Usa fill_value para reemplazarlos:
# 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)Esto reemplaza NaN con 0, lo cual es más limpio para mostrar y evita problemas en cálculos posteriores.
Índice multinivel (agrupación de filas)
Pasa una lista de columnas a index para crear etiquetas de fila jerárquicas:
table = pd.pivot_table(sales, values='revenue',
index=['region', 'salesperson'],
columns='product', aggfunc='sum')
print(table)Salida:
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.0Cada salesperson queda anidado bajo su region, dando una vista de “drill-down” de los datos.
Columnas multinivel
De forma similar, pasa una lista a columns para encabezados de columna jerárquicos:
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)Esto crea un encabezado de columna de dos niveles con product arriba y month en el segundo nivel.
Uso de funciones de agregación personalizadas
Puedes pasar cualquier callable a aggfunc, incluyendo funciones lambda y funciones de 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)Salida:
product Gadget Widget
region
East 30 50
North 50 200
South 170 400Otras agregaciones personalizadas útiles:
# Coefficient of variation
table = pd.pivot_table(sales, values='revenue', index='region',
aggfunc=lambda x: x.std() / x.mean() * 100)Ejemplo del mundo real: análisis de calificaciones de estudiantes
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))Salida:
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.3Esto muestra inmediatamente que los promedios del semestre Spring son ligeramente más altos en general, y que English presenta la mayor mejora.
Ejemplo del mundo real: reporte mensual de ventas
# 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)Esto produce el mismo tipo de reporte de resumen mensual que crearías en Excel, con totales para cada región y mes.
pivot_table vs pivot vs groupby vs crosstab
Estas cuatro funciones se solapan en capacidades, pero cada una tiene un caso de uso distinto:
| Feature | pivot_table() | pivot() | groupby() | crosstab() |
|---|---|---|---|---|
| Aggregation | Sí (cualquier función) | No (lanza error con duplicados) | Sí (cualquier función) | Sí (limitado) |
| Handles duplicates | Sí, vía aggfunc | No | Sí | Sí |
| Subtotals (margins) | Sí | No | No (manual) | Sí |
| Fill missing values | Sí (fill_value) | No | No | Sí (fill_value) |
| Input | DataFrame | DataFrame | DataFrame | Series/arrays |
| Output | DataFrame | DataFrame | DataFrame/Series | DataFrame |
| Multi-level index | Sí | Sí | Sí | Sí |
| Best for | Resumir datos con agregación | Reestructurar datos con claves únicas | Análisis agrupado flexible | Tablas de frecuencia |
| Default function | mean | N/A | N/A | count |
Cuándo usar cada una
Usa pivot_table() cuando necesites resúmenes estilo Excel con agregación, subtotales o múltiples funciones de agregación. Es la opción más potente y legible para crear tablas de resumen.
Usa pivot() cuando tus datos tengan combinaciones únicas de index y columns y solo quieras reestructurar sin agregación. Es más rápido que pivot_table() porque omite el paso de agregación.
Usa groupby() cuando necesites cálculos agrupados pero no necesites un resultado en formato ancho. groupby devuelve salida en formato largo por defecto. Puedes lograr una salida tipo pivot_table con groupby().unstack(), pero pivot_table es más legible.
Usa crosstab() cuando estés calculando tablas de frecuencia o tablas cruzadas de variables categóricas. crosstab() acepta Series o arrays directamente (no solo DataFrames) y por defecto cuenta.
Ejemplo de equivalencia
Estas tres líneas producen el mismo resultado:
# 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 versión con pivot_table es más legible, especialmente cuando añades margins, fill values o múltiples funciones de agregación.
Aplanar columnas MultiIndex
Después de crear una tabla dinámica con múltiples funciones de agregación, a menudo obtienes columnas MultiIndex difíciles de manejar:
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)Salida:
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.0Ahora las columnas son strings planos más fáciles de referenciar.
Ordenar y filtrar tablas dinámicas
Las tablas dinámicas son DataFrames normales, así que puedes ordenarlas y filtrarlas:
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)Exportar tablas dinámicas
Guarda tu tabla dinámica en Excel (donde los stakeholders la esperan) o en 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')Consejos de rendimiento
pivot_table() llama a groupby internamente, así que el rendimiento es similar. Para datasets grandes:
| Dataset Size | Expected Time |
|---|---|
| 100K rows, 2 group cols | ~5 ms |
| 1M rows, 2 group cols | ~50 ms |
| 10M rows, 3 group cols | ~500 ms |
Estrategias de optimización:
- Reduce datos antes de pivotar: filtra filas y selecciona solo las columnas necesarias antes de llamar a
pivot_table(). - Usa dtypes categóricos: convierte columnas string a dtype
categorypara agrupar más rápido. - Evita aggfuncs con lambda: los nombres string integrados (
'sum','mean') usan código C optimizado. Las funciones lambda caen en bucles Python más lentos.
# 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 optimizedVisualiza tablas dinámicas con PyGWalker
Aunque pd.pivot_table() es excelente para resúmenes numéricos, a veces necesitas visualizar patrones en tus datos de forma interactiva. PyGWalker (opens in a new tab) es una librería open-source de Python que te permite crear tablas dinámicas, gráficos de barras, heatmaps y más mediante una interfaz visual de arrastrar y soltar, sin necesidad de escribir código después de la configuración inicial.
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 te ofrece una interfaz tipo Tableau donde puedes arrastrar region a filas, product a columnas y revenue a valores para crear una tabla dinámica visualmente. Puedes alternar entre vista de tabla y vista de gráfico al instante, probar diferentes funciones de agregación y exportar el resultado, todo sin escribir código adicional.
Prueba PyGWalker en Google Colab (opens in a new tab), Kaggle (opens in a new tab), o instala con
pip install pygwalker.
FAQ
¿Cuál es la diferencia entre pivot y pivot_table en pandas?
pivot() reestructura datos sin agregación: requiere combinaciones únicas de valores de index y columns y lanza un error si existen duplicados. pivot_table() maneja duplicados agregándolos con una función como sum o mean. Usa pivot() para reestructuración simple y pivot_table() cuando necesites agregación o subtotales.
¿Cómo agrego totales (margins) a una tabla dinámica de pandas?
Establece margins=True en la llamada a pivot_table(): pd.pivot_table(df, values='revenue', index='region', columns='product', aggfunc='sum', margins=True). Esto añade una fila y una columna All con subtotales. Personaliza la etiqueta con margins_name='Total'.
¿Puedo usar múltiples funciones de agregación en una tabla dinámica?
Sí. Pasa una lista a aggfunc: aggfunc=['sum', 'mean', 'count']. Esto crea un encabezado de columnas MultiIndex con un nivel para la función y otro para las columnas de valores. También puedes pasar un diccionario para aplicar funciones distintas por columna: aggfunc={'revenue': 'sum', 'units': 'mean'}.
¿En qué se diferencia pivot_table de groupby en pandas?
Ambas agregan datos, pero pivot_table() produce un resultado en formato ancho (con encabezados de columna derivados de una de las variables de agrupación), mientras que groupby() produce salida en formato largo por defecto. pivot_table() también soporta margins (subtotales) y fill values de forma nativa. Internamente, pivot_table() usa groupby().
¿Cómo manejo valores NaN en una tabla dinámica?
Usa el parámetro fill_value para reemplazar NaN con un valor específico: pd.pivot_table(df, ..., fill_value=0). El parámetro dropna=True (por defecto) excluye columnas donde todas las entradas son NaN.
¿Puedo crear una tabla dinámica con valores porcentuales?
Sí. Primero crea la tabla dinámica con recuentos o sumas y luego divide por el total. También puedes usar normalize en pd.crosstab() para tablas cruzadas basadas en porcentajes. Para tablas dinámicas, calcula los porcentajes manualmente: table = table.div(table.sum(axis=1), axis=0) * 100.
Conclusión
La función pivot_table() de pandas es la herramienta más versátil para crear tablas de resumen en Python. Estos son los puntos clave a recordar:
- Usa
values,indexycolumnspara definir qué resumir, cómo agrupar filas y cómo agrupar columnas. - Usa
aggfuncpara especificar la función de agregación. Pasa una lista para múltiples funciones o un diccionario para funciones por columna. - Usa
margins=Truepara añadir subtotales, el equivalente a “Grand Total” en Excel. - Usa
fill_valuepara reemplazar combinaciones faltantes con un valor por defecto (típicamente 0). - Prefiere
pivot_table()frente agroupby().unstack()por legibilidad, especialmente cuando necesitas margins o fill values. - Prefiere
pivot()frente apivot_table()cuando tus datos tengan combinaciones de clave únicas y no necesites agregación. - Prefiere
crosstab()para tablas de frecuencia simples de variables categóricas.
Para una exploración interactiva de tus tablas dinámicas, PyGWalker (opens in a new tab) ofrece una interfaz visual de arrastrar y soltar que replica la experiencia de tabla dinámica de Excel dentro de Jupyter Notebook.