生存者バイアスとは?バックテストが嘘をつく最大の理由【Python検証】
バックテストの結果が「思ったより良い」とき、戦略が優秀なのではなく、検証に使ったデータが最初から勝者だけを集めていた——ということがよくあります。これが 生存者バイアス(survivorship bias) です。
過剰最適化やコスト計上漏れと違って、生存者バイアスはコードをどれだけ丁寧に書いても防げません。データの入手方法そのものに原因があるからです。この記事では、その仕組みと影響の大きさをPythonで実際に測ってみます。
⚠️ 本記事は教育目的の情報共有です。投資は自己責任で行ってください。
目次
- 生存者バイアスとは
- なぜバックテストで必ず起きるのか
- Pythonで影響の大きさを測る
- 身近な生存者バイアス(ファンドランキング・成功談)
- 対策:完全には消せない、それでもできること
- まとめ
1. 生存者バイアスとは
生存者バイアスとは、「最後まで残ったものだけを見て、全体を判断してしまう」という誤りです。
有名な例が第二次大戦中の爆撃機の話です。帰還した機体の被弾箇所を集計して「よく撃たれる場所を補強しよう」と考えた——しかしこれは逆でした。帰還できた機体は、そこを撃たれても飛べたのです。本当に補強すべきは、帰還機に一つも被弾跡がない場所、つまりそこを撃たれた機体が落ちてしまった場所でした。
株式のバックテストでもまったく同じことが起きます。私たちが手にできるデータは、たいてい帰還した機体のデータだけなのです。
2. なぜバックテストで必ず起きるのか
たとえば「S&P500構成銘柄で20年前からモメンタム戦略を検証する」としましょう。多くの人はこうします。
# 今のS&P500構成銘柄リストを取得して…
tickers = get_sp500_tickers() # 2026年時点の500銘柄
data = yf.download(tickers, start="2006-01-01")
一見まっとうですが、この時点で検証は壊れています。理由は単純で、get_sp500_tickers() が返すのは 2026年の今、S&P500に入っている銘柄 だからです。
つまりこのリストには、
- 20年の間に倒産した企業
- 業績悪化で指数から外された企業
- 買収されて消えた企業
が一社も入っていません。過去20年を勝ち抜いた企業だけを選んで、過去20年を検証していることになります。「20年後に生き残る銘柄をあらかじめ知っていた」状態で運用しているのと同じで、これは典型的な先読み(look-ahead)です。
似た問題は個別銘柄のデータ取得でも起きます。上場廃止された銘柄は、多くの無料データソースからティッカーごと消えるため、そもそも取得できません。「データが取れた銘柄だけで検証する」という自然な流れが、そのまま生存者バイアスになります。
3. Pythonで影響の大きさを測る
「理屈はわかったが、どれくらい効くのか」が肝心です。実データで正しく測るには上場廃止銘柄を含むデータベースが要るので、ここではシミュレーションで大きさを体感します。
500銘柄・20年、毎年2%の銘柄が消える(破綻・買収・指数除外)という設定で、次の2つを比べます。
- (A) 正しい検証:その年に実在した銘柄すべてを等ウェイトで持つ
- (B) 生存者バイアスあり:20年後まで生き残った銘柄だけで、最初から検証する
import numpy as np
rng = np.random.default_rng(42)
N, YEARS = 500, 20 # 銘柄数・年数
MU, SIGMA = 0.08, 0.30 # 年率リターンの平均・標準偏差
FAIL = 0.02 # 毎年2%の銘柄が上場廃止
ret = rng.normal(MU, SIGMA, (N, YEARS)) # 各銘柄・各年のリターン
alive = np.ones((N, YEARS + 1), dtype=bool) # 生存フラグ
for t in range(YEARS):
# 消える銘柄はその年 -60% で終わる(破綻・整理は無傷では終わらない)
dying = alive[:, t] & (rng.random(N) < FAIL)
ret[dying, t] = -0.60
alive[:, t + 1] = alive[:, t] & ~dying
survivor = alive[:, -1] # 20年後まで生き残った銘柄
# (A) 正しい検証: その年に実在した銘柄すべてを等ウェイトで持つ
correct = np.array([ret[alive[:, t], t].mean() for t in range(YEARS)])
# (B) 生存者バイアスあり: 「今のリスト」で最初から検証
biased = ret[survivor].mean(axis=0)
cagr = lambda r: ((1 + r).prod() ** (1 / YEARS) - 1) * 100
print(f"生き残った銘柄 : {survivor.sum()} / {N}")
print(f"(A) 全銘柄で検証 : CAGR {cagr(correct):5.2f}%")
print(f"(B) 現存銘柄で検証 : CAGR {cagr(biased):5.2f}%")
実行結果はこうなります。
生き残った銘柄 : 328 / 500
(A) 全銘柄で検証 : CAGR 6.35%
(B) 現存銘柄で検証 : CAGR 7.74%
年率で約1.4ポイントの水増しです。1.4%と聞くと小さく感じますが、20年複利では最終資産に1.3倍の差がつきます。
ここで重要なのは、この差は戦略の良し悪しとまったく無関係だということ。銘柄選択もタイミングも何もしていない、ただの等ウェイト保有ですらこれだけズレます。実際の投資信託を対象にした研究でも、生存者バイアスによる押し上げは年率1%前後と報告されることが多く、シミュレーションの数字はおおむね現実的な水準です。
なお、消滅率(FAIL)を上げるほど差は広がります。小型株・新興市場・仮想通貨のように退場率が高い市場ほど、生存者バイアスは深刻になります。
4. 身近な生存者バイアス(ファンドランキング・成功談)
このバイアスはバックテストの外にも溢れています。
投資信託の「過去10年リターンランキング」 成績が悪いファンドは繰上償還されて一覧から消えます。残ったファンドだけを平均すれば、当然カテゴリ全体の実力より良く見えます。
「この手法で1億円」という体験談 同じ手法で退場した人はSNSで発信しません。見えているのは常に帰還した機体だけです。手法の勝率を語るには、失敗した人の数が要ります。
個別株の長期チャート 「20年持っていればこうなった」という銘柄は、20年後まで存在したから今チャートを見られるわけです。当時それを選べたかは、まったく別の問題です。
5. 対策:完全には消せない、それでもできること
正直に言うと、無料データだけで生存者バイアスを完全に消すのは困難です。上場廃止銘柄を含む履歴データ(CRSP、Norgate、Sharadarなど)は基本的に有料だからです。それでも、できることはあります。
① 「今のリスト」で過去を検証しない これが最大の一手です。特定時点の構成銘柄リストを使うなら、その時点で実在したリストを用意する。用意できないなら、その検証結果は「上振れしている」と断ってから読む。
② 指数ETF(SPY・VTIなど)を検証対象にする ETFの価格そのものには、構成銘柄の入れ替えや倒産の影響が既に織り込まれています。個別銘柄群ではなく指数を対象にする戦略なら、生存者バイアスはほぼ回避できます。レジームフィルタのような指数ベースの手法が扱いやすいのはこのためです。
③ バイアスの分だけ結果を割り引く 消せないなら、見積もって差し引く。「大型株中心なら年1%前後、小型株なら数%」を成績から引いて、それでも成立するかを確認します。私は他のバイアスと合わせて、バックテストのCAGRを一律で3〜4割ほど割り引いて判断しています。
④ 「良すぎる結果」を疑う癖をつける CAGR 50%のような数字が出たら、戦略の発見ではなくデータか手続きのバグを疑う。生存者バイアス・過剰最適化・コスト計上漏れの3つが、good-to-be-trueの主犯です。
6. まとめ
- 生存者バイアス=最後まで残ったものだけを見て全体を判断する誤り
- 「今の銘柄リストで過去を検証する」だけで発生し、コードでは防げない
- シミュレーションでは年率約1.4ポイント、20年で最終資産1.3倍の水増し
- 退場率の高い市場(小型株・新興市場)ほど深刻
- 対策は①当時のリストを使う ②指数ETFを対象にする ③割り引いて読む ④良すぎる結果を疑う
バックテストで戦う相手は市場ではなく、自分を騙しにくるデータです。生存者バイアスはその中でも最も静かで、最も気づきにくい相手だと思っています。
過剰最適化への対策はこちら → ウォークフォワード検証とは? 実運用前の最終確認はこちら → ペーパートレード(仮想売買)とは?
質問や感想は X (@QuantNobu) までどうぞ。
⚠️ 免責:本記事は教育目的の情報共有であり、投資助言ではありません。実際の投資判断はご自身の責任で行ってください。