This is an old question, and the previous answers were very good, but I will try to answer it, to get a clearer picture. Maybe that can help someone.
Let's get the data and the contrast matrix:
hsb2 = read.table('https://stats.idre.ucla.edu/stat/data/hsb2.csv', header=T, sep=",")
hsb2$race.f = factor(hsb2$race, labels=c("Hispanic", "Asian", "African-Am", "Caucasian"))
mat = matrix(c(1/4, 1/4, 1/4, 1/4, 1, 0, -1, 0, -1/2, 1, 0, -1/2, -1/2, -1/2, 1/2, 1/2), ncol = 4)
mat
[,1] [,2] [,3] [,4]
[1,] 0.25 1 -0.5 -0.5
[2,] 0.25 0 1.0 -0.5
[3,] 0.25 -1 0.0 0.5
[4,] 0.25 0 -0.5 0.5
The actual contrasts that you want is:
C <- t(mat)
C
[,1] [,2] [,3] [,4]
[1,] 0.25 0.25 0.25 0.25
[2,] 1.00 0.00 -1.00 0.00
[3,] -0.50 1.00 0.00 -0.50
[4,] -0.50 -0.50 0.50 0.50
And the contrasts are:
\begin{equation}
C\mu = \begin{pmatrix}
\phantom{..} 1/4 & 1/4 & 1/4 & 1/4 \\
1 & 0 & -1 & 0\\
-1/2 & 1 & 0 & -1/2\\
-1/2 & -1/2 & 1/2 & 1/2\\
\end{pmatrix}\
\begin{pmatrix}\mu1 \\\mu2 \\\mu3 \\\mu4 \end{pmatrix}
\end{equation}
If we calculate them on the data:
means <- with(hsb2, tapply(X = write, INDEX = race.f, FUN = mean))
C %*% means
[,1]
[1,] 51.678376
[2,] -1.741667
[3,] 7.743247
[4,] -1.101580
Which are the same values given on the website, using their notation:
mymat = solve(t(mat))
summary(lm(write ~ race.f, hsb2, contrasts = list(race.f= mymat[,2:4])))
Call:
lm(formula = write ~ race.f, data = hsb2, contrasts = list(race.f = mymat[,
2:4]))
Residuals:
Min 1Q Median 3Q Max
-23.0552 -5.4583 0.9724 7.0000 18.8000
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) 51.6784 0.9821 52.619 < 2e-16 ***
race.f1 -1.7417 2.7325 -0.637 0.52461
race.f2 7.7432 2.8972 2.673 0.00816 **
race.f3 -1.1016 1.9642 -0.561 0.57556
So we know why we take the transpose, why do we take the inverse?
The model we can use is:
\begin{equation}
y \ =\ X\mu \ +\ \epsilon \
\end{equation}
with $X$ the design matrix for the cell means model and $\mu$ the vector of means.
We can evaluate the means based on this model, using the least squares method:
\begin{equation}
\hat{\mu} =(X^{\prime }X)^{-1}\ X^{\prime }y\\
\end{equation}
Now, we constructed the contrast matrix so that C is square and full rank, and we can take its inverse $C^{-1}$, and insert them in the model equation:
\begin{equation}
y \ =\ X\mu \ +\ \epsilon \ = \ X I\mu \ +\ \epsilon \ =\ X \ (C^{-1}C)\ \ \mu \ +\ \epsilon = \ (X C^{-1}) \ (C \mu) \ + \epsilon
\end{equation}
That means that we can take $XC^{-1}$ as the modified design matrix, to evaluate the contrasts $C\mu$ using the least squares method. In this case, if we name the modified design matrix $X_{1} = XC^{-1}$:
\begin{equation}
\hat{C\mu} = (X_{1}^{'}X_{1})^{-1}X_{1}^{'}y
\end{equation}
So we use the inverse of the contrast matrix to evaluate the actual contrasts.
To see that it works in R:
X <- model.matrix( ~ hsb2$race.f + 0) # model matrix for the cell means model
X1 <- X %*% solve(C) # this is the modified model matrix, using the inverse.
And we solve by the method of least squares or by lm().
# least squares equations:
solve ( t(X1) %*% X1 ) %*% t(X1) %*% hsb2$write
[,1]
[1,] 51.678376
[2,] -1.741667
[3,] 7.743247
[4,] -1.101580
# lm() with modified design matrix:
summary(lm(write ~ X1 + 0, data= hsb2))
Call:
lm(formula = write ~ X1 + 0, data = hsb2)
Residuals:
Min 1Q Median 3Q Max
-23.0552 -5.4583 0.9724 7.0000 18.8000
Coefficients:
Estimate Std. Error t value Pr(>|t|)
X11 51.6784 0.9821 52.619 < 2e-16 ***
X12 -1.7417 2.7325 -0.637 0.52461
X13 7.7432 2.8972 2.673 0.00816 **
X14 -1.1016 1.9642 -0.561 0.57556
# lm() with contrast argument:
summary(lm(write ~race.f, data= hsb2, contrasts = list(race.f= MASS::ginv(C[-1,]))))
Call:
lm(formula = write ~ race.f, data = hsb2, contrasts = list(race.f = MASS::ginv(C[-1,
])))
Residuals:
Min 1Q Median 3Q Max
-23.0552 -5.4583 0.9724 7.0000 18.8000
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) 51.6784 0.9821 52.619 < 2e-16 ***
race.f1 -1.7417 2.7325 -0.637 0.52461
race.f2 7.7432 2.8972 2.673 0.00816 **
race.f3 -1.1016 1.9642 -0.561 0.57556
Which is the same result as above.
Here we generated C so that it is invertible. In the last call, we provided the pseudoinverse of the contrast matrix without the intercept row. lm() adds the intercept column to generate $C^{-1}$. We could have used contrasts = list(race.f = solve(C)[, -1]) .
For pre-defined contrasts, it's the same thing. They are provided without the intercept term in lm(), but it is added internally and the contrasts are evaluated using the least squares method using the modified design matrix.
For example, if we use
contr.treatment(4)
2 3 4
1 0 0 0
2 1 0 0
3 0 1 0
4 0 0 1
The actual contrasts that are evaluated are:
C <- solve(cbind(1, contr.treatment(4)))
1 0 0 0
-1 1 0 0
-1 0 1 0
-1 0 0 1
And we can evaluate the contrasts as before:
X1 <- X %*% solve(C)
# least squares equations:
solve ( t(X1) %*% X1 ) %*% t(X1) %*% hsb2$write
[,1]
46.458333
2 11.541667
3 1.741667
4 7.596839
# lm() with modified design matrix:
summary(lm(write ~ X1 +0, data= hsb2))
Call:
lm(formula = write ~ X1 + 0, data = hsb2)
Residuals:
Min 1Q Median 3Q Max
-23.0552 -5.4583 0.9724 7.0000 18.8000
Coefficients:
Estimate Std. Error t value Pr(>|t|)
X1 46.458 1.842 25.218 < 2e-16 ***
X12 11.542 3.286 3.512 0.000552 ***
X13 1.742 2.732 0.637 0.524613
X14 7.597 1.989 3.820 0.000179 ***
# lm with pre-defined contrasts:
summary(lm(write ~race.f, data= hsb2, contrasts = list(race.f = contr.treatment(4))))
Call:
lm(formula = write ~ race.f, data = hsb2, contrasts = list(race.f = contr.treatment(4)))
Residuals:
Min 1Q Median 3Q Max
-23.0552 -5.4583 0.9724 7.0000 18.8000
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) 46.458 1.842 25.218 < 2e-16 ***
race.f2 11.542 3.286 3.512 0.000552 ***
race.f3 1.742 2.732 0.637 0.524613
race.f4 7.597 1.989 3.820 0.000179 ***
And indeed:
C %*% means
[,1]
46.458333
2 11.541667
3 1.741667
4 7.596839
$\textbf{Edit in response to @skan}$:
When there are more than one categorical variable, the easiest way to proceed is to insert the contrast arguments in the formula, for each categorical variable.
An example is provided here:
https://rstudio-pubs-static.s3.amazonaws.com/84177_4604ecc1bae246c9926865db53b6cc29.html
in the section "Two factors: one treatment-coded, one deviation-coded".
f <- expand.grid(w=c("left","right"),h=c("low","mid","high")) #
factor level combinations
d <- data.frame(w=rep(f$w,100),h=rep(f$h,100)) # data frame with
factor levels only
d$v <-rnorm(nrow(d),100,15)+ifelse(d$w=="left",-3,3)+ifelse(d$h=="low",-5,ifelse(d$h=="high",5,0))
str(d)
'data.frame': 600 obs. of 3 variables:
$ w: Factor w/ 2 levels "left","right": 1 2 1 2 1 2 1 2 1 2 ...
$ h: Factor w/ 3 levels "low","mid","high": 1 1 2 2 3 3 1 1 2 2 ...
$ v: num 79.7 121.5 99.5 97.4 117.9 ...
If we use treatment contrast (default) for variable h and sum contrast for variable w, we can enter:
mod <- lm(v ~ w * h, d, contrasts=list(w=contr.sum, h=contr.treatment))
summary(mod)
Call:
lm(formula = v ~ w * h, data = d, contrasts = list(w = contr.sum,
h = contr.treatment))
Residuals:
Min 1Q Median 3Q Max
-56.194 -9.112 0.287 9.269 47.108
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) 93.9555 1.0303 91.191 < 2e-16 ***
w1 -3.9217 1.0303 -3.806 0.000156 ***
h2 5.6757 1.4571 3.895 0.000109 ***
h3 11.8542 1.4571 8.136 2.41e-15 ***
w1:h2 1.0408 1.4571 0.714 0.475312
w1:h3 -0.6258 1.4571 -0.429 0.667716
For lme4, this is the same syntax, see for example:
How to set custom contrasts with lmer in R
You can use user-defined contrasts, by building each contrast and inverting it, removing the intercept term if needed, and include it in the contrast arg (as described above).
If you want to build your contrasts to run the model without using the contrast arguments, it is more cumbersome (you won't also easily recover the sum of squares for the Anova, using the method below).
Here is how to do it for the example above (sum contrast for variable w):
contw <- solve(cbind(1, contr.sum(2))) # contrast for variable w
conth <- solve(cbind(1, contr.treatment(3))) # contrast for variable h
contw
0.5 0.5
0.5 -0.5
conth
1 0 0
-1 1 0
-1 0 1
f <- expand.grid(w= levels(d$w), h= levels(d$h)) # combination of levels
f
w h
1 left low
2 right low
3 left mid
4 right mid
5 left high
6 right high
# Now we build the contrast matrix C
C <- matrix(nrow= nrow(f), ncol= nrow(f))
idx <- 0
for (i in 1:nrow(contw)) {
for (j in 1:nrow(conth)) {
idx= idx+1
C[idx,] <- c(contw[i,] %o% conth[j,]) # outer product of contrasts
}
}
colnames(C) <- with(f, paste0(w, "_", h))
C
left_low right_low left_mid right_mid left_high right_high
[1,] 0.5 0.5 0.0 0.0 0.0 0.0
[2,] -0.5 -0.5 0.5 0.5 0.0 0.0
[3,] -0.5 -0.5 0.0 0.0 0.5 0.5
[4,] 0.5 -0.5 0.0 0.0 0.0 0.0
[5,] -0.5 0.5 0.5 -0.5 0.0 0.0
[6,] -0.5 0.5 0.0 0.0 0.5 -0.5
This is the contrast matrix, which you need to interpret the final results.
For example contrast 6 correspond to an interaction term:
1/2 [ (left_high - left_low) - (right_high - right_low ) ]
Now we insert the contrast matrix in the model, using its inverse, as described above:
cond <- with(d, paste0(w, "_", h))
X <- model.matrix(~cond + 0) # design matrix
colnames(X) <- sub("cond", "", colnames(X))
X <- X[, colnames(C)] # to make sure the column order is the same as for the contrast matrix
X1 <- X %*% solve(C) # modified design matrix
summary(lm(v~X1+0, data= d)) # solve using the modified design matrix
Call:
lm(formula = v ~ X1 + 0, data = d)
Residuals:
Min 1Q Median 3Q Max
-56.194 -9.112 0.287 9.269 47.108
Coefficients:
Estimate Std. Error t value Pr(>|t|)
X11 93.9555 1.0303 91.191 < 2e-16 ***
X12 5.6757 1.4571 3.895 0.000109 ***
X13 11.8542 1.4571 8.136 2.41e-15 ***
X14 -3.9217 1.0303 -3.806 0.000156 ***
X15 1.0408 1.4571 0.714 0.475312
X16 -0.6258 1.4571 -0.429 0.667716
which corresponds to the results above (using the contrast argument). The order of contrasts is slightly different but values are the same, of course.