<jupyter_start><jupyter_text>Multiple Regression Analysis: Further IssuesIn this notebook, we cover some issues regarding the implementation of regression analyses. We first discuss more flexible specification of regression equations such as variable scaling, standardization, polynomials, and interactions. They can be conveinently included in the **formula** and used in the **statsmodels** OLS estimation. Then, we discuss the model predictions and their confidence intervals. After that we would like to introduce using the **qualitatve regressors** or **categorical variable** in the regression model. Topics:1. Model Formula / Specification2. Prediction3. Qualitative Regressors 1. Model Formula / SpecificationIf we run a regression in **statsmodels** using a syntax like``` pythonsmf.ols('y ~ x1 + x2 + x3', data = sample)```the expression **y ~ x1 + x2 + x3** is referred to as a model **formula** or **specification**. It is a compact symbolic way to describe our regression equation. The dependent variable is separated from the regressors by a '~' and the regressors are separated by a '+' indicating that they enter the equation in a linear fashion. A constant is added by default. Such formula can be specified in more complex ways to indicate different kinds of regression equations. We will cover the most important ones in this section. Data Scaling: Arithmetic Operations within a FormulaWoodldrige (2019) discusses how different scaling of the variables in the model affect the parameter estimates and other statistics in Section 6.1. As an example, a model relating the birth weight to cigarette smoking of the mother during pregnancy and the family income. The basic model equation is$$bwght = \beta_0 + \beta_1 cigs + \beta_2 faminc + u$$which translates into formula syntax as **bwght ~ cigs + faminc**.If we want to measure the weight in pounds rather than ounces, there are two ways to implement different rescaling in *Python*. We can- Define a different variable like **bwghtlbs = bwght / 16** and use this variable in the formula: **bwghtlbs ~ cigs + faminc**- Specify this rescaling directly in the formula: **I(bwght/16) ~ cigs + faminc**The later approach can be more convenient. Note that the **I(...)** brackets describe any parts of the formula in which we specify arithmetic transformations.If we want to measure the number of cigarettes smoked per day in packs, we could again define a new variable **pack = cigs / 20** and use it as a regressor or simply specify the formula **bwght ~ I(cigs/20) + faminc**. Here, the importance to use the **I** function is easy to see. If we specified the formula **bwght ~ I(cigs/20 + faminc)** instead, we would have a nonsense model with only one regressor: the sum of the packs smoked and the income.Below example demonstrates these features. As discussed in Wooldridge (2019, Section 6.1), dividing the dependent variable by 16 changes all coefficients by the same factor $\frac{1}{16}$ and dividing a regressor by 20 changes its coefficient by the factor 20. Other statistics like $R^2$ are unaffected.<jupyter_code># Import the dependencies
import wooldridge as woo
import pandas as pd
import statsmodels.formula.api as smf
# Load the data set 'bwght'
bwght = woo.dataWoo('bwght')
# Regress and report coefficients:
reg = smf.ols(formula = 'bwght ~ cigs + faminc', data = bwght)
results = reg.fit()
# Weight in pounds, manual way:
bwght['bwght_lbs'] = bwght['bwght'] / 16
reg_lbs = smf.ols(formula = 'bwght_lbs ~ cigs + faminc', data = bwght)
results_lbs = reg_lbs.fit()
# Weight in pounds, direct way:
reg_lbs2 = smf.ols(formula = 'I(bwght/16) ~ cigs + faminc', data = bwght)
results_lbs2 = reg_lbs2.fit()
# Packs of cigarettes:
reg_packs = smf.ols(formula = 'bwght ~ I(cigs / 20) + faminc', data = bwght)
results_packs = reg_packs.fit()
# Compare results:
table = pd.DataFrame({"No Scaling": round(results.params, 4),
                     "Manual (Pounds)": round(results_lbs.params, 4),
                     "Direct (Pounds)": round(results_lbs2.params, 4),
                     "Packs of Cigarettes": round(results_packs.params, 4)})

print(f'Compare Results: \n{table}\n')<jupyter_output>Compare Results: 
              No Scaling  Manual (Pounds)  Direct (Pounds)  \
I(cigs / 20)         NaN              NaN              NaN   
Intercept       116.9741           7.3109           7.3109   
cigs             -0.4634          -0.0290          -0.0290   
faminc            0.0928           0.0058           0.0058   

              Packs of Cigarettes  
I(cigs / 20)              -9.2682  
Intercept                116.9741  
cigs                          NaN  
faminc                     0.0928<jupyter_text>Standardization: Beta CoefficientsA specific arithmetic operation is the standardization. A variable is standardized by subtracting its mean and dividing by its standard deviation. For example, the standardized dependent variable $y$ and regressor $x_1$ are$$z_y = \frac{y - \bar{y}}{sd(y)}$$and$$z_{x_{1}} = \frac{x_1 - \bar{x_1}}{sd(x_1)}$$If the regression model only contains standardized variables, the coefficients have a special interpretation. They measure by how many *standard deviation* $y$ changes as the respective independent variable by *one standard deviation*. Inconsistent with the notation used here, they are sometimes referred to as beta coefficients.In *Python*, we can use the same type of arithmetic transformations to subtract the mean and divide by the standard deviation. It can be done more conveniently by defining and using a function **scale()** directly for all variables we want to standardize.  Wooldridge, Example 6.1: Effects of Pollution on Housing PricesWe are interested in how air pollution (nox) and other neighborhood characteristics affect the value of a house. A model using standardization for all variables is expressed in a formula as``` pythonprice_sc ~ 0 + nox_sc + crime_sc + rooms_sc + dist_sc + stratio_sc```With **variable_sc** denoting the scaled version of **variable**. The ouptut shows the parameter estimates of this model. The housing price drops by 0.34 standard deviations as the air pollution increases by one standard deviation.<jupyter_code># Import the dependencies
import wooldridge as woo
import pandas as pd
import numpy as np
import statsmodels.formula.api as smf
# Import the data set 'hprice2'
hprice2 = woo.dataWoo('hprice2')
# Define a function for the standardization
def scale(x):
    x_mean = np.mean(x)
    x_var = np.var(x, ddof = 1)
    x_scaled = (x - x_mean) / np.sqrt(x_var)
    return x_scaled
