FIREの資産推移シミュレーション── 3,000のパラレル世界を生成、証券会社は絶対に見せない残酷な現実を突きつけた
第1章:FIREの幻想と「65%ルール」
FIREは本当に大丈夫なのか
FIRE──Financial Independence, Retire Early。
経済的自立を果たし、早期にリタイアして自由に生きる。
この言葉を耳にするだけで心が弾む方も多いことでしょう。
その理論的な支柱として有名なのが、トリニティ・スタディに基づく「4%ルール」です。
「資産を毎年4%ずつ取り崩しても、30年間は資産が尽きない」という極めてシンプルな考え方です。
しかし、このルールには致命的な落とし穴があります。
4%ルールの「成功」の定義
4%ルールの研究においては、次のように「成功」が定義されています。
30年後に資産がゼロになっていなければ成功
言い換えれば、途中で資産が半分に減っても、暴落により生活不安に追い込まれても、
30年後に多少でも残っていれば「成功」という扱いになるのです。
これは果たしてFIREの成功と呼べるのでしょうか。
FIREに必要なのは「30年後に残高がゼロでないこと」ではなく、
途中で生活基盤が崩壊しないことにあります。
暴落を考慮した「65%ルール」
現実の相場は直線的な右肩上がりではありません。
2008年のリーマンショックでは米株は50%以上下落し、2020年のコロナショックでは35%の下落を記録しました。
こうした現実を踏まえ、本記事では次のルールを採用しました。
資産が初期の65%を割り込んだ時点で即失敗と判定する
コロナショック級の下落(▲35%)で資産は65%水準に到達します。
このラインを下回ると、心理的にも資金的にも「再起不能ゾーン」に突入すると考えています。
この基準は「仮定」にすぎませんが、従来の「ゼロまで減らなければ成功」よりは現実的であり、FIREの持続可能性を測る上で有効な視点となります。
モンテカルロ・シミュレーションで未来を覗く
未来の株価や金利は誰にも分かりません。
しかし「あり得る未来」を多数生成し、その確率を測定することは可能です。
それがモンテカルロ・シミュレーションです。
同じ条件で3,000個のパラレルワールドを生成し、それぞれの未来を走らせます。
平均リターンもボラティリティも同じな仮想ポートフォリオなのに、ある世界では資産が倍増し、別の世界では破産してゼロに沈む。
投資とは勝敗の二択ではなく、成功する確率が何%かを把握する確率ゲームなのです。
シミュレーション条件の概要
資産クラス…一般的なリスク、リターンの値から合成リスクと合成リターンを算出した値。
QQQ(ナスダック100):平均15.9% ± 32.5%
GLD(金):平均7.8% ± 15.0%
VOO(S&P500):平均9.2% ± 25.0%
EDV(米長期国債):平均3.9% ± 24.5%
相関関係:株はほぼ同時に下落、金は独立性が高く、長期国債は逆相関気味。
インフレ率:形成期は平均3.94%、取り崩し期は固定2%
課税:利益に対して20.315%課税[*1]
フロア:65%を割り込んだ場合は即失敗
ポートフォリオの構成
P1:半分QQQ+半分GLD(爆発力型)
P2:QQQ35%/GLD35%/VOO10%/EDV20%(守備寄り安定型)
P3:QQQ30%/GLD30%/VOO30%/EDV10%(バランス型)
第1章まとめ
4%ルールは「延命ゲーム」でしかない
本記事では65%ルールを採用し、より現実的に判定
モンテカルロは「3,000通りの未来を走らせる実験」
投資は勝ち負けではなく「成功確率」で見るべき
第2章:形成期(Part A)──「3,000万→8,000万」の挑戦
目標の設定
今回のシミュレーションは以下の条件です。
初期総資産:3,000万円(投資2,300万+現金700万)
投資方法:投資分2,300万円は最初に一括で投入(積立投資は行わない)
期間:19年間
目標:8,000万円
必要CAGR:5.3%
積立投資ではなく一括投資を前提としているため、暴落がいつ来るかで成功・失敗が大きく分かれる点に留意する必要があります。
結果(3,000通りの未来)
P1(QQQ+GLD-heavy):成功率 82.8%
P2(35/35/10/20):成功率 75.3%
P3(30/30/30/10):成功率 73.1%
ここでの「成功率」とは、65%を一度も割らず、最終的に8,000万円に到達する確率です。
途中で65%を割れば、その後回復しても失敗扱いとなります。
分布図(ヒストグラム)
多くのシナリオでは8,000万を超過します。
しかし、底辺には「途中で破綻」した未来も多数存在します。
「8割が成功、2割は失敗」──このシンプルな確率は、決して楽観的に捉えるべきものではありません。
今回グラフ作成の際に再度モンテカルロシミュレーションしたら、結果が変わりましたので画像右上も参考にどうぞ。



