Joins: combinar tablas sin perder filas por error

r
tidyverse
Los 4 joins que importan (left, inner, full, anti), la regla de oro de validar claves antes del join, y cómo evitar duplicación silenciosa de filas.

¿Qué es un join y por qué importa?

Casi nadie llega a un análisis con todos los datos en una sola tabla. Lo normal es tener:

  • Una tabla de ventas con id_cliente, id_producto, cantidad, fecha.
  • Una tabla de clientes con id_cliente, nombre, pais, segmento.
  • Una tabla de productos con id_producto, categoria, precio_unidad.

Para responder “¿cuáles son las categorías más vendidas en cada país?” tienes que combinar esas tres tablas. Esa operación es un join.

En R base existe merge(). Igual que pasó con [ y select(), dplyr ofrece una API más expresiva y compatible con el pipe. Y, lo más importante, con comportamiento explícito ante claves duplicadas.

El patrón mental: claves y dos tablas

Para hacer un join necesitas:

  1. Dos tablas: la “izquierda” (x) y la “derecha” (y).
  2. Una clave: la columna (o columnas) que tienen valor común en ambas. Ejemplo: id_cliente en ventas y en clientes.
library(dplyr)

ventas |>
  left_join(clientes, by = "id_cliente")

by = "id_cliente" dice cómo emparejar las filas: una fila de ventas se combina con la fila de clientes que tenga el mismo id_cliente.

Si las columnas se llaman distinto en cada tabla:

ventas |>
  left_join(clientes, by = c("cliente_id" = "id"))

Y si la clave es compuesta (varias columnas):

ventas |>
  left_join(precios, by = c("id_producto", "fecha"))

Los 4 joins que importan

Función Qué hace Cuándo usar
left_join() Todas las filas de x + columnas de y donde haya match El default. Conservas todo lo que tienes y enriqueces con la otra tabla.
inner_join() Solo las filas que hacen match en ambas tablas Cuando solo te interesan filas presentes en ambas.
full_join() Todas las filas de ambas (con NA donde no hay match) Raro en producción. Útil para auditar diferencias.
anti_join() Filas de x que NO hacen match en y Filtrar por ausencia (“clientes sin ventas”).
# left_join: el caso 95% de las veces
ventas |> left_join(clientes, by = "id_cliente")

# inner_join: descartar ventas a clientes desconocidos
ventas |> inner_join(clientes, by = "id_cliente")

# anti_join: ¿qué clientes no han comprado nada?
clientes |> anti_join(ventas, by = "id_cliente")

La regla de oro: valida claves ANTES del join

Esta es la sección que separa código profesional de código que “funciona en mi máquina”.

Antes de un join, comprueba siempre cuántas filas tiene cada clave en la tabla derecha. Si tu tabla clientes se supone que tiene un id_cliente único pero por error hay duplicados, el join va a multiplicar filas en silencio.

Cómo se hace:

clientes |>
  count(id_cliente, sort = TRUE) |>
  filter(n > 1)

Si esto devuelve 0 filas, id_cliente es único en clientes y el join es seguro. Si devuelve algo, tienes un problema antes de hacer el join, lo arreglas primero.

dplyr 1.1+ trae un mecanismo formal para esto:

ventas |>
  left_join(clientes, by = "id_cliente", relationship = "many-to-one")

relationship = "many-to-one" significa: “muchas ventas pueden corresponder a un mismo cliente, pero cada id_cliente aparece solo una vez en clientes. Si la afirmación es falsa, dplyr lanza un error, no un warning. Has bloqueado el bug silencioso.

Otras relaciones disponibles:

  • "one-to-one": clave única en ambas tablas.
  • "one-to-many": clave única en x, puede repetirse en y.
  • "many-to-many": clave puede repetirse en ambas (raro y casi siempre un error).

Adopta el hábito: especifica relationship siempre que conozcas la cardinalidad. Pequeño coste de escribir, gran red de seguridad.

Cuando hay duplicados: el bug clásico

Imagina que tienes 1.000 ventas y 1.000 clientes. Haces:

ventas |> left_join(clientes, by = "id_cliente")

Y el resultado tiene 3.500 filas, no 1.000. ¿Qué pasó?

Hay clientes duplicados en clientes. Quizá un cliente aparece tres veces (con datos ligeramente diferentes por un error de carga). El join multiplica cada venta de ese cliente por las tres filas que coinciden.

Diagnóstico:

clientes |>
  count(id_cliente) |>
  filter(n > 1)

Si encuentras duplicados, las opciones son:

  • Limpiar antes del join quedándote con un único registro por clave:

    clientes |> distinct(id_cliente, .keep_all = TRUE)
  • Aceptar la multiplicación explícitamente con relationship = "many-to-many". Pero ahora sabes lo que estás haciendo y no es un accidente.

semi_join y anti_join: filtrar con tabla externa

left_join y inner_join añaden columnas además de filtrar. A veces solo quieres filtrar:

# Ventas hechas por clientes premium (sin añadir columnas de clientes)
ventas |>
  semi_join(filter(clientes, segmento == "premium"), by = "id_cliente")

semi_join mantiene solo las filas de x que tienen match en y, sin añadir columnas de y. Es el filtro relacional.

# Clientes que NO tienen ninguna venta
clientes |>
  anti_join(ventas, by = "id_cliente")

anti_join es lo contrario: filas de x que NO tienen match en y. Útil para auditar (“¿qué clientes no han comprado nada?”, “¿qué productos nunca se han vendido?”).

Trampas habituales

  • El join silencioso que multiplica filas. El bug número uno. Valida la cardinalidad de la clave en la tabla derecha antes de cualquier join, o usa relationship = "many-to-one".
  • Joins sin by = explícito. Sin by, dplyr empareja por todas las columnas con el mismo nombre en ambas tablas y te muestra un mensaje. Si las tablas comparten columnas que no son claves (por ejemplo, ambas tienen fecha), el join sale mal. Especifica by siempre.
  • Sufijos por defecto cuando hay columnas con el mismo nombre. Si x e y tienen ambas una columna nombre (que no es la clave), el resultado las llama nombre.x y nombre.y. Controla esto con suffix = c("_ventas", "_clientes") cuando quieras nombres descriptivos.
  • NA en la clave. NA == NA evalúa a NA, no a TRUE. Filas con NA en la clave NO se emparejan (ni siquiera entre sí). Si necesitas que sí lo hagan, sustituye los NA por un valor centinela antes del join.

En la siguiente entrega

Has aprendido a combinar varias tablas. El siguiente reto es reorganizar una sola tabla entre formato ancho y largo: lo que el tidyverse llama tidy data. Esa transformación es lo que separa datos “para humanos” de datos “para análisis”, y se hace con pivot_longer y pivot_wider. Es lo siguiente.