【Python】pandas vs polars 简单性能对比测试

Polars 这两年在 python 数据科学领域很火,性能号称能够碾压 pandas。今天我们设计一个简单的性能对比测试,看看 polars 是否存在显著优势。

实验目的

在 500 万行的数据集上,测 4 项常见数据操作的耗时,看看差距到底有多大。

我的电脑是 MacBook(M1 pro,16GB 内存),所以数据集不能太大,既要让差异显现出来,又要在合理时间内跑完,500 万行刚好。

实验设计

数据:用脚本生成合成数据,500 万行 × 8 列。包含一个主键,一个分组键(1000 个组),5 个数值列,1 个时间列。用 numpy 固定随机种子生成,先写入 CSV,然后 pandas 和 polars 各自从同一个文件读,保证数据一致性。

测试项(常见数据操作):

  1. 筛选 :value_0 > 500 且 group_id 是偶数
  2. 分组聚合 :按 group_id 分组,对所有数值列求 sum 和 mean
  3. 排序 :按 value_0 降序
  4. 滑动均值 :value_0 的窗口为 100 的滚动均值

计时方法 :用 time.perf_counter,每项跑 3 次取最快那次。

为啥不取平均?因为干扰因素(GC,系统调度,后台进程)只会让某次运行变慢,不会变快。最小值最接近算法的真实下界,而且天然不受离群值影响。

另外说明:读 CSV 的时间不计算在内,只测纯计算耗时。

环境:Python 3.13 / pandas 3.0.6 / polars 2.0.0。

完整代码

python 复制代码
import time
import os
import shutil
import numpy as np
import pandas as pd
import polars as pl

# 配置
N_ROWS = 5_000_000  # 500 万行
N_NUMERIC = 5       # 数值列数
N_GROUPS = 1_000    # 分组基数
DATA_DIR = "data"
CSV_PATH = os.path.join(DATA_DIR, "synthetic.csv")


# ==================== 数据准备 ====================

def generate_and_save() -> None:
    """生成合成数据集并保存为 CSV 文件。
    """
    os.makedirs(DATA_DIR, exist_ok=True)
    print(f"[数据准备] 生成 {N_ROWS:,} 行 x {N_NUMERIC + 3} 列 数据集...")

    rng = np.random.default_rng(seed=42)
    n = N_ROWS

    cols = {
        "id": np.arange(n, dtype=np.int64),
        "group_id": rng.integers(0, N_GROUPS, size=n).astype(np.int32),
    }
    for i in range(N_NUMERIC):
        cols[f"value_{i}"] = rng.random(n).astype(np.float64) * 1000

    df = pd.DataFrame(cols)
    df.to_csv(CSV_PATH, index=False)
    print(f"[数据准备] 已保存至 {CSV_PATH} (约 {os.path.getsize(CSV_PATH) / 1024**2:.0f} MB)")


# ==================== 基准测试框架 ====================

def run_bench(label: str, fn_pd: callable, fn_pl: callable, repeat: int = 3) -> dict:
    """对同一逻辑分别执行 Pandas 与 Polars 实现并计时对比。

    Args:
        label: 测试项名称。
        fn_pd: Pandas 实现函数,参数为 DataFrame。
        fn_pl: Polars 实现函数,参数为 DataFrame。
        repeat: 重复执行次数,取最快一次以消除抖动。
    Returns:
        包含 pandas/polars 耗时及加速比的字典。
    """
    times = {"pandas": [], "polars": []}

    for _ in range(repeat):
        # Pandas
        df_pd = pd.read_csv(CSV_PATH)
        t0 = time.perf_counter()
        try:
            fn_pd(df_pd)
            times["pandas"].append(time.perf_counter() - t0)
        except Exception as e:
            print(f"  [Pandas 异常] {e}")
            times["pandas"].append(float("nan"))

        # Polars
        df_pl = pl.read_csv(CSV_PATH)
        t0 = time.perf_counter()
        try:
            fn_pl(df_pl)
            times["polars"].append(time.perf_counter() - t0)
        except Exception as e:
            print(f"  [Polars 异常] {e}")
            times["polars"].append(float("nan"))

    # 排除失败项,取最小值
    pd_t = np.nanmin(times["pandas"]) if not all(np.isnan(t) for t in times["pandas"]) else float("nan")
    pl_t = np.nanmin(times["polars"]) if not all(np.isnan(t) for t in times["polars"]) else float("nan")

    return {"label": label, "pandas": pd_t, "polars": pl_t}


# ==================== 具体操作实现 ====================

# 1. 筛选 (Filter)

def filter_pd(df: pd.DataFrame) -> pd.DataFrame:
    """基于复杂条件的行筛选 (Pandas 实现)。"""
    return df[(df["value_0"] > 500) & (df["group_id"] % 2 == 0)]


def filter_pl(df: pl.DataFrame) -> pl.DataFrame:
    """基于复杂条件的行筛选 (Polars 实现)。"""
    return df.filter((pl.col("value_0") > 500) & (pl.col("group_id") % 2 == 0))


# 2. 分组聚合 (GroupBy + Aggregation)

def group_agg_pd(df: pd.DataFrame) -> pd.DataFrame:
    """按 group_id 分组并对所有数值列求和与均值 (Pandas 实现)。"""
    return df.groupby("group_id").agg(["sum", "mean"])


def group_agg_pl(df: pl.DataFrame) -> pl.DataFrame:
    """按 group_id 分组并对所有数值列求和与均值 (Polars 实现)。"""
    value_cols = [c for c in df.columns if c.startswith("value_")]
    return df.group_by("group_id").agg(
        [pl.col(c).sum().alias(f"{c}_sum") for c in value_cols]
        + [pl.col(c).mean().alias(f"{c}_mean") for c in value_cols]
    )


