news 2026/9/9 13:25:27

从 pandas 迁移到 Polars:概念差异、表达式思维与代码改写实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从 pandas 迁移到 Polars:概念差异、表达式思维与代码改写实战指南

从 pandas 迁移到 Polars:概念差异、表达式思维与代码改写实战指南

【免费下载链接】polarsExtremely fast Query Engine for DataFrames, written in Rust项目地址: https://gitcode.com/GitHub_Trending/po/polars

导读:本文是官方用户指南中「Coming from Pandas」(pandas.md)的深度扩展版,面向已有 pandas 经验、希望转向 Polars 的开发者。文章先厘清两大库在索引、内存格式、并行执行、求值模式与类型系统上的根本差异,再逐条演示数据选择、惰性查询、并行列赋值、窗口函数与缺失值处理的 pandas → Polars 改写套路,并结合仓库源码与配套示例讲解其底层原理,帮助读者建立"表达式优先"的 Polars 心智模型。

概念层面:pandas 与 Polars 的根本差异

pandas 与 Polars 虽然都面向表格型数据,但底层设计哲学截然不同。看懂下面这组概念差异,是写出高质量 Polars 代码的前提。

Polars 没有索引(index),更没有 MultiIndex

pandas 为每一行打上一个标签(index),因此有.loc/.ilocset_indexreset_index等一系列围绕索引展开的操作与隐患。而 Polars 中每一行由它在表中的整数位置唯一确定:

  • Polars 的 DataFrame 始终是一个二维、异构类型的表。列的数据类型可以嵌套(如ListStruct),但表结构本身不会因为"索引操作"而变形;
  • 诸如重采样(resampling)等需求,由专门的函数/方法以"动词(verb)"的方式作用在表上,并显式声明其操作的列;
  • 查询的语义不会因为索引状态或一次reset_index调用而改变,从而保证结果可预期、查询可读
  • 官方文档的立场是:去掉索引让事情更简单、更显式、更可读、更少出错。

需要澄清的是:Polars 作为优化手段也会在内部构建类似数据库的 index 数据结构,只是它从不暴露给用户语义层。从 Python API 看,数据选择与修改的入口都集中在 frame.py 中,方法名(selectwith_columnsfilter等)本身就是"动词",替代了 pandas 中围绕.loc的各种读写套路。

内存表示:Apache Arrow 列式格式 vs NumPy 数组

pandas 默认以 NumPy 数组承载数据,而 Polars 严格遵循Apache Arrow 内存规范(对应仓库中的 polars-arrow crate,其 Cargo 清单见 polars-arrow/Cargo.toml)。Arrow 是内存列式分析的事实标准,可以带来更快的加载速度、更低的内存占用和更快的计算,并且天然支持与其他 Arrow 生态工具零拷贝互操作。

若需要把 Polars 数据交给 NumPy 生态处理,官方提供了显式转换通道:to_numpy方法。

并行能力:线程级并发 vs 单线程核心

pandas 只有部分操作是多线程的,核心仍是单线程;要并行化通常得额外引入 Dask 之类的框架。Polars 用 Rust 编写,充分利用 Rust 的并发安全能力把大量操作拆到多线程并行执行。

这一点在仓库结构上非常直观:整个计算栈被拆分为 polars-mem-engine(内存态执行器,其实现位于该 crate 的src/executors)与 polars-stream(流式执行器,src/nodes目录下是按算子拆分的 153 个节点实现)。表达式层面,凡是在同一个select/with_columns/filter上下文内的操作都可以并行,而不像 pandas 需要靠外部框架分片。

注:原指南文档称"Polars 比所有并行化 pandas 代码的开源方案都快",这是项目官方文档中的表述;实际相对性能取决于硬件、数据形态与具体算子,建议以自己场景的基准测试为准。

多引擎支持:内存引擎、流式引擎与 GPU 引擎

Polars 原生提供三类执行引擎,并保证语义一致性(各引擎输出相同的结果):

  1. 内存(in-process)引擎:针对适合装入内存的数据集优化;
  2. 流式(streaming)引擎:面向超过内存容量的大规模数据,配合惰性查询做增量流水线处理;
  3. CuDF 支持的 GPU 引擎:把查询下推到 GPU 执行,详见 GPU engine 指南。

