Unir tablas: merge
En resumen: merge en pandas une dos tablas por una clave común, como un JOIN en SQL, y how decide qué filas se quedan.
Problema
«¿Qué categoría aporta más dinero?»
El gerente pregunta qué categoría vende más. Las líneas de pedido dicen cuántas unidades de qué producto se compraron, y a qué precio:
Pero ahí no hay categoría. Solo hay product_id, el número de producto. El nombre y la categoría están en products.csv, y el nombre de la categoría está en una tercera tabla, categories.csv.
Los datos de una tienda casi siempre están divididos así: cada cosa se anota en un solo sitio, y las otras tablas apuntan a ella con un número. Para responder tienes que unir las tablas en una.
La idea
Unir por una clave: merge
merge toma cada fila de la tabla izquierda, busca en la tabla derecha la fila con el mismo valor en la columna clave y pega las dos filas en una. Eso se llama join (unión), y la columna por la que busca es la clave. En SQL lo mismo es JOIN.
Aquí la clave tiene un nombre distinto en cada tabla: product_id en las líneas, id en los productos. left_on y right_on dicen qué columna está en cada lado:
Las filas siguen siendo 152, y las columnas pasan a 10: las cuatro de items y las seis de products. Las columnas product_id y id tienen el mismo número, ahora dos veces.
Por eso, lo más habitual es renombrar primero la clave para que tenga el mismo nombre y unir con on. Además, tomas solo las columnas que necesitas:
Míralo
Cómo encuentra merge los pares
Aquí lo mismo con tablas pequeñas: 5 líneas de pedido y una lista de precios de 4 filas. En la lista de precios el café (17) aparece dos veces, con el precio antiguo y el nuevo, y el producto 26 todavía no está. Con how="left" se queda cada línea de pedido:
import pandas as pd
lines = pd.DataFrame({ 'order_id': [7, 7, 8, 8, 9], 'product_id': [1, 17, 5, 1, 26], 'quantity': [2, 1, 3, 1, 2],})catalog = pd.DataFrame({ 'product_id': [1, 5, 17, 17], 'name': ['Pan blanco', 'Leche fresca', 'Café en grano', 'Café en grano'], 'price': [1.2, 1.6, 13.9, 14.9],})both = lines.merge(catalog, on='product_id', how='left')print(len(lines), len(both))bothPaso a paso, en palabras
- lines son 5 líneas de pedido. product_id dice qué producto se compró.
- catalog es la lista de precios: 4 filas. El producto 17 está dos veces, con el precio antiguo y el nuevo.
- Línea 1: product_id 1. merge busca todas las filas con 1 en catalog: Pan blanco. El par se convierte en una fila de both.
- Línea 2: product_id 17 está dos veces en catalog, con 13,90 € y 14,90 €. Los dos pares se convierten en dos filas.
- Línea 3: product_id 5, es Leche fresca.
- Línea 4: product_id 1, otra vez Pan blanco. Una fila de catalog puede ser el par de muchas líneas.
- Línea 5: el producto 26 no está en catalog. Con how='left' la línea se queda, y name y price son NaN.
- Entraron 5 líneas, salieron 6 filas: la clave repetida duplicó una línea. Por eso cuentas len antes y después.
- Cada línea de pedido busca todas las filas de la lista de precios con el mismo
product_id. - La línea del café encuentra dos y se convierte en dos filas. Cinco líneas pasan a 6.
- El producto 26 no encuentra nada. Con
how="left"la línea se queda, y las columnas de la lista de precios son NaN.
Pruébalo
¿Cuántas filas?
La idea
Qué filas se quedan: how
No todas las filas tienen pareja. Dos de los 20 clientes todavía no han pedido nada, y hay 60 pedidos. El parámetro how decide qué pasa con las filas sin pareja:
inner(el valor por defecto): solo los pares. 60 filas, una por cada pedido. Los clientes sin pedidos desaparecen.left: cada fila de la izquierda, con NaN donde no hay pareja. 62 filas: 60 pedidos y los dos clientes sin pedidos.right: cada fila de la derecha. Aquí son 60, porque cada pedido tiene un cliente.outer: cada fila de los dos lados.
indicator=True te dice de dónde vino cada fila. Añade una columna _merge con los valores both, left_only y right_only:
Los dos clientes sin pedidos tienen los id 19 y 20. right_only es 0: ningún pedido pertenece a un cliente que falte en la tabla. Si lo hubiera, sería un error en los datos, y indicator lo mostraría.
Más precisamente, para curiosos
indicator es la comprobación más rápida después de un join. value_counts() sobre _merge te dice enseguida cuántas filas no encontraron pareja. Si esperas 0 y hay 300, las claves no coinciden: por ejemplo, una es el texto '17' y la otra el número 17.
Pruébalo
Productos sin reseña
¿Qué productos todavía no tienen ninguna reseña? Une products con las reseñas de modo que cada producto se quede, y guarda en no_review solo las filas sin pareja.
Solución
import pandas as pd
products = pd.read_csv('products.csv')
reviews = pd.read_csv('reviews.csv')
both = products.merge(reviews[['product_id', 'rating']], left_on='id', right_on='product_id', how='left', indicator=True)
no_review = both[both['_merge'] == 'left_only']
print(len(no_review))
no_review['name']Con how="left" e indicator=True se quedan los 25 productos, y los que no tienen reseña tienen _merge == "left_only". Son 12, entre ellos el bollo de mantequilla, la banitsa de queso y la tarjeta de regalo. Con un inner join los productos sin reseña desaparecen en el propio join, y left_only queda vacío.
La idea
Cuando la clave se repite
Ahora el gerente quiere la valoración del producto junto a cada línea de pedido. Las reseñas están en reviews.csv, y parece natural unirlas por product_id:
Las filas pasaron a ser 167 en lugar de 152, y el total de los pedidos saltó de 1963,50 € a 2485,60 €. El café tiene 3 reseñas, así que cada línea del café formó pareja con las tres y se convirtió en tres filas. La sartén (22) tiene 2 reseñas. Nada falla, pero los números ya son incorrectos.
Por eso cuentas las filas antes y después de cada join. Si la tabla derecha debe tener una fila por clave, díselo a pandas con validate:
validate="many_to_one" significa «a la izquierda la clave puede repetirse, a la derecha debe ser única». Si no lo es, merge se detiene con MergeError y te dice qué clave se repite. También existen "one_to_one" y "one_to_many".
Pruébalo
Una valoración en cada línea
Pon la valoración media del producto junto a cada línea de pedido. Las filas deben seguir siendo las que son (152), y los productos sin reseña deben recibir NaN.
Solución
import pandas as pd
items = pd.read_csv('order_items.csv')
reviews = pd.read_csv('reviews.csv')
avg = reviews.groupby('product_id', as_index=False)['rating'].mean()
with_rating = items.merge(avg, on='product_id', how='left', validate='many_to_one')
print(len(items), len(with_rating))
with_rating.head(3)Primero reviews.groupby("product_id", as_index=False)["rating"].mean() da una fila por producto: 13 productos, el café con 3,33. Ahora la clave de la derecha es única, validate="many_to_one" pasa, y las filas siguen siendo 152. 75 de ellas son de productos sin reseña y tienen NaN. Con un inner join desaparecerían, y sin groupby las filas serían 167.
La idea
Dos columnas de nombre: suffixes
Ahora viene la categoría. Tanto los productos como las categorías tienen las columnas id y name. Tras el join pandas conserva las dos y añade las terminaciones _x (izquierda) y _y (derecha). Con suffixes las eliges tú:
suffixes=("", "_category") deja las columnas del producto como están y añade _category a las de la categoría. La tarjeta de regalo no tiene categoría, así que name_category es NaN.
Aquí está la trampa de la lección. Si olvidas how="left", el inner join tira la tarjeta de regalo sin una sola advertencia:
152 filas pasan a 148. Cuatro ventas de la tarjeta de regalo, 240 € en total, desaparecen del informe por categoría. Con how="left" las filas siguen siendo 152, y la categoría que falta aparece como NaN.
La idea
Una debajo de otra: concat
A veces las tablas no deben pegarse una al lado de otra, sino una debajo de otra. La tienda cambió de sistema: los pedidos hasta finales de 2025 se exportaron del antiguo, y los de 2026 del nuevo, con las mismas columnas. pd.concat las apila en una tabla:
21 + 39 = 60 filas. ignore_index=True vuelve a numerar las filas desde 0. Sin él se conservan los índices de las dos tablas y pueden repetirse.
Las fechas aquí son texto como «2025-09-01», así que comparar < '2026-01-01' en orden alfabético coincide con el orden en el tiempo. Las fechas de verdad vienen en la lección «Fechas y series temporales».
Más precisamente, para curiosos
merge busca pares por una clave y pone las columnas una al lado de otra. concat no busca nada: apila las filas (o, con axis=1, las columnas) en el orden de las tablas. Si las columnas no coinciden, concat funciona igualmente, pero las celdas que faltan se vuelven NaN, así que compara antes df.columns de las dos tablas.
Ejercicio
El informe por categoría
Responde al gerente: ¿cuánto es el ingreso de cada categoría, sin los pedidos cancelados? Une las líneas con los pedidos (para status), con los productos (para category_id) y con las categorías, de modo que después de cada join sigan siendo 152 filas. Los productos sin categoría van bajo «Sin categoría». Guarda el resultado en by_category.
Solución
import pandas as pd
items = pd.read_csv('order_items.csv')
orders = pd.read_csv('orders.csv').rename(columns={'id': 'order_id'})
products = pd.read_csv('products.csv').rename(columns={'id': 'product_id'})
categories = pd.read_csv('categories.csv').rename(columns={'id': 'category_id', 'name': 'category'})
lines = items.merge(orders[['order_id', 'status']], on='order_id', how='left', validate='many_to_one')
lines = lines.merge(products[['product_id', 'category_id']], on='product_id', how='left', validate='many_to_one')
lines = lines.merge(categories[['category_id', 'category']], on='category_id', how='left', validate='many_to_one')
print(len(items), len(lines))
lines['category'] = lines['category'].fillna('Sin categoría')
sold = lines[lines['status'] != 'cancelled']
sold['revenue'] = sold['quantity'] * sold['unit_price']
by_category = sold.groupby('category')['revenue'].sum().round(2).sort_values(ascending=False)
by_categoryCon how="left" y validate="many_to_one" los tres joins conservan 152 filas. fillna("Sin categoría") da un nombre a la tarjeta de regalo. Sin los pedidos cancelados quedan 131 líneas, 1697,00 € en total. «Cocina» aporta más, 516,90 €, y la tarjeta de regalo hace 160,00 €. Con un inner join la tarjeta desaparece, y si no quitas los cancelados, «Cocina» sale con 601,50 €.
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Filtrar y ordenar · Columnas nuevas · Tipos y valores faltantes · Grupos: groupby
- Continúa en: Ancho y largo: pivot_table y melt · Fechas y series temporales · El primer gráfico · Qué gráfico para qué pregunta
- Ejercicios sobre el tema: Preparar los datos