平均リターンとの乖離
必要CAGR:5.3%
各PFの期待リターン:10%超
それでも成功率は80%前後
平均リターンだけを見ても意味はありません。
暴落と課税が「途中死」を生むのです。
第2章まとめ
形成期の成功率はP1が最も高い(82.8%)
平均リターン10%超でも2割は途中で失敗
FIREに必要なのは「平均」ではなく「成功確率」での判断
第3章:取り崩し期(Part B)──「8,000万から45年サバイバル」
FIREの本番はここから
形成期で8,000万円を築いたとしても、FIREはまだ半分です。
本番はここから始まります。
毎年の生活費の取り崩し
インフレでの支出増加
利益にかかる課税
そして65%ルール
退職と同時に、長いサバイバルゲームが始まるのです。
条件設定
初期資産:8,000万円
支出:
最初の5年間は年間150万円(この間は現金700万円を利用して生活費をカバーし、投資資産を温存)
6年目以降は年間250万円(インフレ率2%で毎年増加)
インフレ率:固定2%
課税:売却益に20.315%課税[*1]
失敗条件:初期資産の65%割れ
結果
P1:成功率 90.4%
P2:成功率 92.3%
P3:成功率 85.6%
形成期で強かったP1よりも、守備力のあるP2の方が安定しました。
グラフで見る実態
成功シナリオでは資産が維持または増加
失敗シナリオでは序盤に暴落を受け、65%割れで即失敗判定
平均は安心に見えても、下振れシナリオでは無慈悲に破綻
今回グラフ作成の際に再度モンテカルロシミュレーションしたら、結果が変わりましたので画像右上も参考にどうぞ。



一応、証券会社等が出してくるシミュレーションで表示される平均的な線も計算して「点線」で表示してます。
しかし実際の私の生成した「3000個の世界の仮想ポートフォリオ」は綺麗に上がり下がりを繰り返すことはなく実際の動きをモデルとしてるため、このような図となってます。
税制リスク
現行の課税率(20.315%)でも負担は大きいのですが、将来さらに厳しくなる可能性があります。
もし所得税が20%に引き上げられた場合、合計課税率は25.42%となり、取り崩しの難易度は一段と高まるでしょう。
第3章まとめ
取り崩し期では守備型のP2が最も安定(グラフ生成時はP1のが成績良)
形成期と取り崩し期で「強いポートフォリオ」が入れ替わる
序盤の現金700万円は安全弁として有効
将来の税制変更は成功率をさらに厳しくする要因
第4章:連結シミュレーション──「最初から最後まで生き残れるのか」
形成期と取り崩し期をつなぐ
FIREは一度きりの人生ゲームです。
形成期で資産を築いても、その後の取り崩しで破綻しては意味がありません。
連結シミュレーションの結果
P1:連結成功率 74.8%
P2:連結成功率 69.5%
P3:連結成功率 62.6%
形成期・取り崩しを別々に見ると成功率は高いですが、連結すると一気に下がります。
つまり、例えるなら最初から最後まで走り切れるのは、3人に2人程度に過ぎません。



