Polars 流式处理中实现字符串列的自动类型推断与转换

在 polars 中对超大 parquet 文件进行流式读取时,若所有列均为 string 类型,需通过两遍扫描完成类型推断(如转为 int64/float64)并保存为带正确 schema 的新文件。本文提供可复用的两阶段方案及健壮的类型探测函数。

Polars 的流式(streaming=True)执行模型要求 输出 schema 必须在开始计算前完全确定——这意味着无法在单次流式扫描中边读取边动态决定列类型。尤其当原始 Parquet 文件所有列均为 pl.String 时,直接尝试 .cast(pl.Int64, strict=False) 会导致无效字符串被转为 null,而非跳过或降级处理,从而破坏数据完整性。

因此,必须采用两阶段策略

  1. 探查阶段(Schema Discovery):对每列 string 字段分别尝试强制转换为目标数值类型(先 Int64,再 Float64),捕获 ComputeError 判断是否全域兼容;
  2. 转换阶段(Streaming Sink):基于探查结果构建确定性表达式,执行一次流式 select + sink_parquet,确保高效、内存友好的写入。

以下为完整可运行示例:

import polars as pl
from polars.exceptions import ComputeError

def try_dtype(lf: pl.LazyFrame, dtype: pl.DataType, cols_to_skip: list[str] | None = None) -> list[str]:
    """
    探查 LazyFrame 中哪些 string 列可安全 cast 到指定 dtype。
    使用 streaming.collect() 避免全量加载,仅验证可行性。
    """
    schema = lf.schema
    cols_to_check = [
        col for col, typ in schema.items()
        if typ == pl.String and (cols_to_skip is None or col not in cols_to_skip)
    ]

    if not cols_to_check:
        return []

    good_cols = []
    for col in cols_to_check:
        try:
            # 仅验证:流式执行 cast,不收集结果
            lf.select(pl.col(col).cast(dtype)).collect(streaming=True)
            good_cols.append(col)
        except ComputeError:
            continue
    return good_cols

# --- 主流程 ---
lf = pl.scan_parquet("large_file.parquet")

# 阶段1:识别可转为整数的列(优先 int,避免 float 误吞整数)
int_cols = try_dtype(lf, pl.Int64)

# 阶段2:在剩余 string 列中识别可转为浮点的列
float_cols = try_dtype(lf, pl.Float64, cols_to_skip=int_cols)

# 构建最终转换表达式列表
exprs = []
for col in lf.columns:
    if col in int_cols:
        exprs.append(pl.col(col).cast(pl.Int64))
    elif col in float_cols:
        exprs.append(pl.col(col).cast(pl.Float64))
    else:
        exprs.append(pl.col(col))  # 保持原 string 类型

# 阶段3:流式执行并写入新 Parquet(schema 已确定)
lf.select(*exprs).sink_parquet("final_cast.parquet")

关键优势说明

  • try_dtype 内部使用 collect(streaming=True),仅触发轻量计算图执行,不将全部数据载入内存;
  • 严格按 Int64 → Float64 顺序探测,避免将 "123" 错判为 float 而丢失精度;
  • sink_parquet 保证最终文件具备精确的物理 schema,下游系统(如 DuckDB、Spark)可直接按类型读取,无需额外解析。

⚠️ 注意事项

  • 若某列含混合格式(如 "123" 和 "123.45"),它将既不匹配 Int64 也不匹配 Float64,最终保留为 string —— 这是预期行为,保障数据安全;
  • 对极宽表(数百 string 列),探查阶段会有一定 I/O

    开销(但仍是 O(列数) 次轻量扫描,非 O(行数×列数));
  • 如需支持日期/布尔等类型,可扩展 try_dtype 并增加对应探测逻辑。

该方案在 Polars 0.20+ 版本中稳定可用,兼顾工程鲁棒性与性能可预测性,是处理“schema-less 字符串大数据”的标准实践路径。