越来越多团队在新工作中采用 Polars,许多团队原先使用 pandas。Check Technologies 在不到两周内,将100多个 Airflow DAG 从 pandas 迁移到 Polars,云账单减少25%;Rabobank 使用 Polars 重建代码库的核心部分,性能提升30倍,使预测引擎具备原先无法实现的能力。生态也在跟进:越来越多库已原生支持 Polars。
这使 Polars 成为 Python DataFrame 工作的现代默认选择。
但这并不自动意味着应该重写所有已有代码。有些流水线已经充分测试,很少变动,也没有可靠性、内存或成本问题;另一些依赖尚不支持 Polars 的自定义库,或者处于严格的变更控制之下,重新运行资格验证测试所耗费的工时,比翻译代码本身还多。本文针对的是确实应该迁移的流水线。接下来要决定的是一次迁移多少,以及由人手工改写,还是交给 LLM。
所有流水线都从这里起步,接下来要决定如何向前推进。
迁移一个片段:以最少工作获得最大收益
多数 ETL 流水线可以拆为多个片段,每段对流入的数据执行一组转换。某些片段比其他片段更敏感于性能,因而适合成为迁移候选。
在边界处转换输入,在 Polars 中完成转换;只有下一个片段仍需要 pandas 时,才转换回 pandas。
用可量化的问题决定迁移哪个片段
选一个存在可量化问题的片段,例如任务耗尽内存,或者需要一台成本越来越高的机器。保存它的输入和输出:接收的 DataFrame,以及交给后续步骤的 DataFrame。这一对数据也会成为核对迁移结果的测试样本。两个检查点之间的全部逻辑,就能独立翻译和优化,而不必修改流水线其余部分。
Polars 惰性求值查询,并以并行方式执行。对于多数工作负载,相较于等价 pandas 代码,实际耗时和峰值内存通常更低,参见官方基准测试。
片段两侧各需要一次转换
边界就是数据跨库的位置。将输入转换为 Polars DataFrame,通过 lazy() 切换到 Lazy API,构建查询;只有后续片段需要 pandas 时,才返回 pandas。
import polars as pl
pandas_df = load_data()
def step_1(pandas_df): ... # pandas
def step_2(pandas_df): # the migrated segment
polars_lf = pl.from_pandas(pandas_df).lazy()
query = polars_lf.filter(...).group_by(...).agg(...)
return query.collect().to_pandas(use_pyarrow_extension_array=True)
def step_3(pandas_df): ... # pandas
pandas_df = step_1(pandas_df)
pandas_df = step_2(pandas_df) # pandas in, Polars work, pandas out
pandas_df = step_3(pandas_df)
转换通常是零拷贝,但并非总是如此
Polars 使用 Apache Arrow 作为内存布局;pandas 可以使用 NumPy 或 Arrow。Arrow 是共享的内存布局,因此当双方都使用它时,边界转换通常不需要复制:两个库读取同一缓冲区。应向 to_pandas() 传入 use_pyarrow_extension_array=True,确保 pandas 使用 Arrow 后端。但这并非总是可行。
根据 schema 和 pandas 配置,to_pandas() 可能产生全部或部分拷贝。发生拷贝时,同一列会同时存在于两侧。因此应对自己的 DataFrame 测量耗时与峰值内存,而不能假定边界没有成本。
- 优点:尝试快、完全可逆,不触及片段以外的代码。
- 缺点:流水线同时运行两套框架。多数数据类型的边界转换成本低,但并非全都免费;优化器无法跨越边界观察两侧的 pandas 代码。
- 适用情形:想为旧代码快速获得低风险的性能收益,但还不准备承诺迁移周围所有代码。
让整条流水线使用一个惰性计划:获得最佳性能
当每个片段都使用 Polars,优化器就能从头到尾观察同一个惰性计划。最佳推进方式,是按照上一节的方法逐段改写,直到不再有 pandas。以下语义注意事项与验证方法,无论改写者是人还是模型,都适用。
翻译错误来自语义,而不仅是语法
例如:Polars 没有索引,NaN 和 null 是不同类型的缺失值;日期时间单位、时区处理与行顺序等细节,即使代码看起来没有问题,也可能发生变化。用户指南的从 pandas 迁移页面提供了更完整的列表。
assert_frame_equal 很适合进行这种比较。调整关键字参数,可以先严格比较,再一次只放宽一个条件:
from polars.testing import assert_frame_equal
assert_frame_equal(
actual,
expected,
check_row_order=False, # only if the segment defines no row order
check_dtypes=False, # only if the schema difference is intentional
rel_tol=0, # rel_tol defaults to 1e-5 and dominates abs_tol at large magnitudes
abs_tol=1e-6, # only for a defined absolute floating-point tolerance
)
已验证的相邻片段之间不需要转换
逐段推进后,最终会有连续片段都在 Polars 中运行。此时可以移除一侧结尾的 .collect().to_pandas() 和另一侧开头的 pl.from_pandas(...).lazy(),改为将片段以 LazyFrame 串接,只在最末保留一次 .collect()。这样,优化器可以更好地选择执行方式,可能进一步提高性能。
相邻片段一旦验证通过,中间转换就可以删除;最后留下的是单个惰性计划和一次 collect。
- 优点:流水线只有一套框架和一套惯用写法,不需要承担边界转换成本,优化器能够观察完整惰性计划。
- 缺点:比迁移单一片段成本高。工作量随片段数量增加,每段都必须验证后才能信任。
- 适用情形:成本、可靠性或维护问题贯穿整个流水线,而非集中在一个片段;或者希望流水线只使用一个 DataFrame 库。手工改写让每项语义决定都经过你,也让团队建立对新流水线的理解;对于关键流程,这值得投入。
LLM 可以执行迁移,但不能判断语义
模型可以执行同样的逐段迁移:改写常规代码,为命令式 pandas 写法提出基于表达式的替代方案,并通过反复针对测试样本运行 assert_frame_equal、修复错误,直到检查通过。完整迁移的成本随流水线与片段数量增长,LLM 改变的就是这部分成本。
代价在于团队没有人亲手编写新代码,对代码的理解必须来自审阅。模型也无法决定空值处理、行顺序或业务规则的变化是否可以接受。这项判断仍属于你;上一节的样本与断言,就是告诉模型所提改写是否正确的方法。
LLM 对照样本改写并修复,直到检查通过。随后由你审阅意图,再切换实现。
结果取决于交接方式,而不只是模型选择
LLM 迁移的效果,相比模型本身,更取决于如何交代任务。以下四点有帮助:
- 让测试样本成为规格。每段保存的输入、输出对,是模型自己可以运行的检查,不必再向你询问正确结果。没有这些样本,就难以确定改写是否正确。
- 每个提示只针对一个片段。输入有限、差异有限,且检查结果明确为通过或失败。一次交出整条流水线,会得到庞大的差异,审阅时没有清晰起点。
- 提供 Polars 专用指导。模型如今常能生成正确代码,但仍有模型会误用 Polars API。上一篇文章介绍了有时会出错的模式及对应指导。
- 允许模型自主运行修复循环。当模型能执行
assert_frame_equal并修复自己的失败时,就能以通过检查的代码收尾。
这样,你不必亲自重写流水线,而是审阅代码意图。应注意意外引入的 Python 循环、不必要的物化,以及从 pandas 原样搬来的 apply 模式。Polars 表达式通常能替代它们;仍适合使用 UDF 的情形,参见用户自定义 Python 函数指南。
DataFrame 领域之外也有先例:一项 Amazon Science 研究在将最多9700行的 Go 项目翻译为 Rust 时,通过等价性检查自动验证了73%的函数。
- 优点:以手工成本的一小部分,为更多流水线进行相同的逐段完整迁移。
- 缺点:模型无法判断语义变化是否可以接受,因此通过检查不能替代审阅。团队通过读代码而非写代码来建立理解;测试样本也仍需你先准备。
- 适用情形:改写属于常规工作,涉及的流水线多于你愿意手工处理的数量,同时每段都有测试样本,团队也具备足够审阅能力。
两个决定:迁移多少,以及由谁改写
先决定触及流水线的多少部分。应根据能明确描述的问题选择:可测量的性能问题通常指向迁移一个片段;贯穿全流水线的成本、可靠性或维护问题,则支持完整迁移。经验上,先做能解决实际问题的最小改动,等问题确实要求时,再扩大范围会更容易。
如果完整迁移,第二个选择是由谁改写。手工改写每条流水线较慢,但每项语义决定都会经过改写者:空值是否重要,行顺序是否只是偶然,某种 pandas 行为是否无意产生且不值得复现。关键流程更需要这种判断,因为顺序改变,或应为 null 的值变为 NaN,可能影响下游;团队也因此能在事后推理新流水线。
交给 LLM 时,正确性主要由测试样本和断言承担,你的注意力转向审阅意图。对于大规模常规工作,这是一项合适的交换,也让原本不值得投入手工成本的流水线进入迁移范围。
| 策略 | 优点 | 缺点 | 适用情形 |
|---|---|---|---|
| 迁移一个性能敏感片段 | 快速、可逆、低风险 | 两套框架并行,优化器无法跨越边界 | 不承诺完整迁移,先获得运行性能收益 |
| 手工完整迁移 | 单一框架、完整计划优化,每项语义决定当场审阅 | 每条流水线成本最高 | 问题贯穿全流程,或关键流程值得建立亲手改写带来的理解 |
| 由 LLM 完整迁移 | 以少量成本迁移更多流水线 | 不能判断语义变化,对新代码的理解来自审阅 | 大规模常规改写,各片段有测试样本,并具备审阅能力 |
下一篇文章《Most of the Python data stack accepts Polars without a copy》将介绍 Polars 的生态支持:哪些库能零拷贝保留 Polars 数据,哪些会通过 NumPy 或 pandas 转换,哪些由于完全不支持 Polars 而必须手动转换。
脚注
遵照 pandas 项目的要求,原文即使在句首也将 pandas 写作小写;本译文沿用这一写法。











暂无评论内容