3.10 Regression with categorical variables

Since the regression model should have only numeric variables, We should convert any existing categorical variable to dummy variables so that each level will be assigned to a separate dummy variable. Therefore, if the level exists for a particular individual in the sample, so its associated dummy variable value gets one, otherwise gets zero. To well understand the regression with categorical variables, we start with a single categorical variable in this subsection. In the next one, we will fit a model with more than one categorical one and show the interaction between them. Then, in the last one, our model will have a mix of categorical and numeric variables.

3.10.1 Regression with one categorical variables

Let us start by an example. Say we have the categorical variable status with three levels single, married, and divorced to explain the monthly consumption variable. That means, with three levels, we should get three dummy variable shown in the following example:

Table 3.13: Dummy variables for each level
cons status \(d_1\) \(d_2\) \(d_3\)
12.30 sing 1 0 0
13.24 sing 1 0 0
16.53 sing 0 0 0
12.76 mar 0 1 0
23.14 mar 0 1 0
32.45 mar 0 1 0
10.34 mar 0 1 0
21.12 div 0 0 1
23.41 div 0 0 1

Now we can include these dummies in the model in place of the original categorical variable. However, since these dummies sum up to one \(d_1+d_2+d_3=1\), included together will violate the assumption A9 (see the definition 3.6) that says the regressors should be uncorrelated. we can easily solve this issue by including only two dummy variables in the model, and the third one will be taken into account by the intercept. For instance, if we remove \(d_1\), then the estimated model will be (without subscript for simplification):

\[\begin{equation*} \widehat y=\widehat\beta_1+\widehat\beta_2 d_2+\widehat\beta_2 d_2 \end{equation*}\]

Then depending on the level value, the response variable will be:

\[\begin{equation*} \widehat y= \begin{cases} \widehat\beta_1+\widehat\beta_2\overbrace{d_2}^{=0}+\widehat\beta_3\overbrace{d_3}^{=0}=\widehat\beta_1 & \text{si}\quad d_1=1,\quad \text{single}\\ \widehat\beta_1+\widehat\beta_2\overbrace{d_2}^{=1}+\widehat\beta_3\overbrace{d_3}^{=0}=\widehat\beta_1+\widehat\beta_2 & \text{si}\quad d_2=1,\quad \text{married}\\ \widehat\beta_1+\widehat\beta_2\overbrace{d_2}^{=0}+\widehat\beta_3\overbrace{d_3}^{=1}=\widehat\beta_1+\widehat\beta_3 & \text{si}\quad d_3=1,\quad \text{divorced} \end{cases} \end{equation*}\]

Including the expectation operator, that formula will be:

\[\begin{equation*} E(\widehat y)=y= \begin{cases} E(\widehat\beta_1)=\beta_1 & \text{si}\quad d_1=1,\quad \text{single}\\ E(\widehat\beta_1)+E(\widehat\beta_2)=\beta_1+\beta_2 & \text{si}\quad d_2=1,\quad \text{married}\\ E(\widehat\beta_1)+E(\widehat\beta_3)=\beta_1+\beta_3 & \text{si}\quad d_3=1,\quad \text{divorced} \end{cases} \end{equation*}\]

Now the coefficients interpretation is straightforward. The estimate \(\widehat\beta_1\) is the consumption mean of the single persons, \(\widehat\beta_2\) is the difference between the mean of married persons and that of single ones, and lastly, \(\widehat\beta_3\) is the difference between the mean of divorced ones and that of single ones.

Therefore, if the means of all the levels are close to each other, then we expect \(\widehat\beta_2\) and \(\widehat\beta_3\) to be not significantly different from zero, which means that the categorical variable does not affect at all the dependent variable. Otherwise, if at least two of the means are significantly different, then that variable has some significant effect.

That is why, when dealing with categorical variables, we use, instead, the ANOVA analysis. you might have noticed, that the fitted response has only three possible values, \(\widehat y=\widehat\beta_1\) \(\widehat y=\widehat\beta_1+\widehat\beta_2\), or \(\widehat y=\widehat\beta_1+\widehat\beta_3\). If all the coefficients are significant, then we should have three horizontal parallel lines so that each one represents the mean of the corresponding level.

