13  Chapter 13 - Categorical predictors and non-linear effects in Regression Models

In this chapter we cover three extensions of the multiple regression model, all of which use the same linear machinery of Chapter 12 but let it represent much richer relationships:

The unifying idea is worth stating up front. “Linear regression” means linear in the coefficients, not linear in the variables. As long as the model is a weighted sum of terms, we are free to make those terms anything we like — a dummy variable, a product of two variables, a squared variable. This one observation extends the reach of the linear model enormously.

13.1 Introduction

So far we have included only continuous independent variables in our regression models. A categorical variable is usually non-numeric and represents groups into which observations can be classified. Before including one in a regression, we must code it in a special way.

A categorical variable typically has no meaningful numeric ranking, but is useful for classification. For a sample of companies, industry is categorical: we cannot sum or average industries, only count how many firms fall into each.

Some numeric variables can also be treated as categorical. Year, for example, can enter a regression either as a number (capturing a linear trend) or as a set of categories (capturing a separate effect for each year, of any shape).

When we want to evaluate the effect of one variable on a dependent variable, it is good practice to include control variables. A control variable is not the focus of the study, but prior research or reasoning suggests it is related to the dependent variable. Categorical variables are often included as controls.

If we regress Y on an explanatory variable of interest plus one or two controls, and the explanatory variable remains significant, we can say the effect holds even after accounting for the controls. This makes the result substantially more credible. As we will see in this chapter’s example, adding the right control variable can change a coefficient dramatically — which is precisely why omitting it would have been a mistake.

13.2 Coding categorical variables

Imagine a dataset in which each observation has several numeric features of a company plus one categorical variable classifying firms into two groups: manufacturing and non-manufacturing. We cannot use the text label as an X variable. We must create a dummy variable — a variable taking only the values 0 and 1 — assigning 1 to manufacturing firms and 0 to the rest.

13.2.1 Dummy coding versus one-hot coding

With more than two categories, there are two ways to proceed, and the difference matters.

One-hot encoding creates one binary column per category. For a variable with three industries (Retail, Manufacturing, Services) it creates three columns:

Firm Retail Manufacturing Services
A 1 0 0
B 0 1 0
C 0 0 1

Dummy encoding drops one column — the reference or base category:

Firm Manufacturing Services
A (Retail) 0 0
B 1 0
C 0 1
ImportantWhy one-hot encoding breaks a regression: the dummy variable trap

Look at the one-hot table again. In every row, the three columns sum to exactly 1:

\text{Retail} + \text{Manufacturing} + \text{Services} = 1 \quad \text{for every observation}

But the regression model already contains an intercept, which is a column of 1s. So one of the dummy columns is an exact linear combination of the others and the intercept. This is perfect multicollinearity, and it violates assumption 7 from Chapter 9.

Why is that fatal? Because the OLS solution requires inverting the matrix (X'X), and perfect collinearity makes that matrix singular — it has no inverse. Geometrically, there is no longer a unique set of coefficients that minimizes the sum of squared errors: infinitely many combinations give exactly the same fit. The optimization problem has no single answer, so the software either returns an error or silently drops a column.

The fix is simply to drop one category. The dropped category becomes the reference, and every remaining coefficient is interpreted relative to it. No information is lost — with k categories, k-1 dummies fully determine which group an observation belongs to.

Note that one-hot encoding is perfectly fine for machine learning models that do not invert a matrix (decision trees, neural networks with regularization). It is specifically the combination of one-hot encoding and an intercept in a linear model that fails. This is why the two conventions coexist.

13.3 An example: study hours, teaching method and test grades

Our dataset d1 is a random sample of 60 students with three variables:

  • Y_grade — the grade obtained on a test
  • X1_hrs — the number of hours spent studying for the test
  • X2_method — whether the student followed a didactic method based on writing, or followed no particular method

Let’s download the data:

import pandas as pd
import numpy as np
import requests
import matplotlib.pyplot as plt
import statsmodels.formula.api as smf

headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
                         'AppleWebKit/537.36 (KHTML, like Gecko) '
                         'Chrome/91.0.4472.124 Safari/537.36'}

url1 = 'https://www.apradie.com/datos/ch11_d1.csv'
response1 = requests.get(url1, headers=headers)
with open('ch11_d1.csv', 'wb') as f:
    f.write(response1.content)

df = pd.read_csv('ch11_d1.csv')
print(df.head())
print(df.tail())
     Y_grade  X1_hrs       X2_method
0  49.353420      10  writing method
1  84.115315      26  writing method
2  79.857837      22  writing method
3  70.981086      20  writing method
4  45.973687      10  writing method
      Y_grade  X1_hrs  X2_method
