Polars GroupBy:如何高效忽略 NaN 值计算均值

17次阅读

Polars GroupBy:如何高效忽略 NaN 值计算均值

在 polars 中,`mean()` 默认不忽略 nan,需显式调用 `drop_nans()` 或 `fill_nan(none)` 预处理,二者语义等价但性能表现随数据规模和分组数变化;推荐优先使用 `fill_nan(none).mean()` 以获得更优并行效率。

Polars 的聚合函数(如 pl.col(“values”).mean())默认将 NaN 视为有效值参与计算——一旦组内存在任意 NaN,整组均值即返回 NaN,这与 pandas 的 nanmean 行为不一致。要实现“忽略 NaN 求均值”,最简洁、高效且符合 Polars 原生范式的方式是在聚合前清除 NaN 语义干扰,而非依赖 python 层的 map_elements(因其破坏查询优化、无法向量化、严重拖慢性能)。

✅ 推荐方案:fill_nan(None).mean()
这是目前最优实践。fill_nan(None) 将 NaN 替换为 NULL(Polars 的缺失值原生表示),而 mean() 对 null 值天然跳过(无需额外配置):

import polars as pl import numpy as np  test_data = pl.DataFrame({     "group": ["A", "A", "B", "B"],     "values": [1.0, np.nan, 2.0, 3.0] })  result = test_data.group_by("group").agg(     pl.col("values").fill_nan(None).mean().alias("mean_ignore_nan") ) print(result)

输出:

shape: (2, 2) ┌───────┬────────────────┐ │ group ┆ mean_ignore_nan │ │ ---   ┆ ---             │ │ str   ┆ f64             │ ╞═══════╪═════════════════╡ │ A     ┆ 1.0             │ │ B     ┆ 2.5             │ └───────┴─────────────────┘

⚠️ 替代方案:drop_nans().mean() 同样正确,但实测在大数据量(如亿级行)下略慢于 fill_nan(None)。其原理是物理删除 NaN 元素后再计算,而 fill_nan(None) 仅做标记替换,更利于底层内存布局优化与线程调度。

? 性能关键洞察:

  • 1 亿行、20% NaN、少量分组 场景下,fill_nan(None).mean() 比 drop_nans().mean() 快约 1.6×(737ms vs 1210ms);
  • 但当分组数急剧增加(如数千组),drop_nans() 的并行粒度优势可能反超——建议在实际业务数据上用 %timeit 验证;
  • 二者结果完全一致,且均远快于 map_elements(Lambda x: np.nanmean(x.to_numpy()))(后者在亿级数据上可能慢 10–100 倍)。

? 注意事项:

  • fill_nan(None) 仅影响当前表达式链,不修改原始列;
  • 若列中同时存在 null 和 NaN,fill_nan(None) 会将 NaN 转为 null,之后 mean() 自动统一忽略所有 null;
  • 确保数值列类型为浮点型(如 f64),整型列无法存储 NaN,需先 cast(pl.Float64);
  • 使用 maintain_order=True 可保留分组输出顺序,便于调试或下游确定性消费。

总之,摒弃 map_elements,拥抱 fill_nan(None).mean() —— 它是 Polars 原生、可优化、高性能且语义清晰的标准解法。

text=ZqhQzanResources