Polars の API に慣れるには、株価データの典型処理を一通り書いてみるのが近道です。本記事では、複数銘柄の価格テーブルに対して、with_columns / group_by / over / rolling を組み合わせて、リターン計算・移動平均・銘柄間の順位付けまでを実装します。

J-Quants 風のロング形式データ(Date, Code, C, Vo)を題材にします。

目次

  1. インストール
  2. サンプルデータ
  3. 銘柄ごとの単純リターン
  4. 移動平均(SMA)
  5. 累積最大値とドローダウン
  6. 銘柄ごとの統計サマリ
  7. 銘柄間の順位付け
  8. ワイド形式への変換(pivot)
  9. 遅延評価で大規模ファイルを処理する
  10. pandas からの移行ポイント

インストール

Terminal window
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_dateDate 型に変換しておきます。後段の時系列処理で必須です。

銘柄ごとの単純リターン

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_meanover と組み合わせると、銘柄ごとの移動平均が書けます。

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_maxover を組み合わせると、銘柄ごとの「過去最高値」と「そこからの下落率」を計算できます。

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() で初めて実行されます。途中で filterselect を挟めば、必要な列・行だけを読み込む最適化が行われます。

pandas からの移行ポイント

書き換えの定番パターンを表にまとめます。

pandasPolars
df["x"] = exprdf = 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 で最適化される