直線グラフと現実のギャップ
証券会社のライフプラン・シミュレーションでは、8,000万円に到達するまで綺麗な右肩上がりの直線が描かれることが多いです。
しかし実際は、暴落による乱高下で「山あり谷あり」となります。
綺麗な直線を信じてしまうと、現実の暴落で「こんなはずではなかった」と計画が崩れてしまいます。
綺麗な直線は理想の未来を描きます。
しかし現実の相場は、崖と谷を繰り返すジェットコースターです。
本当に備えるべきなのは、直線の夢ではなく谷をどう耐えるかです。
第4章まとめ
連結すると成功率は60〜70%台に低下
最初から最後まで走り切れるのは3人に2人程度
右肩上がりの直線は幻想、現実は乱高下する曲線
P1は爆発力、P2は堅実、P3は中庸
第5章:ポートフォリオ診断──性格で選ぶ投資スタイル
ポートフォリオは「性格診断」
投資スタイルは単なる数字の組み合わせではなく、投資家の性格や価値観の反映です。
本章では、シミュレーションで扱った3つのポートフォリオを「人間の性格診断」に見立てて説明します。
P1:QQQ50+GLD50
特徴:成功率は高めだが、荒波を受けやすい
投資家像:短期的な暴落に動じず、上昇局面で一気に資産を伸ばすことを狙う「攻めの姿勢」
メリット:形成期で圧倒的な伸び
デメリット:取り崩し期の不安定さ
例えるなら「起業家タイプ」。成功すれば莫大なリターンを得るが、途中の波を楽しめる強いメンタルが必要です。
P2:QQQ35/GLD35/VOO10/EDV20
特徴:形成期はやや弱いが、取り崩し期に最も安定
投資家像:守りを重視し、着実にFIREを実現したい「堅実派」
メリット:取り崩し期の成功率が最も高い
デメリット:形成期において目標達成にやや届きにくい
例えるなら「公務員タイプ」。爆発力はないが、安定して最後まで走り切る可能性が高い。
P3:QQQ30/GLD30/VOO30/EDV10
特徴:一見バランス型だが、結局どちらにも振り切れない
投資家像:リスクとリターンの両立を図りたいが、中途半端になりがち
メリット:極端な失敗は避けやすい
デメリット:連結成功率は最も低い
例えるなら「優等生タイプ」。万能そうに見えるが、大きな局面では突出できない。
第5章まとめ
ポートフォリオ選びは「数字」だけでなく「自分の性格」に合うかどうかが重要
攻めるならP1、守るならP2、中庸ならP3
FIREの成功は「どの性格で挑むか」によっても左右される
第6章:投資心理とFIREの落とし穴
暴落時の心理的負担
シミュレーションでは「65%割れ」で即失敗としましたが、これは単に数値上の基準ではなく、投資家が心理的に耐えられるかどうかを象徴しています。トリニティ大学の0円が失敗とする前に人は退場を考えると想定しています。
資産が35%減少した時、人は「このままゼロになるのでは」と感じる
実際には市場が回復する可能性が高くても、多くの人は恐怖で売却してしまう
FIRE生活では給与収入がないため「生活費をどう賄うか」という現実の不安が加速する
行動経済学が示す「人は合理的でない」
カーネマンやセイラーの研究が示す通り、人間は「損失回避バイアス」を持ちます。
100万円得る喜びよりも、100万円失う痛みの方が数倍強く感じられる。
このバイアスがある以上、シミュレーションで「65%を割らない」と判定されても、実際に資産が急落すれば多くの人は冷静でいられません。
FIRE生活における「現金比率」
暴落に耐えるためには、精神的な安心材料が不可欠です。
その一つが「数年分の生活費を現金で確保する」ことです。
本記事のシミュレーションでも序盤5年間は現金700万円で賄う設計としましたが、これは暴落時の「心理的なセーフティネット」として極めて重要です。
第6章まとめ
投資は数字の問題だけではなく、心理との戦いでもある
人間は損失に弱く、暴落で計画を崩しやすい
現金比率の確保は心理的安定に大きく寄与する
第7章:税制シナリオ──20.315%と25.42%の未来
税金はFIRE最大のリスク要因
シミュレーションでは現行の20.315%課税を前提としました。
しかし、税制は未来永劫固定されるものではなく、むしろ上昇する可能性が高いと考える方が現実的です。
もし所得税が20%に引き上げられたら
現行:所得税15%+住民税5%+復興特別所得税0.315%=20.315%
将来シナリオ:所得税20%+住民税5%+復興特別所得税0.42%(仮)=25.42%
この差は一見小さく見えますが、長期の複利計算においては決定的な違いを生みます。
成功率への影響
現行税率でも成功率は60〜70%台
25.42%課税となれば、取り崩し額が増え、資産寿命は短縮
成功率はさらに数%ポイント低下すると想定される
税制はコントロールできないリスク
インフレや暴落はある程度想定して行動できますが、税制は投資家の意思でコントロールできません。
つまり「見えない最大のリスク」として常に頭に置いておく必要があります。
第7章まとめ
FIREにおける税金の影響は極めて大きい
税率が25.42%となれば成功率はさらに低下
コントロールできない税制リスクを織り込むことが重要
第8章:限界と今後の研究
本シミュレーションの限界
対象資産は米国株・金・債券のみ
為替リスクは考慮していない
日本株や不動産、REIT、暗号資産などは含まれていない
インフレ率も固定値を前提とした単純化
読者への呼びかけ
FIREを考える際、1つのシミュレーション結果に依存するのは危険です。
多様な資産、異なるシナリオ、変化する税制──これらを取り入れて自分自身の「オーダーメイドFIREモデル」を構築することが求められます。
第8章まとめ
本研究は「一例」にすぎない
為替・税制・資産多様化を考慮したさらなる研究が必要
最適解は人それぞれ。自分の前提条件で確率を測ることが重要
第9章:結論──「FIREは確率ゲームである」
FIREの現実
形成期成功率:P1 82.8%、P2 75.3%、P3 73.1%
取り崩し期成功率:P1 90.4%、P2 92.3%、P3 85.6%
連結成功率:P1 74.8%、P2 69.5%、P3 62.6%
このFIREの条件で最初から最後まで走り切れるのは、3人に2人程度。
言い換えれば、2〜3割は途中で破産する未来です。それを知らず投資をしている人が多いのが現状です。
FIREは確率のゲーム
今回提示した3つのポートフォリオは、相関係数を考慮して設計しています。
狙いは「大金持ちになる」ことではなく、破産確率を下げるため、資産の上昇曲線をなだらかにまた大きな変動をせずに安定的にさせることです。
そのために事前にシミュレーションと検証を繰り返し、FIRE生活に耐えうる設計を目指しました。いつか記事にします。
~ちなみに参考までに、オルカン100%(全世界株式インデックス)に資産5,000万円を投入し、年率8.5%のリターンを仮定、毎年270万円を取り崩すという条件で1,000回のモンテカルロ・シミュレーションを実施しました~
その結果、オルカン全力の方で30年後に「0円=破産」になったシナリオは279例(約28%)も存在していました。
1000番あるうちの世界の279番目以上に運が悪い世界のあなたなら…オルカンであっても30年後、破産してます。
ここでの判定基準は本記事のように「資産が65%を割り込んだら失敗」ではなく、完全にゼロになるまで放置するという甘い条件にもかかわらず、成功率は本記事で扱ったポートフォリオと同程度に低下していたのです。
つまり言い換えれば、本稿のポートフォリオを“ゼロ円破産”を基準に判定すれば、オルカンよりも良い成績を残したということになります。
まあ要するに、
「オルカンに突っ込んでおけば右肩上がりで安泰」
という神話は、確率の世界では簡単に崩れ去ります。

