Saltar al contenido

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

EjemploApilar dos tablas con la misma forma
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

EjemploSobre datos de verdad: tres archivos que no suman el original
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.

La comprobación que hay que escribir siempre

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

EjemploColumnas que no coinciden: concat no avisa, rellena con NaN
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

Ejemplokeys=: de que archivo 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

Ejemploaxis=1 pega por indice, y casi nunca es lo que queres
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

(sin error) el resultado tiene columnas de más, medio vacías

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.

(sin error) loc[0] devuelve varias filas después de apilar

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.

(sin error) las columnas numéricas quedaron float

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

(sin error) los datos quedaron cruzados entre columnas

Causa: se usó axis=1 sobre tablas cuyo índice no es la clave.

Arreglo: merge(on="clave"). Es la lección 6.2.

(sin error) al apilar aparecieron filas duplicadas

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

concat
ExpresiónQué haceCuándo
pd.concat([a, b])Apila filas conservando los índicesCasi nunca así: falta ignore_index
pd.concat([a, b], ignore_index=True)Apila y renumera 0..n−1El caso normal
pd.concat(lista, keys=nombres)Agrega un nivel con el origenPara no perder de qué archivo vino
pd.concat([a, b], join="inner")Solo las columnas comunesCuando sobran columnas y no faltan
pd.concat([a, b], axis=1)Pega columnas alineando por índiceSolo si el índice es la clave en las dos
set(a.columns) ^ set(b.columns)Los nombres que están en una solaAntes de apilar, siempre
assert len(j) == sum(map(len, tablas))No se perdió ninguna filaDespués de apilar
j.index.is_uniqueboolSi el índice significaba algo
j.duplicated(subset=CLAVE).sum()Filas repetidas entre archivosCuando los períodos se superponen

Los archivos de este ejemplo

Los tres archivos mensuales son derivados: salen de partir ventas_mensuales.csv (7.3 kB) por el prefijo de la fecha. Están publicados para que puedas practicar el apilado:

Ponete a prueba

1. ¿Que hace pd.concat([a, b]) con el indice?Nivel 1 · Recordar
2. Dos tablas tienen columnas parcialmente distintas. ¿Que hace concat por defecto?Nivel 1 · Recordar
3. Tres archivos de 60, 56 y 59 filas se apilan y el resultado tiene 175. El archivo del que se partieron tiene 180. ¿Que conclusion se saca?Nivel 2 · Predecir la salida
4. ¿Para que sirve keys= en concat?Nivel 2 · Predecir la salida
5. Dos tablas de 3 filas cada una comparten la columna sku pero con los productos en distinto orden. ¿Que da pd.concat([a, b], axis=1)?Nivel 3 · Elegir entre dos casi correctas
6. ¿Cual es la comprobacion que conviene escribir despues de apilar doce archivos mensuales?Nivel 3 · Elegir entre dos casi correctas
7. Este codigo apila doce archivos y el total de unidades da la mitad de lo esperado. Las 12.000 filas estan. ¿Por que?
tablas = [pd.read_csv(f"ventas_{m}.csv") for m in MESES]
juntas = pd.concat(tablas, ignore_index=True)
print(juntas["unidades"].sum())
Nivel 4 · Encontrar el defecto