Polars DataFrame 聚合方法全解析:逐列归约与水平聚合的 API 语义与底层实现
2026/9/9 20:43:27 网站建设 项目流程

Polars DataFrame 聚合方法全解析:逐列归约与水平聚合的 API 语义与底层实现

【免费下载链接】polarsExtremely fast Query Engine for DataFrames, written in Rust项目地址: https://gitcode.com/GitHub_Trending/po/polars

Polars 为DataFrame提供了一整套将整张表归约为单行的聚合 API,覆盖计数、最值、求和、均值、中位数、方差、标准差、连乘与分位数等统计量,并提供"按列聚合"与"按行横向聚合"两种维度。本文以官方 API 参考文档 aggregation.rst 所列的 13 个方法为主线,逐一讲解调用方式、空值与类型语义,并结合 Python 包装层与 Rust 内核源码说明这些聚合是如何被执行的,帮助你写出既正确又高效的数据归约代码。

一、聚合 API 全景:13 个方法分三类

Polars 中DataFrame的聚合 API 覆盖三个维度。整份参考索引来自 aggregation.rst,其 autosummary 列表如下:

逐列(vertical / per-column)归约,结果为单行 DataFramecountmaxminsummeanmedianstdvarproductquantile

横向(horizontal / per-row)归约,结果为与行数等长的 Seriesmax_horizontalmin_horizontalsum_horizontalmean_horizontal

两种维度都能直接以df.方法名()的方式无参数(或极少参数)调用,逐列聚合对每一列独立计算后拼成一行,横向聚合则对每一行跨列计算。全部方法的 Python 实现集中在 frame.py,其中每个方法的 docstring 都附有完整可复现的输出示例。

二、逐列聚合:把每一列压缩成一个标量

逐列聚合的共同形状约定是:输入是 N 行 DataFrame,输出永远是(1, C)的单行 DataFrame,列名保持不变,列数与输入一致。这组方法对应LazyFrame上同名方法(见 lazyframe/frame.py),并经由self.lazy().xxx()._collect_eager(...)在构建逻辑计划后立即执行,详见下文"执行路径"一节。

2.1 count:按列统计非空值数量

与直觉不同,count统计的不是行数,而是每列中非 null 元素的个数

import polars as pl df = pl.DataFrame({"a": [1, 2, 3, 4], "b": [1, 2, 1, None], "c": [None] * 4}) print(df.count())

输出:

shape: (1, 3) ┌─────┬─────┬─────┐ │ a ┆ b ┆ c │ │ --- ┆ --- ┆ --- │ │ u32 ┆ u32 ┆ u32 │ ╞═════╪═════╪═════╡ │ 4 ┆ 3 ┆ 0 │ └─────┴─────┴─────┘

注意输出列类型是u32,全 null 的c列计数为 0 而非 null(docstring 见 frame.py)。如果需要的是"行数 / 每列长度",应改用df.heightpl.len(),与count的语义区分清楚。

2.2 min / max:数值与字符串都适用的最值

minmax是唯二天然支持字符串列的数值型归约(按字典序取最值),也可用于数值、布尔列。以max为例:

df = pl.DataFrame({"foo": [1, 2, 3], "bar": [6, 7, 8], "ham": ["a", "b", "c"]}) print(df.max())
shape: (1, 3) ┌─────┬─────┬─────┐ │ foo ┆ bar ┆ ham │ │ --- ┆ --- ┆ --- │ │ i64 ┆ i64 ┆ str │ ╞═════╪═════╪═════╡ │ 3 ┆ 8 ┆ c │ └─────┴─────┴─────┘

df.min()得到1 / 6 / "a",列类型同样保持不变。如果列内含有 null,归约会忽略它们(全 null 列结果为 null)。

2.3 sum:求和,布尔按 0/1 参与

sum对数值列求和;布尔列视为 0/1 参与并输出整数;字符串列无法求和,结果为 null(见 frame.py 的示例输出中ham列值为null):

df = pl.DataFrame({"foo": [1, 2, 3], "bar": [6, 7, 8], "ham": ["a", "b", "c"]}) print(df.sum())
shape: (1, 3) ┌─────┬─────┬──────┐ │ foo ┆ bar ┆ ham │ │ --- ┆ --- ┆ --- │ │ i64 ┆ i64 ┆ str │ ╞═════╪═════╪══════╡ │ 6 ┆ 21 ┆ null │ └─────┴─────┴──────┘

2.4 mean:平均值,统一输出浮点

mean对数值列取算术平均并统一转换为f64;有意思的是布尔列也会参与——把True视为 1、False视为 0,null 被忽略,因此[True, False, None]的均值是 0.5。字符串列输出 null:

df = pl.DataFrame( {"foo": [1, 2, 3], "bar": [6, 7, 8], "ham": ["a", "b", "c"], "spam": [True, False, None]} ) print(df.mean())
shape: (1, 4) ┌─────┬─────┬──────┬──────┐ │ foo ┆ bar ┆ ham ┆ spam │ │ --- ┆ --- ┆ --- ┆ --- │ │ f64 ┆ f64 ┆ str ┆ f64 │ ╞═════╪═════╪══════╪══════╡ │ 2.0 ┆ 7.0 ┆ null ┆ 0.5 │ └─────┴─────┴──────┴──────┘

