S&P 500 Earnings Growth Rates in Python
I recently read through a 2015 post on the Philsophical Economics blog about understanding S&P 500 earnings. Essentially, less and less dividends are paid out in the US, and are instead being reinvested either in operations or in company stock.
This has a few implications. Earnings growth rate calculations are fodder for forecasts (note: I am not a fan of forecasts). Second, monitoring the change in earnings, say year-over-year, does have usefulness as an indicator of an oncoming recession. When you adjust for earnings being reinvested (aka Total Return EPS) rather than paid out, it changes the discussion around earnings growth.
The source data comes from Prof. Robert Shiller’s website, here.
To understand the original post better and keep an eye on it, I coded up the calculation in Python:
import numpy as np import pandas as pd import matplotlib.pyplot as plt import matplotlib def shiller(): spdvds = pd.read_excel('http://www.econ.yale.edu/~shiller/data/ie_data.xls','Data',header=7) spdvds = spdvds.ix[:spdvds[spdvds['Date']==spdvds.Date.dropna().iloc[-1]].index[0],:] yr = np.floor(spdvds.Date) mn = (spdvds.Date - yr)*100 spdvds['Date'] = pd.to_datetime(dict(year=yr,month=mn,day=1)) spdvds = spdvds.set_index('Date') return spdvds def total_return_eps(): t = shiller() cape = t['CAPE'].dropna().mean() sh = (1-1/(t['Earnings']*cape/t['Dividend']*12)) #shares of prior month remaining after theoretical buyback sh = sh.dropna().cumprod() #cumulative remaining shares return (t['Earnings']/sh).dropna(),cape def treps_chart(): plt.figure() ax = plt.gca() treps,cape = total_return_eps() treps.plot(ax=ax,logy=True) xmin,xmax = ax.get_xlim() ymin,ymax = ax.get_ylim() x = np.arange(len(treps))+ax.get_xlim()[0]-1 y = treps.values fit = np.polyfit(x,np.log(y),1) fit_fn = np.poly1d(fit) yhat = np.exp(fit_fn(x)) ax.plot(x,yhat,'--k') ticklbl = [ymin,.10*ymax,ymax] ticklbl = [np.round(t,-1) for t in ticklbl] ax.set_yticks(ticklbl) ax.get_yaxis().set_major_formatter(matplotlib.ticker.ScalarFormatter()) txt = 'Total Return EPS\nShiller Real EPS if all Dividends Reinvested at CAPE={:.2f}' plt.title(txt.format(cape)) rate = np.exp(np.log(yhat[-1]/yhat[0])/(x[-1]-x[0])*12.)-1 txt = ' Total Return EPS growth rate: {:.2f}%\n (Uncorrected)'.format(rate*100) ax.text(xmin,ymax*0.25,txt) def eps_chart(): plt.figure() ax = plt.gca() rs = shiller() eps = rs['Earnings'].dropna() eps.plot(ax=ax,logy=True) xmin,xmax = ax.get_xlim() ymin,ymax = ax.get_ylim() x = np.arange(len(eps))+ax.get_xlim()[0]-1 y = eps.values fit = np.polyfit(x,np.log(y),1) fit_fn = np.poly1d(fit) yhat = np.exp(fit_fn(x)) ax.plot(x,yhat,'--k') ticklbl = [ymin,.10*ymax,ymax] ticklbl = [np.round(t,-1) for t in ticklbl] ax.set_yticks(ticklbl) ax.get_yaxis().set_major_formatter(matplotlib.ticker.ScalarFormatter()) ax.set_ylim([ymin,ymax]) txt = 'Shiller EPS (Real; corrected for CPI inflation)' plt.title(txt) rate = np.exp(np.log(yhat[-1]/yhat[0])/(x[-1]-x[0])*12.)-1 txt = ' Real EPS growth rate: {:.2f}%\n(Uncorrected)'.format(rate*100) ax.text(xmin,ymax*0.25,txt)
Breaking Away
The original post was an insightful look at flaws in a traditional macroeconomic measure. Coding it up in Python will be useful for future reference. Going forward, earnings per share is more likely to grow close to 5.5% than it is 1.5%. If trends continue and P/E ratios stay constant, stock prices will be at higher levels than economists expect (or have predicted). Good lesson: do not predict.
(Disclaimer: P/E ratios don’t stay constant! This is not a recommendation to buy stocks. Further, this is not professional investment advice. I am expressing an opinion and make no representations as to the accuracy, completeness, suitability, or validity of any of the information presented.)