Can you still beat the market by investing in low P/E ratio stocks? [2021]
![Can you still beat the market by investing in low P/E ratio stocks? [2021]](https://firemymoneymanager.com/wp-content/uploads/2021/08/plt-800x247.png)
In the 70s and 80s, there were many studies on U.S. stocks that showed that by investing in low P/E stocks (“value” stocks), it was possible to outperform the market. The most famous of those studies was by a researcher named Sanjoy Basu, who found that he could beat the market in both absolute and risk-adjusted terms by setting up a portfolio of low P/E stocks. But things have changed since the 70s, and with widespread algorithmic trading and so many more market participants, is it still possible to beat the market using low-P/E ratio value stocks?
References
- Common risk factors in the returns on stocks and bonds
- The cross section of expected stock returns
- Investment Performance of Common Stock in Relation to their Price-Earnings Ratios: BASU 1977 Extended Analysis
- Investment Performance and Price-Earnings Ratios: Basu 1977 Revisited
- Data Science Pipeline: Financial Data and Machine Learning
Building a model
The first thing we will do is load in the data.
We use Quandl’s SF1 data, which includes PE ratios by quarter, along with prices. Unfortunately, the prices in the data are not adjusted for splits and other actions, so they’re essentially useless for our analysis. Instead, we will have to combine the SF1 daily data with daily adjusted prices to get a useable data table:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import quandl
import pickle
import csv
from datetime import date
import sys
import statsmodels.api as sm
import scipy.optimize as sco
import scipy.stats as stats
plt.style.use('fivethirtyeight')
np.random.seed(777)
pd.set_option('display.max_rows', 100)
pd.set_option('display.max_columns', 100)
alldata = pd.DataFrame()
pd.set_option('display.max_rows', 100)
pd.set_option('display.max_columns', 100)
try:
ticker_data = pd.read_csv("tickers.csv")
except FileNotFoundError as e:
pass
ticker_data = ticker_data.loc[((ticker_data['table'] == 'SF1') | (ticker_data['table'] == 'SFPY')) & (ticker_data['isdelisted'] == 'N') & (ticker_data['currency'] == 'USD') & ((ticker_data.exchange == 'NYSE') | (ticker_data.exchange == 'NYSEMKT') | (ticker_data.exchange == 'NYSEARCA') | (ticker_data.exchange == 'NASDAQ'))]
for t in ticker_data['ticker']:
if t == 'TRUE': continue
print(t)
try:
data = pd.read_csv(t + ".csv",
header = None,
usecols = [0, 1, 12],
names = ['ticker', 'date', 'adj_close'])
if (len(data) < 2000):
continue
alldata = alldata.append(data)
except FileNotFoundError as e:
pass
tickers = ticker_data['ticker']
df = alldata.set_index('date')
table = df.pivot(columns='ticker')
table.columns = [col[1] for col in table.columns]
table = table['2010-01-01':]
table.index = pd.to_datetime(table.index)
mtable = table.resample('BA').last()
dp = pd.DataFrame(columns = ['dp', 'change'])
mtable = mtable.pct_change()
mtable = mtable['2010-02-01':]
try:
daily = pd.read_csv("daily.csv")
except FileNotFoundError as e:
pass
daily['date'] = pd.to_datetime(daily['date'])
daily = daily[daily['ticker'].isin(tickers)]
daily.set_index(['date','ticker'])
Choosing a strategy
In our previous article on P/E ratios, we found that there was a very weak relationship between individual stock P/E ratios and their returns. However, there are several studies (including ones with recent data) that suggest that you can achieve excess return by choosing stocks by P/E ratio.
So what can we do to reproduce those results? We will follow Fama and French’s methodology and try to segment the universe of stocks. Our plan: first segment all stocks by size, in order to minimize the confounding effect of size on returns. Then, we segment all stocks of a given size by P/E.
Quandl’s data segments stocks by size, from “Mega-cap” to “Nano-cap”. There are six different levels, which work perfectly for our analysis.
For each size segment, we run the following code to create a table of P/E ratios (“dp” in the code) and returns in the following period (this is definitely not the most efficient way of doing this, but it makes the code easier to read):
ind = mtable.index for ticker in mtable.columns: print(ticker) t = mtable[ticker] for i in range(1, len(t)): cdp = [] k=i-1 while (len(cdp)< 1): cdp = daily.loc[(daily['ticker'] == ticker) & (daily['date'] == ind[k])] k -= 1 dp.loc[len(dp)] = [float(cdp['pe']), t.iloc[i]]
Now, we can look at the returns for the different P/E segments. The P/E ratios go from lowest P/E to highest P/E:
Mega:
[0.13068503269408385,
0.08326752061144241,
0.1380462229254901,
0.117719617051882,
0.13216684174767918,
0.14361117529086112,
0.16333668810419613,
0.205575724187894,
0.259988838374162,
0.23714170348735714]
Large:
[0.2602685291550176,
0.1408437515613175,
0.1472637837186117,
0.12810054388845457,
0.1439081333254362,
0.13699477876245372,
0.1345270411961278,
0.1335653382380293,
0.1457851810854936,
0.15770516217138766]
Mid:
[0.16237083712643496,
0.1440487438183479,
0.12408102363908026,
0.13220148708123755,
0.09534966271276092,
0.09051593111424015,
0.07561844078734582,
0.08486941622644986,
0.11348194467952495,
0.09144510435381892]
Small:
[0.19132591414519495,
0.12820274038108942,
0.09745781688023002,
0.11474877987611032,
0.09702471683984862,
0.07064636319161406,
0.09047229550531244,
0.0828232086765549,
0.04409273393810726,
0.020270416006202725]
Micro:
[0.17124009248583802,
0.14331515786674054,
0.13093854405283298,
0.1293375950116475,
0.09443798212548235,
0.08379384185333447,
0.09268144187528211,
0.048244706566666934,
0.03798160669492658,
0.06787391604198931]
Nano:
[0.09477948639891882,
-0.2553948911601226,
-0.06578412838435217,
0.046386198100281335,
0.21148023917830475,
0.11741721425109572,
-0.038402081750737435,
0.16489474736888213,
-0.00752587887449796,
-0.04861228658528758]
Mega:
[0.19349508729629636,
0.08170861789358486,
0.10335788696558595,
0.15172125575317202,
0.12474620074587271,
0.20554666421477968,
0.14387144284321216,
0.24155644548545854,
0.21899822928155024,
0.18844368661132702]
Large:
[0.18051089433141054,
0.15161381644314398,
0.1282648452872524,
0.11635037171828862,
0.14175540492858066,
0.14988266567120198,
0.14229371532314422,
0.14706812855208015,
0.13023004902981858,
0.14152766956747506]
Mid:
[0.27945562084133735,
0.17879796173281368,
0.14067649967264556,
0.12890605747331618,
0.10475947497437907,
0.13823494507685608,
0.12353034935699093,
0.11823404448760805,
0.0729309362573854,
0.14235647834799478]
Small:
Micro:
Nano:
[0.19349508729629636,
0.08170861789358486,
0.10335788696558595,
0.15172125575317202,
0.12474620074587271,
0.20554666421477968,
0.14387144284321216,
0.24155644548545854,
0.21899822928155024,
0.18844368661132702]
Mega:
[0.009599684431421535,
0.007604692930039574,
0.010074241328499666,
0.00906479345104507,
0.012220214373535827,
0.011644010904054144,
0.014240209559025262,
0.014827627847797042,
0.018280913215721364,
0.019121877404537562]
Large:
Mid:
[0.27945562084133735,
0.17879796173281368,
0.14067649967264556,
0.12890605747331618,
0.10475947497437907,
0.13823494507685608,
0.12353034935699093,
0.11823404448760805,
0.0729309362573854,
0.14235647834799478]
Small:
Micro:
Nano:
This result provides some interesting insight. While there are patterns in micro to mega stocks, the patterns are different and not necessarily statistically significant. Large-cap and mid-cap stocks have a familiar U shape P/E ratio, which Fama and French predicted. However, small- and micro-cap stocks seem to have a linear relationship, and nano-cap stocks have what appears to be a random relationship.
Let’s look at each segment, starting with mega-cap stocks. From these numbers, there seems to be a pretty clear positive relationship between P/E and returns. That is, the higher the P/E ratio, the more the returns. This is the opposite of what Fama and French predict.
Large-cap stocks
No Comments