# Standardize the variables
hprice2['price_sc'] = scale(hprice2['price'])
hprice2['nox_sc'] = scale(hprice2['nox'])
hprice2['crime_sc'] = scale(hprice2['crime'])
hprice2['rooms_sc'] = scale(hprice2['rooms'])
hprice2['dist_sc'] = scale(hprice2['dist'])
hprice2['stratio_sc'] = scale(hprice2['stratio'])
# Build the regression model based on the standardized variables
reg = smf.ols(formula = 
              'price_sc ~ 0 + nox_sc + crime_sc + rooms_sc + dist_sc + stratio_sc',
             data = hprice2)
results = reg.fit()
# Print Regression Table
table = pd.DataFrame({'Betas': round(results.params, 4),
                     'SE': round(results.bse, 4),
                     "t-Stat": round(results.tvalues, 4),
                     "pValue": round(results.pvalues, 4)})

print(f'Regression Table: \n{table}\n')<jupyter_output>Regression Table: 
             Betas      SE   t-Stat  pValue
nox_sc     -0.3404  0.0445  -7.6511     0.0
crime_sc   -0.1433  0.0307  -4.6693     0.0
rooms_sc    0.5139  0.0300  17.1295     0.0
dist_sc    -0.2348  0.0430  -5.4641     0.0
stratio_sc -0.2703  0.0299  -9.0274     0.0<jupyter_text>LogarithmsWe have already seen in previous section that we can include **numpy** functions **log** directly in formulas to represent logarithmic and semi-logarithmic models. A simple example of a partially logarithmic model and its formula would be$$log(y) = \beta_0 + \beta_1 log(x_1) + \beta_2 x_2 + u$$which can be expressed as **np.log(y) ~ np.log(x1) + x2**.Below script shows this again for the house price example.As the air pollution *nox* increases by *one percent*, the hous price drops by about 0.72 *percent*. As the number of rooms increases by *one*, the value of the house increases by roughly 30.6%. Wooldridge (2019, Section 6.2) discusses how the latter value is only an approximation and the actual estimated effect is (exp(0.306) - 1) = 0.358 which is 35.8%.<jupyter_code># Import the dependencies
import wooldridge as woo
import pandas as pd
import numpy as np
import statsmodels.formula.api as smf
# Import the data set 'hprice2'
hprice2 = woo.dataWoo('hprice2')
# Build the log-log regression model
reg = smf.ols(formula = 'np.log(price) ~ np.log(nox) + rooms', data = hprice2)
results = reg.fit()
# Print regression table
table = pd.DataFrame({'Betas': round(results.params, 4),
                     'Standarde Errors': round(results.bse, 4),
                     't Statistics': round(results.tvalues, 4),
                     'p Value': round(results.pvalues, 4)})
print(f'Regression Table: \n{table}\n')<jupyter_output>Regression Table: 
              Betas  Standarde Errors  t Statistics  p Value
Intercept    9.2337            0.1877       49.1835      0.0
np.log(nox) -0.7177            0.0663      -10.8182      0.0
rooms        0.3059            0.0190       16.0863      0.0<jupyter_text>Quadratics and PolynomialsSpecifying quadratic terms or higher powers of regressors can be a useful way to make a model more flexible by allowing the partial effects or (semi-)elasticities to decrease or increase with the value of the regressor.Instead of creating additional variables containing the squared value of a regressor, in *Python* we can simply add **$I(x**2)$** to a formula. Higher order terms are specified accordingly. A simple cubic model and its corresponding formula are$$y = \beta_0 + \beta1 x + \beta_2 x^2 + \beta_3 x^3 + u$$which translates to **y ~ x + I(x**2) + I(x**3)** in formula syntax.For nonlinear model like this, it is often useful to get a graphcial illustration of the effects, which will be discussed in the later section. Wooldridge, Example 6.2: Effects of Pollution on Housing PricesThis example of Wooldridge (2019) demonstrates the combination of logarithmic and quadratic specifications. The model for house price is$$log(price) = \beta_0 + \beta_1 log(nox) + \beta_2 log(dist) + \beta_3 rooms + \beta_4 rooms^2 + \beta_5 stratio + u$$In below example, we implement this model and present detailed results including *t* statistics and their *p* values. The quadratic term of *rooms* has a significantly positive coefficient $\hat{\beta_4}$ implying that the semi-elasticity increases with more rooms. The negative coefficient for rooms and the positive coefficient for *rooms* imply that for "small" number of rooms, the price decreases with the number of rooms and for "large" values, it increases. The number of rooms implying the smallest price can be found as$$\text{rooms*} = -\frac{\beta_3}{2\beta_4} \approx 4.4$$<jupyter_code># Import the dependencies
import wooldridge as woo
import pandas as pd
import numpy as np
import statsmodels.formula.api as smf
# Import the data set 'hprice2'
hprice2 = woo.dataWoo('hprice2')
# Build the regression model with quadratic term of rooms
reg = smf.ols(formula = 'np.log(price) ~ np.log(nox) + np.log(dist) + rooms + I(rooms**2) + stratio', 
              data = hprice2)
results = reg.fit()
# Print regresssion table
table = pd.DataFrame({'Betas': round(results.params, 4),
                     'Standarde Errors': round(results.bse, 4),
                     't Statistics': round(results.tvalues, 4),
                     'p Value': round(results.pvalues, 4)})
print(f'Regression Table: \n{table}\n')<jupyter_output>Regression Table: 
                 Betas  Standarde Errors  t Statistics  p Value
Intercept      13.3855            0.5665       23.6295   0.0000
np.log(nox)    -0.9017            0.1147       -7.8621   0.0000
np.log(dist)   -0.0868            0.0433       -2.0051   0.0455
rooms          -0.5451            0.1655       -3.2946   0.0011
I(rooms ** 2)   0.0623            0.0128        4.8623   0.0000
stratio        -0.0476            0.0059       -8.1293   0.0000<jupyter_text>Hypothesis TestingA natural question to ask is whether a regressor has additional statistically significant explanatory power in a regression model, given all the other regressors. In simple model specifications, this quation can be answered by a simple *t* test, so the results for all regresssors are availabel with a quick look at the standard regression table. When working with polynomials or other specifications, the influnece of one regressor is captured by several parameters. We can test its significance with an *F* test of the joint null hypothesis that all of these parameters are equal to zero. Let's revisit our previous example model$$log(price) = \beta_0 + \beta_1 log(nox) + \beta_2 log(dist) + \beta_3 rooms + \beta_4 rooms^2 + \beta_5 stratio + u$$The significance of *rooms* can be assessed with an *F* test of $H_0: \beta_3 = \beta_4 = 0$. As discussed, such as test can be performed with the command **f_test()** form the module **statsmodels**.<jupyter_code># Import the dependencies
import wooldridge as woo
import numpy as np
import statsmodels.formula.api as smf
# Import the data set 'hprice2'
hprice2 = woo.dataWoo('hprice2')
# Build the regression model with quadratic term of rooms
reg = smf.ols(formula = 'np.log(price) ~ np.log(nox) + np.log(dist) + rooms + I(rooms**2) + stratio', 
              data = hprice2)