The ANOVA analysis is a better approach to assess the difference between the means of the categorical variable levels by comparing their variances. The idea behind this approach is the OLS regression of \(y\) on the intercept that gives the minimum sum squared residuals when \(\widehat\beta_1=\overline y\), that is \(\sum\limits^n_{t=1}(y_t-\overline y)^2\). Thus, by using our example, if the means of the levels are approximately equal, that is \(\overline y \approx\overline y_{d_1}\approx\overline y_{d_2}\approx\overline y_{d_3}\), then we expect the following equality to be significant:

\[\begin{equation*} \sum\limits^n(y_t-\overline y)^2=\sum\limits^{n_1}(y_t-\overline y_{d_1})^2+\sum\limits^{n_2}(y_t-\overline y_{d_2})^2+\sum\limits^{n_3}(y_t-\overline y_{d_3})^2 \end{equation*}\]

We can check that visually using the following box plot.

ggplot(aes(x=status, y=cons, color=status), data=df )+
  geom_boxplot(fill='lightgrey')+
  theme_classic()
Box plot to check the equality of means

Figure 3.6: Box plot to check the equality of means

The conclusion will depend on whether the boxes are all overlapped or not. As we see, the div and sing boxes do not (with a significance level of 0.05), so we can conclude that the means are not equal. However, the mean of mar is significantly equal to both div and sing due to its large variability, which is an important issue, because the anova analysis requires the variances to be equal as an assumption.

Example 3.9 Let us generate the following data with only two variables, one continuous as a dependent variable, and the second one is of factor type as a regressor.

In R:
library(simglm)
set.seed(1133)
arguments <- list(
  formula = cons ~ 1 + status,
  fixed = list(status = list(
    var_type = 'factor', levels = c('sing', 'mar', 'div')
  )),
  error = list(variance = 4),
  sample_size = 30,
  reg_weights = c(2, 2.5, 0.7)
)

dat <- simulate_fixed(data = NULL, arguments) %>% 
  simulate_error(arguments) %>% 
  generate_response(arguments)

# keep only the the dependent variable and the dummy variables

dat <- dat[,c("cons", "status")] 
head(dat)
cons status
3.3537307 div
4.8247590 sing
1.0313386 sing
7.1554922 sing
2.5342504 sing
0.1814665 sing

We can go ahead and fit the model.

mod_dat <- lm(cons~status, data=dat)
tidy(mod_dat)
term estimate std.error statistic p.value
(Intercept) 1.6755620 0.6622482 2.5301118 0.0175442
statusmar 2.8128160 0.9576093 2.9373315 0.0066960
statusdiv 0.7570396 1.0910598 0.6938571 0.4936988

As we see only one level is significant, statusmar. But as whole, we can say that the categorical variable status is significant.

The best approach to analyze this type of variables is by using the anova analysis. R provides an easy way to do this through the function aov.

mod_dat2 <- aov(cons~status, data=dat)
tidy(mod_dat2)
term df sumsq meansq statistic p.value
status 2 47.2645 23.632251 4.490372 0.0207235
Residuals 27 142.0975 5.262872 NA NA

If we use the significance level of 0.05, we should reject the null hypothesis that the means are all equal, and hence the categorical variable has a significant effect.

This analysis assumes that the variances are equal. Let us thus compute those variances.

tapply(dat$cons, dat$status, 'var')
[out]     sing      mar      div 
[out] 5.714665 3.821520 6.836839

The following test (among others) test this assumption5.

bartlett.test(dat$cons~dat$status)
[out] 
[out]   Bartlett test of homogeneity of variances
[out] 
[out] data:  dat$cons by dat$status
[out] Bartlett's K-squared = 0.68771, df = 2, p-value = 0.709

With that p-value 0.709, we should not reject the null hypothesis that says the variances are equal, which justifies the use of the ANOVA results.

  • We can obtain the ANOVA table by using the function summary.aov with the original model mod_dat as an argument summary.aov(mod_dat).
In Python:

