La librería Polars¶
Durante años, Pandas ha sido (y sigue siendo) el estándar indiscutible para la manipulación y el análisis de datos en Python. Sin embargo, a medida que los volúmenes de datos han crecido exponencialmente, las limitaciones de Pandas en términos de velocidad y consumo de memoria RAM se han vuelto evidentes.
Aquí es donde entra Polars: una librería de manipulación de datos de última generación diseñada específicamente para superar estas barreras y ofrecer un rendimiento extremo sin renunciar a una sintaxis intuitiva.
1. Características principales de Polars¶
A diferencia de Pandas, que se construyó sobre cimientos más antiguos como NumPy, Polars está diseñado desde cero aprovechando la arquitectura del hardware moderno:
- Escrito en Rust: Polars está desarrollado en Rust, un lenguaje de programación de sistemas que garantiza un rendimiento cercano a C/C++ y un manejo de memoria nativo.
- Paralelización nativa: Mientras que Pandas ejecuta la mayoría de sus operaciones en un solo núcleo del procesador (single-thread), Polars distribuye automáticamente las tareas entre todos los núcleos disponibles.
- Apache Arrow como motor de memoria: Polars utiliza el formato de memoria columnar de Apache Arrow, lo que permite un almacenamiento ultraeficiente en RAM y evita copias de datos innecesarias al operar.
- Evaluación Perezosa (Lazy Evaluation) y Optimización de Consultas: Polars no solo permite la ejecución inmediata (Eager execution, como Pandas), sino que introduce un modo "perezoso" (Lazy execution). En este modo, Polars analiza todo tu flujo de transformaciones, crea un plan de ejecución optimizado y elimina operaciones o columnas redundantes antes de procesar un solo dato.
1.1. ¿Cuándo conviene utilizar Polars?¶
Normalmente Pandas puede servir para muchos de los datos con que podamos trabajar, pero en algunas situaciones será más eficiente utilizar Polars:
- Archivos medianos y grandes: Cuando se trabaja con archivos CSV, Parquet o JSON que superan unos pocos cientos de megabytes o varios gigabytes y Pandas empieza a ralentizarse o lanzar errores de falta de memoria (MemoryError).
- Pipelines de transformación intensivos: Procesos con múltiples agregaciones, uniones (joins) o filtrados complejos sobre millones de filas.
- Integración con formatos modernos: Se complementa a la perfección con el formato Parquet, permitiendo leer únicamente las columnas o filas necesarias directamente del disco.
1.2. Instalación y primeros pasos¶
Para instalar polars debemos ejecutar el correspondiente comando:
pip install polars
A la hora de importarlo en nuestros programas, es habitual asignarle el alias pl para abreviar:
import polars as pl
A continuación veremos cómo realizar en Polars algunas de las operaciones habituales que también se hacen en librerías populares como Pandas, explicada aquí. Puedes consultar más información sobre su librería en su referencia oficial
2. Carga de DataFrames¶
Igual que ocurre en Pandas, existen distintas formas de crear o cargar DataFrames en Polars:
- Podemos, por ejemplo, crear el DataFrame a mano con los datos que queramos:
import polars as pl
import datetime as dt
df = pl.DataFrame(
{
"name": ["Alice Archer", "Ben Brown", "Chloe Cooper", "Daniel Donovan"],
"birthdate": [
dt.date(1997, 1, 10),
dt.date(1985, 2, 15),
dt.date(1983, 3, 22),
dt.date(1981, 4, 30),
],
"weight": [57.9, 72.5, 53.6, 83.1], # (kg)
"height": [1.56, 1.77, 1.65, 1.75], # (m)
}
)
- También podemos leer la información desde un fichero CSV:
df_csv = pl.read_csv("docs/assets/data/output.csv", separator=";")
- Admite también lectura desde formato Parquet. En este caso debemos diferenciar entre lectura inmediata (recomendado para archivos pequeños) o lectura perezosa (lazy), recomendado para archivos pesados, cuando queremos filtrar antes de cargar en memoria.
# Lectura inmediata (eager)
df = pl.read_parquet("datos.parquet")
# Lectura perezosa (lazy)
lazy_df = pl.scan_parquet("datos.parquet")
- Podemos también cargar la información de una base de datos. En este caso Polars utiliza el motor Apache Arrow y conexiones optimizadas.
uri_conexion = "mysql://usuario:password@host:puerto/nombreBD"
query = "SELECT ..."
df_sql = pl.read_database(
query=query,
connection=uri_conexion,
engine="connectorx"
)
- Finalmente, comentaremos también la carga desde API REST
url_api = "https://fakestoreapi.com/products"
df_productos = pl.read_json(url_api)
Nota
En el caso de que la API REST necesite una clave de acceso (API KEY), es más recomendable usar la librería requests, como se explica aquí, y luego cargar los datos desde el diccionario que se haya generado, con pl.from_dicts
3. Operaciones básicas¶
En Polars tenemos disponibles muchas de las operaciones habituales que existen en librerías como Pandas. Analizaremos algunas de las más habituales:
- Las funciones
headytailsirven igualmente para mostrar los primeros o últimos n elementos del DataFrame - La propiedad
shapeobtiene una tupla con el tamaño del DataFrame en formato(filas, columnas). Además, tenemos las propiedadeswidthyheightpara obtener estos datos por separado. - Para asignar o cambiar los nombres de las columnas podemos usar
renamee indicar el cambio de cada nombre viejo por el nuevo, o usar la propiedadcolumnse indicar los nombres de todas
# Uso de rename
df_renombrado = df.rename({"col1": "id", "col2": "categoria"})
# Renombrar o asignar todas de golpe
df.columns = ["Nombre1", "Nombre2", ...]
- Para seleccionar casillas o rangos tenemos distintas opciones, según lo que queramos seleccionar. Aquí vemos algunos ejemplos:
valor = df[2, "precio"] # Casilla de la tercera fila, columna "precio"
valor = df[2, 2] # Casilla de la tercera fila, tercera columna
df_sub = df[1:4] # Filas de la 1 a la 3
df_sub_slice = df.slice(1, 3) # Desde la fila 1, tomar 3 filas
df_cols = df.select(["producto", "precio"]) # Columnas "producto" y "precio"
df_cols_pos = df[:, 0:2] # Primeras 2 columnas
- Los filtrados se definen con el método
filterindicando la condición
# Columnas "producto" y "precio" de los elementos con precio superior a 12
df_resultado = (
df
.filter(pl.col("precio") > 12)
.select(["producto", "precio"])
)
- Para añadir o quitar filas o columnas, hay que tener en cuenta que los DataFrames en Polars son inmutables, para optimizar así su gestión en memoria. Por tanto, añadir o quitar elementos supondrá siempre crear un nuevo DataFrame partiendo del original. Para añadir columnas podemos emplear el método
with_columns, y para quitar columnas podemos empleardroposelect. En cuanto a filas, podemos añadirlas conconcat, y quitarlas confilter
df = pl.DataFrame({
"producto": ["A", "B", "C"],
"precio": [10.0, 20.0, 15.0]
})
# Añadir una columna calculada y una columna con valor constante (pl.lit)
df_nuevo = df.with_columns(
(pl.col("precio") * 1.21).alias("precio_iva"),
pl.lit("EUR").alias("moneda")
)
# Eliminar columna "moneda"
df_sin_moneda = df_nuevo.drop("moneda")
# Conservar solo las columnas que interesan con .select()
df_seleccion = df_nuevo.select(["producto", "precio_iva"])
df1 = pl.DataFrame({"a": [1, 2], "b": ["X", "Y"]})
df2 = pl.DataFrame({"a": [3, 4], "b": ["Z", "W"]})
# Añadir las filas de df2 debajo de df1
df_unido = pl.concat([df1, df2])
# Quitar filas
df_filtrado = df_unido.filter(pl.col("a") > 1)
- El reemplazo o mapeo puede hacerse con el método
replacede forma directa, o a través del métodowhenuotherwhisepara establecer distintas franjas o condiciones con su mapeo correspondiente.
df = pl.DataFrame({
"codigo": ["ES", "FR", "IT", "DE"],
"puntuacion": [10, 20, 30, 40]
})
mapa_paises = {
"ES": "España",
"FR": "Francia",
"IT": "Italia"
}
# Añadir columna "pais" con el nombre de cada país, reemplazando a "codigo"
df_parcial = df.with_columns(
pl.col("codigo").replace(mapa_paises).alias("pais")
)
df_ventas = pl.DataFrame({
"cliente": ["A", "B", "C", "D"],
"monto": [50, 150, 300, 80]
})
# Asignar categoría según el rango del monto
df_categorizado = df_ventas.with_columns(
pl.when(pl.col("monto") >= 200).then(pl.lit("VIP"))
.when(pl.col("monto") >= 100).then(pl.lit("Premium"))
.otherwise(pl.lit("Estándar"))
.alias("nivel_cliente")
)
- Las ordenaciones se definen con el método
sort, pudiendo añadir un parámetrodescendingsi queremos un orden descendente
df_descendente = df.sort("precio", descending=True)
# Ordenar por varias columnas con diferentes criterios
df_ordenado = df_ventas.sort(
by=["categoria", "precio"],
descending=[False, True]
)
Ejercicio 1
Crea un programa llamado ParquesEolicosPotenciaPolars.py que utilice este archivo CSV sobre parques eólicos de la comunidad de Castilla y León. Se deben ordenar los parques eólicos de mayor a menor potencia total, y quedarnos con el nombre, municipio, provincia y potencia de los 10 primeros.
Ejercicio 2
Sobre este fichero Parquet crearemos un programa TaxisNYCPolars.py para filtrar aquellos viajes cuya distancia sea superior a 10 millas (columna trip_distance superior a 10) y número de pasajeros (columna passenger_count) superior a 2. Quédate con las columnas 'tpep_pickup_datetime' (fecha y hora de recogida), 'passenger_count', 'trip_distance', 'fare_amount' (tarifa base) y 'tip_amount' (propina). Muestra los resultados.