You should definitely use SciPy and statsmodels for statistical modeling in Python. For my linear regression class, I misheard that we had to do the variance analysis without any built-in or external packages…by hand! so I wrote an analysis of variance class aov_table() and a t-test function meanCI() to test the significance of a regression and significance of individual regressors. As the title says, my stuff is inferior to the standard packages lol. You can check out my code below or on Kaggle
aov_table() Analysis of variance: overall model adequacy
The aov_table() class takes in your linear model and estimators and creates an ANOVA table object with your standard ANOVA table values. When you create your object, you’ll get a readout of (almost) all associated info: values and degrees of freedom for the sums of squares, mean sums of squares, F-statistic, and R\(^2\)/R\(^2_{adjust}\). aov_table() comes with class functions to pull out specific values from the table. You can also specify whether in include the intercept in the analysis.
meanCI() Hypothesis testing with t-distribution: individual regressor significance
I also wrote the function meanCI() to test individual regressors (t-test) for your linear model. It uses values calculated with aov_table() and gives you the t-score, (1-\(\alpha\))% CI, and hypothesis conclusion.
Thoughts?
I used statsmodels to cross check my code. I much prefer statsmodels over my stuff because it is so much more readable! If I have time, maybe I can play around with the output format. I really enjoyed practicing Python, specifically building a class and working with fstrings. I also gained a much deeper understanding of sums of squares which had always been nebulously floating around in my head.
Example and Code
See my Kaggle workbook for the code and an example :)
Code for analysis of variance object
class aov_table:
def __init__(self, X, y, B_hat, nregressors, intercept):
self.X = X
self.y = y
self.B_hat = B_hat
self.p = np.shape(self.B_hat)[0]
self.k = nregressors
self.ybar = sum(self.y)/len(self.y)
if intercept==False:
self.ybar=0
else:
self.ybar = sum(self.y)/len(self.y)
def y_hat(self):
y_h=np.matmul(self.X,self.B_hat)
#print('Y_hat')
#print(' y_hat = X * B_hat')
return y_h
def resid(self):
y_resid = self.y-np.matmul(self.X,self.B_hat)
#print('Residuals')
#print(' e = y - X * B_hat')
return y_resid
def SS_e(self):
# section 3.2.4, eq 3.16
# eT * e
first_mult= np.matmul(self.B_hat.T,self.X.T)
ssres = np.matmul(self.y.T,self.y) - np.matmul(first_mult,self.y)
#print('Sum of Square of Residuals SS_res')
#print(' SSE = SS_res = e.T * e')
#print(' where e = y - X * B_hat')
return float(ssres)
'''def SS_r(self):
# equation from 3.24
# b-hat.T * X.T * y - sumsquare(y)/n
firstpart=np.matmul(self.B_hat.T,self.X.T)
first_half=np.matmul(firstpart,self.y)
y_part = sum(self.y)*sum(self.y)/len(self.y)
ssr = first_half-y_part
return float(ssr)'''
def SS_r(self):
# equation from appendic C.3.1
# (y-hat - 1*ybar).T * (y-hat - 1*ybar)
ones=np.ones((len(self.y),1))
#self.ybar=sum(self.y)/len(self.y)
ssr=np.matmul((self.y_hat()-ones*self.ybar).T, (self.y_hat()-ones*self.ybar))
return float(ssr)
def SS_T(self):
# equation from C.3.1 (pg 581)
# (y-y_bar)T * (y-y_bar)
self.ybar = sum(self.y)/len(self.y)
sst = np.matmul(self.y.T,self.y) - sum(self.y)*self.ybar
#print('Sum of Square Total SST')
#print(' SST = (y-y_bar)T * (y-y_bar)')
#print(' = SS_r + SS_e')
return float(sst)
def MS_res(self):
# equation 3.17
# SS_res / (n-p), p=#params
n=len(self.X)
MSRES=(self.SS_e())/(n-self.k-1)
#print('residual Mean Square MS_res')
#print(f' MS_res = SS_res / (n-p) = {float(MSRES)}')
return float(MSRES)
def MS_r(self):
# SS_r / (n)
n=len(self.X)
MSR=(self.SS_r())/(self.k)
#print('residual Mean Square MS_res')
#print(f' MS_r = SS_r / (k) = {float(MSR)}')
return float(MSR)
def F(self):
# pg 85
# MS_r / MS_res
n=len(self.X)
MSRES=(self.SS_e())/(n-self.k-1)
MSR=(self.SS_r())/(self.k)
f = MSR / MSRES
return float(f)
def R2(self):
# SSR/SST
r2=self.SS_r()/self.SS_T()
return float(r2)
def R2_adjust(self):
# eq 3.27
# 1 - SS_res/(n-p-1) / SST/(n-1)
# R2 adjusted penalizes us for adding too many parameters
n=len(self.y)
num=self.SS_e()/(n-self.k-1)
denom=self.SS_T()/(n-1)
r2a = 1 - num/denom
return float(r2a)
def summary(self):
n=len(self.y)
import scipy.stats
f_crit = scipy.stats.f.ppf(q=1-.05, dfn=self.k, dfd=n-self.k-1)
MSRES=(self.SS_e())/(n-self.k-1)
MSR=(self.SS_r())/(self.k)
f = MSR / MSRES
print(f'''
Source of variation Sum Sqaures df Mean Square
---
Regression SSR k MS_R
(y-hat-y-bar)^2 {round(self.SS_r(),2)} {self.k} {round(self.MS_r(),2)}
Residual SSres n-k-1 MS_res
errors^2 {round(self.SS_e(),2)} {n-self.k-1} {round(self.MS_res(),2)}
TOTAL SST n-1
(y-y_bar)^2 {round(self.SS_T(),2)} {n-1}
---
F-statistic F0 F_crit(alpha=0.05, df={self.k}, {n-self.k-1})
MS_R / MS_resid {round(f,2)} {round(f_crit,2)}
''')
Code for testing significance of individual regressors
# Calculate 95% CI for beta_1
'''
beta1-hat +- (t*) (sqrt(sigma-hat^2 Cjj) )
sigma-hat^2 = MS_res = dataset.MS_res()
Cjj = inv(X.T * X)
'''
def meanCI(X,beta,j,alpha):
import scipy.stats
Cjj=np.linalg.inv(np.matmul(X.T,X))[j][j]
sigma_hat_2 = dataset.MS_res()
se_Beta_j = np.sqrt(sigma_hat_2*Cjj)
beta_hat = beta[j]
degreefreedom=len(X)-9-1
t_crit = np.abs(scipy.stats.t.ppf(q=1-alpha/2,df=degreefreedom))
t0=float(beta_hat)/se_Beta_j
rejecto = np.abs(t0)>t_crit
conclusion = 'reject H0'
if rejecto == False:
conclusion = 'fail to reject H0'
lower = float(beta_hat - t_crit * se_Beta_j)
upper = float(beta_hat + t_crit* se_Beta_j)
print(f'''
--------------------------------------------
Hypothesis test
--------------------------------------------
H0: beta{j} = 0
H1: beta{j} != 0
beta_hat{i} = {round(float(beta_hat),3)}
se(Beta{i}) = {round(se_Beta_j,3)}
t0 = beta_hat/se(beta_hat) = {round(t0,3)}
t_crit = {round(t_crit,3)} for alpha = {alpha}, df = {degreefreedom}
|t0| > t_crit is {rejecto} ---> {conclusion}''')
print(f'''
--------------------------------------------
Confidence Interval
--------------------------------------------
{1-alpha} Confidence interval for Beta{j}
beta_hat +/- t_crit * se(Beta_j)
lower: {round(lower,3)}, upper: {round(upper,3)}
''')
return conclusion