本文介绍如何在 Polars 中为多列组合(如 (a, b))生成全局唯一、紧凑且不跳号的整数序号(即 dense ranking),适用于构建跨组唯一 ID、去重排序索引等场景。
本文介绍如何在 polars 中为多列组合(如 `(a, b)`)生成全局唯一、紧凑且不跳号的整数序号(即 dense ranking),适用于构建跨组唯一 id、去重排序索引等场景。
在数据处理中,常需将多列联合值映射为一个全局唯一、连续、无间隙的整数 ID —— 例如,将 (group_id, item_id) 对转换为全表唯一的 global_item_rank,且相同组合必须对应同一编号(如 (1,1) 始终为 1,(2,1) 首次出现时应为 4,而非 5)。这本质上不是累积求和(cum_sum),而是稠密排名(dense ranking):按字典序对 (a, b) 全局排序后,为每个首次出现的唯一组合分配递增编号,重复组合复用已分配编号。
直接使用 cum_sum() 无法解决该问题,原因在于:
- cum_sum() 是行序敏感的前缀累加,依赖物理顺序,无法自动识别逻辑唯一性;
- 尝试先 max().over('a') 再 cum_sum().over('c') 会错误地按中间聚合值分组累加,违背“组合唯一性”语义。
✅ 正确解法:使用 pl.struct("a", "b").rank(method="dense")
rank(method="dense") 是 Polars 专为此类需求设计的高性能表达式:
- pl.struct("a", "b") 将两列打包为结构化标量,支持自然字典序比较;
- .rank("dense") 按该结构体全局排序,为每个首次出现的唯一值分配最小正整数(1, 2, 3…),重复值不跳号(即 1,2,2,3 而非 1,2,2,4);
- 整个操作向量化、零中间列、无需 group_by 或 sort 显式调用,由 Polars 自动优化。
import polars as pl
df = pl.DataFrame({
'a': [1, 1, 1, 1, 2, 2, 2, 2],
'b': [1, 2, 3, 1, 1, 2, 2, 2]
})
result = df.with_columns(
global_id = pl.struct("a", "b").rank("dense").cast(pl.Int64)
)
print(result)
输出:
shape: (8, 3) ┌─────┬─────┬───────────┐ │ a ┆ b ┆ global_id │ │ --- ┆ --- ┆ --- │ │ i64 ┆ i64 ┆ i64 │ ╞═════╪═════╪═══════════╡ │ 1 ┆ 1 ┆ 1 │ │ 1 ┆ 2 ┆ 2 │ │ 1 ┆ 3 ┆ 3 │ │ 1 ┆ 1 ┆ 1 │ │ 2 ┆ 1 ┆ 4 │ │ 2 ┆ 2 ┆ 5 │ │ 2 ┆ 2 ┆ 5 │ │ 2 ┆ 2 ┆ 5 │ └─────┴─────┴───────────┘
⚠️ 注意事项:
- method="dense" 是关键:若用 "min"(默认)或 "ordinal",结果相同但语义不同;"dense" 确保编号绝对紧凑(无跳号),符合示例中 d 列要求;
- 类型转换建议:rank() 默认返回 UInt32,如需 Int64 可链式调用 .cast(pl.Int64);
- 扩展性:可轻松扩展至更多列,如 pl.struct("a", "b", "c", "d").rank("dense");
- 性能优势:相比手动 unique().with_row_count() + join,此方法单次扫描完成,内存友好,适合千万级数据。
总结:当目标是“为唯一组合分配全局连续序号”时,请放弃 cum_sum 思维,拥抱 rank(method="dense") —— 它是 Polars 中语义精准、性能卓越的标准解法。











