「まとまったお金があるけど、一括で投資するのは怖い。やっぱり毎月コツコツ積立の方が安全では?」——投資を始める多くの人がぶつかるこの問い、感覚ではなくデータで決着をつけましょう。
筆者は製造業の開発現場で、DX推進やデータ分析に携わるエンジニアです。ものづくりの現場では「バッチ処理とストリーム処理のどちらを採用するか」を要件に応じて使い分けますが、実はこの考え方がドルコスト平均法(DCA)と一括投資の本質的な違いを理解する鍵になります。
この記事では、Pythonのyfinanceライブラリを使い、S&P500の実データ20年分でDCA vs 一括投資を徹底シミュレーションします。「いつ始めても同じ結果なのか?」「暴落直前に一括投資したら?」——読み終わる頃には、自分の状況に合った投資方法を、データに基づいて選べるようになります。(記事#01から読み始めるのがおすすめです。前回の記事#04では、インデックス vs 個別株を20年データで検証しました。)
結論:統計的には一括投資の方が有利な期間が多い。しかし、DCAは「最悪ケースの損失を抑える」効果がある。どちらが正解かは、あなたのリスク許容度と手元資金の状況で決まる。
動作環境
- Python 3.11 / yfinance 0.2.36 / matplotlib 3.8 / pandas 2.1 / numpy 1.26
- Google Colab でもローカル環境でも動作します
- 所要時間:約40分(コードのコピペ+実行+条件変更)
- インストール:
pip install yfinance matplotlib pandas numpy
免責事項
本記事は投資助言を目的としたものではなく、技術・分析手法の紹介です。記事中のシミュレーション結果は過去データに基づく検証であり、将来のリターンを保証するものではありません。投資判断はご自身の責任で行ってください。
ドルコスト平均法と一括投資、何が違うのか
まずは2つの投資方法を整理します。
| 項目 | ドルコスト平均法(DCA) | 一括投資(Lump Sum) |
|---|---|---|
| 投資タイミング | 定期的に一定額を分散して投資 | 手元資金を一度に全額投資 |
| 平均取得単価 | 高値でも安値でも買うため平均化される | 投資時点の価格で固定 |
| 心理的負担 | 小さい(自動化しやすい) | 大きい(タイミング判断が必要) |
| 機会損失 | 未投資分が現金のまま待機 | 即座に市場に全額投入 |
よく「DCAはリスクを下げる」と言われますが、正確に言えば「購入単価を平均化する」効果であり、リターンそのものを上げるわけではありません。一方、一括投資は市場が長期的に右肩上がりであれば、早く市場に入った分だけ複利の恩恵を長く受けられるというメリットがあります。
エンジニア的に言い換えると
DCAと一括投資の違いは、データ処理におけるストリーム処理とバッチ処理の違いに似ています。
- DCA ≒ ストリーム処理:データ(資金)を少量ずつ継続的に処理する。各時点の状態に応じた処理ができるが、全データを使った最適化はできない
- 一括投資 ≒ バッチ処理:データ(資金)をまとめて一括処理する。効率は高いが、処理タイミングの影響を大きく受ける
どちらが優れているかは「データの性質(市場環境)」と「処理の制約(リスク許容度)」で決まります。
S&P500の月次データを取得する
記事#04と同様にyfinanceでS&P500のデータを取得します。今回は月次データに変換して使います。
import yfinance as yf
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
# --- 日本語フォントの設定(環境に合わせて変更してください) ---
# Mac: plt.rcParams['font.family'] = 'Hiragino Sans'
# Windows: plt.rcParams['font.family'] = 'Yu Gothic'
# Linux: plt.rcParams['font.family'] = 'Noto Sans CJK JP'
# --- S&P500の月次データを取得 ---
START: str = "2005-01-01"
END: str = "2025-01-01"
raw_data = yf.download("^GSPC", start=START, end=END, progress=False)
# MultiIndex対応
if isinstance(raw_data.columns, pd.MultiIndex):
close_prices = raw_data[("Close", "^GSPC")]
else:
close_prices = raw_data["Close"]
# 月初の終値に変換(毎月1回の投資タイミングを想定)
monthly_prices: pd.Series = close_prices.resample("MS").first().dropna()
print(f"取得期間: {monthly_prices.index[0].strftime('%Y-%m')} 〜 {monthly_prices.index[-1].strftime('%Y-%m')}")
print(f"月数: {len(monthly_prices)}")
print(f"開始時の価格: ${monthly_prices.iloc[0]:,.0f}")
print(f"終了時の価格: ${monthly_prices.iloc[-1]:,.0f}")
実装①:ドルコスト平均法のシミュレーション
まずはDCA(毎月定額積立)のシミュレーションを実装します。毎月一定額を投資した場合、最終的にいくらになるかを計算します。
from typing import TypedDict
class DCAResult(TypedDict):
"""ドルコスト平均法のシミュレーション結果"""
total_invested: float # 投資総額
final_value: float # 最終評価額
total_return_pct: float # 総リターン(%)
total_units: float # 累計口数
avg_cost: float # 平均取得単価
monthly_values: pd.Series # 月次の評価額推移
def simulate_dca(
prices: pd.Series,
monthly_amount: float = 30_000,
) -> DCAResult:
"""ドルコスト平均法のシミュレーション
Args:
prices: 月次の価格データ(Series)
monthly_amount: 毎月の投資額(デフォルト: 30,000円)
Returns:
DCAResult: シミュレーション結果
"""
units: float = 0.0 # 累計口数
total_invested: float = 0.0 # 累計投資額
values: list[float] = []
for price in prices:
# 今月の購入口数 = 投資額 ÷ 価格
new_units: float = monthly_amount / price
units += new_units
total_invested += monthly_amount
# 現在の評価額 = 累計口数 × 現在価格
values.append(units * price)
final_value: float = values[-1]
return DCAResult(
total_invested=total_invested,
final_value=final_value,
total_return_pct=(final_value / total_invested - 1) * 100,
total_units=units,
avg_cost=total_invested / units,
monthly_values=pd.Series(values, index=prices.index),
)
# --- 実行 ---
dca_result: DCAResult = simulate_dca(monthly_prices, monthly_amount=30_000)
print(f"投資総額: ¥{dca_result['total_invested']:,.0f}")
print(f"最終評価額: ¥{dca_result['final_value']:,.0f}")
print(f"リターン: {dca_result['total_return_pct']:+.1f}%")
print(f"平均取得単価: ${dca_result['avg_cost']:,.0f}")
注意:為替の影響について
このシミュレーションではS&P500の価格をドル建てで取得し、投資額を円で設定しています。簡易的な検証として「毎月一定額で価格を割って口数を求める」ロジックを使っていますが、実際の投資では為替レートの変動が大きく影響します。厳密な円建てシミュレーションには為替データの組み込みが必要ですが、この記事では「DCA vs 一括投資の投資手法の比較」に焦点を絞るため、為替影響は割愛しています。
実装②:一括投資のシミュレーション
次に、同じ総額を最初に一括で投資した場合のシミュレーションです。DCAで投資する総額と同じ金額を、初月に全額投資します。
class LumpSumResult(TypedDict):
"""一括投資のシミュレーション結果"""
total_invested: float # 投資総額
final_value: float # 最終評価額
total_return_pct: float # 総リターン(%)
monthly_values: pd.Series # 月次の評価額推移
def simulate_lump_sum(
prices: pd.Series,
total_amount: float,
) -> LumpSumResult:
"""一括投資のシミュレーション
Args:
prices: 月次の価格データ(Series)
total_amount: 投資総額
Returns:
LumpSumResult: シミュレーション結果
"""
# 初月に全額投資
initial_price: float = prices.iloc[0]
units: float = total_amount / initial_price
# 各月の評価額
values: pd.Series = units * prices
final_value: float = values.iloc[-1]
return LumpSumResult(
total_invested=total_amount,
final_value=final_value,
total_return_pct=(final_value / total_amount - 1) * 100,
monthly_values=values,
)
# --- DCAと同じ総額で一括投資 ---
lump_result: LumpSumResult = simulate_lump_sum(
monthly_prices,
total_amount=dca_result["total_invested"],
)
print(f"投資総額: ¥{lump_result['total_invested']:,.0f}")
print(f"最終評価額: ¥{lump_result['final_value']:,.0f}")
print(f"リターン: {lump_result['total_return_pct']:+.1f}%")
DCA vs 一括投資:20年間の評価額を比較する
2つのシミュレーション結果を重ねてグラフ化します。
def plot_dca_vs_lump(
dca: DCAResult,
lump: LumpSumResult,
monthly_amount: float = 30_000,
) -> None:
"""DCA vs 一括投資の評価額推移を比較グラフで表示
Args:
dca: DCAのシミュレーション結果
lump: 一括投資のシミュレーション結果
monthly_amount: 毎月の積立額
"""
fig, ax = plt.subplots(figsize=(12, 7))
# 評価額の推移
ax.plot(
dca["monthly_values"].index,
dca["monthly_values"] / 10_000,
label=f"DCA (monthly ¥{monthly_amount:,.0f})",
color="#38a169",
linewidth=2.5,
)
ax.plot(
lump["monthly_values"].index,
lump["monthly_values"] / 10_000,
label=f"Lump Sum (¥{lump['total_invested']:,.0f} at start)",
color="#3182ce",
linewidth=2.5,
)
# 投資総額の推移(DCA)
n_months = len(dca["monthly_values"])
invested_line = pd.Series(
[monthly_amount * (i + 1) / 10_000 for i in range(n_months)],
index=dca["monthly_values"].index,
)
ax.plot(
invested_line.index,
invested_line,
label="Total Invested (DCA)",
color="#718096",
linewidth=1.5,
linestyle=":",
)
ax.set_xlabel("Year", fontsize=12)
ax.set_ylabel("Value (万円)", fontsize=12)
ax.set_title("DCA vs Lump Sum Investment: S&P500 (2005-2025)", fontsize=14)
ax.legend(fontsize=11, loc="upper left")
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig("dca_vs_lump_sum.png", dpi=150)
plt.show()
plot_dca_vs_lump(dca_result, lump_result)
このグラフから読み取れる重要なポイントがあります。
- 一括投資は序盤からリードする。市場が長期的に上昇する場合、早く全額を投入した方が複利の恩恵を長く受けられる
- DCAは暴落時のダメージが小さい。2008年のリーマンショック時、一括投資の評価額は大きく下落するが、DCAはまだ投資途中なので影響が限定的
- DCAは暴落後の回復局面で追い上げる。安値で多くの口数を買えるため、回復時に加速する
ポイント:一括投資が有利な「条件」
一括投資が有利になるのは「市場が長期的に右肩上がり」の場合です。過去のS&P500データではこの条件が成り立つ期間が多いため、統計的には一括投資が有利とされています。Vanguardの研究(”Dollar-cost averaging just means taking risk later”, 2012)でも、約3分の2の期間で一括投資がDCAを上回るという結果が出ています。
実装③:「いつ始めても同じ?」ローリング分析で検証
ここまでは2005年スタートの1パターンだけを見ました。しかし、投資開始タイミングによって結果は変わるはずです。任意の開始時点で10年間投資した場合、DCAと一括投資のどちらが有利だったかを、ローリング(スライドウィンドウ)方式で全パターン検証します。
class RollingResult(TypedDict):
"""ローリング比較の結果"""
start_date: str # 開始日
dca_return_pct: float # DCAのリターン(%)
lump_return_pct: float # 一括投資のリターン(%)
lump_wins: bool # 一括投資が勝ったか
def rolling_comparison(
prices: pd.Series,
investment_years: int = 10,
monthly_amount: float = 30_000,
) -> pd.DataFrame:
"""ローリング方式でDCA vs 一括投資を比較
Args:
prices: 月次の価格データ
investment_years: 投資期間(年)
monthly_amount: 毎月の積立額
Returns:
全開始時点の比較結果DataFrame
"""
n_months: int = investment_years * 12
results: list[RollingResult] = []
for i in range(len(prices) - n_months):
window: pd.Series = prices.iloc[i:i + n_months]
# DCA
dca: DCAResult = simulate_dca(window, monthly_amount)
# 一括投資(DCAと同じ総額)
lump: LumpSumResult = simulate_lump_sum(window, dca["total_invested"])
results.append(RollingResult(
start_date=window.index[0].strftime("%Y-%m"),
dca_return_pct=dca["total_return_pct"],
lump_return_pct=lump["total_return_pct"],
lump_wins=lump["total_return_pct"] > dca["total_return_pct"],
))
return pd.DataFrame(results)
# --- 10年間のローリング比較 ---
rolling_df: pd.DataFrame = rolling_comparison(monthly_prices, investment_years=10)
lump_win_rate: float = rolling_df["lump_wins"].mean() * 100
print(f"検証パターン数: {len(rolling_df)}")
print(f"一括投資の勝率: {lump_win_rate:.1f}%")
print(f"DCAの勝率: {100 - lump_win_rate:.1f}%")
def plot_rolling_comparison(rolling_df: pd.DataFrame) -> None:
"""ローリング比較の結果をグラフ化
Args:
rolling_df: rolling_comparison()の結果DataFrame
"""
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 10))
# --- 上段: リターンの比較 ---
ax1.plot(
rolling_df["start_date"],
rolling_df["dca_return_pct"],
label="DCA",
color="#38a169",
linewidth=1.5,
)
ax1.plot(
rolling_df["start_date"],
rolling_df["lump_return_pct"],
label="Lump Sum",
color="#3182ce",
linewidth=1.5,
)
ax1.set_ylabel("Total Return (%)", fontsize=12)
ax1.set_title("Rolling 10-Year: DCA vs Lump Sum Returns by Start Date", fontsize=14)
ax1.legend(fontsize=11)
ax1.grid(True, alpha=0.3)
# x軸ラベルを間引く
tick_positions = range(0, len(rolling_df), 12)
ax1.set_xticks(tick_positions)
ax1.set_xticklabels(
[rolling_df["start_date"].iloc[i] for i in tick_positions],
rotation=45,
)
# --- 下段: リターン差分 ---
diff = rolling_df["lump_return_pct"] - rolling_df["dca_return_pct"]
colors = ["#3182ce" if d > 0 else "#e53e3e" for d in diff]
ax2.bar(range(len(diff)), diff, color=colors, width=1.0, alpha=0.7)
ax2.axhline(y=0, color="#333", linewidth=0.5)
ax2.set_ylabel("Lump Sum - DCA (%p)", fontsize=12)
ax2.set_title("Return Difference (positive = Lump Sum wins)", fontsize=14)
ax2.set_xticks(list(tick_positions))
ax2.set_xticklabels(
[rolling_df["start_date"].iloc[i] for i in tick_positions],
rotation=45,
)
ax2.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig("rolling_dca_vs_lump.png", dpi=150)
plt.show()
plot_rolling_comparison(rolling_df)
この分析で見えてくるのは、以下の事実です。
- 多くの開始時点で一括投資がDCAを上回る——これは市場が長期的に上昇傾向にあるため
- DCAが勝つのは「暴落直前に始めたケース」——2007年後半〜2008年頃に投資を開始した場合、一括投資は暴落の直撃を受けるが、DCAは暴落後の安値で多く買えるため巻き返す
- 差が最も大きいのは上昇相場の初期——安値圏で一括投資できた場合のリターンは圧倒的
実装④:「暴落直前に一括投資」最悪ケースの検証
投資で最も怖いシナリオ——「まとまったお金を一括投資した直後に暴落が来た」ケースを検証します。2007年10月(リーマンショック前の高値圏)に投資を開始した場合を見てみましょう。
def worst_case_analysis(
prices: pd.Series,
start_date: str,
years: int = 10,
monthly_amount: float = 30_000,
) -> None:
"""最悪ケースのシミュレーションと可視化
Args:
prices: 月次の価格データ
start_date: 開始年月(YYYY-MM形式)
years: 投資期間(年)
monthly_amount: 毎月の積立額
"""
# 指定開始日からのデータを切り出し
mask = prices.index >= pd.Timestamp(start_date)
window = prices[mask].iloc[:years * 12]
dca: DCAResult = simulate_dca(window, monthly_amount)
lump: LumpSumResult = simulate_lump_sum(window, dca["total_invested"])
print(f"=== 開始: {start_date} / 期間: {years}年 ===")
print(f"投資総額: ¥{dca['total_invested']:,.0f}")
print()
print(f"[DCA] 最終評価額: ¥{dca['final_value']:,.0f} リターン: {dca['total_return_pct']:+.1f}%")
print(f"[一括] 最終評価額: ¥{lump['final_value']:,.0f} リターン: {lump['total_return_pct']:+.1f}%")
print()
# 最大ドローダウンの計算
dca_values = dca["monthly_values"]
dca_invested = pd.Series(
[monthly_amount * (i + 1) for i in range(len(dca_values))],
index=dca_values.index,
)
dca_drawdown = ((dca_values - dca_invested) / dca_invested).min() * 100
lump_values = lump["monthly_values"]
lump_drawdown = ((lump_values - lump["total_invested"]) / lump["total_invested"]).min() * 100
print(f"[DCA] 最大含み損(対投資額): {dca_drawdown:.1f}%")
print(f"[一括] 最大含み損(対投資額): {lump_drawdown:.1f}%")
# グラフ
fig, ax = plt.subplots(figsize=(12, 7))
ax.plot(
dca_values.index,
dca_values / 10_000,
label="DCA",
color="#38a169",
linewidth=2.5,
)
ax.plot(
lump_values.index,
lump_values / 10_000,
label="Lump Sum",
color="#3182ce",
linewidth=2.5,
)
ax.plot(
dca_invested.index,
dca_invested / 10_000,
label="Total Invested",
color="#718096",
linewidth=1.5,
linestyle=":",
)
ax.set_xlabel("Year", fontsize=12)
ax.set_ylabel("Value (万円)", fontsize=12)
ax.set_title(f"Worst Case: Starting {start_date} ({years}-Year)", fontsize=14)
ax.legend(fontsize=11, loc="upper left")
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig(f"worst_case_{start_date}.png", dpi=150)
plt.show()
# --- リーマンショック直前(2007年10月)に開始 ---
worst_case_analysis(monthly_prices, "2007-10", years=10)
結果を見ると、暴落直前に投資を開始した「最悪ケース」では。
- 一括投資は暴落で大きな含み損を抱える——リーマンショックで投資額の-50%以上の含み損が発生
- DCAは含み損が限定的——まだ投資途中のため、暴落の影響は投資済み分のみ。さらに暴落後の安値で多く買える
- しかし10年後の最終リターンでは一括投資が逆転する場合もある——2007年開始でも、2017年にはS&P500は大きく回復しているため
重要:「最悪ケース」をどう考えるか
統計的には一括投資の方がリターンが高い期間が多い。しかし、最悪ケースの損失は一括投資の方が圧倒的に大きい。ここで問われるのは「平均的にどちらが有利か」ではなく、「自分が最悪ケースに耐えられるか」です。この問いへの答えが、あなたにとっての正解を決めます。
シミュレーション結果の総括
| 指標 | DCA(毎月積立) | 一括投資 |
|---|---|---|
| 統計的な勝率(10年ローリング) | 約30-40% | 約60-70% |
| 平均リターン | やや低い | やや高い |
| 最悪ケースの含み損 | 限定的 | 大きい(-50%超も) |
| 心理的負担 | 小さい | 大きい |
| 自動化のしやすさ | 非常に高い(証券会社の自動積立) | 低い(タイミング判断が必要) |
※ 上記はS&P500の過去データに基づく概算値です。投資期間や市場環境により結果は変わります。
本業で学んだ「バッチ処理 vs ストリーム処理」の判断基準
筆者が本業で携わったプロジェクトの一つに、工場の生産データを分析基盤に取り込むデータパイプラインの設計がありました。そこで直面したのが「バッチ処理 vs ストリーム処理」の選択です。
当初は「リアルタイムで処理した方がいいに決まっている」と考え、ストリーム処理のアーキテクチャを提案しました。しかし、実際に要件を分析すると、以下の問題が見えてきました。
- ストリーム処理は運用コストが高い——障害時のリカバリ、順序保証、重複排除など、考慮すべき点が多い
- データの利用タイミングは翌日の朝会——リアルタイムである必要がなかった
- チームにストリーム処理の運用経験がなかった——学習コストと運用リスクが見合わない
結果として、「1日1回のバッチ処理」を採用しました。技術的に劣る選択かもしれませんが、チームの運用能力とデータの利用サイクルに合った選択でした。
投資に置き換えると
- 一括投資 ≒ バッチ処理:理論的にはスループットが高い(リターンが高い)。しかし、処理タイミングを誤ると影響が大きく、運用者(投資家)の経験値が必要
- DCA ≒ ストリーム処理(ただし小さく始められるパターン):理論的な効率はやや劣るが、障害(暴落)への耐性が高く、運用が簡単。「自動積立」の設定だけで始められる
「最適解」より「運用可能な解」を選ぶ——これはDXの現場でも投資でも同じ原則です。投資初心者にDCAが勧められるのは、「リスク許容度と運用能力に合った選択」だからです。
まとめ:データが示す3つの事実
S&P500の20年分データでDCA vs 一括投資をシミュレーションした結果、以下のことが確認できました。
- 統計的には一括投資の方が有利な期間が多い。10年ローリング分析で約60-70%の開始時点で一括投資がDCAのリターンを上回る。これは市場が長期的に右肩上がりであることの裏返し
- DCAは最悪ケースの損失を大幅に抑える。暴落直前に開始した場合、一括投資は-50%超の含み損を抱えるが、DCAは投資途中のため影響が限定的
- 「どちらが正解か」は、あなたの状況で決まる。手元に投資できるまとまった資金があり、暴落に耐えられるなら一括投資。毎月の収入から少しずつ投資したいなら、DCAが自然な選択
今日からできる3つのアクション
アクション1:自分の投資条件でシミュレーションしてみる
この記事のコードをコピーして、monthly_amountを自分の月額投資予定額に変更してみてください。月1万円でも月5万円でも、DCAの挙動を自分の数字で確認することが重要です。
アクション2:最悪ケースの含み損を「金額」で確認する
worst_case_analysis関数の結果で表示される含み損を、自分の投資額に置き換えてみてください。「-50%」という数字は抽象的ですが、「100万円が50万円になる」と考えると、自分のリスク許容度が見えてきます。
アクション3:証券口座で自動積立を設定する
「DCAが自分に合っている」と感じたら、証券口座の自動積立機能を設定しましょう。多くの証券会社では、月額100円からインデックスファンドの自動積立が可能です。記事#02で解説した「自動化」の原則——まさにこれが実践です。
次回予告:モンテカルロ法で将来リターンを確率的に考える
次回(記事#06)では、モンテカルロ・シミュレーションを使って「将来のリターンを確率分布で捉える」方法を実装します。
- 「20年後に資産が2倍になる確率は何%?」を計算する
- 1万回のランダムシミュレーションで将来の資産分布を可視化
- 「最悪の5%」のシナリオを見ることで、リスクを実感する
今回の「DCA vs 一括投資」で実データの分析力を身につけました。次回は確率的な思考を投資に応用します。コードのチラ見せです。
import numpy as np
# モンテカルロ・シミュレーションの概要(次回の記事で詳しく実装します)
n_simulations: int = 10_000
years: int = 20
annual_return: float = 0.07
annual_std: float = 0.18
# ランダムウォークで資産推移を生成
final_values = []
for _ in range(n_simulations):
returns = np.random.normal(annual_return, annual_std, years)
final_value = np.prod(1 + returns)
final_values.append(final_value)
# 「2倍以上になる確率」を計算(次回の記事で詳しく実装します)
シリーズ全体像:投資×DX 3段階モデル
- 基礎編 #01〜#10:インデックス投資 × DX思想(標準化・データドリブン・自動化)
- 応用編 #11〜#20:高配当株 × データパイプライン(Python自動スクリーニング)
- 発展編 #21〜#30:成長株CAN-SLIM × アーキテクチャ設計力
▶ 記事#01からスタート | 記事#02 DX3原則 | 記事#03 複利Python | 記事#04 インデックスvs個別株 | 記事#05 本記事 | 記事#06 モンテカルロ法
投資の具体的な始め方は記事#09「NISA・iDeCoの最適活用をエンジニア的に設計する」で詳しく解説予定です。
免責事項
本記事は投資助言を目的としたものではなく、技術・分析手法の紹介です。記事中のシミュレーション結果は過去データに基づく検証であり、将来のリターンを保証するものではありません。投資判断はご自身の責任で行ってください。

コメント