ローリングβをヒートマップで可視化する

7.2.5

ローリングβをヒートマップで可視化する

最終更新 2020-03-25 読了時間 2 分
まとめ
  • 複数銘柄のローリングβをヒートマップで並べ、市場連動性の時間変化を一覧する。
  • imshowとカラーマップ(RdYlGn)で、ディフェンシブ銘柄とアグレッシブ銘柄を色で区別する。
  • ウィンドウ長の選択やβの分布分析で、リスク管理の判断材料を得る。

直感 #

株式のβ(ベータ)は市場全体に対する感応度を示す指標で、β>1なら市場より値動きが大きく、β<1ならディフェンシブな性質を持ちます。しかしβは固定値ではなく、市場環境によって変動します。60日や120日のローリングウィンドウで算出したβをヒートマップにすると、「いつ・どの銘柄が市場と連動しやすくなったか」が時系列で一目瞭然になります。

詳細な解説 #

擬似データの生成とローリングβの算出 #

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from pathlib import Path

np.random.seed(42)
plt.style.use("scripts/k_dm.mplstyle")

dates = pd.date_range("2023-01-01", periods=260, freq="B")
market = np.random.normal(0.0005, 0.01, size=len(dates)).cumsum()
returns = pd.DataFrame(index=dates)
returns["market"] = np.diff(np.insert(market, 0, 0))

for ticker, beta, vol in [
    ("AAA", 1.1, 0.012),
    ("BBB", 0.8, 0.009),
    ("CCC", 1.4, 0.015),
    ("DDD", 0.6, 0.008),
]:
    noise = np.random.normal(0, vol, size=len(dates))
    returns[ticker] = beta * returns["market"] + noise

window = 60
rolling_beta = (
    returns[["AAA", "BBB", "CCC", "DDD"]]
    .rolling(window)
    .apply(
        lambda col: np.cov(col, returns.loc[col.index, "market"])[0, 1]
        / np.var(returns.loc[col.index, "market"]),
        raw=False,
    )
)

ヒートマップの描画 #

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
fig, ax = plt.subplots(figsize=(10, 4.5))
im = ax.imshow(
    rolling_beta.T,
    aspect="auto",
    cmap="RdYlGn",
    vmin=0.4,
    vmax=1.6,
)

ax.set_yticks(range(len(rolling_beta.columns)))
ax.set_yticklabels(rolling_beta.columns)
ax.set_xticks(np.linspace(0, len(dates) - 1, 6))
ax.set_xticklabels(pd.Series(dates).dt.strftime("%Y-%m-%d").iloc[::len(dates) // 6])
ax.set_title("60日ローリングβ(市場指数との回帰)")
ax.set_xlabel("日付")
ax.set_ylabel("銘柄")

cbar = fig.colorbar(im, ax=ax, pad=0.01)
cbar.ax.set_ylabel("β", rotation=90)

output = Path("static/images/finance/visualize/rolling_beta_heatmap.svg")
output.parent.mkdir(parents=True, exist_ok=True)
fig.tight_layout()
fig.savefig(output)

ローリングβヒートマップ


βの分布を箱ひげ図で確認 #

ヒートマップで時間変化を把握した後、各銘柄のβの統計的なばらつきを箱ひげ図で確認すると、どの銘柄が安定しているかがわかります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
fig, ax = plt.subplots(figsize=(8, 4))
rolling_beta.dropna().boxplot(ax=ax)
ax.axhline(1.0, color="#ef4444", linestyle="--", linewidth=1, label="β=1")
ax.set_ylabel("ローリングβ")
ax.set_title("銘柄別 ローリングβの分布")
ax.legend()
ax.grid(axis="y", alpha=0.3)
fig.tight_layout()
plt.show()

# 要約統計量
print(rolling_beta.dropna().describe().round(3))

ウィンドウ長による安定性の比較 #

ローリングウィンドウの長さはβの安定性に直接影響します。短いウィンドウ(30日)はノイズが多く、長いウィンドウ(120日)は滑らかですが変化の検出が遅れます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
fig, axes = plt.subplots(1, 3, figsize=(14, 3.5), sharey=True)

for ax, w in zip(axes, [30, 60, 120]):
    rb = returns["AAA"].rolling(w).apply(
        lambda col: np.cov(col, returns.loc[col.index, "market"])[0, 1]
        / np.var(returns.loc[col.index, "market"]),
        raw=False,
    )
    ax.plot(dates, rb, linewidth=1.2, color="#3b82f6")
    ax.axhline(1.0, color="#ef4444", linestyle="--", linewidth=0.8)
    ax.set_title(f"ウィンドウ={w}日")
    ax.set_ylabel("β" if w == 30 else "")
    ax.grid(alpha=0.3)
    ax.tick_params(axis="x", rotation=30)

fig.suptitle("AAAのローリングβ: ウィンドウ長の影響", fontsize=13)
fig.tight_layout()
plt.show()

読み方のポイント #

  • βが1.0を超えると市場より値動きが大きくなり、0.8など1.0を下回るとディフェンシブに変化したと解釈できます。
  • ノイズが多い銘柄はβの変動が激しくなるため、ローリングウィンドウを長めに設定するか、指数加重移動平均(EWMA)で安定化すると読みやすくなります。
  • MSCIなどのセクター指数で同じヒートマップを作ると、セクターごとのリスクオン・リスクオフ局面を定性的に把握できます。
  • βが急上昇した時期は市場全体のボラティリティが高まっていることが多く、VIXなどの恐怖指数と併せて確認すると背景の理解が深まります。