The function used to fit models with categorical variables is the same used for continuous ones, but we should first copy the data into python

if not 'dat_py' in globals():
  dat_py = r.dat  
import statsmodels.formula.api as smf

mod_py = smf.ols('cons~status', data=dat_py).fit()
result_py = mod_py.summary2().tables[1]
result_py
Table 3.14: the estimates in python
Coef. Std.Err. t P>|t| [0.025 ]
Intercept 1.7326016 0.8670864 1.9981879 0.0558598 -0.0465128 3.511716
status[T.mar] 2.7557764 1.1091807 2.4845153 0.0194687 0.4799256 5.031627
status[T.sing] 0.6429604 1.0910598 0.5892989 0.5605598 -1.5957095 2.881630

And we retrieve the ANOVA table as follows:

import statsmodels.api as sm

# type=2 to get the result in a data frame
anova_py = sm.stats.anova_lm(mod_py, type=2)
anova_py
Table 3.15: ANOVA table in Python
df sum_sq mean_sq F PR(>F)
status 2 40.29112 20.145561 3.827864 0.0343912
Residual 27 142.09755 5.262872 NaN NaN

3.10.2 Regression with two categorical variables

For simplicity, we will include only two categorical variables, since the same analysis can be extended. Let us first include another categorical variable to the above data.

set.seed(1133)
arguments <- list(
  formula = cons ~ 1 + status+sex,
  fixed = list(status = list(var_type = 'factor', levels = c('sing', 'mar', 'div')), sex = list(var_type = 'factor', levels = c('female', 'male'))),
  error = list(variance = 4),
  sample_size = 30,
  reg_weights = c(2, 3.5, 2.5, 0.7)
)

dat_R <- simulate_fixed(data = NULL, arguments) %>%
  simulate_error(arguments) %>%
  generate_response(arguments)

# keep only the the dependent variable and the dummy variables
dat_R <- dat_R[,c("cons", "sex", "status")]
head(dat_R)
cons sex status
5.012040 female div
3.589554 female sing
1.766998 male sing
3.025467 female sing
1.721436 male sing
5.746511 female sing
In R:

Before fitting the model, we should first visualize the distribution of the levels of each factor.



p1 <- ggplot(data = dat_R, aes(x = status, y = cons)) +   
  geom_boxplot(fill = 'lightblue') +  
  ggtitle('status') +  
  theme_classic()  

p2 <- ggplot(data = dat_R, aes(x = sex, y = cons)) +   
  geom_boxplot(fill = 'yellow') +  
  ggtitle('sex') +  
  theme_classic()  

# use patchwork package to concatenate the plots
p1 + p2

From the first plot, since the boxes do not overlap, we expect the factor status to be significant. Whereas the sex factor boxes overlap, indicating its non-significance in explaining the dependent variable cons.
Besides the separate effect of each factor, there exists another one that comes from the interaction between these factors. It can be visualized as follows:

ggplot(data = dat_R, aes(x=status, y=cons, color=sex))+  
  geom_boxplot()+  
  theme_classic()

The first two boxes show that there is no significant difference between the divorced males and divorced females. The same conclusion applies to the second two boxes for married persons. However, the third two boxes show a slight separation between single males and single females. Since so, we cannot decide about interaction effect, and hence further statistic analysis is needed.

Let us verify this conclusion by fitting the model with the function lm.

mod_R_2fac <- lm(cons~sex*status, data=dat_R)
tidy(mod_R_2fac)
term estimate std.error statistic p.value
(Intercept) 3.06 0.69 4.43 0.00
sexmale -1.71 0.90 -1.89 0.07
statusmar 1.95 0.90 2.16 0.04
statusdiv 1.24 1.29 0.96 0.34
sexmale:statusmar 1.51 1.32 1.14 0.26
sexmale:statusdiv 2.54 1.58 1.61 0.12

