Joins: merge y concat

python
pandas
eda
Cómo combinar DataFrames en pandas. merge() para joins relacionales con sus cuatro tipos (left, right, inner, outer), concat() para apilar y los gotchas de claves duplicadas y many-to-many.

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 el left_join() de dplyr.
  • concat(): apila DataFrames poniendo uno encima del otro (o uno al lado del otro). Como rbind() o cbind() 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   Valencia

El 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 NaN

Resumen 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 al left_join de 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 argumento indicator=True (más abajo).
  • right: raro en la práctica, simplemente cambia el orden de los argumentos y usa left.

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    casa

El 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_only

Una 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    250

Atenció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    250

Apilar 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.0

Es 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" o validate="one_to_many" para que pandas avise.
  • Índices duplicados tras concat. Olvida ignore_index=True y verás dos filas con el mismo índice 0. Sucede en silencio hasta que .loc[0] devuelve dos filas y rompe operaciones aguas abajo.
  • merge que pierde filas sin esperarlo. El default es inner, si no esperas perder filas, sé explícito con how="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 _x y _y. Para controlar: suffixes=("_origen", "_destino").
  • Concat de DataFrames con columnas mal escritas. Si en uno la columna se llama Cliente y en otro cliente, concat crea 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.