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/元素] end

2.2 执行模型对比

特性 pandas Polars
执行模式 急切执行(Eager) 惰性执行(Lazy)+ 查询优化
并行度 单线程 多线程(Rayon)
中间对象 每步操作生成新 DataFrame 查询计划优化后一次执行
类型系统 NumPy dtype(object 兜底) Arrow 强类型(自动推断最优类型)
缺失值 float 列用 NaN,其他用 None 统一用 null(Arrow 原生支持)

三、性能基准测试与代码实践

3.1 数据加载与预处理

python 复制代码
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', periods=n_rows, freq='100ms'
        ),
    })


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, on='user_id', how='left')
    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, on='user_id', how='left')
    polars_time = time.perf_counter() - start

    return pandas_time, polars_time

3.2 基准测试结果(1000 万行数据)

操作 pandas 耗时 Polars 耗时 加速比
加载 + 预处理 + 聚合 12.3s 1.8s 6.8x
LEFT JOIN(500万 × 100万) 8.7s 1.2s 7.3x
窗口函数 groupby + transform 25.6s 2.1s 12.2x
字符串列过滤 + 聚合 15.4s 2.8s 5.5x
flowchart 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 的表达式 API(pl.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 数据分析的性能演进方向。

相关推荐
冬奇Lab3 小时前
Workflow 系列(06):安全——跨步骤注入传播与四层防御
人工智能·工作流引擎
冬奇Lab3 小时前
每日一个开源项目(第149篇):RAG-Anything - 把图片、表格、公式当成一等公民的多模态 RAG 框架
人工智能·开源
米小虾3 小时前
AI Agent 安全实战指南:当智能体开始"不听话",开发者该如何应对?
人工智能·安全·agent
IT_陈寒5 小时前
Vite的热更新突然不香了,排查三小时差点砸键盘
前端·人工智能·后端
阿里云大数据AI技术7 小时前
构建高转化海外电商搜索:阿里云OpenSearch行业算法版的全链路智能优化策略实战
人工智能·搜索引擎
Awu12277 小时前
⚡从零开发 Agent CLI(五)实现一个可治理、可扩展的工具系统
前端·人工智能·claude
字节跳动视频云技术团队7 小时前
让 Agent 成为音视频工作台:AI MediaKit CLI + Skill 发布
人工智能·音视频开发
魏祖潇7 小时前
framework 整合实战——DDD/TDD/SDD 三件套在 framework 仓的真实落地
人工智能·后端
Token炼金师8 小时前
去噪扩散:从随机噪声到高保真图像的数学之路
人工智能·aigc