Survivorship Bias in Python
A backtesting distortion caused by testing a strategy only on assets that survived to the present, ignoring those that failed or were delisted.
Definition
Survivorship Bias is a systematic error introduced when a backtest is conducted using a universe of assets that includes only those that currently exist — companies still listed today — while excluding the vast number of companies that went bankrupt, were delisted, merged, or otherwise disappeared during the test period. Because failed companies were typically in distress before their demise (low price, high volatility, deteriorating fundamentals), any strategy that is long equities will appear significantly better than it actually was in a survivorship-biased dataset.
Why It Matters in Backtesting
Studies estimate that survivorship bias inflates apparent equity strategy returns by 1.5% to 4.5% per year on average — enough to turn a marginally unprofitable strategy into an apparently excellent one. A backtest of a small-cap momentum strategy on today's Russell 2000 constituents is entirely invalid for historical analysis because the index composition in 2010 was completely different. Institutional-grade backtesting requires point-in-time constituent databases — snapshots of exactly which securities existed and were investable at each historical date, including all delistings.
Python Implementation
import pandas as pd
import numpy as np
def detect_survivorship_bias(current_universe: pd.Index, historical_universe: pd.Index,
historical_returns: pd.DataFrame) -> dict:
"""
Quantifies survivorship bias by comparing current vs historical universe.
current_universe: Index of tickers currently in the dataset.
historical_universe: Point-in-time Index of tickers that existed historically.
historical_returns: DataFrame of returns for the historical universe.
"""
delisted_tickers = historical_universe.difference(current_universe)
survived_tickers = historical_universe.intersection(current_universe)
survival_rate = len(survived_tickers) / len(historical_universe) if len(historical_universe) > 0 else 0.0
# Estimate return inflation: compare survivor returns vs full universe returns
if len(delisted_tickers) > 0 and all(t in historical_returns.columns for t in delisted_tickers):
survivor_avg_return = historical_returns[survived_tickers].mean(axis=1).mean() * 252
full_avg_return = historical_returns[historical_universe].mean(axis=1).mean() * 252
bias_estimate = survivor_avg_return - full_avg_return
else:
bias_estimate = None
return {
"original_universe_size": len(historical_universe),
"current_universe_size": len(current_universe),
"delisted_count": len(delisted_tickers),
"survival_rate": survival_rate,
"annualized_return_inflation_estimate": bias_estimate,
"bias_severity": "HIGH" if survival_rate < 0.7 else "MODERATE" if survival_rate < 0.85 else "LOW"
}Test this in a live environment
Stop running Jupyter notebooks locally. Paste this Survivorship Bias code directly into Valetha's Strategy Lab and run a full historical backtest in seconds.
Open the Python Strategy Lab