Saltar al contenido

lección 6

GroupBy: agrupar y agregar en Python

Dividir, aplicar, combinar: el patrón que resuelve el 80% de las preguntas de negocio con una sola línea de pandas.

50 min

### El montón de facturas encima de la mesa

Ya sabes hacer GROUP BY en SQL: SELECT region, SUM(importe) FROM pedidos GROUP BY region. En pandas existe exactamente el mismo concepto, con la misma lógica de dividir-agregar-combinar, pero con una sintaxis propia que añade un par de trampas (el MultiIndex que aparece al agrupar por dos columnas, el reset_index que siempre se olvida). La buena noticia es que la intuición que ya tienes sirve al cien por cien: si sabes pensar en grupos y funciones de agregación, el salto es solo de sintaxis. Esta lección te da las equivalencias directas entre SQL y pandas, y la pieza que en pandas se escribe de otra forma: aplicar varias funciones a la vez con .agg().

En SQL ya conoces este patrón: es el GROUP BY que llevas usando desde hace semanas. SELECT cliente, SUM(importe) FROM facturas GROUP BY cliente. La lógica es idéntica. Lo que cambia es la sintaxis y un par de trampas que pandas pone en el camino (sobre todo el MultiIndex, que aparece cuando agrupas por dos columnas y de repente tu DataFrame se comporta de forma extraña). Esta lección te enseña a dominar esas trampas para que groupby sea tu herramienta más productiva, no una fuente de frustración.

La historia ayuda a entender por qué existe. Antes de que hubiera bases de datos relacionales, los informes de gestión se hacían a mano: alguien recorría un libro mayor, apuntaba subtotales por departamento en un papel auxiliar y sumaba con calculadora. El modelo relacional que Edgar Codd propuso en 1970 no incluía la agregación — definía cómo seleccionar, proyectar y combinar tablas, no cómo resumirlas —; la agregación por grupos llegó después, como extensión, porque era justo lo que los contables llevaban siglos haciendo a mano y ninguna de las operaciones originales cubría. SQL la formalizó con GROUP BY, y cuando Wes McKinney creó pandas en 2008 la llevó a Python, porque agrupar y sumar es buena parte del trabajo de un analista financiero.

### El patrón: dividir, aplicar, combinar

El nombre técnico del patrón es split-apply-combine (dividir-aplicar-combinar), y fue formalizado por Hadley Wickham en 2011. Suena académico, pero es exactamente lo de las facturas encima de la mesa:

  1. 01.Dividir (split): separar el DataFrame en grupos según el valor de una o más columnas. Cada grupo es un mini-DataFrame independiente.
  2. 02.Aplicar (apply): ejecutar una función de agregación sobre cada grupo. Puede ser una suma, una media, un conteo, el máximo, el mínimo, o cualquier función que tome muchos valores y devuelva uno solo.
  3. 03.Combinar (combine): juntar todos los resultados en un único DataFrame o Series, con una fila por grupo.
Tres pasos, una línea de código: df.groupby("cliente")["importe"].sum()

### Sintaxis básica: groupby + función de agregación

La forma más directa de usar groupby es: df.groupby("columna_de_grupo")["columna_de_valores"].funcion(). Es una sola línea que hace los tres pasos a la vez. La columna de grupo es la que define los montones (por qué criterio separas las facturas). La columna de valores es sobre la que calculas (qué número sumas, promedias o cuentas). Y la función dice QUÉ haces con cada montón.

1import pandas as pd
2
3ventas = pd.DataFrame({
4 'vendedor': ['Ana', 'Luis', 'Ana', 'Luis', 'Ana', 'Luis'],
5 'importe': [200, 150, 300, 50, 100, 200],
6 'producto': ['Laptop', 'Mouse', 'Monitor', 'Teclado', 'Webcam', 'Laptop']
7})
8
9# Total vendido por cada vendedor
10total_por_vendedor = ventas.groupby('vendedor')['importe'].sum()
11print(total_por_vendedor)

groupby básico: total de ventas por vendedor

Las funciones de agregación más habituales son las mismas que en SQL. Aquí tienes las que usarás a diario:

  • .sum() — suma total del grupo. "Cuánto ha vendido cada vendedor este mes."
  • .mean() — media aritmética. "Cuál es el ticket medio por tienda."
  • .count() — número de filas con valor en la columna que le pases; las filas con la columna vacía no se cuentan. "Cuántas transacciones ha hecho cada cliente."
  • .min() / .max() — valor mínimo o máximo del grupo. "Cuál fue la venta más grande de cada región."
  • .nunique() — número de valores únicos. "Cuántos productos distintos vende cada tienda."
  • .median() — la mediana, menos sensible a valores extremos que la media. "Cuál es el pedido típico de cada segmento."

Consejo de senior: count() cuenta filas no nulas de la columna que le pases. Si quieres contar filas del grupo sin importar nulos, usa .size() en lugar de .count(). La diferencia es sutil pero importa cuando hay datos faltantes: count() te dice "cuántas filas tienen valor en esta columna", size() te dice "cuántas filas tiene el grupo, punto".

