← Back to Algorithmic Glossary

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

Ready to find your edge ?

Start for Free