这些引擎共享同一个查询优化器。pandas 虽然也可以在 NumPy 与 PyArrow 后端之间切换,但由于其类型约束松散,两个后端可能产生不同的 dtype 与语义,容易埋下隐蔽 bug;Polars 用统一类型系统规避了这一点。

惰性求值与自动查询优化

  • Eager(即时)求值:代码一执行就出结果;
  • Lazy(惰性)求值:执行某行代码只是把逻辑加入一棵"查询计划(query plan)",并不真正计算。

pandas 只支持 eager;Dask 通过生成查询计划支持 lazy。Polars两种都支持,而且 lazy 模式的价值在于:查询优化器会在真正执行前分析整棵计划树,寻找加速查询或降低内存的手段(详见后文"查询优化")。

在 Python 侧,.lazy()collect()的完整链路由 lazyframe/frame.py 承载,可参看 Lazy API 使用指南 与 执行 Lazy 查询。

严格类型系统

Polars 对数据类型非常严格:类型解析取决于操作图,由优化器统一推导。pandas 则会宽松地"隐式转型",例如在整型列中引入缺失值后,整型列会被悄悄转成 float 列。Polars 的做法带来更少 bug 与更可预测的行为——整型列里的缺失值就是null,列仍保持整型。

基于表达式的、更通用的 API

pandas 没有表达式系统,复杂逻辑常需借助 Pythonlambda表达。Polars 几乎所有操作(selectfilterwith_columnsgroup_by.agg…)都接受表达式(Expr)输入——你只要学会一次表达式,知识就能在整个 API 中迁移复用。Polars 把"必须写 Python lambda"视为 API 表达力不足的信号,并尽量提供原生支持。

表达式的 Python 实现集中在 expr/expr.py,条件分支三件套when/then/otherwise见 expr/whenthen.py。后文的大量示例都会体现这一点。

关键语法差异:从 pandas 逐行改写

官方文档把最关键的一句话总结为:

polars != pandas

如果你的 Polars 代码看起来像 pandas 代码,它可能能跑,但大概率比应有的速度慢——因为 pandas 的写法(逐行 eager 变换、lambda、局部掩码)会阻止 Polars 进入最优执行路径。

数据选择:用表达式代替.loc/.iloc

因为 Polars 没有索引,所以没有.loc/.iloc,相应地也没有 pandas 的SettingWithCopyWarning

pandas 取列:

df["a"] df.loc[:, "a"]

Polars 取列用.select

df.select("a")

按值筛选行,pandas 用布尔掩码或query,Polars 用.filter

df.filter(pl.col("a") < 10)

由于select/filter接受表达式并整体交给优化器,多组选择条件可以被并行执行并联合优化。

变得"懒惰":用scan_csv+collect替代read_csv

惰性模式应该成为 Polars 的默认工作方式,因为只有 lazy 才能触发查询优化。进入 lazy 有两种途径:使用隐式惰性的读取函数(如scan_csv),或对已有DataFrame调用.lazy()

考虑官方文档的例子:磁盘上有一个列很多的 CSV,我们只想按id1分组并对v1求和。

pandas 写法:

df = pd.read_csv(csv_file, usecols=["id1", "v1"]) grouped_df = df.loc[:, ["id1", "v1"]].groupby("id1").sum()

Polars 惰性写法(只需把 eager 的read_csv换成惰性的scan_csv):

df = pl.scan_csv(csv_file) grouped_df = df.group_by("id1").agg(pl.col("v1").sum()).collect()

这里发生了两件重要的事:

  1. 投影下推(projection pushdown):优化器发现最终只需要id1v1两列,于是从 CSV扫描阶段就只读取这两列,而不是像 pandas 那样先整表读入内存再裁剪(pandas 只能靠手动usecols补救);
  2. .collect()触发求值:第二行末尾调用.collect()才指示 Polars 真正执行整条查询。

若确实想用 eager 模式,把scan_csv换回read_csv即可。完整的优化手段清单(谓词下推、投影下推、切片下推、公共子计划消除、表达式简化、join 排序、类型强转、基数估计等)可查阅 Optimizations 文档。