Cuidado con los nulos en la columna por la que agrupas. groupby los descarta por defecto: si diez pedidos tienen la región vacía, esos diez no salen en el informe por región y nadie te lo dice. El resultado es correcto para cada región y aun así el total no cuadra con el total de la tabla. Dos costumbres: comprobar df["region"].isna().sum() antes de agrupar, y si quieres que salgan, df.groupby("region", dropna=False)["importe"].sum(), que añade un grupo con los nulos.

### Agrupar por varias columnas

En el mundo real, rara vez agrupas por una sola cosa. "Cuánto ha vendido cada vendedor" está bien como primer vistazo, pero el director comercial quiere más detalle: "cuánto ha vendido cada vendedor EN CADA CATEGORÍA". Eso es agrupar por dos columnas a la vez. En SQL escribirías GROUP BY vendedor, categoria. En pandas, pasas una lista:

1import pandas as pd
2
3ventas = pd.DataFrame({
4 'vendedor': ['Ana', 'Ana', 'Ana', 'Luis', 'Luis', 'Luis'],
5 'categoria': ['Electronica', 'Perifericos', 'Electronica', 'Electronica', 'Perifericos', 'Perifericos'],
6 'importe': [1200, 45, 350, 150, 25, 60]
7})
8
9# Agrupar por vendedor Y categoria
10por_vendedor_cat = ventas.groupby(['vendedor', 'categoria'])['importe'].sum()
11print(por_vendedor_cat)
12print()
13print(type(por_vendedor_cat))

Agrupación por dos columnas: el resultado tiene un MultiIndex

El gotcha del MultiIndex: cuando agrupas por varias columnas, esas columnas ya no son columnas — se han convertido en los niveles del índice. Eso rompe lo que las trata como columnas: resultado[resultado["vendedor"] == "Ana"] da KeyError porque "vendedor" ya no está entre las columnas. Se puede trabajar con el índice usando .loc o level=, pero es más código y menos legible. La solución es reset_index() inmediatamente después del groupby: devuelve las columnas de grupo a su sitio y todo vuelve a funcionar como en cualquier otro DataFrame. Lo verás en el apartado siguiente.

### reset_index(): volver a un DataFrame plano

Después de un groupby hay dos cosas que decidir, y son independientes. La primera: si seleccionas una columna — groupby("region")["importe"] — obtienes una Series; si no la seleccionas o usas .agg(), obtienes un DataFrame. La segunda: si agrupas por una columna, el resultado tiene un índice normal; si agrupas por varias, tiene un MultiIndex (un índice con varios niveles). En los cuatro casos, las columnas de grupo se han convertido en el ÍNDICE del resultado y ya no son columnas. Eso es el problema práctico, y para eso existe reset_index().

1import pandas as pd
2
3ventas = pd.DataFrame({
4 'vendedor': ['Ana', 'Ana', 'Ana', 'Luis', 'Luis', 'Luis'],
5 'categoria': ['Electronica', 'Perifericos', 'Electronica', 'Electronica', 'Perifericos', 'Perifericos'],
6 'importe': [1200, 45, 350, 150, 25, 60]
7})
8
9# Sin reset_index: el resultado tiene MultiIndex
10con_multi = ventas.groupby(['vendedor', 'categoria'])['importe'].sum()
11print("Con MultiIndex:")
12print(con_multi)
13print()
14
15# Con reset_index: DataFrame plano, columnas normales
16plano = ventas.groupby(['vendedor', 'categoria'])['importe'].sum().reset_index()
17print("Plano (reset_index):")
18print(plano)

reset_index() convierte el índice jerárquico en columnas normales

reset_index() es el interruptor entre "resultado bonito para leer" y "DataFrame listo para seguir trabajando"

Consejo de senior: casi siempre quieres reset_index() después del groupby. Solo lo omito cuando estoy explorando en un notebook y voy a imprimir el resultado directamente. En código de producción o en cualquier script que alimente un informe, SIEMPRE pongo reset_index(). Es una línea extra que te ahorra una hora de depuración cuando algo falla tres pasos después porque el índice no era el que esperabas.

### .agg(): varias funciones a la vez

Hasta ahora hemos aplicado UNA función por groupby. Pero en la vida real, el director financiero no te pide solo el total por cliente: te pide el total, la media, el número de transacciones y el máximo, todo en la misma tabla. Podrías hacer cuatro groupby separados y luego unirlos con merge, pero eso es ineficiente y tedioso. Para eso existe .agg() (abreviatura de aggregate, agregar): le pasas varias funciones a la vez y te devuelve una columna por cada una.

1import pandas as pd
2
3ventas = pd.DataFrame({
4 'vendedor': ['Ana', 'Ana', 'Ana', 'Luis', 'Luis', 'Luis'],
5 'importe': [200, 300, 100, 150, 50, 200]
6})
7
8# Varias funciones sobre la misma columna
9resumen = ventas.groupby('vendedor')['importe'].agg(['sum', 'mean', 'count', 'max'])
10print(resumen)

.agg() con una lista de funciones: una columna de resultado por cada función

### Funciones distintas para columnas distintas

