lección 7
Combinar y remodelar DataFrames
Merge, concat, melt y pivot: unir tablas y girar de formato largo a ancho (y viceversa) con pandas.
⏱ 50 min
### El problema de las dos hojas de cálculo
Dos hojas de cálculo que deberían ser una: la historia más repetida del mundo de los datos. Ventas tiene los pedidos con su importe y su fecha; CRM tiene los clientes con su ciudad y su segmento. Viven separadas porque las mantienen equipos distintos, pero cualquier pregunta interesante las necesita juntas: «¿están cayendo las ventas en el norte?» requiere cruzar el pedido con la ciudad del cliente. En SQL lo resolverías con un JOIN. En pandas la operación se llama merge y funciona con la misma lógica — columna en común, tipo de cruce (inner, left, outer) — pero la sintaxis tiene sus propias reglas. Y combinar no es solo cruzar: a veces necesitas apilar tablas (concat, el equivalente de UNION) o girar la forma de una tabla de larga a ancha y viceversa (pivot y melt). Estos cuatro — merge, concat, melt, pivot — resuelven casi todos los problemas de forma y combinación que te vas a encontrar.
Pero combinar no es solo cruzar. A veces no necesitas poner una tabla AL LADO de otra, sino DEBAJO: apilar los pedidos de enero con los de febrero en un solo DataFrame, como si pegaras dos hojas en una. Eso es pd.concat, el equivalente del UNION de SQL. Y luego está el otro problema: el de la FORMA de la tabla. A veces recibes los datos en formato ancho (una columna por mes) y los necesitas en formato largo (una fila por mes) para poder agrupar, graficar o alimentar un modelo. Eso es melt. Y a veces es al revés: tienes una tabla interminable en formato largo y necesitas un resumen con una columna por categoría. Eso es pivot. Estos cuatro — merge, concat, melt, pivot — son las herramientas que resuelven casi todos los problemas de forma y combinación que te vas a encontrar.
### pd.merge: cruzar dos tablas por una columna común
Si vienes de SQL, merge es tu JOIN de toda la vida. La idea es idéntica: tienes dos tablas que comparten una columna (o varias), y quieres juntarlas en una sola con todas las columnas de ambas, emparejando fila a fila por ese valor común. En pandas, la sintaxis básica es pd.merge(tabla_izquierda, tabla_derecha, on="columna_comun"). El parámetro on indica por qué columna se emparejan las filas. Si las columnas se llaman distinto en cada tabla (por ejemplo, id_cliente en una y cliente_id en la otra), usas left_on y right_on en su lugar.
1import pandas as pd23pedidos = pd.DataFrame({4 'pedido_id': [1, 2, 3, 4],5 'cliente_id': [101, 102, 101, 103],6 'importe': [250, 80, 430, 150]7})89clientes = pd.DataFrame({10 'cliente_id': [101, 102, 103],11 'nombre': ['Ana Garcia', 'Luis Lopez', 'Marta Ruiz'],12 'ciudad': ['Madrid', 'Barcelona', 'Valencia']13})1415# Cruzar pedidos con datos del cliente16resultado = pd.merge(pedidos, clientes, on='cliente_id')17print(resultado.to_string(index=False))
merge básico: unir pedidos con datos de cliente por cliente_id
Por defecto, pd.merge hace un inner join (intersección): solo conserva las filas que tienen correspondencia en AMBAS tablas. Si un pedido tiene un cliente_id que no existe en la tabla de clientes, esa fila desaparece del resultado. Y si un cliente no tiene ningún pedido, tampoco aparece. Es exactamente el mismo comportamiento que INNER JOIN en SQL. Pero hay cuatro tipos de join, y elegir el correcto depende de la pregunta de negocio:
- inner (por defecto) — solo filas que casan en AMBAS tablas. "Dame los pedidos de clientes que tengo registrados." Si un cliente no está en la tabla de clientes, su pedido desaparece.
- left — todas las filas de la tabla izquierda, aunque no casen. "Dame TODOS los pedidos, y si el cliente no está registrado, déjame su nombre en blanco (NaN)." Útil cuando no quieres perder filas de tu tabla principal.
- right — todas las filas de la tabla derecha, aunque no casen. "Dame TODOS los clientes, aunque no tengan pedidos." Menos común; normalmente reorganizas el orden para usar left.
- outer — TODAS las filas de ambas tablas, casen o no. "Quiero ver todo: pedidos sin cliente conocido Y clientes sin pedidos." Las filas sin correspondencia tienen NaN en las columnas de la otra tabla.
1import pandas as pd23pedidos = pd.DataFrame({4 'pedido_id': [1, 2, 3],5 'cliente_id': [101, 102, 999],6 'importe': [250, 80, 430]7})89clientes = pd.DataFrame({10 'cliente_id': [101, 102, 103],11 'nombre': ['Ana', 'Luis', 'Marta']12})1314# inner: solo filas que casan (pedido 3 se pierde, Marta se pierde)15print("=== INNER ===")16print(pd.merge(pedidos, clientes, on='cliente_id').to_string(index=False))1718print()1920# left: todos los pedidos, aunque el cliente no exista21print("=== LEFT ===")22print(pd.merge(pedidos, clientes, on='cliente_id', how='left').to_string(index=False))2324print()2526# outer: todo, con NaN donde no hay correspondencia27print("=== OUTER ===")28print(pd.merge(pedidos, clientes, on='cliente_id', how='outer').to_string(index=False))
Los tres tipos de merge más habituales: inner, left y outer
Consejo de senior: la mayoría de los merge del día a día son left join. La lógica es: "tengo mi tabla principal (pedidos, usuarios, transacciones) y quiero añadirle información de una tabla auxiliar (clientes, productos, tiendas). No quiero perder ninguna fila de mi tabla principal, aunque la auxiliar no tenga el dato." Si usas inner y luego el informe tiene menos filas de las que esperabas, probablemente te comiste filas que no casaron. El left join te obliga a ver esos NaN y preguntarte por qué están ahí — que es justo lo que un analista debe hacer.
### Duplicados en la clave: el merge que multiplica filas
Hay una trampa clásica del merge que a todos nos ha explotado alguna vez: si la columna de cruce tiene valores repetidos en una de las tablas (o en ambas), el resultado tendrá MÁS filas que las tablas originales. Piensa en ello así: si un cliente tiene 3 pedidos y 2 direcciones de envío, al cruzar pedidos con direcciones por cliente_id obtienes 3 x 2 = 6 filas. Pandas empareja cada fila de un lado con TODAS las del otro que tengan la misma clave. Es un producto cartesiano por grupo, y es exactamente lo que hace un JOIN en SQL cuando la clave no es única.
Error clásico: "después del merge mi tabla tiene el doble de filas y no sé por qué". Siempre que hagas un merge, comprueba el número de filas ANTES y DESPUÉS con len(). Si el resultado tiene más filas de las que esperabas, es casi seguro que la columna de cruce tiene duplicados en alguna de las dos tablas. Usa df["col"].duplicated().sum() para comprobarlo. En análisis esto pasa mucho con tablas de mapeo que alguien ha editado a mano y tiene filas repetidas sin querer.
### El indicator: saber qué filas casaron y cuáles no
Cuando haces un left o un outer merge y quieres saber EXACTAMENTE qué filas encontraron pareja y cuáles se quedaron solas, pandas tiene un parámetro muy útil: indicator=True. Añade una columna llamada _merge con la procedencia de cada fila: "both" si existe en las dos tablas, "left_only" si solo está en la izquierda y "right_only" si solo está en la derecha. Cuáles puedes ver depende del tipo de cruce: con left solo saldrán both y left_only, porque las filas que únicamente están a la derecha ya las ha descartado el propio merge; para ver las tres necesitas outer. Es como un informe de calidad del cruce: te dice dónde hay huecos.
1import pandas as pd23pedidos = pd.DataFrame({4 'pedido_id': [1, 2, 3],5 'producto_id': ['A01', 'B02', 'Z99'],6 'cantidad': [2, 1, 5]7})89productos = pd.DataFrame({10 'producto_id': ['A01', 'B02', 'C03'],11 'nombre': ['Laptop', 'Raton', 'Monitor']12})1314# Left merge con indicator para ver que caso sin correspondencia15resultado = pd.merge(pedidos, productos, on='producto_id', how='left', indicator=True)16print(resultado.to_string(index=False))
indicator=True añade _merge para ver qué filas casaron
### pd.concat: apilar tablas (UNION)
El otro gran problema de combinación es más sencillo conceptualmente: tienes varias tablas con las MISMAS columnas y quieres pegarlas una debajo de otra. El caso típico: recibes un fichero CSV por mes (enero.csv, febrero.csv, marzo.csv) y quieres un solo DataFrame con todos los meses juntos. O descargas datos de varias fuentes que tienen la misma estructura y los quieres consolidar. En SQL esto es UNION ALL. En pandas, pd.concat.
1import pandas as pd23enero = pd.DataFrame({4 'fecha': ['2024-01-05', '2024-01-12'],5 'producto': ['Laptop', 'Monitor'],6 'importe': [1200, 350]7})89febrero = pd.DataFrame({10 'fecha': ['2024-02-03', '2024-02-18'],11 'producto': ['Raton', 'Teclado'],12 'importe': [25, 45]13})1415# Apilar enero y febrero en un solo DataFrame16todo = pd.concat([enero, febrero], ignore_index=True)17print(todo.to_string(index=False))
concat básico: apilar dos DataFrames con las mismas columnas
Cuidado: si las tablas tienen columnas diferentes, concat no falla — las añade todas y rellena con NaN donde faltan. Eso puede ser útil (si febrero tiene una columna nueva que enero no tenía) o puede ser una señal de que algo va mal (si los nombres de columna tienen un typo y no coinciden). Siempre revisa el .columns del resultado después de un concat para confirmar que todo ha casado como esperabas.
Consejo de senior: si tienes 12 ficheros CSV (uno por mes) y quieres concatenarlos todos, no hagas 12 variables. Usa una lista con un bucle: dfs = [pd.read_csv(f"ventas_{mes}.csv") for mes in range(1, 13)] y luego pd.concat(dfs, ignore_index=True). Es el patrón que usarás una y otra vez cuando el equipo de operaciones te mande datos partidos por mes, por tienda o por región.
### melt: de formato ancho a formato largo
Ahora entramos en el otro gran bloque de esta lección: remodelar. No combinas dos tablas, sino que CAMBIAS LA FORMA de una sola. El caso más común: recibes una tabla donde cada mes es una columna (enero, febrero, marzo...) y necesitas que cada mes sea una FILA. Eso es pasar de formato ancho (muchas columnas, pocas filas) a formato largo (pocas columnas, muchas filas). La analogía: imagina que tienes un calendario de pared con 12 columnas, una por mes. Y necesitas convertirlo en una agenda con una línea por día. La información es la misma; la forma es distinta. En pandas, la función que hace esto se llama melt (fundir, como fundir un bloque en tiras).
¿Por qué necesitas esto? Porque la mayoría de las operaciones de análisis (groupby, gráficas, modelos) esperan datos en formato largo. Si tienes ventas_enero, ventas_febrero, ventas_marzo como columnas separadas, no puedes hacer groupby("mes"). Necesitas una columna "mes" y una columna "ventas", y una fila por cada combinación. El formato ancho es cómodo para LEER (una tabla resumen en una presentación), pero el formato largo es el que necesitas para ANALIZAR.
1import pandas as pd23ventas_ancho = pd.DataFrame({4 'tienda': ['Centro', 'Norte', 'Sur'],5 'enero': [500, 300, 200],6 'febrero': [600, 400, 250],7 'marzo': [450, 350, 180]8})910# Convertir de ancho a largo con melt11ventas_largo = pd.melt(12 ventas_ancho,13 id_vars=['tienda'], # columnas que se MANTIENEN (identificadores)14 var_name='mes', # nombre para la nueva columna de "cabeceras"15 value_name='ventas' # nombre para la nueva columna de valores16)1718print(ventas_largo.to_string(index=False))
melt con los tres parámetros clave: id_vars, var_name, value_name
### pivot_table: de formato largo a ancho
La operación inversa de melt es pivot: tomar una tabla larga y "girarla" para que los valores únicos de una columna se conviertan en columnas del resultado. El caso de uso: tienes una tabla con miles de filas (una por venta) y quieres un resumen donde cada fila sea una tienda y cada columna sea un mes — exactamente la tabla que un director pega en su presentación. En pandas hay dos funciones: pivot (sin agregación, exige que no haya duplicados) y pivot_table (con agregación, maneja duplicados sumando, promediando o contando). En la práctica casi siempre usas pivot_table, porque los datos reales siempre tienen duplicados.
1import pandas as pd23ventas = pd.DataFrame({4 'tienda': ['Centro', 'Centro', 'Centro', 'Norte', 'Norte', 'Norte'],5 'mes': ['enero', 'febrero', 'marzo', 'enero', 'febrero', 'marzo'],6 'ventas': [500, 600, 450, 300, 400, 350]7})89# De largo a ancho con pivot_table10resumen = ventas.pivot_table(11 index='tienda', # que va en las filas12 columns='mes', # que va en las columnas13 values='ventas', # que valores rellenan la tabla14 aggfunc='sum' # como agregar si hay duplicados15)1617print(resumen.to_string())
pivot_table: la operación inversa de melt
La diferencia entre pivot y pivot_table: pivot exige que la combinación (index, columns) sea ÚNICA — si hay dos filas con tienda="Centro" y mes="enero", falla con un ValueError que dice literalmente duplicate entries. pivot_table no falla: agrupa esas filas y les aplica aggfunc. En la práctica usa pivot_table, porque los datos reales casi siempre traen duplicados — pero pásale siempre aggfunc explícitamente. Su valor por defecto es la media, así que si esperabas una tabla de totales y no lo pusiste, obtienes una tabla de medias con aspecto de totales y sin un solo aviso. Ese es el error más caro que puedes cometer aquí, y es silencioso: dos ventas de 10 y 20 en la misma celda salen como 15.0, no como 30. Si lo que quieres es detectar los duplicados en vez de agregarlos, pivot es tu amigo: se niega a continuar y te dice por qué.
### melt y pivot son inversas: el viaje de ida y vuelta
Un truco mental para no confundirlas: melt es "fundir" la tabla ancha en una larga y estrecha (como fundir un bloque de hielo en un chorro). pivot es "pivotar" una tabla larga para abrirla como un abanico. Si te preguntas cuál usar, piensa: ¿tengo demasiadas columnas que en realidad son valores de una misma variable? Melt. ¿Tengo una tabla interminable y necesito un resumen cruzado? Pivot.
### Resumen: cuándo usar cada herramienta
- pd.merge — cuando tienes dos tablas con una columna en común y quieres combinar sus columnas. Equivale a JOIN en SQL. Parámetro clave: how (inner, left, right, outer).
- pd.concat — cuando tienes varias tablas con las mismas columnas y quieres apilarlas. Equivale a UNION ALL en SQL. Parámetro clave: ignore_index=True.
- pd.melt — cuando una tabla tiene valores repartidos en varias columnas que deberían ser filas (formato ancho a largo). Parámetros clave: id_vars, var_name, value_name.
- pivot_table — cuando una tabla larga necesita resumirse en formato cruzado (formato largo a ancho). Parámetros clave: index, columns, values, aggfunc.
- indicator=True en merge — para ver qué filas casaron y cuáles no. Imprescindible para auditar la calidad de un cruce.
Regístrate para guardar tu progreso.
## comentarios
Reporta erratas, ayuda a otros o comparte tu opinión. Sé constructivo.
Inicia sesión para comentar y responder.
cargando comentarios...