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) |
|---|---|---|
| pandas | 32.192 | 3505.9 |
| Polars(eager) | 11.984 | 4581.0 |
| Polars(lazy) | 4.698 | 1001.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 は強力な選択肢

コメント