pandas と Polars はどちらも DataFrame ライブラリですが、設計思想と得意領域が異なります。「速いから Polars」「資産があるから pandas」という単純な二者択一ではなく、データサイズ・周辺ライブラリの都合・チームの慣れに応じて使い分けるのが実務的です。
本記事では、同じ処理を pandas と Polars で書いてベンチマークし、選択基準と生成AI への依頼時の指定方法までを整理します。
目次
- インストール
- ベンチマーク用データの生成
- 共通の処理
- pandas 版の実装
- Polars 版の実装
- 計測の実行
- 計測結果(参考値)
- どちらを選ぶかの基準
- 落とし穴
- 生成AI への依頼で指定すべき項目
インストール
pip install pandas polars pyarrow検証バージョン: Python 3.12.5 / pandas 2.2.3 / Polars 1.7.1
ベンチマーク用データの生成
ロング形式の株価データを 3 つのサイズで生成します。
import numpy as npimport 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 A | CSV を読み込んで日付パース、銘柄ごとリターン計算 |
| Bench B | 銘柄ごとの 25 日移動平均と 25 日標準偏差 |
| Bench C | 銘柄横断ランキング(日次リターンの順位) |
pandas 版の実装
import timeimport 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 total0 small pandas 0.04 0.07 0.01 0.121 small polars 0.02 0.01 0.01 0.042 medium pandas 1.40 2.10 0.30 3.803 medium polars 0.30 0.10 0.10 0.504 large pandas 12.30 18.50 2.40 33.205 large polars 1.60 0.80 0.70 3.10傾向としてはこのような違いが出ます。
- データが小さいときは差が小さい(数百ミリ秒の違い)
- 中規模(50 万行クラス) で 5〜10 倍の差がつく
- 大規模(数百万行) では 10 倍前後に開く
- rolling 系の処理 は Polars の優位が特に大きい(マルチスレッド + Rust 実装の恩恵)
過去の結果は環境・データ・ライブラリ版で変わります。本記事の数値は将来の性能を保証するものではありません。
どちらを選ぶかの基準
| 状況 | 推奨 |
|---|---|
| データが 10 万行未満で、可視化や ML との連携が中心 | pandas |
| 数百万行以上の前処理・集計を頻繁に行う | Polars |
| 既存コード資産がほぼ pandas | pandas をベース、ボトルネックだけ Polars |
| Parquet ファイルを大量に扱う | Polars(scan_parquet が強力) |
| scikit-learn / statsmodels を多用する | pandas(直接の入力に対応) |
| 厳密な型管理を重視する分析パイプライン | Polars |
「Polars に全部寄せる」のはコストが高いので、ボトルネックの前処理だけ Polars にして、結果を to_pandas() で渡す段階移行が現実的です。
落とし穴
両者を併用するときに引っかかりやすい点を挙げます。
- 欠損値の扱い: pandas の
NaNと Polars のnullは概念が異なる。Polars は整数列でも欠損を表現できる - インデックス: Polars には pandas のような行インデックスが無い。ソート済みかどうかは
sortで明示 - 時間帯: タイムゾーン付き日時の扱いはバージョン差がある。
DateとDatetimeの使い分けを意識 - API の安定性: Polars はバージョンによって API 名が変わることがある(例:
pivotの引数columns→on)
生成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 スタイルを必ず明示する