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