pl.scan_*系列不仅限于 CSV——官方文档说明它覆盖 CSV、IPC、Parquet、JSON 等常见格式。想确认某个 lazy 查询到底被优化成了什么样子,可以先用explain打印优化前后的计划树,相关说明见 Query Plan 文档。

表达你自己:用表达式在单个上下文内并行

pandas 脚本的本质是多步顺序执行的变换(每个中间结果都要落一次内存);Polars 则把多个变换折叠进表达式,让它们在一个上下文内并行执行

列赋值:with_columnsvsassign

假设df有一列value,我们要新增tenXValue(×10)与hundredXValue(×100)两列。

pandas 用assign+ lambda,两步顺序执行

df.assign( tenXValue=lambda df_: df_.value * 10, hundredXValue=lambda df_: df_.value * 100, )

Polars 用.with_columns一次性挂多个表达式,且可以并行执行

df.with_columns( tenXValue=pl.col("value") * 10, hundredXValue=pl.col("value") * 100, )
基于条件的列赋值:when → then → otherwise

假设dfabc三列,当c == 2时用b覆盖a

pandas 用mask

df.assign(a=lambda df_: df_["a"].mask(df_["c"] == 2, df_["b"]))

Polars 用条件表达式:

df.with_columns( pl.when(pl.col("c") == 2) .then(pl.col("b")) .otherwise(pl.col("a")) .alias("a") )

注意 Polars 可以并行计算if → then → otherwise的每个分支——当分支本身很昂贵时(比如每个分支内是复杂聚合),这种并行性价值尤为明显。

过滤与过滤融合

pandas 过滤房产数据:

df.query("m2_living > 2500 and price < 300000") # 或等价掩码 df[(df["m2_living"] > 2500) & (df["price"] < 300000)]

Polars:

df.filter( (pl.col("m2_living") > 2500) & (pl.col("price") < 300000) )

更进一步:即使你把过滤写成多个分散的.filter调用,查询优化器也会检测到并把它们合并为单个 filter(属于谓词下推/表达式简化范畴,可对照 Optimizations 文档 中的说明),尽量避免多次遍历数据。

pandastransform→ Polars 窗口表达式.over()

pandas 文档中经典的groupby(...).transform(...)用法,在 Polars 中对应的是窗口函数。给定如下DataFrame,我们希望新增一列size,表示每个c分组内的行数:

df = pd.DataFrame({ "c": [1, 1, 1, 2, 2, 2, 2], "type": ["m", "n", "o", "m", "m", "n", "n"], }) df["size"] = df.groupby("c")["type"].transform(len)

pandas 的思路是:按c分组 → 取type→ 算组长度 → 再把结果join 回原表

c type size 0 1 m 3 1 1 n 3 2 1 o 3 3 2 m 4 4 2 m 4 5 2 n 4 6 2 n 4

Polars 用窗口表达式一步到位(不需要手动 join):

df.with_columns( pl.col("type").count().over("c").alias("size") )
shape: (7, 3) ┌─────┬──────┬──────┐ │ c ┆ type ┆ size │ │ --- ┆ --- ┆ --- │ │ i64 ┆ str ┆ u32 │ ╞═════╪══════╪══════╡ │ 1 ┆ m ┆ 3 │ │ 1 ┆ n ┆ 3 │ │ 1 ┆ o ┆ 3 │ │ 2 ┆ m ┆ 4 │ │ 2 ┆ m ┆ 4 │ │ 2 ┆ n ┆ 4 │ │ 2 ┆ n ┆ 4 │ └─────┴──────┴──────┘

为什么窗口比"transform + join"更强?因为整组逻辑被压缩进一个表达式,你可以在同一个上下文中组合多个窗口函数,甚至可以基于不同的分组键同时计算。而且 Polars 会缓存作用于同一分组的窗口表达式——把多次.over("c")放进同一个.with_columns既方便又是最优选择:

