Polars の API に慣れるには、株価データの典型処理を一通り書いてみるのが近道です。本記事では、複数銘柄の価格テーブルに対して、with_columns / group_by / over / rolling を組み合わせて、リターン計算・移動平均・銘柄間の順位付けまでを実装します。
J-Quants 風のロング形式データ(Date, Code, C, Vo)を題材にします。
目次
- インストール
- サンプルデータ
- 銘柄ごとの単純リターン
- 移動平均(SMA)
- 累積最大値とドローダウン
- 銘柄ごとの統計サマリ
- 銘柄間の順位付け
- ワイド形式への変換(pivot)
- 遅延評価で大規模ファイルを処理する
- pandas からの移行ポイント
インストール
pip install polars pyarrow検証バージョン: Python 3.12.5 / Polars 1.7.1
サンプルデータ
3 銘柄 × 6 営業日の小さなロング形式データを作ります。実運用では J-Quants から取得した数十万〜数百万行を想定します。
import polars as pl
df = pl.DataFrame({ "Date": [ "2026-04-01", "2026-04-02", "2026-04-03", "2026-04-04", "2026-04-07", "2026-04-08", "2026-04-01", "2026-04-02", "2026-04-03", "2026-04-04", "2026-04-07", "2026-04-08", "2026-04-01", "2026-04-02", "2026-04-03", "2026-04-04", "2026-04-07", "2026-04-08", ], "Code": [ 7203, 7203, 7203, 7203, 7203, 7203, 9984, 9984, 9984, 9984, 9984, 9984, 8306, 8306, 8306, 8306, 8306, 8306, ], "C": [ 2900, 2925, 2880, 2910, 2945, 2960, 9800, 9750, 9900, 9850, 9820, 9880, 1450, 1448, 1462, 1470, 1480, 1495, ], "Vo": [ 11_000_000, 9_500_000, 12_500_000, 10_200_000, 9_800_000, 11_300_000, 4_200_000, 5_100_000, 4_800_000, 3_900_000, 4_400_000, 4_600_000, 18_000_000, 17_500_000, 19_200_000, 18_800_000, 17_900_000, 18_400_000, ],}).with_columns(pl.col("Date").str.to_date("%Y-%m-%d"))print(df.head(6))文字列の Date を str.to_date で Date 型に変換しておきます。後段の時系列処理で必須です。
銘柄ごとの単純リターン
pandas でいう df.groupby("Code")["C"].pct_change() を Polars では over で書きます。over("Code") は「Code の値が同じ行の中だけで処理を完結させる」という指定です。
df = df.with_columns( (pl.col("C") / pl.col("C").shift(1).over("Code") - 1) .alias("ret"))print(df)shift(1).over("Code") で「銘柄ごとの 1 行前の C(終値)」を取り、商を取って 1 を引くことでリターンになります。over を忘れると、銘柄をまたいだ shift になり、初日のリターンが前の銘柄の終値で計算されてしまいます。これは Polars でよくある事故ポイントです。
ソート順は重要です。Date が昇順になっていることを sort で保証してから処理に入るのが安全です。
df = df.sort(["Code", "Date"])移動平均(SMA)
Polars の rolling_mean を over と組み合わせると、銘柄ごとの移動平均が書けます。
df = df.with_columns([ pl.col("C").rolling_mean(window_size=3).over("Code").alias("sma_3"), pl.col("C").rolling_mean(window_size=5).over("Code").alias("sma_5"),])print(df.tail(8))ウィンドウ幅に満たない先頭部分は null になります。これは pandas の rolling().mean() と同じ挙動です。min_periods 引数で「最低 N 件あれば計算する」を指定できます。
rolling_std / rolling_max / rolling_min も同じ書き方で使えます。
累積最大値とドローダウン
cum_max と over を組み合わせると、銘柄ごとの「過去最高値」と「そこからの下落率」を計算できます。
df = df.with_columns([ pl.col("C").cum_max().over("Code").alias("running_max"),])df = df.with_columns( (pl.col("C") / pl.col("running_max") - 1).alias("drawdown"))print(df.select(["Code", "Date", "C", "running_max", "drawdown"]).tail(8))最大ドローダウン(MDD)は銘柄ごとに drawdown の最小値を取れば求まります。
mdd = df.group_by("Code").agg(pl.col("drawdown").min().alias("mdd"))print(mdd)銘柄ごとの統計サマリ
group_by + agg で、銘柄ごとの代表的な統計を一括で出します。
summary = df.group_by("Code").agg([ pl.len().alias("n_days"), pl.col("C").mean().alias("mean_close"), pl.col("ret").mean().alias("mean_ret"), pl.col("ret").std(ddof=1).alias("std_ret"), pl.col("Vo").sum().alias("sum_volume"),])print(summary)pl.len() は行数、pl.col("...").mean() のような式に alias を付けて結果列名を決めます。pandas の agg(新列名=("元列","関数")) よりも、式と名前のペアが一目で分かる構造です。
銘柄間の順位付け
「日ごとに、当日のリターンが大きい順に銘柄をランク付けする」処理は、over("Date") で書けます。
df = df.with_columns( pl.col("ret").rank(method="dense", descending=True).over("Date").alias("rank_today"))print(df.select(["Date", "Code", "ret", "rank_today"]).sort(["Date", "rank_today"]).head(9))over("Code") が「同じ銘柄の中で計算」なのに対し、over("Date") は「同じ日付の中で計算」です。横断的な集計を書きたいときに便利です。
ワイド形式への変換(pivot)
ロング → ワイドの変換は pivot です。#4-3「pandas で集計・グループ化」 の pandas と同じ発想ですが、書き味は短くまとまります。
wide = df.pivot(values="C", index="Date", on="Code")print(wide)on で「列に展開する元の列名」を指定します(古い API では columns でしたが、最近のバージョンでは on に統一されました)。
逆に、ワイド → ロングは unpivot です。
long = wide.unpivot(index="Date", on=[7203, 9984, 8306], variable_name="Code", value_name="C")print(long.head())遅延評価で大規模ファイルを処理する
ファイルサイズが大きいときは scan_csv と LazyFrame で書きます。
q = ( pl.scan_csv("prices.csv", try_parse_dates=True) .sort(["Code", "Date"]) .with_columns( (pl.col("C") / pl.col("C").shift(1).over("Code") - 1).alias("ret") ) .group_by("Code") .agg([ pl.col("ret").mean().alias("mean_ret"), pl.col("ret").std(ddof=1).alias("std_ret"), ]))result = q.collect()print(result)scan_csv は実際に読み込まずに「読み込む計画」を立てるだけで、collect() で初めて実行されます。途中で filter や select を挟めば、必要な列・行だけを読み込む最適化が行われます。
pandas からの移行ポイント
書き換えの定番パターンを表にまとめます。
| pandas | Polars |
|---|---|
df["x"] = expr | df = df.with_columns(expr.alias("x")) |
df[df["a"] > 0] | df.filter(pl.col("a") > 0) |
df.groupby("k")["x"].mean() | df.group_by("k").agg(pl.col("x").mean()) |
df.groupby("k")["x"].transform("mean") | pl.col("x").mean().over("k") |
df["x"].pct_change() | pl.col("x") / pl.col("x").shift(1) - 1 |
df["x"].rolling(5).mean() | pl.col("x").rolling_mean(5) |
df.merge(other, on="k") | df.join(other, on="k", how="inner") |
transform 相当が over になるのが、最も多く使う書き換えです。
生成AI へのプロンプト例
pandas のコードを Polars に書き換える依頼の型です。
次の pandas コードを Polars 1.7 系で書き換えてください。
[pandas]df = df.sort_values(["Code","Date"])df["ret"] = df.groupby("Code")["C"].pct_change()df["sma25"] = df.groupby("Code")["C"].transform(lambda s: s.rolling(25).mean())
要件:- with_columns と over を使う- ループ・apply は使わない- 入力 / 出力の列構成は同じ- 遅延評価は使わず、通常の DataFrame で書くover の必要性に気付かない生成結果が出たら、必ずレビューします。銘柄をまたいだリーク事故の温床になります。
まとめ
- 銘柄ごとの処理は
over("Code")、日付ごとの処理はover("Date") - リターン:
pl.col("C") / pl.col("C").shift(1).over("Code") - 1 - 移動平均:
pl.col("C").rolling_mean(window).over("Code") - 累積最大値とドローダウン:
cum_max().over("Code")と組み合わせ - 大規模データは
scan_csv+ LazyFrame で最適化される