1. 为什么我彻底告别了 Pandas,转投 Polars 的怀抱
去年冬天,我在处理一个客户交付的电商行为日志时,第一次真正被“卡住”了。那是一份 87GB 的原始 CSV 文件,记录着过去三年内所有用户的点击、加购、下单和支付事件。用 Pandas 加载它,我的 32GB 内存直接爆满,Jupyter Notebook 卡死三次,最后靠chunksize分块读取,再手动拼接,光是数据加载就花了 42 分钟。更糟的是,当我尝试对用户 ID 做去重计数并按时间窗口聚合时,df.groupby('user_id').agg({'event_time': 'min', 'order_id': 'count'})这一行代码跑了整整 19 分钟——而此时,隔壁组用 Rust 写的 CLI 工具,只用了 83 秒就完成了全部清洗和聚合。那一刻,我盯着屏幕上那个缓慢滚动的进度条,不是在想“怎么优化这行代码”,而是在想:“我是不是还在用十年前的工具,干着今天最重的活?”
这就是 Polars 击中我的第一记重拳:它不承诺“更好用”,但它直击痛点——当你的数据量突破单机内存舒适区,当你的迭代周期被 I/O 和计算拖成以小时计,当你的老板问“模型训练前的数据准备还要多久”,你拿不出一个体面的答案时,Polars 就不是“可选项”,而是“止损线”。它不是 Pandas 的升级版,而是为另一个时代重新设计的引擎。关键词是:多线程、列式存储、延迟执行、Arrow 原生。这四个词背后,是现代 CPU 架构、SSD 随机读写能力、以及大数据分析范式的深刻变迁。如果你每天还在为pd.read_csv()的耗时皱眉,为.groupby().apply()的慢如蜗牛而妥协,为内存 OOM 错误反复重启内核——那么这篇文字,就是为你写的。它不讲虚的“未来趋势”,只讲我亲手在生产环境里跑通的每一步:从安装、语法迁移、性能压测,到如何把 Polars 嵌进你现有的 Scikit-learn 或 PySpark 流水线里。这不是一篇“理论对比文”,而是一份我踩过所有坑后,写给自己的操作手册。
2. 核心设计思路拆解:为什么 Polars 能快出一个数量级?
2.1 不是“优化 Pandas”,而是“重建地基”
很多人初看 Polars,下意识会把它当成“Pandas 的加速插件”。这是最大的认知陷阱。Pandas 的核心设计哲学,诞生于 2008 年——那时主流 CPU 是单核或双核,内存价格昂贵,数据集普遍在 MB 到 GB 级别。它的 DataFrame 是一个基于 NumPy 数组的、行优先(row-major)的二维结构,所有操作默认 eager(立即执行),每一次.filter()、.groupby()都会立刻触发一次完整的内存拷贝和计算。这种设计,在小数据上极其直观、易调试;但在大数据上,它成了性能的“原罪”。
Polars 的设计起点完全不同。它从第一天起,就明确拒绝“兼容旧范式”的包袱。它的底层引擎完全重写,核心是三个不可妥协的硬性选择:
列式存储(Columnar Storage):Pandas 把一整行(比如
['user_123', '2024-01-01', 99.99, 'shoes'])作为一个单元存放在内存里;Polars 则把所有user_id存在一个连续内存块,所有event_time存在另一个块,所有price存在第三个块。这带来的直接好处是:当你只查询price > 50时,Polars 只需扫描price这一列的内存块,跳过其他 99% 的数据。而 Pandas 必须把每一行都从磁盘/内存里完整读出来,再拆解出price字段做判断——I/O 成本直接差一个数量级。这就像查图书馆的书:Pandas 是把整本书(一行)搬出来翻页找关键词;Polars 是直接去“价格索引区”(price 列)快速扫一遍。多线程原生(Multi-threaded by Default):Pandas 的
.apply()默认是单线程的,你得手动加concurrent.futures或dask才能并行。Polars 的每一个核心操作——读取、过滤、聚合、连接——在编译时就被设计为自动利用所有可用 CPU 核心。它没有“开启多线程”的开关,因为“单线程”根本不在它的设计选项里。其内部使用 Rust 编写的 Arrow 计算内核,能将一个大任务(如 5000 万行的groupby)自动切分成 N 个子任务,分发给 N 个线程并行处理,最后再高效合并结果。这不需要你写任何并发逻辑,是开箱即用的物理定律级优势。延迟执行(Lazy Evaluation):这是最反直觉、也最具威力的设计。Pandas 每写一行代码,就立刻执行一次。
df = pd.read_csv('data.csv')→ 立刻加载;df = df[df['price']>50]→ 立刻过滤;df = df.groupby('cat').sum()→ 立刻聚合。三次操作,三次全量数据遍历。Polars 的.lazy()模式则完全不同:df = pl.read_csv('data.csv').lazy()→ 此时什么都没发生,只是记下“我要读这个文件”;df = df.filter(pl.col('price')>50).groupby('cat').sum()→ 还是什么都没发生,只是在内存里构建了一个“执行计划树”(Query Plan)。真正的计算,只发生在你调用.collect()的那一刻。而这个“执行计划树”,Polars 会在.collect()前进行深度优化:它会发现“先过滤再聚合”比“先聚合再过滤”快得多,会把多个连续的.filter()合并成一个,会预判哪些列根本不需要参与计算而跳过加载……这就像你给快递公司下了一串指令(“从 A 仓取货→运到 B 仓→分拣→打包→发往 C 地”),Pandas 是每步都立刻执行;Polars 是先画一张最优物流路线图,再一次性派车出发。
提示:理解“延迟执行”是掌握 Polars 的钥匙。它不是为了炫技,而是为了解决一个根本矛盾:人类写代码的习惯(线性、逐步)与机器执行效率(批处理、优化)之间的鸿沟。
.lazy()不是让你“变懒”,而是让 Polars 有机会“变聪明”。
2.2 为什么不是 Dask 或 Modin?—— Polars 的独特定位
看到这里,你可能会问:“那 Dask DataFrame 或 Modin 呢?它们不也号称能并行、能处理大文件?” 这是个极好的问题,它触及了 Polars 的核心差异化价值。
Dask是一个分布式计算调度框架,它的 DataFrame 是对 Pandas API 的“分布式模拟”。它把一个大 DataFrame 切成很多小块(partitions),然后用一个中央调度器(scheduler)把计算任务分发到多个进程或机器上。它的优势在于“无限扩展”,劣势在于“调度开销”和“API 兼容性妥协”。Dask 的
.compute()本质还是在模拟 Pandas 的 eager 行为,它无法像 Polars 那样对列式计算进行底层优化。在单机场景下,Dask 的启动、序列化、网络通信(即使本地)开销,常常让它比原生 Pandas 还慢。它解决的是“超大规模集群”问题,而不是“单机性能瓶颈”问题。Modin的目标更接近 Polars,它也是想加速 Pandas。但它的策略是“在 Pandas 上层加一个并行引擎”,通过修改 Pandas 的底层调用,把
.read_csv()、.groupby()等函数重定向到自己的并行实现。这带来了巨大的兼容性优势(几乎 100% Pandas 语法),但也继承了 Pandas 的底层缺陷:它依然是行式存储,依然受限于 Python GIL 在某些操作上的瓶颈,其优化深度无法触及 Polars 的 Rust 内核。
Polars 的选择是“釜底抽薪”:放弃对 Pandas API 的 100% 兼容,换来的是从物理层(内存布局)、到逻辑层(执行模型)、再到语言层(Rust 编译)的全栈重构。它不试图“让旧车跑得更快”,而是“造一辆全新的、为高速公路设计的车”。所以,当你在一台 8 核 32GB 的笔记本上处理 10GB 数据时,Dask 可能因调度开销而挣扎,Modin 可能因架构限制而提升有限,而 Polars 会像一道闪电一样划过——因为它就是为这个场景而生的。
3. 核心细节解析与实操要点:从零开始构建你的第一个 Polars 流水线
3.1 安装与环境确认:避开那些“看似正常”的坑
Polars 的安装异常简单,但有几个关键点,决定了你后续能否发挥它的全部威力:
# 基础安装(必须) pip install polars # 强烈推荐:安装完整功能包(含 Parquet、Arrow、SQL 支持) pip install polars[all] # 如果你用 conda(推荐用于数据科学环境) conda install -c conda-forge polars安装完成后,务必验证三件事,这是我踩过的第一个深坑:
版本检查:
polars[all]包含了所有可选依赖,但有时 pip 会因为缓存或网络问题,只安装了核心包。运行以下代码:import polars as pl print(pl.__version__) # 确保是 0.20.0+(本文写作时最新稳定版) print(pl.show_versions()) # 重点看 "pyarrow" 和 "connectorx" 是否为 True如果
pyarrow显示False,说明 Arrow 支持没装上,read_parquet()和内存效率会大打折扣。Arrow 后端确认:Polars 的高性能严重依赖 Arrow。运行:
# 创建一个简单 DataFrame df = pl.DataFrame({"a": [1, 2, 3], "b": ["x", "y", "z"]}) print(df.schema) # 输出应为: {'a': Int64, 'b': String} # 关键!检查底层是否使用 Arrow print(df.to_arrow()) # 如果报错或输出奇怪,说明 Arrow 未正确集成多线程确认:Polars 默认启用所有核心,但某些 Linux 环境或 Docker 容器可能受限。运行一个简单压力测试:
import time import polars as pl import numpy as np # 生成一个 1000 万行的测试数据 n_rows = 10_000_000 df = pl.DataFrame({ "id": np.arange(n_rows), "value": np.random.randn(n_rows) }) start = time.time() # 执行一个需要大量计算的聚合 result = df.select([ pl.col("value").mean().alias("mean"), pl.col("value").std().alias("std"), pl.col("value").quantile(0.95).alias("p95") ]).collect() # 注意 .collect()! end = time.time() print(f"10M rows aggregation took {end-start:.2f}s on {pl.threadpool_size()} threads")pl.threadpool_size()应该返回你的 CPU 物理核心数(如 8)。如果时间远超预期(比如 > 10s),检查系统是否设置了OMP_NUM_THREADS=1等环境变量,它会强制 Polars 单线程运行。
注意:永远不要在 Jupyter 中省略
.collect()!在.lazy()模式下,不调用.collect(),你的代码不会执行任何计算,只会构建一个空的 Query Plan。这是新手最常见的“代码没报错但也没结果”的原因。
3.2 语法迁移:不是“学新命令”,而是“换一种思维”
从 Pandas 切换到 Polars,最大的障碍不是“记不住新函数名”,而是“大脑还停留在 eager 模式”。下面用最常犯的五个错误,来展示思维转换:
| 场景 | Pandas 写法(错误示范) | Polars 正确写法 | 为什么这样改? |
|---|---|---|---|
| 1. 读取 CSV | df = pd.read_csv("data.csv") | df = pl.read_csv("data.csv") | Polars 的read_csv默认就是多线程、内存映射的,无需额外参数。它甚至能自动推断数据类型,比 Pandas 更准。 |
| 2. 选择列 | df["price"]或df[["price", "category"]] | df.select("price")或df.select(["price", "category"]) | Polars 的select()是一个强大的列操作入口,支持表达式(如pl.col("price").log10()),而不仅仅是索引。df["price"]在 Polars 中会报错。 |
| 3. 过滤行 | df[df["price"] > 100] | df.filter(pl.col("price") > 100) | pl.col("price")是创建一个“列表达式”(Column Expression),它是 Polars 的核心抽象。所有计算(比较、数学运算、字符串处理)都必须基于pl.col()构建。df["price"] > 100是非法的。 |
| 4. 分组聚合 | df.groupby("category")["price"].sum() | df.groupby("category").agg(pl.col("price").sum()) | Pandas 的groupby().agg()接受字符串(如"sum")或函数;Polars 的agg()必须接受一个或多个pl.col()表达式。pl.col("price").sum()明确告诉引擎:“对 price 列求和”。 |
| 5. 连接表 | df1.merge(df2, on="id", how="left") | df1.join(df2, on="id", how="left") | join()的语义更接近 SQL,且 Polars 的连接算法针对列式存储做了极致优化,速度远超 Pandas 的merge()。 |
关键思维转变口诀:“一切皆表达式,操作即定义,执行靠 collect。”
pl.col("x") + pl.col("y")不是计算,是定义一个“x+y”的新列表达式;df.with_columns((pl.col("x") + pl.col("y")).alias("sum_xy"))不是立刻加列,是向 DataFrame 添加一个“待执行”的列定义;- 直到
.collect(),所有这些定义才被编译、优化、并行执行。
3.3 实战案例:用 Polars 重写一个真实的 ETL 流水线
让我们用一个真实场景收尾:一个电商公司的每日销售报表生成。原始需求是:从sales_raw.csv(5000 万行)和products.csv(10 万行)中,计算每个品类(category)的日销售额、订单数、平均客单价,并筛选出销售额 Top 10 的品类。
Pandas 版本(典型写法,耗时约 12 分钟):
import pandas as pd import time start = time.time() # 1. 加载(慢) sales = pd.read_csv("sales_raw.csv") products = pd.read_csv("products.csv") # 2. 连接(慢,且内存爆炸) merged = sales.merge(products, on="product_id", how="left") # 3. 聚合(慢,多次遍历) report = merged.groupby("category").agg({ "revenue": "sum", "order_id": "nunique", "revenue": "mean" }).rename(columns={"revenue": "total_revenue", "order_id": "order_count", "revenue": "avg_order_value"}) # 4. 排序取 Top 10(慢) top10 = report.sort_values("total_revenue", ascending=False).head(10) print(f"Pandas total time: {time.time()-start:.2f}s")Polars 版本(优雅且极速,耗时约 42 秒):
import polars as pl import time start = time.time() # 1. 加载(快!多线程,列式) sales = pl.read_csv("sales_raw.csv") products = pl.read_csv("products.csv") # 2. 连接(快!Arrow 优化) merged = sales.join(products, on="product_id", how="left") # 3. 一步到位聚合(快!延迟执行,单次遍历) report = ( merged .groupby("category") .agg([ pl.col("revenue").sum().alias("total_revenue"), pl.col("order_id").n_unique().alias("order_count"), pl.col("revenue").mean().alias("avg_order_value") ]) .sort("total_revenue", descending=True) .limit(10) # 直接 limit,避免全量排序 ) # 4. 执行! top10 = report.collect() print(f"Polars total time: {time.time()-start:.2f}s")为什么快了 17 倍?逐行拆解:
- 加载阶段:Polars 的
read_csv使用多线程解析,且只加载实际需要的列(如果指定了columns=["col1","col2"]),而 Pandas 默认加载所有列。 - 连接阶段:Polars 的
join基于哈希表,且利用列式特性,只对product_id列做哈希,跳过其他字段的处理。 - 聚合阶段:这是最大差异。Pandas 的
agg({...})会为每个聚合函数(sum, nunique, mean)单独遍历一次整个mergedDataFrame,共 3 次。Polars 的agg([...])是一个原子操作,引擎在一次数据遍历中,同时计算所有聚合值,内存局部性极佳。 - 排序阶段:Pandas 的
sort_values().head(10)会先对 10 万行(品类数)全量排序,再取前 10;Polars 的.limit(10)在sort()之后,是告诉引擎“我只需要前 10 条”,它会使用部分排序(Partial Sort)算法,复杂度从 O(n log n) 降到 O(n log k),k=10。
实操心得:在 Polars 中,尽可能把多个操作链式写在一个表达式里(用
.连接),并最后统一.collect()。这给了 Polars 最大的优化空间。拆成多行.collect(),就等于主动放弃了延迟执行的优势。
4. 实操过程与核心环节实现:性能压测与生产部署指南
4.1 严谨的基准测试:用真实数据说话,而非“玩具数据”
网上的很多 Polars 性能对比,用的是pl.DataFrame({"a": range(1000000)})这样的合成数据。这毫无意义。真实世界的性能瓶颈,往往藏在数据类型、缺失值、字符串编码、以及 I/O 瓶颈里。我为你设计了一套生产级的压测方案:
测试数据集(必须使用):
sales_50m.csv: 5000 万行,12 列(order_id,user_id,product_id,category,revenue,qty,date,country,device,source,is_return,discount_pct)。revenue和qty为浮点/整型,category和country为高基数字符串(>10000 个唯一值),date为 ISO 格式字符串。products_100k.csv: 10 万行,8 列(product_id,name,category,brand,price,weight,in_stock,launch_date)。
测试环境(严格复现):
- 硬件:Intel i7-11800H (8C/16T), 32GB DDR4 RAM, 1TB NVMe SSD
- 软件:Ubuntu 22.04, Python 3.10, Polars 0.20.5, Pandas 2.0.3 (已启用 Arrow backend)
- 关键设置:
export POLARS_MAX_THREADS=16(确保 Polars 使用所有逻辑核心)
五大核心测试项(代码与结果):
CSV 加载时间(I/O + 解析)
# Polars start = time.time() df = pl.read_csv("sales_50m.csv") load_time = time.time() - start print(f"Polars Load: {load_time:.2f}s, Memory: {df.estimated_size('mb'):.0f}MB") # Pandas (with dtype optimization) start = time.time() dtypes = {"user_id": "category", "product_id": "category", "category": "category"} df_pd = pd.read_csv("sales_50m.csv", dtype=dtypes) load_time_pd = time.time() - start print(f"Pandas Load: {load_time_pd:.2f}s, Memory: {df_pd.memory_usage(deep=True).sum()/1024**2:.0f}MB")结果:Polars 3.8s / 1.2GB vs Pandas 29.1s / 4.7GB。Polars 快7.7x,内存省3.9x。原因:Polars 多线程解析 + 列式存储 + 自动类型推断(
category类型无需手动指定)。复杂过滤(CPU + 内存带宽)
任务:找出revenue > 1000且country == "US"且date >= "2023-01-01"的所有订单。
结果:Polars 0.62s vs Pandas 4.3s (6.9x)。Polars 的列式过滤只需扫描revenue、country、date三列,且date字符串比较被优化为字节比较;Pandas 需要加载所有列并逐行解析。高基数 GroupBy(哈希 + 内存分配)
任务:按user_id(5000 万行中约 800 万唯一值)分组,计算revenue.sum()和order_id.n_unique()。
结果:Polars 2.1s vs Pandas 18.7s (8.9x)。Polars 的哈希表实现针对高基数优化,且n_unique()在列式下是 O(1) 的位图操作;Pandas 的nunique()需要为每个 group 构建 Python set。大表 Join(内存 + 算法)
任务:将sales_50m.csv与products_100k.csv通过product_id进行 inner join。
结果:Polars 1.4s vs Pandas 11.2s (8.0x)。Polars 的 join 使用 radix hash,且只对product_id列构建哈希表;Pandas 的 merge 需要对整个 DataFrame 做索引和匹配。混合聚合(计算密集型)
任务:对sales_50m.csv,计算revenue.mean()、revenue.std()、revenue.quantile(0.99)、qty.sum()。
结果:Polars 0.85s vs Pandas 6.2s (7.3x)。Polars 的agg()在一次遍历中完成所有计算;Pandas 需要四次独立遍历。
压测结论表格:
| 操作 | Polars 耗时 (s) | Pandas 耗时 (s) | 加速比 | 主要瓶颈突破点 |
|---|---|---|---|---|
| CSV 加载 | 3.8 | 29.1 | 7.7x | 多线程 I/O + 列式解析 |
| 复杂过滤 | 0.62 | 4.3 | 6.9x | 列式扫描 + 字符串优化 |
| 高基数 GroupBy | 2.1 | 18.7 | 8.9x | Radix Hash + 位图计数 |
| 大表 Join | 1.4 | 11.2 | 8.0x | Radix Hash + 内存局部性 |
| 混合聚合 | 0.85 | 6.2 | 7.3x | 单次遍历多聚合 |
提示:压测时,务必在每次测试前用
import gc; gc.collect()清理 Python 垃圾,并用psutil.Process().memory_info().rss监控实时内存,排除缓存干扰。真实世界的数据,永远比玩具数据残酷。
4.2 生产部署:如何安全、平滑地将 Polars 引入现有项目
把 Polars 引入一个已有百万行代码的 Pandas 项目,绝不能“一刀切”。我的经验是采用“洋葱式”渐进策略:
第 1 层:I/O 层(最安全,收益最高)
- 替换
pd.read_csv()/pd.read_parquet():这是零风险、高回报的切入点。Polars 的read_*函数返回的是pl.DataFrame,但你可以立刻用.to_pandas()转回 Pandas,下游代码完全无感。# 旧代码 df = pd.read_csv("data.csv") # 新代码(无缝替换) df = pl.read_csv("data.csv").to_pandas() # 速度提升 5-10x,业务逻辑零改动 - 收益:数据加载速度提升,内存占用下降,且不改变任何业务逻辑。
第 2 层:计算密集型模块(中等风险,高收益)
- 识别“性能热点”:用
line_profiler或py-spy找出耗时最长的.groupby()、.merge()、.apply()模块。 - 隔离重写:将这些模块抽取成独立函数,用 Polars 重写,并提供 Pandas 兼容接口:
def calculate_daily_metrics_polars(sales_df: pl.DataFrame) -> pl.DataFrame: return (sales_df .groupby("date") .agg([pl.col("revenue").sum(), pl.col("order_id").n_unique()]) .sort("date")) # 在主流程中 if USE_POLARS: sales_pl = pl.from_pandas(sales_pd) # Pandas -> Polars metrics = calculate_daily_metrics_polars(sales_pl).to_pandas() # Polars -> Pandas else: metrics = calculate_daily_metrics_pandas(sales_pd)
第 3 层:核心流水线(高风险,最高收益)
- 重构为 Lazy 模式:当你的整个 ETL 流水线都稳定后,去掉所有中间
.collect(),构建一个完整的 Lazy Query Plan,最后.collect()一次。这是性能的终极形态。 - 引入类型检查:Polars 的 schema 是强类型的。在
.collect()前,用.schema断言输出结构,防止上游数据变更导致静默错误:result = (df.lazy() .filter(pl.col("revenue") > 0) .groupby("category") .agg(pl.col("revenue").sum()) .collect()) assert result.schema == {"category": pl.Utf8, "revenue": pl.Float64}
关键注意事项:
- 永远保留 Pandas 回滚路径:在配置文件中设置
USE_POLARS = True/False,确保上线后可秒级回滚。 - 监控内存与线程:在生产环境中,用
pl.Config.set_streaming(True)启用流式处理(对超大文件),并用pl.Config.set_fmt_str_lengths(1000)控制日志长度,避免日志爆炸。 - 警惕字符串操作:Polars 的字符串方法(
.str.contains(),.str.split())虽快,但某些正则操作仍不如 Pandas 的str.extract()灵活。遇到复杂文本解析,先用 Polars 加载,再用 Pandas 处理特定列,最后合并。
5. 常见问题与排查技巧实录:那些文档里不会写的“血泪教训”
5.1 “为什么我的 Polars 代码比 Pandas 还慢?”—— 五种致命错误
这是最常被问到的问题。Polars 绝非银弹,用错了方式,它会比 Pandas 更慢。以下是我在生产环境里亲手踩过的坑:
| 错误类型 | 错误代码示例 | 为什么慢? | 正确做法 |
|---|---|---|---|
1. 频繁.collect() | df = df.filter(...).collect()df = df.sort(...).collect()df = df.head(10).collect() | 每次.collect()都触发一次完整的计算和内存分配,相当于把一个大任务拆成 10 个小任务,调度开销巨大。 | 链式调用,最后.collect():result = df.filter(...).sort(...).head(10).collect() |
2. 滥用.to_pandas() | df_pl = pl.read_csv("big.csv")df_pd = df_pl.to_pandas()df_pd["new_col"] = df_pd["a"] + df_pd["b"]df_pl = pl.from_pandas(df_pd) | .to_pandas()是昂贵的内存拷贝操作,会把整个列式数据转成行式,失去所有 Polars 优势。 | 在 Polars 内部完成所有计算:df_pl = df_pl.with_columns((pl.col("a") + pl.col("b")).alias("new_col")) |
| 3. 忽略数据类型 | df = pl.read_csv("data.csv")df = df.filter(pl.col("date") > "2023-01-01") | 如果date列被读成Utf8(字符串),比较是字节比较,无法利用日期索引。 | 显式指定类型或转换:df = pl.read_csv("data.csv", try_parse_dates=True)或 df = df.with_columns(pl.col("date").str.strptime(pl.Date, "%Y-%m-%d")) |
| 4. 在循环中创建 DataFrame | results = []for item in items:temp_df = pl.DataFrame({"x": [item]})results.append(temp_df)final_df = pl.concat(results) | 每次pl.DataFrame()都有构造开销,concat在循环中是 O(n²) 复杂度。 | 批量构建,一次concat:data_list = [[item] for item in items]final_df = pl.DataFrame({"x": data_list}) |
5. 误用apply | df = df.with_columns(pl.col("text").apply(lambda x: x.upper())) | apply是 Python 回调,会破坏 Polars 的并行和向量化,退化为单线程。 | 使用内置向量化方法:df = df.with_columns(pl.col("text").str.to_uppercase()) |
提示:当你怀疑性能问题时,第一反应不是“Polars 有问题”,而是运行
df.explain()。它会打印出 Polars 为你生成的物理执行计划(Physical Plan),清晰显示每一步操作、使用的算法(如HASH AGGREGATION)、以及预计的数据大小。这是你最强大的调试武器。
5.2 “Polars 不支持我的库!”—— 兼容性破局实战
Polars 的生态确实不如 Pandas 成熟,但这不意味着你要放弃。以下是几个高频场景的破局方案:
场景 1:需要 Matplotlib/Seaborn 画图
- 错误做法:试图让 Polars 直接支持
.plot()。 - 正确做法:只在绘图前一刻转换,且只转换必要列:
# 只转换用于绘图的两列,避免全量拷贝 plot_data = df.select(["date", "revenue"]).to_pandas() plot_data.plot(x="date", y="revenue", kind="line")
场景 2:需要 Statsmodels 做统计建模
- 错误做法:
model = sm.OLS(df["y"], df["X"]).fit()(Polars 不支持)。 - 正确做法:用
.to_numpy()获取底层数组,这是零拷贝的(Arrow 内存共享):# X 是一个 2D NumPy 数组,y 是 1D,都是 Arrow 内存视图 X = df.select(pl.col("feature1"), pl.col("feature2")).to_numpy() y = df["target"].to_numpy() model = sm.OLS(y, X).fit()