Chris Parmer — home

Market map (treemap) of S&P 500 top 50 by market cap

Finance Terminal

Example from the compendium of canonical charts

Finance Terminal — Market map (treemap) of S&P 500 top 50 by market cap

Python Code

"""Finance Terminal — Market map (treemap) of S&P 500 top 50 by market cap."""


import numpy as np
import pandas as pd
import plotly.graph_objects as go
import warnings
warnings.filterwarnings("ignore")


SP500_URL = "https://raw.githubusercontent.com/datasets/s-and-p-500-companies/main/data/constituents.csv"

def get_market_data():
    """Fetch top-50 S&P 500 constituents and their market cap + daily change."""
    try:
        import yfinance as yf
        sp500 = fetch_csv(SP500_URL)
        # Column names: Symbol, Security, GICS Sector
        name_col = next((c for c in sp500.columns if "Security" in c or c == "Name"), sp500.columns[1])
        sector_col = next((c for c in sp500.columns if "Sector" in c), sp500.columns[2])
        # Take first 50 symbols for rate-limit friendliness
        symbols = sp500["Symbol"].head(50).tolist()
        names = dict(zip(sp500["Symbol"], sp500[name_col]))
        sectors = dict(zip(sp500["Symbol"], sp500[sector_col]))

        # Batch download 2 days of close prices
        data = yf.download(symbols, period="2d", auto_adjust=True, progress=False)["Close"]
        # Ensure DataFrame even if one symbol
        if isinstance(data, pd.Series):
            data = data.to_frame()
        pct_change = data.pct_change().iloc[-1].dropna()

        # Get market caps via fast_info
        market_caps = {}
        for sym in pct_change.index:
            try:
                mc = yf.Ticker(sym).fast_info.market_cap
                if mc and mc > 0:
                    market_caps[sym] = mc
            except Exception:
                pass

        if len(market_caps) < 5:
            raise ValueError("Too few market caps fetched")

        rows = []
        for sym, mc in market_caps.items():
            if sym in pct_change.index:
                rows.append({
                    "symbol": sym,
                    "name": names.get(sym, sym),
                    "sector": sectors.get(sym, "Unknown"),
                    "market_cap": mc,
                    "pct_change": pct_change[sym] * 100,
                })
        return pd.DataFrame(rows)

    except Exception as e:
        print(f"  yfinance failed ({e}), using synthetic data")
        return None


def make_synthetic():
    """Synthetic S&P 500 market map data."""
    rng = np.random.default_rng(42)
    sectors_data = {
        "Technology": [("AAPL", "Apple"), ("MSFT", "Microsoft"), ("NVDA", "NVIDIA"),
                       ("GOOGL", "Alphabet"), ("META", "Meta"), ("AVGO", "Broadcom"),
                       ("AMD", "AMD"), ("ORCL", "Oracle")],
        "Healthcare": [("LLY", "Eli Lilly"), ("UNH", "UnitedHealth"), ("JNJ", "Johnson & Johnson"),
                       ("ABBV", "AbbVie"), ("MRK", "Merck")],
        "Financials": [("BRK-B", "Berkshire"), ("JPM", "JPMorgan"), ("V", "Visa"),
                       ("MA", "Mastercard"), ("BAC", "Bank of America")],
        "Consumer Disc.": [("AMZN", "Amazon"), ("TSLA", "Tesla"), ("HD", "Home Depot"),
                           ("MCD", "McDonald's"), ("NKE", "Nike")],
        "Industrials": [("GE", "GE"), ("CAT", "Caterpillar"), ("UPS", "UPS"),
                        ("HON", "Honeywell"), ("BA", "Boeing")],
        "Energy": [("XOM", "ExxonMobil"), ("CVX", "Chevron"), ("COP", "ConocoPhillips"),
                   ("SLB", "Schlumberger")],
        "Communication": [("NFLX", "Netflix"), ("DIS", "Disney"), ("T", "AT&T"),
                          ("VZ", "Verizon")],
        "Utilities": [("NEE", "NextEra"), ("DUK", "Duke Energy"), ("SO", "Southern Co")],
        "Materials": [("LIN", "Linde"), ("APD", "Air Products"), ("SHW", "Sherwin")],
        "Real Estate": [("PLD", "Prologis"), ("AMT", "American Tower"), ("EQIX", "Equinix")],
    }
    rows = []
    base_caps = {"Technology": 2e12, "Healthcare": 5e11, "Financials": 6e11,
                 "Consumer Disc.": 8e11, "Industrials": 3e11, "Energy": 4e11,
                 "Communication": 3e11, "Utilities": 1e11, "Materials": 1e11, "Real Estate": 1.5e11}
    for sector, stocks in sectors_data.items():
        n = len(stocks)
        caps = rng.dirichlet(np.ones(n)) * base_caps.get(sector, 2e11)
        changes = rng.normal(0.3, 1.5, n)
        for (sym, name), cap, chg in zip(stocks, caps, changes):
            rows.append({"symbol": sym, "name": name, "sector": sector,
                         "market_cap": cap, "pct_change": chg})
    return pd.DataFrame(rows)


