计算 Polars DataFrame 中滚动窗口内的百分位排名

25次阅读

计算 Polars DataFrame 中滚动窗口内的百分位排名

本文详解如何在 polars 中对时间序列或索引序列数据执行滚动窗口(如 4 期)的百分位排名计算,解决 `group_by_dynamic` 排序异常与除零错误,并推荐更稳定、语义清晰的 `rolling()` 方案。

金融或时序分析中,常需基于滑动窗口(例如最近 252 个交易日 × 3 = 756 天,或简化为 4 期)计算某资产价格相对于窗口内其他值的相对位置——即滚动百分位排名(percentile rank)。Polars 提供了高性能的窗口操作能力,但正确使用需注意排序状态、窗口边界语义及空值/长度不足场景的处理。

✅ 正确做法:用 rolling() 替代 group_by_dynamic

group_by_dynamic 要求索引列全局严格单调递增且无重复,且 every=”1i”(逐单位滚动)在日期列上易因精度或缺失导致“未显式排序”报错;而 rolling() 专为滑动窗口设计,语义更直观、容错更强,且天然支持 closed=”left” 等边界控制。

以下以整数索引为例(实际中可替换为 date 列,但需确保其为 pl.Date 或 pl.Datetime 并已排序):

import polars as pl  prices = pl.DataFrame({     "int_index": range(6),     "asset_1": [1.1, 3.4, 2.6, 4.8, 7.4, 3.2],     "asset_2": [4, 7, 8, 3, 4, 5],     "asset_3": [1, 3, 10, 20, 2, 4], })  # 定义待计算列(排除索引列) rank_cols = pl.all().exclude("int_index")  # 执行滚动窗口百分位计算:窗口大小为 4,左闭右开(包含当前行及后续3行) percentiles = (     prices.sort("int_index")  # 确保索引有序(必要!)     .rolling(         index_column="int_index",         period="4i",      # 窗口跨度:4 个单位         offset="0i",      # 不偏移,从当前索引开始         closed="left"     # 包含左端点(当前行),不包含右端点(第 i+4 行)     )     .agg(         (rank_cols.rank(method="min").first() * 100.0 / rank_cols.count())         .name.suffix("_percentile")     ) )  print(percentiles)

输出结果:

shape: (6, 4) ┌───────────┬────────────────────┬────────────────────┬────────────────────┐ │ int_index ┆ asset_1_percentile ┆ asset_2_percentile ┆ asset_3_percentile │ │ ---       ┆ ---                ┆ ---                ┆ ---                │ │ i64       ┆ f64                ┆ f64                ┆ f64                │ ╞═══════════╪════════════════════╪════════════════════╪════════════════════╡ │ 0         ┆ 25.0               ┆ 50.0               ┆ 25.0               │ │ 1         ┆ 50.0               ┆ 75.0               ┆ 50.0               │ │ 2         ┆ 25.0               ┆ 100.0              ┆ 75.0               │ │ 3         ┆ 66.666667          ┆ 33.333333          ┆ 100.0              │ │ 4         ┆ 100.0              ┆ 50.0               ┆ 50.0               │ │ 5         ┆ 100.0              ┆ 100.0              ┆ 100.0              │ └───────────┴────────────────────┴────────────────────┴────────────────────┘

? 验证逻辑: 第 0 行窗口为 [1.1, 3.4, 2.6, 4.8] → 1.1 排名第 1(最小),百分位 = (1 / 4) × 100 = 25% 第 1 行窗口为 [3.4, 2.6, 4.8, 7.4] → 3.4 排名第 2 → (2 / 4) × 100 = 50%

⚠️ 关键注意事项

  • 必须先 sort() 再 rolling():即使数据看似有序,Polars 也不自动推断排序状态,sort() 是强制保障。
  • 窗口长度不足时的行为:当剩余行数
  • method=”min” 更合理:处理重复值时,min 方法赋予相同值最小排名(如 [2,2,3] 中两个 2 均得排名 1),符合多数百分位定义;默认 average 可能产生非整数排名。
  • 日期列适配:若 index_column=”date” 为 pl.Date,period=”4d” 即 4 天窗口;若为 pl.Datetime,可用 “4h”、”7d” 等。确保日期无重复且已排序。
  • 性能提示:rolling() 在 Polars 中高度优化,远快于手动循环pandas rolling,尤其适用于大数据集。

? 总结

  • ❌ 避免对非时间连续索引滥用 group_by_dynamic + every=”1i”,易触发排序异常和除零错误;
  • ✅ 优先选用 rolling() + closed=”left” 实现“当前行及其后 N-1 行”的前向滚动窗口;
  • ✅ 显式调用 sort()、指定 rank(method=”min”)、用 .first() 提取当前行排名,是健壮计算的核心;
  • ✅ 结果列命名统一用 .name.suffix(),保持代码简洁可维护。

通过以上方法,你可高效、准确地在 Polars 中实现任意数值列的滚动百分位排名,无缝对接量化策略回测、异常检测等生产场景。

text=ZqhQzanResources