# 3. 排序 (Sort)

def sort_pd(df: pd.DataFrame) -> pd.DataFrame:
    """按 value_0 降序排序 (Pandas 实现)。"""
    return df.sort_values("value_0", ascending=False)


def sort_pl(df: pl.DataFrame) -> pl.DataFrame:
    """按 value_0 降序排序 (Polars 实现)。"""
    return df.sort("value_0", descending=True)


# 4. 窗口函数 (Rolling Mean)

def rolling_pd(df: pd.DataFrame) -> pd.DataFrame:
    """计算 value_0 的滑动均值,窗口大小 100 (Pandas 实现)。"""
    return df["value_0"].rolling(window=100).mean()


def rolling_pl(df: pl.DataFrame) -> pl.DataFrame:
    """计算 value_0 的滑动均值,窗口大小 100 (Polars 实现)。"""
    return df.with_columns(
        pl.col("value_0").rolling_mean(window_size=100).alias("rolling_mean")
    )


# ==================== 主流程 ====================

if __name__ == "__main__":
    generate_and_save()

    benchmarks = [
        ("筛选 (Filter)", filter_pd, filter_pl),
        ("分组聚合 (GroupBy + Agg)", group_agg_pd, group_agg_pl),
        ("排序 (Sort)", sort_pd, sort_pl),
        ("滑动均值 (Rolling Mean)", rolling_pd, rolling_pl),
    ]

    results = [run_bench(label, fn_pd, fn_pl) for label, fn_pd, fn_pl in benchmarks]

    # 输出汇总报告
    print("\n" + "=" * 55)
    print(f"{'基准测试结果汇总':^45}")
    print(f"{'(数据规模: 500万行, 取3次最快)':^45}")
    print("=" * 55)
    print(f"{'测试项':<28} {'Pandas (s)':>12} {'Polars (s)':>12} {'加速比':>8}")
    print("-" * 55)
    for r in results:
        speedup = r["pandas"] / r["polars"] if r["polars"] else float("nan")
        print(f"{r['label']:<28} {r['pandas']:>12.3f} {r['polars']:>12.3f} {speedup:>7.1f}x")
    print("=" * 55)

    # 清理临时数据
    shutil.rmtree(DATA_DIR)
    print("\n[清理] 已删除临时测试数据。")

结果分析

测试数据如下(每项 3 次取最快):

测试项 Pandas (s) Polars (s) 加速比
筛选 (Filter) 0.048 0.012 4.1x
分组聚合 (GroupBy + Agg) 0.189 0.111 1.7x
排序 (Sort) 1.076 0.182 5.9x
滑动均值 (Rolling Mean) 0.044 0.079 0.6x

解读:

  • 排序差距最大,约 5.9 倍。Polars 的排序是多线程并行的,而且列式内存布局对缓存更友好。pandas 底层的 NumPy 排序是单线程的,差距明显。
  • 筛选 4.1 倍。Polars 会把多个条件融合成一个表达式,一次性跑完,还能分片到多个核上处理。
  • 分组聚合 1.7 倍。推测是分组本身的哈希开销占了大头,这部分两边都躲不掉,差异就缩小了。
  • 滑动均值,pandas 反而更快。这是最反直觉的一个结果。pandas 的 rolling 是高度优化的 C 实现,单列计算已经很快。Polars 虽然也快,但并行调度的开销在这里收益不大,反而拖了后腿。

实验没兼顾的地方

这个实验比较粗糙,有几个明显的坑:

  • 规模单一。只有 500 万行,没做梯度测试。数据量更小或更大时,加速比可能完全不同。
  • 没测内存。Polars 的一大卖点是省内存,我只测了时间,没测峰值内存占用。
  • 忽略 I/O 和惰性求值。polars 的 scan_csv 能做谓词下推和投影下推,可能才是它真正的杀手锏。
  • 没测字符串和数据合并。字符串处理是 pandas 的传统弱项,Join 是实际开发的常客,本次均没有测试。
  • 没测超内存数据集。polars 的流失处理模式专门处理比内存大的数据,这是 pandas 做不到的。
相关推荐
苦瓜汤补钙1 小时前
Matlab 2022b(中文)的使用教程——Win11
开发语言·matlab
Ai-_Man1 小时前
我需要批量导出Poe左边栏所有对话,而不是一条具体的对话内容,你们AI导出鸭可以吗?
开发语言·前端·javascript·人工智能·小程序
学习论之费曼学习法1 小时前
无人值守AI内容矩阵实战(7):Playwright 发布器避坑——编辑器竞态、索引延迟与风控
python·线性代数·矩阵·编辑器·浏览器自动化·playwright·踩坑记录
福大大架构师每日一题1 小时前
rust 1.99.1 正式更新:聚焦 UEFI、嵌入式、x86_64 FFI 与 macOS 链接体验的三项修复
开发语言·macos·rust
wuyk5551 小时前
PyQt6-第06讲-主窗口框架
python·pyqt
Pin_Qing1 小时前
缓存能活多久,现在是个要算的参数
java·大数据·开发语言·人工智能·程序人生
小道士写程序1 小时前
Rust 元组详解:复合类型的基础
开发语言·rust
ss.li1 小时前
rppg相关论文阅读笔记10.22
论文阅读·笔记·python
tryCbest1 小时前
Python之Flask项目部署到阿里云
python·阿里云·flask