results = reg.fit()
# Implement F Test for rooms
hypotheses = ['rooms = 0', 'I(rooms ** 2) = 0']
ftest = results.f_test(hypotheses)
fstat = ftest.statistic[0][0]
fpval = ftest.pvalue

print(f'F Statistics: {fstat}\n')
print(f'F Test p-value: {fpval}\n')<jupyter_output>F Statistics: 110.4187819266896

F Test p-value: 1.9193250019598816e-40<jupyter_text>Interaction TermsModels with interaction terms allow the effect of one variable $x_1$ to depend on the value of another variable $x_2$. A simple model including an interaction term would be$$y = \beta_0 + \beta_1 x_1 + \beta2 x_2 + \beta_3 x_1 x_2 + u$$Of course, we can implement this in *Python* by defining a new variable containing the product of the two regressors. But again, a direct specification in model formula is more convenient. The expression **x1:x2** within a formula adds the interaction term $x_1 x_2$. Even more conveniently, **$x1*x2$** adds not only the interaction but also both original variables allowing for a very concise syntax. So the model can be specified in *Python* as either of the two formulas:``` pythony ~ x1 + x2 + x1:x2```Or``` pythony ~ x1*x2```If one variable $x_1$ is interacted with a set of other varaibles, they can be grouped by parentheses to allow for a compact syntax. For example, the shortest way to express the model equation$$y = \beta_0 + \beta_1 x_1 + \beta2 x_2 + \beta_3 x_3 + \beta_4 x_1 x_2 + \beta_5 x_1 x_3 + u$$in *Python* syntax is $``` pythony ~ x1*(x2 + x3)```**Wooldridge, Example 6.3: Effects of Attendance on Final Exam Performance**This example analyze a model including a standardized dependent variable, quadratic terms and an interaction. Standardized scores in the final exam are explained by class attendance, prior performance and an interaction term:$$stndfnl = \beta_0 + \beta_1 atnrte + \beta2 priGPA + \beta_3 ACT + \beta_4 priGPA^2 + \beta_5 ACT^2 + \beta_6 (priGPA \cdot atndrte) + u$$We estimate this model. The effect of attending classes is$$\frac{\delta stndfnl}{\delta atndrte} = \beta_1 + \beta_6 priGPA$$For the average $\bar{priGPA}$ = 2.59, we estimate this partial effect to be around 0.0078. It tests the null hypothesis that this effect is zero using a simple *F* test. With *p* value of 0.0034, this hypothesis can be rejected at all common significance level.<jupyter_code># Import the dependencies
import wooldridge as woo
import pandas as pd
import numpy as np
import statsmodels.formula.api as smf
# Import the data set 'attend'
attend = woo.dataWoo('attend')
# Find the number of observations
n = attend.shape[0]
# Build the regression model
reg = smf.ols(formula = 'stndfnl ~ atndrte*priGPA + ACT + I(priGPA ** 2), I(ACT ** 2)',
             data = attend)
results = reg.fit()
# Print regression table
table = pd.DataFrame({'Betas': round(results.params, 4),
                     'Standarde Errors': round(results.bse, 4),
                     't Statistics': round(results.tvalues, 4),
                     'p Value': round(results.pvalues, 4)})
print(f'Regression Table: \n{table}\n')
# Estimate for partial effect at priGPA = 2.59
b = results.params
partial_effect = b['atndrte'] + 2.59 * b['atndrte:priGPA']
print(f'Partial Effect: {partial_effect}\n')
# F test for partial effect at priGPA = 2.59
hypotheses = 'atndrte + 2.59 * atndrte:priGPA = 0'
ftest = results.f_test(hypotheses)
fstat = ftest.statistic[0][0]
fpval = ftest.pvalue

print(f'F Statistics: {fstat}\n')
print(f'F Test p-value: {fpval}\n')<jupyter_output><empty_output><jupyter_text>2. PredictionIn this section, we are concerned with predicting the value of the dependent variable *y* given certain values of the regressors $x_1, ..., x_k$. If these are the regressor values in our estimation sample, we called these predictions "fitted values" and discussed their calculation in the previous notebook. Now, we generalize this to arbitrary values and add standard errors, confidence intervals, and prediction intervals. Confidence and Prediction Intervals for PredictionsConfidence intervals reflect the uncertainty about the *expected value* of the dependent variable given values of the regressors. If we are interested in predicting the college GPA of an *individual*, prediction intervals account for the additional uncertainty regarding the unobserved characteristics reflected by the error term *u*.Given a model$$y=\beta_0 + \beta_1x_1 + \beta_2x_2 + ... + \beta_kx_k + u$$we are interested in the expected value of *y* given the regressors take specific values $c_1, c_2, ..., c_k$:$$\theta_0 = E(y|x_1 = c_1, x_2 = c_2, ..., x_k = c_k) = \beta_0 + \beta_1 c_1 + \beta_2 c_2 + ... + \beta_k c_k$$The natural point estimates are $$\hat{\theta_0} = \hat{\beta_0} + \hat{\beta_1} c_1 + \hat{\beta_2} c_2 + ... + \hat{\beta_k} c_k$$and can readily be obtained once the parameter estimates $\hat{\beta_0}, \hat{\beta_1}, \hat{\beta_2}, ... \hat{\beta_k}$ are calculated.Standard errors and confidence intervals are less straightforward to compute. Wooldridge (2019, Section 6.4) suggests a smart way to obtain these for a modified regression. **statsmodels** provides an even simpler and more convenient approach.The method **predict()** automatically calculates $\hat{theta}_0$. The method can be called on an object created by the **fit()** method. Its argument is a data frame containing the values of the regressors $c_1, c_2, ..., c_k$ of the regressors $x_1, x_2, ..., x_k$ with the same variable names as in the data frame used for estimation. If we don't have one yet, it can for example be specified with **pandas** as``` pythonpd.DataFrame({'x1':[c1], 'x2':[c2], ..., 'xk':[ck]}, index = ['newobservation1'])```where **x1** through **xk** are the variable names and **c1** through **ck** are the values which can also be specified as lists to get predictions at several values of the regressors.<jupyter_code># Import the dependencies
import wooldridge as woo
import pandas as pd
import statsmodels.formula.api as smf
# Import data set 'gpa2
gpa2 = woo.dataWoo('gpa2')
# Build the regression model
reg = smf.ols(formula = 'colgpa ~ sat + hsperc + hsize + I(hsize**2)',
             data = gpa2)
