
在 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 原生、可优化、高性能且语义清晰的标准解法。










