In the ANOVA and the GLM chapter we let R turn a factor into numbers for us and moved on. That was a simplification worth revisiting. R always codes a categorical predictor into numbers before it fits anything, and the code you choose decides what every coefficient means. Change the code and the intercept, the slopes, and the t-tests all change their meaning, even though the model’s fit (its \(R^2\), its \(F\), its predicted values) does not change at all.
That is the whole chapter in one idea. The coding scheme is a lens, not a model. Pick the lens that answers your question, and the coefficients read like plain English. Pick a careless lens and you get numbers that are technically correct but not meaningful, what we will call nonsense coding.
19.1 Learning Objectives
See that a categorical predictor is just a continuous variable we chopped into bins - and that “how we chop” is a choice.
Interpret dummy (reference), effects (deviation), weighted effects, and contrast codes - reading each coefficient straight off the group means.
Recover every coefficient by hand from the marginal means, so no lm() output is ever a mystery.
Understand why the omnibus fit is invariant to coding, and why bad codes give “nonsense” coefficients.
19.2 A Category Is a Binned Continuum
Let’s build a categorical variable from scratch so we can see the seams. Take a continuous “dogness” score, chop it into four bins, and call them cat, fish, pig, dog:
set.seed(725)n <-200x <-rnorm(n) # a continuous "dogness" scorepet <-cut(x, 4, labels =c("cat", "fish", "pig", "dog"))happy <- x +rnorm(n) # more dogness -> happier, on averaged <-data.frame(pet, happy)table(pet)
pet
cat fish pig dog
9 60 94 37
pet is now a factor: R stores it as integer codes with labels attached. The moment we put it in a model, R replaces those four labels with three columns of numbers (four groups need \(4-1=3\) columns, plus the intercept). Which three columns is the coding scheme. Here are the group means we will spend the rest of the chapter reconstructing:
Dummy coding - R’s default, also called reference or indicator coding - picks one group as the reference and compares every other group to it. Let’s make dog the reference (the 4th level):
(Intercept) is the reference-group mean - the mean happiness of dog.
each slope is that group’s mean minus the reference mean: pet1 is \(\bar{y}_{cat} - \bar{y}_{dog}\), pet2 is \(\bar{y}_{fish} - \bar{y}_{dog}\), and so on.
Don’t take my word for it - check:
mu <-setNames(means$happy, means$pet) # named group meansc(intercept_is_dog_mean =all.equal(unname(coef(m_dummy)[1]), unname(mu["dog"])),cat_slope_is_cat_minus_dog =all.equal(unname(coef(m_dummy)["pet1"]),unname(mu["cat"] - mu["dog"])))
Both TRUE. Every dummy coefficient is a simple difference from the reference. That is exactly why dummy coding is the right lens when you have a control group and want each treatment compared to it.
WarningThe interaction trap
Add a second predictor with an interaction and a dummy slope is no longer “group mean minus reference mean” overall - it is that difference only at the reference level of the other predictor (i.e., where the other predictor’s dummy is 0). This is the single most common misreading of regression output. When an interaction is in the model, stop reading coefficients off marginal means and solve the full equation instead. (This bites hardest in the factorial models of ANOVA & the GLM.)
19.4 Effects (Deviation) Coding
Dummy coding asks “how does each group differ from one group?” Effects coding (a.k.a. deviation coding, contr.sum) asks a different question: “how does each group differ from the grand mean?” The zero point moves from a reference group to the center of everything.
(Intercept) is the grand mean - but note which grand mean: the mean of the four group means, not the mean of the raw data (those differ when groups are unequal in size).
each slope is a group’s mean minus the grand mean - its “effect.”
the last group (dog) has no coefficient of its own. Because effects sum to zero, its effect is minus the sum of the others.
Watch the grand-mean subtlety, because it trips everyone:
GM_groups <-mean(means$happy) # mean of the GROUP meansGM_raw <-mean(d$happy) # mean of the raw datac(intercept =unname(coef(m_eff)[1]),grand_mean_of_groups = GM_groups, # this one matchesgrand_mean_of_raw = GM_raw) # this one (slightly) does not
TRUE. That is the whole trick of effects coding: the coefficients are deviations that must sum to zero, so the omitted group is always recoverable.
ImportantDummy vs. effects, in one line
Dummy codes make the zero explicit (it is the reference group); effects codes make the zero implicit (it is the grand mean, sitting between the deviations). That is the mnemonic to carry out of this chapter.
WarningAlways check your contrasts
contr.sum on a two-level factor assigns +1 to the first level and -1 to the second, which is often the opposite of what you expect, and it silently flips the sign of your effect. R’s default level ordering (alphabetical) can catch you the same way. Print contrasts(yourfactor)before you interpret anything.
19.5 Weighted Effects Coding
Plain effects coding centers on the mean of the group means, treating a group of 10 and a group of 200 as equals. When your cells are unbalanced and you want the population grand mean (weighted by how many people are actually in each group), you need weighted effects coding.
Let’s make an unbalanced world - many pigs, few dogs - and build the weighted codes by hand, pushing the cell-size ratios onto the comparison group’s row:
set.seed(9)cnt <-c(cat =20, fish =30, pig =40, dog =10)petw <-factor(rep(names(cnt), cnt), levels =names(cnt))# group-specific means so the demo has structure:mu_w <-c(cat =0, fish =1, pig =2, dog =3)happyw <-rnorm(length(petw), mean = mu_w[as.character(petw)], sd =1)contrasts(petw) <-contr.sum(4) # start from unweighted effects codesM <-contrasts(petw)N <-table(petw)# reweight the comparison (last) group's row by the cell-size ratios:M[4, ] <-c(M[4, 1] * N[1] / N[4], M[4, 2] * N[2] / N[4], M[4, 3] * N[3] / N[4])contrasts(petw) <- Mround(M, 2)
[,1] [,2] [,3]
cat 1 0 0
fish 0 1 0
pig 0 0 1
dog -2 -3 -4
m_weff <-lm(happyw ~ petw)
The payoff: the intercept is now the sample grand mean - the ordinary mean of the raw data, weighted by cell size - not the mean of the group means:
The intercept tracks the raw mean. The weights get pushed onto the comparison-group means, so unbalanced data no longer distorts your grand mean.
19.6 Contrast Coding
Dummy and effects codes answer fixed questions. Contrast codes let you pose the question - “is the average of these two groups different from the average of those two?” - by writing the comparison directly into the code columns. Done right (orthogonally), each coefficient is one clean, independent comparison.
19.6.1 Helmert contrasts
R ships one useful family, Helmert codes, which compare each level to the pooled mean of the levels before it:
The intercept is again the (unweighted) grand mean. The first Helmert slope is half the gap between the first two group means - why half? Because the code uses \(-1\) and \(+1\), a two-unit spread, so the slope is the difference divided by 2:
The real power is writing your own. Suppose the scientific questions are: (1) are the “usual” pets (cat, fish) different from the “unusual” ones (pig, dog)? (2) cat vs. fish? (3) pig vs. dog? Encode exactly those, and name the columns so the output documents itself:
These three contrasts are orthogonal (each pair of columns is uncorrelated), so the three comparisons are independent - each coefficient answers its question without contamination from the others. Confirm the first one really is the usual-vs-unusual gap:
Effects and contrast codes must be centered to move between means and coefficients - so you divide by the right denominator (the spread of the codes) when you compute a \(b\) from marginal means, and multiply back when you go the other way. Miss the denominator and your hand-check will be “off by a factor” and you’ll think you broke something. You didn’t; you forgot to center.
19.7 Nonsense Coding
Now the capstone, and the point of the whole chapter. What if we assign arbitrary numbers to the groups, with no reference, no deviations, and no meaningful comparison behind them?
Those coefficients are not interpretable, which is exactly why the columns are named huh, wat, and lol. You cannot read a single one off the group means. And yet look at what has not changed:
# The model fit is byte-for-byte identical to the sensible codings:c(same_fitted =isTRUE(all.equal(fitted(m_nonsense), fitted(m_dummy))),same_r2 =all.equal(summary(m_nonsense)$r.squared, summary(m_dummy)$r.squared),same_F =all.equal(summary(m_nonsense)$fstatistic[1],summary(m_dummy)$fstatistic[1]))
same_fitted same_r2 same_F
TRUE TRUE TRUE
All TRUE. Every coding scheme in this chapter fits the exact same model - same predicted values, same \(R^2\), same omnibus \(F\). Dummy, effects, weighted, contrast, and nonsense codes are five different descriptions of one fitted surface. The omnibus test (“does pet matter at all?”) is blind to coding. Only the individual coefficients - the intercept and the \(b\)’s - carry the fingerprint of the code you chose.
That is the lesson worth holding onto:
ImportantCoding does not change the model - only the meaning of the coefficients
The data decide the fit. You decide, through the coding scheme, which questions the coefficients answer. Choose codes whose contrasts match your scientific question and the output reads like a sentence. Choose thoughtlessly and you get nonsense coding: correct arithmetic, meaningless numbers.
19.8 Summary of All Schemes
Scheme
R helper
Intercept means
A slope means
Use it when
Dummy / reference
contr.treatment
reference-group mean
group − reference
you have a control/baseline group
Effects / deviation
contr.sum
grand mean (of group means)
group − grand mean
groups are balanced; you want deviations
Weighted effects
hand-built from contr.sum
sample grand mean (weighted)
group − weighted grand mean
groups are unbalanced
Contrast (Helmert/custom)
contr.helmert / your matrix
grand mean
one planned comparison
you have specific, orthogonal questions
Nonsense
arbitrary matrix
nothing interpretable
nothing interpretable
never (it’s the cautionary tale)
Across every row the \(R^2\), \(F\), and fitted values are identical. The columns “Intercept means” and “A slope means” are the only things that move.
19.9 A Real Example: Data From the GLM Course
Simulated pets are friendly, but let’s prove the machinery on real data - the Module 3 dataset from the graduate GLM course (data/mod3data.csv). It has an outcome y1 and a genuine four-level grouping factor, f3, with unequal cells (real data is always lumpy):
d3 <-read.csv("data/mod3data.csv")d3$grp <-factor(d3$f3, labels =c("A", "B", "C", "D"))mu3 <-tapply(d3$y1, d3$grp, mean)round(mu3, 3) # the four real group means
A B C D
0.506 -0.334 -0.132 -0.019
table(d3$grp) # unequal cell sizes
A B C D
5 7 11 7
* Group means and cell sizes of y1 by grp.
MEANS TABLES=y1 BY grp
/CELLS=MEAN STDDEV COUNT.
Both TRUE - the intercept is group A’s mean, each slope a group’s mean minus A’s. Switch to effects coding and the intercept becomes the grand mean of the four group means, unequal cells and all:
TRUE. Real data, real (unbalanced) cells, and every rule from the pet example still holds, because the arithmetic works the same whether the groups are house pets or the conditions of an actual study.
19.10 Challenge
TipDo One Yourself
Refit the four-group model with contr.treatment using cat as the reference instead of dog. Which coefficients change sign, and why is \(R^2\) untouched?
Build a balanced version of the pet data (equal cell sizes) and show that weighted and unweighted effects coding then give the same intercept. Explain in one sentence why the weights stop mattering.
Invent your own orthogonal contrast set for four groups (check orthogonality with crossprod(yourmatrix) - the off-diagonals should be 0). Fit it, and reconstruct one coefficient from the group means, remembering to divide by the code spread.
19.11 Where We Go Next
Coding is where categorical data enter the linear model, and where careful thought pays off in coefficients you can actually explain. It is also the last piece of the observed-variable linear model. But the constructs we most want to study, such as depression, curiosity, and intelligence, are never observed directly; we see only their noisy indicators. The next part turns to latent variables and the factor and structural models built for them, and after that, to what you do when the normal curve no longer describes your data well.