Using Python for panel data statistical analysis in your MBA research

Why use python for your MBA data analysis? Most business school give a short course on stata, SAS, or SPSS. And these tools seem so easy to use! However, this easiness is the first reason you should consider using something different. Why? Many of my students come to me complaining that they've done the analysis using the instructions they'd got in class but they have no idea now how to write the interpretation or, even worse, they've got the results that were mentioned in the lectured as "not good", and now they do not know what to do. The easiness of using using stata, SAS, or SPSS hides the math behind simple procedures, and does not require a student to understand what is happening with the data. It is fine in simple cases where the relationship is obvious, but more difficult cases require understanding of the data and understanding why the results look the way they do. Since data processing in python requires much more thinking about the shape and relationships among the data, it is often easier to understand the results of a statistical analysis.

There is also another reason. Python is widely used in the industry. And in over 20 years of consulting I've never had a chance to work with a company using STATA or SPSS. I worked with 2 companies that used SAS, one of these companies was SAS itself. So, instead of spending time to learn a software package that you will never use in your professional career, why not start straight away with something that is likely to be useful again and again?

So, today we will go through an example panel data regression in Python, using a possible finance problem.

TOC

1. Panel data

   1.1. Data

    1.2. Data checks

2. Panel model

   2.3. Random effects model

1. Panel data 

Panel data is a time series data where observations of different entities are mixed together to find insights that are true across entities. Panel data is often used when time observations for each entity are not sufficient to derive a separate regression for it. For example, annual performance of companies during a financial crisis usually can have only several data points, which is not sufficient to test even a single relationship. Combining the data from many companies allows to increase the number of data points, thus, allowing to draw inferences.
In this example, we will use media sentiment data for a range of companies, tracked daily over several months. In this case, some of the companies are mentioned often in the media while others are mentioned seldom. Using panel data allows to see if meaningful insights about the influence of media sentiment on stock price can be drawn for all companies, whether routinely discussed by the media or mentioned once in a while.

1.1. Data

The data used in this example is the historical stock price data and media sentiment scores measured using different algorithms and presented in tone1..tone5 variables. As the media sentiment is meant to predict whether or not the stock price will go up the next day, we will calculate the change of price for each day. Also, as it is well known that individual stocks tend to move with the market, we add a column "market_change" to track the daily change in the overall market. The data then looks as follows:
changetone1tone2tone3tone4tone5market_change
00.0050170.0365390.3639080.0868210.0078820.0051300.011425
10.006349-0.1151850.4652590.1552330.0763640.0076100.000490
20.0077140.1492360.4511780.1131960.0837230.0021400.002659
30.0019380.0343750.3859010.1008780.0000000.0026180.000980
40.002371-0.1128750.4793420.1027160.0000000.0013480.000489
........................
25280.005556-0.0666870.4398450.1376250.0532490.0038840.001744
25290.028052-0.0473800.5127290.1295010.0580930.0053200.000277
25300.019991-0.0960780.4199510.1652780.1468630.0021250.000832
25310.022237-0.0993260.4743620.1216750.0403420.0033820.008384
25320.007889-0.0007710.4462640.1208380.0784840.0048780.002499

Further, we need to get our libraries and define our endogenous (dependent) and exogenous (independent) variables:

    
    import pandas as pd
import os import numpy as np import scipy as sp import statsmodels.api as sm import matplotlib.pyplot as plt exog_vars = ['date', 'company', 'tone1', 'tone2', 'tone3', 'tone4','tone5', 'change','market_change'] df1=df[exog_vars] df1 = df.set_index(["company", "date"]) endog = df1['change'] df1 = df1[independent_vars] exog = sm.add_constant(df1)

1.2. Data checks

Before running the regressions, it is necessary to do a few checks. First, we need to verify that there's no strong correlation between the exogenous variables:

    independent_vars=['tone1', 'tone2','tone3', 'tone4','tone5','market_change']
    df[['change']+independent_vars].corr()
    
The correlation table is shown below:
changetone1tone2tone3tone4tone5market_change
change1.0000000.008409-0.020838-0.0180910.0379690.0350730.204896
tone10.0084091.000000-0.089915-0.2854870.179817-0.0432170.020401
tone2-0.020838-0.0899151.0000000.0604210.1997470.0085900.032540
tone3-0.018091-0.2854870.0604211.000000-0.1376270.031506-0.001985
tone40.0379690.1798170.199747-0.1376271.0000000.100439-0.002575
tone50.035073-0.0432170.0085900.0315060.1004391.000000-0.052801
market_change0.2048960.0204010.032540-0.001985-0.002575-0.0528011.000000
Usually, it is sufficient to remove the variables that are correlated at more than 0.6. In our case, there's no variables like that, thus, we can proceed.

