← Home

Analysing Damodaran's S&P500 analysis

AIM: check if Damodaran's analysis suggest it is an appropriate time to invest in the S&P500

Aswart Damodaran (website) is a SternU professor. I watched most of his teeaching materials, including his full valuation class. He is very analytical in the way he dissects the components that contribute to the value of a company. Similarly, and this is relevant to this notebook, he decomposes the value of stock expected returns, using the S&P proxy.

Basically, the expected stock return is decomposed into:

  • RFR (Risk Free Rate), proxied as the 10Y US Treasury yield that is causally related to the expected USD inflation and therefore also the future stability of United States
  • Implied ERP (Equity Risk Premium), which is computed from the current S&P500 price and the expected future earnings. It captures the willingnes of investors to risk over equities to have higher return at the price of possible drawdowns.

He updates this analysis every first day of the month.

Note that this analysis is US-centric, but considering that US is currently 55% of the world market cap (FTSE All World Index, May 2023), and geopolitically the driver of the open-market economy, it is fair to focus on it, also considering the larger availability of data.

In [1]:
import pandas as pd # Note: needs xlrd and openpyxl to be installed to read excel files
import plotly.express as px
import plotly.graph_objects as go
import plotly.io as pio
import ssl
from datetime import datetime

pd.set_option('plotting.backend', 'plotly')
pio.renderers.default = 'notebook_connected'
ssl._create_default_https_context = ssl._create_unverified_context # Needed to download data

DEBUG = False
In [ ]:
# There are three kinds of data:
# - yearly data, since 1960
# - monthly data, since September 2008
# - daily data, for exceptional high-volatility events
url_yearly = "https://pages.stern.nyu.edu/~adamodar/pc/datasets/histimpl.xls"
url_monthly = "https://pages.stern.nyu.edu/~adamodar/pc/implprem/ERPbymonth.xlsx"
url_dayly_2008 = "https://pages.stern.nyu.edu/~adamodar/pc/blog/ERPbyDay2008Crisis.xlsx"
url_dayly_covid = "https://pages.stern.nyu.edu/~adamodar/pc/blog/ERPbyDayCOVID.xlsx"
url_dayly_tariff = "https://pages.stern.nyu.edu/~adamodar/pc/blog/TariffERPbyday.xlsx"
url_dayly_iran = "https://pages.stern.nyu.edu/~adamodar/pc/blog/AlldataMarch2026.xlsx"
excel_engine = "calamine"


LAST_YEAR = datetime.now().year-1 # Modify accordingly, to skip the part under the main table
dfy = (
    pd.read_excel(url_yearly, sheet_name="Historical Impl Premiums", skiprows=6, nrows=LAST_YEAR-1959, engine=excel_engine)
)
dfm = (
    pd.read_excel(url_monthly, sheet_name="Historical ERP", engine=excel_engine)
)
dfd = {
    "2008": pd.read_excel(url_dayly_2008, sheet_name="Main Data", skiprows=2, engine=excel_engine),
    "COVID": pd.read_excel(url_dayly_covid, sheet_name="Raw Data", skiprows=0, engine=excel_engine),
    "TARIFF": pd.read_excel(url_dayly_tariff, sheet_name="Sheet1", skiprows=0, engine=excel_engine),
    "IRAN": pd.read_excel(url_dayly_iran, sheet_name="Sheet1", skiprows=1, engine=excel_engine)
}

if DEBUG:
    display(dfy)
    display(dfm)
    for key, dfdaily in dfd.items():
        print(f"Daily data for {key}:")
        display(dfdaily)
In [3]:
# Checkpoint to reload original data if I mess up
dfm_orig, dfy_orig, dfd_orig = dfm.copy(), dfy.copy(), dfd.copy()
In [4]:
# Clean data and make column names consistent
dfy, dfm, dfd = dfy_orig.copy(), dfm_orig.copy(), dfd_orig.copy()

# Identify for all: rERP, T.Bond Rate, S&P 500, rGROWTH (where r stand for reference)
dfy = (
    dfy
    .assign(
        Date=lambda x: pd.to_datetime((x["Year"]+1).astype(str)), # Date refers to the end of the year, so I will add 1 to convert it to the first day of the next year
        rERP=lambda x: x["Implied ERP (FCFE)"],
        rGROWTH=lambda x: x["Analyst Growth Estimate"],
        ) 
)
if DEBUG:
    print("Yearly data")
    display(dfy)