df.with_columns( pl.col("c").count().over("c").alias("size"), pl.col("c").sum().over("type").alias("sum"), pl.col("type").reverse().over("c").alias("reverse_type"), )
shape: (7, 5) ┌─────┬──────┬──────┬─────┬──────────────┐ │ c ┆ type ┆ size ┆ sum ┆ reverse_type │ │ --- ┆ --- ┆ --- ┆ --- ┆ --- │ │ i64 ┆ str ┆ u32 ┆ i64 ┆ str │ ╞═════╪══════╪══════╪═════╪══════════════╡ │ 1 ┆ m ┆ 3 ┆ 5 ┆ o │ │ 1 ┆ n ┆ 3 ┆ 5 ┆ n │ │ 1 ┆ o ┆ 3 ┆ 1 ┆ m │ │ 2 ┆ m ┆ 4 ┆ 5 ┆ n │ │ 2 ┆ m ┆ 4 ┆ 5 ┆ n │ │ 2 ┆ n ┆ 4 ┆ 5 ┆ m │ │ 2 ┆ n ┆ 4 ┆ 5 ┆ m │ └─────┴──────┴──────┴─────┴──────────────┘

在这个例子里,sizereverse_type都按c分组(缓存复用),而sumtype分组——三种窗口统计同时计算、互不干扰。

.over()的能力不止于此:它支持多列分组(如.over("Type 1", "Type 2")),也能配合ranksort_byhead等实现"组内 Top-N",官方配套示例见 window.py;.over()的完整行为在 表达式/窗口相关文档 中有进一步展开。

缺失值:nullNaN的严格区分

pandas 的混乱 vs Polars 的整齐

pandas 依据列 dtype 混用NaN/None表示缺失,且行为还会因是否启用可选的可空数组而不同。Polars 的规则非常干净:

  • 所有数据类型的缺失值统一用null表示;
  • 浮点列允许出现NaN,但NaN是"特殊的浮点值",不算缺失数据
  • 整型列出现缺失时,pandas(除非用可空 dtype)会把整型列悄悄转成带NaN的 float 列;Polars 中整型列的缺失值就是null,列保持整型

处理缺失值:fill_null的四种填充方式

关于缺失值的完整讲解见 Missing data 文档,其可执行示例位于 missing-data.py。fill_null支持四类填充来源:

  1. 字面量.fill_null(0)用常量替换所有null
  2. 表达式.fill_null(pl.col("b") * 2),用另一列的派生值逐行填充;
  3. 邻值策略.fill_null(strategy="forward" | "backward"),用前/后第一个非null值填充;
  4. 插值.fill_null(pl.col("x").interpolate())——注意用interpolate方法而非fill_null,且序列首尾的null保持为null

NaN的特殊语义

Polars 把NaN视为浮点数值而非缺失,因此:

  • NaN不计入null_count
  • fill_null不填充NaN,需要用专门的fill_nan
  • null不同,Polars 并不为NaN维护元数据,is_nan需要真实计算;
  • 数值聚合(meansum等)会跳过null,但会NaN计入并让它传播到结果。若希望聚合忽略NaN,可先用fill_nan(None)(等价写法fill_nan(None)NaN转成null)再聚合。

顺带一提:把NaN写进整型列,pandas 会静默转型成 float;Polars 不会转型而是直接抛异常——这正是前文"严格类型系统"的体现。

别到处.pipe():用"返回表达式的函数"替代

pandas 生态里很流行用.pipe把一个函数依次作用在DataFrame上:

def add_foo(df: pd.DataFrame) -> pd.DataFrame: df["foo"] = ... return df def add_bar(df: pd.DataFrame) -> pd.DataFrame: df["bar"] = ... return df def add_ham(df: pd.DataFrame) -> pd.DataFrame: df["ham"] = ... return df (df .pipe(add_foo) .pipe(add_bar) .pipe(add_ham) )

如果把这套习惯原样搬进 Polars,你会得到3 个独立的with_columns上下文,迫使 Polars 串行执行 3 段变换,并行度为零,并产生次优的查询计划。

正确做法是:把每个变换写成"创建表达式"的函数。下面的写法在同一个with_columns上下文中注入 3 个表达式,从而被允许并行执行:

def get_foo(input_column: str) -> pl.Expr: return pl.col(input_column).some_computation().alias("foo") def get_bar(input_column: str) -> pl.Expr: return pl.col(input_column).some_computation().alias("bar") def get_ham(input_column: str) -> pl.Expr: return pl.col(input_column).some_computation().alias("ham") # 单个上下文,3 个表达式并行运行 df.with_columns( get_ham("col_a"), get_bar("col_b"), get_foo("col_c"), )