If we have two factors, the first one with \(n\) levels and the second one with \(m\) levels, then the total number of dummies that we should create is \(n+m+(n\times m)\), where \((n\times m)\) for the interaction. But, as discussed in the previous subsection, we should remove some dummies to ensure the invertibility of the design matrix \(X\). In our case, for instance, The female and div levels are taken into account by the intercept, such that it remains male (from sex), mar and sing(from status), male-mar and male-sing (from interaction). To understand how the estimates computed, see the following formulation:

For females:

\[\begin{equation*} \widehat {cons}=\begin{cases} \overset{\Large\textit{ divorced female}}{\widehat\alpha_0+\widehat\alpha_1 \overbrace{mal}^{=0}+\widehat\beta_1 \overbrace{mar}^{=0}+\widehat\beta_2 \overbrace{sing}^{=0}+\widehat\gamma_1 \overbrace{malmar}^{=0}+\widehat\gamma_2 \overbrace{malsing}^{=0}=\widehat\alpha_0}\\ \overset{\Large\textit{married female}}{\widehat\alpha_0+\widehat\alpha_1 \overbrace{mal}^{=0}+\widehat\beta_1 \overbrace{mar}^{=1}+\widehat\beta_2 \overbrace{sing}^{=0}+\widehat\gamma_1 \overbrace{malmar}^{=0}+\widehat\gamma_2 \overbrace{malsing}^{=0}=\widehat\alpha_0+\widehat\beta_1}\\ \overset{\Large\textit{single female}}{\widehat\alpha_0+\widehat\alpha_1 \overbrace{mal}^{=0}+\widehat\beta_1 \overbrace{mar}^{=0}+\widehat\beta_2 \overbrace{sing}^{=1}+\widehat\gamma_1 \overbrace{malmar}^{=0}+\widehat\gamma_2 \overbrace{malsing}^{=0}=\widehat\alpha_0+\widehat\beta_2} \end{cases} \end{equation*}\]

  • \(\widehat\alpha_0\): divorced female average.
  • \(\widehat\beta_1\): married female average - divorced female average.
  • \(\widehat\beta_2\): single female average - divorced female average.

For males:

\[\begin{equation*} \widehat {cons}=\begin{cases} \overset{\Large\textit{divorced male}}{\widehat\alpha_0+\widehat\alpha_1 \overbrace{mal}^{=1}+\widehat\beta_1 \overbrace{mar}^{=0}+\widehat\beta_2 \overbrace{sing}^{=0}+\widehat\gamma_1 \overbrace{malmar}^{=0}+\widehat\gamma_2 \overbrace{malsing}^{=0}=\widehat\alpha_0+\widehat\alpha_1}\\ \overset{\Large\textit{married male}}{\widehat\alpha_0+\widehat\alpha_1 \overbrace{mal}^{=1}+\widehat\beta_1 \overbrace{mar}^{=1}+\widehat\beta_2 \overbrace{sing}^{=0}+\widehat\gamma_1 \overbrace{malmar}^{=1}+\widehat\gamma_2 \overbrace{malsing}^{=0}=\widehat\alpha_0+\widehat\alpha_1+\widehat\beta_1+\widehat\gamma_1}\\ \overset{\Large\textit{single male}}{\widehat\alpha_0+\widehat\alpha_1 \overbrace{mal}^{=1}+\widehat\beta_1 \overbrace{mar}^{=0}+\widehat\beta_2 \overbrace{sing}^{=1}+\widehat\gamma_1 \overbrace{malmar}^{=0}+\widehat\gamma_2 \overbrace{malsing}^{=1}=\widehat\alpha_0+\widehat\alpha_1+\widehat\beta_2+\widehat\gamma_2} \end{cases} \end{equation*}\]

  • \(\widehat\alpha_0+\widehat\alpha_1\): divorced male average.
  • \(\widehat\beta_1+\widehat\gamma_1\): married male average - divorced male average.
  • \(\widehat\beta_2+\widehat\gamma_2\): single male average - divorced male average.

Now let us verify these coefficients by comparing those averages from the data.

# compute the means 
FFF <- tapply(dat_R$cons, list(dat_R$sex, dat_R$status), mean)

# convert the result into a long format
FFF <- as.data.frame.table(FFF)

