La idea
Casi todos los informes son "algo por algo": envío por canal, ingresos por pedido, productos por categoría. groupby responde a todos con el mismo patrón de tres pasos, llamado dividir, aplicar, combinar (split-apply-combine).
Dividir las filas en grupos según el valor de una columna (o de varias). Aplicar a cada grupo por separado un cálculo que lo reduzca a un único valor. Combinar esos valores en una tabla nueva con una fila por grupo.
Figura 1
orders.groupby('channel')['shipping_fee'].sum(), un grupo a la vez
| order_id | channel | shipping_fee |
|---|---|---|
| 1003 | marketplace | 4.99 |
| 1005 | web | 0.00 |
| 1007 | marketplace | 7.65 |
| 1009 | app | 0.00 |
| 1010 | web | 6.95 |
| 1012 | app | 0.00 |
| channel | shipping_fee |
|---|---|
| marketplace | 12.64 |
| web | 6.95 |
| app | 0.00 |
- Seis pedidos con un canal y un coste de envío. La pregunta: ¿cuánto envío por canal?
- Dividir.
groupby('channel')reparte las filas en montones según el valor del canal. Marketplace recibe los pedidos 1003 y 1007. Las otras filas quedan por ahora en sus propios montones. - Aplicar.
.sum()reduce el montón de marketplace a un número: 4.99 + 7.65 = 12.64. - Lo mismo ocurre con web (0.00 + 6.95) y con app (0.00 + 0.00). Cada montón se procesa por separado y nunca ve a los demás.
- Combinar. Una fila por grupo, con el valor del grupo como etiqueta de la fila. Seis filas pasaron a tres. El resultado es una tabla nueva, lista para ordenar o graficar.
La imagen que debes conservar son los montones. groupby('channel') no calcula nada. Reparte las filas en montones, uno por canal, y recuerda en qué montón está cada fila. Solo la llamada siguiente, .sum(), .mean() o .size(), hace aritmética, y la hace dentro de cada montón por separado.
Por qué existe
En Python a secas esto es el bucle de conteo del capítulo 2 con un diccionario de listas, escrito a mano para cada pregunta: crear un diccionario vacío, recorrer cada fila, añadirla a su montón y luego recorrer los montones. En SQL es GROUP BY. En una hoja de cálculo es una tabla dinámica o un SUMIF por grupo. La idea es siempre la misma. En pandas es una línea, y el resultado es un DataFrame que puedes ordenar, filtrar, graficar o combinar con otra tabla.
La otra razón es el volumen. La tienda tiene 30.000 líneas de pedido. Nadie puede revisarlas buscando las cestas más grandes, y nadie debería: todo el trabajo es "dale un total a cada pedido y luego mira los totales".
Cómo funciona por dentro
De qué se hacen los grupos
groupby recibe un nombre de columna, o una lista de ellos. Con dos claves, groupby(['channel', 'status']), existe un montón por cada combinación que de verdad ocurre en los datos: marketplace y cancelled, web y completed, y así. Una combinación que nunca sucede simplemente no tiene montón. Los valores de la clave pasan a ser el índice del resultado, así que un resultado con dos claves tiene dos niveles de índice. reset_index() los devuelve a columnas normales, que suele ser lo que quieres antes de ordenar o combinar.
Qué es una agregación
Una agregación reduce muchos valores a uno: sum, mean, min, max, size, count, nunique. La elección decide qué significa la pregunta. size() cuenta las filas de cada montón, incluidas las que tienen vacíos. count() cuenta los valores presentes de una columna. sum() los suma. Para "cuántos pedidos por canal" usa size. Para "cuántas unidades vendidas por producto" usa sum sobre la cantidad, no count, porque count devuelve el número de líneas.
Primero la columna nueva, luego agrupar
Agrupa lo correcto. La tienda guarda cantidad, precio unitario y descuento en cada línea, no el dinero, así que el primer nivel del capítulo es calcular line_revenue con una expresión vectorizada, exactamente como en los Fundamentos. Solo entonces tiene sentido agrupar.
Figura 2
Primero una columna nueva, luego agrupar: ingresos por pedido
| order_id | quantity | unit_price | discount_pct | line_revenue |
|---|---|---|---|---|
| 1002 | 4 | 11.99 | 0.0 | 47.96 |
| 1002 | 1 | 149.00 | 0.0 | 149.00 |
| 1002 | 2 | 19.99 | 0.0 | 39.98 |
| 1002 | 2 | 25.99 | 0.0 | 51.98 |
| 1003 | 1 | 92.99 | 10.0 | 83.69 |
| 1003 | 1 | 349.00 | 10.0 | 314.10 |
| order_id | revenue | n_lines |
|---|---|---|
| 1002 | 288.92 | 4 |
| 1003 | 397.79 | 2 |
- Harbor & Pine guarda cantidad, precio y descuento en cada línea, pero no el dinero.
line_revenuetodavía no existe en los datos, así que una suma de ella tampoco. - Cálculala primero, para todas las filas a la vez:
quantity * unit_price * (1 - discount_pct / 100), redondeada a centavos. Ahora cada línea lleva sus propios ingresos. - Luego
groupby('order_id')divide las líneas en un montón por pedido, y una agregación reduce cada montón. El pedido 1002 tiene cuatro líneas, así que cuatro ingresos suman 288.92. - El pedido 1003 tiene dos líneas: 83.69 + 314.10 = 397.79. El mismo paso dio también
n_lines, que cuenta las filas de cada montón: 4 y 2.
Fíjate en la granularidad. La tabla de origen tiene una fila por línea de pedido, y el resultado tiene una fila por pedido. Agrupa por otra clave y obtienes otra granularidad: por product_id da ingresos por producto, y por channel necesitaría primero el canal del pedido, que está en orders, no en order_items. Cuando una clave vive en otra tabla, primero combinas, que es el capítulo 7.
Agregación con nombre
.agg(revenue=('line_revenue', 'sum'), n_lines=('order_item_id', 'size')) calcula varias agregaciones a la vez y nombra cada columna resultante al crearla. La forma es nuevo_nombre=(columna, función). Obtienes exactamente los nombres de columna que quieres, sin encabezados de varios niveles que desenredar, y el código se explica solo: cualquiera puede leer qué es revenue.
Filtrar después de agregar
Filtrar las filas antes de agrupar y filtrar los grupos después son preguntas distintas. "Pedidos de la web" es un filtro de filas, que se aplica primero. "Departamentos con al menos siete personas" es un filtro de grupos: no puedes saber el tamaño de un departamento hasta contarlo, así que agregas primero y luego filtras la tabla resumen con una máscara, como en el capítulo 5.
Figura 3
Un groupby como cadena de pasos
-
order_items.assign(line_revenue=...)una columna que sumar -
.groupby('order_id')dividir -
.agg(revenue=('line_revenue', 'sum'))aplicar y combinar -
[lambda t: t['revenue'] > 1000]filtrar los grupos -
.sort_values('revenue')ordenar el resultado
- Prepara los datos: asegúrate de que la columna que quieres agregar existe, fila a fila.
groupbypor sí solo no hace aritmética. Solo recuerda qué filas van juntas.aggreduce cada grupo a una fila. La agregación con nombre,revenue=('line_revenue', 'sum'), nombra la columna resultante e indica qué columna y qué función.- El resultado es un DataFrame normal con una fila por pedido. Filtrarlo ahora conserva pedidos, no líneas. Así se piden los pedidos de más de $1,000.
- Después se puede ordenar como cualquier otra tabla para encontrar las cestas más grandes.
Lee una cadena así desde arriba: cada paso toma la tabla que creó el anterior. Si un número sale mal, ejecuta la cadena paso a paso y mira la tabla tras cada uno, para encontrar el paso donde se torció.
Una comprobación antes de creer en la tabla
Dos comprobaciones rápidas atrapan la mayoría de los errores de groupby. Primero, el total del resultado debe coincidir con el total del origen: result['revenue'].sum() frente a order_items['line_revenue'].sum(). Si los números difieren, se perdieron filas, normalmente porque la clave tiene valores vacíos, que groupby descarta por defecto. Segundo, el número de filas del resultado debe ser igual al nunique() de la clave. Compara, por ejemplo, los 12.000 pedidos de la tienda con las filas que salen de agrupar las líneas por order_id. Una diferencia te avisa de que algo anda mal antes de que lo note nadie más.
Errores comunes
"groupby me calcula el promedio"
groupby solo devuelve un objeto que no ha calculado nada, e imprimirlo muestra algo como <DataFrameGroupBy object>. El cálculo viene del método que llamas después. Eso también significa que los mismos grupos pueden responder muchas preguntas: mean, sum y size son tres reducciones distintas de montones idénticos.
"count() y sum() son intercambiables"
count() devuelve cuántos valores hay y sum() los suma. En una columna de cantidades, count() da el número de líneas y sum() da las unidades vendidas. Una lista de más vendidos hecha con count ordena los productos por en cuántos pedidos aparecen, no por cuántos se vendieron, y se ve totalmente creíble. Plantea primero la pregunta con palabras (pedidos, líneas, unidades o dinero) y luego elige la función.
"La columna de agrupación es una columna más del resultado"
Por defecto la clave de agrupación pasa a ser el índice del resultado, no una columna. Por eso a un resultado de groupby a menudo parece "faltarle" la primera columna, y por eso result['channel'] lanza un KeyError. Llama a reset_index() o pasa as_index=False cuando quieras la clave como columna normal.
Ponte a prueba
Una pregunta. Al responderla, la lección queda marcada en el mapa; nunca bloquea un nivel.
No del todo. Son reducciones distintas del mismo grupo. Una cuenta filas, la otra suma los valores.Prueba otra respuesta.
Correcto. count() cuenta cuántos valores hay en el grupo, aquí uno por línea. sum() suma las cantidades. Elegir count cuando querías sum es un clásico ranking de más vendidos equivocado.
No del todo. Al revés. count() nunca suma valores, los cuenta.Prueba otra respuesta.
No del todo. count() no busca valores distintos (eso es nunique()), y el sum() de la cantidad son unidades, no dinero.Prueba otra respuesta.
Lección hecha: ya está marcada en el mapa.