Joins: combinar tablas sin perder filas por error
¿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:
- Dos tablas: la “izquierda” (
x) y la “derecha” (y). - Una clave: la columna (o columnas) que tienen valor común en ambas. Ejemplo:
id_clienteen 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 enx, puede repetirse eny."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. Sinby, 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 tienenfecha), el join sale mal. Especificabysiempre. - Sufijos por defecto cuando hay columnas con el mismo nombre. Si
xeytienen ambas una columnanombre(que no es la clave), el resultado las llamanombre.xynombre.y. Controla esto consuffix = c("_ventas", "_clientes")cuando quieras nombres descriptivos. - NA en la clave.
NA == NAevalúa aNA, no aTRUE. Filas conNAen la clave NO se emparejan (ni siquiera entre sí). Si necesitas que sí lo hagan, sustituye losNApor 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.