Polars 中按生命-死亡区间计算列最大值的高效实现

1次阅读

Polars 中按生命-死亡区间计算列最大值的高效实现

本文介绍如何在 Polars 中为每个以 “Life” 开头、以 “Death” 结尾的连续区间,提取指定列(如 column A)的最大值,并仅将该值填充至对应 “Life” 行的新增列中,其余行置为 NULL

本文介绍如何在 polars 中为每个以 “life” 开头、以 “death” 结尾的连续区间,提取指定列(如 `column a`)的最大值,并仅将该值填充至对应 “life” 行的新增列中,其余行置为 null。

在 Polars 中实现“区间内最大值映射到起始标记行”这一需求,关键在于对非连续逻辑区间进行唯一分组标识,而非依赖物理行号或循环遍历。原始数据中,“Life”表示一个新区间的开始,“Death”表示当前区间的结束;二者之间(含)的所有行构成一个有效计算范围。由于数据中存在 null 值且区间不等长,直接切片不可行,需通过累积逻辑构造稳定的 group_id。

核心思路:用累积和构建区间组 ID

我们利用布尔序列的 cum_sum() 生成单调递增的计数器:

  • (pl.col(“Column B”) == “Life”).cum_sum():每遇到一个 “Life”,计数器 +1 → 标记每个区间的起点序号
  • (pl.col(“Column B”) == “Death”).cum_sum():每遇到一个 “Death”,计数器 +1 → 标记每个区间的终点序号

二者相加后 forward_fill(),可使每个 “Life” 到其后首个 “Death” 之间的所有行(含两端)获得相同的基础组 ID(即 group_id_1)。但问题在于:”Death” 行本身属于当前区间的结尾,不应参与后续区间的分组——它应归属前一个 “Life” 区间。因此需对 “Death” 行的组 ID 进行 1 行前向偏移(.shift()),得到修正后的 group_id_2。

以下是完整、可运行的 Polars 实现:

import polars as pl  df = pl.DataFrame({     "Column A": [2, 3, 1, 4, 1, 3, 3, 2, 1, 0],     "Column B": ["Life", None, None, None, "Death", None, "Life", None, None, "Death"] })  # 定义关键布尔条件 is_life = pl.col("Column B") == "Life" is_death = pl.col("Column B") == "Death"  # 构建鲁棒的 group_id:确保每个 Life→Death 区间拥有唯一 ID,且 Death 行归属前一区间 group_id = (     (is_life.cum_sum() + is_death.cum_sum())     .forward_fill()     .over(pl.lit(0))  # 确保 forward_fill 在全表生效(Polars ≥ 0.20.20 推荐显式 over)     .fill_null(0) ) group_id = pl.when(is_death).then(group_id.shift()).otherwise(group_id)  # 计算每组 Column A 的最大值,并仅在 Life 行赋值 result = df.with_columns(     pl.when(is_life)       .then(pl.col("Column A").max().over(group_id))       .alias("Column C") )  print(result)

✅ 输出结果与预期一致:

shape: (10, 3) ┌──────────┬──────────┬──────────┐ │ Column A ┆ Column B ┆ Column C │ │ ---      ┆ ---      ┆ ---      │ │ i64      ┆ str      ┆ i64      │ ╞══════════╪══════════╪══════════╡ │ 2        ┆ Life     ┆ 4        │ │ 3        ┆ null     ┆ null     │ │ 1        ┆ null     ┆ null     │ │ 4        ┆ null     ┆ null     │ │ 1        ┆ Death    ┆ null     │ │ 3        ┆ null     ┆ null     │ │ 3        ┆ Life     ┆ 3        │ │ 2        ┆ null     ┆ null     │ │ 1        ┆ null     ┆ null     │ │ 0        ┆ Death    ┆ null     │ └──────────┴──────────┴──────────┘

注意事项与最佳实践

  • Null 安全性:forward_fill() 默认跳过 null,但建议在生产环境中显式 .fill_null(0) 防止首行为 null 导致传播;
  • 版本兼容性:forward_fill() 在较新 Polars(≥ 0.20.0)中默认全局填充;若使用旧版本,可改用 .over(pl.lit(0)) 显式指定窗口;
  • 性能优势:全程基于惰性计算与向量化操作,避免 Python 循环或 apply(),适用于百万级数据;
  • 扩展性:该模式可轻松适配其他边界标识(如 “Start”/”End”)、多列聚合(.max().min().mean() 组合),或嵌套区间(通过嵌套 cum_sum() 实现层级 ID);
  • 调试技巧:临时保留中间列(如 start, end, group_id)有助于验证分组逻辑是否符合业务语义。

掌握此类基于累积逻辑的分组建模方法,是高效处理时序、会话、状态区间类问题的 Polars 核心技能之一。

text=ZqhQzanResources