
本文介绍如何在 Polars 中替代 pandas 的 groupby(...).describe(),通过自定义 describe 表达式函数配合 agg() 实现高性能、可读性强的分组统计,避免低效的 map_groups,并支持懒加载优化。
本文介绍如何在 polars 中替代 pandas 的 `groupby(...).describe()`,通过自定义 `describe` 表达式函数配合 `agg()` 实现高性能、可读性强的分组统计,避免低效的 `map_groups`,并支持懒加载优化。
在从 Pandas 迁移至 Polars 的过程中,一个常见痛点是:Pandas 中简洁的 df.groupby('col').value.describe() 在 Polars 中没有直接等价语法。初学者常误用 group_by(...).map_groups(...) 模拟行为,但该方式会破坏查询优化、无法利用 Polars 的向量化优势,且在 LazyFrame 场景下完全失效。
更优解是将 describe 逻辑拆解为一组 Polars 表达式(Expr),再通过 agg(**dict) 统一聚合。这种方式完全兼容 eager/lazy 模式,执行速度快,语义清晰,也便于复用和扩展。
以下是一个生产就绪的 describe 工具函数:
def describe(column, percentiles=None):
"""
构建 Polars 表达式字典,用于 group_by.agg() 实现类似 pandas.describe() 的统计。
Parameters:
-----------
column : pl.Expr
待统计的列表达式(如 pl.col("value"))
percentiles : list of float, optional
百分位数列表,默认为 [0.25, 0.5, 0.75]
Returns:
--------
dict: 键为统计名(str),值为 pl.Expr
"""
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"{pct * 100:g}%": column.quantile(pct, interpolation="linear")
for pct in percentiles
},
"max": column.max(),
}使用示例如下(复现原始数据):
from io import BytesIO
import polars as pl
S = b"""group,value
3,245
3,28
3,48
1,113
1,288
1,165
2,90
2,21
2,109"""
pl_df = pl.read_csv(BytesIO(S))
result = (
pl_df
.group_by("group")
.agg(**describe(pl.col("value")))
.sort("group") # 可选:按 group 排序提升可读性
)
print(result)输出结构清晰、列名语义明确:
shape: (3, 10) ┌───────┬───────┬────────────┬────────────┬───────────┬───────┬───────┬───────┬───────────┐ │ group ┆ count ┆ null_count ┆ mean ┆ std ┆ min ┆ 25% ┆ 50% ┆ max │ │ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │ │ i64 ┆ u32 ┆ u32 ┆ f64 ┆ f64 ┆ i64 ┆ f64 ┆ f64 ┆ i64 │ ╞═══════╪═══════╪════════════╪════════════╪═══════════╪═══════╪═══════╪═══════╪═══════════╡ │ 1 ┆ 3 ┆ 0 ┆ 188.666667 ┆ 87.771731 ┆ 113 ┆ 165.0 ┆ 165.0 ┆ 288 │ │ 2 ┆ 3 ┆ 0 ┆ 73.333333 ┆ 45.092499 ┆ 21 ┆ 90.0 ┆ 90.0 ┆ 109 │ │ 3 ┆ 3 ┆ 0 ┆ 107.0 ┆ 112.716458 ┆ 28 ┆ 48.0 ┆ 48.0 ┆ 245 │ └───────┴───────┴────────────┴────────────┴───────────┴───────┴───────┴───────┴───────────┘
✅ 关键优势总结:
-
性能卓越:纯表达式链,全程保留 Polars 查询优化器能力(尤其在
LazyFrame中可融合、剪枝、并行化); -
零运行时开销:无需 Python 回调(
map_groups内部需序列化/反序列化 DataFrame); -
高度可定制:轻松增删统计项(如添加
median、iqr、n_unique)、调整百分位插值方式(interpolation="higher"等); -
类型安全:返回
DataFrame列类型由表达式自动推导(如count→u32,mean→f64); -
无缝兼容 Lazy:只需将
pl.read_csv(...)替换为pl.scan_csv(...),其余代码完全不变。
⚠️ 注意事项:
-
quantile()默认使用"nearest"插值,若需与 Pandas 的'linear'行为严格对齐,请显式传入interpolation="linear"(如上例所示); - 若需处理含空值的列,
null_count和count已天然区分,但mean/std等默认跳过 null;如需包含 null 的统计逻辑,需额外预处理; - 不建议在
describe()中混用pl.lit()或非列表达式——这会强制触发 eager 计算,破坏 lazy 优势。
掌握这一模式,你就能以 Polars 原生、高效、可维护的方式,优雅替代 Pandas 中最常用的 .describe() 分组分析场景。

















