
本文介绍如何在 Polars 中按账户(acc)分组,将一笔待分配值(可正可负)依次填充或反向清空多行账单记录,确保每行结果值介于 0 与该行容量(val)之间,并严格遵循“从前往后填、从后往前撤”的顺序逻辑。
本文介绍如何在 polars 中按账户(`acc`)分组,将一笔待分配值(可正可负)依次填充或反向清空多行账单记录,确保每行结果值介于 0 与该行容量(`val`)之间,并严格遵循“从前往后填、从后往前撤”的顺序逻辑。
在财务结算、库存调配或资源配额管理等场景中,常需将一个汇总值(如退款、补款、调拨额)按优先级规则分摊到多条明细记录上:若值为正,则从首个未满行开始正向填充;若为负,则从最后一个已填充行开始反向清空(即“unfill”)。关键约束包括:
- 每行最多承载其
val字段容量; - 分配必须按行序严格进行(不可跳过中间行);
- 结果值
res必须满足0 ≤ res ≤ val; - 同一
acc组内独立处理,互不影响。
Polars 提供高效的窗口计算能力,可优雅实现该逻辑,无需循环或自定义聚合函数。核心思路是:
计算组内总目标量:
total = sum(原sum列) + 待分配val
(即:当前所有账单已占额度 + 新增/扣减额度)追踪累计已用额度:
used = val.cum_sum().shift(1).fill_null(0)
(即:前 N−1 行已分配的总量,首行used=0)逐行计算剩余可分配量:
res = clip(total − used, 0, val)
(即:本行最多还能分多少,受总量余量和自身容量双重限制)
以下为完整可运行代码(基于 Polars v1.0+):
import polars as pl
# 输入数据:待分配值(每账户一行)
left = pl.DataFrame({
"acc": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
"val": [-100, -123, -75, -300, -77, -500, 111, 123, 300, 75]
})
# 输入数据:账单明细(每行含容量val与当前已占sum)
right = pl.DataFrame({
"acc": [1, 1, 1, 2, 2, 2, 3, 3, 4, 4, 4, 5, 6, 6, 6, 6, 7, 7, 7, 8, 8, 8, 9, 9, 9, 10, 10],
"val": [100, 100, 100, 123, 150, 123, 70, 70, 150, 100, 150, 77, 500, 500, 500, 500, 100, 100, 100, 123, 123, 123, 100, 150, 200, 10, 10],
"sum": [100, 50, 0, 123, 0, 0, 70, 5, 150, 100, 150, 78, 500, 500, 500, 500, 10, 0, 0, 123, 0, 0, 0, 0, 0, 0, 0]
})
# 执行分配逻辑
result = (
right
.join(left, on="acc", suffix="_new")
.with_columns(
# 【可选】辅助列:组内总目标量 = 原sum之和 + 新val
(pl.col("sum").sum() + pl.col("val_new")).over("acc").alias("total"),
# 【核心】前N-1行已分配总量(cum_sum后shift)
pl.col("val").cum_sum().shift(1).fill_null(0).over("acc").alias("used"),
)
.with_columns(
# 【核心】本行实际分配值 = clip(剩余总量, 0, 自身容量)
(pl.col("total") - pl.col("used"))
.clip(0, pl.col("val"))
.over("acc")
.alias("res")
)
# 【可选】清理辅助列
.select("acc", "val", "sum", "res")
)
print(result)
✅ 关键设计说明:
-
cum_sum().shift(1)确保“先填前排”,因为第 i 行的used只含前 i−1 行的val和,自然形成顺序依赖; -
clip(0, pl.col("val"))强制结果合法化,避免负值或超容(如total 时自动截断为 0); -
.over("acc")保证所有计算严格按账户分组,无跨组干扰; - 正负值统一处理:当
val_new 时,<code>total变小,后续total − used更快触达 0,从而自然实现“从后往前清空”。
⚠️ 注意事项:
- 输入
right中sum列仅作参考,不参与计算(本方案完全重算,忽略原始sum); - 若需保留原始
sum用于审计对比,可在select()中显式包含; - 当
total为负且绝对值较大时,所有行res均为 0 —— 这符合“全部清空”的业务语义; - 性能优异:全向量化操作,即使百万级记录亦毫秒级完成。
该方案简洁、健壮、可扩展,是 Polars 处理此类状态依赖型分配问题的典型范式。










