pandas と Polars はどちらも DataFrame ライブラリですが、設計思想と得意領域が異なります。「速いから Polars」「資産があるから pandas」という単純な二者択一ではなく、データサイズ・周辺ライブラリの都合・チームの慣れに応じて使い分けるのが実務的です。

本記事では、同じ処理を pandas と Polars で書いてベンチマークし、選択基準と生成AI への依頼時の指定方法までを整理します。

目次

  1. インストール
  2. ベンチマーク用データの生成
  3. 共通の処理
  4. pandas 版の実装
  5. Polars 版の実装
  6. 計測の実行
  7. 計測結果(参考値)
  8. どちらを選ぶかの基準
  9. 落とし穴
  10. 生成AI への依頼で指定すべき項目

インストール

Terminal window
pip install pandas polars pyarrow

検証バージョン: Python 3.12.5 / pandas 2.2.3 / Polars 1.7.1

ベンチマーク用データの生成

ロング形式の株価データを 3 つのサイズで生成します。

import numpy as np
import pandas as pd
def make_prices(n_tickers: int, n_days: int) -> pd.DataFrame:
"""Code × Date のロング形式データを乱数で生成。"""
rng = np.random.default_rng(0)
codes = np.arange(1000, 1000 + n_tickers)
dates = pd.bdate_range("2020-01-01", periods=n_days)
df = pd.DataFrame({
"Date": np.tile(dates, n_tickers),
"Code": np.repeat(codes, n_days),
})
base = rng.normal(2000, 500, size=n_tickers).repeat(n_days)
walk = rng.normal(0, 20, size=len(df)).reshape(n_tickers, n_days).cumsum(axis=1).ravel()
df["C"] = (base + walk).round(2)
df["Vo"] = rng.integers(1_000_000, 20_000_000, size=len(df))
return df
small = make_prices(50, 250) # 1.25 万行
medium = make_prices(500, 1000) # 50 万行
large = make_prices(2000, 2000) # 400 万行
print(len(small), len(medium), len(large))

CSV に保存しておきます。後で Polars の scan_csv でも使うためです。

small.to_csv("prices_small.csv", index=False)
medium.to_csv("prices_medium.csv", index=False)
large.to_csv("prices_large.csv", index=False)

共通の処理

3 種類の典型処理で比較します。

ベンチ項目内容
Bench ACSV を読み込んで日付パース、銘柄ごとリターン計算
Bench B銘柄ごとの 25 日移動平均と 25 日標準偏差
Bench C銘柄横断ランキング(日次リターンの順位)

pandas 版の実装

import time
import pandas as pd
def bench_pandas(path: str) -> dict:
t0 = time.perf_counter()
df = pd.read_csv(path, parse_dates=["Date"])
df = df.sort_values(["Code", "Date"]).reset_index(drop=True)
df["ret"] = df.groupby("Code")["C"].pct_change()
t_a = time.perf_counter() - t0
t0 = time.perf_counter()
g = df.groupby("Code")["C"]
df["sma25"] = g.transform(lambda s: s.rolling(25).mean())
df["std25"] = g.transform(lambda s: s.rolling(25).std(ddof=1))
t_b = time.perf_counter() - t0
t0 = time.perf_counter()
df["rank_today"] = df.groupby("Date")["ret"].rank(method="dense", ascending=False)
t_c = time.perf_counter() - t0
return {"A_load_ret": t_a, "B_rolling": t_b, "C_rank": t_c, "total": t_a + t_b + t_c}

Polars 版の実装

import polars as pl
def bench_polars(path: str) -> dict:
t0 = time.perf_counter()
df = (
pl.read_csv(path, try_parse_dates=True)
.sort(["Code", "Date"])
.with_columns(
(pl.col("C") / pl.col("C").shift(1).over("Code") - 1).alias("ret")
)
)
t_a = time.perf_counter() - t0
t0 = time.perf_counter()
df = df.with_columns([
pl.col("C").rolling_mean(window_size=25).over("Code").alias("sma25"),
pl.col("C").rolling_std(window_size=25, ddof=1).over("Code").alias("std25"),
])
t_b = time.perf_counter() - t0
t0 = time.perf_counter()
df = df.with_columns(
pl.col("ret").rank(method="dense", descending=True).over("Date").alias("rank_today")
)
t_c = time.perf_counter() - t0
return {"A_load_ret": t_a, "B_rolling": t_b, "C_rank": t_c, "total": t_a + t_b + t_c}

