Lección 0.5.4 · 45 min
Polars: API lazy frente a eager, expresiones, rendimiento y cuándo elegirlo frente a pandas
Polars es una librería de tablas escrita en Rust que describe los cálculos como expresiones y deja que un optimizador decida cómo ejecutarlos. Esta lección repite con Polars los cálculos de las lecciones 0.5.2 y 0.5.3 sobre SINT, enseña a leer un plan de consulta y termina con una regla práctica para elegir entre Polars y pandas.
- Módulo
- 0.5 · Computación numérica y datos
- Duración
- 45 min
Lecciones del nivel 0
0.1 · Bienvenida y expectativas
0.2 · Entorno de trabajo moderno
- 0.2.1 Instalación de Python con uv: versiones y ciclo de soporte
- 0.2.2 Entornos virtuales y dependencias: uv init, uv add, pyproject.toml y lockfiles
- 0.2.3 Editores (VS Code, Cursor, PyCharm) y notebooks (Jupyter, JupyterLab, marimo)
- 0.2.4 Terminal básica, rutas, variables de entorno y ficheros .env
- 0.2.5 Git y GitHub: commits, ramas, pull requests y .gitignore
- 0.2.6 Introducción a los asistentes de código con IA: prompts, revisión y tests
0.3 · Fundamentos del lenguaje
- 0.3.1 Variables, tipos y operadores; coma flotante y decimal para precios y ticks
- 0.3.2 Cadenas, f-strings y formato de números y fechas
- 0.3.3 Listas, tuplas, diccionarios, conjuntos y comprensiones
- 0.3.4 Control de flujo: if, elif, else, for, while y match-case
- 0.3.5 Funciones: argumentos, *args y **kwargs, lambdas y ámbito
- 0.3.6 Excepciones; módulos y paquetes, con los imports perezosos de Python 3.15
0.4 · Python intermedio
- 0.4.1 Programación orientada a objetos: clases, herencia, composición y dataclasses
- 0.4.2 Tipado: type hints, mypy o ty y Pydantic para configuraciones
- 0.4.3 Iteradores, generadores, decoradores y context managers
- 0.4.4 Ficheros (CSV, JSON, YAML, TOML) y fechas con zonas horarias
- 0.4.5 Logging profesional y testing con pytest
- 0.4.6 Concurrencia: threading, multiprocessing, asyncio y free-threading
0.5 · Computación numérica y datos
- 0.5.1 NumPy 2: arrays, broadcasting, vectorización, álgebra lineal y generadores aleatorios
- 0.5.2 pandas 3.0: Series, DataFrame, índices temporales, Copy-on-Write, tipo str y pd.col()
- 0.5.3 Selección, filtrado, groupby, merge, pivot, resample y ventanas móviles
- 0.5.4 Polars: API lazy frente a eager, expresiones, rendimiento y cuándo elegirlo frente a pandas
- 0.5.5 DuckDB y SQL esencial: SELECT, JOIN, GROUP BY y funciones de ventana sobre Parquet
0.6 · Visualización
0.7 · Matemáticas y estadística mínimas
Otra forma de trabajar con tablas
pandas nació en 2008 sobre NumPy y conserva decisiones de entonces: un índice en cada tabla, operaciones que se ejecutan una a una en el orden en que se escriben y, en buena parte, un solo hilo. Polars, escrito en Rust y con el formato en memoria de Apache Arrow, parte de otras ideas:
| pandas | Polars | |
|---|---|---|
| Índice | Sí (DatetimeIndex, alineación) | No: las fechas son una columna |
| Forma de escribir | Métodos sobre Series y DataFrame | Expresiones dentro de contextos |
| Ejecución | Paso a paso (eager) | Eager o lazy (consulta optimizada) |
| Paralelismo | Mayoritariamente un hilo | Multihilo por defecto |
| Ausentes | NaN (y NA en algunos tipos) | null en todos los tipos |
El curso usa Polars 2.0, publicado en octubre de 20261Nota 1 · Polars 2.0.0 en PyPI (pypi.org/project/polars/2.0.0), publicado el 6 de octubre de 2026; consultado el 21 de
octubre de 2026. (versión fijada en curso-code/uv.lock). Las funciones que
aparecen en esta lección ya existían en la serie 1.x: lo aprendido aquí sirve también para código algo más antiguo.
import polars as pl
df = pl.read_csv("sint_diario.csv", try_parse_dates=True)
print(pl.__version__, df.shape)
print(df.schema)
df.select("fecha", "cierre", "volumen").head(3)2.0.0 (1303, 6)
Schema({'fecha': Date, 'apertura': Float64, 'maximo': Float64, 'minimo': Float64, 'cierre': Float64, 'volumen': Int64})
shape: (3, 3)
┌────────────┬────────┬─────────┐
│ fecha ┆ cierre ┆ volumen │
│ --- ┆ --- ┆ --- │
│ date ┆ f64 ┆ i64 │
╞════════════╪════════╪═════════╡
│ 2021-01-04 ┆ 99.14 ┆ 2322609 │
│ 2021-01-05 ┆ 99.25 ┆ 1517954 │
│ 2021-01-06 ┆ 97.38 ┆ 3245658 │
└────────────┴────────┴─────────┘try_parse_dates=True reconoce la columna de fechas, que pasa a ser de tipo Date (un día sin hora). No hay índice:
la tabla tiene seis columnas, y fecha es una de ellas. Las filas se seleccionan por posición o con filtros, nunca por
etiqueta.
Expresiones y contextos
La pieza central de Polars es la expresión: una descripción de un cálculo sobre columnas que todavía no se ha
ejecutado. pl.col("cierre") es «la columna cierre», y pl.col("cierre").pct_change() es «la variación de la columna
cierre respecto a la fila anterior». Una expresión se evalúa en un contexto:
| Contexto | Qué hace | Equivalente en pandas |
|---|---|---|
select(...) | Devuelve solo las columnas de las expresiones | df[[...]], agg |
with_columns(...) | Añade o sustituye columnas | assign, pd.col() |
filter(...) | Conserva las filas en que la expresión es verdadera | máscara de bool |
group_by(...).agg(...) | Evalúa las expresiones dentro de cada grupo | groupby().agg |
r = pl.col("cierre").pct_change()
print(r)
barras = df.with_columns(
r=r,
rango=(pl.col("maximo") - pl.col("minimo")) / pl.col("cierre"),
)
barras.select("fecha", "cierre", "r", "rango").head(3)col("cierre").pct_change([dyn int: 1])
shape: (3, 4)
┌────────────┬────────┬───────────┬──────────┐
│ fecha ┆ cierre ┆ r ┆ rango │
│ --- ┆ --- ┆ --- ┆ --- │
│ date ┆ f64 ┆ f64 ┆ f64 │
╞════════════╪════════╪═══════════╪══════════╡
│ 2021-01-04 ┆ 99.14 ┆ null ┆ 0.015836 │
│ 2021-01-05 ┆ 99.25 ┆ 0.00111 ┆ 0.011688 │
│ 2021-01-06 ┆ 97.38 ┆ -0.018841 ┆ 0.019203 │
└────────────┴────────┴───────────┴──────────┘Impresa, r es solo la descripción del cálculo. Se puede guardar en una variable y reutilizar en varias consultas:
es la misma idea que pd.col() de pandas 3.0 (lección 0.5.2). La primera rentabilidad sale
null, el valor ausente de Polars, que existe igual en columnas de números, texto y fechas.
Dentro de un contexto, las expresiones se evalúan en paralelo y cada una produce una columna. Por eso cada
resultado necesita un nombre propio: dos expresiones sobre r sin alias darían un error de nombre duplicado.
caidas = barras.filter(
(pl.col("r") < -0.03)
& (pl.col("volumen") > pl.col("volumen").median())
)
print(caidas.height, "caídas fuertes con volumen alto")
barras.select(
media=pl.col("r").mean(),
vol_anual=pl.col("r").std() * 252 ** 0.5,
sesiones=pl.col("r").count(),
)10 caídas fuertes con volumen alto
shape: (1, 3)
┌──────────┬───────────┬──────────┐
│ media ┆ vol_anual ┆ sesiones │
│ --- ┆ --- ┆ --- │
│ f64 ┆ f64 ┆ u32 │
╞══════════╪═══════════╪══════════╡
│ 0.000342 ┆ 0.180962 ┆ 1302 │
└──────────┴───────────┴──────────┘Las diez sesiones son las mismas que en la lección 0.5.3, y la mediana del volumen se calcula dentro del filtro sin
variables auxiliares. count() no cuenta los null: 1.302 rentabilidades de 1.303 cierres.
group_by y ventanas
group_by funciona como en pandas, con la clave escrita también como expresión. El resultado de un group_by no
conserva el orden de los grupos (se calculan en paralelo): para una tabla legible se ordena con sort o se pide
maintain_order=True.
por_ano = (
barras.group_by(pl.col("fecha").dt.year().alias("ano"))
.agg(
n=pl.col("r").count(),
anual=(1 + pl.col("r")).product() - 1,
vol=pl.col("r").std() * 252 ** 0.5,
)
.sort("ano")
)
por_ano.with_columns(pl.col("anual", "vol").round(4))shape: (5, 4)
┌──────┬─────┬────────┬────────┐
│ ano ┆ n ┆ anual ┆ vol │
│ --- ┆ --- ┆ --- ┆ --- │
│ i32 ┆ u32 ┆ f64 ┆ f64 │
╞══════╪═════╪════════╪════════╡
│ 2021 ┆ 259 ┆ 0.152 ┆ 0.1599 │
│ 2022 ┆ 260 ┆ -0.085 ┆ 0.2483 │
│ 2023 ┆ 260 ┆ 0.1421 ┆ 0.1495 │
│ 2024 ┆ 262 ┆ 0.1432 ┆ 0.1499 │
│ 2025 ┆ 261 ┆ 0.0424 ┆ 0.1792 │
└──────┴─────┴────────┴────────┘Las cifras coinciden con las de pandas en la lección 0.5.3: −8,5 % en 2022 con una volatilidad del 24,8 %. pl.col
admite varios nombres a la vez, y la expresión se aplica a cada columna.
Las ventanas móviles y acumuladas son métodos de las expresiones. La volatilidad de 21 sesiones y el drawdown, en un solo contexto:
riesgo = barras.with_columns(
vol_21=pl.col("r").rolling_std(21) * 252 ** 0.5,
dd=pl.col("cierre") / pl.col("cierre").cum_max() - 1,
)
riesgo.select(
vol_max=pl.col("vol_21").max(),
sin_ventana=pl.col("vol_21").null_count(),
peor_dd=pl.col("dd").min(),
fecha_fondo=pl.col("fecha").get(pl.col("dd").arg_min()),
)shape: (1, 4)
┌──────────┬─────────────┬───────────┬─────────────┐
│ vol_max ┆ sin_ventana ┆ peor_dd ┆ fecha_fondo │
│ --- ┆ --- ┆ --- ┆ --- │
│ f64 ┆ u32 ┆ f64 ┆ date │
╞══════════╪═════════════╪═══════════╪═════════════╡
│ 0.339454 ┆ 21 ┆ -0.250278 ┆ 2022-10-20 │
└──────────┴─────────────┴───────────┴─────────────┘Volatilidad máxima del 33,9 %, 21 valores sin ventana completa y un drawdown del 25 % con fondo el 20 de octubre de 2022: los mismos números que en pandas. Polars no tiene índice, así que «la fecha del mínimo» se escribe como una expresión: la fecha en la posición del mínimo del drawdown.
Los cálculos por grupo que conservan todas las filas usan over, el equivalente de las funciones de ventana de SQL
(lección 0.5.5). En el libro de operaciones, el peso de cada operación en el resultado de su símbolo:
ops = pl.read_csv("operaciones_sint.csv", try_parse_dates=True)
signo = pl.when(pl.col("lado") == "largo").then(1).otherwise(-1)
pnl = (signo * (pl.col("precio_salida") - pl.col("precio_entrada"))
* pl.col("cantidad") - pl.col("comision"))
ops = ops.with_columns(pnl=pnl).with_columns(
n_simbolo=pl.len().over("simbolo"),
total_simbolo=pl.col("pnl").sum().over("simbolo"),
)
ops.select("id", "simbolo", "pnl", "n_simbolo", "total_simbolo").head(4)shape: (4, 5)
┌─────┬─────────┬──────────┬───────────┬───────────────┐
│ id ┆ simbolo ┆ pnl ┆ n_simbolo ┆ total_simbolo │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ i64 ┆ str ┆ f64 ┆ u32 ┆ f64 │
╞═════╪═════════╪══════════╪═══════════╪═══════════════╡
│ 1 ┆ ORO ┆ -3123.56 ┆ 3 ┆ -3237.96 │
│ 2 ┆ SINT ┆ 7.43 ┆ 30 ┆ 4405.08 │
│ 3 ┆ SINT ┆ -31.5 ┆ 30 ┆ 4405.08 │
│ 4 ┆ SINT ┆ -306.72 ┆ 30 ┆ 4405.08 │
└─────┴─────────┴──────────┴───────────┴───────────────┘pl.when(...).then(...).otherwise(...) es el np.where de las expresiones. Con over("simbolo"), la suma se calcula
dentro de cada símbolo y se repite en todas sus filas: la operación 1, de ORO, ve el total de las tres operaciones
de ORO. Con group_by habría una fila por símbolo; con over, se conservan las 40.
Lazy frente a eager
Todo lo anterior es eager: cada with_columns o filter se ejecuta al momento y devuelve un DataFrame. La API
lazy cambia el orden: scan_parquet (o scan_csv, o df.lazy()) devuelve un LazyFrame, que solo acumula la
consulta. Nada se lee ni se calcula hasta collect(), y antes de ejecutar, el optimizador reescribe el plan completo:
Fuente Mermaid
flowchart LR A[scan_parquet] --> B[filter] B --> C[with_columns] C --> D[collect]
consulta = (
pl.scan_parquet("sint_diario.parquet")
.filter(pl.col("fecha").dt.year() == 2022)
.with_columns(r=pl.col("cierre").pct_change())
.select("fecha", "r")
)
print(type(consulta).__name__)
print(consulta.explain())LazyFrame
simple π 2/2 ["fecha", "r"]
WITH_COLUMNS:
[col("cierre").pct_change([dyn int: 1]).alias("r")]
Parquet SCAN [sint_diario.parquet]
PROJECT 2/6 COLUMNS
SELECTION: (col("fecha").dt.year() == 2022)
ESTIMATED ROWS: 1303El plan se lee de abajo arriba. El optimizador ha hecho dos cosas que en eager no son posibles:
- Proyección empujada (
PROJECT 2/6 COLUMNS): la consulta solo usafechaycierre, así que el lector de Parquet no carga las otras cuatro columnas del disco. - Filtro empujado (
SELECTION): la condición del año se aplica mientras se lee el fichero, y Parquet puede saltarse bloques enteros que no cumplen la condición gracias a las estadísticas que guarda de cada uno.
Con un fichero de 50 KB no se nota. Con años de barras de un minuto de cientos de símbolos, la diferencia está entre leer unos cientos de megas y leer decenas de gigas. Este plan, sin embargo, tiene un problema que no es de rendimiento:
antes = consulta.collect()
despues = (
pl.scan_parquet("sint_diario.parquet")
.with_columns(r=pl.col("cierre").pct_change())
.filter(pl.col("fecha").dt.year() == 2022)
.select("fecha", "r")
.collect()
)
print(antes.head(2))
print(despues.head(2))shape: (2, 2)
┌─────────────────────┬───────────┐
│ fecha ┆ r │
│ --- ┆ --- │
│ datetime[μs] ┆ f64 │
╞═════════════════════╪═══════════╡
│ 2022-01-03 00:00:00 ┆ null │
│ 2022-01-04 00:00:00 ┆ -0.017069 │
└─────────────────────┴───────────┘
shape: (2, 2)
┌─────────────────────┬───────────┐
│ fecha ┆ r │
│ --- ┆ --- │
│ datetime[μs] ┆ f64 │
╞═════════════════════╪═══════════╡
│ 2022-01-03 00:00:00 ┆ 0.000263 │
│ 2022-01-04 00:00:00 ┆ -0.017069 │
└─────────────────────┴───────────┘En la primera consulta, el filtro se aplica antes de pct_change: el 3 de enero de 2022 ya no tiene el cierre del 31
de diciembre y su rentabilidad sale null. En la segunda, la rentabilidad se calcula con toda la serie y después se
filtra: el 3 de enero tiene su 0,03 %. El optimizador no cambia el orden en este caso, porque pct_change depende de
la fila anterior y el resultado sería otro; la responsabilidad de escribir el orden correcto es de quien escribe la
consulta. El error, en sentido contrario, es más grave: cualquier cálculo que mire hacia atrás (shift, ventanas,
máximos acumulados) debe hacerse con la historia completa antes de recortar el periodo de un backtest.
¿Cuándo usar cada modo? Eager es cómodo para explorar en un notebook: cada paso muestra su resultado. Lazy conviene
en cuanto los datos vienen de ficheros grandes o la consulta tiene varios pasos, y es el modo natural de los scripts que
procesan datos de forma periódica (la descarga diaria de barras, el recálculo nocturno de señales). Una consulta lazy
termina en collect(), que devuelve un DataFrame, o en sink_parquet("salida.parquet"), que escribe el resultado
en disco sin reunirlo entero en memoria. Y df.lazy() convierte un DataFrame ya cargado en un LazyFrame, para
aprovechar el optimizador en una cadena larga de operaciones.
fecha aparece como datetime[μs] y no como Date: el Parquet de los datos lo guardó pandas, que escribe las fechas
con hora. Leer el mismo dato de dos fuentes y comparar los tipos es un control que evita uniones que no casan.
Rendimiento sin cronómetro
Esta lección no da tiempos: dependen de la máquina, de la versión y del tamaño de los datos, y los de un fichero de 1.303 filas no dicen nada. Lo que sí se puede afirmar es por qué Polars suele ser más rápido con datos grandes, y cuándo deja de serlo:
- Las expresiones se ejecutan en Rust, en varios hilos y sobre columnas contiguas de Arrow, sin un objeto de Python por valor.
- En lazy, el optimizador elimina columnas y filas innecesarias antes de leerlas y reutiliza subexpresiones comunes.
- Con el motor de streaming, una consulta lazy procesa ficheros más grandes que la memoria por bloques.
- Una función de Python dentro de una expresión (
map_elements) devuelve el cálculo a Python fila a fila y anula casi todas las ventajas. Si una operación no existe como expresión, suele haber una combinación de expresiones que la hace.
Polars y pandas juntos
La mayoría de los proyectos usan las dos. Las conversiones son directas, con dos detalles: pandas necesita un índice y los tipos de fecha cambian de resolución.
pdf = df.to_pandas().set_index("fecha")
print(type(pdf).__name__, pdf.index.dtype, pdf.shape)
de_vuelta = pl.from_pandas(pdf, include_index=True)
print(de_vuelta.schema["fecha"], de_vuelta.shape)DataFrame datetime64[ms] (1303, 5)
Datetime(time_unit='ms', time_zone=None) (1303, 6)El Date de Polars llega a pandas como datetime64[ms] y vuelve como Datetime, no como Date. Si el resto del
código compara fechas entre tablas, conviene normalizar el tipo en un único sitio (por ejemplo, con
pl.col("fecha").cast(pl.Date)). La conversión usa Arrow por debajo y, para tipos numéricos, es rápida; no es gratis
con tablas grandes, así que se hace una vez, en la frontera entre las dos partes del código.
| Situación | Elección | Por qué |
|---|---|---|
| Ficheros Parquet grandes, filtros y agregaciones | Polars lazy | Proyección y filtro empujados, multihilo |
| Barras intradía de muchos símbolos por grupo | Polars | group_by y over en paralelo |
Series temporales con índice, resample, alineación | pandas | El índice hace el trabajo |
| statsmodels, scikit-learn, librerías de backtesting | pandas (o NumPy) | Es lo que esperan |
| Tablas de cientos o miles de filas | La que ya use el proyecto | La diferencia no se nota |
La lección 0.5.5 añade una tercera opción para las mismas consultas: SQL sobre Parquet con DuckDB.
Ejercicios
- Con
group_by_dynamic("fecha", every="1mo"), calcula el último cierre de cada mes de SINT y su rentabilidad mensual. Compara la etiqueta de cada mes con la deresample("ME")de pandas (lección 0.5.3). ¿Qué riesgo tiene esa etiqueta?
Solución
mensual = (
df.group_by_dynamic("fecha", every="1mo")
.agg(pl.col("cierre").last())
.with_columns(r=pl.col("cierre").pct_change())
)
print(mensual.height)
mensual.head(3)60
shape: (3, 3)
┌────────────┬────────┬──────────┐
│ fecha ┆ cierre ┆ r │
│ --- ┆ --- ┆ --- │
│ date ┆ f64 ┆ f64 │
╞════════════╪════════╪══════════╡
│ 2021-01-01 ┆ 95.99 ┆ null │
│ 2021-02-01 ┆ 97.65 ┆ 0.017293 │
│ 2021-03-01 ┆ 103.27 ┆ 0.057552 │
└────────────┴────────┴──────────┘Los valores coinciden con los de pandas (1,73 % en febrero de 2021), pero la etiqueta es el inicio del mes
(2021-02-01) y el valor es el cierre del final. Si esa tabla se cruza por fecha con datos diarios, el
1 de febrero «conocería» el cierre del 26 de febrero: anticipación. Lo seguro es desplazar la etiqueta al final del
periodo o usar el resultado solo a partir del mes siguiente.
- Escribe una consulta lazy sobre
sint_diario.parquetque devuelva la rentabilidad anual de cada año (compuesta a partir de las rentabilidades diarias, calculadas con toda la serie) y comprueba conexplain()cuántas columnas se leen del fichero.
Solución
anual = (
pl.scan_parquet("sint_diario.parquet")
.with_columns(r=pl.col("cierre").pct_change())
.group_by(pl.col("fecha").dt.year().alias("ano"))
.agg(anual=(1 + pl.col("r")).product() - 1)
.sort("ano")
)
print("PROJECT 2/6" in anual.explain())
anual.collect().with_columns(pl.col("anual").round(4))True
shape: (5, 2)
┌──────┬────────┐
│ ano ┆ anual │
│ --- ┆ --- │
│ i32 ┆ f64 │
╞══════╪════════╡
│ 2021 ┆ 0.152 │
│ 2022 ┆ -0.085 │
│ 2023 ┆ 0.1421 │
│ 2024 ┆ 0.1432 │
│ 2025 ┆ 0.0424 │
└──────┴────────┘Solo se leen fecha y cierre. product() ignora el null de la primera fila, así que 2021 compone sus 259
rentabilidades. Las cifras son las mismas que con pandas y que en el group_by eager.
- Con
ops(que ya tienepnl), calcula para cada operación la diferencia entre su resultado y la media de las operaciones de su mismo símbolo, sin perder filas. Muestra las tres operaciones deTECH.
Solución
(
ops.with_columns(
frente_media=pl.col("pnl")
- pl.col("pnl").mean().over("simbolo"),
)
.filter(pl.col("simbolo") == "TECH")
.select("id", "pnl", "frente_media")
)shape: (3, 3)
┌─────┬─────────┬──────────────┐
│ id ┆ pnl ┆ frente_media │
│ --- ┆ --- ┆ --- │
│ i64 ┆ f64 ┆ f64 │
╞═════╪═════════╪══════════════╡
│ 10 ┆ 239.14 ┆ 250.813333 │
│ 18 ┆ -47.0 ┆ -35.326667 │
│ 38 ┆ -227.16 ┆ -215.486667 │
└─────┴─────────┴──────────────┘over calcula la media dentro de cada símbolo y la repite en sus filas; la resta se hace fila a fila. Es el patrón de
cualquier normalización por grupo (rentabilidad frente a la media del sector, volumen frente a la media del símbolo).
Footnotes
-
Polars 2.0.0 en PyPI (pypi.org/project/polars/2.0.0), publicado el 6 de octubre de 2026; consultado el 21 de octubre de 2026. ↩
Respuesta plegada en cada pregunta
Autoevaluación
¿Qué devuelve
pl.col("cierre").pct_change()si se evalúa sola, fuera de unDataFrame?- AUna
Seriescon las rentabilidades de SINT - BUna expresión: la descripción de un cálculo que se ejecutará cuando se use dentro de
select,with_columnsofilter - CUn error, porque no se ha leído ningún fichero
- DUn número: la rentabilidad media
Ver respuesta
Respuesta B. Una expresión: la descripción de un cálculo que se ejecutará cuando se use dentro de
select,with_columnsofilterLas expresiones de Polars no contienen datos. Se pueden guardar en variables y combinar, y solo se evalúan en un contexto (
select,with_columns,filter,group_by().agg).- AUna
En una consulta lazy, ¿qué significa
PROJECT 2/6 COLUMNSdentro deParquet SCANen la salida deexplain()?- AQue el fichero tiene dos columnas dañadas
- BQue solo se leerán del fichero las 2 columnas que la consulta necesita, de 6
- CQue la consulta devolverá 2 filas de cada 6
- DQue se usarán 2 hilos de 6
Ver respuesta
Respuesta B. Que solo se leerán del fichero las 2 columnas que la consulta necesita, de 6
Es la proyección empujada al lector (*projection pushdown*): Polars analiza la consulta completa y no lee del disco las columnas que no se usan. Con ficheros grandes, es la mayor parte del ahorro.
Se quiere la rentabilidad diaria de SINT en 2022. ¿Por qué
filtrar 2022 → pct_changeypct_change → filtrar 2022dan resultados distintos?- ANo dan resultados distintos: el optimizador los iguala
- BPorque en la primera, la rentabilidad del primer día de 2022 no tiene el cierre del 31 de diciembre de 2021 y sale nula
- CPorque la segunda lee el fichero dos veces
- DPorque
pct_changeno funciona en modo lazy
Ver respuesta
Respuesta B. Porque en la primera, la rentabilidad del primer día de 2022 no tiene el cierre del 31 de diciembre de 2021 y sale nula
pct_changedepende de la fila anterior. Si el filtro va antes, la fila anterior al 3 de enero de 2022 ha desaparecido. El optimizador respeta el orden cuando cambiar el orden cambiaría el resultado.¿En cuál de estos casos tiene más sentido Polars que pandas?
- AUn notebook que usa statsmodels y una librería de backtesting que espera un
DataFramede pandas conDatetimeIndex - BAgregar barras de un minuto de cientos de símbolos guardadas en decenas de ficheros Parquet
- CUna tabla de 40 operaciones
- DCualquier caso: Polars siempre es mejor
Ver respuesta
Respuesta B. Agregar barras de un minuto de cientos de símbolos guardadas en decenas de ficheros Parquet
Polars gana con datos grandes y consultas que filtran y agregan, sobre todo en lazy y sobre Parquet. Con tablas pequeñas o código que depende del ecosistema de pandas, la conversión y el cambio de API no compensan.
- AUn notebook que usa statsmodels y una librería de backtesting que espera un
Ideas clave
- Polars no tiene índice: las fechas son una columna más. Los cálculos se escriben como expresiones (
pl.col(...)) dentro deselect,with_columns,filterygroup_by().agg. - Eager (
read_*) ejecuta cada paso al momento; lazy (scan_*) acumula la consulta y la ejecuta entera concollect(), después de optimizarla. explain()muestra el plan: el filtro y las columnas que se empujan al lector de Parquet son lo que más ahorra en ficheros grandes.- El orden de las operaciones importa con cálculos que miran a la fila anterior (
pct_change,shift, ventanas): filtrar antes puede romperlos. - Polars para datos grandes y consultas de agregación; pandas cuando el resto del código lo espera.
to_pandas()ypl.from_pandas()conectan los dos.