Regression with Categorical Variables in R: Dummy Coding

Put factors into a linear model — how R dummy-codes a categorical predictor, how to read the coefficients against the reference level, change the baseline, and handle factors with more than two levels

Biostatistics

Use categorical (factor) predictors in linear regression in R. See how R automatically dummy-codes a factor, interpret each coefficient as a difference from the reference level, change the baseline with relevel(), handle factors with more than two levels, and get the overall factor test with car::Anova().

Published

June 23, 2026

Modified

July 7, 2026

TipKey takeaways
  • A categorical predictor (factor) goes straight into lm() — R dummy-codes it automatically into 0/1 indicator variables.
  • A factor with two levels adds one coefficient = the difference between the two groups; a factor with n levels adds n − 1 coefficients, each a difference from the reference level.
  • The reference (baseline) level is the first factor level; change it with relevel() — it changes the interpretation, not the model fit.
  • Read a coefficient like sexMale as “the average difference from the reference (Female), holding other predictors constant.”
  • For the overall test of a multi-level factor, use car::Anova(model) — one p-value for the whole factor, not per-level.

Introduction

Regression needs numbers, but real data is full of categories — sex, treatment group, department, rank. A categorical predictor (factor) can go directly into a linear model: R recodes it into dummy (indicator) variables behind the scenes, and the coefficients become group differences.

The scenario: a college tracks faculty salaries and wants to know whether they differ by sex, and how salary varies across professor rank — controlling for experience and discipline. That’s a regression with categorical predictors.

The data: academic salaries

We use the Salaries dataset (from carData, installed with the car package) — 2008–09 nine-month salaries for college faculty, with sex, rank, discipline and yrs.service:

data("Salaries", package = "carData")
head(Salaries, 4)
      rank discipline yrs.since.phd yrs.service  sex salary
1     Prof          B            19          18 Male 139750
2     Prof          B            20          16 Male 173200
3 AsstProf          B             4           3 Male  79750
4     Prof          B            45          39 Male 115000

A two-level factor: sex

Put the factor straight into lm(). R creates a dummy variable sexMale (1 = Male, 0 = Female) and the coefficient is the salary difference between the groups:

data("Salaries", package = "carData")

model <- lm(salary ~ sex, data = Salaries)
summary(model)$coef
             Estimate Std. Error   t value     Pr(>|t|)
(Intercept) 101002.41   4809.386 21.001103 2.683482e-66
sexMale      14088.01   5064.579  2.781674 5.667107e-03

Interpret it against the reference level (Female, the first level):

  • (Intercept) = 101,002 — the average salary for the reference group (Female).
  • sexMale = 14,088 — males earn on average $14,088 more; so the male average is 101,002 + 14,088 = 115,090. The small p-value says this raw difference is significant.

contrasts() shows exactly how R coded it:

data("Salaries", package = "carData")
contrasts(Salaries$sex)
       Male
Female    0
Male      1

Male is coded 1, Female 0 — Female is the baseline. The choice is arbitrary and doesn’t change the fit, only which group the coefficients are measured against.

Change the reference level with relevel()

Use relevel() to make Male the baseline instead. The numbers mirror: now the intercept is the male average and the coefficient is negative:

data("Salaries", package = "carData")

Salaries$sex <- relevel(Salaries$sex, ref = "Male")
model <- lm(salary ~ sex, data = Salaries)
summary(model)$coef
             Estimate Std. Error   t value      Pr(>|t|)
(Intercept) 115090.42   1587.378 72.503463 2.459122e-230
sexFemale   -14088.01   5064.579 -2.781674  5.667107e-03

Now (Intercept) = 115,090 (male average) and sexFemale = −14,088 — being Female is associated with a $14,088 decrease relative to Males. Same model, same fit; only the interpretation flipped.

A factor with more than two levels: rank

A factor with n levels becomes n − 1 dummy variables. rank has three levels (AsstProf, AssocProf, Prof), so it adds two coefficients — each a contrast with the reference (AsstProf). Look at the groups first:

library(ggpubr)

data("Salaries", package = "carData")

ggboxplot(Salaries, x = "rank", y = "salary",
          color = "rank", palette = "jco",
          add = "jitter", add.params = list(size = 0.6),
          xlab = "Rank", ylab = "Salary", legend = "none")

A boxplot of academic salary by professor rank with jittered points, showing salary increasing from assistant to associate to full professor.

R’s model.matrix() shows the dummy coding it builds for rank:

data("Salaries", package = "carData")

res <- model.matrix(~ rank, data = Salaries)
head(res[, -1])     # the two dummy columns (AsstProf is the baseline = both 0)
  rankAssocProf rankProf
1             0        1
2             0        1
3             0        0
4             0        1
5             0        1
6             1        0
Note

R uses treatment contrasts by default (each level vs a reference) — the most natural coding for most analyses, and what every interpretation here assumes. Other schemes exist (e.g. effect/sum coding, contr.sum, which compares each level to the grand mean using a −1/+1 pattern); switch with options(contrasts = ...) or contrasts()<- only when a specific analysis calls for it.

Test the whole factor and adjust for other variables

A multi-level factor’s per-level p-values don’t tell you whether the factor as a whole matters. Fit the full model and use car::Anova() for one p-value per predictor (it handles unbalanced designs correctly):

library(car)

data("Salaries", package = "carData")
model2 <- lm(salary ~ yrs.service + rank + discipline + sex, data = Salaries)
Anova(model2)
Anova Table (Type II tests)

Response: salary
                Sum Sq  Df  F value    Pr(>F)    
