スポンサーリンク
Python

📘 Pythonでpandas と Polars の CSV 読み込み速度を徹底比較

Python
この記事は約10分で読めます。

20,000,000 行 × 22 列の巨大データで本気のベンチマーク

大規模データを扱うとき、pandas の処理速度やメモリ使用量に悩む場面は多いです。 最近は「Polars が速い」という話題をよく耳にしますが、実際どれくらい違うのか気になったので、2,000 万行の巨大 CSV を自前で生成し、pandas / Polars の読み込み速度とメモリ使用量を比較してみました。

🐻 Polars とは?読み方は「ポーラーズ」

Polars は Rust で実装された高速データフレームライブラリです。 読み方は 「ポーラーズ」

主な特徴は次の通り。

  • Rust + Apache Arrow ベースでとにかく高速
  • マルチスレッドで CPU をフル活用
  • メモリ効率が良い(pandas の 1/2〜1/4)
  • Lazy(遅延評価)による最適化が強力
  • pandas 互換 API(polars-lts)も登場し移行が容易に

pandas が Python オブジェクトを大量に作るのに対し、 Polars は Arrow のカラムナ型メモリを使うため、構造的に高速です。

⚙ eager モードと lazy モードの違い

Polars には 2 つの実行モードがあります。

eager(イージャー)

  • pandas と同じ「即時実行」
  • pl.read_csv() でその場で DataFrame を構築
  • 単純な読み込みや軽い処理に向く
  • 読み込みベンチマークでは最速になりやすい

lazy(レイジー)

  • 遅延評価(SQL のクエリ最適化のようなもの)
  • pl.scan_csv().collect() で実行
  • filter / groupby / join などの複雑処理で最適化が効く
  • 大規模 ETL では eager より速くなることも多い

今回のような「読み込みだけ」のベンチマークでも、lazy が最速でした。

🏗️ 検証データの生成(20,000,000 行 × 22 列)

今回の検証では、Polars を使って 2,000 万行 × 22 列の巨大 CSV を生成しました。

  • 数値列 20 本
  • category 列
  • text 列 → 合計 22 列

🧪 実際に使用したベンチマークコード

以下が今回の検証で実際に使用したコードです。 巨大 CSV の生成 → pandas / Polars の読み込み速度とメモリ使用量の計測 → Markdown レポート生成まで一気通貫で動きます。

import time
import os
import psutil
import pandas as pd
import polars as pl
from datetime import datetime

# ============================================
# 1. 巨大 CSV を生成(20,000,000 行 × 22 列)
# ============================================
# Polars の DataFrame を使って巨大 CSV を作成します。
# ・id 列(0〜n-1)
# ・num_0〜num_19(id に +i した数値列)
# ・category(id % 10)
# ・text_col(固定文字列)
# 合計 22 列のデータを生成します。

def generate_csv(path, n=20_000_000, num_cols=20):
    # id 列を Series で作成(Expr を避けて安定動作)
    df = pl.DataFrame({
        "id": pl.Series(range(n))
    })

    # 数値列を追加(num_0〜num_19)
    for i in range(num_cols):
        df = df.with_columns((pl.col("id") + i).alias(f"num_{i}"))

    # カテゴリ列(0〜9 の文字列)
    df = df.with_columns(((pl.col("id") % 10).cast(pl.Utf8)).alias("category"))

    # 文字列列(全行 "sample_text")
    df = df.with_columns(pl.Series(["sample_text"] * n).alias("text_col"))

    # CSV 出力
    df.write_csv(path)


# ============================================
# 2. メモリ使用量取得(MB)
# ============================================
# psutil を使って現在プロセスの RSS を取得します。
# 読み込み前後の差分を取ることで、各ライブラリが
# どれだけメモリを消費したかを測定できます。

def get_memory_mb():
    process = psutil.Process(os.getpid())
    return process.memory_info().rss / 1024 / 1024


# ============================================
# 3. ベンチマーク(読み込み時間+メモリ使用量)
# ============================================
# pandas / Polars(eager / lazy)の 3 パターンで
# CSV 読み込み時間とメモリ使用量を比較します。

def benchmark_read(csv_path):
    results = {}

    # pandas
    mem_before = get_memory_mb()
    start = time.time()
    df_pd = pd.read_csv(csv_path)
    results["pandas"] = {
        "time": time.time() - start,
        "memory": get_memory_mb() - mem_before
    }

    # Polars eager(即時実行)
    mem_before = get_memory_mb()
    start = time.time()
    df_pl = pl.read_csv(csv_path)
    results["polars_eager"] = {
        "time": time.time() - start,
        "memory": get_memory_mb() - mem_before
    }

    # Polars lazy(遅延実行 → collect で実行)
    mem_before = get_memory_mb()
    start = time.time()
    df_lazy = pl.scan_csv(csv_path).collect()
    results["polars_lazy"] = {
        "time": time.time() - start,
        "memory": get_memory_mb() - mem_before
    }

    return results