55  93.255247      36  no method
56  73.744501      25  no method
57  61.859911      22  no method
58  62.120559      21  no method
59  37.892966      15  no method

Before modeling anything, let’s look at the data — the lesson of Anscombe’s quartet in Chapter 8.

plt.figure(figsize=(7, 4.5))
for label, marker in [("writing method", "o"), ("no method", "^")]:
    sub = df[df["X2_method"] == label]
    plt.scatter(sub["X1_hrs"], sub["Y_grade"], marker=marker, alpha=0.8, label=label)
plt.xlabel("Hours of study")
plt.ylabel("Test grade")
plt.title("Grade vs study hours by method")
plt.legend()
plt.tight_layout()
plt.show()
Figure 13.1: Test grade versus study hours, with students separated by teaching method.

The plot already reveals the structure: two roughly parallel bands of points. Both groups improve with study time at a similar rate, but one band sits consistently above the other. Keep that picture in mind — the models that follow are simply ways of describing it numerically.

Let’s also look at the group averages:

df.groupby("X2_method")[["Y_grade", "X1_hrs"]].mean().round(2)
Y_grade X1_hrs
X2_method
no method 76.78 29.10
writing method 83.44 23.27

Notice something surprising. Students who used the writing method obtained higher grades, and yet they studied fewer hours on average. This is exactly the situation in which omitting a variable will distort our conclusions.

13.3.1 Model 1: ignoring the categorical variable

First, the naive model that regresses grade on study hours alone:

m1 = smf.ols("Y_grade ~ X1_hrs", data=df).fit()
print(m1.summary().tables[1])
print(f"R-squared = {m1.rsquared:.4f}")
==============================================================================
                 coef    std err          t      P>|t|      [0.025      0.975]
------------------------------------------------------------------------------
Intercept     25.4894      4.470      5.703      0.000      16.542      34.437
X1_hrs         2.0862      0.162     12.904      0.000       1.763       2.410
==============================================================================
R-squared = 0.7417

Each additional hour of study is associated with about 2.09 additional grade points, and the model explains about 74% of the variance. That looks like a perfectly respectable result — and it is biased.

13.3.2 Model 2: adding the dummy variable

Now we code the categorical variable as a dummy, using “no method” as the reference category:

# 1 if the student used the writing method, 0 otherwise:
df["method"] = (df["X2_method"] == "writing method").astype(int)

m2 = smf.ols("Y_grade ~ X1_hrs + method", data=df).fit()
print(m2.summary().tables[1])
print(f"R-squared = {m2.rsquared:.4f}")
==============================================================================
                 coef    std err          t      P>|t|      [0.025      0.975]
------------------------------------------------------------------------------
Intercept      4.7044      2.224      2.115      0.039       0.250       9.158
X1_hrs         2.4770      0.071     34.993      0.000       2.335       2.619
method        21.1086      1.257     16.798      0.000      18.592      23.625
==============================================================================
R-squared = 0.9566

The change is dramatic. R^2 jumps from 0.74 to 0.96, and the coefficient on study hours rises from 2.09 to 2.48.

ImportantOmitted variable bias, seen directly

This is one of the clearest demonstrations of omitted variable bias you will encounter, so it is worth unpacking carefully.

In Model 1, the effect of the teaching method was not measured — so it went into the error term. But method is correlated with study hours (writing-method students studied fewer hours). That correlation violates the exogeneity assumption of Chapter 9, and the coefficient on X1_hrs absorbed part of the method effect with the wrong sign, dragging it down from 2.48 to 2.09.

The direction of the bias follows a simple rule:

\text{Bias} = \beta_{\text{omitted}} \times (\text{correlation between omitted variable and } X)

Here the method effect is strongly positive (+21) and its correlation with study hours is negative, so the product is negative — the estimate was biased downward. Model 1 understated the value of studying by about 16%.

The general lesson is the one that matters most in applied work: a coefficient is only interpretable relative to what else is in the model. Adding more data would not have fixed Model 1. Only adding the right variable does.

Now the interpretation of each coefficient in Model 2:

  • Intercept (b_0 \approx 4.70): the expected grade for a student who studies zero hours and uses no method (the reference category). Note this is an extrapolation outside the data range, so it should not be over-interpreted.

  • X1_hrs (b_1 \approx 2.48): each additional hour of study raises the expected grade by about 2.48 points, holding the method constant. This is now the effect of study hours purified of the method effect.

  • method (b_2 \approx 21.11): students using the writing method score about 21 points higher than students using no method, holding study hours constant. This is the key interpretive rule for a dummy coefficient: it is the difference in the intercept between the group and the reference group.

