Joins: merge y concat
Dos operaciones distintas: merge y concat
Combinar DataFrames es dos operaciones diferentes, no una sola:
merge(): junta DataFrames emparejando filas por una columna clave. Como un join de SQL o elleft_join()de dplyr.concat(): apila DataFrames poniendo uno encima del otro (o uno al lado del otro). Comorbind()ocbind()de R.
Confundirlas es uno de los errores más frecuentes al venir de R o de SQL. Antes de combinar, pregúntate: ¿quiero emparejar filas por una clave (merge) o simplemente añadir filas/columnas (concat)?
merge(): joins relacionales
merge() empareja filas de dos DataFrames usando una clave común.
import pandas as pd
ventas = pd.DataFrame({
"cliente_id": [1, 2, 3, 4],
"monto": [1500, 2100, 1800, 2400],
})
clientes = pd.DataFrame({
"cliente_id": [1, 2, 3, 5],
"nombre": ["Ana", "Bea", "Carlos", "Diego"],
"ciudad": ["Madrid", "Barcelona", "Valencia", "Sevilla"],
})
ventas.merge(clientes, on="cliente_id")
# cliente_id monto nombre ciudad
# 0 1 1500 Ana Madrid
# 1 2 2100 Bea Barcelona
# 2 3 1800 Carlos ValenciaEl cliente 4 desaparece (no está en clientes) y Diego desaparece (no tiene ventas). Es el comportamiento inner join por defecto: solo filas con clave en ambos lados.
Los cuatro tipos de join
El argumento how controla qué filas se conservan:
ventas.merge(clientes, on="cliente_id", how="left") # todas las de ventas
ventas.merge(clientes, on="cliente_id", how="right") # todas las de clientes
ventas.merge(clientes, on="cliente_id", how="inner") # solo coincidencias (default)
ventas.merge(clientes, on="cliente_id", how="outer") # todas, rellenando NaNResumen mental:
how |
Qué hace |
|---|---|
"inner" (default) |
Solo filas presentes en ambos |
"left" |
Todas las del izquierdo. Relleno NaN en el derecho |
"right" |
Todas las del derecho. Relleno NaN en el izquierdo |
"outer" |
Unión completa. NaN donde falte |
Equivalente exacto a los joins de dplyr (left_join, right_join, inner_join, full_join).
Cuándo cada uno
left: el caso más común en análisis. “Empieza con la tabla principal, añade información de otra”. Equivalente alleft_joinde dplyr, el que se usa el 80 % del tiempo.inner: cuando quieres solo lo que tiene match en ambos lados.outer: para detectar qué falta. Pista: usa el argumentoindicator=True(más abajo).right: raro en la práctica, simplemente cambia el orden de los argumentos y usaleft.
Claves con nombres distintos
Cuando la columna clave se llama distinto en cada DataFrame:
ventas.merge(
clientes,
left_on="cliente_id",
right_on="id",
)Resultado: tendrás las dos columnas (cliente_id y id), son redundantes. Para limpiar: .drop(columns="id").
Merge por índice
Si una columna clave coincide con el índice del otro DataFrame:
clientes_idx = clientes.set_index("cliente_id")
ventas.merge(clientes_idx, left_on="cliente_id", right_index=True)Y si ambos tienen la clave en el índice, hay un atajo:
ventas.set_index("cliente_id").join(clientes_idx).join() es básicamente merge por índice. Menos flexible que merge pero más conciso para joins simples.
El gotcha: many-to-many
Si la clave se repite en uno o ambos DataFrames, el merge multiplica filas:
ordenes = pd.DataFrame({
"cliente_id": [1, 1, 2],
"producto": ["A", "B", "C"],
})
direcciones = pd.DataFrame({
"cliente_id": [1, 1, 2],
"tipo": ["casa", "trabajo", "casa"],
})
ordenes.merge(direcciones, on="cliente_id")
# cliente_id producto tipo
# 0 1 A casa
# 1 1 A trabajo
# 2 1 B casa
# 3 1 B trabajo
# 4 2 C casaEl cliente 1 tenía 2 órdenes y 2 direcciones → quedan 4 filas (producto cartesiano). Esto suele ser un bug, no lo que querías. Para evitar sorpresas:
ordenes.merge(direcciones, on="cliente_id", validate="one_to_one")validate lanza error si la cardinalidad no es la esperada:
"one_to_one","one_to_many","many_to_one","many_to_many".
Recomendación: pon siempre validate en merges importantes. Te avisa de claves duplicadas inesperadas antes de que te muerdan en producción.
indicator=True: detectar qué falta
ventas.merge(clientes, on="cliente_id", how="outer", indicator=True)
# cliente_id monto nombre ciudad _merge
# 0 1 1500.0 Ana Madrid both
# 1 2 2100.0 Bea Barcelona both
# 2 3 1800.0 Carlos Valencia both
# 3 4 2400.0 NaN NaN left_only
# 4 5 NaN Diego Sevilla right_onlyUna columna _merge te dice de dónde viene cada fila: both, left_only, right_only. Ideal para diagnóstico, “qué clientes en mi tabla no están en el sistema CRM”.
concat(): apilar
concat() apila DataFrames sin alinear por clave. Como rbind o bind_rows de R.
ventas_2023 = pd.DataFrame({"mes": [1, 2], "monto": [100, 200]})
ventas_2024 = pd.DataFrame({"mes": [1, 2], "monto": [150, 250]})
pd.concat([ventas_2023, ventas_2024])
# mes monto
# 0 1 100
# 1 2 200
# 0 1 150 ← índice duplicado
# 1 2 250Atención al índice: concat mantiene los índices originales, lo que crea duplicados. Casi siempre quieres ignore_index=True:
pd.concat([ventas_2023, ventas_2024], ignore_index=True)
# mes monto
# 0 1 100
# 1 2 200
# 2 1 150
# 3 2 250Apilar horizontal (columnas)
pd.concat([df_a, df_b], axis=1)Pega columnas lado a lado. Importante: empareja por índice, no por orden. Si los índices no coinciden, aparece NaN. Para append puro, ambos deben tener el mismo índice.
Concat con columnas distintas
Si los DataFrames tienen columnas distintas, concat rellena con NaN las que faltan en cada uno:
a = pd.DataFrame({"x": [1, 2], "y": [3, 4]})
b = pd.DataFrame({"x": [5, 6], "z": [7, 8]})
pd.concat([a, b], ignore_index=True)
# x y z
# 0 1 3.0 NaN
# 1 2 4.0 NaN
# 2 5 NaN 7.0
# 3 6 NaN 8.0Es práctico pero también peligroso, un error tipográfico en un nombre de columna te da NaNs silenciosos. Verifica con .info() después.
Atajo: añadir una fila
En lugar de df.append({"x": 1, "y": 2}) (deprecado desde pandas 2.0):
nueva = pd.DataFrame([{"x": 1, "y": 2}])
df = pd.concat([df, nueva], ignore_index=True)append ya no existe. La forma moderna es siempre concat.
Resumen práctico
- ¿Empareja filas por una clave? →
merge(). - ¿Apila filas con la misma estructura? →
concat([..], ignore_index=True). - ¿Junta columnas alineadas por índice? →
concat([..], axis=1).
Trampas habituales
- Many-to-many inesperado. La causa #1 de bugs al combinar tablas. Usa
validate="one_to_one"ovalidate="one_to_many"para que pandas avise. - Índices duplicados tras
concat. Olvidaignore_index=Truey verás dos filas con el mismo índice 0. Sucede en silencio hasta que.loc[0]devuelve dos filas y rompe operaciones aguas abajo. mergeque pierde filas sin esperarlo. El default esinner, si no esperas perder filas, sé explícito conhow="left".- Columnas duplicadas con sufijos automáticos. Si los dos DataFrames tienen columnas con el mismo nombre (que no son la clave), pandas las renombra con sufijos
_xy_y. Para controlar:suffixes=("_origen", "_destino"). - Concat de DataFrames con columnas mal escritas. Si en uno la columna se llama
Clientey en otrocliente,concatcrea dos columnas separadas. Normaliza nombres antes de combinar.
En la siguiente entrega
Tienes manipulación, agregación, reshape y joins, el núcleo del análisis tabular. Lo siguiente es un dominio en sí mismo: fechas. pd.to_datetime, el DatetimeIndex, las operaciones de resample y los patrones de filtrado temporal. Lo siguiente.