# ============================================
# 4. Markdown レポート生成
# ============================================
# 実行結果を Markdown 形式でまとめます。
# ブログやドキュメントにそのまま貼れる形式です。

def generate_markdown(results, csv_path, n, num_cols):
    now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")

    md = f"""
# CSV 読み込みベンチマークレポート
実行日時: {now}

## 📊 対象データ
- 行数: {n:,} 行
- 列数: {num_cols + 2} 列(数値 {num_cols} + category + text)
- ファイル: `{csv_path}`

## ⚡ 読み込み速度比較

| ライブラリ | 時間(秒) | メモリ使用量(MB) |
|-----------|------------|---------------------|
| pandas | {results["pandas"]["time"]:.3f} | {results["pandas"]["memory"]:.1f} |
| polars(eager) | {results["polars_eager"]["time"]:.3f} | {results["polars_eager"]["memory"]:.1f} |
| polars(lazy) | {results["polars_lazy"]["time"]:.3f} | {results["polars_lazy"]["memory"]:.1f} |

"""

    return md


# ============================================
# 5. 実行(CSV 生成 → ベンチマーク → レポート出力)
# ============================================

csv_path = "test_big.csv"
n = 20_000_000
num_cols = 20

# 巨大 CSV を生成
generate_csv(csv_path, n=n, num_cols=num_cols)

# ベンチマーク実行
results = benchmark_read(csv_path)

# Markdown レポート生成
markdown_report = generate_markdown(results, csv_path, n, num_cols)

# ファイル出力
with open("benchmark_report.md", "w", encoding="utf-8") as f:
    f.write(markdown_report)

print("Markdownレポートを生成しました: benchmark_report.md")

📊 ベンチマーク結果(時間+メモリ)

実際に 20,000,000 行の CSV を読み込んだ結果がこちらです。

ライブラリ時間(秒)メモリ使用量(MB)
pandas32.1923505.9
Polars(eager)11.9844581.0
Polars(lazy)4.6981001.9

🔍 結果の読み解き

🐼 pandas

  • 読み込みに 32 秒
  • メモリ使用量は 約 3.5GB
  • Python オブジェクトを大量に生成するため、行数が増えるほど線形に遅くなる
  • メモリ効率も悪く、大規模 CSV では負荷が大きい

🐻 Polars(eager)

  • 読み込みに 約 12 秒
  • pandas より速いが、今回のケースでは メモリ使用量が最も大きい(約 4.5GB)
🔥 なぜ eager がメモリを多く使うのか?

理由は 「即時実行で全データを一気に Arrow メモリへ構築するため」 です。

具体的には:

  • eager は CSV を読み込んだ瞬間に 全列を Arrow のカラムナ型メモリへ展開する
  • その際、パース用バッファ + Arrow カラム + 内部最適化用の一時領域が同時に存在する
  • 巨大 CSV ではこの「同時に持つメモリ」が増えやすい
  • lazy は必要な部分だけ読み込むため、メモリ使用量が抑えられる

つまり、

eager は「最速で DataFrame を作るためにメモリを積極的に使う」 lazy は「最適化しながら必要な部分だけ読むのでメモリ効率が良い」

という構造的な違いが原因と思われます。

🐻⚡ Polars(lazy)

  • 読み込みに 4.7 秒(今回の最速)
  • メモリ使用量は 約 1GB と圧倒的に少ない
  • lazy は「遅延評価」で、必要な部分だけ最適化して読み込む
  • 大規模 ETL では eager より高速になることが多い
  • 今回のような巨大 CSV 読み込みでも 速度・メモリともに最も優秀

🐼 pandas と比べたときのデメリット

Polars は強力ですが、万能ではありません。

  • pandas ほどのエコシステムはまだない
  • 行方向の複雑処理は pandas の方が書きやすい
  • 既存コードが pandas 前提だと移行コストがある

📝 まとめ

  • Polars lazy は 最速(4.7 秒)かつ最小メモリ(1GB)
  • eager は速いが メモリを多く使う構造的理由がある
  • pandas は速度・メモリともに厳しい
  • pandas → Polars の移行は以前よりずっと簡単
  • Polars → pandas の変換も可能だが巨大データでは注意
  • 大規模データを扱うなら Polars は強力な選択肢

コメント

タイトルとURLをコピーしました