Both coefficients have t-statistics far above 2, so both effects are highly significant.

13.3.3 Writing the equation for each group

A regression with a dummy variable is really two parallel lines in one equation. Substituting the two possible values of the dummy:

For students with no method (method = 0):

\widehat{Grade} = 4.70 + 2.48 \times Hours + 21.11 \times 0 = 4.70 + 2.48 \times Hours

For students with the writing method (method = 1):

\widehat{Grade} = 4.70 + 2.48 \times Hours + 21.11 \times 1 = 25.81 + 2.48 \times Hours

The two lines have the same slope (2.48) and different intercepts (4.70 versus 25.81). This is the geometric meaning of a dummy variable: it shifts the line up or down without tilting it.

Let’s draw them over the data:

b0, b1, b2 = m2.params["Intercept"], m2.params["X1_hrs"], m2.params["method"]
grid = np.linspace(df["X1_hrs"].min(), df["X1_hrs"].max(), 50)

plt.figure(figsize=(7, 4.5))
for label, marker, d in [("writing method", "o", 1), ("no method", "^", 0)]:
    sub = df[df["method"] == d]
    plt.scatter(sub["X1_hrs"], sub["Y_grade"], marker=marker, alpha=0.7, label=label)
    plt.plot(grid, b0 + b1*grid + b2*d, lw=2)

plt.xlabel("Hours of study")
plt.ylabel("Test grade")
plt.title("Two parallel regression lines")
plt.legend()
plt.tight_layout()
plt.show()
Figure 13.2: The dummy variable model fits two parallel lines: same slope, different intercepts.

13.3.4 More than two categories

With k categories we create k-1 dummies. Suppose the method variable had three levels — writing, flashcards, and no method — with “no method” as the reference:

Grade = b_0 + b_1 Hours + b_2 D_{writing} + b_3 D_{flashcards} + \varepsilon

This produces three parallel lines. b_2 is the gap between writing and no method; b_3 is the gap between flashcards and no method; and the gap between writing and flashcards is b_2 - b_3. In pandas, pd.get_dummies(df["X2_method"], drop_first=True) performs exactly this coding — note the drop_first=True, which is what avoids the dummy variable trap.

13.4 Interaction effects

The model above forces both groups onto parallel lines: it allows the method to change the level of the grade, but not the return to studying. What if we believe the writing method also makes each hour of study more productive? That is a question about whether the two lines have different slopes, and it is answered with an interaction term.

An interaction is simply the product of two variables:

Grade = b_0 + b_1 Hours + b_2 Method + b_3 (Hours \times Method) + \varepsilon

# The * operator in a formula creates both main effects and the interaction:
m3 = smf.ols("Y_grade ~ X1_hrs * method", data=df).fit()
print(m3.summary().tables[1])
print(f"R-squared = {m3.rsquared:.4f}")
=================================================================================
                    coef    std err          t      P>|t|      [0.025      0.975]
---------------------------------------------------------------------------------
Intercept         6.5532      3.371      1.944      0.057      -0.200      13.307
X1_hrs            2.4134      0.112     21.514      0.000       2.189       2.638
method           18.2684      4.080      4.478      0.000      10.095      26.441
X1_hrs:method     0.1061      0.145      0.732      0.467      -0.184       0.397
=================================================================================
R-squared = 0.9570

Again, write out the equation for each group:

For no method (method = 0), the interaction term vanishes:

\widehat{Grade} = b_0 + b_1 \times Hours

For the writing method (method = 1):

\widehat{Grade} = (b_0 + b_2) + (b_1 + b_3) \times Hours

So now both the intercept and the slope differ between groups:

  • b_2 is the difference in intercepts
  • b_3 is the difference in slopes — the extra grade points per study hour that the writing method delivers
NoteReading this particular result

The interaction coefficient b_3 is small and its p-value is well above 0.05, so we cannot reject the hypothesis that the two slopes are equal. Notice also that R^2 barely moved from Model 2.

The conclusion is that the writing method gives students a constant advantage of roughly 21 points, but does not change how much each additional hour of study is worth. Both groups gain about 2.4 points per hour.

This is a genuinely useful finding, and it matches the two parallel bands we saw in the very first scatter plot. It also illustrates good practice: prefer the simpler model when the added complexity is not statistically supported. Model 2 is the model to report here.

