Polars中为何需要第二个over?哪些函数会打破窗口上下文?
按分组新增列的窗口函数逻辑解析
原始数据
import polars as pl df = pl.DataFrame({ 'year': [ 5, 5, 5, 10, 10, 15, 15, 30, 30, 30 ], 'usage': ['farm', 'best', '', 'manual', 'best', 'best', 'city', 'random', 'best', 'farm' ], 'value': [0.825, 0.83, 0.85, 0.935, 0.96, 1.12, 1.305, 1.34, 1.34, 1.455], 'source': ['wood', 'metal', 'water', 'metal', 'water', 'wood', 'water', 'wood', 'metal', 'water' ]})
原始输出
┌──────┬────────┬───────┬────────┐ │ year ┆ usage ┆ value ┆ source │ │ --- ┆ --- ┆ --- ┆ --- │ │ i64 ┆ str ┆ f64 ┆ str │ ╞══════╪════════╪═══════╪════════╡ │ 5 ┆ farm ┆ 0.825 ┆ wood │ │ 5 ┆ best ┆ 0.83 ┆ metal │ │ 5 ┆ ┆ 0.85 ┆ water │ │ 10 ┆ manual ┆ 0.935 ┆ metal │ │ 10 ┆ best ┆ 0.96 ┆ water │ │ 15 ┆ best ┆ 1.12 ┆ wood │ │ 15 ┆ city ┆ 1.305 ┆ water │ │ 30 ┆ random ┆ 1.34 ┆ wood │ │ 30 ┆ best ┆ 1.34 ┆ metal │ │ 30 ┆ farm ┆ 1.455 ┆ water │ └──────┴────────┴───────┴────────┘
需求
新增一列best,取值为每个year分组中usage等于"best"对应的source字段值。
用户疑问与解决方案
用户给出的可行代码如下,但存在两个疑问:
df.with_columns( pl.col('source').gather(pl.col('usage').eq('best').cast(pl.Int8).arg_max().over('year')).over('year').alias('best') )
- 为什么需要两个
over窗口?去掉第二个over后分组逻辑失效,原以为第一个over会作用于整个表达式。 gather函数会打破窗口上下文,还有哪些函数有这类特性?
问题解析
一、为什么需要两个over?
1. 第一个over的作用
pl.col('usage').eq('best').cast(pl.Int8).arg_max().over('year')这部分是在每个year分组内定位目标行索引:
eq('best')把符合条件的行转成1,其他为0arg_max()取最大值(即1)所在的行索引.over('year')限定计算范围为每个year分组
这一步得到的是每个分组对应的单个索引值,但还只是分组级别的标量,没有对应到原数据的每一行。
2. gather的特性
gather是按索引提取元素的函数,它不会继承之前的窗口上下文——也就是说,当你用pl.col('source').gather(索引值)时,它只会用这个索引去取整个source列的对应值,而非当前分组内的source列。如果没有第二个over,gather只会返回和分组数一致的结果,无法匹配原数据的10行,导致分组逻辑失效。
3. 第二个over的作用
第二个.over('year')的核心是广播:把gather得到的单个分组值,复制到该year分组的每一行,确保原数据的每一行都能拿到对应分组的best来源值。
二、哪些函数会打破窗口上下文?
这类函数的共性是将整个列作为操作对象,而非保留分组后的局部上下文,常见的有:
gather/take:按索引提取元素,默认操作整个列first/last(无窗口时):取整个列的首尾,而非分组内的首尾sum/mean等聚合函数(无窗口时):计算整个列的聚合值,而非分组内聚合sort_by:对整个列排序,不会保留分组内的排序逻辑reverse:反转整个列,而非分组内反转
简单来说,所有不需要依赖窗口就能独立完成计算的函数,在窗口表达式内部使用时,都会打破之前的分组上下文,需要再次通过over明确分组范围。
内容的提问来源于stack exchange,提问作者rhug123
相关产品推荐
相关产品推荐

