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 数据分析的性能演进方向。

相关推荐
liuyunshengsir7 小时前
从 TVM 到 TileLang:一文读懂深度学习编译器为什么走向 Tile 化
人工智能·深度学习·tvm·tilelang
海天一色y7 小时前
GSPO:重新定义大语言模型的强化学习训练范式
人工智能·机器学习·语言模型
云和数据.ChenGuang7 小时前
fastapi项目拆分实战数据模型
java·服务器·数据库·人工智能·深度学习·fastapi·强化学习
watersink8 小时前
机器学习HMM
人工智能·机器学习
东方小月8 小时前
从零开发一个 Coding Agent(九):实现 Agent 的工具调用闭环
人工智能·前端框架·node.js
Luhui Dev8 小时前
如何在 WorkBuddy 中使用大角几何:从 MCP 接入到 AI 几何作图
人工智能·数学·算法·agent·luhuidev
孙启超8 小时前
【AI应用开发】什么是混合检索(Hybrid Search)?向量检索 + BM25 关键词检索,适用场景与 RRF 融合原理
人工智能·缓存·llm·向量数据库·bm25·向量化·ai应用开发
The moon forgets8 小时前
Qwen团队提出Ego2Robot, 第一人称视频助力具身VLA训练新数据
人工智能·机器学习·音视频
szxinmai主板定制专家8 小时前
基于 RK3588 + Xilinx Kintex-7 FPGA 异构工业主控板设计方案
arm开发·人工智能·嵌入式硬件·fpga开发·zynq