results = reg.fit()
# Print the regression table
table = pd.DataFrame({'Betas': round(results.params, 4),
                     'Standarde Errors': round(results.bse, 4),
                     't Statistics': round(results.tvalues, 4),
                     'p Value': round(results.pvalues, 4)})
print(f'Regression Table: \n{table}\n')
# Generate data set containing the regressor values for predictions
cvalues1 = pd.DataFrame({'sat':[1200], 'hsperc':[30],
                        'hsize':[5]}, index = ['newPerson1'])
print(f'Input Values for Prediction: \n{cvalues1}\n')
# Point estimate of prediction 
colgpa_pred1 = results.predict(cvalues1)
print(f'Predicted College GPA: \n{colgpa_pred1}\n')
# Define three sets of regressor variables
cvalues2 = pd.DataFrame({'sat':[1200, 900, 1400],
                       'hsperc':[30, 20, 5],
                       'hsize':[5, 3, 1]},
                       index = ['newPerson1', 'newPerson2', 'newPerson3'])
print(f'Three Sets of INput Values for Prediction: \n{cvalues2}\n')
# Point estimate of prediction
colgpa_pred2 = results.predict(cvalues2)
print(f'Predicted College GPA: \n{colgpa_pred2}\n')<jupyter_output><empty_output><jupyter_text>The method **get_prediction()** calculates not only $\hat{\theta}_0$ (i.e. the exact same predictions as method **predict()**), but also- standard errors of the predictions (column **mean_se**)- confidence intervals (columns **mean_ci_lower** and **mean_ci_upper**) and- prediction intervals (columns **obs_ci_lower** and **obs_ci_upper**). Wooldridge (2019) explains how to calculate the prediction interval manually.All you have to do is calling a second method **summary_frame()** to provide the significance level.  Wooldridge, Example 6.5: Confidence Interval for Predicted College GPAWe try to predict the college GPA, for example to support the admission decisions for our college. Our regression model equation is$$colgpa = \beta_0 + \beta_1 sat + \beta_2 hsperc + \beta_3 hsize + \beta_4hsize^2 + u$$In this example, we show the implementation of the estimation and prediction. The estimation results are stored as the variable **results**. The values of the regressors for which we want to do the prediction are stored in the new data frame **cvalues2**. Then the commands **get_prediction()** and **summary_frame()** are called. For an SAT score of 1200, a high school percentile of 30 and a high school size of 5 (i.e. 500 students), the predicted college GPA is 2.7. Wooldridge (2019) obtains the same value using a general but more cumbersome regression approach. We define two other types of studnets with different values of *sat, hsperc,* and *hsize* in the data frame **cvalues2**.It aslo calculates the 95% and 99% confidence and prediction intervals. The object **colgpa_PICI_95** contains the 95% confidence interval, for example, which is reported in colmuns **mean_ci_lower** and **mean_ci_upper**. With 95% confidence we can say that the expected college GPA for students with the features fo the student named **newPerson1** is between 2.66 and 2.74. The object **colgpa_PICI_99** contains the 99% prediction interval, for example, which is reported in columns **obs_ci_lower** and **obs_ci_upper**. All results are the same as those manually calculated by Wooldridge (2019).<jupyter_code># Import the dependencies
import wooldridge as woo
import pandas as pd
import statsmodels.formula.api as smf
# Import data set 'gpa2
gpa2 = woo.dataWoo('gpa2')
# Build the regression model
reg = smf.ols(formula = 'colgpa ~ sat + hsperc + hsize + I(hsize**2)',
             data = gpa2)
results = reg.fit()
# Define three sets of regressor variables
cvalues2 = pd.DataFrame({'sat':[1200, 900, 1400],
                       'hsperc':[30, 20, 5],
                       'hsize':[5, 3, 1]},
                       index = ['newPerson1', 'newPerson2', 'newPerson3'])
# Point estimates and 95% confidence and prediction intervals
colgpa_PICI_95 = results.get_prediction(cvalues2).summary_frame(alpha = 0.05)
print(f'Prediction Table (95% confidence level): \n{colgpa_PICI_95}\n')
# Point estimates and 99% confidence and prediction intervals
colgpa_PICI_99 = results.get_prediction(cvalues2).summary_frame(alpha = 0.01)
print(f'Prediction Table (99% confidence level): \n{colgpa_PICI_99}\n')<jupyter_output>Prediction Table (99% confidence level): 
       mean   mean_se  mean_ci_lower  mean_ci_upper  obs_ci_lower  \
0  2.700075  0.019878       2.648850       2.751301      1.256386   
1  2.425282  0.014258       2.388540       2.462025      0.982034   
2  3.457448  0.027891       3.385572       3.529325      2.012879   

   obs_ci_upper  
0      4.143765  
1      3.868530  
2      4.902018<jupyter_text>Effect Plots for Nonlinear SpecificationsIn models with quadratic or other nonlinear terms, the coefficients themselves are often difficult to interpret directly. We have to do additional calculations to obtain the partial effect at different values of the regressors or derive the extreme points. For a better visuzl understanding of the implications of our model, it is often useful to calculate predictions for *different values of one regressor* of interest while keeping *the other regressors fixed* at certain values like their overall sample means. By plotting the results against the regressor value, we get a very intuitive graph showing the estimated *ceteris paribus* effects of the regressor.We alreayd know how to calculate predictions and their confidence intervals. In this example, we repeat the regression from Example 6.2 and creates an effects plot for the number of rooms. The number of rooms is varied between 4 and 8 and other variables are set to their respective sample means for all predictions. The regressor values and the implied predictions are shown in a table then plotted with their confidence bands. We see the minimum at a number of rooms of around 4.<jupyter_code># Import the dependencies
import wooldridge as woo
import pandas as pd
import numpy as np
import statsmodels.formula.api as smf
import matplotlib.pyplot as plt
# Import data set 'hprice2'
hprice2 = woo.dataWoo('hprice2')
# Repeating the regresssion from Example 6.2
reg = smf.ols(formula = 'np.log(price) ~ np.log(nox) + np.log(dist) + rooms + I(rooms**2) + stratio', 
              data = hprice2)
