josiete.com

Polars: Python acelera sus DataFrames con Rust y varios hilos

Ilustración del logo de Polars rodeado de columnas de datos y líneas de procesamiento paralelo

Python sigue siendo uno de los lenguajes más cómodos para explorar y transformar datos. Sin embargo, la comodidad de un DataFrame no siempre se traduce en velocidad: cuando crecen las columnas y las filas, el coste de memoria y de CPU se vuelve muy visible. Polars aborda ese problema con una librería de DataFrames escrita en Rust y con una API disponible desde Python.

¿Qué es Polars?

Polars es un motor de consultas analíticas y una librería de DataFrames para trabajar con datos tabulares. Su núcleo está escrito en Rust, utiliza un modelo de memoria compatible con Apache Arrow y combina ejecución vectorizada, optimización de consultas y procesamiento por columnas.

La API ofrece dos estilos. El modo eager ejecuta cada operación al momento, algo útil para exploraciones interactivas. El modo lazy construye un plan de consulta: Polars puede reordenar operaciones, eliminar columnas que no hacen falta y aplicar filtros tan pronto como sea posible antes de ejecutar el resultado.

No es una sustitución universal de una base de datos o de un sistema distribuido. Su punto fuerte es exprimir una máquina —portátil o servidor— y trabajar con CSV, Parquet, JSON y otras fuentes sin obligar al usuario a administrar un clúster.

El paralelismo: más de un hilo sin escribir código de concurrencia

Una diferencia importante frente a muchos flujos tradicionales de Python es que Polars no delega todo el trabajo pesado en el hilo principal del intérprete. Las operaciones del motor están implementadas en Rust y su planificador utiliza un thread pool para repartir tareas entre los núcleos disponibles. El motor es vectorizado, columnar y multihilo; por eso un filtro, una agregación o un join puede aprovechar varios cores automáticamente.

En la práctica, esto significa que el usuario escribe expresiones normales de Polars y el motor decide cómo paralelizarlas. No hace falta crear un ThreadPoolExecutor, serializar cada partición ni coordinar manualmente los resultados. La API de Python actúa como una interfaz; el cálculo intensivo ocurre en el núcleo nativo.

Se puede consultar el tamaño del grupo de hilos con pl.thread_pool_size() en versiones actuales. Polars lo configura automáticamente de forma razonable, pero en un entorno compartido —por ejemplo, dentro de un worker que ya limita la CPU— puede fijarse POLARS_MAX_THREADS antes de importar la librería:

POLARS_MAX_THREADS=4 python analizar_ventas.py

Conviene no confundir este paralelismo con la ejecución distribuida: varios hilos aprovechan los núcleos de una sola máquina. Para datos que no caben en memoria, el motor lazy también puede ejecutar en streaming cuando el plan lo permite; para escalar horizontalmente existe una capa distribuida separada.

La empresa detrás del proyecto

Polars nació en 2020 como un proyecto personal de Ritchie Vink, inicialmente para aprender sobre motores de consultas, Apache Arrow y Rust. El 3 de agosto de 2023, Vink y Chiel Peters anunciaron la creación de Polars Inc., una empresa con sede en Ámsterdam orientada a aportar ingeniería y estabilidad a largo plazo al ecosistema.

La empresa no sustituyó al proyecto comunitario: Polars continúa siendo software libre bajo licencia MIT. Su objetivo es financiar el desarrollo del motor, mejorar conectores y ofrecer productos para ejecutar consultas a mayor escala, incluido Polars Cloud. Es una separación interesante: una compañía puede construir servicios comerciales alrededor del proyecto mientras el núcleo abierto sigue disponible para cualquiera.

Un ejemplo en Python

Instalamos la librería con pip y construimos una pequeña transformación. scan_csv devuelve un LazyFrame, por lo que las operaciones se planifican y collect() ejecuta el plan optimizado.

pip install polars
import polars as pl

ventas = pl.scan_csv("ventas.csv")

resultado = (
    ventas
    .filter(pl.col("estado") == "pagado")
    .with_columns(
        (pl.col("unidades") * pl.col("precio")).alias("importe")
    )
    .group_by("categoria")
    .agg(
        pl.col("importe").sum().alias("facturacion"),
        pl.col("unidades").sum().alias("unidades")
    )
    .sort("facturacion", descending=True)
    .collect()
)

print(resultado)

El código expresa qué queremos obtener y deja que Polars decida cómo recorrer las columnas y repartir el trabajo. En un fichero grande, el modo lazy puede evitar leer columnas innecesarias y empujar el filtro hacia la lectura. Si el origen ya es Parquet, pl.scan_parquet("datos/*.parquet") permite mantener el mismo patrón.

¿Cuándo elegirlo?

Polars encaja especialmente bien en pipelines de transformación, análisis exploratorio con volúmenes grandes, preparación de datos para modelos y servicios que necesitan respuestas rápidas sin levantar un sistema distribuido. Si el proyecto depende de un ecosistema concreto de pandas, la migración requiere revisar algunas operaciones y tipos; aun así, ambas librerías pueden convivir y Polars permite convertir a pandas cuando sea necesario.

La idea central es sencilla: Python conserva una interfaz expresiva, mientras Rust se ocupa del trabajo intensivo, la memoria y el paralelismo. Esa combinación convierte a Polars en una opción muy atractiva cuando un DataFrame deja de ser pequeño, pero todavía queremos que el análisis viva cerca de nuestro código.