「分散投資はリスクを下げる」とよく言われますが、その効き目は 銘柄間の相関 に左右されます。本記事では、複数銘柄の日次リターンから相関行列を計算し、seaborn のヒートマップで全体像を確認するまでを Python で整理します。

目次

  1. なぜ相関を見るのか
  2. 必要なライブラリ
  3. サンプルデータの準備
  4. 相関行列の計算
  5. ヒートマップで可視化
  6. 上三角だけを描く
  7. クラスタリングで並び替える
  8. 期間で変わる相関

なぜ相関を見るのか

ポートフォリオの分散 σp2\sigma_p^2 は、構成銘柄の分散と相関で決まります。等加重で 2 銘柄を持つ場合の例で書くと、

σp2=14σ12+14σ22+12ρ12σ1σ2\sigma_p^2 = \frac{1}{4} \sigma_1^2 + \frac{1}{4} \sigma_2^2 + \frac{1}{2} \rho_{12} \sigma_1 \sigma_2

ρ12\rho_{12} が小さい(あるいは負)ほど、ポートフォリオの分散は小さくなります。逆に ρ12=1\rho_{12} = 1 なら、銘柄を増やしてもリスクは下がりません。「同じセクターばかりに分散しても効果が薄い」という直感は、この式から読み取れます。

必要なライブラリ

Terminal window
pip install pandas numpy matplotlib seaborn

seaborn は matplotlib の上に乗った可視化ライブラリです。ヒートマップが 1 行で書けるため、相関行列の可視化に向きます。

サンプルデータの準備

5 銘柄の日次対数リターンを、相関のある多変量正規分布から生成します(実データを使う場合は J-Quants の終値から対数リターンに直したものを使います)。

import numpy as np
import pandas as pd
rng = np.random.default_rng(seed=33)
n_days = 750
tickers = ["7203", "9984", "8306", "6758", "4063"]
# 真の相関を仮定(対角は 1)
true_corr = np.array([
[1.00, 0.30, 0.45, 0.50, 0.35],
[0.30, 1.00, 0.20, 0.40, 0.25],
[0.45, 0.20, 1.00, 0.30, 0.30],
[0.50, 0.40, 0.30, 1.00, 0.40],
[0.35, 0.25, 0.30, 0.40, 1.00],
])
sigma = np.array([0.018, 0.022, 0.016, 0.020, 0.017]) # 日次ボラ
cov = true_corr * np.outer(sigma, sigma)
returns = pd.DataFrame(
rng.multivariate_normal(mean=np.zeros(5), cov=cov, size=n_days),
index=pd.date_range("2023-01-04", periods=n_days, freq="B"),
columns=tickers,
)
print(returns.head().round(4))

実データを使う場合は、ロング形式 DataFrame から pivot でワイド形式に変換します。

# wide = long_df.pivot(index="Date", columns="Code", values="log_return")

相関行列の計算

pandas の .corr() 一発で相関行列が出ます。

corr = returns.corr()
print(corr.round(2))
  • 対角は常に 1
  • 値域は [1,1][-1, 1]
  • 欠損(NaN)は自動的に除外される(ペアワイズ)

NaN を含むデータで計算する場合、銘柄 A と B の相関は両方の値が揃っている日だけで取られます。期間が銘柄ごとに違うと結果が安定しないので、returns.dropna() で全銘柄のデータが揃う期間に絞るほうが安全です。

ヒートマップで可視化

seaborn の heatmap は対称行列を描くのに向いています。

import matplotlib.pyplot as plt
import seaborn as sns
fig, ax = plt.subplots(figsize=(7, 6))
sns.heatmap(
corr,
annot=True,
fmt=".2f",
cmap="coolwarm",
vmin=-1, vmax=1, center=0,
square=True,
cbar_kws={"shrink": 0.8},
ax=ax,
)
ax.set_title("Daily return correlation")
plt.tight_layout()
plt.savefig("corr_heatmap.png", dpi=120)
plt.close(fig)
5銘柄の日次リターン相関ヒートマップ

引数のポイントは次の通りです。

  • annot=True: 各セルに数値を書き込む
  • cmap="coolwarm": 0 を中心に青↔赤のグラデーション
  • vmin=-1, vmax=1, center=0: 値域を固定し、視覚的に比較しやすくする

