Saltar al contenido
dumaloor.dev
~/academia
Python

Pandas + Polars: cuando tu DataFrame ya no cabe en RAM

El día que abrí un CSV de 8 millones de filas en Pandas y mi portátil cayó. Cómo Polars me lo agregó en 1,8 segundos sin tocar la RAM, y por qué no uso solo Polars.

4 min de lecturapythonpandaspolarsdata-engineeringperformance

Cuando un DataFrame no cabe en RAM con Pandas, cambiar a Polars suele resolverlo sin tocar la máquina: Polars, escrito en Rust sobre Apache Arrow, lee el archivo en modo perezoso con scan_csv() en vez de cargarlo entero, construye un plan de consulta que solo trae a memoria las columnas y filas que el filter() y el group_by() final realmente necesitan, y paraleliza esa lectura en todos los núcleos del CPU. En un dataset de unos 8 millones de filas eso bajó un groupby de 41 segundos y 7,4 GB de RAM en Pandas a 1,8 segundos y 480 MB en Polars. La regla práctica: por debajo de 100 MB la diferencia no se nota y Pandas tiene mejor ecosistema; por encima de 1 GB, Polars en modo perezoso con scan_csv() o scan_parquet() es la opción, salvo que el equipo no lo conozca o el paso siguiente dependa de una librería que solo habla Pandas.

Hubo un punto en el que mis scripts de análisis empezaron a dejar de funcionar. No por bug. Por RAM. Un CSV de ventas de un año entero, 8 millones de filas, abierto con pd.read_csv(...): 6,2 GB de memoria solo para cargarlo. Mi portátil con 16 GB se ahogaba en cuanto intentaba un groupby no trivial. Reiniciar Python cada cinco minutos no es una metodología.

Entró Polars. Y cambió las reglas.

#El problema en concreto

Imagina el caso: necesito el GMV por evento y por día, ordenado por fecha, de un dataset con esta forma:

transaction_id, event_id, ticket_type, price_eur, channel, country, postal_code, created_at, status, ... (18 cols)

Unos 8 millones de filas. CSV de casi 2 GB en disco. Con Pandas estándar:

python10 líneas
import pandas as pd

df = pd.read_csv("ventas_2025.csv")            # 6,2 GB RAM, 28 segundos
out = (
    df[df["status"] == "completed"]
    .groupby([df["created_at"].dt.date, "event_id"])
    .agg(gmv=("price_eur", "sum"), n=("price_eur", "size"))
    .reset_index()
)
# Tiempo total: 41 segundos. RAM pico: 7,4 GB.

41 segundos no es el infierno. El problema es que estoy fundiendo el portátil para una operación que conceptualmente es trivial: SUM por día y evento.

#La misma operación en Polars

python15 líneas
import polars as pl

out = (
    pl.scan_csv("ventas_2025.csv")              # lazy, no carga nada
    .filter(pl.col("status") == "completed")
    .with_columns(pl.col("created_at").str.to_datetime().dt.date().alias("day"))
    .group_by(["day", "event_id"])
    .agg(
        pl.col("price_eur").sum().alias("gmv"),
        pl.len().alias("n"),
    )
    .sort("day")
    .collect()                                  # ahora sí ejecuta
)
# Tiempo total: 1,8 segundos. RAM pico: 480 MB.

23 veces más rápido. 15 veces menos RAM. Sin truco.

Por qué la diferencia

Pandas carga TODO en memoria, en un único hilo. Polars (escrito en Rust sobre Apache Arrow) hace tres cosas a la vez: paraleliza por columnas en todos los núcleos del CPU, evalúa lazy (encadena operaciones y solo ejecuta lo necesario), y usa columnar storage que es más eficiente en cache y compresión.

#Cuándo merece la pena Polars

No siempre. La regla que uso:

Tamaño datasetHerramienta
< 100 MBPandas. La diferencia es invisible y el ecosistema es más maduro.
100 MB – 1 GBPandas si es one-shot; Polars si vas a iterar muchas veces.
1 GB – 50 GBPolars con scan_* (lazy). Aquí es donde brilla.
> 50 GBPolars streaming, o DuckDB sobre el CSV directo, o cargar a Postgres.

Y dos casos en los que no uso Polars aunque encaje el tamaño:

  • Cuando el equipo lo va a tocar y no conoce Polars. El API es similar a Pandas pero no idéntico. Si yo me voy y el siguiente solo sabe Pandas, dejo el código en Pandas.
  • Cuando necesito un ecosistema específico (statsmodels, scikit-learn, geopandas). Estos esperan Pandas. Convierto al final con df.to_pandas().

#El patrón híbrido que uso a diario

Cargar y pre-procesar con Polars (rápido y barato). Pasar a Pandas solo al final, para los pasos que necesiten librerías del ecosistema:

