Agrupación y agregación — una respuesta por grupo
Cómo convertir preguntas de «por sucursal», «por categoría» y «por día» en una sola línea de groupby: la clave, el valor y la agregación, varios números a la vez con agregaciones con nombre, size frente a count, dos claves y la trampa de la media de medias.
¿Cómo quieres aprender?
El problema que vamos a resolver
El dueño de la tienda te envía una sola línea: «¿Cuántas unidades vendimos en cada sucursal esta semana?».
Tienes orders.csv: ocho pedidos, una fila por pedido. Con lo aprendido en los cuatro capítulos anteriores, ya puedes responder. Filtra una sucursal, suma su columna, repite:
import pandas as pd
orders = pd.read_csv("orders.csv")
for branch in ["north", "south", "east"]:
units = orders[orders["branch"] == branch]["quantity"].sum()
print(branch, units)north 48
south 6
east 27Los números son correctos. El método tiene tres problemas, y cada uno empeora a medida que los datos crecen.
Primero, tenías que conocer las sucursales de antemano. Escribiste la lista tú mismo. Cuando el archivo de la próxima semana contenga un pedido de una nueva sucursal west, este bucle no lo verá: no fallará, simplemente dejará fuera a west, y los totales ya no cuadrarán con el conjunto completo.
Segundo, cada nueva pregunta es un nuevo bucle. «Ingresos por categoría» requiere un segundo bucle sobre una segunda lista escrita a mano. «Unidades por sucursal y categoría» requiere un bucle dentro de otro bucle.
Tercero, la respuesta no es una tabla. Son líneas de texto impreso. No puedes ordenarla, filtrarla ni colocarla junto a otra respuesta sin reconstruirla.
Lo que realmente quieres es decir, en una sola línea: divide las filas por sucursal, suma la cantidad dentro de cada parte y devélveme esas partes como una tabla. Eso es groupby, y es la forma en que se escribe prácticamente cualquier resumen en pandas: un informe, una cifra para un panel de control, la primera respuesta a «¿de dónde proviene el dinero?».
Al final de este capítulo puedes
- Convertir una pregunta como «unidades por sucursal» en sus tres partes —clave, columna de valores, agregación— y comprobar la clave antes de agrupar
- Escribir
df.groupby(key)[column].sum()y leer la Series que devuelve, con la clave como índice - Calcular varios resúmenes a la vez con
.agg()y con agregaciones con nombre - Distinguir
size()decount(), y decidir qué ocurre con las claves ausentes y los valores ausentes - Agrupar por dos columnas, aplanar o reorganizar el resultado y evitar el error de la «media de medias»
Requisitos previos: Ordenar filas.
El archivo que vamos a usar
El mismo orders.csv de los capítulos anteriores: ocho pedidos de una pequeña tienda. Si no lo tienes, créalo en la carpeta de tu proyecto exactamente con estas líneas:
order_id,date,branch,product,category,quantity,price
1001,2024-03-01,north,pen,stationery,12,15.0
1002,2024-03-01,south,notebook,stationery,5,60.0
1003,2024-03-02,north,bag,accessories,2,850.0
1004,2024-03-02,east,bottle,accessories,7,120.0
1005,2024-03-03,north,eraser,stationery,30,8.0
1006,2024-03-03,south,bag,accessories,1,850.0
1007,2024-03-04,east,pen,stationery,20,15.0
1008,2024-03-04,north,bottle,accessories,4,120.0Una fila es un pedido: de qué sucursal proviene, qué producto, su categoría, cuántas unidades y el precio de una unidad.
Antes de escribir el código
Toda pregunta con agrupaciones tiene el mismo esqueleto. Antes de escribir groupby, descompón la pregunta.
1. Plantea la pregunta en tres partes
Una pregunta con agrupaciones siempre contiene tres elementos, y las palabras de la pregunta te indican cuál es cuál:
- Clave (key) — la columna cuyos valores definen los grupos. Es la palabra que va después de por, según o para cada. En «unidades por sucursal» es
branch. - Valor (value) — la columna que se resume: lo que se está midiendo. Aquí,
quantity. - Agregación (aggregation) — cómo varios valores se convierten en uno: total, promedio, cuántos, el mayor. Aquí,
sum.
Algunos ejemplos más para que el patrón quede claro:
- «Ingresos por categoría» — clave
category, valorrevenue, agregaciónsum - «Tamaño promedio del pedido en cada sucursal» — clave
branch, valorquantity, agregaciónmean - «Cuántos pedidos hubo cada día» — clave
date, sin columna de valores (se cuentan filas), agregaciónsize - «Pedido individual más grande por producto» — clave
product, valorquantity, agregaciónmax
Si no puedes nombrar las tres partes, la pregunta aún no está lista para el código. «¿Qué sucursal es la mejor?» no tiene valor ni agregación: ¿la mejor por unidades?, ¿por ingresos?, ¿por número de pedidos? Pregunta antes de calcular. Las tres respuestas pueden ser sucursales distintas.
2. Mira la entrada antes de agruparla
Agrupar oculta filas. Una vez que los ocho pedidos se convierten en tres totales, una fila rota queda invisible dentro de una suma. Por lo tanto, las comprobaciones se hacen antes:
import pandas as pd
orders = pd.read_csv("orders.csv")
print(orders.shape)
print(orders.dtypes)(8, 7)
order_id int64
date str
branch str
product str
category str
quantity int64
price float64
dtype: objectAquí importan dos cosas. La columna de valor, quantity, es int64 —numérica, por lo que sum sumará en lugar de concatenar texto—. La columna de la clave, branch, es str, lo cual es adecuado para una clave.
Luego observa los valores reales de la clave. Esta es la comprobación que la gente pasa por alto, y es la que más importa:
print(orders["branch"].value_counts())
print(orders[["branch", "quantity"]].isna().sum())branch
north 4
south 2
east 2
Name: count, dtype: int64
branch 0
quantity 0
dtype: int64Tres sucursales distintas, nada ausente en ninguna de las dos columnas. groupby crea un grupo por cada valor distinto, carácter por carácter —por lo que "north" y "north " serían dos grupos—. Ahí es donde value_counts() te lo mostraría, antes de que se convierta en un informe incorrecto.
Esto es lo que ocurre cuando se omite la comprobación. Tres filas que se refieren todas a la sucursal north: una escrita correctamente, una con un espacio al final y otra con mayúscula inicial:
messy = pd.DataFrame({"branch": ["north", "north ", "North"], "quantity": [12, 2, 30]})
print(messy.groupby("branch")["quantity"].sum())
messy["branch"] = messy["branch"].str.strip().str.lower()
print(messy.groupby("branch")["quantity"].sum())branch
North 30
north 12
north 2
Name: quantity, dtype: int64
branch
north 44
Name: quantity, dtype: int64Ningún error, y tres «sucursales» donde debería haber una sola. La cifra de cada grupo es correcta según lo que se le dio a pandas, y el informe sigue estando mal. Limpiar la clave —eliminar los espacios con strip y pasar a minúsculas— antes de agrupar hace que formen un solo grupo con el total real de 44.
3. Esboza la salida antes de generarla
Decide cómo debería verse la respuesta, para que puedas notar cuándo no es así:
branch units
east ?
north ?
south ?A partir de las comprobaciones anteriores ya conoces tres datos sobre ella: tendrá exactamente 3 filas (una por cada sucursal distinta), las sucursales saldrán en orden alfabético (groupby ordena sus claves) y los tres números deben sumar el total de toda la columna. Esto último será tu comprobación posterior.
4. Elige la herramienta
- Un solo número por grupo, a partir de una columna:
df.groupby(key)[col].sum()(omean,max, ...) - Varios números a partir de una columna:
df.groupby(key)[col].agg(["sum", "mean"]) - Varios números a partir de varias columnas, con tus propios nombres:
df.groupby(key).agg(name=(col, "sum"), ...) - Cuántas filas tiene cada grupo:
df.groupby(key).size() - Una columna que aún no existe, como los ingresos: créala primero y luego agrupa
Ahora el código.
Dividir, aplicar, combinar
groupby hace tres cosas en orden, y vale la pena conocer sus nombres porque toda pregunta con agrupaciones consta de estos tres pasos:
- Dividir (split) las filas en partes, una por cada valor distinto de la clave
- Aplicar (apply) un cálculo a cada parte por separado
- Combinar (combine) los resultados en un nuevo objeto, indexado por la clave
Puedes ver la división por sí sola. Se puede iterar sobre un objeto groupby con un bucle, lo que te da cada clave y su parte de la tabla:
for branch, part in orders.groupby("branch"):
print(branch, part.shape, list(part["order_id"]))east (2, 7) [1004, 1007]
north (4, 7) [1001, 1003, 1005, 1008]
south (2, 7) [1002, 1006]Cada parte es un DataFrame normal con las siete columnas: la parte de north tiene cuatro filas y las otras dos tienen dos. Nadie tuvo que escribir los nombres de las sucursales; pandas los encontró en los datos. Un pedido de una nueva sucursal west la próxima semana sería simplemente una cuarta parte.
Rara vez usarás un bucle como este en código real. Está aquí para que la idea quede clara: todo lo que sigue consiste en «hacer algo con cada parte y luego unir las respuestas».
Un número por grupo
La pregunta «unidades por sucursal» en sus tres partes —clave branch, valor quantity, agregación sum— se escribe exactamente en ese orden:
units = orders.groupby("branch")["quantity"].sum()
print(units)branch
east 27
north 48
south 6
Name: quantity, dtype: int64Léelo de izquierda a derecha: agrupa por branch, toma quantity, súmalo. Los mismos números que en el bucle, en una sola línea, sin ninguna lista de sucursales escrita a mano.
Ahora compruébalo frente al boceto. Tres filas: sí. En orden alfabético: sí. Y la prueba:
print(units.sum(), orders["quantity"].sum())81 81Los grupos suman el total, por lo que no se perdió ninguna fila ni se contó ninguna dos veces. Vale la pena convertir esta línea de comprobación en un hábito: siempre que agrupes, compara el total del resultado con el total de la entrada.
El resultado es una Series, y la clave es su índice
print(type(units))
print(units.index)
print(units["north"])<class 'pandas.Series'>
Index(['east', 'north', 'south'], dtype='str', name='branch')
48El paso de combinar colocó las sucursales en el índice, no en una columna. Por eso la salida muestra branch en una línea propia sobre los valores: es el nombre del índice. También significa que buscas un valor por el nombre de la sucursal, units["north"], exactamente como lo harías con .loc en el capítulo cuatro.
Como es una Series normal, todo lo visto en los capítulos anteriores funciona con ella. Ordenarla responde a «¿qué sucursal vendió más?»:
print(units.sort_values(ascending=False))
print(units.idxmax())branch
north 48
east 27
south 6
Name: quantity, dtype: int64
northidxmax() devuelve la etiqueta del valor más grande: la sucursal, no el número. Para saber «quién está en la cima», eso es normalmente lo que buscas.
Por qué eliges la columna primero
Es tentador omitir ["quantity"] y dejar que pandas lo sume todo:
print(orders.groupby("branch").sum())order_id date ... quantity price
branch ...
east 2011 2024-03-022024-03-04 ... 27 135.0
north 4017 2024-03-012024-03-022024-03-032024-03-04 ... 48 993.0
south 2008 2024-03-012024-03-03 ... 6 910.0
[3 rows x 6 columns]Sin errores, y casi cada número en él es un sinsentido. order_id se sumó, como si los números de pedido fueran importes. Las cadenas de date se concatenaron una tras otra, porque «sumar» texto significa unirlo. price también se sumó: el 993.0 de north es la suma de los precios de cuatro productos distintos, lo cual no mide nada.
orders.groupby("branch").sum() no genera ningún error. Suma números de identificación, concatena fechas y totaliza precios unitarios, y devuelve el resultado con la apariencia exacta de un informe.Solo quantity tiene sentido, y podrías haber pedido únicamente eso. Por esta razón la regla es elegir la columna de valores antes de la agregación: sum hace lo que signifique la suma para cada tipo de dato, y para la mayoría de las columnas esa no es una pregunta que nadie haya hecho.
mean es más estricto y se niega a operar en lugar de generar un sinsentido:
print(orders.groupby("branch").mean())TypeError: dtype 'str' does not support operation 'mean'Ese error es la misma equivocación hecha visible. Léelo como: me pediste promediar una columna de texto. La solución no es numeric_only=True —eso seguiría promediando order_id y price—, sino indicar a qué columna te refieres.
Varios números a la vez: .agg()
Un solo resumen rara vez es suficiente. «¿Cuánto vendió cada sucursal, en promedio por pedido, y a lo largo de cuántos pedidos?» son tres agregaciones de la misma columna. Pasa una lista con sus nombres a .agg():
print(orders.groupby("branch")["quantity"].agg(["sum", "mean", "count"]))sum mean count
branch
east 27 13.5 2
north 48 12.0 4
south 6 3.0 2Ahora el resultado es un DataFrame: una columna por agregación, una fila por sucursal. Cuenta una historia que el total por sí solo ocultaba: east vendió poco más de la mitad de unidades que north, pero su pedido promedio es mayor. La sucursal north recibe más pedidos; east recibe menos pedidos, pero ligeramente más grandes.
Los nombres en la lista son cadenas: "sum", "mean", "count", "min", "max", "median", "nunique" (número de valores distintos), "first", "last". Son los mismos métodos que has usado en una sola columna, aplicados a cada parte.
Agregación con nombre: tus columnas, tus nombres
Los informes reales resumen columnas distintas de formas distintas y necesitan nombres de columna legibles. La agregación con nombre (named aggregation) hace ambas cosas en una sola llamada. Cada argumento con nombre es el nombre de una columna de salida, y su valor es un par: (input column, aggregation).
Los ingresos son lo más evidente que se debe informar, y todavía no son una columna: son la cantidad multiplicada por el precio en cada fila. Crear columnas tiene su propio capítulo, el siguiente; por ahora basta con una sola línea, y muestra el paso 4 del plan en acción: si la columna de valores no existe, créala antes de agrupar.
orders["revenue"] = orders["quantity"] * orders["price"]
summary = orders.groupby("branch").agg(
orders=("order_id", "count"),
units=("quantity", "sum"),
revenue=("revenue", "sum"),
)
print(summary)orders units revenue
branch
east 2 27 1140.0
north 4 48 2600.0
south 2 6 1150.0Este es un informe que podrías enviar. Cada línea de la llamada a agg se lee como una frase: orders es el recuento de order_id, units es la suma de quantity, revenue es la suma de revenue. Los nombres de las columnas de salida los eliges tú, no se heredan como sum y count, por lo que no hay que renombrarlas después.
Y esto cambia la respuesta anterior. Por unidades, east era claramente la segunda; por ingresos, south se adelanta: una mochila de 850 supera a veinte bolígrafos. Por eso el paso 1 insistía en nombrar el valor: la «mejor sucursal» por unidades y la «mejor sucursal» por ingresos son preguntas distintas.
El resultado es un DataFrame, por lo que se ordena como tal:
print(summary.sort_values("revenue", ascending=False))orders units revenue
branch
north 4 48 2600.0
south 2 6 1150.0
east 2 27 1140.0Contar: size() frente a count()
«Cuántos pedidos por sucursal» parece una sola pregunta. Pandas tiene dos respuestas, y difieren exactamente cuando faltan datos.
size()cuenta las filas de cada grupo, sin importar lo que contengan.count()cuenta los valores presentes (no ausentes) de una columna en cada grupo.
En el archivo limpio coinciden. Para ver cómo discrepan, aquí hay una pequeña tabla creada a mano junto a orders, con una sucursal ausente y una cantidad ausente:
gaps = pd.DataFrame(
{
"order_id": [1001, 1002, 1003, 1004, 1005],
"branch": ["north", "south", None, "east", "north"],
"quantity": [12, 5, 2, None, 30],
}
)
print(gaps)order_id branch quantity
0 1001 north 12.0
1 1002 south 5.0
2 1003 NaN 2.0
3 1004 east NaN
4 1005 north 30.0(quantity se convirtió en float debido al valor ausente; el capítulo seis explicó por qué).
print(gaps.groupby("branch").size())
print(gaps.groupby("branch")["quantity"].count())branch
east 1
north 2
south 1
dtype: int64
branch
east 0
north 2
south 1
Name: quantity, dtype: int64east tiene un pedido (size = 1), pero cero cantidades conocidas (count = 0). Ambas cosas son ciertas; responden a preguntas distintas. Usa size() para «cuántos pedidos hay» y count() para «de cuántos pedidos tengo realmente una cifra».
Dos cosas que los datos ausentes hacen en silencio
Vuelve a mirar esas salidas. El pedido 1003, el que no tiene sucursal, no está en ninguna de ellas. Entraron cinco pedidos; salieron cuatro:
print(gaps.groupby("branch").size().sum(), len(gaps))4 5Por defecto, groupby descarta las filas cuya clave está ausente. No existe ningún grupo llamado «desconocido», por lo que la fila desaparece de todos los resultados. La comprobación del total que vimos antes detecta esto de inmediato. Si esas filas importan —y un pedido sin sucursal sigue siendo un pedido—, pídelas con dropna=False:
print(gaps.groupby("branch", dropna=False)["quantity"].sum())branch
east 0.0
north 42.0
south 5.0
NaN 2.0
Name: quantity, dtype: float64Ahora los cuatro grupos están presentes, y el grupo NaN contiene las 2 unidades que nadie pudo ubicar.
El segundo comportamiento silencioso está en esa misma salida: east muestra 0.0. La sucursal east no vendió cero; su única cantidad es desconocida. sum trata «nada que sumar» como cero. Si lo «desconocido» debe permanecer desconocido, indícalo con min_count=1: «solo calcula una suma si existe al menos un valor real»:
print(gaps.groupby("branch", dropna=False)["quantity"].sum(min_count=1))branch
east NaN
north 42.0
south 5.0
NaN 2.0
Name: quantity, dtype: float64Cuál es la opción adecuada depende de la pregunta, y esa es la clave: decídelo en el paso de planificación, no por accidente. La solución para ambos problemas suele estar antes de todos modos: limpiar los valores ausentes (capítulo seis) antes de agrupar.
Dos claves: grupos dentro de grupos
«Ingresos por sucursal y categoría» tiene dos claves. Pasa una lista:
by_both = orders.groupby(["branch", "category"])["revenue"].sum()
print(by_both)branch category
east accessories 840.0
stationery 300.0
north accessories 2180.0
stationery 420.0
south accessories 850.0
stationery 300.0
Name: revenue, dtype: float64Una fila por cada combinación que realmente ocurre: seis en este caso. El índice ahora tiene dos niveles, branch y category; pandas llama a esto un MultiIndex, e imprime el nivel exterior solo una vez por bloque para mantener la legibilidad. El espacio en blanco debajo de east significa «sigue siendo east».
Puedes seleccionar elementos de él de la forma esperada:
print(by_both.loc["north"])
print(by_both.loc[("north", "stationery")])category
accessories 2180.0
stationery 420.0
Name: revenue, dtype: float64
420.0Una sola etiqueta te da todo lo de north; una tupla con ambas etiquetas te da un único número.
Recuperar columnas normales
Un MultiIndex es compacto, pero muchos de los pasos siguientes —guardar en CSV, filtrar con una máscara, combinarlo con otra tabla, algo que veremos más adelante— resultan más sencillos con columnas normales. Existen dos formas, y ambas generan la misma tabla:
flat = orders.groupby(["branch", "category"], as_index=False)["revenue"].sum()
print(flat)
print(flat.equals(by_both.reset_index()))branch category revenue
0 east accessories 840.0
1 east stationery 300.0
2 north accessories 2180.0
3 north stationery 420.0
4 south accessories 850.0
5 south stationery 300.0
Trueas_index=False le indica a groupby que no mueva las claves al índice desde un principio; reset_index() las devuelve a las columnas después. Usa la opción que se lea mejor en cada contexto.
Reorganizar para las personas: unstack()
Para una persona que lee el resultado, una cuadrícula es mejor que una lista larga: las sucursales a un lado y las categorías arriba:
print(by_both.unstack())category accessories stationery
branch
east 840.0 300.0
north 2180.0 420.0
south 850.0 300.0unstack() toma el nivel interior del índice, category, y convierte sus valores en columnas. Los números son exactamente los seis de antes. Si alguna sucursal nunca hubiera vendido una categoría, su celda sería NaN; unstack(fill_value=0) escribe 0 en su lugar.
La trampa de la media de medias
Una pregunta tentadora: ¿cuánto cuesta una unidad, en promedio, en cada sucursal? La respuesta rápida es calcular la media de la columna de precios:
print(orders.groupby("branch")["price"].mean())branch
east 67.50
north 248.25
south 455.00
Name: price, dtype: float64Ese es el promedio de los precios en las líneas de pedido, y cada línea cuenta una sola vez: la única mochila de la sucursal south (1 unidad a 850) pesa lo mismo que sus cuadernos (5 unidades a 60). Pero los clientes no compraron una de cada línea: compraron unidades. El precio promedio por unidad vendida es el total de ingresos dividido entre el total de unidades, y debe calcularse a partir de sumas:
per_unit = orders.groupby("branch").agg(
units=("quantity", "sum"),
revenue=("revenue", "sum"),
)
per_unit["avg_unit_price"] = (per_unit["revenue"] / per_unit["units"]).round(2)
print(per_unit)units revenue avg_unit_price
branch
east 27 1140.0 42.22
north 48 2600.0 54.17
south 6 1150.0 191.67La cifra de south cae de 455 a unos 192. Ninguno de los dos números es «incorrecto» como operación aritmética; solo uno responde a la pregunta. La regla: cuando lo que buscas es una proporción o ratio, agrega el numerador y el denominador por separado, y luego divide. Calcular la media de algo que ya es una media, o de un precio que ya es por unidad, pondera cada fila por igual, ya fuera de una unidad o de treinta.
Un ejemplo completo
branch_report.py responde adecuadamente a la pregunta del dueño: por sucursal, número de pedidos, unidades, ingresos, porcentaje sobre los ingresos totales y el producto más vendido en unidades, y comprueba sus propios totales.
import pandas as pd
orders = pd.read_csv("orders.csv")
# 1. Check the input before grouping hides anything.
print("Rows, columns:", orders.shape)
print("Branches:", sorted(orders["branch"].unique()))
print("Missing in key/value:", int(orders[["branch", "quantity", "price"]].isna().sum().sum()))
print()
# 2. Create the value column that does not exist yet.
orders["revenue"] = orders["quantity"] * orders["price"]
# 3. One grouped call, one row per branch.
report = orders.groupby("branch").agg(
orders=("order_id", "count"),
units=("quantity", "sum"),
revenue=("revenue", "sum"),
)
report["share_pct"] = (report["revenue"] / report["revenue"].sum() * 100).round(1)
# 4. Best product per branch: group by both keys, then keep each branch's top row.
by_product = orders.groupby(["branch", "product"], as_index=False)["quantity"].sum()
top = by_product.sort_values("quantity", ascending=False).drop_duplicates("branch")
report["top_product"] = top.set_index("branch")["product"]
report = report.sort_values("revenue", ascending=False)
print(report)
print()
# 5. Prove nothing was lost or double-counted.
print("Revenue check:", report["revenue"].sum(), "==", orders["revenue"].sum())
print("Orders check :", report["orders"].sum(), "==", len(orders))Rows, columns: (8, 7)
Branches: ['east', 'north', 'south']
Missing in key/value: 0
orders units revenue share_pct top_product
branch
north 4 48 2600.0 53.2 eraser
south 2 6 1150.0 23.5 notebook
east 2 27 1140.0 23.3 pen
Revenue check: 4890.0 == 4890.0
Orders check : 8 == 8Por qué está escrito de esta manera:
- Las comprobaciones van primero y se imprimen. Tres sucursales distintas y cero valores ausentes son las condiciones bajo las cuales el informe es fiable. Si el archivo de la próxima semana muestra cuatro sucursales o dos ausentes, lo ves arriba de todo, antes de leer cualquier total.
revenuese crea antes de agrupar. Sumarquantity * pricepor fila y luego agrupar da los ingresos reales. Multiplicar después una cantidad sumada por un precio promediado volvería a ser la trampa de la media de medias.- Una sola llamada a
agg, columnas con nombre. Cada columna de salida se declara una vez, con el nombre que verá quien lea el informe. Añadir una cuarta medida requiere solo una línea más. share_pctse calcula sobre el resultado, no sobre la entrada. Después de agrupar,reportes un DataFrame normal indexado por sucursal, por lo que las operaciones aritméticas habituales entre columnas funcionan con él. Sus porcentajes suman 100.0.- El producto más vendido utiliza una agrupación por dos claves y luego se ordena.
groupby(["branch", "product"])da las unidades por cada par sucursal-producto; ordenar porquantityde forma descendente y conservar la primera fila de cada sucursal (drop_duplicates("branch")) deja el producto más vendido de cada una. Asignartop.set_index("branch")["product"]alinea los productos por la etiqueta de la sucursal, no por su posición: el índice se encarga de hacer la correspondencia. - Las dos últimas líneas son la comprobación. Los ingresos y los recuentos de pedidos de los grupos coinciden con los de la entrada. Si hubiera faltado una sucursal, o si el archivo tuviera una sucursal en blanco, las cifras diferirían y lo sabrías antes de que el informe saliera.
Cuando algo falla
KeyError: 'Branch' La columna clave está escrita de forma distinta a la del archivo: aquí con B mayúscula. Los nombres de columna distinguen entre mayúsculas y minúsculas, y groupby los comprueba de inmediato. Imprime list(orders.columns) y copia el nombre exactamente; las comillas dentro de una lista también delatan espacios finales como 'branch '.
KeyError: 'Column not found: Quantity' El mismo error, un paso después: la clave estaba bien, pero la columna de valor en groupby("branch")["Quantity"] no existe. El mensaje dice «Column not found» en lugar de solo el nombre, lo que te indica que falló la selección posterior a groupby.
KeyError: "Label(s) ['qty'] do not exist" En la agregación con nombre, el primer elemento de cada par debe ser una columna real: units=("qty", "sum") falla porque no existe qty. El nombre de salida a la izquierda (units) puede ser cualquiera; la entrada a la derecha, no.
ValueError: Cannot subset columns with a tuple with more than one element. Use a list instead. Escribiste orders.groupby("branch")["quantity", "price"]. Dos nombres dentro de un único par de corchetes forman una tupla. Para seleccionar varias columnas necesitas una lista —dobles corchetes, exactamente igual que en el capítulo cuatro: orders.groupby("branch")[["quantity", "price"]].sum().
TypeError: dtype 'str' does not support operation 'mean' Una agregación llegó a una columna de texto, ya sea porque no se seleccionó ninguna columna (groupby("branch").mean()) o porque la columna seleccionada se importó como texto. En el segundo caso, info() mostrará str donde esperabas un número; pd.to_numeric(..., errors="coerce") del capítulo seis es la solución.
KeyError: 'quantity' al leer el resultado Después de units = orders.groupby("branch")["quantity"].sum(), las sucursales son el índice de units y quantity es solo su nombre. units["quantity"] busca una sucursal llamada quantity. Usa units["north"], o units.sum() para el total.
Paso 4 de 6 — Predice
Comprueba lo que has entendido
¿Qué se imprime?
from io import StringIO
import pandas as pd
# Stands in for orders.csv, so this snippet runs on its own.
RAW = """order_id,date,branch,product,category,quantity,price
1001,2024-03-01,north,pen,stationery,12,15.0
1002,2024-03-01,south,notebook,stationery,5,60.0
1003,2024-03-02,north,bag,accessories,2,850.0
1004,2024-03-02,east,bottle,accessories,7,120.0
1005,2024-03-03,north,eraser,stationery,30,8.0
1006,2024-03-03,south,bag,accessories,1,850.0
1007,2024-03-04,east,pen,stationery,20,15.0
1008,2024-03-04,north,bottle,accessories,4,120.0
"""
orders = pd.read_csv(StringIO(RAW))
print(orders.groupby("branch")["quantity"].sum())- A branch quantity 0 east 27 1 north 48 2 south 6
- B81
- Cbranch north 48 south 6 east 27 Name: quantity, dtype: int64
- Dbranch east 27 north 48 south 6 Name: quantity, dtype: int64
El objetivo es la cantidad total y el precio total por sucursal. ¿Qué sucede?
from io import StringIO
import pandas as pd
# Stands in for orders.csv, so this snippet runs on its own.
RAW = """order_id,date,branch,product,category,quantity,price
1001,2024-03-01,north,pen,stationery,12,15.0
1002,2024-03-01,south,notebook,stationery,5,60.0
1003,2024-03-02,north,bag,accessories,2,850.0
1004,2024-03-02,east,bottle,accessories,7,120.0
1005,2024-03-03,north,eraser,stationery,30,8.0
1006,2024-03-03,south,bag,accessories,1,850.0
1007,2024-03-04,east,pen,stationery,20,15.0
1008,2024-03-04,north,bottle,accessories,4,120.0
"""
orders = pd.read_csv(StringIO(RAW))
by_branch = orders.groupby("branch")["quantity", "price"].sum()- A`ValueError: Cannot subset columns with a tuple with more than one element. Use a list instead.`
- BFunciona y devuelve una tabla con dos columnas
- C`KeyError: ('quantity', 'price')`
- DDevuelve solo la columna quantity
¿Cuál es la diferencia entre size() y count() después de groupby?
- ANo hay ninguna; son dos nombres para lo mismo
- B`size()` mide la memoria utilizada por cada grupo
- C`size()` cuenta las filas de cada grupo, incluidos los valores ausentes; `count()` cuenta solo los valores presentes
- D`count()` cuenta filas; `size()` cuenta valores distintos
Para responder necesitas una cuenta
Inicia sesión para comprobar tus respuestas
Las preguntas están arriba, y resolverlas mentalmente es lo que de verdad importa. Inicia sesión para ver las respuestas, las explicaciones y las pistas de tres niveles.
Tu turno
El dueño ahora quiere el mismo tipo de respuesta por categoría y por día. Usando orders.csv, escribe category_report.py que haga exactamente estas cuatro cosas:
- Lee el archivo e imprime
shape, la lista ordenada de categorías distintas y el número de valores ausentes encategory,quantityypricejuntos - Crea una columna
revenue, luego construye una tabla con una fila por categoría y estas columnas, ordenadas por ingresos de mayor a menor:orders(número de pedidos),units(cantidad total),revenue(ingresos totales),avg_order_units(cantidad promedio por pedido) yavg_unit_price(ingresos divididos entre unidades, redondeados a 2 decimales) - Imprime los ingresos por día y luego el día con mayores ingresos
- Imprime una línea de comprobación: los ingresos de la tabla por categoría, de la tabla diaria y de todo el archivo
Antes de escribir código, planifica en papel: para las condiciones 2 y 3, indica la clave, el valor y la agregación de cada columna, y anota cuántas filas debe tener cada resultado.
Cuando sea correcto, el programa imprime exactamente esto:
Rows, columns: (8, 7)
Categories: ['accessories', 'stationery']
Missing: 0
orders units revenue avg_order_units avg_unit_price
category
accessories 4 14 3870.0 3.50 276.43
stationery 4 67 1020.0 16.75 15.22
date
2024-03-01 480.0
2024-03-02 2540.0
2024-03-03 1090.0
2024-03-04 780.0
Name: revenue, dtype: float64
Best day: 2024-03-02 with 2540.0
Proof: 4890.0 4890.0 4890.0Luego rómpelo a propósito y observa qué ocurre cada vez:
- Cambia la categoría del pedido 1001 a
"Stationery"(con S mayúscula). ¿Cuántas filas tiene tu tabla ahora y sigue pasando la comprobación? - Reemplaza
"sum"por"mean"paraunits. ¿Qué número cambia y a qué pregunta responde ahora? - Calcula
avg_unit_pricecomo la media depriceen su lugar. ¿Qué categoría varía más y por qué?
Solución
Planifica primero.
- Condición 2: clave
category.orders= recuento deorder_id;units= suma dequantity;revenue= suma derevenue;avg_order_units= media dequantity;avg_unit_price= no es una agregación de una columna —es una proporción o ratio, por lo que se calcula después a partir derevenueyunits—. El archivo tiene dos categorías, por lo que la tabla tiene 2 filas. - Condición 3: clave
date, valorrevenue, agregaciónsum. Cuatro fechas distintas, por lo tanto 4 filas; el mejor día es la etiqueta del índice del valor máximo, que esidxmax(). - La comprobación: los ingresos de ambas tablas deben sumar los 4890.0 del archivo.
category_report.py:
import pandas as pd
orders = pd.read_csv("orders.csv")
print("Rows, columns:", orders.shape)
print("Categories:", sorted(orders["category"].unique()))
print("Missing:", int(orders[["category", "quantity", "price"]].isna().sum().sum()))
print()
orders["revenue"] = orders["quantity"] * orders["price"]
table = orders.groupby("category").agg(
orders=("order_id", "count"),
units=("quantity", "sum"),
revenue=("revenue", "sum"),
avg_order_units=("quantity", "mean"),
)
table["avg_unit_price"] = (table["revenue"] / table["units"]).round(2)
table = table.sort_values("revenue", ascending=False)
print(table)
print()
daily = orders.groupby("date")["revenue"].sum()
print(daily)
print("Best day:", daily.idxmax(), "with", daily.max())
print()
print("Proof:", table["revenue"].sum(), daily.sum(), orders["revenue"].sum())Rows, columns: (8, 7)
Categories: ['accessories', 'stationery']
Missing: 0
orders units revenue avg_order_units avg_unit_price
category
accessories 4 14 3870.0 3.50 276.43
stationery 4 67 1020.0 16.75 15.22
date
2024-03-01 480.0
2024-03-02 2540.0
2024-03-03 1090.0
2024-03-04 780.0
Name: revenue, dtype: float64
Best day: 2024-03-02 with 2540.0
Proof: 4890.0 4890.0 4890.0Dos filas y cuatro filas, tal como se había planificado, y los tres totales de ingresos coinciden. Observa lo que la tabla revela y que ninguna columna por sí sola mostraría: stationery mueve casi cinco veces más unidades, pero accessories aporta casi cuatro veces más dinero.
Lo que muestran los tres fallos inducidos.
- Con
"Stationery"en una fila, la tabla tiene 3 filas:Stationeryes su propio grupo con un solo pedido. La comprobación sigue pasando porque no se perdió ninguna fila; solo se clasificó mal. Por eso la línea con las categorías distintas se imprime al inicio: ahora listaría tres categorías y verías el problema antes de leer cualquier cifra. - Con
"mean",unitspasa a ser 3.5 y 16.75 —lo mismo queavg_order_units—. Ahora responde a «qué tan grande es un pedido típico», no a «cuánto vendimos».avg_unit_pricetambién cambia, porque divide los ingresos entre una cifra incorrecta. - Calcular la media de
priceda para accessories(850 + 120 + 850 + 120) / 4 = 485.0, muy por encima de los 276.43 reales, porque los dos pedidos de mochilas de una y dos unidades pesan lo mismo que los pedidos de botellas de siete y cuatro unidades. Stationery sube de 15.22 a 24.5 —menos drástico solo porque todos sus precios son bajos—. Promediar una cifra por unidad a lo largo de las líneas de pedido es la trampa; dividir sumas es la solución.
Paso 6 de 6
Reto — el quiz del capítulo
Diez preguntas de fácil a difícil. Las últimas son difíciles a propósito.
Inicia sesión para hacer el quiz