Compose in lazy pipelines¶
Keep the statistics inside the query: no collect() in the middle, no to_numpy(), no Python callback.
Score inside a lazy query¶
Every method returns a pl.Expr, so it slots into a LazyFrame plan and runs under the optimiser. Join the
parameters, score, filter, and collect once:
import polars as pl
import polars_stats as ps
readings = pl.LazyFrame(
{
"sensor": ["a", "a", "b", "b"],
"reading": [9.8, 10.4, 100.0, 135.0],
}
)
baselines = pl.LazyFrame(
{
"sensor": ["a", "b"],
"mu": [10.0, 100.0],
"sigma": [0.5, 2.0],
}
)
anomalies = (
readings.join(baselines, on="sensor")
.with_columns(score=ps.Normal(mu="mu", sigma="sigma").sf("reading"))
.filter(pl.col("score") < 0.01)
.collect()
)
print(anomalies)
shape: (1, 5)
┌────────┬─────────┬───────┬───────┬────────────┐
│ sensor ┆ reading ┆ mu ┆ sigma ┆ score │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ str ┆ f64 ┆ f64 ┆ f64 ┆ f64 │
╞════════╪═════════╪═══════╪═══════╪════════════╡
│ b ┆ 135.0 ┆ 100.0 ┆ 2.0 ┆ 7.1635e-69 │
└────────┴─────────┴───────┴───────┴────────────┘
Each reading is scored against the baseline carried on its own row, the tail probability is filtered in the same plan, and the whole thing collects once.
Aggregate a density within a window¶
Methods are elementwise, so they compose inside a window expression: compute per row, then aggregate with over. Here
each row's density is divided by its group's total to give a within-group share, kept aligned to the frame:
df = pl.DataFrame(
{
"group": ["a", "a", "b"],
"mu": [0.0, 0.0, 5.0],
"sigma": [1.0, 1.0, 2.0],
"x": [0.0, 1.0, 5.0],
}
)
dist = ps.Normal(mu="mu", sigma="sigma")
print(
df.with_columns(
density=dist.pdf("x"),
group_share=dist.pdf("x") / dist.pdf("x").sum().over("group"),
)
)
shape: (3, 6)
┌───────┬─────┬───────┬─────┬──────────┬─────────────┐
│ group ┆ mu ┆ sigma ┆ x ┆ density ┆ group_share │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ str ┆ f64 ┆ f64 ┆ f64 ┆ f64 ┆ f64 │
╞═══════╪═════╪═══════╪═════╪══════════╪═════════════╡
│ a ┆ 0.0 ┆ 1.0 ┆ 0.0 ┆ 0.398942 ┆ 0.622459 │
│ a ┆ 0.0 ┆ 1.0 ┆ 1.0 ┆ 0.241971 ┆ 0.377541 │
│ b ┆ 5.0 ┆ 2.0 ┆ 5.0 ┆ 0.199471 ┆ 1.0 │
└───────┴─────┴───────┴─────┴──────────┴─────────────┘
Collect per-group values with group_by¶
Under group_by(...).agg(...) the same elementwise call collects into one list per group rather than reducing:
shape: (2, 2)
┌───────┬──────────────────────┐
│ group ┆ density │
│ --- ┆ --- │
│ str ┆ list[f64] │
╞═══════╪══════════════════════╡
│ a ┆ [0.398942, 0.241971] │
│ b ┆ [0.199471] │
└───────┴──────────────────────┘
Wrap it in an aggregation if you want a single number per group, for example
agg(mean_density=dist.pdf("x").mean()).
Related¶
- Use column-valued parameters: where those
mu/sigmacolumns come from. - Explanation / Architecture: why a scalar parameter is expanded to a row-aligned expression, and why that keeps these methods elementwise under partitioning.