Software Multiple Compression

I was listening to a recent epsisode of Bg2 with venture capitalists Brad Gerstner and Bill Gurley. In this clip, Gerstner describes the current state of the market for public software companies, and I thought his description perfectly captures what is going on right now in tech. Screenshot of Bg2 Pod

Essentially, software companies are facing three main headwinds:

  • Increase in uncertainty
  • Decrease in growth
  • Increase in interest rates

These three forces are compressing software multiples and by extension, software valuations. This multiple compression increases pressure on profitability, raises scrutiny over headcount, and promotes belt-tightening within software companies as we have been seeing for the past two years.

Multiples are currently far below historical averages—software is cheap.

I wanted to explore this dataset for myself, so I decided to put together some Python code to look into the current state of the market for public software companies. I put all the code into a Jupyter Notebook, which is available on my Github and on Google Colab if you want to follow along. You can also re-run this same analysis for any list of tickers if you want to do comparisons with other public companies.

Yahoo Finance and Thread Pools

To start, I grabbed the list of tickers included in the Altimeter Capital graphic with an image to text tool, then I parsed these and added them to a list. I wrote a quick function to pull the financials (market cap, debt, cash, revenue, and free cash flow) for a given ticker from Yahoo Finance using the yfinance module, filtering out any tickers with missing data.
python
def compute_multiples(self, ticker): stock = yf.Ticker(ticker) try: market_cap = stock.info['marketCap'] debt = stock.info.get('totalDebt', 0) cash = stock.info.get('totalCash', 0) ev = self.compute_ev(market_cap, debt, cash) revenue = stock.financials.loc['Total Revenue'][0] fcf = stock.cashflow.loc['Free Cash Flow'][0] ev_revenue = ev / revenue if revenue else None ev_fcf = ev / fcf if fcf else None if ev_revenue is not None and not np.isnan(ev_revenue): self.ev_revenue[ticker] = ev_revenue if ev_fcf is not None and not np.isnan(ev_fcf): self.ev_fcf[ticker] = ev_fcf except KeyError as e: print(f"Data not available for {ticker}: {e}") self.skips += 1

Yahoo Finance is great for free, public financial data, however it is mostly limited to current data. For example, it does not have historical market cap data. Because of this I limited my analysis to just a snapshot of the present.

Next, I setup a thread pool so we can call Yahoo Finance async from multiple threads. This will speed up the data retrieval so we can quickly pull data for different collections of tickers. I added some logic for chunking the tickers into groups to distribute the load evenly across threads. For more details on multithreading in Python see my previous blog post here.
python
def chunk_tickers(self): chunk_size = len(self.tickers) // self.threads remainder = len(self.tickers) % self.threads chunks = [self.tickers[i:i + chunk_size] for i in range(0, len(self.tickers)-remainder, chunk_size)] for i in range(remainder): chunks[i % self.threads].append(self.tickers[-(i+1)]) print("Chunking Tickers into {} chunks with average size {}".format(len(chunks), chunk_size)) return chunks def process_tickers(self): chunks = self.chunk_tickers() for chunk in chunks: for ticker in chunk: self.pool.submit(self.compute_multiples, ticker) self.pool.shutdown(wait=True) print("Skipped {} tickers. Processed {} total.".format(self.skips, len(self.tickers) - self.skips)) self.sort_data()

Finally, I added some code to aggregate stats, plot the data, and export to a CSV.

Results and Analysis

After retrieving and filtering the data, we are left with 77 tickers, down from the original 105 from Altimeter Capital. Here are the results for our aggregate statistics:

EV/RevenueEV/FCF
Mean7.4230.99
Median5.9529.11
Standard Deviation5.52139.97

Our results for median EV / Revenue come out to 5.9, compared to 5.1 for the results from Altimeter. These differences are likely due to the tickers we exluded, plus changes in the data over the last couple of weeks.

Similarly, our results for median EV / FCF came out to 29.1 compared to 31.8 from the analysis by Goldman Sachs included in episode 10 of Bg2. Again, I think these differences are due to recent changes in the data, and likely a different set of tickers.

Since we have data for all the individual tickers, we can go a bit deeper than a "Software Index" calculation. We can take a look at the distribution of these multiples. Below I included the graphs and a dump of some of the data. The graphs are a bit hard to read and are more clear on Google Colab here. Regardless, this data gives an idea of the current landscape of the market for software companies. Many of these companies are trading at multiples far below the current and long term median values. If you are long software then, in the words of Brad Gerstner, it's time to go hunting! Graph of EV / Revenue Graph of EV / FCF
Companies Trading Below Median EV / Revenue
TickerEV/Revenue
ONTF0.25
EGHT0.76
KLTR0.9
TWOU0.95
DOMO1.02
LAW1.6
TWLO1.65
OLO1.7
RNG1.84
TDC1.93
RSKD1.94
FSLY2.15
BIGC2.26
ZM2.34
CXM2.45
ZUO3.02
DBX3.02
EVCM3.26
DOCU3.45
BOX3.54
U3.54
FIVN3.55
PATH3.63
WKME3.71
JAMF3.8
SSNC3.9
ASAN3.95
APPN4.08
BASE4.12
FRSH4.19
BILL4.31
PD4.35
PAYC4.72
BL5.04
SMAR5.4
SPT5.7
QTWO5.92
WIX5.95
Companies Trading Below Median EV / FCF
TickerEV/Revenue
ONTF0.25
EGHT0.76
KLTR0.9
TWOU0.95
DOMO1.02
LAW1.6
TWLO1.65
OLO1.7
RNG1.84
TDC1.93
RSKD1.94
FSLY2.15
BIGC2.26
ZM2.34
CXM2.45
ZUO3.02
DBX3.02
EVCM3.26
DOCU3.45
BOX3.54
U3.54
FIVN3.55
PATH3.63
WKME3.71
JAMF3.8
SSNC3.9
ASAN3.95
APPN4.08
BASE4.12
FRSH4.19
BILL4.31
PD4.35
PAYC4.72
BL5.04
SMAR5.4
SPT5.7
QTWO5.92
WIX5.95