dfm = (
    dfm
    .assign(
        Date=lambda x: pd.to_datetime(x["Start of month"]),
        rERP=lambda x: x["ERP (T12m)"],
        rGROWTH=lambda x: x["Expected growth rate"],
        ) 
    .dropna(subset=["Date"])
    .drop(index=192) # problemetic data
)
if DEBUG:
    print("Monthly data")
    display(dfm)
dfd = (
    pd.concat([ # The following is necessary to fetch consistent column names across the different datasets
        dfd["2008"].assign(**{
            "Date": lambda x: pd.to_datetime(x["Date"]),
            "rERP": lambda x: x["Implied Premium"],
            "T.Bond Rate": lambda x: x["10-yr Treasuries"],
            "rGROWTH": lambda x: x["Earnings g- next 5 years"],
            "S&P 500": lambda x: x["S&P 500"],
            "Event": lambda x: "2008 Financial Crisis",
        }),
        dfd["COVID"].assign(**{
            "Date": lambda x: pd.to_datetime(x["Date"]),
            "rERP": lambda x: x["ERP"],
            "T.Bond Rate": lambda x: x["T. Bond Rate"],
            "rGROWTH": lambda x: pd.NA,
            "S&P 500": lambda x: x["S&P 500"],
            "Event": lambda x: "COVID-19 Pandemic",
        }),
        dfd["TARIFF"].assign(**{
            "Date": lambda x: pd.to_datetime(x["Date"]),
            "rERP": lambda x: x["Implied ERP"],
            "T.Bond Rate": lambda x: x["T. Bond Rate"],
            "rGROWTH": lambda x: pd.NA,
            "S&P 500": lambda x: pd.to_numeric(x["S&P 500"].astype("string").str.replace(",", ".", regex=False), errors="raise"),
            "Event": lambda x: "US Tariff War",
        }).dropna(subset=["rERP"]),
        dfd["IRAN"].assign(**{
            "Date": lambda x: pd.to_datetime(x["Date"]),
            "rERP": lambda x: x["ERP"],
            "T.Bond Rate": lambda x: x["10-year T.Bond"],
            "rGROWTH": lambda x: pd.NA,
            "S&P 500": lambda x: x["Close"],
            "Event": lambda x: "Iran Sanctions",
        })
    ])
    # Now fill the dates in between to avoid straight lines in the plot
    .set_index("Date")
    .pipe(
        lambda df: df.reindex(
            pd.date_range(
                start=df.index.min(),
                end=df.index.max(),
                freq="D"
            )
        )
    )
    .reset_index()
    .rename(columns={"index": "Date"})
)
if DEBUG:
    print("Daily data")
    display(dfd[["Date", "rERP", "T.Bond Rate", "rGROWTH", "S&P 500", "Event"]])
In [5]:
fig = go.Figure()
fig.add_trace(go.Scatter(x=dfy["Date"], y=dfy["rERP"], mode="lines+markers", name="Yearly Estimate", 
                line=dict(color='limegreen', width=3)))
fig.add_trace(go.Scatter(x=dfm["Date"], y=dfm["rERP"], mode="lines", name="Monthly  Estimate",
                line=dict(color='darkgreen', width=2)))
fig.add_trace(go.Scatter(x=dfd["Date"], y=dfd["rERP"], mode="lines", name="Daily  Estimate",
                line=dict(color='blue', width=1)))
fig.update_layout(xaxis_title="Date of Estimation", yaxis_title="Implied ERP", yaxis_type="linear", yaxis_tickformat='.2%')
fig.show()

fig = go.Figure()
fig.add_trace(go.Scatter(x=dfy["Date"], y=dfy["T.Bond Rate"], mode="lines+markers", name="RFR Yearly Estimate",
              line=dict(color='violet', width=3)))
fig.add_trace(go.Scatter(x=dfm["Date"], y=dfm["T.Bond Rate"], mode="lines", name="RFR Monthly Estimate",
              line=dict(color='magenta', width=2)))