results = reg.fit()
# Predictions with rooms = 4 - 8, all others at the sample mean
nox_mean = np.mean(hprice2['nox'])
dist_mean = np.mean(hprice2['dist'])
stratio_mean = np.mean(hprice2['stratio'])
X = pd.DataFrame({'rooms': np.linspace(4, 8, num =5),
                 'nox': nox_mean,
                 'dist': dist_mean,
                 'stratio': stratio_mean})
print(f'Input Values for Prediction: \n{X}\n')
# Calculate 95% confidence interval
lpr_PICI = results.get_prediction(X).summary_frame(alpha = 0.05)
lpr_CI = lpr_PICI[['mean', 'mean_ci_lower', 'mean_ci_upper']]
print(f'Predicted Values with 95% Confidence Interval: \n{lpr_CI}\n')
# Plot the predicted values against 'rooms'
plt.plot(X['rooms'], lpr_CI['mean'], color = 'black',
        linestyle = '-', label = '')
plt.plot(X['rooms'], lpr_CI['mean_ci_upper'], color = 'lightgrey',
        linestyle = "--", label = 'Upper CI')
plt.plot(X['rooms'], lpr_CI['mean_ci_lower'], color = 'darkgrey',
        linestyle = '--', label = 'lower CI')
plt.ylabel('lprice')
plt.xlabel('rooms')
plt.legend()
plt.savefig('images/Effect-Manual.pdf')
plt.show()<jupyter_output><empty_output><jupyter_text>3. Qualitative RegressorsMany variables of interest are qualitative rather than quantitative. Examples include gender, race, labor market status, marital status, and brand choice. In this section, we discuss the use of qualitative variables as regressors. Wooldridge (2019, Section 7.5) also covers linear probability models wtih a binary dependent variable in a linear regression. Since this does not change the implementation, we will skip this topic here.Qualitative information can be represented as binary or dummy varaibles which can only take the value zero or one. In our examples, we see that dummy varaible can be used as regressors just as any other variable. An even more natural way to store *yes / no* type of information in *Python* is to use Boolean variables which can also be directly used as regressors.While qualitative varaibles with more than two outcomes can be represented by a set of dummy variables, the more natrual and convenient way to do this are categorical varaibles as covered in the previous notebook. There are some special cases in which we wish to break a numeric variable into categories is discussed in the later example. Finally, in the last section, we revisit interaction effect and show how these can be used with categorical varaibles to conveniently allow and test for difference in the regression equation. Linear Regression with Dummy as RegressorsIf qualitative data are stored as dummy variable (i.e. variables taking the value zero or one), these can easily be used as regressors in linear regression. If a single dummy varaible is used in a model, its coefficent represents the difference in the intercept between groups, see Wooldridge (2019, Section 7.2).A qualitative variable can also take $g > 2$ values. A variable **MobileOS** could for example take one of the $g = 4$ values "Android", "iOS", "Windows", "other". This information can be represented by $g - 1$ dummy variables, each take the value zero or one, where one category is left out to serve as reference category. They take the value one if the respective operating system is used and zero otherwise. Wooldridge (2019, Section 7.3) gives more information on these variables and their interpretation.Here, we are concerned with implmenting linear regressions with dummy variables as regressors. Everything works as before once we have generated the dummy variables. In the example, data sets provided with Wooldridge (2019), this has usually alreayd been done for us, so we don't have to learn anything new in terms of implmentation. We show two examples. Wooldridge, Example 7.1: Hourly Wage EquationWe are interested in the wage differences by gender and regress the hourly wage on a dummy variable which is equal to one for females and zero for males. We also include regressors for education, experience, and tenure. The implementation with **statsmodels** is standard and the dummy variable **female** is used just as any other regressor. Its estimated coefficent of -1.81 indicates that on average, a woman maeks $1.81 per hour less than a man with the same education, experience, and tenure.<jupyter_code># Import the dependencies
import wooldridge as woo
import pandas as pd
import statsmodels.formula.api as smf
# Import data set "wage1"
wage1 = woo.dataWoo('wage1')
# Build the regression model
reg = smf.ols(formula = 'wage ~ female + educ + exper + tenure',
             data = wage1)
results = reg.fit()
# Print regression table
table = pd.DataFrame({'Betas': round(results.params, 4),
                     'Standarde Errors': round(results.bse, 4),
                     't Statistics': round(results.tvalues, 4),
                     'p Value': round(results.pvalues, 4)})
print(f'Regression Table: \n{table}\n')<jupyter_output>Regression Table: 
            Betas  Standarde Errors  t Statistics  p Value
Intercept -1.5679            0.7246       -2.1640   0.0309
female    -1.8109            0.2648       -6.8379   0.0000
educ       0.5715            0.0493       11.5836   0.0000
exper      0.0254            0.0116        2.1951   0.0286
tenure     0.1410            0.0212        6.6632   0.0000<jupyter_text>Wooldridge, Example 7.6: Log Hourly Wage EquationWe used log wage as dependent variable and distinguish gender and marital status using a qualitative variable with the four outcomes "single female", "single male", "married female", and "married male". We actually implement this regression using an interaction term between **married** and **female**. Relative to the reference group of single makes with the same education, experience, and tenure, married males make about 21.3% more (the coefficent of **married**), and single females make about 11.0% less (the coefficient of **female**). The coefficient of the interaction term implies that married females make around 30.1% - 21.3% = 8.7% less than single females, 30.1% - 21.3% = 41.1% less than married males, and 30% + 11.0% - 21.3% = 19.8% less than single males. Note once again that the approximate interpretation as percent may be inaccurate.<jupyter_code># Import the dependencies
import wooldridge as woo
import pandas as pd
import numpy as np
import statsmodels.formula.api as smf
# Import data set "wage1"
wage1 = woo.dataWoo('wage1')
# Build the regression model
reg = smf.ols(formula = 'np.log(wage) ~ married*female + educ + exper +'
              'I(exper**2) + tenure + I(tenure**2)',
             data = wage1)
