Window Functions in Polars: Grouped Ranking, Sorting, and Aggregation with `over`

发布时间:2026/9/10 3:21:52
Window Functions in Polars: Grouped Ranking, Sorting, and Aggregation with `over` Window Functions in Polars: Grouped Ranking, Sorting, and Aggregation withover【免费下载链接】polarsExtremely fast Query Engine for DataFrames, written in Rust项目地址: https://gitcode.com/GitHub_Trending/po/polars本指南以 Polars 官方用户手册中 window-functions.md 为主体系统讲解 PolarsRust 实现的极速 DataFrame 查询引擎中通过.over()实现的分组窗口表达式在select/with_columns上下文中按组做排名、排序、聚合并映射回原表行涵盖group_to_rows/explode/join三种结果映射策略。读完你将掌握按组打分的over写法、与group_by的取舍、三种mapping_strategy的语义与性能差异以及.over()在 Python API 与 Rust 引擎中的实现脉络。仓库相关文档与源码本文所述均为当前仓库内实际内容用户指南 expressions/index.md、Python 窗口函数参考 py-polars/docs/source/reference/expressions/window.rst。什么是 Window Function不压缩行数的分组计算Window function 是一类“带超能力”的表达式。普通的group_by聚合会把多行折叠成每组一行而窗口函数允许你在select上下文里对分组执行聚合同时保留每一行的位置把计算结果映射回对应的行。这是它区别于普通聚合最核心的一点group_by通常产出一个行数等于“分组个数”的 DataFrameover通常产出一个与原始 DataFrame 行数相同的 DataFrame。这与 SQL 中 PostgreSQL 的 window function 语义一致——Polars Python API 对over的 docstring 中也直接把它与 PostgreSQL 的窗口函数对照见 py-polars/src/polars/expr/expr.py 中over方法说明。由于.over()这类窗口表达式不改变行数它非常适合在同一张表的列上边算分组结果边保留明细典型场景是组内排名、组内排序取 Top-N、组内均值/求和并广播回每行。数据准备加载宝可梦数据集先加载一个宝可梦数据集。为后续演示“按类型分组”的语义示例将Type 1、Type 2两列 cast 为Enum类型import polars as pl types ( Grass Water Fire Normal Ground Electric Psychic Fighting Bug Steel Flying Dragon Dark Ghost Poison Rock Ice Fairy.split() ) type_enum pl.Enum(types) # then lets load some csv data with information about pokemon pokemon pl.read_csv( docs/assets/data/pokemon.csv, ).cast({Type 1: type_enum, Type 2: type_enum}) print(pokemon.head())该示例的完整可执行脚本位于 docs/source/src/python/user-guide/expressions/window.py对应的 Rust 版本示例位于 docs/source/src/rust/user-guide/expressions/window.rsRust 侧通过reqwest从远程读取该 CSV。按组运算对 “Speed” 列做组内排名假设我们要给宝可梦的 “Speed”速度排名但不希望做全局排名而是希望在每个由 “Type 1” 定义的类型组内部排名。做法是先写出对 “Speed” 列排名的表达式再追加.over(Type 1)指明“在列 ‘Type 1’ 的每个唯一值上执行”result pokemon.select( pl.col(Name, Type 1), pl.col(Speed).rank(dense, descendingTrue).over(Type 1).alias(Speed rank), ) print(result)Python 侧.over()签名见 py-polars/src/polars/expr/expr.py。Rust 侧对应实现是Expr::over完整签名含mapping_strategy的等价参数见下文“源码层面的三种映射”一节。背后的执行直觉可以把 Polars 想象成先选出 “Type 1” 列取值相同的那些行子集仅对这个子集计算排名表达式然后把该组的结果投影回原始行Polars 对所有存在的组重复这一过程。下图高亮了 “Type 1” 为 “Grass” 的那组宝可梦的排名计算过程注意一个容易误解的例子宝可梦 “Golbat” 的 “Speed” 值是90比 “Venusaur” 的80更大但 Venusaur 却排名第 1——原因正是 Golbat 与 Venusaur 的 “Type 1” 列取值不同二者处于不同分组各自在组内排名。使用多个列进行更细粒度的分组over接受任意数量的表达式/列名作为分组键。上面的排名也可以改为按 “Type 1” 与 “Type 2” 的组合分组得到更细粒度的组内排名result pokemon.select( pl.col(Name, Type 1, Type 2), pl.col(Speed) .rank(dense, descendingTrue) .over(Type 1, Type 2) .alias(Speed rank), ) print(result)多个分组键之间是“组合并集”的关系等价于 SQL 中PARTITION BY Type 1, Type 2组内计算互不影响。over与group_byexplode的关系与形状差异从一般意义上说.over()能拿到的结果也可以用先聚合、再explode实现只不过行序会不同result ( pokemon.group_by(Type 1) .agg( pl.col(Name), pl.col(Speed).rank(dense, descendingTrue).alias(Speed rank), ) .select(pl.col(Name), pl.col(Type 1), pl.col(Speed rank)) .explode(Name, Speed rank) ) print(result)对比上面两条代码路径可以总结二者的结果形态差异group_by通常产出行数等于分组个数的 DataFrame每组一行聚合值以 List 形式存在列里over通常产出与原始 DataFrame 行数一致的 DataFrame聚合结果映射回每一行。两者的使用取舍要看意图group_by用于“报表型”汇总over用于“保留明细同时附加分组统计量”的特征工程或窗口计算。需要特别说明的是over并不总是保证输出与原始 DataFrame 行数一致——这正是下面要展开的mapping_strategy参数所控制的。将结果映射回 DataFrame 行mapping_strategyover接受一个参数mapping_strategy它决定分组表达式的结果如何被映射回 DataFrame 的行。为了讲清三种策略的差异先构造一个运动员数据框athletes pl.DataFrame( { athlete: list(ABCDEF), country: [PT, NL, NL, PT, PT, NL], rank: [6, 1, 5, 4, 2, 3], } ) print(athletes)数据共 6 名运动员A–F分属两个国家PT葡萄牙3 人与 NL荷兰3 人。group_to_rows默认默认策略是group_to_rows组内表达式的计算结果长度应与该组行数一致结果按原行位置映射回该组的每一行。下面按国籍内部对运动员的排名排序。荷兰运动员原本位于第 2、3、6 行——执行后它们仍留在这些位置变化的是运动员姓名的顺序从 “B”、“C”、“F” 变为 “B”、“F”、“C”result athletes.select( pl.col(athlete, rank).sort_by(pl.col(rank)).over(pl.col(country)), pl.col(country), ) print(result)下图的左列展示了按国家排序前后的原始行位置右列则对应结果——group_to_rows不改变各组的整体行位置只重排组内内容explode如果把mapping_strategy设为explode则同一国家的运动员被集中到一起但最终行的顺序就国家而言与原始顺序不再一致正如下图所示因为 Polars 无需跟踪每个组内行在原表中的位置explode通常比group_to_rows更快。但使用它需要更小心它意味着我们想保留的其它列也必须一并重排否则会出现列间错位。因此这里的示例是对pl.all()整体做 sort over把所有列同步重排result athletes.select( pl.all() .sort_by(pl.col(rank)) .over(pl.col(country), mapping_strategyexplode), ) print(result)joinmapping_strategy的另一个可选值是join它先把每组聚合的结果收集成一个List然后把该 List重复广播到该组的每一行result athletes.with_columns( pl.col(rank).sort().over(pl.col(country), mapping_strategyjoin), ) print(result)注意此时rank列会被替换为一个 List 列每行都持有其所属国家的完整排序结果。Python docstring 对此有明确警告该策略可能非常耗费内存见 py-polars/src/polars/expr/expr.py因为要在每一行复制整组数据。源码层面的三种映射三种策略在 Rust 引擎中有精确对应。WindowMapping枚举定义于 crates/polars-plan/src/dsl/options/mod.rsGroupsToRows默认值把组内值映射回组内对应位置Explode把聚合出的 List 展开并直接做横向拼接hstack而不是 join要求各组有序结果才有意义Join把各组以Listgroup_dtype形式 join 到各行的位置上——源码注释明确警告“这可能非常消耗内存”。物理执行侧窗口表达式的计算结构WindowExpr定义在 crates/polars-expr/src/expressions/window.rs其中记录了分组键group_by、排序键order_by、要施加窗口函数的列apply_columns、物理函数与映射方式mapping。而在该文件内部还根据组内结果类型选择MapStrategycrates/polars-expr/src/expressions/window.rsJoin按 key join对归约聚合而言最贵、Explode直接展开、Map用一次 arg_sort 把结果映射回原位置。这正是文档所讲“explode更快、join最贵、group_to_rows需保持位置”三种结论的引擎实现依据。Rust 侧调用mapping_strategyexplode的对应写法是Expr::over_with_options(Some(partition), None, WindowMapping::Explode)示例见 docs/source/src/rust/user-guide/expressions/window.rs。窗口化聚合表达式标量结果的广播如果应用到某个组上的表达式最终产出的是标量值例如均值、求和、最大值则这个标量会被广播到该组的所有行。这是最常见的特征工程用法为每个类型组计算平均速度并把均值写回组内每一行result pokemon.select( pl.col(Name, Type 1, Speed), pl.col(Speed).mean().over(pl.col(Type 1)).alias(Mean speed in group), ) print(result)此时输出行数与原始一致新增列 “Mean speed in group” 在属于同一 “Type 1” 的行上是同一个均值。更贴近真实建模的组合写法同一张表里同时计算“按类型平均攻击力”、“按类型组合平均防御力”以及“全局平均攻击力”三种粒度的统计量——这正是over相比逐步 join 的优势所在result pokemon.select( Type 1, Type 2, pl.col(Attack).mean().over(Type 1).alias(avg_attack_by_type), pl.col(Defense) .mean() .over([Type 1, Type 2]) .alias(avg_defense_by_type_combination), pl.col(Attack).mean().alias(avg_attack), ) print(result)Rust 侧等价的均值窗口表达式是col(Speed).mean().over([Type 1])见 docs/source/src/rust/user-guide/expressions/window.rs说明over在 Python 与 Rust API 之间的语义是一致的。更多示例分组 Top-N 组合练习最后做几道综合练习把“排序 headoverexplode”串起来。下面一次性计算了 5 个窗口结果按类型排序所有宝可梦输出 “Type 1” 组内有序的头部元素每种 “Type 1” 类型取前3只类型内按速度降序取前3命名fastest/group类型内按攻击力降序取前3命名strongest/group类型内按名字排序取前3命名sorted_by_alphabet。result pokemon.sort(Type 1).select( pl.col(Type 1).head(3).over(Type 1, mapping_strategyexplode), pl.col(Name) .sort_by(pl.col(Speed), descendingTrue) .head(3) .over(Type 1, mapping_strategyexplode) .alias(fastest/group), pl.col(Name) .sort_by(pl.col(Attack), descendingTrue) .head(3) .over(Type 1, mapping_strategyexplode) .alias(strongest/group), pl.col(Name) .sort() .head(3) .over(Type 1, mapping_strategyexplode) .alias(sorted_by_alphabet), ) print(result)关键点在于外层先sort(Type 1)保证同一类型相邻随后每个分组表达式把head(3)限定的行以新行的形式产出配合mapping_strategyexplode输出“每种类型前 3 名”的紧凑结果。这正是“窗口函数负责分组内取值、explode 负责改变行数”的典型配合。sort_by/head/over三种算子叠加时Rust 侧等价实现需要对head结果再显式.explode(...)见 docs/source/src/rust/user-guide/expressions/window.rs这一点体现了 Python API 对窗口展开路径的自动化封装。补充over的完整参数与排序支持Python 侧over完整签名py-polars/src/polars/expr/expr.py为Expr.over( partition_byNone, # 分组键支持列名或表达式可迭代 *more_exprs, # 更多分组键位置参数 order_byNone, # 在每个分区内先按此排序再计算 descendingFalse, # order_by 的排序方向 nulls_lastFalse, # 排序时 null 是否放最后 mapping_strategygroup_to_rows, # group_to_rows | join | explode )其中order_by参数对顺序敏感的窗口运算如cum_sum、diff尤其有用它可以先在每个分组内部排序再施加窗口表达式省去显式sort_by的书写。结合前面所述的行数语义可以总结使用建议想要不改变行数、只附加分组统计量用默认group_to_rows标量结果自动广播想要组内排序并改变行数、让同组相邻用explode获得最佳性能想要每一行都携带整组的聚合列表用join但需留意其内存开销组内结果长度与组大小不一致时group_to_rows不适用应选用explode或join。这些窗口行为的回归测试可在 crates/polars/tests/it/lazy/expressions/window.rs 中找到仓库 Rust 集成测试目录适合作为进一步阅读与验证的实现参考。延伸阅读分组聚合的完整讲解与over对照使用expressions/aggregation.md列表与数组列类型expressions/lists-and-arrays.mdPython 窗口表达式 API 参考py-polars/docs/source/reference/expressions/window.rst表达式机制与上下文说明user-guide/expressions/index.md【免费下载链接】polarsExtremely fast Query Engine for DataFrames, written in Rust项目地址: https://gitcode.com/GitHub_Trending/po/polars创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考