---
title: "0.5.4 Polars: lazy API, expressions and performance"
description: "Polars on the SINT data: expressions, with_columns, group_by and windows; the lazy API with scan_parquet and explain; when to pick it over pandas."
url: "https://pythonparatrading.com/en/python-trading-course/level-0/polars-lazy-api-and-expressions/"
lang: "en"
image: "https://pythonparatrading.com/og/en/python-trading-course/level-0/polars-lazy-api-and-expressions.png"
---

# 0.5.4 · Polars: lazy versus eager API, expressions, performance and when to pick it over pandas

> Polars is a table library written in Rust that describes computations as expressions and lets an optimizer decide how to run them. This lesson repeats with Polars the computations from lessons 0.5.2 and 0.5.3 on SINT, shows how to read a query plan and ends with a practical rule for choosing between Polars and pandas.

- Course: [Level 0: Python from scratch and a data mindset](https://pythonparatrading.com/en/python-trading-course/level-0/)
- Module: 0.5 · Numerical computing and data
- Duration: 45 min
- Prerequisites: [0.5.3 · Selection, filtering, groupby, merge, pivot, resample and moving windows](https://pythonparatrading.com/en/python-trading-course/level-0/pandas-groupby-merge-resample-rolling/)

## By the end of this lesson

- Read CSV and Parquet with Polars and create derived columns with `pl.col`, `with_columns`, `select` and `filter`.
- Aggregate with `group_by` and compute rolling windows and per-group values with `rolling_std`, `cum_max` and `over`.
- Build a lazy query with `scan_parquet`, read its plan with `explain()` and recognize the filter and the projection pushed down to the file.
- Convert tables between Polars and pandas and decide which one to use based on data size and the rest of the code.

## Another way to work with tables

pandas was born in 2008 on top of NumPy and keeps decisions from that time: an index on every table, operations that
run one by one in the order they are written and, for the most part, a single thread. Polars, written in Rust and using
the Apache Arrow in-memory format, starts from different ideas:

| | pandas | Polars |
|---|---|---|
| Index | Yes (`DatetimeIndex`, alignment) | No: dates are a column |
| Way of writing | Methods on `Series` and `DataFrame` | Expressions inside contexts |
| Execution | Step by step (*eager*) | *Eager* or *lazy* (optimized query) |
| Parallelism | Mostly a single thread | Multithreaded by default |
| Missing values | `NaN` (and `NA` in some types) | `null` in all types |

The course uses Polars 2.0, released in October 2026[^1] (version pinned in `curso-code/uv.lock`). The functions that
appear in this lesson already existed in the 1.x series: what you learn here also applies to somewhat older code.

```python
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)
```
```output
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` recognizes the date column, which becomes of type `Date` (a day with no time). There is no
index: the table has six columns, and `fecha` is one of them. Rows are selected by position or with filters, never by
label.

## Expressions and contexts

The central piece of Polars is the **expression**: a description of a computation on columns that has not run yet.
`pl.col("cierre")` is "the cierre column", and `pl.col("cierre").pct_change()` is "the change of the cierre column
relative to the previous row". An expression is evaluated in a **context**:

| Context | What it does | pandas equivalent |
|---|---|---|
| `select(...)` | Returns only the columns of the expressions | `df[[...]]`, `agg` |
| `with_columns(...)` | Adds or replaces columns | `assign`, `pd.col()` |
| `filter(...)` | Keeps the rows where the expression is true | `bool` mask |
| `group_by(...).agg(...)` | Evaluates the expressions within each group | `groupby().agg` |

```python
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)
```
```output
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 │
└────────────┴────────┴───────────┴──────────┘
```

Printed, `r` is only the description of the computation. It can be stored in a variable and reused in several
queries: it is the same idea as pandas 3.0's `pd.col()` (lesson 0.5.2). The first return comes out `null`, the Polars
missing value, which exists in the same way in columns of numbers, text and dates.

Inside a context, expressions are evaluated **in parallel** and each one produces a column. That is why each result
needs its own name: two expressions on `r` without `alias` would raise a duplicate name error.

```python
caidas = barras.filter(
    (pl.col("r") < -0.03)
    & (pl.col("volumen") > pl.col("volumen").median())
)
print(caidas.height, "sharp drops on high volume")

barras.select(
    media=pl.col("r").mean(),
    vol_anual=pl.col("r").std() * 252 ** 0.5,
    sesiones=pl.col("r").count(),
)
```
```output
10 sharp drops on high volume
shape: (1, 3)
┌──────────┬───────────┬──────────┐
│ media    ┆ vol_anual ┆ sesiones │
│ ---      ┆ ---       ┆ ---      │
│ f64      ┆ f64       ┆ u32      │
╞══════════╪═══════════╪══════════╡
│ 0.000342 ┆ 0.180962  ┆ 1302     │
└──────────┴───────────┴──────────┘
```

The ten sessions are the same as in lesson 0.5.3, and the volume median is computed inside the filter without
auxiliary variables. `count()` does not count `null` values: 1,302 returns from 1,303 closes.

## group_by and windows

`group_by` works as in pandas, with the key also written as an expression. The result of a `group_by` does not keep the
order of the groups (they are computed in parallel): for a readable table, sort it with `sort` or ask for
`maintain_order=True`.

```python
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))
```
```output
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 │
└──────┴─────┴────────┴────────┘
```

The figures match the pandas ones in lesson 0.5.3: −8.5 % in 2022 with a volatility of 24.8 %. `pl.col` accepts
several names at once, and the expression is applied to each column.

Rolling and cumulative windows are methods of expressions. The 21-session volatility and the drawdown, in a single
context:

```python
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()),
)
```
```output
shape: (1, 4)
┌──────────┬─────────────┬───────────┬─────────────┐
│ vol_max  ┆ sin_ventana ┆ peor_dd   ┆ fecha_fondo │
│ ---      ┆ ---         ┆ ---       ┆ ---         │
│ f64      ┆ u32         ┆ f64       ┆ date        │
╞══════════╪═════════════╪═══════════╪═════════════╡
│ 0.339454 ┆ 21          ┆ -0.250278 ┆ 2022-10-20  │
└──────────┴─────────────┴───────────┴─────────────┘
```

Maximum volatility of 33.9 %, 21 values without a full window and a 25 % drawdown with its trough on October 20,
2022: the same numbers as in pandas. Polars has no index, so "the date of the minimum" is written as an expression:
the date at the position of the drawdown minimum.

**Per-group computations that keep all rows** use `over`, the equivalent of SQL window functions (lesson 0.5.5). In
the trade log, the weight of each trade in the result of its symbol:

```python
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)
```
```output
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(...)` is the `np.where` of expressions. With `over("simbolo")`, the sum is computed
within each symbol and repeated on all its rows: trade 1, on `ORO`, sees the total of the three `ORO` trades. With
`group_by` there would be one row per symbol; with `over`, all 40 are kept.

## Lazy versus eager

Everything above is **eager**: each `with_columns` or `filter` runs immediately and returns a `DataFrame`. The
**lazy** API changes the order: `scan_parquet` (or `scan_csv`, or `df.lazy()`) returns a `LazyFrame`, which only
accumulates the query. Nothing is read or computed until `collect()`, and before running, the optimizer rewrites the
whole plan:

```mermaid
flowchart LR
  A[scan_parquet] --> B[filter]
  B --> C[with_columns]
  C --> D[collect]
```

```python
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())
```
```output
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: 1303
```

The plan is read from bottom to top. The optimizer has done two things that are not possible in eager mode:

- **Projection pushdown** (`PROJECT 2/6 COLUMNS`): the query only uses `fecha` and `cierre`, so the Parquet reader
  does not load the other four columns from disk.
- **Predicate pushdown** (`SELECTION`): the year condition is applied while the file is read, and Parquet can skip
  whole blocks that do not meet the condition thanks to the statistics it stores for each one.

With a 50 KB file you will not notice. With years of one-minute bars for hundreds of symbols, the difference is between
reading a few hundred megabytes and reading tens of gigabytes. This plan, however, has a problem that is not about
performance:

```python
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))
```
```output
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 │
└─────────────────────┴───────────┘
```

In the first query, the filter is applied before `pct_change`: January 3, 2022 no longer has the December 31 close
and its return comes out `null`. In the second, the return is computed with the whole series and filtered afterwards:
January 3 has its 0.03 %. The optimizer does not change the order in this case, because `pct_change` depends on the
previous row and the result would be different; writing the correct order is the responsibility of whoever writes the
query. The mistake in the opposite direction is more serious: any computation that looks backwards (`shift`, windows,
cumulative maxima) must be done with the full history before trimming the period of a backtest.

When should you use each mode? Eager is convenient for exploring in a notebook: each step shows its result. Lazy pays
off as soon as the data comes from large files or the query has several steps, and it is the natural mode for scripts
that process data periodically (the daily bar download, the nightly signal recalculation). A lazy query ends in
`collect()`, which returns a `DataFrame`, or in `sink_parquet("salida.parquet")`, which writes the result to disk
without gathering it all in memory. And `df.lazy()` turns an already loaded `DataFrame` into a `LazyFrame`, to take
advantage of the optimizer in a long chain of operations.

`fecha` appears as `datetime[μs]` and not as `Date`: the data's Parquet file was saved by pandas, which writes dates
with a time. Reading the same data from two sources and comparing the types is a check that prevents joins that do not
match.

## Performance without a stopwatch

This lesson gives no timings: they depend on the machine, the version and the data size, and those of a 1,303-row file
say nothing. What can be stated is **why** Polars is usually faster with large data, and when it stops being so:

- Expressions run in Rust, on several threads and on contiguous Arrow columns, without a Python object per value.
- In lazy mode, the optimizer removes unnecessary columns and rows before reading them and reuses common
  subexpressions.
- With the *streaming* engine, a lazy query processes files larger than memory in chunks.
- A Python function inside an expression (`map_elements`) hands the computation back to Python row by row and cancels
  almost all the advantages. If an operation does not exist as an expression, there is usually a combination of
  expressions that does it.

<aside class="callout" data-kind="nota">

To measure on your machine, compare the same query in pandas and in Polars with data of the real size of your research
and with `timeit` (lesson 0.5.1). A toy file favors whichever has the lower fixed cost, not the faster one.

</aside>

## Polars and pandas together

Most projects use both. Conversions are direct, with two details: pandas needs an index and date types change
resolution.

```python
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)
```
```output
DataFrame datetime64[ms] (1303, 5)
Datetime(time_unit='ms', time_zone=None) (1303, 6)
```

The Polars `Date` reaches pandas as `datetime64[ms]` and comes back as `Datetime`, not as `Date`. If the rest of the
code compares dates across tables, it is worth normalizing the type in a single place (for example, with
`pl.col("fecha").cast(pl.Date)`). The conversion uses Arrow under the hood and, for numeric types, it is fast; it is not
free with large tables, so it is done once, at the boundary between the two parts of the code.

| Situation | Choice | Why |
|---|---|---|
| Large Parquet files, filters and aggregations | Polars lazy | Projection and predicate pushdown, multithreaded |
| Intraday bars for many symbols by group | Polars | `group_by` and `over` in parallel |
| Indexed time series, `resample`, alignment | pandas | The index does the work |
| statsmodels, scikit-learn, backtesting libraries | pandas (or NumPy) | It is what they expect |
| Tables of hundreds or thousands of rows | Whichever the project already uses | The difference is not noticeable |

Lesson 0.5.5 adds a third option for the same queries: SQL on Parquet with DuckDB.

## Exercises

1. With `group_by_dynamic("fecha", every="1mo")`, compute the last close of each month for SINT and its monthly
   return. Compare the label of each month with that of pandas `resample("ME")` (lesson 0.5.3). What risk does that
   label carry?

**Solution.**

```python
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)
```
```output
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 │
└────────────┴────────┴──────────┘
```

The values match the pandas ones (1.73 % in February 2021), but the label is the **start** of the month
(`2021-02-01`) and the value is the close at the **end**. If that table is joined by date with daily data, February 1
would "know" the February 26 close: look-ahead bias. The safe option is to shift the label to the end of the period or
to use the result only from the following month on.

2. Write a lazy query on `sint_diario.parquet` that returns the annual return of each year (compounded from the daily
   returns, computed with the whole series) and check with `explain()` how many columns are read from the file.

**Solution.**

```python
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))
```
```output
True
shape: (5, 2)
┌──────┬────────┐
│ ano  ┆ anual  │
│ ---  ┆ ---    │
│ i32  ┆ f64    │
╞══════╪════════╡
│ 2021 ┆ 0.152  │
│ 2022 ┆ -0.085 │
│ 2023 ┆ 0.1421 │
│ 2024 ┆ 0.1432 │
│ 2025 ┆ 0.0424 │
└──────┴────────┘
```

Only `fecha` and `cierre` are read. `product()` ignores the `null` in the first row, so 2021 compounds its 259
returns. The figures are the same as with pandas and as in the eager `group_by`.

3. With `ops` (which already has `pnl`), compute for each trade the difference between its result and the mean of the
   trades on the same symbol, without losing rows. Show the three `TECH` trades.

**Solution.**

```python
(
    ops.with_columns(
        frente_media=pl.col("pnl")
        - pl.col("pnl").mean().over("simbolo"),
    )
    .filter(pl.col("simbolo") == "TECH")
    .select("id", "pnl", "frente_media")
)
```
```output
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` computes the mean within each symbol and repeats it on its rows; the subtraction is done row by row. It is the
pattern of any per-group normalization (return versus the sector mean, volume versus the symbol mean).

[^1]: Polars 2.0.0 on PyPI (pypi.org/project/polars/2.0.0), released on October 6, 2026; accessed on October 21,
    2026.

## Self-assessment

1. What does `pl.col("cierre").pct_change()` return when evaluated on its own, outside a `DataFrame`?

   - A. A `Series` with the SINT returns
   - B. An error, because no file has been read
   - C. A number: the mean return
   - D. An expression: the description of a computation that will run when used inside `select`, `with_columns` or `filter`

   *Answer D: An expression: the description of a computation that will run when used inside `select`, `with_columns` or `filter`.* Polars expressions hold no data. They can be stored in variables and combined, and they are only evaluated in a context (`select`, `with_columns`, `filter`, `group_by().agg`).

2. In a lazy query, what does `PROJECT 2/6 COLUMNS` inside `Parquet SCAN` mean in the output of `explain()`?

   - A. That the file has two corrupted columns
   - B. That only the 2 columns the query needs, out of 6, will be read from the file
   - C. That the query will return 2 rows out of every 6
   - D. That 2 threads out of 6 will be used

   *Answer B: That only the 2 columns the query needs, out of 6, will be read from the file.* It is the projection pushed down to the reader (*projection pushdown*): Polars analyzes the whole query and does not read from disk the columns that are not used. With large files, that is most of the savings.

3. You want the daily return of SINT in 2022. Why do `filter 2022 → pct_change` and `pct_change → filter 2022` give different results?

   - A. They do not give different results: the optimizer makes them equal
   - B. Because the second one reads the file twice
   - C. Because in the first one, the return of the first day of 2022 lacks the close of December 31, 2021 and comes out null
   - D. Because `pct_change` does not work in lazy mode

   *Answer C: Because in the first one, the return of the first day of 2022 lacks the close of December 31, 2021 and comes out null.* `pct_change` depends on the previous row. If the filter goes first, the row before January 3, 2022 is gone. The optimizer keeps the order when changing it would change the result.

4. In which of these cases does Polars make more sense than pandas?

   - A. Aggregating one-minute bars for hundreds of symbols stored in dozens of Parquet files
   - B. A notebook that uses statsmodels and a backtesting library that expects a pandas `DataFrame` with a `DatetimeIndex`
   - C. A table of 40 trades
   - D. Any case: Polars is always better

   *Answer A: Aggregating one-minute bars for hundreds of symbols stored in dozens of Parquet files.* Polars wins with large data and queries that filter and aggregate, especially in lazy mode and on Parquet. With small tables or code that depends on the pandas ecosystem, the conversion and the API change do not pay off.

## Key points

- Polars has no index: dates are just another column. Computations are written as expressions (`pl.col(...)`) inside `select`, `with_columns`, `filter` and `group_by().agg`.
- Eager (`read_*`) runs each step immediately; lazy (`scan_*`) accumulates the query and runs it whole with `collect()`, after optimizing it.
- `explain()` shows the plan: the filter and the columns pushed down to the Parquet reader are what saves the most on large files.
- The order of operations matters with computations that look at the previous row (`pct_change`, `shift`, windows): filtering first can break them.
- Polars for large data and aggregation queries; pandas when the rest of the code expects it. `to_pandas()` and `pl.from_pandas()` connect the two.

## Resources

### Research

- [Big Data for trading with ArcticDB](https://pythonparatrading.com/en/big-data-for-trading/) · Research-0019 · 15 min

### Documentation

- [Polars: user guide](https://docs.pola.rs/user-guide/getting-started/) · docs.pola.rs
- [Polars: expressions and contexts](https://docs.pola.rs/user-guide/concepts/expressions-and-contexts/) · docs.pola.rs
- [Polars: lazy API](https://docs.pola.rs/user-guide/concepts/lazy-api/) · docs.pola.rs
- [Polars: migrating from pandas](https://docs.pola.rs/user-guide/migration/pandas/) · docs.pola.rs

### Data

- [Level 0 data: SINT daily bars (Parquet)](https://pythonparatrading.com/curso/datos/nivel-0/sint_diario.parquet) · Download · sint_diario.parquet

Previous lesson: [0.5.3 · Selection, filtering, groupby, merge, pivot, resample and moving windows](https://pythonparatrading.com/en/python-trading-course/level-0/pandas-groupby-merge-resample-rolling/)

Next lesson: [0.5.5 · DuckDB and essential SQL: SELECT, JOIN, GROUP BY and window functions on Parquet](https://pythonparatrading.com/en/python-trading-course/level-0/duckdb-sql-on-parquet/)