results = reg.fit()
# Print regression table
table = pd.DataFrame({'Betas': round(results.params, 4),
                     'Standarde Errors': round(results.bse, 4),
                     't Statistics': round(results.tvalues, 4),
                     'p Value': round(results.pvalues, 4)})
print(f'Regression Table: \n{table}\n')<jupyter_output>Regression Table: 
                 Betas  Standarde Errors  t Statistics  p Value
Intercept       0.3214            0.1000        3.2135   0.0014
married         0.2127            0.0554        3.8419   0.0001
female         -0.1104            0.0557       -1.9797   0.0483
married:female -0.3006            0.0718       -4.1885   0.0000
educ            0.0789            0.0067       11.7873   0.0000
exper           0.0268            0.0052        5.1118   0.0000
I(exper ** 2)  -0.0005            0.0001       -4.8471   0.0000
tenure          0.0291            0.0068        4.3016   0.0000
I(tenure ** 2) -0.0005            0.0002       -2.3056   0.0215<jupyter_text>Boolean VariablesA natural way for storing qualitative *yes / no* informationin *Python* is to use **Boolean** variables. They can take the values **True** or **False** and can be transformed into a 0/1 dummy variable with the function **int()** where **True = 1** and **Flase = 0**. 0/1-coded dummies can *vice versa* be transformed into logical variables with the function **bool()**.Instead of transforming Boolean variable into dummies, they can be directly used as regressors. The coefficient is then named **varname[T.True]** indicating that **True** was treated as **1**. In the following example we repeat the analysis of Example 7.1 with the regressor **female** being coded as **bool** instead of a 0/1 dummy variable.<jupyter_code># Import the dependencies
import wooldridge as woo
import pandas as pd
import statsmodels.formula.api as smf
# Import data set "wage1"
wage1 = woo.dataWoo('wage1')
# Create a Boolean variable "isfemale"
wage1['isfemale'] = (wage1['female'] == 1)
# Build the regression model with boolean variable
reg = smf.ols(formula = 'wage ~ isfemale + educ + exper + tenure',
             data = wage1)
results = reg.fit()
# Print regression table
table = pd.DataFrame({'Betas': round(results.params, 4),
                     'Standarde Errors': round(results.bse, 4),
                     't Statistics': round(results.tvalues, 4),
                     'p Value': round(results.pvalues, 4)})
print(f'Regression Table: \n{table}\n')<jupyter_output>Regression Table: 
                   Betas  Standarde Errors  t Statistics  p Value
Intercept        -1.5679            0.7246       -2.1640   0.0309
isfemale[T.True] -1.8109            0.2648       -6.8379   0.0000
educ              0.5715            0.0493       11.5836   0.0000
exper             0.0254            0.0116        2.1951   0.0286
tenure            0.1410            0.0212        6.6632   0.0000<jupyter_text>In real-world data set, qualitative information is often not readily coded as logical or dummy variables, so we might want to create our own regressors. Suppose a qualitative variable saved as the **numpy** array **OS** take one of the three string values "Android", "iOS", "Windows", or "other". We can manually define the three relevant logical variables with "Android" as the reference category with``` PythoniOS = os == 'iOS'wind = os == 'Windows'oth = os == 'other'```A more convenient and elegant way to deal with qualitative variables are categorical variables discussed in the next section. Categorical VariablesWe have introduced categorical variables of type **Categorical**. They take one of the given set of outcomes which can be labeled arbitrarily. This makes the natural type to store qualitative information.In a linear regression performed by **Statsmodels** we can easily transform any variable into a categorical variable using the function **C()** in the definition of the fomula. The function **ols()** is clever enough to implicitly add $g - 1$ dummy variables if the variable has *g* outcomes. As a reference category, the first category is left out by default.In the following example, we show how categorical varaibles are used. It uses the data set *CPS1982*. This data set is similar to the one used in Example 7.1 and 7.6 in that it contains wage and other data for 534 individuals. The frequency table for the two variables **gender** and **occupation** are shown in the output. The variable **gender** has two categories **male** and **female**. The variable **occupation** has size categories.In the output, the coefficidents are labeled with a combination of the variable and category name. As an example, the estimated coefficient of 0.224 for **C(gender)[T.male]** in **results** implies that men make about 22.4% more than women who are the same in terms of the other regressors. Employees in technical positions earn arround 1% (see coefficient of **C(occupation)[T.technical]**)less than otherwise equal management positions (who are the reference category).We can choose different reference categories using a second argument of the **C** command, where we provide a new reference group **somegroup** with the command **treatment('somegroup')**. In the specification **results_newref**, we choose **male** and **technical**. When we rerun the same regression command, we see the expected results: Variables like **education** and **experience** get the same coefficients. The dummy variable for females gets the negative of what the males got previously. Obviously, it is equivalent to say "female log wages are lower by 0.224" and "male log wages are higher by 0.224".The coefficients for the occupation are now relative to **technical**. From the first regression we already knew that technical positions make 1% less than managers, so it is not surprising that in the second regression we find that managers make 1% more than technical positions. The other occupation coefficents are higher by 0.010085 implying the same relative comparisons as in the first specification.<jupyter_code># Import the dependencies
import wooldridge as woo
import pandas as pd
import statsmodels.formula.api as smf
# Import data set "CPS1985" from local drive
CPS1985 = pd.read_csv('data/CPS1985.csv')
# Rename variable to make outputs more compact
CPS1985['cc'] = CPS1985['occupation']
# Table of categories and frequencies for two categorical variables
freq_gender = pd.crosstab(CPS1985['gender'], columns = 'count')
print(f'Frequency Table by Gender: \n{freq_gender}\n')

freq_occupation = pd.crosstab(CPS1985['cc'], columns = 'count')
print(f'Frequency Table by Occupation: \n{freq_occupation}\n')
# Directly using categorical variables in regression formula
reg = smf.ols(formula = 'np.log(wage) ~ education + experience + C(gender) + C(cc)',
             data = CPS1985)
results = reg.fit()
# Print regression table
table = pd.DataFrame({'Betas': round(results.params, 4),
                     'Standarde Errors': round(results.bse, 4),
                     't Statistics': round(results.tvalues, 4),
                     'p Value': round(results.pvalues, 4)})
print(f'Regression Table: \n{table}\n')
# Rerun the regression with different reference category
reg_newref = smf.ols(formula = 'np.log(wage) ~ education + experience +'
                     'C(gender, Treatment("male")) + C(cc, Treatment("technical"))',
             data = CPS1985)