A veces no quieres aplicar la misma función a la misma columna varias veces, sino funciones DISTINTAS a columnas DISTINTAS. "Del importe quiero la suma; del número de productos quiero la media; del descuento quiero el máximo." En ese caso le das a .agg() un nombre por cada columna que quieres de resultado, y a cada nombre una tupla con la columna de origen y la función:

1import pandas as pd
2
3ventas = pd.DataFrame({
4 'vendedor': ['Ana', 'Ana', 'Ana', 'Luis', 'Luis', 'Luis'],
5 'importe': [200, 300, 100, 150, 50, 200],
6 'unidades': [2, 1, 3, 5, 10, 2],
7 'descuento': [0.05, 0.10, 0.0, 0.15, 0.0, 0.05]
8})
9
10# Funciones distintas para cada columna
11resumen = ventas.groupby('vendedor').agg(
12 total_importe=('importe', 'sum'),
13 media_unidades=('unidades', 'mean'),
14 descuento_max=('descuento', 'max')
15).reset_index()
16
17print(resumen)

Named aggregation: cada columna resultante tiene el nombre que tú decides

Esta sintaxis se llama named aggregation y fue introducida en pandas 0.25 (2019). Antes había que usar un diccionario y después aplanar las columnas a mano, lo cual era un dolor. Si ves código antiguo con .agg({"importe": "sum", "unidades": "mean"}), funciona, pero no te deja elegir el nombre de la columna resultante: se queda con "importe" y "unidades" como nombres, que puede ser confuso cuando la función no es obvia. La forma con tuplas es la recomendada hoy.

### El ejemplo completo: de la pregunta de negocio al DataFrame final

Vamos a juntar todo en un ejemplo que simula una petición real. El escenario: trabajas en una empresa de e-commerce y la responsable de categoría te dice: "Necesito saber, por cada categoría de producto, cuántos pedidos hay, el importe total, el importe medio y cuántos clientes distintos compran. Lo necesito para la reunión de mañana." Eso son cuatro métricas agrupadas por una columna. Veamos cómo se resuelve de principio a fin:

1import pandas as pd
2
3pedidos = pd.DataFrame({
4 'pedido_id': [1, 2, 3, 4, 5, 6, 7, 8],
5 'cliente': ['Ana', 'Luis', 'Ana', 'Marta', 'Luis', 'Ana', 'Marta', 'Luis'],
6 'categoria': ['Electronica', 'Electronica', 'Hogar', 'Hogar', 'Electronica', 'Hogar', 'Electronica', 'Hogar'],
7 'importe': [1200, 350, 45, 80, 60, 150, 500, 30]
8})
9
10resumen_categoria = pedidos.groupby('categoria').agg(
11 num_pedidos=('pedido_id', 'count'),
12 importe_total=('importe', 'sum'),
13 importe_medio=('importe', 'mean'),
14 clientes_unicos=('cliente', 'nunique')
15).reset_index()
16
17print(resumen_categoria.to_string(index=False))

Un informe completo para la reunión de mañana, en seis líneas

El trabajo del analista: traducir una pregunta de negocio en una operación de datos y devolver una respuesta que informe una decisión

### Equivalencia con SQL: la tabla que llevas en la cabeza

Si ya dominas GROUP BY en SQL, la traducción a pandas es casi mecánica. La única diferencia real es que en SQL escribes todo en una sola sentencia (SELECT ... FROM ... GROUP BY) y en pandas encadenas métodos. Aquí tienes la equivalencia directa para que no tengas que adivinar:

  • SELECT col_grupo, SUM(col_valor) FROM tabla GROUP BY col_grupo equivale a df.groupby("col_grupo")["col_valor"].sum().reset_index()
  • **SELECT col_grupo, COUNT(*) FROM tabla GROUP BY col_grupo equivale a df.groupby("col_grupo").size().reset_index(name="count")**
  • SELECT col1, col2, AVG(val) FROM tabla GROUP BY col1, col2 equivale a df.groupby(["col1", "col2"])["val"].mean().reset_index()
  • HAVING (filtrar DESPUÉS de agregar) equivale a filtrar el DataFrame resultante: resultado[resultado["total"] > 1000]

Nota que en pandas no hay una palabra HAVING, pero sí su forma de hacer lo mismo: filtrar el DataFrame resultante después de agregar. Primero agrupas y agregas, luego filtras el resultado. Es más intuitivo que la sintaxis de SQL, donde HAVING es un WHERE que va después del GROUP BY y hay que recordar que no se puede usar WHERE ahí.

### Resumen: el flujo en cuatro pasos

  1. 01.Identifica la pregunta: qué métrica quieres, agrupada por qué dimensión.
  2. 02.Escribe el groupby: df.groupby("dimension") o df.groupby(["dim1", "dim2"]).
  3. 03.Aplica la agregación: .sum(), .mean(), .agg(...) según lo que necesites.
  4. 04.Limpia el resultado: .reset_index() para tener un DataFrame plano listo para filtrar, exportar o visualizar.

Regístrate para guardar tu progreso.

## comentarios

Reporta erratas, ayuda a otros o comparte tu opinión. Sé constructivo.

Inicia sesión para comentar y responder.

cargando comentarios...