2.5 median:中位数

median取每列中位数并输出f64。对奇数个元素,中位数是排序后正中间的值(如[1, 2, 3]的中位数为 2.0);字符串列输出 null:

df = pl.DataFrame({"foo": [1, 2, 3], "bar": [6, 7, 8], "ham": ["a", "b", "c"]}) print(df.median())
shape: (1, 3) ┌─────┬─────┬──────┐ │ foo ┆ bar ┆ ham │ │ --- ┆ --- ┆ --- │ │ f64 ┆ f64 ┆ str │ ╞═════╪═════╪══════╡ │ 2.0 ┆ 7.0 ┆ null │ └─────┴─────┴──────┘

2.6 std / var:标准差与方差,可调 ddof

stdvar都接受一个仅有关键词参数ddof(Delta Degrees of Freedom,默认 1),公式中分母为N - ddof。这意味着默认结果对应样本标准差/样本方差,传ddof=0则得到总体标准差/总体方差:

df = pl.DataFrame({"foo": [1, 2, 3], "bar": [6, 7, 8], "ham": ["a", "b", "c"]}) print(df.std()) # ddof=1 → foo: 1.0, bar: 1.0, ham: null print(df.std(ddof=0)) # foo: 0.816497, bar: 0.816497 print(df.var()) # foo: 1.0, bar: 1.0 print(df.var(ddof=0)) # foo: 0.666667, bar: 0.666667

字符串列两类方法都输出 null。实现层面,ddof以整数传入并最终落到 Rust 的std_reduce(ddof)/var_reduce(ddof)(见下文 Rust reducer 说明)。

2.7 product:连乘(仅数值与布尔)

product对每列求连乘,返回列类型与输入一致。它的特殊性在于实现是逐列构造表达式:遍历 schema 时只对数值与布尔列调用F.col(name).product(),其余类型的列用F.lit(None).alias(name)生成占位 null(见 frame.py):

df = pl.DataFrame({"a": [1, 2, 3], "b": [0.5, 4, 10], "c": [True, True, False]}) print(df.product())
shape: (1, 3) ┌─────┬──────┬─────┐ │ a ┆ b ┆ c │ │ --- ┆ --- ┆ --- │ │ i64 ┆ f64 ┆ i64 │ ╞═════╪══════╪═════╡ │ 6 ┆ 20.0 ┆ 0 │ └─────┴──────┴─────┘

布尔列按 0/1 参与连乘,因此只要出现过False(0)结果即为 0,输出提升为整数类型。仓库测试用例 test_df.py 的test_product覆盖了该行为。

2.8 quantile:任意分位数 + 六种插值策略

quantile(quantile, interpolation)返回输入列在指定分位处的取值,支持范围0.0 ~ 1.0的浮点分位数。第二个参数interpolation的合法取值在类型别名 QuantileMethod 中定义,共六种:

interpolation含义
"nearest"取最接近的分位观测值(默认)
"higher"取不小于目标的最近观测值
"lower"取不大于目标的最近观测值
"midpoint"取上下两个最近观测值的中点
"linear"在相邻观测值间线性插值
"equiprobable"等概率分位策略

当分位数恰好命中排序后的某个位置时(如0.5与奇数个元素),各策略结果一致。文档中的基准示例即用quantile(0.5, "nearest")模拟中位数,结果列统一为f64,字符串列输出 null:

df = pl.DataFrame({"foo": [1, 2, 3], "bar": [6, 7, 8], "ham": ["a", "b", "c"]}) print(df.quantile(0.5, "nearest"))
shape: (1, 3) ┌─────┬─────┬──────┐ │ foo ┆ bar ┆ ham │ │ --- ┆ --- ┆ --- │ │ f64 ┆ f64 ┆ str │ ╞═════╪═════╪══════╡ │ 2.0 ┆ 7.0 ┆ null │ └─────┴─────┴──────┘

Rust 端对应的插值枚举定义在 rolling/mod.rs,默认策略即Nearest,与 Python 层签名interpolation: QuantileMethod = "nearest"保持一致。

三、水平聚合:按行跨列归约,产出 Series

水平聚合与逐列聚合互补:它们对每一行在多个列上计算归约,因此行数不变,返回值是一根Series(列名分别为"max"/"min"/"sum"/"mean")。在DataFrame上,它们通过select(F.max_horizontal(F.all()))之类的调用把所有列传入函数级 API 后再to_series()取回单列(见 frame.py)。

四个函数级入口定义在 horizontal.py,均接受可迭代的表达式/列名/字面量参数。

3.1 max_horizontal / min_horizontal

逐行比较各列取最大/最小值,null 视为缺失并被忽略(整行全为 null 时结果才为 null)。混合整型与浮点列时结果按 supertype 提升为f64

df = pl.DataFrame({"foo": [1, 2, 3], "bar": [4.0, 5.0, 6.0]}) print(df.max_horizontal()) # [4.0, 5.0, 6.0] print(df.min_horizontal()) # [1.0, 2.0, 3.0]

