
本文详解如何在 Polars 中优雅、高效地对多个数值列(如 value1/value2/value3)按时间窗口(如 "30d"、"90d")和分组键(如 strike/maturity)同步计算 Z-Score,避免冗余聚合,充分利用 .over() 窗口函数与正则列选择。
本文详解如何在 polars 中优雅、高效地对多个数值列(如 value1/value2/value3)按时间窗口(如 "30d"、"90d")和分组键(如 strike/maturity)同步计算 z-score,避免冗余聚合,充分利用 `.over()` 窗口函数与正则列选择。
在 Polars 中实现类似 Pandas groupby(...).transform(...) 的 Z-Score 计算,核心在于 避免显式 rolling().agg() + 后续广播除法 这类两阶段操作——它不仅代码冗长、易出错,还会因多次扫描数据而降低性能。推荐方案是:*直接使用 `pl.Expr.rolling_与.over()` 组合,在单次表达式中完成滚动均值/标准差计算与分组广播,再统一求 Z-Score**。
✅ 推荐写法:正则列 + 函数化 + 多窗口支持
以下示例以 value1/value2/value3 三列为基准,按 date 时间列滚动(窗口 "30d"),并在 ["strike", "maturity"] 上分组计算 Z-Score:
import polars as pl
def rolling_zscore(
expr: pl.Expr,
window_size: str,
by: str = "date",
min_periods: int = 1,
**kwargs
) -> pl.Expr:
"""对任意表达式计算滚动 Z-Score,支持分组广播"""
mean_expr = expr.rolling_mean(window_size=window_size, by=by, min_periods=min_periods, **kwargs)
std_expr = expr.rolling_std(window_size=window_size, by=by, min_periods=min_periods, **kwargs)
return (expr - mean_expr) / std_expr
# 主流程(推荐使用 lazy 模式提升性能)
result = (
df.lazy()
.sort("date", "strike", "maturity")
.set_sorted("date") # 关键:声明 date 已排序,加速 rolling 操作
.with_columns(
# 对所有匹配 "^value.*$" 的列批量计算 30 天 Z-Score,并按分组广播
rolling_zscore(pl.col(r"^value.*$"), window_size="30d", min_periods=1)
.over(["strike", "maturity"])
.name.suffix("_30d_zscore")
)
.select(
"date", "maturity", "strike",
pl.col(r"^.*_30d_zscore$") # 仅保留 Z-Score 列
)
.collect()
)? 扩展:同时计算多个时间窗口(如 30d & 90d)
只需将 window_size 参数化,并用生成器表达式循环调用即可,所有计算在 Polars 引擎内并行执行:
windows = ["30d", "90d"]
zscore_cols = [
rolling_zscore(pl.col(r"^value.*$"), window_size=w, min_periods=1)
.over(["strike", "maturity"])
.name.suffix(f"_{w}_zscore")
for w in windows
]
result = (
df.lazy()
.sort("date", "strike", "maturity").set_sorted("date")
.with_columns(zscore_cols)
.select(
"date", "maturity", "strike",
pl.col(r"^.*_zscore$")
)
.collect()
)? 输出列名示例:value1_30d_zscore, value2_90d_zscore, value3_30d_zscore —— 清晰可追溯,无需手动拼接字符串。
⚠️ 注意事项与最佳实践
- 务必调用 .set_sorted("date"):rolling_* 函数要求 by 列已排序,否则结果不可靠;显式声明可跳过内部排序,大幅提升性能。
- 慎用 min_periods=1:默认 min_periods=None 会跳过不足窗口长度的行(返回 null),设为 1 可确保首行有值(等价于 partial=True),但需确认业务逻辑是否允许。
- 避免 rolling().agg() + join 或 with_columns 多次引用:这会导致多次窗口扫描,性能呈线性下降;.over() 方案一次扫描完成全部计算。
- *正则列选择 `pl.col(r"^value.$")` 比硬编码列表更健壮**:新增列时自动纳入,且语义清晰。
- 始终优先使用 .lazy():尤其在多窗口场景下,Polars 查询优化器能自动融合、并行化所有操作,.collect() 仅在最后触发实际计算。
通过以上模式,你不仅能写出更简洁、可维护的代码,还能获得接近原生 Rust 性能的 Z-Score 计算——这才是 Polars “表达式优先”哲学的正确打开方式。

