計測の実行

results = []
for name, path in [("small", "prices_small.csv"),
("medium", "prices_medium.csv"),
("large", "prices_large.csv")]:
p = bench_pandas(path)
q = bench_polars(path)
results.append({"size": name, "lib": "pandas", **p})
results.append({"size": name, "lib": "polars", **q})
bench = pd.DataFrame(results)
print(bench)

計測結果(参考値)

筆者環境(M1 Pro / 16 GB / Python 3.12.5)での参考値です。絶対値は環境で変わるため、pandas / Polars の比 を見るのが実用的です。

size lib A_load_ret B_rolling C_rank total
0 small pandas 0.04 0.07 0.01 0.12
1 small polars 0.02 0.01 0.01 0.04
2 medium pandas 1.40 2.10 0.30 3.80
3 medium polars 0.30 0.10 0.10 0.50
4 large pandas 12.30 18.50 2.40 33.20
5 large polars 1.60 0.80 0.70 3.10

傾向としてはこのような違いが出ます。

  • データが小さいときは差が小さい(数百ミリ秒の違い)
  • 中規模(50 万行クラス) で 5〜10 倍の差がつく
  • 大規模(数百万行) では 10 倍前後に開く
  • rolling 系の処理 は Polars の優位が特に大きい(マルチスレッド + Rust 実装の恩恵)

過去の結果は環境・データ・ライブラリ版で変わります。本記事の数値は将来の性能を保証するものではありません。

どちらを選ぶかの基準

状況推奨
データが 10 万行未満で、可視化や ML との連携が中心pandas
数百万行以上の前処理・集計を頻繁に行うPolars
既存コード資産がほぼ pandaspandas をベース、ボトルネックだけ Polars
Parquet ファイルを大量に扱うPolars(scan_parquet が強力)
scikit-learn / statsmodels を多用するpandas(直接の入力に対応)
厳密な型管理を重視する分析パイプラインPolars

「Polars に全部寄せる」のはコストが高いので、ボトルネックの前処理だけ Polars にして、結果を to_pandas() で渡す段階移行が現実的です。

落とし穴

両者を併用するときに引っかかりやすい点を挙げます。

  • 欠損値の扱い: pandas の NaN と Polars の null は概念が異なる。Polars は整数列でも欠損を表現できる
  • インデックス: Polars には pandas のような行インデックスが無い。ソート済みかどうかは sort で明示
  • 時間帯: タイムゾーン付き日時の扱いはバージョン差がある。DateDatetime の使い分けを意識
  • API の安定性: Polars はバージョンによって API 名が変わることがある(例: pivot の引数 columnson)

生成AI への依頼で指定すべき項目

「どちらで書いてもらうか」は明示しないと、生成AI は学習データに多い pandas を返しがちです。次のような指定をテンプレ化しておくと迷いません。

このコードは Polars 1.7 系で書いてください。pandas は使わないでください。
要件:
- 銘柄ごとの 25 日移動平均と 25 日標準偏差を計算
- 入力: prices.csv(列 Date, Code, C)
- 出力: 同じ列 + sma25, std25 を加えた DataFrame
- group_by ではなく over を使う(transform 相当)
- 遅延評価(scan_csv + collect)を使う

逆に「pandas に書き換えてほしい」「両方書いて並べてほしい」もありです。

次の Polars コードと等価な pandas コードを書いてください。groupby + transform を使います。

まとめ

  • pandas と Polars は得意領域が異なり、データサイズ・周辺資産で使い分ける
  • 数百万行クラスでは Polars の優位が顕著で、特に rolling 系の処理で差がつく
  • 全面移行はコストが大きい。ボトルネックだけ Polars にする段階移行が実用的
  • 生成AI に書かせるときはライブラリ名・バージョン・API スタイルを必ず明示する