# tidy the table
FFF$terms <- with(FFF, paste(Var1, Var2, sep = '-'))
FFF <- FFF[,c("terms", "Freq")]
names(FFF)[2]="means"
FFF
terms means
1 female-sing 3.06
2 male-sing 1.35
3 female-mar 5.01
5 female-div 4.31
4 male-mar 4.81
6 male-div 5.13
DDD <- tidy(mod_R_2fac)
DDD <- DDD[,c("term", "estimate")]
# concatenate the two tables
GGG <- tibble(FFF, DDD)

# reorder the columns
GGG <- GGG[, c("terms", "term", "estimate", "means")] 
GGG
terms term estimate means
female-sing (Intercept) 3.06 3.06
male-sing sexmale -1.71 1.35
female-mar statusmar 1.95 5.01
female-div statusdiv 1.24 4.31
male-mar sexmale:statusmar 1.51 4.81
male-div sexmale:statusdiv 2.54 5.13

Using that table, we can figure out how the coefficients have been computed:

  • \(\widehat\alpha_0=\) divorced female average = \(1.81\).
  • \(\widehat\beta_1=\) married female average - divorced female average=\(5.01-1.81=3.2\).
  • \(\widehat\beta_2=\) single female average - divorced female average=\(5.56-1.81=3.75\).
  • \(\widehat\alpha_1=\) divorced male average \(-\widehat\alpha_0=2.63-1.81=0.82\).
  • \(\widehat\gamma_1=\) (married male average - divorced male average) \(-\widehat\beta_1=(4.81-2.63)-3.2=-1.02\).
  • \(\widehat\gamma_2=\) (single male average - divorced male average) \(-\widehat\beta_2=(3.85-2.63)-3.75=-2.53\).

That said, let us now check the significance of those factors and their interaction. By using the lm function, we get:

term estimate std.error statistic p.value
(Intercept) 3.06 0.69 4.43 0.00
sexmale -1.71 0.90 -1.89 0.07
statusmar 1.95 0.90 2.16 0.04
statusdiv 1.24 1.29 0.96 0.34
sexmale:statusmar 1.51 1.32 1.14 0.26
sexmale:statusdiv 2.54 1.58 1.61 0.12

Using the level of 0.05, the interaction effect is not significant since the Intercept (p-value=0.11), the sexmale:statusmar (p-value=0.53), and the sexmale:statussing (p-value=0.12) are all not significant. The factor sex is also non-significant because, in addition to the non-significance intercept, the sexmale level (p-value=0.53) is not significant. However, the only significant regressor is the factor status.

When, however, we use the ANOVA table the interpretation is straightforward as follows.

mod_R_anova <- aov(cons~sex*status, data=dat_R)
tidy(mod_R_anova)
term df sumsq meansq statistic p.value
sex 1 4.96 4.96 2.08 0.16
status 2 56.56 28.28 11.86 0.00
sex:status 2 6.91 3.46 1.45 0.25
Residuals 24 57.21 2.38 NA NA

As we can see, by looking at the p.value, we can conclude that the only significant effect comes from the categorical variable status without any interaction with the variable sex.

To correct our model, we should remove the non-significant term from the model one by one, but should start first with the interaction term then check again until we left out with only significant ones.

mod_R_2 <- lm(cons~sex+status, data=dat_R)
tidy(mod_R_2)
term estimate std.error statistic p.value
(Intercept) 2.43 0.57 4.24 0.00
sexmale -0.63 0.60 -1.05 0.31
statusmar 2.74 0.67 4.09 0.00
statusdiv 2.92 0.75 3.88 0.00

Even though the sexmale term is non-significant, we cannot remove the sex variable since the intercept is significant so that we we do not know, from this output, if that significance comes from status or sex or from both. That is why whe need the ANOVA table to clear up this confusion.

mod_R_anova2 <- aov(cons~sex+status, data=dat_R)
tidy(mod_R_anova2)
term df sumsq meansq statistic p.value
sex 1 4.96 4.96 2.01 0.17
status 2 56.56 28.28 11.47 0.00
Residuals 26 64.13 2.47 NA NA

It is clear now that the variable sex is also non-significant and should be removed.

