はじめに
React + TypeScript の練習として、ライフプランシミュレーターを作りました。老後の資産推移を可視化するWebアプリで、いわゆる FIRE(Financial Independence, Retire Early) のシミュレーターとしても使えます。
「いくら貯めれば何歳で引退できるのか」「運用がうまくいかなかった場合でも破綻しないか」——そういった疑問を手軽に試せるアプリです。
実装や本記事の作成には Claude(Anthropic の AI)を活用しています。コードレビュー、リファクタリング案の提示、型定義の設計など、ほぼすべてのフェーズでペアプロ的に使いました。「AIと一緒に作ってみた」という視点でも読んでもらえると嬉しいです。
作ったもの
デプロイ先
Vercel でホスティングしています。
https://lifeplan-sim.vercel.app/
ソースコード: https://github.com/tkmz-n/lifeplan_sim
機能紹介
1. 資産推移グラフ(確定的シミュレーション)
現金と運用資産を積み上げグラフで表示します。現役中は収入 - 支出 = 貯蓄が積み上がり、退職後は運用資産を取り崩していく様子が一目でわかります。退職予定年齢には赤い点線の基準線が引かれます。
2. モンテカルロシミュレーション(1000回試行)
ここがこのアプリのメインです。運用リターンは毎年一定ではなく、上振れ・下振れします。その不確実性を1000回のシミュレーションで表現します。
グラフには以下のパーセンタイル帯を表示します:
| 帯 | 意味 |
|---|---|
| 5〜95% | 広い不確実性の範囲 |
| 25〜75% | 現実的な想定範囲 |
| 中央値(太線) | 最も代表的なシナリオ |
3. サマリーカード
3つの数値をひと目で確認できます:
- 破産確率:100歳時点で資産がマイナスになるシミュレーションの割合
- 資産枯渇年齢(中央値):中央値シナリオで資産が尽きる年齢
- 退職時資産(中央値):退職年齢時点での資産の中央値
4. ライフイベント設定
「45歳で住宅購入 -2000万円」「60歳で退職金 +1500万円」など、特定年齢での一時的な収支を自由に追加できます。イベント名・年齢・金額を入力するだけで、シミュレーション結果にリアルタイムで反映されます。
技術スタック
| カテゴリ | 技術 |
|---|---|
| フレームワーク | React 19 + TypeScript |
| ビルドツール | Vite |
| UIライブラリ | MUI (Material UI) v6 |
| グラフ | Recharts |
| デプロイ | Vercel |
実装のポイント
Box-Muller法で正規分布の乱数を生成
モンテカルロシミュレーションでは、毎年の運用リターンを「平均=期待リターン、標準偏差=ボラティリティ」の正規分布に従う乱数として生成します。JavaScriptの Math.random() は一様乱数なので、Box-Muller変換で正規分布に変換しています。
// Box-Muller法で正規分布の乱数を生成
function randomNormal(mean: number, stdDev: number): number {
const u1 = Math.random()
const u2 = Math.random()
const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2)
return mean + stdDev * z
}
これを使って、退職前・退職後それぞれの毎年の資産増減を計算します:
// ランダムな年間リターンを生成(正規分布)
const annualReturn = randomNormal(returnRate, volatility)
if (age < retireAge) {
investment *= (1 + annualReturn / 100)
// ... 貯蓄の積み立て
} else {
investment *= (1 + annualReturn / 100)
// ... 生活費の取り崩し
}
useMemo でモンテカルロの再計算を制御
1000回シミュレーションは計算コストが高いため、useMemo でキャッシュします。ここで一工夫あって、ライフイベントの「名前」変更や「イベント追加ボタン押下直後(金額が 0 の状態)では再計算しないようにしています。
// ラベルを除き、かつ金額が0のイベントはスキップしてキー化
const lifeEventsSimKey = lifeEvents
.filter(e => e.amount !== 0)
.map(e => `${e.age}:${e.amount}`)
.join(',')
const monteCarloResult = useMemo(() => {
return calculateMonteCarloData()
}, [
currentAge, retireAge, currentAssets, annualIncome, annualExpense,
annualExpenseAfterRetire, initialInvestment, returnRate, volatility,
savingsToInvestRatio, lifeEventsSimKey // ← idやlabelは含めない
])
lifeEvents オブジェクトをそのまま依存配列に入れると、ラベル変更のたびに1000回計算が走ってしまいます。age と amount のみを文字列化して依存させることで、不要な再計算を防いでいます。
Recharts でパーセンタイル帯グラフを描く
モンテカルロの結果を「帯グラフ」で表現するため、ComposedChart の Area を積み上げて使っています。Recharts には「帯」の概念がないため、差分を積み上げるというアプローチで実装しました。
// データを帯グラフ用に変換(差分を積み上げる)
const transformedData = data.map(d => ({
age: d.age,
base: d.p5, // ベースライン(透明で表示)
band1: d.p25 - d.p5, // 5〜25% の高さ
band2: d.p75 - d.p25, // 25〜75% の高さ
band3: d.p95 - d.p75, // 75〜95% の高さ
p50: d.p50 // 中央値(Lineとして表示)
}))
{/* ベースライン(透明、積み上げの基準) */}
<Area dataKey="base" stackId="1" fill="transparent" stroke="none" />
{/* 5〜25% と 75〜95%(薄い色) */}
<Area dataKey="band1" stackId="1" fill="#8884d8" fillOpacity={0.2} stroke="none" />
<Area dataKey="band2" stackId="1" fill="#8884d8" fillOpacity={0.4} stroke="none" />
<Area dataKey="band3" stackId="1" fill="#8884d8" fillOpacity={0.2} stroke="none" />
{/* 中央値の線 */}
<Line dataKey="p50" stroke="#333" strokeWidth={2} dot={false} />
stackId を揃えることで積み上げが実現し、透明なベースライン + 差分の積み上げで「任意の範囲を塗る」表現ができます。
MUI でレスポンシブ 2 カラムレイアウト
パラメータパネル(左)とグラフ(右)を lg ブレークポイント以上で横並び、それ以下で縦積みにしています。MUI の sx prop の flexDirection でブレークポイントごとの値を指定するだけで実現できます。
<Box sx={{
display: 'flex',
gap: 3,
flexDirection: { xs: 'column', lg: 'row' }, // lgで横並び
alignItems: 'flex-start'
}}>
{/* 左: パラメータパネル(固定幅) */}
<Paper sx={{ width: { xs: '100%', lg: '340px' }, flexShrink: 0 }}>
...
</Paper>
{/* 右: グラフ(残りスペースを使う) */}
<Box sx={{ flex: 1, minWidth: 0 }}>
...
</Box>
</Box>
minWidth: 0 を設定しているのは、flexboxで子要素がコンテナをはみ出す問題を防ぐためです。
Vercel へのデプロイ
Vite + React の構成は Vercel との相性が抜群で、デプロイは数分で完了しました。
手順はこれだけです:
- GitHub にリポジトリを push
- Vercel で「Import Project」からリポジトリを選択
- Build Command:
npm run build、Output Directory:distを確認(自動検出されます) - Deploy ボタンを押す
以降は main ブランチへの push で自動デプロイされます。フロントエンドのみの静的サイトなら、本当にこれだけで公開できます。
まとめ
- モンテカルロシミュレーション(Box-Muller + 1000試行)で運用の不確実性を可視化できました
-
useMemoの依存配列を工夫することで、無駄な再計算を抑えられました - Recharts の積み上げ Area を使ったパーセンタイル帯グラフは、少しトリッキーですが実現できました
- MUI のブレークポイント対応でレスポンシブレイアウトが簡単に書けました
FIREを目指している方も、そうでない方も、老後のお金を可視化するツールとして使ってみてください。パラメータを変えながらいじるだけでもなかなか楽しいです。




