Combinar DataFrames — apilar con concat, cruzar con merge
Juntar tablas: apilar meses de los mismos datos con concat y añadir información de otra tabla con merge —elegir el tipo de merge, ver qué filas no coincidieron y detectar la clave duplicada que multiplica filas en silencio.
¿Cómo quieres aprender?
El problema que vamos a resolver
Los datos de la tienda no residen en un solo archivo. Nunca lo hacen.
Los pedidos de marzo están en orders.csv, la tabla que has utilizado desde el capítulo cuatro. Los pedidos de abril llegaron esta semana como una exportación independiente, orders_april.csv. Lo que cada producto le cuesta a la tienda —y qué proveedor lo vende— lo gestiona el equipo de compras en su propio archivo, products.csv. Nadie incluyó el coste en el archivo de pedidos, porque el coste es un dato propio de un producto, no de un pedido.
Ahora la dueña de la tienda hace una pregunta: ¿cuánto beneficio aportó cada proveedor a lo largo de marzo y abril?
Ningún archivo por sí solo puede responder a eso. El beneficio requiere la cantidad y el precio de venta (en los pedidos), el coste (en el archivo de productos) y el proveedor (también en el archivo de productos). Y los pedidos están repartidos en dos meses.
En una hoja de cálculo copiarías las filas de abril debajo de las de marzo, luego escribirías un VLOOKUP en una columna nueva para traer el coste de cada fila, arrastrarías hacia abajo y esperarías lo mejor. Pandas hace esos dos mismos trabajos en dos líneas:
- apilar tablas que contienen el mismo tipo de filas —marzo y abril— en una sola tabla más larga:
pd.concat - cruzar cada fila de una tabla con la fila correcta de otra mediante una columna compartida —cada pedido con su producto— y traer las columnas de la otra tabla:
pd.merge
Las líneas son cortas. Lo que hace relevante a este capítulo es lo que puede salir mal sin ningún error: un merge puede descartar silenciosamente pedidos cuyo producto falte en la tabla de búsqueda, o duplicar silenciosamente pedidos cuando la tabla de búsqueda incluye un producto dos veces. En ambos casos se obtiene un total que parece perfectamente razonable y es incorrecto. Por eso la mayor parte de este capítulo trata de saber, antes y después del merge, exactamente cuántas filas deberías tener —y comprobarlo.
Al final de este capítulo puedes
- Distinguir las dos operaciones: apilar filas (
concat) y cruzar por una clave (merge) - Apilar tablas con
pd.concat, corregir el índice repetido conignore_index=Truey observar qué ocurre con las columnas que no coinciden - Unir dos tablas mediante una clave con
pd.merge, y elegirhow="inner","left","right"o"outer"de forma intencionada - Auditar un merge con
indicator=Truey protegerlo convalidate="many_to_one" - Detectar y corregir la explosión de filas producida por claves duplicadas
- Hacer merge con claves que tienen nombres distintos usando
left_on/right_on, y gestionar nombres de columna en conflicto consuffixes - Reconocer los errores de merge —tipos de clave incompatibles, columnas clave ausentes— y los fallos silenciosos que no generan ninguna alerta
Requisitos previos: Añadir columnas.
Crea los archivos primero
Este capítulo utiliza cuatro archivos pequeños. Guárdalos junto a tu script.
orders.csv — marzo, el mismo archivo que en los capítulos anteriores:
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.0orders_april.csv — abril, con las mismas columnas. Fíjate en la grapadora: la tienda empezó a venderla en abril.
order_id,date,branch,product,category,quantity,price
1009,2024-04-01,north,pen,stationery,15,15.0
1010,2024-04-01,east,notebook,stationery,3,60.0
1011,2024-04-02,south,stapler,stationery,6,95.0
1012,2024-04-02,north,bag,accessories,1,850.0products.csv — una fila por producto, gestionado por compras. Dos cosas están deliberadamente «mal» aquí, tal como ocurriría en la vida real: la nueva grapadora aún no se ha añadido, y hay un rotulador que la tienda nunca ha vendido.
product,cost,supplier
pen,9.0,Alpha
notebook,40.0,Alpha
bag,600.0,Bravo
bottle,80.0,Bravo
eraser,5.0,Alpha
marker,25.0,Alphabranches.csv — quién dirige cada una de las tres sucursales de la tienda. La columna se llama branch_name, no branch, porque una persona distinta creó este archivo.
branch_name,manager
north,Mira
south,Omar
east,LenaAntes de escribir el código
Combinar tablas es el paso donde una línea rápida de código produce más a menudo una respuesta errónea que parece segura. Por tanto, la planificación va primero, y consiste principalmente en preguntas que respondes observando los datos.
1. Formula la pregunta en una sola frase
«Beneficio por proveedor, sumando marzo y abril juntos». Esa frase ya te dice cómo será la tabla final: una fila por proveedor, un número por fila. Existen dos proveedores, Alpha y Bravo, por lo que la respuesta debería tener unas dos filas. Si termina teniendo tres, o una sola, algo salió mal por el camino.
2. Decide qué operación necesita cada par de tablas
Solo hay dos formas de combinar, y la prueba es sencilla: ¿contienen las dos tablas el mismo tipo de fila, o datos diferentes sobre la misma entidad?
- Mismas columnas, filas diferentes — pedidos de marzo + pedidos de abril. Usa
pd.concat([a, b]). El resultado crece hacia abajo: más filas, las mismas columnas. - Una columna clave compartida, datos diferentes — pedidos + productos, vinculados por
product. Usapd.merge(a, b, on="product"). El resultado crece hacia los lados: las mismas filas, más columnas.
Marzo y abril son ambos «pedidos» —mismas columnas, más filas—, por lo que se apilan. Pedidos y productos son entidades distintas vinculadas por el nombre del producto, por lo que se cruzan.
3. Observa cada tabla antes de combinarla
Aprendiste en el capítulo tres a comprobar un archivo en cuanto se lee. Con varios archivos, esa costumbre no es opcional: necesitas la forma (shape) de cada uno para predecir la forma del resultado.
import pandas as pd
march = pd.read_csv("orders.csv")
april = pd.read_csv("orders_april.csv")
products = pd.read_csv("products.csv")
for name, df in [("march", march), ("april", april), ("products", products)]:
print(name, df.shape, list(df.columns))march (8, 7) ['order_id', 'date', 'branch', 'product', 'category', 'quantity', 'price']
april (4, 7) ['order_id', 'date', 'branch', 'product', 'category', 'quantity', 'price']
products (6, 3) ['product', 'cost', 'supplier']Marzo y abril tienen exactamente las mismas siete columnas, en el mismo orden: perfecto para apilar. products comparte exactamente un nombre de columna con ellos: product. Esa es la clave, la columna sobre la que se hará la coincidencia.
Comprueba que la clave tenga el mismo tipo en ambos lados. Una clave que es un número en una tabla y texto en la otra no puede coincidir:
print(march["product"].dtype, products["product"].dtype)str strAmbas son str. Se pueden comparar.
4. Comprueba que la clave en el lado de búsqueda sea única
Esta es la comprobación más importante de todo el capítulo. Cada pedido necesita un coste. Si products.csv incluye pen dos veces, cada pedido de bolígrafos coincidirá dos veces y aparecerá dos veces en el resultado.
print(products["product"].is_unique)
print(products["product"].duplicated().sum())True
0is_unique es True y hay cero duplicados: una fila por producto. Cada pedido puede coincidir como máximo con una fila de producto.
5. Comprueba qué claves no encontrarán coincidencia
Antes de hacer el merge, pregúntate: ¿hay pedidos cuyo producto falte en products.csv, y productos que nadie haya pedido? isin (del capítulo de filtrado) responde a ambas preguntas:
orders = pd.concat([march, april], ignore_index=True)
no_cost = ~orders["product"].isin(products["product"])
print(orders.loc[no_cost, ["order_id", "product"]])
never_sold = ~products["product"].isin(orders["product"])
print(products.loc[never_sold, "product"].tolist())order_id product
10 1011 stapler
['marker']Un pedido —la grapadora, 1011— no tiene coste registrado. Un producto, el rotulador, nunca se vendió. Ahora sabes, antes de hacer el merge, a qué tendrá que enfrentarse la operación.
6. Predice el resultado y luego decide
Escribe la predicción, con números:
orderstiene 8 + 4 = 12 filas.productstiene una fila por producto, por lo que el cruce no puede multiplicar pedidos.- Si se conservan todos los pedidos, la tabla combinada tiene 12 filas, y una de ellas (la grapadora) no tiene coste.
- Si solo se conservan los pedidos con coincidencia, tiene 11.
¿Qué prefieres? Para un informe de beneficios, descartar silenciosamente el pedido de la grapadora subestimaría las ventas. Es mejor conservarlo, ver que falta su coste y decidir qué hacer —que es exactamente la elección que determina el argumento how=. Tras el merge, lo primero que imprimes es la forma (shape), y la comparas con esta predicción.
Ese es todo el plan: pregunta en una sola frase, operación por par, observar cada tabla, unicidad de la clave, claves sin coincidencia y recuento de filas previsto. El resto del capítulo presenta las herramientas para llevarlo a cabo.
Apilar filas con pd.concat
pd.concat toma una lista de tablas y las coloca una debajo de la otra:
import pandas as pd
march = pd.read_csv("orders.csv")
april = pd.read_csv("orders_april.csv")
both = pd.concat([march, april])
print(both.shape)
print(both[["order_id", "date", "product", "quantity"]])(12, 7)
order_id date product quantity
0 1001 2024-03-01 pen 12
1 1002 2024-03-01 notebook 5
2 1003 2024-03-02 bag 2
3 1004 2024-03-02 bottle 7
4 1005 2024-03-03 eraser 30
5 1006 2024-03-03 bag 1
6 1007 2024-03-04 pen 20
7 1008 2024-03-04 bottle 4
0 1009 2024-04-01 pen 15
1 1010 2024-04-01 notebook 3
2 1011 2024-04-02 stapler 6
3 1012 2024-04-02 bag 1Doce filas, las mismas siete columnas. Pandas alineó las columnas por nombre, no por posición; si el archivo de abril hubiera listado las columnas en un orden diferente, aun así habrían quedado bajo los encabezados correctos.
Pero mira el borde izquierdo. El índice va de 0 a 7, y luego vuelve a empezar en 0. concat conservó el propio índice de cada tabla, de modo que las etiquetas del 0 al 3 ahora aparecen dos veces. Eso no es un detalle estético:
print(both.loc[0, ["order_id", "date"]])order_id date
0 1001 2024-03-01
0 1009 2024-04-01Pediste la fila 0 y obtuviste dos filas. Cualquier código que asuma que una etiqueta significa una sola fila —loc, un merge posterior sobre el índice, actualizar una celda concreta— ahora hace algo que no pretendías. Puedes comprobarlo:
print(both.index.is_unique)Falseignore_index=True — vuelve a numerar las filas
Cuando el índice anterior solo era un recuento de filas (un RangeIndex, como en cada read_csv hasta ahora), no aporta ningún significado, así que descártalo y deja que concat numere el resultado desde cero:
orders = pd.concat([march, april], ignore_index=True)
print(orders.index.is_unique)
print(orders[["order_id", "date", "product"]].tail(5))True
order_id date product
7 1008 2024-03-04 bottle
8 1009 2024-04-01 pen
9 1010 2024-04-01 notebook
10 1011 2024-04-02 stapler
11 1012 2024-04-02 bagAhora las etiquetas van del 0 al 11, cada una exactamente una vez. Haz de ignore_index=True tu opción predeterminada al apilar archivos; prescinde de él únicamente cuando el índice tenga un significado real (por ejemplo, después de set_index("order_id"), donde los identificadores de pedido ya son únicos).
Lleva el registro de dónde vino cada fila
Después de apilar, nada en la tabla indica a qué mes pertenece cada fila. Aquí la fecha te lo indica, pero a menudo no hay nada que lo haga. Añade una columna antes de concatenar —la técnica del capítulo anterior— y la información viajará con las filas:
march["month"] = "March"
april["month"] = "April"
orders = pd.concat([march, april], ignore_index=True)
print(orders["month"].value_counts())month
March 8
April 4
Name: count, dtype: int64Los recuentos sirven además como comprobación: 8 + 4 = 12, no se perdió nada.
Cuando las columnas no coinciden
Concat alinea las columnas por nombre. Una columna que exista solo en algunas de las tablas seguirá apareciendo en el resultado, y las filas de las tablas que no la tenían recibirán NaN en ese lugar. Supón que se introdujo a mano un pedido excepcional, con una columna discount y sin date, product, category ni price:
manual = pd.DataFrame({
"order_id": [2001],
"branch": ["north"],
"quantity": [3],
"discount": [0.1],
})
mixed = pd.concat([march.head(2), manual], ignore_index=True)
print(mixed[["order_id", "branch", "price", "discount"]])
print(mixed.isna().sum())order_id branch price discount
0 1001 north 15.0 NaN
1 1002 south 60.0 NaN
2 2001 north NaN 0.1
order_id 0
date 1
branch 0
product 1
category 1
quantity 0
price 1
month 1
discount 2
dtype: int64Sin errores. El resultado contiene la unión de todas las columnas, y los huecos se rellenan con NaN. Por eso apilar merece el mismo isna().sum() que aprendiste en el capítulo de datos ausentes: un nombre de columna escrito de forma distinta en un archivo (Quantity frente a quantity) no falla, sino que produce dos columnas semivacías. Si el número de columnas tras un concat es mayor que en cualquiera de las entradas, algo no coincidió.
axis=1 — y por qué normalmente no lo quieres
pd.concat([a, b], axis=1) coloca las tablas una al lado de la otra, haciendo coincidir filas por etiqueta de índice. Resulta tentador para «añadir las columnas del producto a los pedidos», pero no tiene en cuenta el nombre del producto en absoluto: la fila 0 de una tabla se coloca junto a la fila 0 de la otra, contengan lo que contengan. Para hacer coincidir por una columna, usa merge.
Cruzar filas con pd.merge
merge toma dos tablas y una clave. Para cada fila de la tabla izquierda, busca las filas de la tabla derecha con el mismo valor en la clave y junta sus columnas:
products = pd.read_csv("products.csv")
merged = pd.merge(orders, products, on="product")
print(merged.shape)
print(merged[["order_id", "product", "quantity", "price", "cost", "supplier"]])(11, 10)
order_id product quantity price cost supplier
0 1001 pen 12 15.0 9.0 Alpha
1 1002 notebook 5 60.0 40.0 Alpha
2 1003 bag 2 850.0 600.0 Bravo
3 1004 bottle 7 120.0 80.0 Bravo
4 1005 eraser 30 8.0 5.0 Alpha
5 1006 bag 1 850.0 600.0 Bravo
6 1007 pen 20 15.0 9.0 Alpha
7 1008 bottle 4 120.0 80.0 Bravo
8 1009 pen 15 15.0 9.0 Alpha
9 1010 notebook 3 60.0 40.0 Alpha
10 1012 bag 1 850.0 600.0 BravoCada pedido tiene ahora a su lado su coste y su proveedor, sin ningún bucle y sin que importe el orden de ninguno de los dos archivos. La columna clave product aparece una sola vez, porque es la misma en ambos lados.
Ahora compara la forma (shape) con el plan. Predijimos 12 filas si se conservaba todo, y tenemos 11. El pedido de la grapadora, 1011, ha desaparecido. Sin advertencias, sin errores.
No es un fallo. Es el tipo de merge predeterminado, how="inner", haciendo exactamente aquello para lo que está definido: conservar únicamente las claves presentes en ambas tablas. La grapadora no está en products, por lo que su pedido no sobrevive; el rotulador no está en orders, por lo que tampoco aparece. Si no hubieras predicho 12, nunca habrías notado la venta ausente.
Los cuatro tipos de merge
El argumento how= decide qué sucede con las filas cuya clave no tiene pareja en el otro lado. Imagina los dos conjuntos de claves:
how="inner"(el predeterminado) conserva solo las claves encontradas en ambas tablas. Las filas sin coincidencia de cualquiera de los dos lados se descartan. Úsalo cuando solo quieras registros completos.how="left"conserva cada fila de la tabla izquierda. Las filas de la izquierda sin coincidencia recibenNaNen las columnas de la tabla derecha; las filas de la derecha sin coincidencia se descartan. Úsalo para enriquecer una tabla principal: es la opción más común.how="right"es la imagen simétrica: se conserva cada fila de la tabla derecha y se descartan las filas sin coincidencia de la izquierda.how="outer"conserva todas las claves de ambos lados, rellenando los huecos conNaN. Úsalo para auditar dos listas entre sí.
La pregunta que debes hacerte es: ¿cuál es la tabla cuyas filas no deben desaparecer? Aquí son los pedidos. Cada pedido es una venta real; el archivo de productos es una tabla de búsqueda. Así pues, el merge correcto es un merge left, con los pedidos a la izquierda:
merged = pd.merge(orders, products, on="product", how="left")
print(merged.shape)
print(merged[["order_id", "product", "cost", "supplier"]])(12, 10)
order_id product cost supplier
0 1001 pen 9.0 Alpha
1 1002 notebook 40.0 Alpha
2 1003 bag 600.0 Bravo
3 1004 bottle 80.0 Bravo
4 1005 eraser 5.0 Alpha
5 1006 bag 600.0 Bravo
6 1007 pen 9.0 Alpha
7 1008 bottle 80.0 Bravo
8 1009 pen 9.0 Alpha
9 1010 notebook 40.0 Alpha
10 1011 stapler NaN NaN
11 1012 bag 600.0 BravoDoce filas, tal como se predijo. El pedido de la grapadora sigue ahí, con NaN para el coste y el proveedor, lo cual es honesto: el coste se desconoce, y puedes ver que es así. Un valor ausente que puedes ver es mucho mejor que una fila perdida que no puedes ver. A partir de aquí, se aplican las herramientas del capítulo de datos ausentes: puedes contar los huecos con isna().sum(), rellenar el coste si el equipo de compras te lo facilita, o informar del pedido por separado.
Un merge right es la imagen simétrica: cada producto se conserva, y el rotulador aparece sin pedido:
right = pd.merge(orders, products, on="product", how="right")
print(right.shape)
print(right[["order_id", "product", "cost"]].tail(3))(12, 10)
order_id product cost
9 1008.0 bottle 80.0
10 1005.0 eraser 5.0
11 NaN marker 25.0Observa que order_id cambió de números enteros a 1008.0, 1005.0 y demás. La fila del rotulador no tiene identificador de pedido, por lo que la columna ahora contiene un NaN —y, como viste en el capítulo de datos ausentes, una columna de números enteros que recibe un NaN pasa a ser float64. Si a los identificadores les aparece de repente un .0 tras un merge, esa es la razón: algunas filas no encontraron coincidencia.
indicator=True — observa de dónde vino cada fila
Un merge outer conserva todo de ambos lados. Añade indicator=True y pandas agregará una columna, _merge, que indica para cada fila si su clave se encontró en ambas tablas, solo en la izquierda o solo en la derecha:
audit = pd.merge(orders, products, on="product", how="outer", indicator=True)
print(audit[["order_id", "product", "cost", "_merge"]])
print(audit["_merge"].value_counts())order_id product cost _merge
0 1003.0 bag 600.0 both
1 1006.0 bag 600.0 both
2 1012.0 bag 600.0 both
3 1004.0 bottle 80.0 both
4 1008.0 bottle 80.0 both
5 1005.0 eraser 5.0 both
6 NaN marker 25.0 right_only
7 1002.0 notebook 40.0 both
8 1010.0 notebook 40.0 both
9 1001.0 pen 9.0 both
10 1007.0 pen 9.0 both
11 1009.0 pen 9.0 both
12 1011.0 stapler NaN left_only
_merge
both 11
left_only 1
right_only 1
Name: count, dtype: int64Este es el propio boletín de calificaciones del merge. Once filas coincidieron; un pedido no tiene fila de producto (left_only: la grapadora); un producto no tiene pedido (right_only: el rotulador). Es la misma información que en el paso 5 del plan, generada por el propio merge.
El merge outer también ordenó las filas por la clave —bag, bottle, eraser, …— en lugar de conservar el orden de los pedidos. Los merges inner y left conservan el orden de la tabla izquierda; outer no lo garantiza. Si el orden importa, ordena después, como en el capítulo siete.
La columna _merge son datos ordinarios, por lo que puedes filtrar por ella. Listar solo los problemas requiere una sola línea:
problems = audit[audit["_merge"] != "both"]
print(problems[["order_id", "product", "_merge"]])order_id product _merge
6 NaN marker right_only
12 1011.0 stapler left_onlyEjecuta este tipo de auditoría siempre que hagas merge de datos que no hayas creado tú mismo. Cuando la lista de problemas esté vacía, tendrás la prueba de que ambos archivos concuerdan.
El peligro silencioso: claves duplicadas
Hasta ahora products tenía una fila por producto. Supón que el equipo de compras añade un segundo proveedor de bolígrafos a un coste ligeramente superior y simplemente anexa una fila, de modo que pen aparece dos veces:
products_dup = pd.concat(
[products, pd.DataFrame({"product": ["pen"], "cost": [10.0], "supplier": ["Gamma"]})],
ignore_index=True,
)
print(products_dup["product"].is_unique)
exploded = pd.merge(march, products_dup, on="product", how="left")
print(march.shape, "->", exploded.shape)
print(exploded[["order_id", "product", "quantity", "cost", "supplier"]])False
(8, 8) -> (10, 10)
order_id product quantity cost supplier
0 1001 pen 12 9.0 Alpha
1 1001 pen 12 10.0 Gamma
2 1002 notebook 5 40.0 Alpha
3 1003 bag 2 600.0 Bravo
4 1004 bottle 7 80.0 Bravo
5 1005 eraser 30 5.0 Alpha
6 1006 bag 1 600.0 Bravo
7 1007 pen 20 9.0 Alpha
8 1007 pen 20 10.0 Gamma
9 1008 bottle 4 80.0 BravoMarzo tenía 8 pedidos; el merge devolvió 10 filas. Los pedidos 1001 y 1007 —los dos pedidos de bolígrafos— aparecen cada uno dos veces, una por cada fila de pen. Nada falló. Pero suma ahora la cantidad y los bolígrafos se cuentan dos veces: 32 bolígrafos extra que nunca se vendieron.
print(march["quantity"].sum(), exploded["quantity"].sum())81 11381 frente a 113. Esto se llama explosión de filas, y es el error de merge más costoso porque el resultado parece normal. La regla detrás de esto: merge empareja cada fila coincidente de la izquierda con cada fila coincidente de la derecha. Una relación uno a uno o muchos a uno conserva el recuento de filas; una clave repetida en ambos lados lo multiplica.
Un merge que añade filas nunca avisa de ello. El único síntoma es un total demasiado grande, y un total demasiado grande rara vez parece incorrecto. Compara len() antes y después de cada merge: es la única comprobación que detecta esto.validate= — haz que pandas lo compruebe por ti
Puedes declarar la relación que esperas, y pandas se negará a hacer el merge si los datos la rompen. Muchos pedidos comparten un producto, por lo que este merge debería ser muchos a uno (many-to-one):
try:
pd.merge(march, products_dup, on="product", how="left", validate="many_to_one")
except Exception as error:
print(type(error).__name__ + ":", error)MergeError: Merge keys are not unique in right dataset; not a many-to-one merge
Duplicates in right:
product
pen ...Un MergeError, e incluso lista la clave infractora. Los otros valores son "one_to_one", "one_to_many" y "many_to_many". Escribir validate= no cuesta nada y convierte una respuesta errónea silenciosa en un error ruidoso, así que ponlo en cada merge con una tabla de búsqueda.
Cómo solucionarlo: decide qué duplicado es el correcto
El error te indica que algo está mal; decidir qué es lo correcto es tu trabajo. Mira primero los duplicados:
print(products_dup[products_dup["product"].duplicated(keep=False)])product cost supplier
0 pen 9.0 Alpha
6 pen 10.0 Gammaduplicated(keep=False) marca cada copia, no solo la segunda. Ahora hay que tomar una decisión de negocio. Si la fila más reciente sustituye a la antigua, conserva la última copia:
products_clean = products_dup.drop_duplicates(subset="product", keep="last")
fixed = pd.merge(march, products_clean, on="product", how="left", validate="many_to_one")
print(fixed.shape)
print(fixed.loc[fixed["product"] == "pen", ["order_id", "cost", "supplier"]])(8, 10)
order_id cost supplier
0 1001 10.0 Gamma
6 1007 10.0 GammaVolvemos a 8 filas, una por pedido. Si, en cambio, ambos proveedores venden realmente bolígrafos y un pedido registra cuál de ellos, entonces supplier debe figurar también en los pedidos, y la clave de merge pasa a ser de dos columnas: on=["product", "supplier"]. El código no puede tomar esa decisión; la planificación sí.
Cuando las columnas clave tienen nombres distintos
branches.csv llama a su columna branch_name, mientras que los pedidos la llaman branch. on= necesita el mismo nombre en ambos lados, así que nombra cada lado por separado con left_on= y right_on=:
branches = pd.read_csv("branches.csv")
with_manager = pd.merge(orders, branches, left_on="branch", right_on="branch_name", how="left")
print(list(with_manager.columns))['order_id', 'date', 'branch', 'product', 'category', 'quantity', 'price', 'month', 'branch_name', 'manager']Se conservan ambas columnas clave, porque pandas no puede saber que las consideras una sola columna. branch y branch_name ahora contienen valores idénticos, por lo que descarta una:
with_manager = with_manager.drop(columns="branch_name")
print(with_manager[["order_id", "branch", "manager"]].head(4))order_id branch manager
0 1001 north Mira
1 1002 south Omar
2 1003 north Mira
3 1004 east LenaLa alternativa es renombrar antes de hacer el merge —branches.rename(columns={"branch_name": "branch"})— y luego usar simplemente on="branch". Ambas opciones son válidas; renombrar mantiene el resultado limpio sin necesidad de un paso de limpieza posterior.
Cuando ambas tablas tienen una columna con el mismo nombre
Si existe una columna que no sea la clave en ambas tablas, pandas no puede poner dos columnas llamadas price en una misma tabla, por lo que las renombra con los sufijos _x (izquierda) e _y (derecha). Imagina una tabla de precios de catálogo que también llama price a su columna:
list_prices = pd.DataFrame({"product": ["pen", "bag"], "price": [14.0, 800.0]})
compared = pd.merge(march, list_prices, on="product")
print(compared[["order_id", "product", "price_x", "price_y"]])order_id product price_x price_y
0 1001 pen 15.0 14.0
1 1003 bag 850.0 800.0
2 1006 bag 850.0 800.0
3 1007 pen 15.0 14.0price_x y price_y funcionan, pero dentro de tres semanas nadie recordará cuál es cuál. Nómbralas tú mismo con suffixes=:
compared = pd.merge(march, list_prices, on="product", suffixes=("_sold", "_list"))
print(compared[["order_id", "price_sold", "price_list"]])order_id price_sold price_list
0 1001 15.0 14.0
1 1003 850.0 800.0
2 1006 850.0 800.0
3 1007 15.0 14.0Ahora las columnas dicen lo que significan, y el descuento otorgado en cada pedido es simplemente price_list - price_sold. (Un código que más adelante pida compared["price"] obtendría un KeyError, porque después del merge ninguna columna tiene ese nombre exacto: otra razón para elegir los nombres deliberadamente).
join — combinar por índice, brevemente
Los DataFrames también tienen un método .join(). Es un merge que utiliza el índice de la tabla derecha como su clave —muy práctico cuando la tabla de búsqueda ya está indexada por la clave:
by_product = products.set_index("product")
joined = orders.join(by_product, on="product")
print(joined[["order_id", "product", "cost", "supplier"]].head(3))order_id product cost supplier
0 1001 pen 9.0 Alpha
1 1002 notebook 40.0 Alpha
2 1003 bag 600.0 Bravojoin utiliza how="left" por defecto. Hace el mismo trabajo que pd.merge(..., how="left"); merge es la herramienta más general y la que se utilizará en el resto de este curso.
Un ejemplo completo
Volvamos a la pregunta de la dueña de la tienda: beneficio por proveedor a lo largo de marzo y abril, con cada pedido contabilizado.
supplier_profit.py:
import pandas as pd
march = pd.read_csv("orders.csv")
april = pd.read_csv("orders_april.csv")
products = pd.read_csv("products.csv")
# 1. Stack the two months, remembering where each row came from.
march["month"] = "March"
april["month"] = "April"
orders = pd.concat([march, april], ignore_index=True)
print("orders:", orders.shape)
# 2. The lookup key must be unique, or the merge will duplicate orders.
assert products["product"].is_unique
# 3. Attach cost and supplier to every order. Keep every order.
sales = pd.merge(
orders, products, on="product", how="left",
validate="many_to_one", indicator=True,
)
print("after merge:", sales.shape)
assert len(sales) == len(orders)
# 4. Report the orders that found no product row, then set them aside.
unmatched = sales[sales["_merge"] == "left_only"]
print("no cost on file:", unmatched["order_id"].tolist(), unmatched["product"].tolist())
sales = sales[sales["_merge"] == "both"].drop(columns="_merge")
# 5. Profit per row, then per supplier and month.
sales["profit"] = sales["quantity"] * (sales["price"] - sales["cost"])
report = (
sales.groupby(["supplier", "month"], as_index=False)["profit"]
.sum()
.sort_values(["supplier", "profit"], ascending=[True, False])
.reset_index(drop=True)
)
print()
print(report)
print()
print(sales.groupby("supplier")["profit"].sum().sort_values(ascending=False))orders: (12, 8)
after merge: (12, 11)
no cost on file: [1011] ['stapler']
supplier month profit
0 Alpha March 382.0
1 Alpha April 150.0
2 Bravo March 1190.0
3 Bravo April 250.0
supplier
Bravo 1440.0
Alpha 532.0
Name: profit, dtype: float64Por qué está escrito de esta manera:
- Cada paso de combinación va seguido de una comprobación de su forma (
shape).orders: (12, 8)confirma el apilamiento (8 + 4 filas, 7 columnas +month).after merge: (12, 11)confirma que nada se descartó ni se duplicó —3 columnas nuevas:cost,supplier,_merge. Las dimensiones (shape) son la prueba más económica de que cada paso cumplió con lo previsto en el plan. - Las suposiciones se escriben como código.
assert products["product"].is_uniqueyvalidate="many_to_one"protegen ambas contra la explosión de filas;assert len(sales) == len(orders)protege la predicción del plan. Si alguien envía el archivo de productos del mes que viene con un duplicado, el script se detiene en lugar de imprimir un beneficio inflado. how="left"conserva el pedido de la grapadora, eindicator=Truelo identifica. El script no lo descarta silenciosamente; imprime[1011] ['stapler'], para que la dueña sepa que al total de abril le falta una venta hasta que compras añada un coste. Esa línea es parte de la respuesta, no una salida de depuración.- La columna
monthse añadió antes deconcat. De lo contrario, el desglose mensual del informe requeriría analizar y convertir las fechas: posible, pero más trabajo para obtener el mismo resultado. - El beneficio se calcula después del merge, porque necesita columnas de ambos archivos. Este es el orden natural: combinar, luego añadir columnas (capítulo nueve), luego agrupar (capítulo ocho) y luego ordenar (capítulo siete).
La respuesta: Bravo aportó 1.440 de beneficio y Alpha 532, con una venta de abril aún a la espera de su coste. Bravo vende pocos artículos, pero las bolsas y botellas tienen un margen mucho mayor por unidad que los bolígrafos y las gomas de borrar.
Cuando algo falla
ValueError: You are trying to merge on int64 and str columns for key 'order_id'. If you wish to proceed you should use pd.concat
La clave tiene un tipo diferente en cada lado. Aquí tienes un archivo de entregas cuyos identificadores de pedido se exportaron con una almohadilla # delante. Guárdalo como deliveries.csv —también se usa en «Tu turno». (Si aún conservas el registro de repartidores con el mismo nombre del capítulo de datos ausentes, sobrescríbelo; este es un archivo diferente).
order_id,status
#1001,delivered
#1002,delivered
#1003,returned
#1005,delivered
#1006,pendingimport pandas as pd
march = pd.read_csv("orders.csv")
deliveries = pd.read_csv("deliveries.csv")
print(march["order_id"].dtype, deliveries["order_id"].dtype)
try:
pd.merge(march, deliveries, on="order_id", how="left")
except ValueError as error:
print("ValueError:", error)int64 str
ValueError: You are trying to merge on int64 and str columns for key 'order_id'. If you wish to proceed you should use pd.concat1001 y "#1001" son valores distintos, y pandas se niega a adivinar. (La sugerencia sobre pd.concat es para una situación diferente; ignórala). La solución es hacer que las claves tengan el mismo tipo y el mismo texto: elimina el # y luego convierte el tipo:
deliveries["order_id"] = deliveries["order_id"].str.removeprefix("#").astype("int64")
status = pd.merge(march, deliveries, on="order_id", how="left")
print(status[["order_id", "product", "status"]])order_id product status
0 1001 pen delivered
1 1002 notebook delivered
2 1003 bag returned
3 1004 bottle NaN
4 1005 eraser delivered
5 1006 bag pending
6 1007 pen NaN
7 1008 bottle NaNLos pedidos sin registro de entrega reciben NaN en status; el merge left los conserva, que es exactamente lo que deseas cuando la pregunta es «¿qué pedidos no han sido entregados?».
MergeError: Merge keys are not unique in right dataset; not a many-to-one merge Has usado validate="many_to_one" y la tabla derecha repite una clave. El mensaje lista los duplicados. No elimines validate para que el error desaparezca: examina las filas duplicadas con duplicated(keep=False) y decide cuál es la correcta, como en la sección de explosión de filas.
KeyError: 'branch' La columna indicada en on= no existe en una de las tablas:
branches = pd.read_csv("branches.csv")
try:
pd.merge(march, branches, on="branch")
except KeyError as error:
print("KeyError:", error)KeyError: 'branch'branches la llama branch_name. Imprime list(df.columns) para ambas tablas; usa left_on="branch", right_on="branch_name", o renombra un lado. Un espacio al final en un nombre de columna ("branch ") causa el mismo error y solo es visible en formato de lista.
El merge se ejecutó, pero las columnas nuevas son todas NaN Sin error, pero ninguna fila coincidió. Las claves parecen iguales pero no lo son: "north " con un espacio al final, "South" con mayúscula inicial, o el mismo identificador como texto en un archivo y con un prefijo en el otro. Comprueba con isin antes de hacer el merge:
branches_messy = pd.DataFrame({"branch_name": ["north ", "South", "east"], "manager": ["Mira", "Omar", "Lena"]})
print(march["branch"].isin(branches_messy["branch_name"]).sum(), "of", len(march), "orders match")
branches_messy["branch_name"] = branches_messy["branch_name"].str.strip().str.lower()
print(march["branch"].isin(branches_messy["branch_name"]).sum(), "of", len(march), "orders match")2 of 8 orders match
8 of 8 orders matchDos de ocho antes de la limpieza, ocho de ocho después. Limpia la clave de la misma forma en ambos lados —str.strip() y un uso uniforme de mayúsculas y minúsculas— antes de hacer el merge.
El recuento de filas aumentó después de un merge La tabla derecha tiene una clave duplicada; consulta «El peligro silencioso» más arriba. Compara len() antes y después de cada merge, y usa validate=.
Desaparecieron filas después de un merge Usaste el how="inner" predeterminado y algunas claves no tenían pareja. Usa how="left" con la tabla principal a la izquierda, y indicator=True para ver qué filas no tuvieron coincidencia.
TypeError: concat() takes 1 positional argument but 2 were given
try:
pd.concat(march, deliveries)
except TypeError as error:
print("TypeError:", error)TypeError: concat() takes 1 positional argument but 2 were givenconcat toma un solo argumento: una lista de tablas. Escribe pd.concat([march, april]): los corchetes son la clave.
Un índice duplicado después de concat, y loc devuelve dos filas Cada entrada conservó su propio 0, 1, 2, …. Añade ignore_index=True.
Paso 4 de 6 — Predice
Comprueba lo que has entendido
Marzo tiene 8 pedidos y abril 4. ¿Qué se imprime?
from io import StringIO
import pandas as pd
# Stand in for orders.csv, orders_april.csv and products.csv,
# so this snippet runs on its own.
MARCH = """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
"""
APRIL = """order_id,date,branch,product,category,quantity,price
1009,2024-04-01,north,pen,stationery,15,15.0
1010,2024-04-01,east,notebook,stationery,3,60.0
1011,2024-04-02,south,stapler,stationery,6,95.0
1012,2024-04-02,north,bag,accessories,1,850.0
"""
PRODUCTS = """product,cost,supplier
pen,9.0,Alpha
notebook,40.0,Alpha
bag,600.0,Bravo
bottle,80.0,Bravo
eraser,5.0,Alpha
marker,25.0,Alpha
"""
march = pd.read_csv(StringIO(MARCH))
april = pd.read_csv(StringIO(APRIL))
products = pd.read_csv(StringIO(PRODUCTS))
both = pd.concat([march, april])
print(both.shape, both.index[-1])- A(12, 7) 11
- B(12, 14) 3
- C(12, 7) 3
- D(8, 7) 7
Alguien añadió una segunda fila para pen a la lista de productos. ¿Qué se imprime?
from io import StringIO
import pandas as pd
# Stand in for orders.csv, orders_april.csv and products.csv,
# so this snippet runs on its own.
MARCH = """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
"""
APRIL = """order_id,date,branch,product,category,quantity,price
1009,2024-04-01,north,pen,stationery,15,15.0
1010,2024-04-01,east,notebook,stationery,3,60.0
1011,2024-04-02,south,stapler,stationery,6,95.0
1012,2024-04-02,north,bag,accessories,1,850.0
"""
PRODUCTS = """product,cost,supplier
pen,9.0,Alpha
notebook,40.0,Alpha
bag,600.0,Bravo
bottle,80.0,Bravo
eraser,5.0,Alpha
marker,25.0,Alpha
"""
march = pd.read_csv(StringIO(MARCH))
april = pd.read_csv(StringIO(APRIL))
products = pd.read_csv(StringIO(PRODUCTS))
products = pd.concat([products, pd.DataFrame({"product": ["pen"], "cost": [10.0], "supplier": ["Charlie"]})])
report = pd.merge(march, products, on="product", how="left")
print(len(march), len(report))- A8 6
- B8 8
- CUn `MergeError` sobre claves duplicadas
- D8 10 — cada pedido de pen coincidió con ambas filas de pen y se duplicó, sin ningún error
Cada pedido de marzo necesita el coste de su producto, y no se puede perder ningún pedido, incluso si su producto no está en la lista. ¿Qué how= utilizas?
- A`how="outer"`
- B`how="right"`
- C`how="inner"`
- D`how="left"`, con los pedidos como tabla izquierda
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 equipo de entregas envió deliveries.csv (mostrado en «Cuando algo falla»): una fila por cada pedido del que tienen registro, con los identificadores de pedido escritos como #1001. La dueña de la tienda quiere un informe de entregas que cubra marzo y abril juntos.
Escribe delivery_report.py de modo que:
- Se conserve cada pedido de ambos meses — exactamente una fila por pedido, verificado con un
assertsobre el recuento de filas, con el merge protegido mediantevalidate=y auditado conindicator=True. - Las claves coincidan de verdad — la clave de entrega se limpia para que tenga el mismo tipo que
order_id, y el script se detiene si algún pedido aparece dos veces en el archivo de entregas. - «Ausente» signifique «no record» — los pedidos sin fila de entrega reciben el estado
"no record", nunca unNaNsilencioso. - Imprima, en este orden: la forma apilada, la forma combinada con merge, los recuentos de
_merge, el número de pedidos por estado, los pedidos sin registro (order_id,branch,product, ordenados pororder_id) y el número de pedidos entregados por sucursal.
Cuando sea correcto, la salida es exactamente:
orders: (12, 7)
after merge: (12, 9)
_merge
left_only 7
both 5
right_only 0
Name: count, dtype: int64
status
no record 7
delivered 3
returned 1
pending 1
Name: count, dtype: int64
order_id branch product
3 1004 east bottle
6 1007 east pen
7 1008 north bottle
8 1009 north pen
9 1010 east notebook
10 1011 south stapler
11 1012 north bag
branch
north 2
south 1
dtype: int64Antes de escribir nada de código, explica cada número de esa salida únicamente a partir de los archivos: por qué 12 filas tras apilar, por qué siguen siendo 12 tras el merge y por qué 7 pedidos sin registro. Si no puedes, el plan todavía no está terminado.
Luego rómpelo a propósito, dos veces:
- Elimina
.str.removeprefix("#"). ¿Qué error obtienes y qué línea lo produce? - Añade una segunda fila para
#1003adeliveries.csv(por ejemplo,#1003,delivered). ¿Qué hacevalidate=ahora? ¿Qué habría dicho el informe sin él?
Solución
El plan. La pregunta: «para cada pedido de marzo y abril, ¿cuál es su estado de entrega?»; por lo tanto, el resultado tiene una fila por pedido, 12 filas. Marzo y abril tienen columnas idénticas: apílalas con concat e ignore_index=True (8 + 4 = 12). El archivo de entregas tiene como clave el identificador de pedido, pero como texto con una #, por lo que la clave necesita limpieza antes de poder coincidir con el entero order_id. Cada pedido debería aparecer como máximo una vez en él, por lo que la relación es uno a uno (cada pedido tiene como máximo una fila de entrega, cada fila de entrega un pedido). Cada pedido debe sobrevivir, así que los pedidos van a la izquierda y el merge es how="left". Existen cinco filas de entregas, todas de marzo, por lo que 12 − 5 = 7 pedidos deberían acabar sin registro.
delivery_report.py:
import pandas as pd
march = pd.read_csv("orders.csv")
april = pd.read_csv("orders_april.csv")
deliveries = pd.read_csv("deliveries.csv")
# 1. Stack the months.
orders = pd.concat([march, april], ignore_index=True)
print("orders:", orders.shape)
# 2. Clean the key, then check it.
deliveries["order_id"] = deliveries["order_id"].str.removeprefix("#").astype("int64")
assert deliveries["order_id"].is_unique, "an order appears twice in deliveries.csv"
# 3. Keep every order.
report = pd.merge(
orders, deliveries, on="order_id", how="left",
validate="one_to_one", indicator=True,
)
print("after merge:", report.shape)
assert len(report) == len(orders)
print(report["_merge"].value_counts())
# 4. Missing status means the delivery team has no record.
report["status"] = report["status"].fillna("no record")
# 5. Orders per status.
print()
print(report["status"].value_counts())
# 6. Orders with no record.
missing = report.loc[report["status"] == "no record", ["order_id", "branch", "product"]]
print()
print(missing.sort_values("order_id"))
# 7. Delivered orders per branch.
delivered = report[report["status"] == "delivered"]
print()
print(delivered.groupby("branch").size())orders: (12, 7)
after merge: (12, 9)
_merge
left_only 7
both 5
right_only 0
Name: count, dtype: int64
status
no record 7
delivered 3
returned 1
pending 1
Name: count, dtype: int64
order_id branch product
3 1004 east bottle
6 1007 east pen
7 1008 north bottle
8 1009 north pen
9 1010 east notebook
10 1011 south stapler
11 1012 north bag
branch
north 2
south 1
dtype: int64Cada predicción se cumple: 12 filas tras apilar, 12 tras el merge, 7 sin registro. Los recuentos de _merge lo confirman de forma independiente: 5 both, 7 left_only, 0 right_only (un merge left nunca produce right_only).
Notas sobre las decisiones tomadas:
validate="one_to_one", no"many_to_one". La tabla de pedidos también tiene identificadores de pedido únicos, por lo que ambos lados son únicos. Esta comprobación más estricta detectaría además un identificador de pedido duplicado en los pedidos —por ejemplo, si el archivo de abril repitiese accidentalmente un pedido de marzo.- El
assertsobreis_uniqueva antes del merge, con un mensaje, de modo que el fallo explique qué está mal con palabras claras en lugar de términos de merge. fillna("no record")se aplica después del merge, nunca antes: los estados ausentes solo existen una vez que los pedidos sin fila de entrega han sido conservados por el merge left.- La sucursal east no tiene pedidos entregados, por lo que no aparece en absoluto en la última tabla. Agrupar solo produce los grupos que existen en los datos; si la dueña necesita un 0 para east, eso sería un merge de la lista de sucursales con este resultado,
how="left", y unfillna(0).
Las dos formas de romperlo. Sin removeprefix, la línea con astype("int64") falla primero con ValueError: invalid literal for int() with base 10: '#1001', porque "#1001" no se puede convertir en un número; el error señala la línea de limpieza, que es donde corresponde la corrección. Si además quitas el astype, el propio merge produce ValueError: You are trying to merge on int64 and str columns for key 'order_id'. Con una segunda fila para #1003, el assert detiene el script con AssertionError: an order appears twice in deliveries.csv; si quitas el assert, validate="one_to_one" genera MergeError: Merge keys are not unique in right dataset; not a one-to-one merge. Sin ninguna de las dos protecciones, el informe habría mostrado 13 filas para 12 pedidos y habría contado el pedido 1003 dos veces —una como returned y otra como delivered—, y cada total posterior habría sido erróneo en silencio.
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