Skip to content

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:

regionproductrevenue
NorthWidget1200
NorthGadget800
SouthWidget1500
SouthGadget950
NorthWidget1400
SouthWidget1600

Tabla dinámica (suma de revenue por region y product):

regionGadgetWidget
North8002600
South9503100

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

ParameterDescriptionDefault
dataEl DataFrame a resumirObligatorio
valuesColumna(s) a agregarNone (todas las columnas numéricas)
indexColumna(s) a usar como etiquetas de filaNone
columnsColumna(s) a usar como encabezados de columnaNone
aggfuncFunción(es) de agregación: 'mean', 'sum', 'count', 'min', 'max', o una función/dict/list'mean'
fill_valueValor para reemplazar NaN en el resultadoNone
marginsAñadir totales de fila/columna (subtotales)False
margins_nameNombre para la fila/columna de totales'All'
dropnaExcluir columnas cuyas entradas son todas NaNTrue
observedMostrar solo categorías observadas para columnas categóricasFalse
sortOrdenar el resultadoTrue

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      4330

Esto 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    2600

Ahora 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      2

El 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.0

Revenue 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  12900

La 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.0

Cada 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     400

Otras 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.3

Esto 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:

Featurepivot_table()pivot()groupby()crosstab()
AggregationSí (cualquier función)No (lanza error con duplicados)Sí (cualquier función)Sí (limitado)
Handles duplicatesSí, vía aggfuncNo
Subtotals (margins)NoNo (manual)
Fill missing valuesSí (fill_value)NoNoSí (fill_value)
InputDataFrameDataFrameDataFrameSeries/arrays
OutputDataFrameDataFrameDataFrame/SeriesDataFrame
Multi-level index
Best forResumir datos con agregaciónReestructurar datos con claves únicasAnálisis agrupado flexibleTablas de frecuencia
Default functionmeanN/AN/Acount

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))  # True

La 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.0

Ahora 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 SizeExpected 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:

  1. Reduce datos antes de pivotar: filtra filas y selecciona solo las columnas necesarias antes de llamar a pivot_table().
  2. Usa dtypes categóricos: convierte columnas string a dtype category para agrupar más rápido.
  3. 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 optimized

Visualiza 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, index y columns para definir qué resumir, cómo agrupar filas y cómo agrupar columnas.
  • Usa aggfunc para especificar la función de agregación. Pasa una lista para múltiples funciones o un diccionario para funciones por columna.
  • Usa margins=True para añadir subtotales, el equivalente a “Grand Total” en Excel.
  • Usa fill_value para reemplazar combinaciones faltantes con un valor por defecto (típicamente 0).
  • Prefiere pivot_table() frente a groupby().unstack() por legibilidad, especialmente cuando necesitas margins o fill values.
  • Prefiere pivot() frente a pivot_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.

📚