You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用Polars高效聚合时间序列为值对应的区间结构体列表?

高效提取Polars时间序列中常量值的连续区间列表

我有一个包含ID值的时间序列,序列由值保持恒定的连续块组成。需要高效提取每个值对应的、序列保持恒定的时间区间列表。数据以polars.DataFrame形式存储,包含date和value列,示例代码如下:

import polars as pl
from datetime import datetime

dates = pl.date_range(
    start=datetime.date.fromisoformat("2011-01-01"), 
    end=datetime.date.fromisoformat("2011-01-10"),
    interval="1d",
    eager=True
)

frame = pl.DataFrame({
    "date": dates,
    "value": ["A", "B", "A", "B", "B", "A", "A", "A", "B", "B"]
})

期望结果是一个DataFrame,每个value对应一组由起始/结束日期组成的结构体列表:

>>> shape: (2, 2)
┌───────┬───────────────────────────────────┐
│ value ┆ intervals                         │
│ ---   ┆ ---                               │
│ str   ┆ list[struct[2]]                   │
╞═══════╪═══════════════════════════════════╡
│ A     ┆ [{2011-01-01,2011-01-01}, {2011-… │
│ B     ┆ [{2011-01-02,2011-01-02}, {2011-… │
└───────┴───────────────────────────────────┘

我当前的解法分四步:

  1. 新增布尔列标记块的起始和结束
def is_start(expr: pl.Expr) -> pl.Expr:    
    return (expr.shift(1) != expr).fill_null(True).alias("is_start")

def is_end(expr: pl.Expr) -> pl.Expr:
    return (expr.shift(-1) != expr).fill_null(True).alias("is_end")

x = frame.with_columns(pl.col("value").pipe(is_start), pl.col("value").pipe(is_end))
  1. 按value分组,聚合起止日期为列表
y = x.group_by("value").agg(
    pl.col("date").filter(pl.col("is_start")).alias("start_date"),
    pl.col("date").filter(pl.col("is_end")).alias("end_date")
)
  1. 将列表展开为列
z = y.explode("start_date", "end_date")
  1. 再次按value分组,聚合为区间结构体列表
u = z.group_by("value").agg(
    pl.struct("start_date", "end_date").alias("intervals")
)

想知道是否有更简洁且高效的方式实现该转换,尤其是当前需要两次group_by,而中间结果已包含起止日期但未直接组合为区间结构体的情况。最终要将该转换应用于多个长时序,且已知每个序列的变化点较少,恳请提供优化建议。


优化方案:基于连续块分组的单次聚合

核心思路是先给连续相同的value块分配唯一标识,然后直接按value和块标识分组,提取每个块的起止日期,最后再按value聚合为区间列表。这样只需要一次完整的分组聚合流程,避免了两次group_by和中间的explode操作,效率更高,代码更简洁。

完整代码

import polars as pl
from datetime import datetime

# 生成示例数据
dates = pl.date_range(
    start=datetime.date.fromisoformat("2011-01-01"), 
    end=datetime.date.fromisoformat("2011-01-10"),
    interval="1d",
    eager=True
)

frame = pl.DataFrame({
    "date": dates,
    "value": ["A", "B", "A", "B", "B", "A", "A", "A", "B", "B"]
})

# 执行转换
result = (
    frame
    # 为连续相同的value块生成唯一分组键
    .with_columns(
        group_id=(pl.col("value") != pl.col("value").shift(1)).fill_null(True).cumsum()
    )
    # 按value和group_id分组,提取每个块的起止日期并组合为结构体
    .group_by(["value", "group_id"], maintain_order=True)
    .agg(
        pl.struct(
            start_date=pl.col("date").min(),
            end_date=pl.col("date").max()
        ).alias("interval")
    )
    # 按value聚合,将同value的区间结构体合并为列表
    .group_by("value", maintain_order=True)
    .agg(pl.col("interval").alias("intervals"))
)

print(result)

代码解释

  1. 生成连续块标识:通过(value != value.shift(1)).fill_null(True).cumsum()生成group_id,每次value变化时,布尔值为True,累加后得到新的分组ID,以此区分不同的连续块。
  2. 提取单个块的区间:按value和group_id分组,用date.min()和date.max()直接获取每个连续块的起止日期,并组合为结构体。这里maintain_order=True保证区间顺序和原时序一致。
  3. 聚合同value的区间列表:最后按value分组,将单个块的区间结构体聚合为列表,得到目标结果。

效率优势

  • 避免了原方案中两次group_by和explode带来的额外计算开销,尤其在长时序、变化点少的场景下,分组操作的次数更少,性能提升明显。
  • 直接基于连续块分组,逻辑更直观,减少了中间临时列的生成和处理。

内容的提问来源于stack exchange,提问作者Benjamin Trendelkamp-Schroer

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.08 02:17:10