How to correctly interpret your continuous and categorical variable interactions using R | by Jin Hyun Cheong, PhD | Towards Data Science

The ability to understand and interpret the results of regressions is fundamental for effective data analytics. Understanding how each term was represented in the model specification is critical to accurately interpret the results of the model. Often however, it is tempting to jump to conclusions by looking at the t-statistics or p-values and assume the model did what you wanted it to do without really understanding what happens under the hood. One mistake I often observed from teaching stats to undergraduates was how the main effect of a continuous variable was interpreted when an interaction term with a categorical variable was included.
Here I provide some R code to demonstrate why you cannot simply interpret the coefficient as the main effect unless you’ve specified a contrast.
To illustrate, I am going to create a fake dataset with variables Income, Age, and Gender. My specification is that for Males, Income and Age have a correlation of r = .80, while for Females, Income and Age have a correlation of r = .30.
From this specification, the average effect of Age on Income, controlling for Gender should be .55 (= (.80 + .30) / 2 ). As for average group differences, let’s say Males earn on average $2, while Females earn on average $3.
Data for each gender is generated separately then concatenated to create a combined data frame: data. Data is generated in R using mvrnorm from package MASS:
This code snippet also checks if the randomly generated data has the correlation and average we specified.
## Correlation between Income & Age for Male: 0.8 ## Correlation between Income & Age for Female: 0.3 ## Mean Income for Male: 2 ## Mean Income for Female: 3
Now that we have our sample data, let’s see what happens when we naively run a linear model predicting Income from Age, Gender, and their interaction.
## ## Call: ## lm(formula = "Income~Age*Gender", data = data) ## ## Residuals: ##     Min      1Q  Median      3Q     Max ## -3.4916 -0.4905 -0.0051  0.5044  3.2038 ## ## Coefficients: ##                Estimate Std. Error t value Pr(>|t|)     ## (Intercept)     3.00000    0.02521  118.99   <2e-16 *** ## Age             0.30000    0.02522   11.89   <2e-16 *** ## GenderMale     -1.00000    0.03565  -28.05   <2e-16 *** ## Age:GenderMale  0.50000    0.03567   14.02   <2e-16 *** ## --- ## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1 ## ## Residual standard error: 0.7973 on 1996 degrees of freedom ## Multiple R-squared:  0.4921, Adjusted R-squared:  0.4913 ## F-statistic: 644.6 on 3 and 1996 DF,  p-value: < 2.2e-16
The model summary above prints coefficients for the Intercept, Age, GenderMale, Age:GenderMale. We can clearly see that the effect of Age is .30 which is certainly NOT the average effect controlling for gender but simply the effect for the Female group. The effect of GenderMale is $-1 which is how much the Male group earn less than Female group which is the Intercept at $3. Lastly, the interaction Age:GenderMale represents how much more Income correlates with Age for Male than Female (0.5 = 0.8-0.3).
Importantly, this is the default R behavior with categorical variables that it *alphabetically sets the first variable as the reference level (i.e., the intercept). So in our case Female has been set as our reference level. Then, our categorical variables are dummy coded (a.k.a., treatment contrast) so that Females are 0's, and Males are 1's, which can be verified by using the function contrasts.
contrasts(data$Gender)##        Male ## Female    0 ## Male      1
So, what do we need to do to get the AVERAGE effect of Age on Income controlling for Gender while keeping the interaction? The answer is: specify a contrast centered at 0 so that Females are coded as -.50 and males coded as .50. Let’s see what happens when we specify that contrast and re-run our model.
## ## Call: ## lm(formula = "Income~Age*Gender", data = data) ## ## Residuals: ##     Min      1Q  Median      3Q     Max ## -3.4916 -0.4905 -0.0051  0.5044  3.2038 ## ## Coefficients: ##             Estimate Std. Error t value Pr(>|t|)     ## (Intercept)  2.50000    0.01783  140.23   <2e-16 *** ## Age          0.55000    0.01784   30.84   <2e-16 *** ## Gender1     -1.00000    0.03565  -28.05   <2e-16 *** ## Age:Gender1  0.50000    0.03567   14.02   <2e-16 *** ## --- ## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1 ## ## Residual standard error: 0.7973 on 1996 degrees of freedom ## Multiple R-squared:  0.4921, Adjusted R-squared:  0.4913 ## F-statistic: 644.6 on 3 and 1996 DF,  p-value: < 2.2e-16
We get four terms again but they are specified as Intercept, Age, Gender1, and Age:Gender1. The Age effect is 0.55 which is exactly the average effect across gender as we specified when we generated our data ( 0.55=(0.8+0.3) / 2). The Age:Gender1 interaction is 0.5 which is the difference between the age effects between gender (0.5 =0.8–0.3). Effect of Gender1 is $-1 which represents the average difference between the two genders ($2-$3), as specified by our contrast. The reference Intercept is $2.5 which is the average income across gender ( ($2+$3) / 2 ).
Once again, we can verify what our contrast was with the following:
contrasts(data$Gender)##        [,1]## Female -0.5## Male    0.5
I hope this example makes it clear that when you build linear models with interactions between continuous and categorical variables, you need to be careful in how they are specified (dummy coded or contrasts) as this will change how you interpret the coefficients.
Most importantly, you should only interpret the coefficient of a continuous variable interacting with a categorical variable as the average main effect when you have specified your categorical variables to be a contrast centered at 0. You cannot interpret it as the main effect if the categorical variables are dummy coded as they become the estimate of the effect at the reference level.
For more information, checkout additional answers to this question which has been asked multiple times online at stackexchange and at r-bloggers.
If you want your categorical variables to be treated as dummy codes, you can set it as a treatment contrast. Which replicate the default result provided by R.
## ## Call: ## lm(formula = "Income~Age*Gender", data = data) ## ## Residuals: ##     Min      1Q  Median      3Q     Max ## -3.4916 -0.4905 -0.0051  0.5044  3.2038 ## ## Coefficients: ##             Estimate Std. Error t value Pr(>|t|)     ## (Intercept)  3.00000    0.02521  118.99   <2e-16 *** ## Age          0.30000    0.02522   11.89   <2e-16 *** ## Gender2     -1.00000    0.03565  -28.05   <2e-16 *** ## Age:Gender2  0.50000    0.03567   14.02   <2e-16 *** ## --- ## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1 ## ## Residual standard error: 0.7973 on 1996 degrees of freedom ## Multiple R-squared:  0.4921, Adjusted R-squared:  0.4913 ## F-statistic: 644.6 on 3 and 1996 DF,  p-value: < 2.2e-16
If you run the model without the interaction, then even if your categorical variables are dummy coded, the main effect of Age is the average effect controlling for Gender as you would expect.
## ## Call: ## lm(formula = "Income~Age+Gender", data = data) ## ## Residuals: ##     Min      1Q  Median      3Q     Max ## -3.5525 -0.5102 -0.0055  0.5424  3.2461 ## ## Coefficients: ##             Estimate Std. Error t value Pr(>|t|)     ## (Intercept)  3.00000    0.02642  113.56   <2e-16 *** ## Age          0.55000    0.01869   29.43   <2e-16 *** ## Gender2     -1.00000    0.03736  -26.77   <2e-16 *** ## --- ## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1 ## ## Residual standard error: 0.8354 on 1997 degrees of freedom ## Multiple R-squared:  0.4421, Adjusted R-squared:  0.4416 ## F-statistic: 791.3 on 2 and 1997 DF,  p-value: < 2.2e-16
Thank you for reading and feel free to check out my other posts related to data science.