Polars en Python: DataFrame, Lazy API, expresiones y comparativa con Pandas

Polars es una biblioteca de DataFrames escrita en Rust que rivaliza y supera a Pandas en rendimiento gracias a su motor vectorizado y su compatibilidad con Apache Arrow. Ofrece dos APIs: la Eager API, que ejecuta las operaciones de inmediato, y la Lazy API, que construye un plan de ejecución optimizado antes de procesarlo. Este tutorial cubre ambas con ejemplos prácticos y comparativas.

Instalación

# pip install polars
# pip install polars[all]   # con soporte para Excel, Parquet, Delta Lake, etc.

Crear un DataFrame

import polars as pl

# Desde diccionario
df = pl.DataFrame({
    "nombre": ["Ana", "Luis", "María", "Carlos", "Elena"],
    "edad": [28, 34, 22, 45, 31],
    "ciudad": ["Madrid", "Barcelona", "Madrid", "Valencia", "Barcelona"],
    "salario": [45_000, 62_000, 38_000, 71_000, 55_000],
})

print(df)
print(df.dtypes)   # [String, Int64, String, Int64]
print(df.shape)    # (5, 4)

Eager API: seleccionar, filtrar y ordenar

import polars as pl

df = pl.DataFrame({
    "nombre": ["Ana", "Luis", "María", "Carlos", "Elena"],
    "edad": [28, 34, 22, 45, 31],
    "ciudad": ["Madrid", "Barcelona", "Madrid", "Valencia", "Barcelona"],
    "salario": [45_000, 62_000, 38_000, 71_000, 55_000],
})

# Seleccionar columnas con pl.col
resultado = df.select([
    pl.col("nombre"),
    pl.col("salario"),
    (pl.col("salario") / 12).alias("salario_mensual")
])
print(resultado)

# Filtrar
mayores_40 = df.filter(pl.col("edad") > 40)
print(mayores_40)

# Filtros múltiples con & y |
de_madrid_bien_pagados = df.filter(
    (pl.col("ciudad") == "Madrid") & (pl.col("salario") > 40_000)
)
print(de_madrid_bien_pagados)

# Ordenar
ordenado = df.sort("salario", descending=True)
print(ordenado)

group_by y agg

import polars as pl

df = pl.DataFrame({
    "nombre": ["Ana", "Luis", "María", "Carlos", "Elena"],
    "edad": [28, 34, 22, 45, 31],
    "ciudad": ["Madrid", "Barcelona", "Madrid", "Valencia", "Barcelona"],
    "salario": [45_000, 62_000, 38_000, 71_000, 55_000],
})

estadisticas = (
    df.group_by("ciudad")
    .agg([
        pl.col("salario").mean().alias("salario_medio"),
        pl.col("salario").max().alias("salario_max"),
        pl.col("nombre").count().alias("empleados"),
        pl.col("edad").mean().alias("edad_media"),
    ])
    .sort("salario_medio", descending=True)
)
print(estadisticas)

Lazy API: plan de ejecución optimizado

La Lazy API no ejecuta nada hasta que llamas a collect(). Polars optimiza el plan internamente: elimina columnas innecesarias, reordena filtros, fusiona operaciones, etc.

import polars as pl

# scan_csv es perezosa: no lee el fichero aún
lf = pl.scan_csv("ventas.csv")

resultado = (
    lf
    .filter(pl.col("anio") == 2024)
    .filter(pl.col("importe") > 1000)
    .group_by(["mes", "categoria"])
    .agg([
        pl.col("importe").sum().alias("total"),
        pl.col("cliente_id").n_unique().alias("clientes"),
    ])
    .sort("total", descending=True)
    .limit(10)
)

# Ver el plan optimizado:
print(resultado.explain(optimized=True))

# Ejecutar:
df = resultado.collect()
print(df)

Expresiones: la clave de Polars

Las expresiones de Polars son composables y se ejecutan de forma vectorizada en paralelo:

import polars as pl
from datetime import date

df = pl.DataFrame({
    "precio": [10.5, 20.0, None, 15.75, 8.99],
    "cantidad": [3, 1, 5, 2, 10],
    "fecha": ["2024-01-15", "2024-02-20", "2024-01-30", "2024-03-05", "2024-02-14"],
})

resultado = df.select([
    # Operaciones aritméticas
    (pl.col("precio") * pl.col("cantidad")).alias("total"),
    # Manejo de nulos
    pl.col("precio").fill_null(0.0).alias("precio_sin_nulos"),
    pl.col("precio").is_null().alias("precio_faltante"),
    # Transformaciones de cadenas
    pl.col("fecha").str.to_date("%Y-%m-%d").alias("fecha_date"),
    # Condicionales
    pl.when(pl.col("cantidad") > 5)
      .then(pl.lit("alto"))
      .otherwise(pl.lit("normal"))
      .alias("volumen"),
])
print(resultado)

Joins

import polars as pl

clientes = pl.DataFrame({
    "id": [1, 2, 3, 4],
    "nombre": ["Ana", "Luis", "María", "Carlos"],
    "ciudad_id": [1, 2, 1, 3],
})

ciudades = pl.DataFrame({
    "id": [1, 2, 3],
    "nombre": ["Madrid", "Barcelona", "Valencia"],
})

# Inner join
resultado = clientes.join(ciudades, left_on="ciudad_id", right_on="id", how="inner")
print(resultado.select(["nombre", "nombre_right"]))

# Left join
resultado_left = clientes.join(ciudades, left_on="ciudad_id", right_on="id", how="left")
print(resultado_left)

Lectura y escritura de formatos

import polars as pl

# CSV
df = pl.read_csv("datos.csv", separator=";", encoding="utf8")
df.write_csv("salida.csv")

# Parquet (el formato más rápido para datos analíticos)
df = pl.read_parquet("datos.parquet")
df.write_parquet("salida.parquet", compression="zstd")

# JSON
df = pl.read_json("datos.json")
df.write_json("salida.json")

# Desde Pandas
import pandas as pd
df_pandas = pd.DataFrame({"a": [1, 2, 3]})
df_polars = pl.from_pandas(df_pandas)
df_pandas2 = df_polars.to_pandas()

Polars vs Pandas: ¿cuándo usar cada uno?

Polars brilla cuando:

  • Trabajas con ficheros grandes (>1 GB) donde Pandas se queda sin memoria
  • Necesitas el máximo rendimiento en transformaciones analíticas
  • Quieres el plan de consultas optimizado automáticamente (Lazy API)
  • Prefieres datos inmutables y ausencia de índices implícitos

Pandas sigue siendo preferible cuando:

  • Usas bibliotecas que requieren DataFrames de Pandas (scikit-learn, matplotlib clásico)
  • Tu equipo ya conoce bien Pandas y el conjunto de datos es pequeño
  • Necesitas operaciones de series temporales muy específicas de Pandas

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP