1. Faro

Nivel 4 · ¿Por qué se duplicaron los ingresos de envío?

La solicitud

De Laura Bennett, Directora de Finanzas

“El informe de un compañero dice que cobramos casi el doble en gastos de envío que lo que muestra el proveedor de pagos. ¿Puedes encontrar el problema en su código?”

Tu objetivo

Elige la explicación del total inflado.

El informe ejecuta `orders.merge(order_items, on="order_id")["shipping_fee"].sum()`. ¿Por qué el resultado es mucho mayor que `orders["shipping_fee"].sum()`?

Elige una respuesta y luego compruébala.

La lección

La trampa del fan-out

Cuando unes una tabla donde cada clave aparece una vez (orders: una fila por pedido) con una tabla donde aparece muchas veces (order_items: varias líneas por pedido), el resultado tiene una fila por coincidencia. Cada columna del lado “uno” se repite en cada una de sus filas coincidentes.

Eso no es un error de pandas; es lo que es un join. Se convierte en error cuando luego sumas una columna que pertenecía al lado “uno”:

merged = orders.merge(order_items, on="order_id")
len(orders)          # 12000
len(merged)          # about 30000
merged["shipping_fee"].sum()   # counts each order's fee once per line

Un pedido con tres líneas y un gasto de envío de $6.95 aporta $20.85 en lugar de $6.95. No sale ningún error, y el resultado parece una cifra plausible, solo que demasiado grande.

Cómo detectarlo

  • Número de filas. Si un merge hace crecer la tabla, pregúntate qué columnas se repiten ahora.
  • Grano. Antes de sumar una columna, pregunta “¿qué representa una fila de esta tabla, y esta columna se mide en ese grano?” El envío es por pedido; la cantidad es por línea.
  • Concilia. Compara los totales con la tabla de origen. Los gastos de envío sumados en la tabla unida deberían ser iguales a orders["shipping_fee"].sum(). Cuando no lo son, has encontrado un fan-out.

Cómo arreglarlo

Suma cada medida en su propio grano y luego combina:

shipping = orders["shipping_fee"].sum()                     # order grain
units = order_items["quantity"].sum()                       # line grain

Si necesitas filtrar pedidos por algo que está en las líneas (por ejemplo, pedidos que contienen un producto de Iluminación), encuentra primero los IDs de pedido que cumplen, y luego suma en orders:

lighting_ids = order_items.merge(products, on="product_id").loc[
    lambda d: d["category"] == "Lighting", "order_id"
].unique()
orders.loc[orders["order_id"].isin(lighting_ids), "shipping_fee"].sum()

O agrega el lado “muchos” hasta el grano del lado “uno” antes de unir, de modo que el merge sea one-to-one. Harás exactamente eso en el siguiente nivel.

Fan-out con claves duplicadas

Un segundo tipo de fan-out ocurre cuando una tabla que crees “una por clave” no lo es. Si customers tuviera dos filas con el mismo customer_id, cada pedido de ese cliente se duplicaría con un merge. validate="many_to_one" lo detecta de inmediato, por eso vale la pena añadirlo a cada merge donde esperas claves únicas en un lado.

Este es uno de los errores más comunes de los analistas y una de las preguntas de entrevista más comunes sobre joins. Saber explicarlo con claridad, con el grano de cada tabla, vale tanto como conocer la sintaxis.

Elige qué puede ejecutarse en este sitio. Puedes cambiarlo cuando quieras desde "Configurar cookies", al pie de cada página.