
本文介绍如何在 Polars 中替代低效的 map_groups + describe() 模式,通过自定义聚合函数结合 agg(**dict) 实现高性能、可读性强的分组统计,结果直接返回结构化 DataFrame,语义清晰且兼容 LazyFrame 优化。
本文介绍如何在 polars 中替代低效的 `map_groups` + `describe()` 模式,通过自定义聚合函数结合 `agg(**dict)` 实现高性能、可读性强的分组统计,结果直接返回结构化 dataframe,语义清晰且兼容 lazyframe 优化。
在从 Pandas 迁移至 Polars 的过程中,一个常见痛点是:Pandas 中简洁的 df.groupby('col').value.describe() 在 Polars 中并无直接等价语法。初学者常误用 group_by(...).map_groups(...) 手动调用 Series.describe(),但这不仅性能较差(强制物化每组数据、绕过查询优化),还导致结果需额外 pivot 整理,代码冗长且难以维护。
更优解是将 describe 语义拆解为一组原生 Polars 表达式(Expr),再通过 agg(**mapping) 一次性完成分组聚合。这种方式完全契合 Polars 的惰性计算范式,支持 LazyFrame 链式优化,执行效率高,且输出格式天然规整——每列对应一项统计指标,无需转置或 pivot。
以下是一个生产就绪的 describe 工具函数:
def describe(column, percentiles=None):
if percentiles is None:
percentiles = [0.25, 0.5, 0.75]
return {
"count": column.count(),
"null_count": column.null_count(),
"mean": column.mean(),
"std": column.std(),
"min": column.min(),
**{f"{p*100:g}%": column.quantile(p) for p in percentiles},
"max": column.max(),
}
使用示例如下:
import polars as pl
from io import BytesIO
S = b"""group,value
3,245
3,28
3,48
1,113
1,288
1,165
2,90
2,21
2,109"""
df = pl.read_csv(BytesIO(S))
result = df.group_by("group").agg(**describe(pl.col("value")))
print(result)
输出为标准宽表格式(3 行 × 10 列),每行代表一个 group,各列即统计项(count, mean, 25%, max 等),类型信息完整保留,可直接用于后续分析或导出。
✅ 优势总结:
- ✅ 高性能:避免
map_groups的 Python 层循环与中间 DataFrame 物化; - ✅ 可优化:表达式可被 Polars 查询优化器识别(尤其在
LazyFrame场景); - ✅ 可扩展:轻松增删统计项(如添加
median、iqr或自定义分位数); - ✅ 强类型安全:各统计列保持原始数据类型推断(如
count为u32,mean为f64); - ✅ 零依赖:纯 Polars 原生 API,不引入额外库或运行时开销。
⚠️ 注意事项:
-
column.std()默认为样本标准差(ddof=1),若需总体标准差,请显式写为column.std(ddof=0); - 分位数计算在 Polars 中采用线性插值法(与 NumPy/Pandas 一致),但小数据集结果可能与 Pandas 存在浮点精度级微小差异;
- 若需处理含空值的复杂场景,建议搭配
column.drop_nulls().quantile(...)显式控制缺失值逻辑。
该模式不仅适用于 describe,也适用于任何需“分组后多指标聚合”的场景——核心思想是:用表达式代替函数,用声明代替过程。这是掌握 Polars 函数式数据处理的关键跃迁。










