24. 平均分散ポートフォリオ理論
テーマ: 効率的フロンティア・最小分散ポートフォリオ・シャープレシオ最大化
学習点: dataclass, 型ヒント, NumPy 線形代数, scipy.optimize.minimize, 制約付き最適化
依存: NumPy, SciPy / 難易度: 上級
実行方法
uv run 24_portfolio_optimization.py
スクリプト冒頭の PEP 723 メタデータ(# /// script)により、必要なライブラリは
uv が自動的に仮想環境へ導入します。事前の pip install は不要です。
解説
何をするプログラムか
年金基金や投資信託の資産配分の土台となる平均分散ポートフォリオ理論を、国内株・先進国株・新興国株・国内債券・REIT の 5 資産クラスで実装します。各資産の期待収益率・標準偏差・相関行列を前提に、(1) 均等配分、(2) リスク最小の「最小分散ポートフォリオ」(解析解)、(3) シャープレシオ最大の「接点ポートフォリオ」(解析解)を比較し、さらに空売り禁止条件の下で目標収益ごとの最小リスク配分(効率的フロンティア)を数値最適化で描きます。
終盤では、分散投資がリスクを減らす数学的な仕組みを 2 資産の相関を変えて確かめ、最後に「期待収益率の見積もりをわずかに変えるだけで最適配分が大きく動く」という、この理論を実務で使う際の最大の落とし穴を実演します。
コードの読みどころ
- 市場の前提は
@dataclassのMarketにまとめられています。共分散行列covは入力ではなくfield(init=False)とし、__post_init__でnp.outer(self.sigma, self.sigma) * self.corrから構築します。np.linalg.eigvalshで固有値がすべて正(正定値)であることを assert し、不正な相関行列を早期に検出します。 min_variance()は解析解 w = Σ⁻¹1 / (1’Σ⁻¹1) をそのままinv @ one / (one @ inv @ one)と書いており、数式とコードが 1 対 1 に対応します。tangency()も同様に Σ⁻¹(μ − r_f·1) を正規化するだけです。efficient()はscipy.optimize.minimize(..., method="SLSQP")による制約付き最適化です。「重みの合計 = 1」「期待収益 = 目標値」の 2 本の等式制約を辞書のリストで渡し、bounds=[(0, 1)] * nが空売り禁止を表します。失敗時は NaN を返し、呼び出し側がnp.isnan(w).any()でスキップします。Market.stats()はリスクをnp.sqrt(w @ self.cov @ w)と計算します。2 次形式 w’Σw が@演算子 2 つで書ける点は NumPy 線形代数の読みどころです。
理論的背景
Markowitz(1952)の平均分散理論では、ポートフォリオの期待収益は μ_p = w’μ、分散は σ_p² = w’Σw で表されます。リスクが共分散を通じて決まるため、相関が 1 未満の資産を組み合わせると、ポートフォリオの標準偏差は各資産の標準偏差の加重平均を必ず下回ります。これが分散投資の数学的根拠です。最小分散ポートフォリオと接点ポートフォリオ(無リスク資産と組み合わせたときシャープレシオを最大にする配分)は空売りを許せば閉形式の解析解を持ち、制約を付けた途端に数値最適化が必要になります。
実行結果の見方
最小分散(解析解)は国内債券 96% でリスク 2.886% まで下がりますが、新興国株と REIT にわずかな負の配分(空売り)が現れます。接点ポートフォリオはシャープレシオ 0.3908 で、均等配分の 0.3713 を上回ります。効率的フロンティアの表では、目標収益 1.50% から 8.50% へ上げるにつれ債券 89% → 0% と減り、株式の比率が増えてリスクが 3.24% → 22.52% へ拡大する、リターンとリスクのトレードオフが読み取れます。
[分散投資の効果] では、相関 +1.0 のとき標準偏差は加重平均 10.5% と一致し、相関が下がるほど 7.5% まで縮小します。最後の [注意] が実務上の核心で、先進国株の期待収益を 10% 高く見積もっただけで、接点配分は国内株 1.0% → −2.1%、先進国株 14.7% → 23.8% と大きく入れ替わります。最適化が推定誤差を増幅する(Michaud 1989)ことを数値で確認してください。
ソースコード
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "numpy",
# "scipy",
# ]
# ///
"""24: 平均分散ポートフォリオ理論 -----------------------------------------
テーマ: 効率的フロンティア・最小分散ポートフォリオ・シャープレシオ最大化
学習点: dataclass, 型ヒント, NumPy 線形代数, scipy.optimize.minimize,
制約付き最適化
根拠: Markowitz(1952)。ポートフォリオ分散 σ_p² = w'Σw、期待収益 μ_p = w'μ。
最小分散ポートフォリオは w = Σ^{-1}1 / (1'Σ^{-1}1)(解析解)。
接点ポートフォリオ(シャープレシオ最大)は w ∝ Σ^{-1}(μ - r_f·1)。
"""
from dataclasses import dataclass, field
import numpy as np
from scipy.optimize import minimize
np.set_printoptions(precision=4, suppress=True)
@dataclass
class Market:
names: list[str]
mu: np.ndarray # 期待収益率(年率)
sigma: np.ndarray # 標準偏差(年率)
corr: np.ndarray # 相関行列
rf: float = 0.005 # 無リスク金利
cov: np.ndarray = field(init=False)
def __post_init__(self):
self.cov = np.outer(self.sigma, self.sigma) * self.corr
assert np.allclose(self.cov, self.cov.T)
assert np.all(np.linalg.eigvalsh(self.cov) > 0), "共分散行列が正定値でない"
def stats(self, w):
r = float(w @ self.mu)
v = float(np.sqrt(w @ self.cov @ w))
return r, v, (r - self.rf) / v
def min_variance(m: Market) -> np.ndarray:
"""解析解(空売り制約なし)。"""
inv = np.linalg.inv(m.cov)
one = np.ones(len(m.mu))
return inv @ one / (one @ inv @ one)
def tangency(m: Market) -> np.ndarray:
inv = np.linalg.inv(m.cov)
ex = m.mu - m.rf
w = inv @ ex
return w / w.sum()
def efficient(m: Market, target: float, long_only=True) -> np.ndarray:
"""目標収益率を満たす最小分散ポートフォリオを数値的に求める。"""
n = len(m.mu)
cons = [{"type": "eq", "fun": lambda w: w.sum() - 1},
{"type": "eq", "fun": lambda w: w @ m.mu - target}]
bounds = [(0, 1)] * n if long_only else [(-1, 2)] * n
res = minimize(lambda w: w @ m.cov @ w, np.repeat(1 / n, n),
bounds=bounds, constraints=cons, method="SLSQP")
return res.x if res.success else np.full(n, np.nan)
def main() -> None:
m = Market(
names=["国内株", "先進国株", "新興国株", "国内債券", "REIT"],
mu=np.array([0.055, 0.070, 0.090, 0.008, 0.060]),
sigma=np.array([0.180, 0.190, 0.250, 0.030, 0.200]),
corr=np.array([
[1.00, 0.70, 0.65, -0.10, 0.60],
[0.70, 1.00, 0.75, -0.05, 0.55],
[0.65, 0.75, 1.00, -0.05, 0.50],
[-0.10, -0.05, -0.05, 1.00, 0.05],
[0.60, 0.55, 0.50, 0.05, 1.00],
]),
)
print("[資産クラスの前提]")
for n_, mu_, s_ in zip(m.names, m.mu, m.sigma):
print(f" {n_:<8} 期待収益 {mu_:>6.2%} / 標準偏差 {s_:>6.2%} / "
f"シャープ {(mu_ - m.rf)/s_:>5.3f}")
for label, w in [("均等配分", np.repeat(0.2, 5)),
("最小分散(解析解)", min_variance(m)),
("接点(シャープ最大)", tangency(m))]:
r, v, sr = m.stats(w)
print(f"\n[{label}]")
print(" 配分: " + " ".join(f"{n}={x:>7.2%}" for n, x in zip(m.names, w)))
print(f" 期待収益 {r:.3%} / リスク {v:.3%} / シャープレシオ {sr:.4f}")
print("\n[効率的フロンティア(空売り禁止)]")
print(f"{'目標収益':>9}{'リスク':>9}{'シャープ':>10} 配分")
for t in np.linspace(0.015, 0.085, 8):
w = efficient(m, t)
if np.isnan(w).any():
continue
r, v, sr = m.stats(w)
print(f"{t:>9.2%}{v:>9.2%}{sr:>10.4f} "
+ " ".join(f"{x:>5.0%}" for x in w))
print("\n[分散投資の効果] 相関が下がるとリスクはどれだけ減るか")
print(" 2資産(国内株50% + 国内債券50%)の場合")
s1, s2 = 0.18, 0.03
for rho in [1.0, 0.5, 0.0, -0.5, -1.0]:
v = np.sqrt(0.25 * s1**2 + 0.25 * s2**2 + 2 * 0.25 * rho * s1 * s2)
print(f" 相関 {rho:>+5.1f}: ポートフォリオ標準偏差 {v:.4%}"
f" (加重平均 {0.5*s1+0.5*s2:.4%})")
print(" ※ 相関が1未満なら必ず加重平均を下回る。これが分散投資の数学的根拠。")
print("\n[注意] 期待収益率の推定誤差に対して最適化解は極めて敏感である")
base = tangency(m)
m2 = Market(m.names, m.mu * np.array([1, 1.1, 1, 1, 1]), m.sigma, m.corr, m.rf)
print(" 先進国株の期待収益を +10% 見積もり変更しただけで")
print(" 接点配分: " + " ".join(f"{x:>6.1%}" for x in base))
print(" -> : " + " ".join(f"{x:>6.1%}" for x in tangency(m2)))
print(" ※ これが実務でリスクパリティや制約付き最適化が好まれる理由"
"(Michaud 1989 の error maximization 批判)。")
if __name__ == "__main__":
main()
実行結果
[資産クラスの前提]
国内株 期待収益 5.50% / 標準偏差 18.00% / シャープ 0.278
先進国株 期待収益 7.00% / 標準偏差 19.00% / シャープ 0.342
新興国株 期待収益 9.00% / 標準偏差 25.00% / シャープ 0.340
国内債券 期待収益 0.80% / 標準偏差 3.00% / シャープ 0.100
REIT 期待収益 6.00% / 標準偏差 20.00% / シャープ 0.275
[均等配分]
配分: 国内株= 20.00% 先進国株= 20.00% 新興国株= 20.00% 国内債券= 20.00% REIT= 20.00%
期待収益 5.660% / リスク 13.896% / シャープレシオ 0.3713
[最小分散(解析解)]
配分: 国内株= 4.58% 先進国株= 1.25% 新興国株= -0.32% 国内債券= 96.06% REIT= -1.56%
期待収益 0.985% / リスク 2.886% / シャープレシオ 0.1681
[接点(シャープ最大)]
配分: 国内株= 0.99% 先進国株= 14.68% 新興国株= 11.92% 国内債券= 64.95% REIT= 7.46%
期待収益 3.122% / リスク 6.709% / シャープレシオ 0.3908
[効率的フロンティア(空売り禁止)]
目標収益 リスク シャープ 配分
1.50% 3.24% 0.3088 4% 3% 4% 89% 0%
2.50% 5.17% 0.3865 2% 10% 9% 74% 5%
3.50% 7.69% 0.3901 0% 16% 15% 60% 9%
4.50% 10.37% 0.3856 0% 22% 20% 45% 13%
5.50% 13.12% 0.3810 0% 27% 26% 30% 17%
6.50% 15.90% 0.3772 0% 33% 31% 15% 21%
7.50% 18.70% 0.3742 0% 39% 37% 0% 24%
8.50% 22.52% 0.3553 0% 19% 77% 0% 4%
[分散投資の効果] 相関が下がるとリスクはどれだけ減るか
2資産(国内株50% + 国内債券50%)の場合
相関 +1.0: ポートフォリオ標準偏差 10.5000% (加重平均 10.5000%)
相関 +0.5: ポートフォリオ標準偏差 9.8362% (加重平均 10.5000%)
相関 +0.0: ポートフォリオ標準偏差 9.1241% (加重平均 10.5000%)
相関 -0.5: ポートフォリオ標準偏差 8.3516% (加重平均 10.5000%)
相関 -1.0: ポートフォリオ標準偏差 7.5000% (加重平均 10.5000%)
※ 相関が1未満なら必ず加重平均を下回る。これが分散投資の数学的根拠。
[注意] 期待収益率の推定誤差に対して最適化解は極めて敏感である
先進国株の期待収益を +10% 見積もり変更しただけで
接点配分: 1.0% 14.7% 11.9% 64.9% 7.5%
-> : -2.1% 23.8% 8.2% 63.8% 6.3%
※ これが実務でリスクパリティや制約付き最適化が好まれる理由(Michaud 1989 の error maximization 批判)。