
从 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更没有 MultiIndexpandas 为每一行打上一个标签index因此有.loc/.iloc、set_index、reset_index等一系列围绕索引展开的操作与隐患。而 Polars 中每一行由它在表中的整数位置唯一确定Polars 的 DataFrame 始终是一个二维、异构类型的表。列的数据类型可以嵌套如List、Struct但表结构本身不会因为索引操作而变形诸如重采样resampling等需求由专门的函数/方法以动词verb的方式作用在表上并显式声明其操作的列查询的语义不会因为索引状态或一次reset_index调用而改变从而保证结果可预期、查询可读官方文档的立场是去掉索引让事情更简单、更显式、更可读、更少出错。需要澄清的是Polars 作为优化手段也会在内部构建类似数据库的 index 数据结构只是它从不暴露给用户语义层。从 Python API 看数据选择与修改的入口都集中在 frame.py 中方法名select、with_columns、filter等本身就是动词替代了 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 原生提供三类执行引擎并保证语义一致性各引擎输出相同的结果内存in-process引擎针对适合装入内存的数据集优化流式streaming引擎面向超过内存容量的大规模数据配合惰性查询做增量流水线处理CuDF 支持的 GPU 引擎把查询下推到 GPU 执行详见 GPU engine 指南。这些引擎共享同一个查询优化器。pandas 虽然也可以在 NumPy 与 PyArrow 后端之间切换但由于其类型约束松散两个后端可能产生不同的 dtype 与语义容易埋下隐蔽 bugPolars 用统一类型系统规避了这一点。惰性求值与自动查询优化Eager即时求值代码一执行就出结果Lazy惰性求值执行某行代码只是把逻辑加入一棵查询计划query plan并不真正计算。pandas 只支持 eagerDask 通过生成查询计划支持 lazy。Polars两种都支持而且 lazy 模式的价值在于查询优化器会在真正执行前分析整棵计划树寻找加速查询或降低内存的手段详见后文查询优化。在 Python 侧.lazy()到collect()的完整链路由 lazyframe/frame.py 承载可参看 Lazy API 使用指南 与 执行 Lazy 查询。严格类型系统Polars 对数据类型非常严格类型解析取决于操作图由优化器统一推导。pandas 则会宽松地隐式转型例如在整型列中引入缺失值后整型列会被悄悄转成 float 列。Polars 的做法带来更少 bug 与更可预测的行为——整型列里的缺失值就是null列仍保持整型。基于表达式的、更通用的 APIpandas 没有表达式系统复杂逻辑常需借助 Pythonlambda表达。Polars 几乎所有操作select、filter、with_columns、group_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 取列用.selectdf.select(a)按值筛选行pandas 用布尔掩码或queryPolars 用.filterdf.filter(pl.col(a) 10)由于select/filter接受表达式并整体交给优化器多组选择条件可以被并行执行并联合优化。变得懒惰用scan_csvcollect替代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_csvdf pl.scan_csv(csv_file) grouped_df df.group_by(id1).agg(pl.col(v1).sum()).collect()这里发生了两件重要的事投影下推projection pushdown优化器发现最终只需要id1、v1两列于是从 CSV扫描阶段就只读取这两列而不是像 pandas 那样先整表读入内存再裁剪pandas 只能靠手动usecols补救.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( tenXValuelambda df_: df_.value * 10, hundredXValuelambda df_: df_.value * 100, )Polars 用.with_columns一次性挂多个表达式且可以并行执行df.with_columns( tenXValuepl.col(value) * 10, hundredXValuepl.col(value) * 100, )基于条件的列赋值when → then → otherwise假设df有a、b、c三列当c 2时用b覆盖a。pandas 用maskdf.assign(alambda 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)]Polarsdf.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 4Polars 用窗口表达式一步到位不需要手动 joindf.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 │ └─────┴──────┴──────┴─────┴──────────────┘在这个例子里size和reverse_type都按c分组缓存复用而sum按type分组——三种窗口统计同时计算、互不干扰。.over()的能力不止于此它支持多列分组如.over(Type 1, Type 2)也能配合rank、sort_by、head等实现组内 Top-N官方配套示例见 window.py.over()的完整行为在 表达式/窗口相关文档 中有进一步展开。缺失值null与NaN的严格区分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支持四类填充来源字面量.fill_null(0)用常量替换所有null表达式.fill_null(pl.col(b) * 2)用另一列的派生值逐行填充邻值策略.fill_null(strategyforward | backward)用前/后第一个非null值填充插值.fill_null(pl.col(x).interpolate())——注意用interpolate方法而非fill_null且序列首尾的null保持为null。NaN的特殊语义Polars 把NaN视为浮点数值而非缺失因此NaN不计入null_countfill_null不填充NaN需要用专门的fill_nan与null不同Polars 并不为NaN维护元数据is_nan需要真实计算数值聚合mean、sum等会跳过null但会把NaN计入并让它传播到结果。若希望聚合忽略NaN可先用fill_nan(None)等价写法fill_nan(None)将NaN转成null再聚合。顺带一提把NaN写进整型列pandas 会静默转型成 floatPolars 不会转型而是直接抛异常——这正是前文严格类型系统的体现。别到处.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_columnsfrom 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),仅供参考