
使用 pl.struct("a", "b").rank(method="dense") 可为多列组合生成全局唯一、无间隙的整数 ID,完美解决按值变化递增计数问题。
使用 `pl.struct("a", "b").rank(method="dense")` 可为多列组合生成全局唯一、无间隙的整数 id,完美解决按值变化递增计数问题。
在 Polars 中,当需要为 (a, b) 这样的列组合生成一个全局唯一、连续且无跳跃的序号列(即:相同 (a,b) 值始终映射到同一 ID,不同组合按首次出现顺序分配 1, 2, 3…),最简洁、高效且语义明确的方式是使用 稠密排名(rank(method="dense")),而非手动构造条件累加逻辑。
✅ 正确解法:结构体 + 稠密排名
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(
pl.struct("a", "b").rank(method="dense").cast(pl.Int64).alias("d")
)
print(result)输出:
shape: (8, 3) ┌─────┬─────┬─────┐ │ a ┆ b ┆ d │ │ --- ┆ --- ┆ --- │ │ 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 │ └─────┴─────┴─────┘
? 原理说明:
pl.struct("a", "b") 将每行的 a 和 b 组合成一个结构体(类似元组),rank(method="dense") 对所有结构体值进行全局稠密排序——相同结构体获得相同排名,排名从 1 开始,且不跳号(如 [A,A,B,B,C] → [1,1,2,2,3])。这正是目标列 d 的行为:(1,1)→1, (1,2)→2, (1,3)→3, (2,1)→4, (2,2)→5。
❌ 为什么 cum_sum() + over() 不适用?
你尝试的 pl.max('b').over('a') 得到的是每组最大 b 值(如 a=1 → c=3, a=2 → c=2),但后续 cum_sum().over("c") 是按 c 列值分组累加,而 c 并非唯一标识符(多个组可能有相同 max(b)),导致错误分组与重复计数。更关键的是:cum_sum() 本质是按行序前缀求和,无法直接建模“首次出现新组合即分配新 ID”这一离散映射逻辑——它处理的是数值累积,而非去重排序。
⚙️ 进阶提示与注意事项
- 类型控制:rank() 默认返回 u32;如需有符号整型或兼容下游系统(如数据库写入),建议显式 .cast(pl.Int64)。
- 稳定性保障:rank(method="dense") 在 Polars 中是稳定排序(stable sort),即相等元素保持原始相对顺序,确保结果可复现。
- 扩展性:可轻松扩展至更多列,例如 pl.struct("a", "b", "c", "category").rank("dense")。
- 性能优势:全程向量化、零 Python 循环、无需 .group_by().agg() + .join() 等低效链路,时间复杂度为 O(n log n),远优于手动遍历模拟。
✅ 替代方案对比(不推荐,仅作理解)
| 方法 | 是否可行 | 缺点 |
|---|---|---|
| pl.col("a").cast(pl.Utf8") + "_" + pl.col("b").cast(pl.Utf8).rank("dense") | 可行但冗余 | 字符串拼接开销大,易受特殊字符干扰 |
| 先 .unique(["a","b"]).with_row_index("d") 再 join | 可行 | 需额外内存存储映射表,代码冗长,易出错 |
| 自定义 map_elements + 全局字典 | ❌ 强烈不推荐 | 破坏向量化,GIL 锁死,性能暴跌,无法并行 |
综上,pl.struct(...).rank("dense") 是 Polars 生态中解决「跨组唯一键生成」问题的标准范式——语义清晰、性能卓越、代码极简。请优先采用此方案。

















