Ancho y largo: pivot_table y melt
En resumen: pivot_table hace una tabla ancha con una columna para cada categoría, y melt devuelve la larga con una fila para cada hecho; la forma la eliges según la pregunta.
Problema
«Dámelo por mes y categoría»
El gerente quiere los ingresos del año en una tabla: los meses hacia abajo, las categorías hacia la derecha, como en Excel. Sabes cómo: unir las líneas con los pedidos, los productos y las categorías (merge) y agrupar por dos claves. Los pedidos cancelados no son ingresos, así que los quitas. La tarjeta de regalo no tiene categoría y recibe un nombre propio, o se caería de los grupos:
Los números son correctos, pero son 84 filas: 12 meses por 7 categorías, una debajo de otra. Para comparar enero con julio, buscas en la lista con los ojos. La tabla que quiere el gerente tiene 12 filas y 7 columnas con los mismos 84 números.
El mes aquí es su número: 1 es enero de 2026, y 11 y 12 son noviembre y diciembre de 2025.
La idea
Larga y ancha
Los mismos números pueden estar en dos formas:
- Larga: una fila para cada hecho. Las columnas son
month,categoryyrevenue, y cada par mes–categoría es una fila aparte. - Ancha: una fila para cada mes y una columna para cada categoría. La categoría ya no es una columna: sus valores se han convertido en nombres de columna.
De larga a ancha, con el resultado de groupby, se hace en una línea: unstack() toma el último nivel del índice (la categoría) y lo convierte en columnas:
La forma es (12, 7): 12 meses y 7 categorías. Enero: 593 € cocina, 2038 € lácteos, 1225 € bebidas.
La idea
Directo a ancha: pivot_table
pivot_table agrupa y dispone a la vez. Le dices cuatro cosas:
index: qué va en las filas (el mes);columns: qué se convierte en columnas (la categoría);values: con qué columna trabaja (los ingresos);aggfunc: cómo lo calcula ('sum').
Ahora se lee de un vistazo. Diciembre es el pico: la cocina hace 3241 €, más de 4 veces un mes normal, y la tarjeta de regalo 2160 €. El verano trae bebidas y fruta: las bebidas son 1756 € en julio frente a 1225 € en enero. Septiembre es flojo casi en todo. Por qué, lo descubriremos en el proyecto.
Más precisamente, para curiosos
También existe pivot sin «table»: solo dispone y no suma. Si un par mes–categoría aparece en más de una fila, pivot se detiene con un error, mientras que pivot_table las combina con aggfunc. Para líneas en bruto necesitas pivot_table.
Pruébalo
Unidades por categoría
¿Cuántas unidades vendió cada categoría cada mes? Guarda en units una tabla con las categorías en las filas y los meses en las columnas.
Solución
import pandas as pd
orders = pd.read_csv('year_orders.csv').rename(columns={'id': 'order_id'})
products = pd.read_csv('products.csv').rename(columns={'id': 'product_id'})
categories = pd.read_csv('categories.csv').rename(columns={'id': 'category_id', 'name': 'category'})
orders = orders[orders['status'] != 'cancelled']
orders['month'] = pd.to_datetime(orders['ordered_at']).dt.month
lines = pd.read_csv('year_items.csv').merge(orders[['order_id', 'month']], on='order_id')
lines = lines.merge(products[['product_id', 'category_id']], on='product_id', how='left')
lines = lines.merge(categories[['category_id', 'category']], on='category_id', how='left')
lines['category'] = lines['category'].fillna('Tarjeta de regalo')
lines['revenue'] = lines['quantity'] * lines['unit_price']
units = lines.pivot_table(index='category', columns='month', values='quantity', aggfunc='sum')
unitslines.pivot_table(index='category', columns='month', values='quantity', aggfunc='sum') da 7 filas y 12 columnas: los lácteos venden 1159 unidades en diciembre. Sin aggfunc la tabla sale con la misma forma, pero las celdas tienen las unidades medias por línea, de 1 a 1,6, porque pivot_table calcula mean por defecto. Parece una respuesta, y eso es lo que la hace peligrosa.
La idea
Celdas vacías: fill_value
En la tabla ancha cada par tiene una celda, aunque los datos no tengan ese par. Aquí está cuánto compró cada cliente de cada categoría en los 60 pedidos:
35 celdas son NaN. El cliente 3 no compró pan, limpieza ni bebidas. Aquí vacío significa «cero compras», no «no lo sabemos». Por eso es correcto sustituirlo por cero: fill_value=0.
Es el caso de la lección sobre resúmenes en el que 0 es el sustituto correcto: el cero significa de verdad cero. Si la celda estuviera vacía porque faltaba un informe, el cero mentiría.
La idea
Los totales: margins
margins=True añade una fila y una columna All con los totales. Aquí se muestran dos de las categorías y el total:
La columna All son los ingresos del mes, y la fila All son los ingresos de la categoría en el año: 11 304 € cocina, 17 811 € bebidas. En la esquina está el total: 111 186,20 €. Septiembre es 7129 €, menos que agosto (8813 €) y octubre (9920 €).
La idea
De vuelta a larga: melt
El gerente envía el plan para los próximos tres meses. La tabla viene de Excel y es ancha: una fila por categoría y una columna por mes.
Una tabla ancha es buena para leer, pero no para trabajar. El mes no es una columna, está en los nombres de las columnas, y no hay nada por lo que agrupar. melt la funde de vuelta en una larga: id_vars es la columna que se queda, var_name pone nombre a la columna con los antiguos nombres de columna, y value_name a la columna con los números:
7 filas y 4 columnas pasan a 21 filas y 3 columnas: cada número del plan es una fila aparte. Ahora el mes es una columna, y groupby funciona:
El plan de diciembre es 16 650 €, y el de noviembre 9700 €.
Pruébalo
Qué forma para qué
Ejercicio
El informe para el gerente
Haz el informe de la primera tarjeta: report con los ingresos por mes (filas) y categoría (columnas), con los totales All. Después guarda en dec_share qué parte de los ingresos del año es diciembre.
Solución
import pandas as pd
orders = pd.read_csv('year_orders.csv').rename(columns={'id': 'order_id'})
products = pd.read_csv('products.csv').rename(columns={'id': 'product_id'})
categories = pd.read_csv('categories.csv').rename(columns={'id': 'category_id', 'name': 'category'})
orders = orders[orders['status'] != 'cancelled']
orders['month'] = pd.to_datetime(orders['ordered_at']).dt.month
lines = pd.read_csv('year_items.csv').merge(orders[['order_id', 'month']], on='order_id')
lines = lines.merge(products[['product_id', 'category_id']], on='product_id', how='left')
lines = lines.merge(categories[['category_id', 'category']], on='category_id', how='left')
lines['category'] = lines['category'].fillna('Tarjeta de regalo')
lines['revenue'] = lines['quantity'] * lines['unit_price']
report = lines.pivot_table(index='month', columns='category', values='revenue', aggfunc='sum', margins=True)
dec_share = report.loc[12, 'All'] / report.loc['All', 'All']
print(round(dec_share * 100, 1))lines.pivot_table(index='month', columns='category', values='revenue', aggfunc='sum', margins=True) da la tabla con los totales. report.loc[12, 'All'] / report.loc['All', 'All'] es cerca de 0,143: diciembre aporta el 14,3 % de los ingresos del año, siendo uno de 12 meses. Sin aggfunc, la esquina tendría la línea media, no los ingresos.
¿Cuál es el plan de los tres meses en total para cada categoría? Haz plan largo (plan_long con las columnas category, month y plan) y guarda el total de cada categoría en per_category.
Solución
import pandas as pd
plan = pd.read_csv('plan.csv')
plan_long = plan.melt(id_vars='category', var_name='month', value_name='plan')
per_category = plan_long.groupby('category')['plan'].sum()
print(per_category)plan.melt(id_vars='category', var_name='month', value_name='plan') da 21 filas, y plan_long.groupby('category')['plan'].sum() da un número por categoría: la cocina son 5050 €, y los lácteos 8700 €. Sin id_vars, la columna category también se funde en las filas, y la tabla tiene 28 filas, con nombres de categoría entre los números.
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Columnas nuevas · Tipos y valores faltantes · Grupos: groupby · Unir tablas: merge
- Continúa en: El primer gráfico · Qué gráfico para qué pregunta · Gráficos honestos · Prueba A/B
- Ejercicios sobre el tema: Preparar los datos