
本文详解如何在 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 中实现任意数值列的滚动百分位排名,无缝对接量化策略回测、异常检测等生产场景。










