05.2.5
ローリングβをヒートマップで可視化する
まとめβ(ベータ)は「市場が 1% 動いたとき、その銘柄は何 % 動くか」を表す感応度で、銘柄リターンと市場リターンの共分散を市場リターンの分散で割って求めます。このページでは真の β を 1.1・0.8・1.4・0.6 に設定した 4 銘柄の日次リターンを人工的に作り、rolling(60).apply で直近 60 営業日ごとの β を計算して、imshow に cmap="RdYlGn"、vmin=0.4、vmax=1.6 を渡したヒートマップに並べます。横軸が日付、縦軸が銘柄、色が β なので、「いつ・どの銘柄が市場との連動を強めたか」が赤と緑の帯として読めます。続けて箱ひげ図で銘柄ごとの β の散らばりを確認し、ウィンドウ長を 30・60・120 日に変えて、短いほどノイズが増え長いほど変化の検出が遅れる様子を比べます。
直感
#
株式のβ(ベータ)は市場全体に対する感応度を示す指標で、β>1なら市場より値動きが大きく、β<1ならディフェンシブな性質を持ちます。しかしβは固定値ではなく、市場環境によって変動します。ある銘柄の β を全期間で 1 つだけ計算するのは、1 年間の平均気温だけで季節を語るようなものです。60日や120日のローリングウィンドウで算出したβをヒートマップにすると、「いつ・どの銘柄が市場と連動しやすくなったか」が時系列で一目瞭然になります。4 銘柄 × 260 日の β を折れ線 4 本で描くと線が絡みますが、ヒートマップなら 4 本の帯の色を上下に見比べるだけで済みます。
市場リターンと 4 銘柄のリターンを作る→
60 日窓ごとに cov(銘柄, 市場) / var(市場) を計算→
銘柄 × 日付の β 行列→
imshow で RdYlGn の色に→
箱ひげ図とウィンドウ長の比較で安定性を確認
何が起きているか(ステップで見る)
#
コードを読む前に、この手法が図の上で何をしているかを追ってみましょう。「再生」を押すか、スライダーを動かすと 1 手順ずつ進みます。
見るポイント:r は「直線に沿う強さ」。曲線的な関係や外れ値があると、r や β の 1 つの数値だけでは関係を見誤る。
詳細な解説
#
擬似データの生成とローリングβの算出
#
次のコードは、2023 年の 260 営業日ぶんの市場リターンを乱数で作り、そこに真の β を掛けてノイズを足した 4 銘柄のリターンを生成し、60 日窓のローリング β を計算します。答え(1.1・0.8・1.4・0.6)を知っているデータなので、ヒートマップの色が正しいかを確かめられます。
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,
)
)
|
ヒートマップの描画
#
次のコードは、銘柄 × 日付の β 行列を imshow で色に変え、β 1.0 が黄色になる配色で 4 銘柄を上下に並べます。
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)
|

図の読み方
#
上から AAA・BBB・CCC・DDD の 4 本の帯が並び、CCC の帯が緑(β 1.4 付近)、DDD の帯が赤(β 0.6 付近)で推移します。真の β は一定なのに帯の色が左右で濃淡を変えるのは、60 日窓にノイズが入るためで、この揺らぎの幅が「ローリング β の推定誤差」です。左端の約 60 日ぶんが空白なのは窓が埋まるまで NaN になるためです。
βの分布を箱ひげ図で確認
#
ヒートマップで時間変化を把握した後、各銘柄のβの統計的なばらつきを箱ひげ図で確認すると、どの銘柄が安定しているかがわかります。次のコードは NaN を落とした 201 日ぶんの β を銘柄ごとの箱にし、β = 1 の基準線と要約統計量を添えます。
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日)は滑らかですが変化の検出が遅れます。次のコードは AAA の β を 30・60・120 日の 3 つの窓で計算し、横に並べて比べます。
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を下回るとディフェンシブに変化したと解釈できます。ただしこのページのデータが示す通り、真の β が一定でも 60 日窓の推定値は ±0.2 ほど揺れるので、0.95 と 1.05 の差を「性質が変わった」と読むのは早計です。ノイズが多い銘柄はβの変動が激しくなるため、ローリングウィンドウを 120 日など長めに設定するか、ewm(halflife=30) のような指数加重移動平均(EWMA)で直近を重く・古い日を軽く扱うと読みやすくなります。
MSCIなどのセクター指数で同じヒートマップを作ると、セクターごとのリスクオン・リスクオフ局面を定性的に把握できます。βが急上昇した時期は市場全体のボラティリティが高まっていることが多く、VIXなどの恐怖指数と併せて確認すると、「銘柄固有の変化」か「市場全体の相関上昇」かを区別できます。