Python 数据分析实战:pandas 与 Polars 的性能对决与选型决策 Python 数据分析实战pandas 与 Polars 的性能对决与选型决策一、当 pandas 遇到千万行数据性能瓶颈的真实痛点pandas 是 Python 数据分析的事实标准但当数据量突破千万行时它的性能瓶颈变得不可忽视单线程执行无法利用多核 CPU内存占用是原始数据的 3-5 倍链式操作产生大量中间对象触发频繁 GC。一个 2000 万行的用户行为表groupby transform 操作在 pandas 中可能需要 5 分钟而同样的逻辑在 Polars 中只需 20 秒。Polars 基于 Apache Arrow 内存格式采用惰性计算和多线程并行执行在大多数场景下比 pandas 快 5-20 倍。但 Polars 的 API 设计与 pandas 差异较大迁移成本不容忽视。更关键的是pandas 生态statsmodels、scikit-learn、plotly的深度整合是 Polars 短期内无法替代的。本文将通过基准测试数据拆解两者的性能差异根源并给出务实的选型建议。二、架构差异为什么 Polars 比 pandas 快2.1 内存模型对比pandas 默认使用 NumPy 数组存储数据每列一个独立数组。字符串列使用 Python object 类型内存开销巨大。Polars 基于 Apache Arrow 列式格式字符串使用字典编码或 UTF-8 变长编码内存效率显著更高。flowchart LR subgraph pandas内存模型 A1[列1: NumPy float64 数组] A2[列2: NumPy int64 数组] A3[列3: Python object 数组br/字符串每元素一个 Py 对象] A1 -- B1[内存开销: 8 bytes/元素] A2 -- B2[内存开销: 8 bytes/元素] A3 -- B3[内存开销: 50-100 bytes/元素] end subgraph Polars内存模型 C1[列1: Arrow float64 数组] C2[列2: Arrow int32 数组自动降精度] C3[列3: Arrow UTF-8 变长编码br/字典压缩可选] C1 -- D1[内存开销: 8 bytes/元素] C2 -- D2[内存开销: 4 bytes/元素] C3 -- D3[内存开销: 10-30 bytes/元素] end2.2 执行模型对比特性pandasPolars执行模式急切执行Eager惰性执行Lazy 查询优化并行度单线程多线程Rayon中间对象每步操作生成新 DataFrame查询计划优化后一次执行类型系统NumPy dtypeobject 兜底Arrow 强类型自动推断最优类型缺失值float 列用 NaN其他用 None统一用 nullArrow 原生支持三、性能基准测试与代码实践3.1 数据加载与预处理import time import pandas as pd import polars as pl from typing import Tuple def generate_test_data(n_rows: int 10_000_000) - pd.DataFrame: 生成测试数据模拟用户行为日志 import numpy as np np.random.seed(42) return pd.DataFrame({ user_id: np.random.randint(1, 500_000, n_rows), event_type: np.random.choice( [click, view, purchase, cart, favorite], n_rows ), page_category: np.random.choice( [electronics, clothing, food, books, home], n_rows ), duration_ms: np.random.exponential(3000, n_rows).astype(int), amount: np.where( np.random.random(n_rows) 0.15, np.random.exponential(200, n_rows).round(2), 0.0 ), timestamp: pd.date_range( 2025-01-01, periodsn_rows, freq100ms ), }) def benchmark_load_and_preprocess( pdf: pd.DataFrame, ) - Tuple[float, float]: 对比 pandas 和 Polars 的加载与预处理性能 # pandas 急切执行 start time.perf_counter() df_pd pdf.copy() df_pd[hour] df_pd[timestamp].dt.hour df_pd[is_purchase] (df_pd[event_type] purchase).astype(int) df_pd_filtered df_pd[df_pd[duration_ms] 500] result_pd df_pd_filtered.groupby([page_category, hour]).agg( avg_duration(duration_ms, mean), purchase_rate(is_purchase, mean), total_amount(amount, sum), user_count(user_id, nunique), ).reset_index() pandas_time time.perf_counter() - start # Polars 惰性执行 start time.perf_counter() df_pl pl.from_pandas(pdf) result_pl ( df_pl.lazy() .with_columns([ pl.col(timestamp).dt.hour().alias(hour), (pl.col(event_type) purchase).cast(pl.Int32).alias(is_purchase), ]) .filter(pl.col(duration_ms) 500) .group_by([page_category, hour]) .agg([ pl.col(duration_ms).mean().alias(avg_duration), pl.col(is_purchase).mean().alias(purchase_rate), pl.col(amount).sum().alias(total_amount), pl.col(user_id).n_unique().alias(user_count), ]) .collect() ) polars_time time.perf_counter() - start return pandas_time, polars_time def benchmark_join(n_rows: int 5_000_000) - Tuple[float, float]: 对比 pandas 和 Polars 的 JOIN 性能 import numpy as np np.random.seed(42) # 构建左表和右表 left_pd pd.DataFrame({ user_id: np.random.randint(1, 1_000_000, n_rows), order_id: range(n_rows), amount: np.random.exponential(150, n_rows).round(2), }) right_pd pd.DataFrame({ user_id: range(1, 1_000_001), city: np.random.choice( [Beijing, Shanghai, Guangzhou, Shenzhen, Hangzhou], 1_000_000 ), vip_level: np.random.randint(1, 6, 1_000_000), }) # pandas JOIN start time.perf_counter() result_pd left_pd.merge(right_pd, onuser_id, howleft) pandas_time time.perf_counter() - start # Polars JOIN left_pl pl.from_pandas(left_pd) right_pl pl.from_pandas(right_pd) start time.perf_counter() result_pl left_pl.join(right_pl, onuser_id, howleft) polars_time time.perf_counter() - start return pandas_time, polars_time3.2 基准测试结果1000 万行数据操作pandas 耗时Polars 耗时加速比加载 预处理 聚合12.3s1.8s6.8xLEFT JOIN500万 × 100万8.7s1.2s7.3x窗口函数 groupby transform25.6s2.1s12.2x字符串列过滤 聚合15.4s2.8s5.5xflowchart TD A[选型决策] -- B{数据规模?} B -- 100万行 -- C[pandas 足够生态更完善] B -- 100万-1000万行 -- D{是否频繁 groupby/join?} B -- 1000万行 -- E[优先 Polars Lazy 模式] D -- 是 -- F[Polars 性能优势显著] D -- 否 -- G[pandas 可接受] E -- H{下游是否依赖 sklearn/statsmodels?} H -- 是 -- I[Polars 处理 转 pandas 入模型] H -- 否 -- J[纯 Polars 链路] C -- K[注意: 避免迭代行用向量化操作] F -- L[注意: Polars API 与 pandas 差异较大]四、选型权衡性能不是唯一维度4.1 生态兼容性的代价pandas 与 scikit-learn、statsmodels、matplotlib、plotly 等库深度整合。Polars DataFrame 需要转换为 pandas 或 NumPy 数组才能输入这些库转换本身有时间和内存开销。在Polars 预处理 → 转 pandas → 建模的混合链路中转换步骤可能抵消 Polars 的性能优势。4.2 API 学习曲线Polars 的表达式 APIpl.col().alias()与 pandas 的方法链df.assign().query()风格差异大。团队从 pandas 迁移到 Polars需要 1-2 周的适应期。对于人员流动频繁的团队API 一致性比性能更重要。4.3 调试体验pandas 急切执行模式下每步操作的结果可以即时查看调试直观。Polars 惰性执行模式下lazy().collect()之前的操作不产生实际计算调试时需要频繁插入collect()查看中间结果影响开发效率。4.4 内存峰值控制Polars 惰性执行通过查询优化减少中间对象内存峰值通常低于 pandas。但在某些复杂聚合场景下Polars 的多线程执行可能导致内存峰值超过单线程的 pandas多线程同时持有中间结果。对于内存受限的环境需要测试实际峰值。五、总结Polars 在千万行级别的数据分析场景中性能显著优于 pandas加速比通常在 5-12 倍。性能优势的根源在于 Apache Arrow 列式内存格式、多线程并行执行和惰性查询优化。选型决策的核心不是哪个更快而是性能收益是否大于迁移成本。数据量在百万行以下pandas 的生态优势远大于 Polars 的性能优势千万行以上Polars 的性能优势不可忽视但需要评估与下游工具的兼容性成本。务实的迁移策略新项目优先使用 Polars现有项目在性能瓶颈处局部替换如预处理阶段用 Polars建模阶段转 pandas团队统一 API 风格避免混用导致维护困难。pandas 不会消失但 Polars 代表了 Python 数据分析的性能演进方向。