fig.add_trace(go.Scatter(x=dfd["Date"], y=dfd["T.Bond Rate"], mode="lines", name="RFR Daily Estimate",
              line=dict(color='black', width=1)))

fig.add_trace(go.Scatter(x=dfy["Date"], y=dfy["T.Bond Rate"]+dfy["rERP"], mode="lines+markers", name="RFR+ERP Yearly Estimate",
              line=dict(color='limegreen', width=3)))
fig.add_trace(go.Scatter(x=dfm["Date"], y=dfm["T.Bond Rate"]+dfm["rERP"], mode="lines", name="RFR+ERP  Monthly Estimate",
              line=dict(color='darkgreen', width=2)))
fig.add_trace(go.Scatter(x=dfd["Date"], y=dfd["T.Bond Rate"]+dfd["rERP"], mode="lines", name="RFR+ERP  Daily Estimate",
            line=dict(color='blue', width=1)))

fig.update_layout(xaxis_title="Date of Estimation", yaxis_title="T.Bond Rate + ERP", yaxis_type="linear", yaxis_tickformat=',.2%')
fig.show()

fig = go.Figure()
fig.add_trace(go.Scatter(x=dfy["Date"], y=dfy["rGROWTH"], mode="lines+markers", name="Yearly Estimate",
            line=dict(color='limegreen', width=3)))
fig.add_trace(go.Scatter(x=dfm["Date"], y=dfm["rGROWTH"], mode="lines", name="Monthly  Estimate",
            line=dict(color='darkgreen', width=2)))
fig.update_layout(xaxis_title="Date of Estimation", yaxis_title="Growth estimate", yaxis_type="linear", yaxis_tickformat=',.2%')
fig.show()

fig = go.Figure()
fig.add_trace(go.Scatter(x=dfy["Date"], y=dfy["S&P 500"], mode="lines+markers", name="Yearly Estimate",
            line=dict(color='limegreen', width=3)))
fig.add_trace(go.Scatter(x=dfm["Date"], y=dfm["S&P 500"], mode="lines", name="Monthly  Estimate",
            line=dict(color='darkgreen', width=2)))
fig.add_trace(go.Scatter(x=dfd["Date"], y=dfd["S&P 500"], mode="lines", name="Daily  Estimate",
            line=dict(color='blue', width=1)))
fig.update_layout(xaxis_title="Date of Estimation", yaxis_title="S&P 500 price", yaxis_type="log")
fig.show()
In [6]:
fig = go.Figure()
fig.add_trace(go.Scatter(
    x=dfy["Analyst Growth Estimate"], y=dfy["Implied ERP (FCFE)"], 
    hovertext=[ f"Date: {x.date()}" for x in dfy["Date"]], 
    mode="markers", name="Yearly Estimate"))
fig.add_trace(go.Scatter(x=dfm["Expected growth rate"], y=dfm["ERP (T12m)"], 
    hovertext=[ f"Date: {x.date()}" for x in dfm["Date"]], 
    mode="markers", name="Monthly  Estimate"))
fig.add_trace(go.Scatter(x=dfm.iloc[[-1]]["Expected growth rate"], y=dfm.iloc[[-1]]["ERP (T12m)"], 
    hovertext=[ f"Date: {x.date()}" for x in dfm.iloc[[-1]]["Date"]],
    marker_size=20, marker_opacity=0.5, 
    mode="markers", name=f"Now - {dfm.iloc[-1]['Date'].date()}"))
fig.update_layout(
    margin=dict(l=10, r=10, t=10, b=10),
    xaxis_title="Growth Estimate", yaxis_title="Equity Risk Premium",
    xaxis_tickformat=',.2%', yaxis_tickformat=',.2%',
    width=1000, height=800)
fig.show()

In this last plot above you can find:

  • The Growth Estimate (yearly for the next 5 years, CAGR geometric mean) of the S&P500's earnings, taken from Analyst's consensus - you can find it in the latest ERP calculation
  • ERP is the Equity Risk Premium, which is the expected return of the S&P500 over the RFR (10Y US Treasury yield)
  • Blue markers are Yearly estimations since 1986, red markers are monthly estimations since Jan 2009
  • Highlighted in green the last estimate