results_newref = reg_newref.fit()
# Print regression table
table_newref = pd.DataFrame({'Betas': round(results_newref.params, 4),
                     'Standarde Errors': round(results_newref.bse, 4),
                     't Statistics': round(results_newref.tvalues, 4),
                     'p Value': round(results_newref.pvalues, 4)})
print(f'Regression Table: \n{table_newref}\n')<jupyter_output>Regression Table: 
                                              Betas  Standarde Errors  \
Intercept                                    1.1187            0.1765   
C(gender, Treatment("male"))[T.female]      -0.2238            0.0423   
C(cc, Treatment("technical"))[T.management]  0.0101            0.0740   
C(cc, Treatment("technical"))[T.office]     -0.1972            0.0678   
C(cc, Treatment("technical"))[T.sales]      -0.3500            0.0863   
C(cc, Treatment("technical"))[T.services]   -0.3525            0.0750   
C(cc, Treatment("technical"))[T.worker]     -0.1425            0.0705   
education                                    0.0759            0.0101   
experience                                   0.0119            0.0017   

                                             t Statistics  p Value  
Intercept                                          6.3393   0.0000  
C(gender, Treatment("male"))[T.female]            -5.2979   0.0000  
C(cc, Treatment("technical"))[T.management][...]<jupyter_text>ANOVA TablesA natural question to ask is whether a regressor has additional statistically significant explanatory power in a regression model, given all the other regressors. In simple model specification, this question can be answered by a simple *t* test, so the results for all regressors are available with a quick look at the standard regression table. When working with categorical variables, polynomials, or other specifications, the influence of one variable is captured by several regressors. In the previous example, the effect of **occupation** is captured by the five regressors of the respective dummy variables.We can test its significance with an *F* test of the joint null hypothesis that all of these parameters are equal to zero. As an example, let's revisit the underlying model in **reg** from our last example.$$log(wage) = \beta_0 + \beta_1 education + \beta_2 experience + \beta_3 gender + \beta_4 office + \beta5 sales + \beta_6 services + \beta_7 technical + \beta_8 worker + u$$The significance of **occupation** can be assessed with an *F* test of $H_0: \beta_4 = \beta_5 = \beta_6 = \beta_7 = \beta_8 = 0$.As discussed before, such a test can be performed with the command **f_test()** from the module **statsmodels**.A **Type II ANOVA** (analysis of variance) table does exatly this for each variable in the model and displays the results in a clearly arranged table. **statsmodels** implements this in the method **anova_lm()**. Our example shows that all the relevant results from our previous **F** test can be found again in the row labelled **occupation**. Column **df** indicates that this test involves five parameters. All other variables enter the model with a single parameter. Consequently the value of their **F** test statistics corresponds to the respective squared *t* statistics in the object results.The ANOVA table also allows to quickly compare the relevance of the regressors. The first column shows the sum of squared deviations explained by the variable after all the other regressors are controlled for. ANOVA tables of Type I and III are less often of interest. They differ in what other variables are controlled for when testing for the effect of one regressor.In the ANOVA Type II table, we see that **education** has the highest explanatory power. Moreover, **occupation** has a highly significant effect on wages. The explained sum of squares (after controlling for all other regressors) is higher than that of **gender**. But since it is based on five parameters instead of one, the *F* statistic is lower.<jupyter_code># Import the dependencies
import wooldridge as woo
import pandas as pd
import statsmodels.formula.api as smf
import statsmodels.api as sm
# Import data set "CPS1985" from local drive
CPS1985 = pd.read_csv('data/CPS1985.csv')
# Run regression
reg = smf.ols(
    formula = 'np.log(wage) ~ education + experience + gender + occupation',
    data = CPS1985)
results = reg.fit()
# Print regression table
table = pd.DataFrame({'Betas': round(results.params, 4),
                     'Standarde Errors': round(results.bse, 4),
                     't Statistics': round(results.tvalues, 4),
                     'p Value': round(results.pvalues, 4)})
print(f'Regression Table: \n{table}\n')
# Create_ANOVA table
table_anova = sm.stats.anova_lm(results, typ = 2)
print(f'ANOVA Table: \n{table_anova}\n')<jupyter_output>ANOVA Table: 
                sum_sq     df          F        PR(>F)
gender        5.414018    1.0  28.067296  1.727015e-07
occupation    7.152529    5.0   7.416013  9.805485e-07
education    10.980589    1.0  56.925450  2.010374e-13
experience    9.695055    1.0  50.261001  4.365391e-12
Residual    101.269451  525.0        NaN           NaN<jupyter_text>Breaking a Numeric Variable into CategoriesSometimes, we do not use a numeric variable directly in a regression model because the implied linear relation seems implausible or inconvenient to interpret. As an alternative to working with transformation such as logs and quadratic terms, it sometimes makes sense to estimate different levels of different ranges of the variable. Wooldridge (2019, Example 7.8) gives the example of the ranking of a law school and how it relates to the starting salary of its graduatees.Given a numeric variable, we need to generate a categorical variable to represent the range into which the rank of a school falls. In *Python*, the command **cut()** from **pandas** is very convenient for this. It takes a numeric variable and a list of cut points and returns a categorical variable. By default, the upper cut points are included in the corresponding range. Wooldridge, Example 7.8: Effects of Law School Rankings on Starting SalariesThe variable **rank** of the data set *LAWSCH85* is the rank of the law school as a number between 1 and 175. We would like to compare schools in the top 10, ranks 11-25, 26-40, 41-60, 61-100 to the reference group of rank above 100. So we store the cut points 0, 10, 25, 40, 60, 100, and 175 in a variable **cutpts**. In the data frame **lawsch85**, we create our new variable **rc** using the **cut()** command.To be consistent with Wooldridge (2019), we do not want the top 10 schools as a reference category but the last categroy. It is chosen with the second argument of the **C()** command. The regression results imply that graduates from the top 10 schools collect a starting salary which is around 70% higher than those of the schools below rank 100. In fact, this approximation is inaccurate with these large numbers and the coefficient of 0.7 actually implies a difference of exp(0.7) - 1 = 1.013 or 101.3%.The ANOVA table at the end of the of the output shows that at a 5% significance level, the school rank is the only variable that has a significant explanatory power for the salary in this specificiation.<jupyter_code># Import the dependencies
import wooldridge as woo
import pandas as pd
import statsmodels.formula.api as smf
import statsmodels.api as sm
# Import data set 'lawsch85'
lawscho85 = woo.dataWoo('lawsch85')
# Define cut points for the rank
cutpts = [0, 10, 25, 40, 60, 100, 175]
# Create categorical variable containing ranges for the rank
lawsch85['rc'] = pd.cut(lawsch85['rank'], bins = cutpts,
                       labels = ['(0,10]', '(10, 25]', '(25,40]',
                                '(40,60]', '(60,100]', '(100,175]'])