In Python:

In python, after retrieving the data from R, we can visualize the box plots using seaborn package.

copy the data into python.

if not 'data_py' in globals():
  data_py = r.dat_R
import matplotlib.pyplot as plt
import seaborn as sns

# create placeholder for the plots
fig = plt.figure(figsize=(15,15))
g = plt.GridSpec(2,2, wspace=0.4, hspace=0.4)
ax1 = fig.add_subplot(g[0,0])
ax2 = fig.add_subplot(g[0,1])
ax3 = fig.add_subplot(g[1,0:])

# boxplot for status
sns.boxplot(x='status', y= 'cons', data=data_py, ax=ax1)

# boxplot fro sex
sns.boxplot(x='sex', y= 'cons', data=data_py, ax=ax2)

# interaction
sns.boxplot(x='status', y= 'cons', hue='sex', data=data_py, ax=ax3)
plt.show()

The following code fragment is for the estimation.

mod_py_2factor = smf.ols('cons~sex*status', data=data_py).fit()
result2_py= mod_py_2factor.summary2().tables[1]
result2_py
Table 3.16: the estimation in Python
Coef. Std.Err. t P>|t| [0.025 ]
Intercept 1.81 1.09 1.65 0.11 -0.45 4.06
sex[T.male] 0.83 1.29 0.64 0.53 -1.84 3.49
status[T.mar] 3.20 1.24 2.59 0.02 0.65 5.76
status[T.sing] 3.76 1.29 2.91 0.01 1.09 6.42
sex[T.male]:status[T.mar] -1.03 1.61 -0.64 0.53 -4.36 2.31
sex[T.male]:status[T.sing] -2.54 1.58 -1.61 0.12 -5.79 0.72

And the ANOVA table can be retrieved by the following:

anova_py = sm.stats.anova_lm(mod_py_2factor, type=2)
anova_py
Table 3.17: the ANOVA in Python
df sum_sq mean_sq F PR(>F)
sex 1 8.05 8.05 3.38 0.08
status 2 24.97 12.48 5.24 0.01
sex:status 2 6.91 3.46 1.45 0.25
Residual 24 57.21 2.38 NaN NaN

3.10.3 Regression with mix of categorical and continuous variables

This analysis called ANCOVA in the econometric jargon. We will use again the above example with the variable income added as continuous independent variable.

set.seed(1133)
arguments <- list(
  formula = cons ~ 1 + income * sex,
  fixed = list(
    income = list(
      var_type = 'continuous',
      mean = 150,
      sd = 5
    ),
    sex = list(var_type = 'factor', levels = c('female', 'male'))
  ),
  error = list(variance = 4),
  sample_size = 30,
  reg_weights = c(8, 0.65,-0.7, 0.4)
)

dat2_R <- simulate_fixed(data = NULL, arguments) %>% 
  simulate_error(arguments) %>% 
  generate_response(arguments)

# keep only the the dependent variable and the dummy variables
dat2_R <- dat2_R[,c("cons",  "income", "sex")]
head(dat2_R)
cons income sex
108.5085 156.617 female
108.2729 155.336 female
162.1956 147.515 male
157.9419 146.278 male
104.2982 150.930 female
169.2698 155.270 male

Now let us figure out how these coefficient have been computed.

head(model.matrix(~income*sex, dat2_R))
(Intercept) income sexmale income:sexmale
1 156.617 0 0.000
1 155.336 0 0.000
1 147.515 1 147.515
1 146.278 1 146.278
1 150.930 0 0.000
1 155.270 1 155.270

The model creates three regressors from the two original ones. While we know how is sexmale generated from the earlier discussion, we do not yet know for the third variable income:sexmale. It turns out that income:sexmale gives two slopes, each for each level of the factor sex. The following formula should clear up what is going on.