def build_fig(df):
    # Build treemap ids/labels/parents
    # Sector nodes
    sectors = df["sector"].unique().tolist()
    sector_caps = df.groupby("sector")["market_cap"].sum()

    ids = ["S&P 500"] + sectors + df["symbol"].tolist()
    labels = ["S&P 500"] + sectors + df["name"].tolist()
    parents = [""] + ["S&P 500"] * len(sectors) + df["sector"].tolist()
    values = [0] + [sector_caps[s] for s in sectors] + df["market_cap"].tolist()

    pct = df["pct_change"].tolist()
    # Sectors get avg pct_change, root gets 0
    sector_avg = df.groupby("sector")["pct_change"].mean()
    colors = [0.0] + [sector_avg[s] for s in sectors] + pct

    # Clamp colors to [-2, 2]
    colors = [max(-2.0, min(2.0, c)) for c in colors]

    colorscale = [
        [0.0,  "#c0392b"],
        [0.25, "#e74c3c"],
        [0.45, "#f8c8c8"],
        [0.5,  "#f5f5f5"],
        [0.55, "#c8e6c9"],
        [0.75, "#27ae60"],
        [1.0,  "#1a7a40"],
    ]

    fig = go.Figure(go.Treemap(
        ids=ids,
        labels=labels,
        parents=parents,
        values=values,
        marker=dict(
            colors=colors,
            coloraxis="coloraxis",
        ),
        customdata=[""] * (1 + len(sectors)) + [f"{p:+.2f}%" for p in pct],
        texttemplate="<b>%{label}</b><br>%{customdata}",
        hovertemplate="<b>%{label}</b><br>Market Cap: $%{value:,.0f}<br>Change: %{customdata}<extra></extra>",
        textfont=dict(size=11),
        tiling=dict(packing="squarify"),
    ))

    fig.update_layout(
        coloraxis=dict(
            colorscale=colorscale,
            cmin=-2,
            cmid=0,
            cmax=2,
            colorbar=dict(
                title=dict(text="Daily %", side="right"),
                tickformat="+.1f",
                ticksuffix="%",
                thickness=12,
            ),
        ),
        margin=dict(t=40, b=20, l=10, r=10),
    )
    return fig


def generate():
    df = get_market_data()
    if df is None or len(df) < 5:
        df = make_synthetic()
        print("  using synthetic market data")
    else:
        # Reject if pct_change is nearly all zero (flat market data / weekend fetch)
        if df["pct_change"].abs().mean() < 0.05:
            print("  pct_change near-zero (flat data), using synthetic")
            df = make_synthetic()
        else:
            print(f"  fetched {len(df)} stocks from yfinance")

    fig = build_fig(df)
    return fig


if __name__ == "__main__":
    generate()

Made with Plotly