The next step is to check whether our data has any outliers. One of the easiest ways to check that is to make a Leverage plot. For this plot, we need to define our endogenous (dependent) and exogenous (independent) variables:
    
    
      exog_vars = ['date', 'company', 'tone1', 'tone2', 'tone3', 'tone4','tone5', 'change','market_change']
      df1=df[exog_vars]
      df1 = df.set_index(["company", "date"])
      endog = df1['change']
      df1 = df1[independent_vars]
      exog = sm.add_constant(df1)
      
Now, the Leverage plot:
       
      from statsmodels.graphics.regressionplots import plot_leverage_resid2
      results = sm.OLS(endog, exog).fit()
      fig, ax = plt.subplots(figsize=(16, 12))
      fig = plot_leverage_resid2(results, ax=ax)
      
      
In the leverage plot, the values on top left and top right are important outliers. From the graph above, we can see that BAC and JPM values in June are valuable outliers. It makes sense to remove them: 
            
        endog=endog.drop(('jpm', '2021-06-16'))
        exog=exog.drop(('jpm', '2021-06-16'))
        endog=endog.drop(('bac', '2021-06-27'))
        exog=exog.drop(('bac', '2021-06-27'))
        

2. Panel model

Panel regression can be built using either Pooled OLS or Fixed effects or Random effects models. These models are presented below.

2.1. Pooled OLS regression

            
        from linearmodels import PooledOLS
        import statsmodels.api as sm
        mod = PooledOLS(endog, exog, check_rank=False)
        pooledOLS_res = mod.fit()
        # Store values for checking homoskedasticity graphically
        fittedvals_pooled_OLS = pooledOLS_res.predict().fitted_values
        residuals_pooled_OLS = pooledOLS_res.resids
        print(pooledOLS_res)
        
PooledOLS Estimation Summary
            
================================================================================
Dep. Variable:                 change   R-squared:                        0.0530
Estimator:                  PooledOLS   R-squared (Between):              0.0179
No. Observations:                2531   R-squared (Within):               0.0538
Date:                Sun, Sep 19 2021   R-squared (Overall):              0.0530
Time:                        16:27:37   Log-likelihood                    7706.5
Cov. Estimator:            Unadjusted                                           
                                        F-statistic:                      23.548
Entities:                          51   P-value                           0.0000
Avg Obs:                       49.627   Distribution:                  F(6,2524)
Min Obs:                       1.0000                                           
Max Obs:                       83.000   F-statistic (robust):             23.548
                                        P-value                           0.0000
Time periods:                      83   Distribution:                  F(6,2524)
Avg Obs:                       30.494                                           
Min Obs:                       18.000                                           
Max Obs:                       48.000                                           
                                                                                
                               Parameter Estimates                               
=================================================================================
               Parameter  Std. Err.     T-stat    P-value    Lower CI    Upper CI
---------------------------------------------------------------------------------
const             0.0139     0.0029     4.7743     0.0000      0.0082      0.0197
tone1             0.0050     0.0032     1.5767     0.1150     -0.0012      0.0112
tone2            -0.0144     0.0062    -2.3143     0.0207     -0.0265     -0.0022
tone3            -0.0011     0.0091    -0.1221     0.9028     -0.0189      0.0167
tone4             0.0169     0.0041     4.1100     0.0000      0.0088      0.0249
tone5             0.0187     0.0157     1.1901     0.2341     -0.0121      0.0496
market_change     0.7602     0.0722     10.523     0.0000      0.6186      0.9019
=================================================================================
Normally, Pooled OLS does not work too well if the samples for each period are not taken randomly. As, this is the case in our example because the same companies were tracked over the time interval, we need to develop Fixed or Random effects models.

2.2. Fixed effects model