In [7]:
# Compare annual ERPs with the realized annualized S&P 500 excess gain over the following five years.
# The risk-free rate is the 10-year Treasury rate available when the ERP was estimated.
FORWARD_YEARS = 5

yearly_erp_outcomes = (
    dfy
    .sort_values("Date")
    .loc[:, ["Date", "rERP", "T.Bond Rate", "S&P 500"]]
    .dropna()
    .assign(
        **{
            "ERP observation year": lambda x: x["Date"].dt.year - 1,
            "Subsequent S&P 500 annualized gain": lambda x: (
                x["S&P 500"].shift(-FORWARD_YEARS).div(x["S&P 500"]).pow(1 / FORWARD_YEARS).sub(1)
            ),
            "Subsequent S&P 500 excess gain": lambda x: (
                x["Subsequent S&P 500 annualized gain"] - x["T.Bond Rate"]
            ),
        }
    )
    .dropna(subset=["rERP", "Subsequent S&P 500 excess gain"])
)

correlation = yearly_erp_outcomes[["rERP", "Subsequent S&P 500 excess gain"]].corr().iloc[0, 1]

fig = px.scatter(
    yearly_erp_outcomes,
    y="rERP",
    x="Subsequent S&P 500 excess gain",
    color="ERP observation year",
    color_continuous_scale="Viridis",
    hover_name="ERP observation year",
    hover_data={
        "rERP": ":.2%",
        "T.Bond Rate": ":.2%",
        "Subsequent S&P 500 annualized gain": ":.2%",
    },
    labels={
        "rERP": "Equity Risk Premium",
        "Subsequent S&P 500 excess gain": "Subsequent 5-year annualized S&P 500 gain **minus RFR**",
    }
)
fig.update_traces(marker={"size": 9, "opacity": 1})
fig.add_vline(x=0, line_dash="dot", line_color="red")
fig.update_layout(
    margin=dict(l=10, r=10, t=10, b=10),
    xaxis_tickformat=',.2%', yaxis_tickformat=',.2%',
    width=800, height=600)
fig.show()
print(f"Pearson correlation between ERP and subsequent 5-year S&P 500 excess gain: {correlation:.2f}")
Pearson correlation between ERP and subsequent 5-year S&P 500 excess gain: 0.47

Conclusions

  • Independently from the RFR (10-years US Treasury yield) which is a proxy for expected growth+inflation we can use the ERP (Equity Risk Premium) as a proxy for the market greed/fear
  • When the ERP is high there is a lot of fear, e.g., the market expect oscillations in the near-future
  • When the Growth Estimate is high, typically the market is overexcited, and the growth will mean-revert to some more reasonable long-term trends
  • This last statement is more controversial because the analyst may already expect a low groewth after a bull run, still underestimating the downturn of the index: this is the case of low-GrowthEstimate in late 2019, when the analyst were conservative in projecting a lower growth after a booming year. Maybe not a good moment to enter the market, despite the already-low estimated growth expectation.
  • As investors, if we assume that there won't be major market disruptions in the future, we want to enter the market when the ERP is high and the Growth Estimate is low (assuming analyst are already conservative), therefore we want to be in the top-left quadrant of the GrowthEst/ERP plot

Last reads

  • May 2023 - We are currently in a mildly good condition to enter the market, with below-average market growth expectation and quite average market fear/greed (considering the ERP). We are not in extreme condition to conclude the market is a bargain nor that it is overpriced.
  • Aug 2025 - After the concerns due to escalation with Iran and Hormuz strait, analysts are now projecting record-high growth which is priced in the market (ERP is near historical average). With the same high growth, an high ERP would mean that the market is skeptical about the growht, a low ERP would mean that the market is overconfident about the exceptional growth.

Follow-up

  • Add the future 1-5-10 years growth, to check if the growth estimate was legit
  • Include other macro indicators, to understand when the analyst are too optimistic/pessimistic in their Growth Estimate: this is not easy, as it is a recursive evaluation, i.e., using macro indicators to evaluate the analyst that are evaluating the macro indicators.
← Home