WarningTwo rules for interactions
  1. Always include the main effects. If you include Hours \times Method, you must also include Hours and Method separately. Omitting a main effect forces the model through arbitrary constraints and makes the interaction uninterpretable.

  2. The main effect changes meaning. Once an interaction is present, b_1 is no longer “the effect of hours” in general — it is the effect of hours when the other variable equals zero. This is why interaction models are often estimated with the continuous variables centred (each variable minus its mean), so that “zero” means “at the average” rather than an impossible value.

Interactions are how a linear model expresses the idea that “it depends”. Some business examples:

  • The effect of a price discount on sales depends on whether the product is advertised.
  • The effect of firm size on profitability depends on the industry.
  • The effect of debt on firm value depends on the volatility of cash flows — the inverted-U from Chapter 2.

13.5 Quadratic effects

Interactions let a slope depend on another variable. A quadratic term lets the slope depend on the variable itself — that is, it models a curve.

The trick is exactly as simple as it sounds: create a new variable equal to X squared and put it in the model.

Y = b_0 + b_1 X + b_2 X^2 + \varepsilon

This is still a linear regression, because it is linear in the coefficients b_0, b_1, b_2. We have not changed the estimation method at all; we have only added a column.

The slope of Y with respect to X is now the derivative:

\frac{\partial Y}{\partial X} = b_1 + 2 b_2 X

which depends on X. This gives the model its curvature:

  • If b_2 > 0, the curve is U-shaped (convex): the effect of X gets stronger as X grows.
  • If b_2 < 0, the curve is inverted-U (concave): the effect of X weakens and eventually turns negative.

Setting the derivative to zero gives the turning point, the value of X at which the effect changes direction:

X^{*} = -\frac{b_1}{2 b_2}

This turning point is often the most interesting number in the whole model — it is the optimal leverage ratio, the profit-maximizing price, the stress level beyond which performance declines.

Let’s test whether the effect of study hours is curved in our data. There is a plausible theory that it should be: at some point, additional hours produce diminishing returns from fatigue.

df["hrs2"] = df["X1_hrs"] ** 2

m4 = smf.ols("Y_grade ~ X1_hrs + hrs2 + method", data=df).fit()
print(m4.summary().tables[1])
print(f"R-squared = {m4.rsquared:.4f}")
==============================================================================
                 coef    std err          t      P>|t|      [0.025      0.975]
------------------------------------------------------------------------------
Intercept      3.9713      5.353      0.742      0.461      -6.752      14.695
X1_hrs         2.5419      0.437      5.823      0.000       1.667       3.416
hrs2          -0.0013      0.008     -0.151      0.881      -0.018       0.016
method        21.1316      1.277     16.552      0.000      18.574      23.689
==============================================================================
R-squared = 0.9566

The quadratic term is not statistically significant, so we have no evidence of diminishing returns within the observed range of 10 to 40 hours. The linear specification of Model 2 stands.

This is worth emphasizing as a methodological point: testing for a non-linear effect and not finding one is a real result. It licenses the simpler model. The mistake would have been to assume linearity without ever checking.

WarningCautions when using quadratic terms
  • Multicollinearity. X and X^2 are strongly correlated, which inflates standard errors. Centring X before squaring it (using (X - \bar{X}) and (X - \bar{X})^2) largely fixes this without changing the fit.

  • Never interpret b_1 and b_2 separately. Neither one is “the effect of X”. Only the combined derivative b_1 + 2b_2 X is meaningful. Reporting “the coefficient on hours is 1.30” from a quadratic model is meaningless without the quadratic term alongside it.

  • Check that the turning point falls inside your data. A model can report a turning point at 200 study hours when the data only ranges to 40. That turning point is an artifact of extrapolation, not a finding.

  • Quadratics extrapolate terribly. A parabola eventually shoots off to infinity in both directions. Never use a quadratic model to predict outside the range of the observed data.

13.6 Summary

All three techniques in this chapter extend the reach of the linear model without changing its machinery:

Technique New term What it lets the model do Interpretation
Dummy variable D (0/1) Shift the line up or down by group b = difference in intercepts
Interaction X \times D or X_1 \times X_2 Let the slope differ by group b = difference in slopes
Quadratic X^2 Let the slope change with X Turning point at -b_1 / 2b_2

The recurring theme is the one from the start of the chapter: linear regression requires linearity in the coefficients, not in the variables. Once you internalize that, an enormous class of relationships — group differences, conditional effects, diminishing returns, optimal points — becomes accessible with the tools you already have.

And the recurring caution is equally important: every one of these terms adds parameters, and every added parameter raises R^2 whether or not it captures anything real. Add them because theory suggests them, keep them because the data supports them, and always plot the result to check that it means what you think it means.