Saltar al contenido

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

EjemploDos tablas, una clave en comun
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.

Paso a pasoLos cuatro how de merge sobre las mismas dos tablas
  1. 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")
  2. 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"))
  3. 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"))
  4. 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"))
  5. 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

EjemploSobre datos de verdad: cuanto pierde un inner
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.
El 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"

Ejemplohow="left": no perder nada de la tabla principal
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

EjemploCuando la clave se llama distinto en cada tabla
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.
El 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

EjemploEl merge que agrega filas: la clave repetida multiplica
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.

(sin error) el resultado tiene menos filas de las esperadas

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".

(sin error) el resultado tiene más filas que las dos entradas

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).

MergeError: Passing 'suffixes' which cause duplicate columns

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.

(sin error) el merge no encontró ninguna pareja

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.

(sin error) una columna de enteros salió con decimales

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

Qué 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 usar left.

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

merge
ExpresiónQué haceCuándo
a.merge(b, on="k")Cruce inner por esa columnaFalta el how explícito
a.merge(b, on="k", how="left")Todas las de aEnriquecer sin perder
a.merge(b, on="k", how="outer")Todo de las dosUnificar dos fuentes
on=["k1", "k2"]Clave compuestaCuando una sola columna no identifica
left_on= / right_on=La clave se llama distintoLo habitual entre tablas de sistemas distintos
suffixes=("_a", "_b")Desambigua columnas homónimasSiempre que haya nombres repetidos
a["k"].is_uniqueboolAntes de cruzar, en las dos tablas
a["k"].dtype == b["k"].dtypeboolCuando el cruce devuelve 0 filas
len(resultado) vs len(a)Perdió o multiplicóDespués de todo merge

Ponete a prueba

1. ¿Cual es el how por defecto de merge?Nivel 1 · Recordar
2. Dos tablas de 60 y 56 filas se cruzan con how="inner" y el resultado tiene 46. ¿Que paso con las otras?Nivel 1 · Recordar
3. ¿De donde salen los NaN que aparecen despues de un how="left"?Nivel 2 · Predecir la salida
4. Cruzas dos tablas por una clave que se llama estudiante_id en una e id en la otra. ¿Como se escribe?Nivel 2 · Predecir la salida
5. Cruzas dos tablas de 62 y 57 filas con inner y el resultado tiene 47. Deduplicando primero da 46. ¿De donde sale la fila de mas?Nivel 3 · Elegir entre dos casi correctas
6. Un merge devuelve 0 filas y estas seguro de que las claves coinciden. ¿Que conviene mirar primero?Nivel 3 · Elegir entre dos casi correctas
7. Este codigo tiene que dar el total facturado. Da menos de lo que dice la contabilidad, y las 500 ventas estan en el archivo. ¿Por que?
ventas = pd.read_csv("dw_hecho_ventas.csv")
prod = pd.read_csv("dw_dim_producto.csv")
cruce = ventas.merge(prod, on="producto_id")
print(cruce["ingreso"].sum())
Nivel 4 · Encontrar el defecto

Prácticas de esta lección