先週、日本株製造メーカー10銘柄×5年分の日足データでパラメータ最適化をしようとしたら、僕のノートPCが「ウィィィン」と唸り出して、そのまま30分近く固まりました。。。for文でパラメータを100通り回そうとしただけなのに。→さすがにこれは何かがおかしいと思って調べたら、「2026年はもうPandasじゃなくてPolarsの時代らしい」という情報にたどり着いたので、実際に乗り換えて速度を測ってみました。
正直、僕は「Pandas以外考えたことがない」くらいPandas一本でここまで来ました。子供が生まれてPythonを始めたときに最初に覚えたのがPandasだったので、愛着もあります。でも愛着と実用性は別問題ですよね。今回は「なぜ遅いのか」「Polarsに変えるとどれくらい速くなるのか」「どこまで書き換えが必要なのか」を、自分のバックテストコードで検証してみます。
そもそもなぜPandasは遅いのか
Pandasは内部的にNumPyの配列を使っていますが、1行1行の処理やfor文的な操作になると、Pythonのオブジェクトを介するオーバーヘッドが積み重なります。さらにシングルスレッドが基本なので、CPUのコアが余っていても使い切れません。バックテストの最適化のように「同じ処理を条件を変えて何百回も回す」ようなケースだと、この非効率が丸ごと何十倍にも増幅されてしまいます。
一方Polarsは、Rust実装+Apache Arrowのメモリレイアウトをベースにしていて、最初からマルチスレッド・遅延評価(Lazy Evaluation)を前提に設計されています。処理を「今すぐ実行する」のではなく「これから何をするか計画を立ててから、まとめて最適な順序で実行する」という発想なので、無駄な中間データの生成が少なく、メモリ効率も段違いです。
実際に速度を比較してみた
手元にある日本株の日足データ(10銘柄×5年分、約1万2000行)に、移動平均とボリンジャーバンドを計算してバックテストする処理をPandas版とPolars版で書いて、実行時間を比較しました。
# pip install polars --break-system-packages
import time
import pandas as pd
import polars as pl
# --- Pandas版 ---
def backtest_pandas(df: pd.DataFrame):
df["sma20"] = df["close"].rolling(20).mean()
df["std20"] = df["close"].rolling(20).std()
df["upper"] = df["sma20"] + 2 * df["std20"]
df["lower"] = df["sma20"] - 2 * df["std20"]
df["signal"] = (df["close"] < df["lower"]).astype(int)
return df
start = time.perf_counter()
for _ in range(100):
backtest_pandas(pandas_df.copy())
print(f"Pandas: {time.perf_counter() - start:.2f}秒")
# --- Polars版(LazyFrame)---
def backtest_polars(lf: pl.LazyFrame):
return lf.with_columns([
pl.col("close").rolling_mean(20).alias("sma20"),
pl.col("close").rolling_std(20).alias("std20"),
]).with_columns([
(pl.col("sma20") + 2 * pl.col("std20")).alias("upper"),
(pl.col("sma20") - 2 * pl.col("std20")).alias("lower"),
]).with_columns([
(pl.col("close") < pl.col("lower")).cast(pl.Int8).alias("signal"),
]).collect()
start = time.perf_counter()
for _ in range(100):
backtest_polars(polars_lf)
print(f"Polars: {time.perf_counter() - start:.2f}秒")
結果は、僕の環境(4コアのノートPC)でPandas版が約9.8秒、Polars版が約1.4秒でした。だいたい7倍くらいの差です。ネット上では「10倍〜50倍」という報告も見かけましたが、僕のデータ量(1万2000行程度)だとそこまで劇的ではなく、7倍でも十分ありがたい差でした。データ量がもっと大きくなる(分足データや複数年のティックデータなど)ほど差は開くようです。
乗り換えで詰まったポイント
いいことばかり書きましたが、実際に乗り換えるとPandasの感覚のまま書けない部分が結構ありました。特に僕がハマったのは以下の3つです。
1つ目は、インデックス(行ラベル)という概念がないこと。Pandasだと日付をindexにしてdf.loc["2026-07-01"]みたいに書けますが、Polarsでは日付も普通のカラムとして扱い、filterで絞り込みます。最初は「あれ、locがない」と焦りました。
2つ目は、applyで自作関数を回す書き方がほぼ推奨されないこと。Pandas感覚でループ的な処理を書こうとすると、Polarsの速さが台無しになります。pl.col()を使った式(Expression)ベースの書き方に慣れる必要があり、最初の数日はドキュメントとにらめっこでした。
3つ目は、yfinanceやj-Quantsなど既存のライブラリはPandasのDataFrameを返すものが多いので、結局どこかでpl.from_pandas()による変換が必要になること。全部を100%Polarsだけで完結させるのは、今のライブラリ事情だとまだ少し早い印象です。
まとめ:僕はどう使い分けることにしたか
結論として、データ取得(yfinance・j-Quants)はPandasのまま、パラメータ最適化のように同じ処理を大量に繰り返すバックテスト部分だけPolarsに置き換える、というハイブリッド構成に落ち着きました。全部を書き換える体力は正直なかったです。。。
子供が寝たあとの限られた時間でコードを回している身としては、待ち時間が7分の1になったのは本当に助かります。次は分足データでのウォークフォワード検証にもPolarsを使えるか試してみるつもりです。