yrs.service 3.2448e+08   1   0.6324    0.4270    
rank        1.0288e+11   2 100.2572 < 2.2e-16 ***
discipline  1.7373e+10   1  33.8582 1.235e-08 ***
sex         7.7669e+08   1   1.5137    0.2193    
Residuals   2.0062e+11 391                       
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Adjusting for the others, rank and discipline are highly significant, while sex is no longer significant (p = 0.22) — the raw sex gap is largely explained by rank, discipline and experience. To read the individual contrasts, look at the coefficients:

data("Salaries", package = "carData")
model2 <- lm(salary ~ yrs.service + rank + discipline + sex, data = Salaries)
round(summary(model2)$coef, 2)
              Estimate Std. Error t value Pr(>|t|)
(Intercept)   68351.67    4482.20   15.25     0.00
yrs.service     -88.78     111.64   -0.80     0.43
rankAssocProf 14560.40    4098.32    3.55     0.00
rankProf      49159.64    3834.49   12.82     0.00
disciplineB   13473.38    2315.50    5.82     0.00
sexMale        4771.25    3878.00    1.23     0.22

For example, disciplineB = 13,473 — applied departments (B) pay on average $13,473 more than theoretical departments (A), holding rank, experience and sex constant.

Note

ANOVA is regression with categorical predictors. A one-way ANOVA is just lm(y ~ factor); car::Anova() extracts the classic ANOVA table from any linear model. See one-way ANOVA for the test-focused view of the same machinery.

Report

A linear regression predicted salary from years of service, rank, discipline and sex. Adjusting for the other variables, rank (F(2, 391) = 100, p < 0.001) and discipline (F(1, 391) = 34, p < 0.001) were significant predictors, whereas sex was not (F(1, 391) = 1.5, p = 0.22). Faculty in applied disciplines earned on average $13,473 more than those in theoretical disciplines.

A factor with levels \(L_0, L_1, \dots, L_{k}\) is encoded as \(k\) indicator variables \(D_1, \dots, D_k\), where \(D_j = 1\) when the observation is in level \(L_j\) and 0 otherwise; the reference level \(L_0\) has all \(D_j = 0\). The model \(y = \beta_0 + \beta_1 D_1 + \dots + \beta_k D_k\) then makes \(\beta_0\) the mean of the reference group and each \(\beta_j\) the difference between level \(L_j\) and the reference — which is exactly why “treatment” (default) contrasts read as group comparisons.

Try it live

Change the reference level of rank to "Prof" and refit — how do the coefficients change? The sandbox boots on first Run.

🟢 With an AI agent

Ask Prova “add a categorical predictor to my regression and interpret the coefficients against the reference level” — it answers with code you can run on your own factors and explains each contrast. The runtime is the judge. Ask Prova →

Common issues

You interpreted a dummy coefficient as an absolute mean. Every factor coefficient is a difference from the reference level, not a group mean. The group mean is intercept + that coefficient (with all other predictors at their reference / zero).

You read per-level p-values as the factor’s significance. For a factor with 3+ levels, the individual summary() p-values test single contrasts; for the overall “does this factor matter” question use car::Anova(model).

Your “categorical” variable is actually numeric codes. If a factor is stored as 1/2/3, R treats it as a number (a linear trend), not categories. Wrap it in factor() so it gets dummy-coded.

Frequently asked questions

R dummy-codes them automatically: a factor with n levels becomes n − 1 indicator (0/1) variables, with the first level as the reference. You just put the factor in the formula (lm(y ~ group)); each coefficient is the difference between that level and the reference.

Each coefficient is the average difference in the outcome between that level and the reference level, holding other predictors constant. The intercept is the reference group’s predicted value. So sexMale = 14088 means males average $14,088 more than the reference (Female).

Use relevel(factor, ref = "level") before fitting, or set the factor levels explicitly with factor(x, levels = ...). It changes which group the coefficients are compared against — the interpretation — but not the model’s fit or predictions.

Use car::Anova(model), which gives one p-value per predictor (the whole factor), not per dummy level. It uses Type-II sums of squares by default, which handles unbalanced designs correctly — preferable to the base anova() for observational data.

Test your understanding

  1. Run it. In the live cell, relevel rank to "Prof" and refit salary ~ rank. What does the rankAsstProf coefficient now mean?
  2. Conceptual. In the full model, sex is non-significant, but a simple lm(salary ~ sex) is highly significant. Which result better describes whether sex causes a pay gap, and why?

Fill the blank with "Prof". With Prof as the baseline, the other coefficients are differences from full professors (so they’ll be negative). For question 2, think about what the full model adjusts for.

With rank releveled to "Prof", rankAsstProf is the average salary difference of assistant professors relative to full professors (a large negative number). For question 2: the full model is more informative about a causal pay gap. The raw lm(salary ~ sex) mixes sex with rank, discipline and experience; once you adjust for those, the direct sex effect is not significant — the raw gap is largely explained by men holding higher ranks and longer service, not by sex per se.

TipWhich model when?

Conclusion

You can now use categorical variables in regression in R: drop a factor into lm() and let R dummy-code it, interpret each coefficient as a difference from the reference level, switch the baseline with relevel(), handle factors with more than two levels, and get the factor’s overall test with car::Anova(). Categories and numbers live happily in the same model.

Reuse

Citation

BibTeX citation:
@online{2026,
  author = {},
  title = {Regression with {Categorical} {Variables} in {R:} {Dummy}
    {Coding}},
  date = {2026-06-23},
  url = {https://www.datanovia.com/learn/biostatistics/regression/regression-with-categorical-variables-in-r},
  langid = {en}
}
For attribution, please cite this work as:
“Regression with Categorical Variables in R: Dummy Coding.” 2026. June 23. https://www.datanovia.com/learn/biostatistics/regression/regression-with-categorical-variables-in-r.