ボラティリティ(価格変動の大きさ)は、リターンと並ぶ定量分析の二大指標の一つです。本記事は、日次リターンの標準偏差を年率換算した「ヒストリカル・ボラティリティ」で銘柄をスクリーニングし、結果をリターンとの散布図で比較するまでを通しで公開します。

過去の集計結果は将来の値動きを保証しません。

目次

  1. ボラティリティの定義
  2. 集計のゴール
  3. 必要なライブラリ
  4. コード(コピペで動く)
  5. 実行結果(イメージ)
  6. スクリーニング条件の組み立て
  7. チャートで「触って探る」
  8. 落とし穴

ボラティリティの定義

本記事ではヒストリカル・ボラティリティを次で計算します。

σannual=std(rt)×252\sigma_{\text{annual}} = \text{std}(r_t) \times \sqrt{252}
  • rtr_t: 日次の対数リターン
  • 252: 年間の営業日数のおおよその値
  • 標本標準偏差(ddof=1)を使う

「直近 1 年(252 営業日)」を計算窓の基本にします。窓を変えると結果も変わるため、複数の窓で並行して見るのが定量分析の定石です。

集計のゴール

出力内容
low_vol_top30.csv過去 252 営業日のボラティリティが低い銘柄 30
risk_return.png銘柄ごとの (年率リターン, 年率ボラ) 散布図

必要なライブラリ

Terminal window
pip install pandas numpy matplotlib

検証バージョン: Python 3.12.5 / pandas 2.2.3 / numpy 2.0 / matplotlib 3.9

コード(コピペで動く)

"""low_volatility.py
銘柄ごとに直近 252 営業日のヒストリカル・ボラティリティと年率リターンを計算し、
低ボラティリティ・トップ 30 を抽出する。
"""
from __future__ import annotations
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
PRICES_PATH = "prices.csv" # Code, Date, C
LISTED_PATH = "listed_info.csv" # Code, CoName, S33Nm
WINDOW = 252
TOP_N = 30
def compute_metrics(df: pd.DataFrame) -> pd.DataFrame:
"""Code ごとに対数リターンの統計量を計算し、年率指標を返す。"""
df = df.sort_values(["Code", "Date"]).copy()
df["log_ret"] = np.log(df["C"] / df.groupby("Code")["C"].shift(1))
# 直近 WINDOW 営業日に絞る(銘柄ごと)
df = df.dropna(subset=["log_ret"])
df = df.groupby("Code", group_keys=False).tail(WINDOW)
grouped = df.groupby("Code")["log_ret"]
out = pd.DataFrame({
"n": grouped.size(),
"ret_daily_mean": grouped.mean(),
"ret_daily_std": grouped.std(ddof=1),
}).reset_index()
out["ret_annual"] = out["ret_daily_mean"] * 252
out["vol_annual"] = out["ret_daily_std"] * np.sqrt(252)
return out
def main() -> None:
prices = pd.read_csv(PRICES_PATH, parse_dates=["Date"])
listed = pd.read_csv(LISTED_PATH)[["Code", "CoName", "S33Nm"]]
metrics = compute_metrics(prices)
# 集計に必要な営業日が揃っている銘柄だけ採用(上場直後を弾く)
metrics = metrics[metrics["n"] >= WINDOW * 0.9]
merged = metrics.merge(listed, on="Code")
low_vol = (
merged.sort_values("vol_annual")
.head(TOP_N)
.reset_index(drop=True)
)
low_vol["vol_annual_pct"] = (low_vol["vol_annual"] * 100).round(2)
low_vol["ret_annual_pct"] = (low_vol["ret_annual"] * 100).round(2)
print(low_vol[["Code", "CoName", "S33Nm", "ret_annual_pct", "vol_annual_pct"]])
low_vol.to_csv("low_vol_top30.csv", index=False)
# リスク・リターン散布図
fig, ax = plt.subplots(figsize=(8, 6))
ax.scatter(merged["vol_annual"] * 100, merged["ret_annual"] * 100, s=8, alpha=0.4, label="all")
ax.scatter(low_vol["vol_annual"] * 100, low_vol["ret_annual"] * 100, s=30, color="crimson", label="low-vol top30")
ax.set_xlabel("Volatility (annualized, %)")
ax.set_ylabel("Return (annualized, %)")
ax.set_title("Risk vs Return — last 252 trading days")
ax.axhline(0, color="gray", linewidth=0.8)
ax.grid(alpha=0.3)
ax.legend()
plt.tight_layout()
plt.savefig("risk_return.png", dpi=120)
plt.close(fig)
if __name__ == "__main__":
main()
低ボラティリティ・トップ30のリスク・リターン散布図

実行結果(イメージ)

Code CoName S33Nm ret_annual_pct vol_annual_pct
0 XXXX サンプル食品A 食料品 4.32 12.45
1 XXXX サンプル食品B 食料品 5.10 13.22
2 XXXX サンプル医薬A 医薬品 3.85 13.50
3 XXXX サンプル鉄道A 陸運業 2.10 13.95
...

低ボラティリティ側には食料品・医薬品・陸運業など、業績が比較的安定する業種が並びがちです。

risk_return.png には、全銘柄のグレーの点と低ボラ・トップ 30 の赤い点が描かれます。低ボラ側に集まりつつ、リターンが正・負に分散している様子が見て取れるはずです。

スクリーニング条件の組み立て

低ボラだけで終わらせず、複数条件を組み合わせるのがスクリーニングの基本です。

condition = (
(merged["vol_annual"] < 0.20) # ボラ 20% 未満
& (merged["ret_annual"] > 0.0) # リターン プラス
& (merged["n"] >= WINDOW) # 営業日が揃っている
)
filtered = merged[condition].sort_values("vol_annual")

条件の数は 小さく始めて 1 つずつ追加 します。最初から多重条件を組むと、結果が 0 件になった原因を切り分けにくくなります。

チャートで「触って探る」

低ボラ・トップ 30 の銘柄は数が多いので、Plotly でホバー表示できる散布図にしておくと、後で振り返るときに名前と業種を確認しやすくなります。Plotly 版の散布図は#12-2「Plotly 応用 — リターン分布・相関行列・ドローダウンの可視化」 で扱います。

落とし穴

  • 窓のサイズ依存: 60 日 / 252 日 / 504 日 で順位がかなり変わります
  • 上場期間が短い銘柄: 観測数が足りない銘柄は標準偏差が信頼できません。n でフィルタする
  • 取引停止・ストップ高安: 連日同値や寄らないストップが続くと、ボラが過小評価される
  • 対数 vs 単純: 短期ならほぼ同じ。長期は対数の方が累積リターンと整合的
  • 生存バイアス: 上場廃止銘柄を含まないデータで集計すると、低ボラ・高リターン側に楽観的なバイアスがかかる

まとめ

  • ヒストリカル・ボラティリティは「日次リターン標準偏差 × √252」で年率化
  • 低ボラ・トップ 30 は食料品・医薬品など安定業種が並びやすい
  • リスク・リターン散布図でランキングと全体像を同時に把握する
  • 窓サイズ・データ期間・対数 / 単純の選択を記録する

過去のボラティリティは将来の値動きを保証しません。