# Display Frequencies
freq = pd.crosstab(lawsch85['rc'], columns = 'count')
print(f'Frequency Table: \n{freq}\n')
# Run regresssion
reg = smf.ols(formula = 'np.log(salary) ~ C(rc, Treatment("(100,175]")) +'
             'LSAT + GPA + np.log(libvol) + np.log(cost)',
             data = lawsch85)
results = reg.fit()
# Print regression table
table = pd.DataFrame({'Betas': round(results.params, 4),
                     'Standarde Errors': round(results.bse, 4),
                     't Statistics': round(results.tvalues, 4),
                     'p Value': round(results.pvalues, 4)})
print(f'Regression Table: \n{table}\n')
# ANOVA table
table_anova = sm.stats.anova_lm(results, typ = 2)
print(f'ANOVA Table: \n{table_anova}\n')<jupyter_output>ANOVA Table: 
                sum_sq     df          F        PR(>F)
gender        5.414018    1.0  28.067296  1.727015e-07
occupation    7.152529    5.0   7.416013  9.805485e-07
education    10.980589    1.0  56.925450  2.010374e-13
experience    9.695055    1.0  50.261001  4.365391e-12
Residual    101.269451  525.0        NaN           NaN<jupyter_text>Interactions and Differences in Regression Functions Across GroupsDummy and categorical variable can be interacted just like any other variable. Wooldridge (2019, Section 7.4) discusses the specification and interpretation in this setup. An important case is a model in which one or more dummy variables are interacted with all other regressors. This allows the whole regression model to differ by groups of observations identified by the dummy variable(s).The example from Wooldridge (2019, Section 7.4-c) is replicated here. Note that the example only applies to the subset of data with **spring == 1**. We use the **subset** option of **ols()** directly to define the estimation sample. Other than that, the script does not introduce any new syntax but combines two tricks we have seen previously:- The dummy variable **femail** is interacted with all other regressors using the "$*$" formula syntax with the other variables contained in parentheses.- The *F* test for all interaction effects is performed using the command **f_test()**.<jupyter_code># Import the dependencies
import wooldridge as woo
import pandas as pd
import statsmodels.formula.api as smf
# Import data set 'gpa3'
gpa3 = woo.dataWoo('gpa3')
# Model with full interactions with female dummy (only for spring data)
reg = smf.ols(formula = 'cumgpa ~ female * (sat + hsperc + tothrs)',
             data = gpa3, subset = (gpa3['spring'] == 1))
results = reg.fit()
# Print regression table
table = pd.DataFrame({'Betas': round(results.params, 4),
                     'Standarde Errors': round(results.bse, 4),
                     't Statistics': round(results.tvalues, 4),
                     'p Value': round(results.pvalues, 4)})
print(f'Regression Table: \n{table}\n')
# F-test for H0 (interaction coefficients of 'female' are zero)
hypotheses = ['female = 0', 'female:sat = 0',
              'female:hsperc = 0', 'female:tothrs = 0']
ftest = results.f_test(hypotheses)
fstat = ftest.statistic[0][0]
fpval = ftest.pvalue

print(f'F Statistics: {fstat}\n')
print(f'F Test p-value: {fpval}\n')<jupyter_output>F Statistics: 8.179111637046036

F Test p-value: 2.544637191823361e-06<jupyter_text>We can estimate the same model parameters by running two separate regressions, one for females and one ofr males. We see that in the joint model, the parameters without interactions (**Intercept, sat, hsperc, and tothrs**) apply to the males and the interaction parameters reflect the *differences* to the males.To reconstruct the parameters for females from the joint model, we need to add the two respective parameters. The intercept for females is 1.4808 - 0.3535 = 1.1273 and the coefficient of **sat** for females is 0.0011 + 0.0008 $\approx$ 0.0018.<jupyter_code># Estimate model for males (& spring data)
reg_m = smf.ols(formula = 'cumgpa ~ sat + hsperc + tothrs',
             data = gpa3, subset = (gpa3['spring'] == 1) & (gpa3['female'] == 0))
results_m = reg_m.fit()
# Print regression table
table_m = pd.DataFrame({'Betas': round(results_m.params, 4),
                     'Standarde Errors': round(results_m.bse, 4),
                     't Statistics': round(results_m.tvalues, 4),
                     'p Value': round(results_m.pvalues, 4)})
print(f'Regression Table: \n{table_m}\n')
# Estimate model for females (& spring data)
reg_f = smf.ols(formula = 'cumgpa ~ sat + hsperc + tothrs',
             data = gpa3, subset = (gpa3['spring'] == 1) & (gpa3['female'] == 1))
results_f = reg_f.fit()
# Print regression table
table_f = pd.DataFrame({'Betas': round(results_f.params, 4),
                     'Standarde Errors': round(results_f.bse, 4),
                     't Statistics': round(results_f.tvalues, 4),
                     'p Value': round(results_f.pvalues, 4)})
print(f'Regression Table: \n{table_f}\n')<jupyter_output>Regression Table: 
            Betas  Standarde Errors  t Statistics  p Value
Intercept  1.1273            0.3616        3.1176   0.0025
sat        0.0018            0.0003        5.1950   0.0000
hsperc    -0.0090            0.0029       -3.0956   0.0027
tothrs     0.0022            0.0014        1.5817   0.1174
{'fraction_non_alphanumeric': 0.09339562219794297, 'fraction_numerical': 0.043344761330670986, 'mean_word_length': 3.5451198630136984, 'pattern_counts': {'":': 6, '<': 55, '<?xml version=': 0, '>': 58, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 11, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '17', 'dir': 'jupyter-structured-clean-dedup', 'id': '473757', 'license': 'MIT', 'n_tokens_mistral': 16772, 'n_tokens_neox': 14927, 'n_words': 6412, 'path': '6. Multiple Regression Analysis - Further Issues.ipynb', 'repo_name': 'NormanLo4319/Econometric-with-Python', 'size': '53086'}