上三角だけを描く

対称行列なので、下三角(または上三角)だけ表示すると見やすくなります。

mask = np.triu(np.ones_like(corr, dtype=bool), k=1)
fig, ax = plt.subplots(figsize=(7, 6))
sns.heatmap(
corr,
annot=True, fmt=".2f",
cmap="coolwarm", vmin=-1, vmax=1, center=0,
square=True, mask=mask,
cbar_kws={"shrink": 0.8},
ax=ax,
)
ax.set_title("Daily return correlation (lower triangle)")
plt.tight_layout()
plt.savefig("corr_heatmap_tri.png", dpi=120)
plt.close(fig)

np.triu(..., k=1) は対角を残して上三角を True にする mask を作り、heatmapmask 引数で対応するセルが非表示になります。

クラスタリングで並び替える

似た動きをする銘柄を近くに並べると、グループが見えやすくなります。sns.clustermap を使うと、階層的クラスタリングで自動的に並び替えてくれます。

g = sns.clustermap(
corr,
annot=True, fmt=".2f",
cmap="coolwarm", vmin=-1, vmax=1, center=0,
figsize=(7, 7),
cbar_kws={"shrink": 0.8},
)
g.fig.suptitle("Clustered correlation", y=1.02)
g.savefig("corr_clustermap.png", dpi=120)

セクター別に並ぶ傾向があれば、「銘柄の業種に沿った相関構造」が読み取れます。

期間で変わる相関

相関は期間によって動きます。市場ストレス時には多くの銘柄の相関が上昇し、平時の分散効果が崩れる場面があります。ローリング相関で確認できます。

WINDOW = 60
pair = ("7203", "6758")
rolling_corr = returns[pair[0]].rolling(WINDOW).corr(returns[pair[1]])
fig, ax = plt.subplots(figsize=(9, 4))
ax.plot(rolling_corr.index, rolling_corr, color="tab:blue")
ax.axhline(0, color="black", linestyle="--", linewidth=0.8)
ax.set_title(f"Rolling correlation: {pair[0]} vs {pair[1]} (window={WINDOW})")
ax.set_ylabel("Correlation")
ax.set_xlabel("Date")
ax.grid(alpha=0.3)
plt.tight_layout()
plt.savefig("rolling_corr.png", dpi=120)
plt.close(fig)

注意点

  • 相関 \ne 因果: 相関が高くても、片方がもう片方を動かしているとは限りません
  • ピアソン相関の前提: 線形関係を仮定する。極端な外れ値や非線形な依存に弱い
  • 異なる売買時間: 国をまたいだ銘柄を比較する場合、市場が開いている時間が違うと相関が低めに出ます(時差リスク)
  • 生存者バイアス: 上場廃止になった銘柄を含めずに相関を計算すると、相関構造が現実より楽観的になります(#10-7「バックテストの落とし穴(俯瞰)」)

生成AI へのプロンプト例

ロング形式 DataFrame から「リターン取得 → 相関 → ヒートマップ保存」までを 1 関数にまとめたい場合の例です。

入力 DataFrame に次の列があります:
- Date (datetime64)
- Code (str)
- log_return (float)
次の処理を行う関数 plot_correlation(df, out_path) を書いてください。
仕様:
- Date と Code でピボットしてワイド形式にする
- 全銘柄でデータが揃う期間に dropna する
- 相関行列を計算する
- seaborn の heatmap で出力(annot=True, cmap="coolwarm", center=0, vmin=-1, vmax=1)
- out_path に PNG として保存する
- 戻り値は計算した相関行列(DataFrame)
要件:
- pandas 2.2 / matplotlib / seaborn 系
- docstring を日本語で書く

まとめ

  • 分散投資の効き目は「銘柄間の相関」で決まる
  • pandas の .corr() で相関行列、seaborn の heatmap で可視化が 1 行
  • 上三角マスク、クラスタリングを使うと構造が見やすい
  • 相関は期間で変わる(特に市場ストレス時に上昇しやすい)
  • 相関が低いペアを混ぜることがリスク低減の出発点