In a fixed effects model, we can control for time or entity effects. If time effects are set to False, the assumption is that the effect remains the same no matter the time interval. If entity effects are set to False, the assumption is that the effect remains the same across all entities. In our case, it is likely that different companies will be differently affected by the change in media sentiment, but it is not likely that the effect of the sentiment is time dependent. Thus, we choose entity_effects only.
from linearmodels.panel import PanelOLS
mod = PanelOLS(endog, exog,entity_effects=True, time_effects=False, drop_absorbed=True)
fe_res = mod.fit()
print(fe_res)
PanelOLS Estimation Summary ================================================================================ Dep. Variable: change R-squared: 0.0572 Estimator: PanelOLS R-squared (Between): 0.0147 No. Observations: 2531 R-squared (Within): 0.0572 Date: Sun, Sep 19 2021 R-squared (Overall): 0.0496 Time: 16:28:21 Log-likelihood 7972.3 Cov. Estimator: Unadjusted F-statistic: 25.035 Entities: 51 P-value 0.0000 Avg Obs: 49.627 Distribution: F(6,2474) Min Obs: 1.0000 Max Obs: 83.000 F-statistic (robust): 25.035 P-value 0.0000 Time periods: 83 Distribution: F(6,2474) Avg Obs: 30.494 Min Obs: 18.000 Max Obs: 48.000 Parameter Estimates ================================================================================= Parameter Std. Err. T-stat P-value Lower CI Upper CI --------------------------------------------------------------------------------- const 0.0111 0.0028 3.9821 0.0001 0.0056 0.0165 tone1 0.0076 0.0031 2.4276 0.0153 0.0015 0.0137 tone2 -0.0066 0.0060 -1.1117 0.2664 -0.0183 0.0051 tone3 0.0026 0.0087 0.2955 0.7676 -0.0145 0.0196 tone4 0.0076 0.0041 1.8842 0.0596 -0.0003 0.0156 tone5 -0.0027 0.0155 -0.1715 0.8638 -0.0332 0.0278 market_change 0.7715 0.0659 11.708 0.0000 0.6423 0.9007 ================================================================================= F-test for Poolability: 11.564 P-value: 0.0000 Distribution: F(50,2474) Included effects: Entity

2.3. Random effects model


In a random effects model, the assumption is that the effects across entities or time are random. Generally, random effects allows to estimate the variability in the population and is more efficient. 
from linearmodels.panel import RandomEffects
mod = RandomEffects(endog, exog)
re_res = mod.fit()
print(re_res)
RandomEffects Estimation Summary                        
================================================================================
Dep. Variable:                 change   R-squared:                        0.0649
Estimator:              RandomEffects   R-squared (Between):              0.0271
No. Observations:                2531   R-squared (Within):               0.0572
Date:                Sun, Sep 19 2021   R-squared (Overall):              0.0459
Time:                        16:28:21   Log-likelihood                    7951.3
Cov. Estimator:            Unadjusted                                           
                                        F-statistic:                      29.187
Entities:                          51   P-value                           0.0000
Avg Obs:                       49.627   Distribution:                  F(6,2524)
Min Obs:                       1.0000                                           
Max Obs:                       83.000   F-statistic (robust):             25.284
                                        P-value                           0.0000
Time periods:                      83   Distribution:                  F(6,2524)
Avg Obs:                       30.494                                           
Min Obs:                       18.000                                           
Max Obs:                       48.000                                           
                                                                                
                               Parameter Estimates                               
=================================================================================
               Parameter  Std. Err.     T-stat    P-value    Lower CI    Upper CI
---------------------------------------------------------------------------------
const             0.0121     0.0029     4.1363     0.0000      0.0064      0.0178
tone1             0.0076     0.0031     2.4575     0.0141      0.0015      0.0137
tone2            -0.0073     0.0059    -1.2373     0.2161     -0.0189      0.0043
tone3             0.0027     0.0086     0.3150     0.7528     -0.0142      0.0196
tone4             0.0079     0.0040     1.9574     0.0504  -1.427e-05      0.0158
tone5            -0.0016     0.0155    -0.1056     0.9159     -0.0320      0.0287
market_change     0.7719     0.0658     11.739     0.0000      0.6429      0.9008
=================================================================================

While Random Effects is generally more efficient and in this case results in a seemingly more explanatory power, random effects model can only be used if they are as stable as the fixed effects model. The stability can be verified using Hausman test:


import numpy.linalg as la
from scipy import stats
import numpy as np
def hausman(fe, re):

b = fe.params
B = re.params
v_b = fe.cov
v_B = re.cov
df = b[np.abs(b) < 1e8].size
chi2 = np.dot((b - B).T, la.inv(v_b - v_B).dot(b - B))
pval = stats.chi2.sf(chi2, df)
return chi2, df, pval
Comparing fixed and random effects in our case gives:
    
    hausman_results = hausman(fe_res,re_res) 
