Polars vs Pandas 2.2:1亿行CSV处理性能终极对决
当数据规模膨胀到1亿行级别,CSV文件的处理就变成了一个真正的技术挑战。作为Python生态中最主流的数据处理工具,Pandas 2.2带来了诸多性能优化,而新兴的Polars则凭借其Rust底层架构异军突起。本文将基于真实1亿行数据集,通过基准测试揭示两者的吞吐量、内存占用和常见操作效率差异,为技术选型提供数据支撑。
1. 测试环境与方法论
1.1 基准测试配置
我们在一台配备32核AMD EPYC处理器、128GB内存和NVMe固态硬盘的服务器上进行测试,确保硬件不会成为性能瓶颈。测试数据集是通过faker库生成的模拟电商交易数据,包含以下字段:
{ "order_id": "uuid", "customer_id": "uuid", "product_id": "uuid", "quantity": "int", "unit_price": "float", "discount": "float", "order_date": "timestamp", "region": "category", "payment_method": "category" }生成1亿行数据的脚本如下:
import pandas as pd from faker import Faker import numpy as np fake = Faker() Faker.seed(42) def generate_data(n_rows): data = { "order_id": [fake.uuid4() for _ in range(n_rows)], "customer_id": [fake.uuid4() for _ in range(n_rows)], "product_id": [fake.uuid4() for _ in range(n_rows)], "quantity": np.random.randint(1, 100, size=n_rows), "unit_price": np.round(np.random.uniform(10, 1000, size=n_rows), 2), "discount": np.round(np.random.uniform(0, 0.3, size=n_rows), 2), "order_date": pd.date_range(start="2020-01-01", periods=n_rows, freq="min"), "region": np.random.choice(["North", "South", "East", "West"], size=n_rows), "payment_method": np.random.choice( ["Credit Card", "PayPal", "Bank Transfer"], size=n_rows ), } return pd.DataFrame(data) df = generate_data(100_000_000) df.to_csv("ecommerce_100m.csv", index=False)1.2 测试指标定义
我们主要关注三个维度的性能表现:
| 指标类别 | 具体测量项 | 测量方法 |
|---|---|---|
| I/O性能 | CSV读取时间 | 从磁盘加载到内存的耗时 |
| 内存效率 | 峰值内存占用 | 使用memory_profiler监控峰值RSS |
| 操作性能 | 过滤、分组聚合、连接等操作耗时 | 多次运行取平均时间 |
所有测试均重复5次,取中位数作为最终结果,以避免偶然波动。
2. 基础性能对比:读取与内存
2.1 CSV加载速度
我们首先测试最基础的CSV文件读取性能。Pandas 2.2引入了PyArrow作为可选后端,而Polars天生基于Arrow内存格式。
# Pandas读取代码 import pandas as pd df = pd.read_csv("ecommerce_100m.csv") # Polars读取代码 import polars as pl df = pl.read_csv("ecommerce_100m.csv")测试结果令人惊讶:
| 工具 | 读取时间(s) | 内存占用(GB) |
|---|---|---|
| Pandas 2.2 | 48.7 | 22.3 |
| Polars | 9.2 | 8.1 |
Polars的读取速度比Pandas快5倍以上,同时内存占用仅为Pandas的36%。这主要得益于:
- 并行读取:Polars自动利用所有CPU核心并行解析CSV
- 零拷贝机制:直接映射Arrow格式到内存,减少数据复制
- 列式存储:仅加载实际需要的列数据
2.2 内存优化技巧
对于必须使用Pandas的场景,我们可以通过以下参数显著降低内存占用:
df = pd.read_csv( "ecommerce_100m.csv", dtype={ "region": "category", "payment_method": "category" }, engine="pyarrow", dtype_backend="pyarrow" )优化后的Pandas内存占用降至14.7GB,但仍高于Polars。关键差异在于:
| 优化手段 | Pandas效果 | Polars原生优势 |
|---|---|---|
| 类型指定 | 部分有效 | 自动类型推断 |
| 分类数据类型 | 需手动设置 | 自动检测优化 |
| 字符串处理 | 对象类型 | UTF-8原生支持 |
3. 核心操作性能对决
3.1 数据过滤效率
我们测试一个典型场景:筛选出2023年之后、金额大于500元的订单。等效代码如下:
# Pandas filtered = df[ (df["order_date"] >= "2023-01-01") & (df["unit_price"] * (1 - df["discount"]) > 500) ] # Polars filtered = df.filter( (pl.col("order_date") >= "2023-01-01") & (pl.col("unit_price") * (1 - pl.col("discount")) > 500) )性能对比:
| 操作 | Pandas耗时(ms) | Polars耗时(ms) |
|---|---|---|
| 条件过滤 | 1,820 | 420 |
Polars的延迟执行和查询优化使其在复杂过滤条件下优势明显。当条件增加到5个时,Polars仍能保持亚秒级响应,而Pandas则线性增长到3.5秒。
3.2 分组聚合性能
分组聚合是数据分析的核心操作。我们测试按地区统计销售额:
# Pandas result = df.groupby("region").agg( total_sales=("unit_price", "sum"), avg_quantity=("quantity", "mean") ) # Polars result = df.group_by("region").agg( pl.col("unit_price").sum().alias("total_sales"), pl.col("quantity").mean().alias("avg_quantity") )性能数据:
| 指标 | Pandas | Polars | 差异倍数 |
|---|---|---|---|
| 执行时间(s) | 3.2 | 0.9 | 3.6x |
| 峰值内存(GB) | 28.1 | 9.8 | 2.9x |
Polars的优势源于:
- 并行聚合:自动并行化groupby操作
- 哈希聚合:使用更高效的哈希表实现
- 延迟执行:优化整个计算图而非单步操作
3.3 表连接操作
我们模拟一个真实场景:将订单数据与生成的客户信息表关联。创建包含1000万客户的维度表:
customers = pl.DataFrame({ "customer_id": [fake.uuid4() for _ in range(10_000_000)], "customer_name": [fake.name() for _ in range(10_000_000)], "customer_tier": np.random.choice(["A", "B", "C"], size=10_000_000) })连接测试代码:
# Pandas joined = pd.merge(df, customers, on="customer_id", how="left") # Polars joined = df.join(customers, on="customer_id", how="left")性能对比:
| 连接类型 | Pandas耗时(s) | Polars耗时(s) |
|---|---|---|
| 左连接(1亿×1千万) | 62.4 | 8.7 |
Polars采用广播连接优化策略,自动选择最适合当前数据规模的连接算法。对于这个规模的连接操作,Polars比Pandas快7倍以上。
4. 高级特性与特殊场景
4.1 惰性执行模式
Polars独有的惰性API可以进一步优化复杂查询。将上述过滤、分组、连接组合成一个查询:
# Polars惰性执行 result = ( pl.scan_csv("ecommerce_100m.csv") .filter( (pl.col("order_date") >= "2023-01-01") & (pl.col("unit_price") * (1 - pl.col("discount")) > 500) ) .join( pl.scan_csv("customers_10m.csv"), on="customer_id", how="left" ) .group_by("region", "customer_tier") .agg( pl.col("unit_price").sum().alias("total_sales"), pl.col("quantity").mean().alias("avg_quantity") ) .collect() # 触发实际执行 )惰性执行的优势:
- 全局优化:分析整个查询计划后选择最优执行策略
- 减少中间结果:避免生成不必要的临时DataFrame
- 并行管道:多个操作可以流水线并行
在我们的测试中,惰性执行比立即执行模式快1.8倍,内存占用减少40%。
4.2 内存受限环境
对于内存不足1亿行数据的场景,两种库都提供了分块处理方案:
# Pandas分块处理 chunk_iter = pd.read_csv("ecommerce_100m.csv", chunksize=1_000_000) results = [] for chunk in chunk_iter: # 处理每个分块 filtered = chunk[chunk["unit_price"] > 500] results.append(filtered) final = pd.concat(results) # Polars流式处理 final = ( pl.scan_csv("ecommerce_100m.csv") .filter(pl.col("unit_price") > 500) .collect(streaming=True) # 流式模式 )内存占用对比:
| 处理方式 | 峰值内存(GB) |
|---|---|
| Pandas分块 | 2.1 |
| Polars流式 | 1.4 |
Polars的流式引擎可以保持恒定的低内存占用,而Pandas的分块处理会因为pd.concat操作导致内存波动。
5. 技术选型建议
根据测试结果,我们总结出以下决策矩阵:
| 考量维度 | Pandas优势场景 | Polars优势场景 |
|---|---|---|
| 小数据集 | 生态丰富,API熟悉度高 | 无明显优势 |
| 大数据集 | 需配合分块处理 | 原生高性能,自动并行 |
| 复杂查询 | 逐步调试方便 | 惰性执行优化明显 |
| 内存限制 | 需手动管理分块 | 流式处理内存效率高 |
| 团队技能 | 广泛普及,学习资源多 | 需要学习新API |
对于特定场景的推荐:
- 交互式分析:Pandas + Jupyter的即时反馈更适合探索性分析
- ETL流水线:Polars的批处理性能更适合生产环境数据管道
- 内存敏感型应用:Polars的流式处理是更安全的选择
- 已有代码迁移:Pandas 2.2的PyArrow后端可以低成本获得部分性能提升
在实际项目中,也可以组合使用两者——用Polars处理大规模数据阶段,将结果转为Pandas进行下游分析和可视化。这种混合模式兼顾了性能和易用性。