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

相关推荐
邵宇然1 小时前
llama.cpp 多模态推理优化:从视觉编码器到跨模态注意力的高效部署实践
人工智能
码农天天1 小时前
从云端走向端侧:解读 AI 硬件与应用形态的迭代之路
人工智能
love530love1 小时前
2026年终极防坑指南:基于 EPGF 架构彻底“本地化” UV 环境与工具
人工智能·windows·python·架构·devops·uv·epgf
糖果店的幽灵1 小时前
AI 驱动 Selenium 测试框架最佳实践:从传统自动化到智能体测试
人工智能·selenium·自动化
人民新视野1 小时前
2026美墨加世界杯伊朗VS新西兰预测分析亚洋二线实力大比拼
人工智能
qq_411262421 小时前
四博智联AI开发宝典(2/3):后端部署、OTA与AT+MCP接入
人工智能·ai·四博
QiLinkOS1 小时前
极客精神与商业思维的融合实践(2)
c语言·c++·人工智能·算法·开源协议
逻辑君1 小时前
认知神经科学研究报告【20260071】
人工智能·深度学习·机器学习·数学建模
Eloudy1 小时前
伊辛解码(Ising Decoding)
人工智能·量子计算