この事実を知らずにSNS上やyoutubeでは「オルカンに入れておけばいい」と皆が言っているから入れる方が多いのが現状です。
最後のメッセージ
FIREは夢ではありますが、全員が救われるゲームではありません。
綺麗な直線を信じた人から退場していきます。
FIREは確率ゲーム。
生き残れるかどうかは、谷を耐え抜けるかどうかにかかっています。
今回使用したPython式
Monte Carlo simulation per spec, generate figures for note
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.ticker as mticker
import pandas as pd
rng = np.random.default_rng(20250823)
YEARS_A, YEARS_B, SIMS = 19, 45, 3000
TAX, FLOOR = 0.20315, 0.65
YTOP_10K = 50000 # y-axis top in x10k JPY
INIT_A_CASH = 700 # x10k JPY
INIT_A_INV = 2300
TARGET_A = 8000
INIT_B = 8000
AFTER_T_NEED_10K = np.array([150]5 + [250](YEARS_B-5)) # x10k JPY (after-tax)
INFL_A_MU, INFL_A_SIG = 0.0394, 0.0131
INFL_B = 0.02
assets = {'QQQ':(0.159,0.325),'GLD':(0.078,0.150),'VOO':(0.092,0.250),'EDV':(0.039,0.245)}
order = ['QQQ','GLD','VOO','EDV']
means = np.array([assets[a][0] for a in order])
stds = np.array([assets[a][1] for a in order])
corr = np.array([[1.00, 0.06, 0.94, -0.11],
[0.16, 1.00, 0.18, 0.08],
[0.92, 0.08, 1.00, -0.11],
[-0.12,0.26,-0.12, 1.00]])
cov = np.outer(stds,stds)*corr
def nearest_spd(A):
B=(A+A.T)/2
w,V=np.linalg.eigh(B)
w[w<1e-12]=1e-12
return (V*(w))@V.T
try:
np.linalg.cholesky(cov)
except np.linalg.LinAlgError:
cov = nearest_spd(cov)
def infl_hist(y,s,rng):
x=rng.normal(INFL_A_MU,INFL_A_SIG,size=(y,s))
return np.maximum(x,0.0)
def returns(y,s,means,cov,rng):
out=np.empty((y,s,len(means)))
L = np.linalg.cholesky(cov)
for t in range(y):
z = rng.standard_normal((s,len(means)))
out[t] = means + z @ L.T
return out
def port_ret(ret, w):
return np.einsum('ysn,n->ys', ret, w)
portfolios = {
'P1_QQQ50_GLD50': {'QQQ':0.49,'GLD':0.50,'VOO':0.00,'EDV':0.01},
'P2_35_35_10_20': {'QQQ':0.35,'GLD':0.35,'VOO':0.10,'EDV':0.20},
'P3_30_30_30_10': {'QQQ':0.30,'GLD':0.30,'VOO':0.30,'EDV':0.10},
}
def simulate_A(weights_key):
w = np.array([portfolios[weights_key][a] for a in order])
r = returns(YEARS_A,SIMS,means,cov,rng)
pr= port_ret(r, w)
grow = np.prod(1+pr,axis=0)
final = INIT_A_INV*grow + INIT_A_CASH # x10k
reach = final >= TARGET_A
# Percentiles
pct = np.percentile(final, [70,50,15,5])
return final, reach, pct
def simulate_B(weights_key, seed_offset=0, start_bal=INIT_B, use_cash_buffer=True):
# run withdrawal simulation with tax & floor
w = np.array([portfolios[weights_key][a] for a in order])
# returns
r = returns(YEARS_B,SIMS,means,cov,rng)
pr= port_ret(r, w)
# inflation path
infl = np.full((YEARS_B,SIMS), INFL_B)
# withdrawal needs (after-tax), inflation-updated
need_base = AFTER_T_NEED_10K.copy()
paths = np.zeros((SIMS,YEARS_B+1)); paths[:,0]=start_bal
cb = np.full(SIMS, start_bal, dtype=float) # cost basis
alive = np.ones(SIMS,bool)
breach= np.zeros(SIMS,bool)
# cash buffer to cover first 5 years
cash_buf = np.full(SIMS, 700.0 if use_cash_buffer else 0.0) # x10k
cum_infl = np.ones(SIMS)
th = start_bal*FLOOR
for t in range(YEARS_B):
# growth before withdrawal
paths[alive,t] *= (1+pr[alive,t])
# compute after-tax need this year (inflation-adjusted)
need_t = need_base[t]*cum_infl[alive]
# offset by cash buffer if available
offset = np.minimum(need_t, cash_buf[alive])
need_t_after_offset = need_t - offset
cash_buf[alive] -= offset
# compute gain ratio for tax
pos = (cb>0)&alive
gr = np.zeros(SIMS)
gr[pos] = np.maximum((paths[pos,t]-cb[pos])/cb[pos], 0.0)
eff = (gr/(1+gr))*TAX # effective tax rate on withdrawals
wdr = np.zeros(SIMS)
wdr[alive] = np.where(1-eff[alive]>0, need_t_after_offset/(1.0-eff[alive]), 0.0)
tax = np.zeros(SIMS); tax[alive]= wdr[alive]*eff[alive]
paths[alive,t] -= wdr[alive]
cb[alive] = np.maximum(cb[alive] - (wdr[alive]-tax[alive]), 0.0)
# floor breach
breach[(paths[:,t]<th)&alive] = True
# death
dead = (paths[:,t]<=0)&alive
alive[dead]=False
paths[dead,t:] = 0.0
# carry forward
paths[alive,t+1]=paths[alive,t]
# update inflation compounding
cum_infl[alive] *= (1+infl[t,alive])
success = (~breach) & (paths[:,-1]>0)
final = paths[:,-1]
pct = np.percentile(final[final>0], [70,50,35,20,15,10]) if np.any(final>0) else np.array([0,0,0,0,0,0])
return paths, success, pct, finalRun simulations for all portfolios
results = {}
for key in portfolios.keys():
finalA, reachA, pctA = simulate_A(key)
# B for all sims (standard B)
pathsB, successB, pctB, finalB = simulate_B(key, use_cash_buffer=True)
# Linked A->B: run B only for those reaching in A, starting from 8000
idx = np.where(reachA)[0]
# For linked, we simulate B anew but only subset of sims is plotted; success is measured on the subset
pathsB_linked, successB_linked, pctB_linked, finalB_linked = simulate_B(key, use_cash_buffer=True)
# linked success rate = proportion of overall sims that both reach A and succeed B when conditioned
# Use success from those that reached A (same distribution), so filter by idx
linked_success = successB_linked[idx].mean() * (len(idx)/SIMS)
results[key] = {
'A_final': finalA, 'A_reach': reachA, 'A_pct': pctA,
'B_paths': pathsB, 'B_success': successB, 'B_pct': pctB, 'B_final': finalB,
'Linked_paths': pathsB_linked[:, :], 'Linked_success_overall': linked_success,
'A_reach_rate': reachA.mean(), 'B_success_rate': successB.mean()
}
Helper: formatting functions
def fmt_comma(x, pos=None):
return f"{int(x):,}"
def annotate_stats_box(ax, lines):
# place in upper right
txt = "\n".join(lines)
ax.text(0.98, 0.98, txt, transform=ax.transAxes, ha='right', va='top',
bbox=dict(boxstyle='round', alpha=0.8))
Create Figure outputs directory paths
fig_paths = []
Figure set 1: Part A histograms per portfolio
for key in portfolios.keys():
final = results[key]['A_final']
reach_rate = results[key]['A_reach_rate']*100
p70,p50,p15,p05 = results[key]['A_pct']
fig = plt.figure(figsize=(8,5))
ax = plt.gca()
ax.hist(final, bins=40)
ax.axvline(TARGET_A, linestyle='--')
ax.set_xlim(0, YTOP_10K)
ax.set_ylim(0, None)
ax.yaxis.set_major_formatter(mticker.FuncFormatter(fmt_comma))
ax.xaxis.set_major_formatter(mticker.FuncFormatter(fmt_comma))
ax.set_title(f"Part A: Final after 19y — {key}")
ax.set_xlabel("Wealth (x10k JPY)")
ax.set_ylabel("Count")
# stats box
lines = [
f"Reach ≥ {TARGET_A:,}: {reach_rate:.2f}%",
f"P70/P50/P15/P05: {int(p70):,} / {int(p50):,} / {int(p15):,} / {int(p05):,}"
]
annotate_stats_box(ax, lines)
out = f"/mnt/data/fig_A_hist_{key}.png"
plt.tight_layout()
plt.savefig(out, dpi=160)
plt.close(fig)
fig_paths.append(out)
Figure set 2: Part B path plots per portfolio
for key in portfolios.keys():
paths = results[key]['B_paths']
success_rate = results[key]['B_success_rate']*100
p70,p50,p35,p20,p15,p10 = results[key]['B_pct']
fig = plt.figure(figsize=(8,5))
ax = plt.gca()
# pick 150 random paths
idx = rng.choice(SIMS, size=150, replace=False)
for i in idx:
ax.plot(range(YEARS_B+1), np.clip(paths[i,:],0,YTOP_10K), linewidth=0.6, alpha=0.5)
# median dashed
med = np.median(paths, axis=0)
ax.plot(range(YEARS_B+1), np.clip(med,0,YTOP_10K), linestyle='--', linewidth=2)
# initial line
ax.axhline(INIT_B, color='black', linewidth=1.2)
ax.set_ylim(0, YTOP_10K)
ax.set_xlim(0, YEARS_B)
ax.yaxis.set_major_formatter(mticker.FuncFormatter(fmt_comma))
ax.set_title(f"Part B: 45y Withdrawal — {key}")
ax.set_xlabel("Years")
ax.set_ylabel("Wealth (x10k JPY)")
# stats
lines = [
f"Success (no <65%): {success_rate:.2f}%",
f"Final P70/P50/P15/P05: {int(p70):,} / {int(p50):,} / {int(p15):,} / {int(p05):,}"
]
annotate_stats_box(ax, lines)
out = f"/mnt/data/fig_B_paths_{key}.png"
plt.tight_layout()
plt.savefig(out, dpi=160)
plt.close(fig)
fig_paths.append(out)
Figure set 3: Linked A->B path plots per portfolio (only those who reached A)
for key in portfolios.keys():
paths = results[key]['Linked_paths']
reach_mask = results[key]['A_reach']
# filter to reached
if reach_mask.sum() == 0:
continue
subset = paths[reach_mask,:]
fig = plt.figure(figsize=(8,5))
ax = plt.gca()
sel = rng.choice(subset.shape[0], size=min(150, subset.shape[0]), replace=False)
for i in sel:
ax.plot(range(YEARS_B+1), np.clip(subset[i,:],0,YTOP_10K), linewidth=0.6, alpha=0.5)
med = np.median(subset, axis=0)
ax.plot(range(YEARS_B+1), np.clip(med,0,YTOP_10K), linestyle='--', linewidth=2)
ax.axhline(INIT_B, color='black', linewidth=1.2)
ax.set_ylim(0, YTOP_10K); ax.set_xlim(0, YEARS_B)
ax.yaxis.set_major_formatter(mticker.FuncFormatter(fmt_comma))
ax.set_title(f"Linked A→B Paths — {key} (only trials reaching 80M)")
ax.set_xlabel("Years"); ax.set_ylabel("Wealth (x10k JPY)")
# stats box
linked_overall = results[key]['Linked_success_overall']*100
lines = [
f"Reach in A: {results[key]['A_reach_rate']*100:.2f}%",
f"Linked success (overall): {linked_overall:.2f}%"
]
annotate_stats_box(ax, lines)
out = f"/mnt/data/fig_Linked_paths_{key}.png"
plt.tight_layout()
plt.savefig(out, dpi=160)
plt.close(fig)
fig_paths.append(out)
Figure set 4: Bar chart comparing success rates
rate_df = pd.DataFrame({
'Portfolio': list(portfolios.keys()),
'PartA_Reach_%': [results[k]['A_reach_rate']*100 for k in portfolios.keys()],
'PartB_Success_%': [results[k]['B_success_rate']*100 for k in portfolios.keys()],
'Linked_Overall_%': [results[k]['Linked_success_overall']*100 for k in portfolios.keys()]
})
Melt to long for simple plotting without seaborn
rows = []
for , r in rate_df.iterrows():
rows.append(('Part A Reach', r['Portfolio'], r['PartA_Reach%']))
rows.append(('Part B Success', r['Portfolio'], r['PartB_Success_%']))
rows.append(('Linked Overall', r['Portfolio'], r['Linked_Overall_%']))
bars = pd.DataFrame(rows, columns=['Metric','Portfolio','Rate'])
Plot one figure per metric (single-plot rule)
bar_fig_paths = []
for metric in bars['Metric'].unique():
dfm = bars[bars['Metric']==metric]
fig = plt.figure(figsize=(7,4.5))
ax = plt.gca()
ax.bar(dfm['Portfolio'], dfm['Rate'])
ax.set_ylim(0, 100)
ax.set_ylabel("Rate (%)")
ax.set_title(f"Success Rates — {metric}")
for i, v in enumerate(dfm['Rate']):
ax.text(i, v+1.0, f"{v:.1f}%", ha='center', va='bottom', fontsize=9)
out = f"/mnt/data/fig_rates_{metric.replace(' ','_')}.png"
plt.tight_layout()
plt.savefig(out, dpi=160)
plt.close(fig)
bar_fig_paths.append(out)
fig_paths += bar_fig_paths
Also export a CSV summary for tables in the article
summary_rows = []
for key in portfolios.keys():
pA70,pA50,pA15,pA05 = results[key]['A_pct']
pB70,pB50,pB35,pB20,pB15,pB10 = results[key]['B_pct']
summary_rows.append({
'Portfolio': key,
'PartA_Reach_%': results[key]['A_reach_rate']*100,
'PartA_P70': pA70, 'PartA_P50': pA50, 'PartA_P15': pA15, 'PartA_P05': pA05,
'PartB_Success_%': results[key]['B_success_rate']*100,
'PartB_P70': pB70, 'PartB_P50': pB50, 'PartB_P35': pB35, 'PartB_P20': pB20, 'PartB_P15': pB15, 'PartB_P10': pB10,
'Linked_Overall_%': results[key]['Linked_success_overall']*100
})
summary_df = pd.DataFrame(summary_rows)
csv_path = "/mnt/data/montecarlo_summary.csv"
summary_df.to_csv(csv_path, index=False)
import caas_jupyter_tools as cj
cj.display_dataframe_to_user("Monte Carlo Summary (Key Stats)", summary_df)
fig_paths, csv_path
いいなと思ったら応援しよう!
この記事は noteマネー にピックアップされました