函数级用法允许只挑选部分列,例如pl.max_horizontal("a", "b")或传入任意表达式。空值与边界行为(含 NaN 与全 null 列)由仓库测试 test_horizontal.py 中的test_max_min_nulls_consistencytest_min_max_horizontal_with_nan_28682等用例覆盖。

3.2 sum_horizontal / mean_horizontal:ignore_nulls 开关

这两个方法多一个仅有关键词参数ignore_nulls(默认True)。其为True时跳过 null 只对非空值聚合;置为False后,只要某行出现任一 null,该行结果即为 null——这是横向聚合中最需要留意的空值传播开关:

df = pl.DataFrame({"foo": [1, 2, 3], "bar": [4.0, 5.0, 6.0]}) print(df.sum_horizontal()) # [5.0, 7.0, 9.0] print(df.mean_horizontal()) # [2.5, 3.5, 4.5] df2 = pl.DataFrame({"a": [1, 8, 3], "b": [4, 5, None]}) print(df2.sum_horizontal()) # 第 3 行为 3(忽略 null) print(df2.sum_horizontal(ignore_nulls=False)) # 第 3 行为 null

mean 的示例来自 horizontal.py。需要额外说明的是:若全部列均为 null 且ignore_nulls=Truesum_horizontal返回 0 而mean_horizontal返回 null(对应的全部 null/无列等边界场景见 test_horizontal.py 的test_mean_horizontal_all_nulltest_sum_null_dtype)。

混合不同类型时水平归约会先做 supertype 提升(如整型 + 浮点 → 浮点);字符串参与sum_horizontal时行为不同,建议先select出数值列再聚合,相关讨论可参照 test_horizontal.py 的字符串用例。

四、执行路径:从 DataFrame 方法到 Rust 内核

理解这些方法"快在哪里"有助于在正确场景使用它们。以df.max()为例,其 Python 实现并非在 Python 层逐列循环,而是:

return self.lazy().max()._collect_eager(optimizations=QueryOptFlags._eager())

即先把 DataFrame 提升为LazyFrame、挂载聚合节点,再以 eager 方式立即收集执行(见 frame.py)。min / sum / mean / median / std / var / quantile / count全部走同一套"lazy 计划 + 立即执行"的通路,从而复用查询优化器对聚合的规划能力;product与水平聚合则通过构造表达式后在select中求值。

在内核一侧,每种逐列聚合最终对应Column上的一个归约函数。仓库在 column/mod.rs 中集中提供了min_reducemax_reducemedian_reducemean_reducestd_reduce(ddof)var_reduce(ddof)sum_reduceproductquantile_reduce(quantile, method)等方法,且同时处理普通 Series 与 Scalar(常量列)两种内部表示——对 Scalar 列会先物化为极小的 Series 再归约,以保证数值语义一致。换句话说,Python 层 13 个方法把"用户意图"翻译为聚合表达式,真正的归约计算全部在 Rust 中以向量化、SIMD 友好的方式完成。

五、实用要点速查

  • 形状记忆:逐列聚合输出单行 DataFrame(1 × C);水平聚合输出与行数等长的 Series;count统计非空数而非行数。
  • 字符串列min/max支持字符串(字典序);sum/mean/median/std/var/quantile/product对字符串列产出 null(product通过占位 null 表达式实现)。
  • 布尔列summeanproduct会将其当作 0/1 参与计算,mean结果提升为f64
  • ddof 语义std/var默认ddof=1(样本统计量),设 0 即为总体统计量。
  • quantile 插值:六种策略中"nearest"为默认,效果与median在 0.5 分位点一致(奇数样本时)。
  • 水平聚合空值max/min_horizontal忽略 null;sum/mean_horizontal默认忽略 null,置ignore_nulls=False可让任何 null 扩散到结果行。
  • 性能路径:DataFrame 聚合均经由 LazyFrame 计划与_collect_eager执行,适合将整列压缩为标量的批量统计任务;若后续还要分组或追加其他变换,应优先使用 lazy API(LazyFrame.max/sum/...)把聚合与变换合并成一次查询。

六、继续深入

  • 聚合方法的全部实现与 docstring:frame.py
  • LazyFrame 侧的等价聚合 API(用于链式查询):lazyframe/frame.py
  • 水平聚合函数级入口(可指定任意列组合):horizontal.py
  • Rust 内核归约实现(min_reduce等):column/mod.rs
  • 分位插值策略的 Rust 枚举: rolling/mod.rs
  • 横向聚合行为测试:test_horizontal.py
  • 聚合相关回归测试:test_aggregations.py

掌握这 13 个方法的语义与空值/类型规则后,再遇到"给每列算一个统计量"或"按行拼出总分/最大值"这类需求,你就可以直接用最简短、可读的 DataFrame 聚合调用完成,同时放心它们底层跑在 Polars 的 Rust 归约内核上。

【免费下载链接】polarsExtremely fast Query Engine for DataFrames, written in Rust项目地址: https://gitcode.com/GitHub_Trending/po/polars

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询