Indice / pandas: combinar y resumir / 6.2
merge: la clave común
Cruzar dos tablas por una columna que comparten. Es la operación
más poderosa de pandas y la que más silenciosamente pierde filas: el
how por defecto tira todo lo que no encuentra pareja, y no dice
cuánto.
Para qué sirve
Los datos casi nunca vienen en una sola tabla. Los productos están en una, el stock en otra; los estudiantes en una, las inscripciones en otra. Juntarlos es el trabajo, y para juntarlos hace falta una clave: una columna que diga qué fila de acá corresponde a qué fila de allá.
Lo que hay que aprender no es la sintaxis - son tres palabras -, es qué filas sobreviven. Ahí se decide si el informe está completo o le faltan 24 productos que nadie va a echar de menos.
ELI5Explicado facil
Tenés dos listas de invitados: una con los nombres y los teléfonos, otra con los nombres y si confirmaron. El nombre es lo que permite juntarlas.
La pregunta importante no es cómo se juntan, es qué hacés con el que está
en una lista y no en la otra. ¿Lo dejás afuera? ¿Lo ponés con el teléfono en
blanco? Esa decisión es el how, y cambiarla cambia cuánta gente entra a
la fiesta.
El ejemplo mínimo
import pandas as pd
precios = pd.DataFrame({"sku": ["A1", "A2", "A3"],
"precio": [10.0, 20.0, 30.0]})
stock = pd.DataFrame({"sku": ["A3", "A1", "A9"],
"unidades": [5, 7, 2]})
print("precios:")
print(precios.to_string(index=False))
print()
print("stock:")
print(stock.to_string(index=False))
print()
print('merge(on="sku") -> por defecto how="inner":')
print(precios.merge(stock, on="sku").to_string(index=False))
print()
print("A2 no esta en stock y A9 no esta en precios: los dos se caen.")
print("El resultado tiene 2 filas, menos que cualquiera de las dos tablas,")
print("y ese es el unico aviso que vas a recibir.")
precios:
sku precio
A1 10.0
A2 20.0
A3 30.0
stock:
sku unidades
A3 5
A1 7
A9 2
merge(on="sku") -> por defecto how="inner":
sku precio unidades
A1 10.0 7
A3 30.0 5
A2 no esta en stock y A9 no esta en precios: los dos se caen.
El resultado tiene 2 filas, menos que cualquiera de las dos tablas,
y ese es el unico aviso que vas a recibir.
Lo mismo, paso a paso
Las mismas dos tablas y los cuatro how, uno por paso. Mirá las dos
tablas de arriba y la de resultado a la vez: lo que cambia es qué filas sobreviven y
cuáles quedan con celdas vacías.
- Las dos tablasCuatro filas de cada catalogo. La columna sku es la clave: es lo unico que las dos tablas tienen en comun y lo que permite decir que fila de una corresponde a que fila de la otra. SKU015 y SKU016 estan en las dos; SKU013 y SKU014 solo en A; SKU061 y SKU062 solo en B.
a = pd.read_csv("catalogo_sistema_a.csv") - how="inner"El default. Solo sobreviven las claves que estan en LAS DOS tablas. Lo que esta en una sola se cae, y nadie avisa: el resultado tiene menos filas que cualquiera de las dos entradas y eso es todo lo que se ve.
a.merge(b, on="sku", how="inner", suffixes=("_a", "_b")) - how="left"Se conservan TODAS las filas de la izquierda. Las que no encontraron pareja quedan con las columnas de la derecha vacias. No se pierde nada de A, y aparecen ausentes que antes no existian.
a.merge(b, on="sku", how="left", suffixes=("_a", "_b")) - how="right"El espejo: todas las de la derecha. Se usa poco, porque casi siempre es mas claro dar vuelta las tablas y usar left.
a.merge(b, on="sku", how="right", suffixes=("_a", "_b")) - how="outer"Todo lo de las dos. Es el unico que no pierde nada, y por eso es el que conviene cuando el trabajo es unificar dos fuentes: primero traer todo, y despues decidir que hacer con lo que aparece en un solo lado.
a.merge(b, on="sku", how="outer", suffixes=("_a", "_b"))
Sobre datos de verdad
import pandas as pd
def unico(nombre):
"""Cada catalogo trae SKU repetidos: se deduplica por recencia primero."""
t = pd.read_csv(nombre)
return (t.sort_values("actualizado", kind="stable")
.drop_duplicates(subset=["sku"], keep="last"))
a = unico("catalogo_sistema_a.csv")
b = unico("catalogo_sistema_b.csv")
print("catalogo A:", len(a), "productos")
print("catalogo B:", len(b), "productos")
inner = a.merge(b, on="sku", how="inner", suffixes=("_a", "_b"))
print()
print("inner :", len(inner), "filas")
print("se pierden de A:", len(a) - len(inner))
print("se pierden de B:", len(b) - len(inner))
print()
print(inner[["sku", "precio_a", "precio_b"]].head(4).to_string(index=False))
print()
print("24 productos desaparecieron y el programa no dijo nada.")
print("Si el informe se hace sobre este resultado, esos 24 no existen.")
catalogo A: 60 productos
catalogo B: 56 productos
inner : 46 filas
se pierden de A: 14
se pierden de B: 10
sku precio_a precio_b
SKU060 582.17 652.03
SKU057 202.75 202.75
SKU054 225.22 225.22
SKU015 146.43 164.00
24 productos desaparecieron y el programa no dijo nada.
Si el informe se hace sobre este resultado, esos 24 no existen.
inner perdió 24 productos y no dijo nada
60 productos en A, 56 en B, 46 en el resultado. Los 14 que están solo en A y los 10 que están solo en B desaparecieron. Si el informe se hace sobre ese resultado, esos 24 productos no existen.
inner es el default. Es la elección correcta muchas veces, pero
tiene que ser una elección, no algo que pasó porque no escribiste
how.
how="left"
import pandas as pd
def unico(nombre):
t = pd.read_csv(nombre)
return (t.sort_values("actualizado", kind="stable")
.drop_duplicates(subset=["sku"], keep="last"))
a = unico("catalogo_sistema_a.csv")
b = unico("catalogo_sistema_b.csv")
izquierda = a.merge(b, on="sku", how="left", suffixes=("_a", "_b"))
print("left:", len(izquierda), "filas (las mismas que A)")
print("sin pareja en B:", int(izquierda["precio_b"].isna().sum()))
print()
print(izquierda.loc[izquierda["precio_b"].isna(),
["sku", "precio_a", "precio_b"]].head(4).to_string(index=False))
print()
print("Los NaN de precio_b NO estaban en ningun archivo: los invento el merge")
print("para las filas que no encontraron pareja. Distinguirlos de un ausente")
print("de verdad es imposible mirando el resultado, y por eso conviene")
print("marcarlos en el momento con indicator=True (leccion 6.3).")
left: 60 filas (las mismas que A)
sin pareja en B: 14
sku precio_a precio_b
SKU008 635.69 NaN
SKU012 445.66 NaN
SKU011 767.47 NaN
SKU004 NaN NaN
Los NaN de precio_b NO estaban en ningun archivo: los invento el merge
para las filas que no encontraron pareja. Distinguirlos de un ausente
de verdad es imposible mirando el resultado, y por eso conviene
marcarlos en el momento con indicator=True (leccion 6.3).
Con left no se pierde nada de la tabla principal, y aparecen
NaN donde no hubo pareja. Ojo con eso: esos ausentes no estaban
en ningún archivo, los inventó el merge. Mirando el resultado
son indistinguibles de un dato que faltaba de verdad, y por eso conviene marcarlos en
el momento con indicator=True - que es la lección siguiente.
Cuando la clave se llama distinto
import pandas as pd
inscripciones = pd.read_csv("academico_inscripciones.csv")
estudiantes = pd.read_csv("academico_estudiantes.csv")
print("inscripciones:", list(inscripciones.columns))
print("estudiantes :", list(estudiantes.columns))
print()
print("La misma cosa se llama estudiante_id en una tabla e id en la otra.")
print("Es lo normal: cada tabla nombra su clave como le queda mejor.")
cruce = inscripciones.merge(estudiantes, left_on="estudiante_id",
right_on="id", how="left")
print()
print("merge(left_on='estudiante_id', right_on='id'):", len(cruce), "filas")
print(cruce[["estudiante_id", "id", "legajo", "nombre", "curso_id", "nota"]]
.head(3).to_string(index=False))
print()
print("Quedaron las DOS columnas de clave, con el mismo contenido.")
print("Conviene sacar una: cruce.drop(columns='id').")
print()
sin_pareja = int(cruce["legajo"].isna().sum())
print("inscripciones sin estudiante en el padron:", sin_pareja)
print("Con how='left' esas filas se conservan, con el legajo vacio.")
print("Con how='inner' habrian desaparecido sin dejar rastro.")
inscripciones: ['estudiante_id', 'curso_id', 'nota']
estudiantes : ['id', 'legajo', 'nombre', 'correo']
La misma cosa se llama estudiante_id en una tabla e id en la otra.
Es lo normal: cada tabla nombra su clave como le queda mejor.
merge(left_on='estudiante_id', right_on='id'): 110 filas
estudiante_id id legajo nombre curso_id nota
35 35.0 E035 Gabriela Bravo 5 5.8
28 28.0 E028 Andrés Duarte 4 7.5
22 22.0 E022 Pablo Escobar 5 7.5
Quedaron las DOS columnas de clave, con el mismo contenido.
Conviene sacar una: cruce.drop(columns='id').
inscripciones sin estudiante en el padron: 1
Con how='left' esas filas se conservan, con el legajo vacio.
Con how='inner' habrian desaparecido sin dejar rastro.
35.0 de la columna id
Aparece con decimales porque el left dejó un NaN en esa
columna - hay una inscripción cuyo estudiante no está en el padrón - y un
int64 común no puede representar un ausente, así que pandas convierte la
columna entera a float64.
Que una columna de enteros se vuelva decimal después de un merge es,
de hecho, una señal barata de que quedaron filas sin pareja.
Dónde se rompe
import pandas as pd
a = pd.read_csv("catalogo_sistema_a.csv")
b = pd.read_csv("catalogo_sistema_b.csv")
print("A:", len(a), "filas |", a["sku"].nunique(), "sku distintos")
print("B:", len(b), "filas |", b["sku"].nunique(), "sku distintos")
print("sku repetidos en A:",
sorted(a.loc[a["sku"].duplicated(keep=False), "sku"].unique()))
print("sku repetidos en B:",
sorted(b.loc[b["sku"].duplicated(keep=False), "sku"].unique()))
sin_limpiar = a.merge(b, on="sku", how="inner", suffixes=("_a", "_b"))
print()
print("inner SIN deduplicar:", len(sin_limpiar), "filas")
def unico(t):
return (t.sort_values("actualizado", kind="stable")
.drop_duplicates(subset=["sku"], keep="last"))
limpio = unico(a).merge(unico(b), on="sku", how="inner", suffixes=("_a", "_b"))
print("inner deduplicando :", len(limpio), "filas")
print()
print("Una fila de diferencia, y viene de SKU020, que esta dos veces en B.")
print(sin_limpiar.loc[sin_limpiar["sku"] == "SKU020",
["sku", "precio_a", "precio_b"]].to_string(index=False))
print()
print("Un merge puede devolver MAS filas que cualquiera de sus entradas.")
print("Si la clave no es unica en las dos tablas, cada par se cruza con cada")
print("par. Con dos duplicados no se nota; con veinte, el archivo explota.")
A: 62 filas | 60 sku distintos
B: 57 filas | 56 sku distintos
sku repetidos en A: ['SKU002', 'SKU007']
sku repetidos en B: ['SKU020']
inner SIN deduplicar: 47 filas
inner deduplicando : 46 filas
Una fila de diferencia, y viene de SKU020, que esta dos veces en B.
sku precio_a precio_b
SKU020 256.96 287.8
SKU020 256.96 145.0
Un merge puede devolver MAS filas que cualquiera de sus entradas.
Si la clave no es unica en las dos tablas, cada par se cruza con cada
par. Con dos duplicados no se nota; con veinte, el archivo explota.
Un merge puede devolver más filas que cualquiera de
sus entradas. Si la clave se repite en las dos tablas, cada aparición de un lado se
cruza con cada aparición del otro. Acá es una fila de más y casi no se nota; con
veinte duplicados, el archivo se multiplica.
Causa: how="inner", que es el default, descarta lo que no
encuentra pareja en las dos tablas.
Arreglo: escribir siempre how= a propósito, y comparar
len antes y después. Si querés conservar todo,
how="outer".
Causa: la clave está repetida en alguna de las dos tablas y el cruce multiplicó.
Arreglo: df["clave"].is_unique en las dos antes de
cruzar, o validate="1:1", que falla en el acto (lección 6.3).
Causa: las dos tablas tienen columnas con el mismo nombre además de la clave, y los sufijos no alcanzan para desambiguarlas.
Arreglo: pasar suffixes=("_a", "_b") explícito, o renombrar
antes de cruzar.
Causa: las claves no son del mismo tipo - "35" contra
35 - o tienen espacios de más.
Arreglo: comparar dtypes de las dos claves antes de cruzar.
pandas no convierte por vos y tampoco se queja: devuelve 0 filas.
Causa: el merge dejó ausentes en esa columna, y
int64 no los admite.
Arreglo: ninguno inmediato: es la señal de que hay filas sin pareja.
Si el ausente es legítimo, astype("Int64") después.
Acá hay que elegir
how corresponde
inner- cuando una fila sin pareja no sirve para nada: una venta sin producto conocido no se puede facturar. Y aun así hay que contar cuántas se fueron.left- cuando una tabla manda y la otra la enriquece. Es el caso más común: «todas mis ventas, con el nombre del cliente si lo tengo». Nunca se pierde una venta.outer- cuando el trabajo es unificar dos fuentes y todavía no sabés qué hacer con lo que aparece en un solo lado. Traés todo y después decidís.right- casi nunca: es más claro dar vuelta las tablas y usarleft.
La regla que ahorra el error: escribí siempre how=, aunque
sea "inner". Un default no es una decisión, y acá la decisión
cambia cuántas filas tiene el informe.
Recetario
| Expresión | Qué hace | Cuándo |
|---|---|---|
a.merge(b, on="k") | Cruce inner por esa columna | Falta el how explícito |
a.merge(b, on="k", how="left") | Todas las de a | Enriquecer sin perder |
a.merge(b, on="k", how="outer") | Todo de las dos | Unificar dos fuentes |
on=["k1", "k2"] | Clave compuesta | Cuando una sola columna no identifica |
left_on= / right_on= | La clave se llama distinto | Lo habitual entre tablas de sistemas distintos |
suffixes=("_a", "_b") | Desambigua columnas homónimas | Siempre que haya nombres repetidos |
a["k"].is_unique | bool | Antes de cruzar, en las dos tablas |
a["k"].dtype == b["k"].dtype | bool | Cuando el cruce devuelve 0 filas |
len(resultado) vs len(a) | Perdió o multiplicó | Después de todo merge |