print('chi-Squared: ' + str(hausman_results[0]))
print('degrees of freedom: ' + str(hausman_results[1]))
print('p-Value: ' + str(hausman_results[2]))
chi-Squared: 0.8483391913225743
degrees of freedom: 7
p-Value: 0.9969187721379007
If p-value is small, the populations are the same, and random effects can be used. If p-value is large as in our case, the populations are different, therefore, fixed effects model should be used.

3. Model evaluation

At this point, we know that our data is described best using the fixed effects model. From the parameters of the model we can see that the model is valid (p-values in the heading are below 0.05). At the same time the model is not very explanatory as it describes only 6% of the data (R-square). This is not necessarily a problem. Generally, it means that other variables exist that can describe the data better. If you are working on a panel model in finance, you can find a variety of control variables in literature that are used to improve the predictability of the model. However, this is outside the scope of this exercise.
The useful information that we can extract from the model is that the stock price variability over a day is significantly and positively correlated with the tone of the media the day before (measured by tone1 and tone5, where tone1 and tone5 are simply different methods for evaluating the positivity/negativity of an article)
As expected, the stock price change over the day is also significantly correlated with the move of the market.
Finally, we can add some more tests to understand the deficiencies of the model better. Since we know that the model does not explain the data too well, we can look at the shape of the residuals and attempt to understand what is happening. We'll start with Q-Q plot:

pplot = sm.ProbPlot(residuals_fixed)
fig = pplot.qqplot()
h = plt.title("Model VD - qqplot - residuals of OLS fit")
plt.show()

The Q-Q plot shows that the data is not normally distributed. While normality is not required in panel data, it is likely that another type of regression will work better with the data.
We can also verify that the residuals are not normally distributed:

n, bins, patches = plt.hist(residuals_fixed, 25, density=True, facecolor='g', alpha=0.75)
plt.xlabel('residuals')
plt.ylabel('Density')
plt.title('Histogram of residuals')
plt.grid(True)
plt.show()

While the non-normality can be guessed from the graph above, it is always better to confirm using a test, such as Shapiro-Wilkinson test:

from scipy.stats import shapiro
#The null-hypothesis of this test is that the population is normally distributed. 
#Thus, if the p value is less than the chosen alpha level, 
#then the null hypothesis is rejected and there is evidence that the data tested are not normally distributed. 
shapiro(res)
print("Shapiro Wilkinson statistic:" + str(shapiro(residuals_pooled_OLS)[0] )+ ", p-value: " + str(shapiro(residuals_pooled_OLS)[1]) )
Shapiro Wilkinson statistic:0.7917779684066772, p-value: 0.0 (Not normal).

Another problem with the residuals is Homoskedastisity, i.e. changing variability of residuals with fitted values. Again, we can present it visually and confirm using a test.

import matplotlib.pyplot as plt

fig=plt.figure(figsize=(20,12))
fig, ax = plt.subplots()
ax.scatter(fittedvals_pooled_OLS, residuals_pooled_OLS, color = 'blue')
ax.axhline(0, color = 'r', ls = '--')
ax.set_xlabel('Fitted Values')
ax.set_ylabel('Residuals')
ax.set_title('Homoskedasticity')
plt.show()


Visually, the plot above is not homoskedastic. However, it is always better to confirm using a test:

from statsmodels.stats.diagnostic import het_breuschpagan
#The null hypothesis for this test is that the error variances are all equal.
# If p-value is small, null hypothesis is rejected and error variances are all different
bp=het_breuschpagan(residuals_pooled_OLS, exog, robust=True)
print("Breusch-Pagan statistic: f-value: " + str(bp[2] )+ ", p-value: " + str(bp[3]) )

print("Breusch-Pagan statistic: lm: " + str(bp[0] )+ ", p-value: " + str(bp[1]) )
Breusch-Pagan statistic: f-value: 2.9517126183995623, p-value: 0.007133642737474482
Breusch-Pagan statistic: lm: 17.635648032499365, p-value: 0.007210254244369134

4. Conclusion

This post explains how to use python-based panel data analysis in your research paper or dissertation. While the model developed in this post is far from perfect, it is important to remember that this is also the case for many models conceived and developed in student research. This is nothing to be scared of. The knowledge is generated gradually, and having proven that the variables of interest (media sentiment measured by the tone) are significant, we can proceed to the next step and develop a better model in the future.
And this is where having a clean easy-to-modify code in Python is also very handy. But this is a topic for another post.
Need more help? Have questions? Contact us

Bibliography

Wooldridge, J. M. (2010)Econometric analysis of cross section and panel data

Comments