Fama-French regression

The purpose of this application is to estimate a Fama-French 3-factor model for a specific corporation. We will study IBM. First, we probably need the usual tools.

In [7]:
import pandas as pd # pandas is excellent for creating and manipulating dataframes, R-style
import numpy as np # great for array manipulations and simulations
import matplotlib.pyplot as plt #graphing module with matlab-like properties
%matplotlib inline 
import requests # to make requests to webpages (like mine)
import statsmodels.api as sm # module full of standard statistical and econometric models, including OLS and time-series stuff
from IPython.display import Latex # to be able to display latex in python command cells
import getFamaFrenchFactors as gff # a library that downloads FF data nicely, could use datareader for that too 
from pandas_datareader.data import DataReader # datereader downloads from all kinds of places including yahoo and google finance
import datetime

Next we build the data we need to run our regressions using various scraping libraries.

In [8]:
# Get five years worth of data for the Fama French 3-factor model (monthly data)

FF3data = gff.famaFrench3Factor(frequency='m') #Datareader could do this too
FF3data=FF3data.tail(60) # keep 5 years of data
FF3data['Year']=FF3data['date_ff_factors'].dt.year #creates month and year variables for merging below

#import yfinance as yf
#IBM = yf.Ticker("IBM")

# get 5 years of adjusted close price data for IBM

IBMdata=DataReader('IBM', 'yahoo', '2015-01-01', end)['Adj Close'] # (at least) five years worth of IBM data 
IBMdataF=IBMdata.resample('M').last() #only keep the last observation of every month
IBMdataF=IBMdataF.to_frame() # converts the series format to a frame format which makes the upcomning merge easier

IBMdataF.rename(columns={'Adj Close':'IBM'},inplace=True)

# now we merge our two datasets into one, using year and month as the merging variables


# finally we create IBM's monthly return data from the adjusted price series
# Note the use of panda's shift operator to create a lag variable

datanow.sort_values(['date_ff_factors'],axis=0, ascending=False, inplace=True)

datanow['rIBM']=(datanow['IBM']/datanow.IBM.shift(-1)-1) #computes monthly return based on adjusted series for IBM 
datanow['Mkt']=datanow['Mkt-RF']+datanow['RF'] # this the market return, we will use that below
datanow.dropna(subset=['rIBM'], inplace=True) # drop the entries with missing returns (missing due to lag operator)

Now let's look at our data a bit to make sure it all looks good.

In [9]:
IBM Year Month date_ff_factors Mkt-RF SMB HML RF rIBM Mkt
59 123.519997 2020 11 2020-11-30 0.1247 0.0546 0.0219 0.0001 0.122256 0.1248
58 110.064018 2020 10 2020-10-31 -0.0210 0.0444 0.0388 0.0001 -0.082272 -0.0209
57 119.930939 2020 9 2020-09-30 -0.0363 0.0006 -0.0256 0.0001 -0.013300 -0.0362
56 121.547493 2020 8 2020-08-31 0.0763 -0.0026 -0.0295 0.0001 0.016142 0.0764

Next we plot IBM's monthly return vs the market and fit a capm line. Here we estimate IBM's CAPM beta to be around $1.2$ (this may changes as I re-run this book and data get updated.)

In [10]:
datanow.sort_values(['date_ff_factors'],axis=0, ascending=False, inplace=True)


fig, ax = plt.subplots()
plt.plot(x, y, 'o') # each dot is a given month

m, b = np.polyfit(x, y, 1)  # fit the best possible line, beta is the slope of that line 
plt.plot(x, m*x + b)  # draw the line on our chart

print('Our estimate of IBMs (CAPM) beta is %.3f' %m)

ax.set_ylabel('Return on IBM')
ax.set_xlabel('Market return')

plt.show() # show our work
Our estimate of IBMs (CAPM) beta is 1.189

Instead of taking the quick route above, we can also run a proper CAPM regression. For that we need the excess return on IBM and then we need to estimate the following model: $$r^{IBM}_t-r^F_t= \alpha + \beta \left(r^{S\&P}_t-r^F_t\right)+ \epsilon_t.$$ If CAPM holds, $\alpha$ should estimate to a number that is not statistically different from zero. We will need to create a few variables first.

As shown below, we get about the same estimate of $\beta$ as with the quick route (that's because $r^F$ shows little variability so it is as if it were not in the regression at all) and we do get a statistically insignificant $\alpha.$

In [11]:

x=sm.add_constant(x) # we run a standard OLS with constant 
OLS Regression Results
Dep. Variable: rIBM-rF R-squared: 0.596
Model: OLS Adj. R-squared: 0.589
Method: Least Squares F-statistic: 84.11
Date: Sun, 24 Jan 2021 Prob (F-statistic): 8.12e-13
Time: 15:44:37 Log-Likelihood: 98.453
No. Observations: 59 AIC: -192.9
Df Residuals: 57 BIC: -188.8
Df Model: 1
Covariance Type: nonrobust
coef std err t P>|t| [0.025 0.975]
const -0.0110 0.006 -1.766 0.083 -0.024 0.001
Mkt-RF 1.1866 0.129 9.171 0.000 0.927 1.446
Omnibus: 1.646 Durbin-Watson: 2.155
Prob(Omnibus): 0.439 Jarque-Bera (JB): 0.942
Skew: -0.256 Prob(JB): 0.624
Kurtosis: 3.348 Cond. No. 21.4

[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.

Now we want to estimate a 3-factor Fama-French model for IBM: $$ \left(r^{IBM}_t-r^F_t\right)= \alpha + \beta_{Market} \left(r^{Market}_t-r^F_t\right) + \beta_{HML} r^{HML}_t + \beta_{SMB} r^{SMB}_t + \epsilon_t$$ where $\epsilon $ is an error term with all the standard properties. The python for this and the results follow. Only the CAPM beta is significant. Its size changes ever so slightly (though not significantly) which owes to the different specification.

In [12]:
X=sm.add_constant(X) # here we're building our right-hand side variables

OLS Regression Results
Dep. Variable: rIBM R-squared: 0.595
Model: OLS Adj. R-squared: 0.573
Method: Least Squares F-statistic: 26.90
Date: Sun, 24 Jan 2021 Prob (F-statistic): 7.67e-11
Time: 15:44:37 Log-Likelihood: 98.406
No. Observations: 59 AIC: -188.8
Df Residuals: 55 BIC: -180.5
Df Model: 3
Covariance Type: nonrobust
coef std err t P>|t| [0.025 0.975]
const -0.0106 0.007 -1.601 0.115 -0.024 0.003
Mkt-RF 1.2066 0.149 8.122 0.000 0.909 1.504
SMB -0.0691 0.277 -0.250 0.804 -0.624 0.486
HML -0.0397 0.196 -0.203 0.840 -0.432 0.352
Omnibus: 1.570 Durbin-Watson: 2.135
Prob(Omnibus): 0.456 Jarque-Bera (JB): 0.894
Skew: -0.256 Prob(JB): 0.639
Kurtosis: 3.317 Cond. No. 46.8

[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.