
本文介绍如何在 Polars 中对两个嵌套列表列(a 为整数列表,b 为字典列表)执行类似 SQL LEFT JOIN 的操作:先展开两列,再按 a[i] == b[j].id 关联,并为每个 a 元素保留对应的 b.x 值(未匹配则为 null)。
本文介绍如何在 polars 中对两个嵌套列表列(`a` 为整数列表,`b` 为字典列表)执行类似 sql left join 的操作:先展开两列,再按 `a[i] == b[j].id` 关联,并为每个 `a` 元素保留对应的 `b.x` 值(未匹配则为 `null``。
在 Polars 中处理嵌套结构时,常见的误区是直接对多列同时 .explode() 后用 .filter() 进行内连接——这会丢失未匹配的左侧元素,无法满足左连接语义。要真正实现「每个 a 元素占一行,优先填充其在 b 中匹配的 x 值,无匹配则补 null」,关键在于分离爆炸、结构化解构、分组聚合决策,而非过滤。
以下是推荐的高效实现方案:
import polars as pl
df = pl.DataFrame({
"a": [[1, 2], [3]],
"b": [
[{"id": 1, "x": 1}, {"id": 3, "x": 3}],
[{"id": 3, "x": 4}]
]
})
result = (
df
.explode("a") # 展开 a → 每个 a 元素独立成行(保留原始行关联)
.explode("b") # 展开 b → 每个字典独立成行(与上一步笛卡尔式组合)
.unnest("b") # 将 struct 列 b 拆为同级字段 id 和 x
.group_by("a", maintain_order=True) # 按 a 分组(保持原始 a 出现顺序)
.agg(
pl.when(pl.col("a") == pl.col("id"))
.then(pl.col("x"))
.sort(nulls_last=False) # null 排最前,非 null 自然靠后
.last() # 取最后一个(即首个非 null;若全 null 则为 null)
)
.rename({"x": "b"}) # 语义对齐:输出列名应为 b
)
print(result)输出结果:
shape: (3, 2) ┌─────┬──────┐ │ a ┆ b │ │ --- ┆ --- │ │ i64 ┆ i64 │ ╞═════╪══════╡ │ 1 ┆ 1 │ │ 2 ┆ null │ │ 3 ┆ 4 │ └─────┴──────┘
✅ 为什么这个方案是左连接?
- .explode("a") 确保所有 a 元素均保留(左表完整性);
- .group_by("a") + .agg(...) 在每组内进行“查找并取值”,不依赖右侧是否存在匹配项;
- pl.when(...).then(...).sort().last() 是核心技巧:它生成一个每组内长度可变的表达式序列(匹配则填 x,否则 null),排序后取最后一个,等价于「取第一个有效值」,天然支持左连接语义。
⚠️ 注意事项:
- maintain_order=True 对于保持 a 的原始顺序(如 [1,2] 先于 3)至关重要,否则分组可能重排;
- 若 b 中存在同一 id 多次出现,.last() 会返回最后一次匹配的 x;如需首次匹配,可改用 .first()(但需将 nulls_last=True 并调整排序逻辑);
- unnest("b") 要求 b 列结构一致(所有字典含相同字段),否则会报错;建议提前用 pl.col("b").struct.field("id").is_null().any() 校验数据质量;
- 性能方面:该方案避免了 join 操作的索引构建开销,在中等规模数据(百万级爆炸后行数)下仍保持高效。
总结来说,Polars 中的“类左连接”不依赖显式 join,而应善用 explode → unnest → group_by → 条件聚合这一范式,既符合函数式链式风格,又能精准控制空值行为与匹配优先级。