python23 líneas
import polars as pl

# Trabajo pesado en Polars
clean = (
    pl.scan_csv("ventas_2025.csv")
    .filter(pl.col("status") == "completed")
    .with_columns(
        pl.col("created_at").str.to_datetime(),
        pl.col("price_eur").cast(pl.Float64),
    )
    .group_by("event_id")
    .agg(
        gmv = pl.col("price_eur").sum(),
        n   = pl.len(),
        avg = pl.col("price_eur").mean(),
        p95 = pl.col("price_eur").quantile(0.95),
    )
    .collect()
)

# Salida a Pandas solo para lo que requiere el ecosistema
df = clean.to_pandas()
df.plot.scatter(x="n", y="avg")  # matplotlib quiere Pandas

#El truco real: scan_csv y lazy execution

Lo que hace especial a Polars no es solo Rust. Es la separación entre scan_* y collect(). Cuando escribes:

python1 líneas
pl.scan_csv("...").filter(...).group_by(...).agg(...).collect()

Polars no lee el CSV de inmediato. Construye un query plan, lo optimiza (push down de filtros, eliminación de columnas no usadas, paralelización), y solo entonces lee del disco. Si tu agregado solo necesita 3 columnas de 18, Polars solo lee esas 3.

Compáralo con Pandas: pd.read_csv lee TODO siempre, luego tú filtras en memoria.

#Conversión rápida: Pandas → Polars

Si tienes un script Pandas que quieres migrar, esta tabla cubre el 80%:

PandasPolars
pd.read_csv("x.csv")pl.read_csv("x.csv") o pl.scan_csv("x.csv")
df[df["col"] > 5]df.filter(pl.col("col") > 5)
df.groupby("a").agg(...)df.group_by("a").agg(...)
df["new"] = df["a"] + df["b"]df.with_columns((pl.col("a") + pl.col("b")).alias("new"))
df.sort_values("date")df.sort("date")
df.dropna()df.drop_nulls()
df.merge(other, on="id")df.join(other, on="id")

El gotcha más común: Polars no tiene índice como Pandas. Olvídate de set_index. Todo es columnar y sin índices implícitos. En la práctica es liberador.

#Cuándo no me sirve Polars

Honestidad obligatoria: hay casos en los que Polars me ha decepcionado.

  • JSON anidado complejo. Polars puede leer JSON, pero estructuras profundas con arrays de objetos no son su fuerte. Pandas con json_normalize sigue siendo más cómodo.
  • Series temporales con frecuencias raras. Pandas tiene resample("Q"), resample("W-MON"), etc. Polars tiene group_by_dynamic pero con menos azúcar.
  • Plotting directo. Polars no tiene .plot() integrado. O conviertes a Pandas o usas plotnine / matplotlib manualmente.
Receta

Si tu dataframe es < 100 MB: Pandas. Si es > 1 GB: Polars con scan_*. Entre medias: depende. Y para lo que falta ecosistema (statsmodels, geopandas, plotting), .to_pandas() al final y listo.

En el próximo post hago el comparativo directo Python vs R para análisis. Spoiler: con Polars en la mesa, R deja de tener ventaja clara en datasets grandes. Pero sigue ganando para EDA rápido y modelado estadístico.

Preguntas frecuentes

¿Y si los datos no vienen de un CSV sino de una conexión a Postgres directamente?

Polars puede leer directamente de una base de datos con pl.read_database(), así que no hace falta pasar por un CSV intermedio. La ventaja de la lectura perezosa se reduce si la propia base de datos no filtra antes de entregar los datos, así que conviene mover el WHERE a la consulta SQL cuando se pueda, no dejarlo todo para el .filter() de Polars.

¿Se comporta igual group_by cuando hay claves con valores nulos?

No exactamente como en Pandas: por defecto Polars mantiene las filas con clave nula y las agrupa aparte, en vez de descartarlas. Si esperas que las filas con event_id nulo desaparezcan del agregado, hace falta un .drop_nulls() explícito antes del group_by, o aparece una fila de más con etiqueta nula en el resultado.

¿Compensa reescribir a Polars un script que ya funciona bien y corre poco tiempo al día?

Casi nunca. Si el script corre una vez al día y tarda 40 segundos, reescribirlo cuesta más tiempo humano del que se ahorra en meses de ejecuciones. Compensa cuando el script corre muchas veces al día, cuando alguien lo espera en directo, o cuando ya empieza a fallar por memoria, no solo porque Polars sea más rápido sobre el papel.

Seguir leyendo

¿Te ha sido útil?

¿Necesitas algo similar para tu negocio?

Construyo dashboards, pipelines y análisis a medida. Si quieres hablar, escríbeme.

Hablamos