如果这些生成表达式的函数确实需要读取 schema才能决定分支,才使用(而且只用一次)pipe——在管道的最外层把LazyFrame传给一个闭包,闭包内读取lf.schema后再构造with_columns

from collections import OrderedDict def get_foo(input_column: str, schema: OrderedDict) -> pl.Expr: if "some_col" in schema: # branch_a ... else: # branch b ... def get_bar(input_column: str, schema: OrderedDict) -> pl.Expr: if "some_col" in schema: # branch_a ... else: # branch b ... def get_ham(input_column: str) -> pl.Expr: return pl.col(input_column).some_computation().alias("ham") # 仅在需要获取 LazyFrame 的 schema 时使用一次 pipe lf.pipe(lambda lf: lf.with_columns( get_ham("col_a"), get_bar("col_b", lf.schema), get_foo("col_c", lf.schema), ))

"返回表达式的函数"还有额外的架构收益:表达式可链式调用、可偏应用、可组合,从而让自定义逻辑具备远超 pandaspipe链的复用性与灵活性——这也是从 pandas 迁移到 Polars 时最值得刻意练习的思维转变。

迁移要点速查

  • 忘掉索引:用整数位置理解行,用select/filter/with_columns这些"动词"操作数据;
  • 默认走 lazy:文件入口用scan_csv/scan_parquet等,内存 DataFrame 调.lazy(),末尾.collect();让投影下推与谓词下推帮你少读数据;
  • 拒绝 lambda:凡是一个 pandas lambda 能做的事,先想想 Polars 是否已有原生表达式(when/then/otherwise、算术、字符串、聚合、窗口…);
  • 把多步assign/pipe收敛为单个上下文内的多个表达式,换取并行执行与更优查询计划;
  • 数据在列内嵌套没关系,但表永远是二维的;需要"行级标签"语义时,显式创建一列即可;
  • 缺失值只认null(浮点列另有不算缺失的NaN),整型列不再因为缺失而悄悄变 float。

完整的惰性 API 讲解可继续阅读 Lazy API 使用指南(配套示例 using.py)与 执行 Lazy 查询;若想对照 Spark 的迁移思路,仓库还提供了一份平行的 Spark 迁移指南。

【免费下载链接】polarsExtremely fast Query Engine for DataFrames, written in Rust项目地址: https://gitcode.com/GitHub_Trending/po/polars

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 13:25:14

离线标签与实时标签的冷热分层架构实践

做了几年用户画像和数据标签平台&#xff0c;我最大的感受是&#xff1a;离线标签和实时标签从来不是一道二选一的选择题&#xff0c;而是一道需要结合业务场景做组合的架构题。几乎每个刚接触标签体系的团队&#xff0c;都会在“要不要上实时”这个问题上反复纠结&#xff0c;…

作者头像 李华
网站建设 2026/9/9 13:25:06

opencode:不绑定模型的AI编程Agent,免费模型也能玩得转

最近我把市面上的 AI 编程 Agent 基本都折腾了一遍&#xff1a;Claude Code、Codex CLI、Gemini CLI&#xff0c;还有一个以前被我忽略的开源选手——opencode。说实话&#xff0c;最早我对它没什么期待&#xff0c;终端里这类工具太多了&#xff0c;直到某天我把 Gemini Flash…

作者头像 李华
网站建设 2026/9/9 13:24:43

STM32全双工网络语音实现:从I2S采集到UDP传输的完整方案

简介&#xff1a;STM32双工网络语音源代码是一套基于STM32微控制器与LwIP协议栈的嵌入式语音通信工程&#xff0c;面向需要实现双向实时语音传输的开发者&#xff0c;可作为相关课程设计与工程项目的起点。项目覆盖音频采集、PCM/G.711编解码、TCP连接管理及双工调度等关键环节…

作者头像 李华
网站建设 2026/9/9 13:23:06

使用 Polars GPUEngine 精细控制 GPU 查询执行

使用 Polars GPUEngine 精细控制 GPU 查询执行 【免费下载链接】polars Extremely fast Query Engine for DataFrames, written in Rust 项目地址: https://gitcode.com/GitHub_Trending/po/polars Polars 的 GPU 引擎&#xff08;GPU engine&#xff09;允许把 LazyFra…

作者头像 李华