Indice / pandas: combinar y resumir / 6.1
concat
Apilar archivos que tienen la misma forma. La operación más simple de la unidad, y la que más silenciosamente pierde o duplica filas cuando la forma resulta no ser tan igual.
Para qué sirve
Es el caso más común del mundo real: un archivo por mes, por sucursal, por exportación. Doce archivos con las mismas cinco columnas que hay que convertir en uno.
concat hace exactamente eso y no pregunta nada. No comprueba que las
columnas coincidan, no avisa si el índice queda repetido y no dice si alguna fila
quedó afuera. Todo eso lo tenés que comprobar vos, y las tres comprobaciones son de
una línea.
ELI5Explicado facil
Es juntar tres pilas de papeles en una sola. Si las tres tienen las mismas columnas, sale perfecto.
Pero si en una pila la columna dice «unidades» y en otra «cantidad», la pila junta va a tener las dos columnas, cada una llena a medias. Nadie te va a avisar: los papeles entran igual.
El ejemplo mínimo
import pandas as pd
enero = pd.DataFrame({"producto": ["Teclado", "Mouse"], "unidades": [3, 5]})
febrero = pd.DataFrame({"producto": ["Teclado", "Webcam"], "unidades": [7, 2]})
juntas = pd.concat([enero, febrero])
print(juntas)
print()
print("Mira el indice de la izquierda: 0, 1, 0, 1.")
print("concat no renumera: cada tabla se trajo sus etiquetas.")
print("indice unico:", juntas.index.is_unique)
print()
renumerada = pd.concat([enero, febrero], ignore_index=True)
print(renumerada)
print("con ignore_index=True el indice queda 0..n-1 y es unico:",
renumerada.index.is_unique)
producto unidades
0 Teclado 3
1 Mouse 5
0 Teclado 7
1 Webcam 2
Mira el indice de la izquierda: 0, 1, 0, 1.
concat no renumera: cada tabla se trajo sus etiquetas.
indice unico: False
producto unidades
0 Teclado 3
1 Mouse 5
2 Teclado 7
3 Webcam 2
con ignore_index=True el indice queda 0..n-1 y es unico: True
ignore_index=True casi siempre
Sin él, cada tabla se trae sus propias etiquetas y el índice del resultado queda
repetido: 0, 1, 0, 1. A partir de ahí, loc[0] devuelve dos
filas, escribir el archivo saca un índice sin sentido y cualquier alineación con otra
tabla se vuelve impredecible.
La excepción es cuando el índice significa algo - un SKU, un legajo -; entonces lo que hay que comprobar es que no se repita entre archivos.
Sobre datos de verdad
import pandas as pd
meses = ["enero", "febrero", "marzo"]
tablas = {m: pd.read_csv(f"derivados/ventas_{m}.csv") for m in meses}
for m, t in tablas.items():
print(f"ventas_{m}.csv:", len(t), "filas")
juntas = pd.concat(tablas.values(), ignore_index=True)
print()
print("apiladas:", len(juntas), "filas")
original = pd.read_csv("ventas_mensuales.csv")
print("original:", len(original), "filas")
print("faltan :", len(original) - len(juntas))
print()
print("Los tres archivos se armaron partiendo el original por mes, y aun asi")
print("apilarlos no lo reconstruye. Las filas que faltan no pertenecen a")
print("ningun mes: tienen la fecha vacia, o rota, o escrita en otro formato.")
sin_mes = pd.read_csv("derivados/ventas_sin_mes.csv", dtype=str,
keep_default_na=False)
print()
print(sin_mes[["fecha", "vendedor", "producto"]].to_string(index=False))
ventas_enero.csv: 60 filas
ventas_febrero.csv: 56 filas
ventas_marzo.csv: 59 filas
apiladas: 175 filas
original: 180 filas
faltan : 5
Los tres archivos se armaron partiendo el original por mes, y aun asi
apilarlos no lo reconstruye. Las filas que faltan no pertenecen a
ningun mes: tienen la fecha vacia, o rota, o escrita en otro formato.
fecha vendedor producto
Beatriz Molina Teclado
Lucía Vega Tablet
15/03/2026 Lucía Vega Teclado
Martín Ríos Tablet
Martín Ríos Webcam
Los tres archivos salieron de partir ventas_mensuales.csv por mes, y
apilarlos no lo reconstruye: 60 + 56 + 59 = 175, y el original tiene
180.
Las 5 que faltan no pertenecen a ningún mes: tres tienen la fecha vacía, una dice
2026-02-30 y otra está escrita 15/03/2026. Ninguna se
perdió por un error del programa - se perdieron al partir, y apilar no
las devuelve.
assert len(juntas) == sum(len(t) for t in tablas) confirma que
concat no perdió nada. Lo que no confirma es que las
partes sumen el total original: eso es otra cuenta, y es la que descubre las 5 filas
de este ejemplo.
Columnas que no coinciden
import pandas as pd
sucursal_a = pd.DataFrame({
"producto": ["Teclado", "Mouse"],
"unidades": [3, 5],
})
sucursal_b = pd.DataFrame({
"producto": ["Webcam"],
"cantidad": [2], # el mismo dato con otro nombre
})
juntas = pd.concat([sucursal_a, sucursal_b], ignore_index=True)
print(juntas)
print()
print("columnas:", list(juntas.columns))
print("Nadie se quejo. Ahora hay dos columnas que significan lo mismo y")
print("ninguna esta completa. sum() de cualquiera de las dos da mal.")
print()
print("unidades.sum() =", juntas["unidades"].sum(), " (le falta la de B)")
print("cantidad.sum() =", juntas["cantidad"].sum(), " (le faltan las de A)")
print()
# La comprobacion que conviene hacer ANTES de apilar.
faltan_en_b = set(sucursal_a.columns) - set(sucursal_b.columns)
faltan_en_a = set(sucursal_b.columns) - set(sucursal_a.columns)
print("columnas que solo estan en A:", sorted(faltan_en_b))
print("columnas que solo estan en B:", sorted(faltan_en_a))
producto unidades cantidad
0 Teclado 3.0 NaN
1 Mouse 5.0 NaN
2 Webcam NaN 2.0
columnas: ['producto', 'unidades', 'cantidad']
Nadie se quejo. Ahora hay dos columnas que significan lo mismo y
ninguna esta completa. sum() de cualquiera de las dos da mal.
unidades.sum() = 8.0 (le falta la de B)
cantidad.sum() = 2.0 (le faltan las de A)
columnas que solo estan en A: ['unidades']
columnas que solo estan en B: ['cantidad']
Este es el modo de falla caro. concat hace la unión de las columnas y
rellena con NaN: quedan dos columnas que significan lo mismo, cada una
llena a medias, y las dos sumas dan mal. El archivo tiene la cantidad
correcta de filas, así que el error pasa cualquier revisión que solo cuente.
Saber de dónde vino cada fila
import pandas as pd
meses = ["enero", "febrero", "marzo"]
tablas = [pd.read_csv(f"derivados/ventas_{m}.csv") for m in meses]
# Sin keys, apilar pierde el origen de cada fila para siempre.
juntas = pd.concat(tablas, keys=meses, names=["mes", "fila"])
print(juntas.head(3).to_string())
print()
print("El indice ahora tiene dos niveles:", juntas.index.names)
print()
# Y con eso se puede volver a preguntar por archivo, sin haber agregado
# ninguna columna a mano.
print("filas por mes:")
print(juntas.groupby(level="mes").size().to_string())
print()
# Muchas veces conviene tenerlo como columna, no como indice.
con_columna = juntas.reset_index(level="mes").reset_index(drop=True)
print(con_columna[["mes", "fecha", "producto", "unidades"]]
.head(3).to_string(index=False))
fecha vendedor producto unidades precio_unitario
mes fila
enero 0 2026-01-18 Martín Ríos Laptop 10.0 955.81
1 2026-01-09 Ana Pérez Webcam 2.0 42.89
2 2026-01-01 Beatriz Molina Auriculares 11.0 58.51
El indice ahora tiene dos niveles: ['mes', 'fila']
filas por mes:
mes
enero 60
febrero 56
marzo 59
mes fecha producto unidades
enero 2026-01-18 Laptop 10.0
enero 2026-01-09 Webcam 2.0
enero 2026-01-01 Auriculares 11.0
keys= agrega un nivel al índice con la etiqueta de cada tabla. Es la
forma barata de no perder el origen - y de poder contestar «¿cuántas filas puso
febrero?» sin haber agregado una columna a mano antes de apilar.
axis=1
import pandas as pd
izquierda = pd.DataFrame({"sku": ["A1", "A2", "A3"],
"precio": [10.0, 20.0, 30.0]})
derecha = pd.DataFrame({"sku": ["A3", "A1", "A2"],
"stock": [5, 7, 9]})
pegadas = pd.concat([izquierda, derecha], axis=1)
print(pegadas)
print()
print("Quedo A1 al lado de A3. concat(axis=1) alinea por INDICE, y los dos")
print("indices son 0,1,2: pego la fila 0 con la fila 0, sin mirar el sku.")
print()
print("Lo que realmente queriamos:")
print(izquierda.merge(derecha, on="sku"))
print()
print("Regla practica: si las dos tablas comparten una columna que identifica")
print("la fila, va merge. concat(axis=1) sirve cuando el indice YA es la clave")
print("en las dos, que es raro y conviene comprobarlo.")
sku precio sku stock
0 A1 10.0 A3 5
1 A2 20.0 A1 7
2 A3 30.0 A2 9
Quedo A1 al lado de A3. concat(axis=1) alinea por INDICE, y los dos
indices son 0,1,2: pego la fila 0 con la fila 0, sin mirar el sku.
Lo que realmente queriamos:
sku precio stock
0 A1 10.0 7
1 A2 20.0 9
2 A3 30.0 5
Regla practica: si las dos tablas comparten una columna que identifica
la fila, va merge. concat(axis=1) sirve cuando el indice YA es la clave
en las dos, que es raro y conviene comprobarlo.
concat(axis=1) no mira las claves: mira el índice
En el ejemplo quedó A1 al lado de A3, porque las dos
tablas tienen el índice 0, 1, 2 y concat pegó fila 0 con
fila 0. El resultado tiene la cantidad de filas correcta y los datos cruzados.
Si las dos tablas comparten una columna que identifica la fila, va
merge, que es la lección siguiente. axis=1 sirve
cuando el índice ya es la clave en las dos, que es raro y conviene
comprobarlo antes.
Dónde se rompe
Causa: las tablas no tienen los mismos nombres de columna.
concat hace la unión y rellena con NaN.
Arreglo: comparar los conjuntos de nombres antes de apilar:
set(a.columns) ^ set(b.columns) tiene que dar vacío.
Causa: faltó ignore_index=True y el índice quedó repetido.
Arreglo: ignore_index=True, o
assert juntas.index.is_unique si el índice tenía que significar
algo.
Causa: una de las tablas aportó NaN en esa columna -
por columnas que no coinciden o porque venía vacía - y un
int64 común no puede representar un ausente.
Arreglo: resolver la causa (los nombres), y si el ausente es legítimo,
astype("Int64").
Causa: se usó axis=1 sobre tablas cuyo índice no es la
clave.
Arreglo: merge(on="clave"). Es la lección 6.2.
Causa: los archivos se superponen - el de febrero incluye los últimos días de enero, o alguien exportó dos veces el mismo período.
Arreglo: duplicated(subset=CLAVE).sum() después de apilar,
y decidir la política (lección 5.5). concat nunca deduplica.
Recetario
| Expresión | Qué hace | Cuándo |
|---|---|---|
pd.concat([a, b]) | Apila filas conservando los índices | Casi nunca así: falta ignore_index |
pd.concat([a, b], ignore_index=True) | Apila y renumera 0..n−1 | El caso normal |
pd.concat(lista, keys=nombres) | Agrega un nivel con el origen | Para no perder de qué archivo vino |
pd.concat([a, b], join="inner") | Solo las columnas comunes | Cuando sobran columnas y no faltan |
pd.concat([a, b], axis=1) | Pega columnas alineando por índice | Solo si el índice es la clave en las dos |
set(a.columns) ^ set(b.columns) | Los nombres que están en una sola | Antes de apilar, siempre |
assert len(j) == sum(map(len, tablas)) | No se perdió ninguna fila | Después de apilar |
j.index.is_unique | bool | Si el índice significaba algo |
j.duplicated(subset=CLAVE).sum() | Filas repetidas entre archivos | Cuando los períodos se superponen |
Los archivos de este ejemplo
Los tres archivos mensuales son derivados: salen de partir
ventas_mensuales.csv por el prefijo de la fecha. Están publicados para
que puedas practicar el apilado:
derivados/ventas_enero.csvderivados/ventas_febrero.csvderivados/ventas_marzo.csvderivados/ventas_sin_mes.csv- las 5 que no cayeron en ningún mes