\[\begin{equation*} \widehat {cons}=\begin{cases} \overset{\Large\textit{Regression equation for females}}{\widehat\alpha_0+\widehat\alpha_1 inc+\widehat\beta \overbrace{mal}^{=0}+\widehat\gamma \overbrace{incmale}^{=0}=\widehat\alpha_0+\widehat\alpha_1 inc}\\ \overset{\Large\textit{Regression equation for males}}{\widehat\alpha_0+\widehat\alpha_1 inc+\widehat\beta \overbrace{mal}^{=1}+\widehat\gamma \overbrace{incmale}^{=inc}=(\widehat\alpha_0+\widehat\beta)+(\widehat\alpha_1+\widehat\gamma) inc}\\ \end{cases} \end{equation*}\]

Now the interpretation is straightforward. The model gives two fitted lines, one for each factor level, and their relationship determines the significance of the different terms. That yields six possible situations where the two trivial ones are (assuming that all the remaining coefficients are significant):

  1. \(\widehat\beta\approx0\) and \(\widehat\gamma\approx0\): The variable sex does not affect at all the response variable.
  2. \(\widehat\alpha_1\approx0\) and \(\widehat\gamma\approx0\): The variable income has no effect on the response variable.

All the other situations englobe the significance of each level and the interaction term. For instance, if only \(\widehat\gamma\approx0\), we do not have the interaction term between the two regressors. In other words, the effect of income on the response is the same whether the associated individual is male or female.

mod_R_ancova <- lm(cons~income*sex, data = dat2_R)
tidy(mod_R_ancova)
term estimate std.error statistic p.value
(Intercept) -10.446 15.027 -0.695 0.493
income 0.769 0.098 7.833 0.000
sexmale 11.279 19.281 0.585 0.564
income:sexmale 0.321 0.127 2.525 0.018

Since \(\widehat\alpha_0\) and \(\widehat\beta\) are non-significant, we have two lines without intercept but with different slopes, the slope for females is \(\widehat\alpha_1=0.769\), whereas for males is \(\widehat\alpha_1+\widehat\gamma=0.769+0.321=1.09\).

The above table displays the effect size of each term. If we want to check only the significance of the regressors included in the model, we should use the function aov to display the ANCOVA table that shows the significance of each variable with their interaction as well.

mod_R_aov <- aov(cons~income*sex, data = dat2_R)
tidy(mod_R_aov)
term df sumsq meansq statistic p.value
income 1 305.960 305.960 83.989 0.000
sex 1 24607.138 24607.138 6754.870 0.000
income:sex 1 23.226 23.226 6.376 0.018
Residuals 26 94.715 3.643 NA NA

Now it is clear that the two variables with their interaction are significant. Lastly, we can visualize these two fitted lines as follows:

ggplot(dat2_R, aes(x=income, y=cons, color=sex))+  
  geom_point()+  
  geom_smooth(method='lm', se=FALSE)+  
  ylab('consumption')+  
  xlab('income')+  
  theme_classic()  

In Python:

First, we have to send a copy of the data to Python workspace.

if not 'dat2_py' in globals():
  dat2_py = r.dat2_R

Then we can fit the model.

import statsmodels.formula.api as smf

# fit the model
mod_py_ancova= smf.ols('cons~inc*sex', data=dat2_py).fit()
mod_result =mod_py_ancova.summary2().tables[1] 
mod_result
Table 3.18: The estimates in Python
Coef. Std.Err. t P>|t| [0.025 ]
Intercept -10.446 15.027 -0.695 0.493 -41.334 20.443
sex[T.male] 11.279 19.281 0.585 0.564 -28.352 50.911
inc 0.769 0.098 7.833 0.000 0.567 0.971
inc:sex[T.male] 0.321 0.127 2.525 0.018 0.060 0.582

Then, we can retrieve the ANCOVA table with the following script.

import statsmodels.api as sm
ancova_py = sm.stats.anova_lm(mod_py_ancova, type=2)
ancova_py
Table 3.19: the ANCOVA in Python
df sum_sq mean_sq F PR(>F)
sex 1 24048.60 24048.60 6601.55 0.00
inc 1 864.50 864.50 237.31 0.00
inc:sex 1 23.23 23.23 6.38 0.02
Residual 26 94.71 3.64 NaN NaN

  1. we will discuss the heteroskedasticity assumption in later chapters↩︎