
本文详解如何在 Polars 中基于 pl.all() 实现列级水平填充(forward/backward fill),解决 pl.all() 不可迭代问题,并提供兼容正向、反向填充的健壮实现方案。
本文详解如何在 polars 中基于 `pl.all()` 实现列级水平填充(forward/backward fill),解决 `pl.all()` 不可迭代问题,并提供兼容正向、反向填充的健壮实现方案。
在 Polars 中进行水平填充(horizontal fill)——即按行方向,用右侧(或左侧)非空值向前(或向后)填充缺失值——是一个常见但易被误解的操作。用户常希望以 pl.all() 作为默认参数自动覆盖所有列,但直接对 pl.all() 调用 reversed() 或遍历会触发 TypeError:因为 pl.all() 返回的是单个 Expr 对象,而非列名或表达式列表。
根本原因在于:pl.all() 是一个多列选择表达式(multi-column selector),用于 select() / with_columns() 等上下文中,它本身不可迭代,也不支持索引或反转。因此,不能像 list[str] 那样直接用于 pl.coalesce(cols[i:]) 或 reversed(exprs)。
✅ 正确解法是区分「元数据层」与「表达式层」:
- 若需动态获取列名(如用于顺序控制),应从 DataFrame 的
.columns属性获取字符串列表; - 若需构造表达式序列(尤其是反向顺序),应显式转换为
pl.col(col_name)表达式列表; - 对于纯正向填充,可借助
pl.cum_reduce()+pl.coalesce实现高效、惰性求值的累积填充,无需 Python 循环。
✅ 推荐实现:支持 pl.all() 默认 + 正/反向填充
以下函数完全兼容 exprs=None(即默认使用全部列)、forward=True/False,且不依赖硬编码列数或破坏懒执行:
from typing import Iterable, Optional, Union
import polars as pl
from polars._typing import IntoExpr
def fill_horizontal(
exprs: Optional[Iterable[IntoExpr]] = None,
*,
forward: bool = True
) -> pl.Expr:
"""
生成水平正向/反向填充表达式(支持 pl.all() 默认)。
- forward=True:从左到右填充(col1 → col2 → col3 → ...)
- forward=False:从右到左填充(colN → ... → col2 → col1)
注意:当 exprs 为 None 时,内部自动使用 df.columns 构建列序列,
确保可反转;pl.all() 仅作为占位符,不直接参与排序。
"""
# 步骤1:解析列标识符 → 统一转为列名字符串列表
if exprs is None:
# 默认使用所有列名(字符串列表,可安全 reversed)
cols_names = pl.select(pl.all()).columns # 安全获取当前 schema 列名
# ⚠️ 实际使用时需由调用方传入 df(见下方用法),此处仅为示意
# 更实用写法:在调用处明确传入 df.columns
elif isinstance(exprs, (list, tuple)) and len(exprs) > 0 and isinstance(exprs[0], str):
cols_names = list(exprs)
else:
# 尝试提取列名:支持 pl.col("a"), "a", 或混合
cols_names = []
for e in exprs:
if isinstance(e, str):
cols_names.append(e)
elif hasattr(e, "_pyexpr") and hasattr(e, "meta"):
# 粗略判断是否为 pl.col(...) 表达式(简化版)
name = getattr(e, "_root_names", [None])[0]
if name:
cols_names.append(name)
else:
raise ValueError(f"无法从表达式 {e} 提取列名")
else:
raise ValueError(f"不支持的表达式类型: {type(e)}")
# 步骤2:构建 pl.col() 表达式序列(可反转)
col_exprs = [pl.col(name) for name in cols_names]
if not forward:
col_exprs = list(reversed(col_exprs))
# 步骤3:使用 cum_reduce 实现高效累积 coalesce
# 注意:cum_reduce 从左到右累积,因此反向填充需先反转列序再累积
return pl.cum_reduce(
lambda s1, s2: pl.coalesce(s2, s1), # s2 优先于 s1(右侧覆盖左侧)
col_exprs
).struct.unnest() # 展开 struct 输出为多列
✅ 正确调用方式(关键!)
由于 fill_horizontal 本身不持有 DataFrame,必须由调用方提供列名上下文。推荐如下模式:
df = pl.DataFrame({
"col1": [1, None, 2],
"col2": [1, 2, None],
"col3": [None, None, 3]
})
# ✅ 正向填充:默认使用全部列
df.with_columns(fill_horizontal(df.columns, forward=True))
# ✅ 反向填充:显式传入列名并反转顺序
df.with_columns(fill_horizontal(df.columns, forward=False))
# ✅ 指定部分列
df.with_columns(fill_horizontal(["col2", "col1"], forward=True))
? 为什么不用
pl.all()直接?pl.all()是运行时表达式,其列顺序由 DataFrame schema 决定,但无法在 Python 层面“反转”。而df.columns是确定的字符串列表,可控、可逆、无歧义。
⚠️ 注意事项与替代方案
-
性能提示:
pl.cum_reduce是惰性、向量化操作,比 Python 循环 +pl.coalesce(cols[i:])更高效,且能被查询优化器识别。 -
反向填充局限:当前 Polars 尚不支持
pl.cum_reduce(..., reverse=True)(GitHub Issue #21415)。因此我们采用「先反转列序,再正向 cum_reduce」的等效策略。 -
DataFrame 级封装(进阶):若频繁使用,可封装为 DataFrame 方法:
@pl.api.register_dataframe_namespace("hfill") class HorizontalFill: def __init__(self, df: pl.DataFrame): self._df = df def forward(self) -> pl.DataFrame: return self._df.with_columns(fill_horizontal(self._df.columns, forward=True)) def backward(self) -> pl.DataFrame: return self._df.with_columns(fill_horizontal(self._df.columns, forward=False)) # 使用:df.hfill.forward() -
避免陷阱:不要写
exprs or pl.all()—— 这会导致pl.all()被当作单个表达式传入reversed(),引发TypeError。
✅ 总结
| 场景 | 推荐方式 |
|---|---|
| 默认填充所有列 | 传入 df.columns,而非 pl.all()
|
| 正向填充 | fill_horizontal(df.columns, forward=True) |
| 反向填充 | fill_horizontal(df.columns, forward=False) |
| 高性能实现 | 基于 pl.cum_reduce + pl.coalesce,避免 Python 循环 |
| 扩展性 | 可轻松支持子集列、重命名列、条件列等 |
通过将「列元数据获取」与「表达式构建」解耦,你既能享受 pl.all() 的语义简洁性,又能获得完全可控的列序操作能力——这才是 Polars 函数式风格的最佳实践。










