Code
using AlgebraOfGraphics
using CairoMakie
using Chain
using DataFrames
using MixedModels
using SMLP2026: dataset
using StatsBase
using StatsModels
progress = falsePhillip Alday
Reinhold Kliegl
2026-06-21
After working through this page you will be able to:
Prerequisites: Transformations of the predictors and the response.
Datasets used: kwdyz11 (see the dataset catalog).
Many researchers have pointed out that contrasts should be “tested instead of, rather than as a supplement to, the ordinary ‘omnibus’ F test” (Hays, 1973, p. 601),
For a (quasi-)experimental set of data, there is (or should be) a clear a priori theoretical commitment to specific hypotheses about differences between factor levels and how these differences enter in interactions with other factors. This specification should be used in the first LMM and reported, regardless of the outcome. If alternative theories lead to alternative a priori contrast specifications, both analyses are justified. If the observed means render the specification completely irrelevant, the comparisons originally planned could still be reported in a Supplement.
In this script, we are working through a large number of different contrasts for the same data. The purpose is to introduce both the preprogrammed (“canned”) and the general options to specify hypotheses about main effects and interactions. Obviously, we do not endorse generating a plot of the means and specifying the contrasts accordingly. This is known as the Texas sharpshooter fallacy.
Irrespective of how results turn out, there is nothing wrong with specifying a set of post-hoc contrasts to gain a better understanding of what the data are trying to tell us. Of course, in an article or report about the study, the a priori and post-hoc nature of contrast specifications must be made clear. Some kind of multiple comparisons correction (e.g., Bonferroni) may be called for, too. And, of course, there are grey zones.
There is quite a bit of statistical literature on contrasts. Two “local” references are Brehm & Alday (2022) and Schad et al. (2020).
For further readings see “Further Readings” in Schad et al. (2020).
We take the KWDYZ dataset from Kliegl et al. (2011). This is an experiment looking at three effects of visual cueing under four different cue-target relations (CTRs). Two horizontal rectangles are displayed above and below a central fixation point or they displayed in vertical orientation to the left and right of the fixation point. Subjects react to the onset of a small visual target occurring at one of the four ends of the two rectangles. The target is cued validly on 70% of trials by a brief flash of the corner of the rectangle at which it appears; it is cued invalidly at the three other locations 10% of the trials each.
We specify three contrasts for the four-level factor CTR that are derived from spatial, object-based, and attractor-like features of attention. They map onto sequential differences between appropriately ordered factor levels.
We also have a dataset from a replication and extension of this study (Kliegl et al., 2015).
| Row | CTR | rt_mean | rt_std | rt_length | rt_se |
|---|---|---|---|---|---|
| String | Float32 | Float32 | Int64 | Float64 | |
| 1 | val | 358.032 | 83.4579 | 20141 | 0.588067 |
| 2 | sod | 391.267 | 92.662 | 2863 | 1.73177 |
| 3 | dos | 405.146 | 92.6892 | 2843 | 1.73836 |
| 4 | dod | 402.3 | 95.3913 | 2863 | 1.78278 |
We use the same formula for all analyses
This is the default order of factor levels.
Controlling the ordering of levels for contrasts:
levels to order the levelsbase a different level can be specified.The SeqDiffCoding contrast corresponds to MASS::contr.sdif() in R.
| Est. | SE | z | p | σ_Subj | |
|---|---|---|---|---|---|
| (Intercept) | 389.7336 | 7.0914 | 54.96 | <1e-99 | 55.2006 |
| CTR: sod | 33.7817 | 3.2874 | 10.28 | <1e-24 | 23.2487 |
| CTR: dos | 13.9852 | 2.3057 | 6.07 | <1e-08 | 10.7540 |
| CTR: dod | -2.7470 | 2.2143 | -1.24 | 0.2148 | 9.5109 |
| Residual | 69.8348 |
What does the intercept represent?
Grand Mean is mean of condition means.
HypothesisCoding is the most general option available. We can implement all “canned” contrasts ourselves. The next example reproduces the test statistics from SeqDiffCoding, with a minor modification illustrating the flexibility of going beyond the default version.
| Est. | SE | z | p | σ_Subj | |
|---|---|---|---|---|---|
| (Intercept) | 389.7336 | 7.0911 | 54.96 | <1e-99 | 55.1977 |
| CTR: spt | 33.7817 | 3.2875 | 10.28 | <1e-24 | 23.2490 |
| CTR: obj | 13.9852 | 2.3059 | 6.06 | <1e-08 | 10.7567 |
| CTR: grv | 2.7469 | 2.2146 | 1.24 | 0.2148 | 9.5157 |
| Residual | 69.8348 |
The difference to the preprogrammed SeqDiffCoding is that for the third contrast we changed the direction of the contrast such that the sign of the effect is positive when the result is in agreement with theoretical expectation, that is we subtract the fourth level from the third, not the third level from the fourth.
This contrast corresponds to contr.treatment() in R
| Est. | SE | z | p | σ_Subj | |
|---|---|---|---|---|---|
| (Intercept) | 358.0914 | 6.1539 | 58.19 | <1e-99 | 47.9099 |
| CTR: dod | 45.0200 | 4.3636 | 10.32 | <1e-24 | 32.2915 |
| CTR: dos | 47.7669 | 3.5566 | 13.43 | <1e-40 | 25.5369 |
| CTR: sod | 33.7817 | 3.2875 | 10.28 | <1e-24 | 23.2491 |
| Residual | 69.8348 |
The DummyCoding contrast has the disadvantage that the intercept returns the mean of the level specified as base, default is the first level, not the GM.
The contrasts returned by DummyCoding may be exactly what we want. Can’t we have them, but also have the intercept estimate the GM, rather than the mean of the base level? Yes, we can! We call this “You can have your cake and it eat, too”-Coding (YCHYCAEIT coding). And we use HypothesisCoding to achieve this outcome.
| Est. | SE | z | p | σ_Subj | |
|---|---|---|---|---|---|
| (Intercept) | 389.7336 | 7.0916 | 54.96 | <1e-99 | 55.2019 |
| CTR: sod | 33.7817 | 3.2875 | 10.28 | <1e-24 | 23.2494 |
| CTR: dos | 47.7669 | 3.5566 | 13.43 | <1e-40 | 25.5372 |
| CTR: dod | 45.0200 | 4.3637 | 10.32 | <1e-24 | 32.2921 |
| Residual | 69.8348 |
We can simply move the column with -1s for a different base.
| Est. | SE | z | p | σ_Subj | |
|---|---|---|---|---|---|
| (Intercept) | 389.7336 | 7.0926 | 54.95 | <1e-99 | 55.2101 |
| CTR: val | -33.7817 | 3.2874 | -10.28 | <1e-24 | 23.2481 |
| CTR: dos | 13.9852 | 2.3061 | 6.06 | <1e-08 | 10.7589 |
| CTR: dod | 11.2382 | 2.3589 | 4.76 | <1e-05 | 11.4695 |
| Residual | 69.8348 |
We can simply relevel the factor with a different base.
EffectsCoding estimates the difference between the Grand Mean and three of the four levels. The difference of the fourth levels can be computed from the Grand Mean and these three differences.
| Est. | SE | z | p | σ_Subj | |
|---|---|---|---|---|---|
| (Intercept) | 389.7336 | 7.1020 | 54.88 | <1e-99 | 55.2836 |
| CTR: sod | 2.1404 | 1.3396 | 1.60 | 0.1101 | 6.0863 |
| CTR: dos | 16.1244 | 1.4337 | 11.25 | <1e-28 | 7.2510 |
| CTR: dod | 13.3774 | 1.9359 | 6.91 | <1e-11 | 12.4968 |
| Residual | 69.8344 |
This contrast corresponds almost to contr.sum() in R. The “almost” qualification refers to the fact that EffectsCoding uses the first level as default base; contr.sum() uses the last factor level.
| Est. | SE | z | p | σ_Subj | |
|---|---|---|---|---|---|
| (Intercept) | 389.7336 | 7.0919 | 54.95 | <1e-99 | 55.2045 |
| CTR: val | -31.6422 | 2.6424 | -11.97 | <1e-32 | 19.9509 |
| CTR: sod | 2.1396 | 1.3338 | 1.60 | 0.1087 | 6.0079 |
| CTR: dos | 16.1248 | 1.4404 | 11.19 | <1e-28 | 7.3317 |
| Residual | 69.8348 |
How could we achieve the default result with HypothesisCoding?
m3c = let levels = ["val", "sod", "dos", "dod"]
contrasts = Dict(
:CTR => HypothesisCoding(
[
-1/4 3/4 -1/4 -1/4 # b - GM = b - (a+b+c+d)/4 => -1/4*a + 3/4*b - 1/4*c - 1/4*d
-1/4 -1/4 3/4 -1/4 # c - GM = c - (a+b+c+d)/4 => -1/4*a - 1/4*b + 3/4*c - 1/4*d
-1/4 -1/4 -1/4 3/4 # d - GM = d - (a+b+c+d)/4 => -1/4*a - 1/4*b - 1/4*c + 3/4*d
];
levels,
labels=levels[2:end],
)
)
fit(MixedModel, form, dat1; contrasts, progress)
end| Est. | SE | z | p | σ_Subj | |
|---|---|---|---|---|---|
| (Intercept) | 389.7336 | 7.0897 | 54.97 | <1e-99 | 55.1873 |
| CTR: sod | 2.1395 | 1.3332 | 1.60 | 0.1085 | 5.9995 |
| CTR: dos | 16.1248 | 1.4406 | 11.19 | <1e-28 | 7.3345 |
| CTR: dod | 13.3779 | 1.9314 | 6.93 | <1e-11 | 12.4546 |
| Residual | 69.8349 |
HelmertCoding codes each level as the difference from the average of the lower levels. With the default order of CTR levels we get the following test statistics. These contrasts are orthogonal.
| Est. | SE | z | p | σ_Subj | |
|---|---|---|---|---|---|
| (Intercept) | 389.7336 | 7.0927 | 54.95 | <1e-99 | 55.2104 |
| CTR: sod | 16.8909 | 1.6438 | 10.28 | <1e-24 | 11.6254 |
| CTR: dos | 10.2920 | 0.8354 | 12.32 | <1e-34 | 5.2574 |
| CTR: dod | 4.4593 | 0.6442 | 6.92 | <1e-11 | 4.1548 |
| Residual | 69.8348 |
+ HeC1: (b - a)/2 # (391 - 358)/2 = 16.5
+ HeC2: (c - (b+a)/2)/3 # (405 - (391 + 358)/2)/3 = 10.17
+ HeC3: (d - (c+b+a)/3)/4 # (402 - (405 + 391 + 358)/3)/4 = 4.33
We can reconstruct the estimates, but they are scaled by the number of levels involved. With HypothesisCoding we can estimate the “unscaled” differences. Also the labeling of the contrasts is not as informative as they could be.
| Est. | SE | z | p | σ_Subj | |
|---|---|---|---|---|---|
| (Intercept) | 389.7336 | 7.0915 | 54.96 | <1e-99 | 55.2009 |
| CTR: 2-1 | 33.7817 | 3.2874 | 10.28 | <1e-24 | 23.2484 |
| CTR: 3-21 | 30.8761 | 2.5062 | 12.32 | <1e-34 | 15.7722 |
| CTR: 4-321 | 17.8371 | 2.5765 | 6.92 | <1e-11 | 16.6183 |
| Residual | 69.8348 |
Reverse HelmertCoding codes each level as the difference from the average of the higher levels. To estimate these effects we simply reverse the order of factor levels. Of course, the contrasts are also orthogonal.
| Est. | SE | z | p | σ_Subj | |
|---|---|---|---|---|---|
| (Intercept) | 389.7336 | 7.0919 | 54.95 | <1e-99 | 55.2046 |
| CTR: dos | 1.3735 | 1.1071 | 1.24 | 0.2147 | 4.7543 |
| CTR: sod | -4.2039 | 0.6842 | -6.14 | <1e-09 | 3.3482 |
| CTR: val | -10.5474 | 0.8807 | -11.98 | <1e-32 | 6.6492 |
| Residual | 69.8348 |
+ HeC1:(c - d)/2 # (405 - 402)/2 = 1.5
+ HeC2:(b - (c+d)/2)/3 # (391 - (405 + 402)/2)/3 = -4.17
+ HeC3:(a - (b+c+d)/3/4 # (356 -(391 + 405 + 402)/3)/4 = -10.83
… and the unscaled-by-number-of-levels estimates.
| Est. | SE | z | p | σ_Subj | |
|---|---|---|---|---|---|
| (Intercept) | 389.7336 | 7.0907 | 54.96 | <1e-99 | 55.1947 |
| CTR: 3-4 | 2.7469 | 2.2141 | 1.24 | 0.2147 | 9.5076 |
| CTR: 2-34 | -12.6117 | 2.0529 | -6.14 | <1e-09 | 10.0485 |
| CTR: 1-234 | -42.1895 | 3.5227 | -11.98 | <1e-32 | 26.5978 |
| Residual | 69.8348 |
For factors with more than four levels there are many options for specifying orthogonal contrasts as long as one proceeds in a top-down strictly hierarchical fashion.
Suppose you have a factor with seven levels and let’s ignore shifting columns. In this case, you have six options for the first contrast, that is 6 vs. 1, 5 vs.2 , 4 vs. 3, 3 vs. 4, 2 vs. 5, and 1 vs. 6 levels. Then, you specify orthogonal contrasts for partitions with more than 2 elements and so on. That is, you don’t specify a contrast that crosses an earlier partition line.
In the following example, after an initial 4 vs 3 partitioning of levels, we specify AnovaCoding for the left and HelmertCoding for the right partition.
contrasts = Dict(
:CTR => HypothesisCoding(
[
-1/4 -1/4 -1/4 -1/4 +1/3 +1/3 +1/3
-1/2 -1/2 +1/2 +1/2 0 0 0
-1/2 +1/2 -1/2 +1/2 0 0 0
+1/2 -1/2 -1/2 +1/2 0 0 0
0 0 0 0 -1 +1 0
0 0 0 0 -1/2 -1/2 1
];
levels=["A1", "A2", "A3", "A4", "A5", "A6", "A7"],
labels=["c567.1234", "B", "C", "BxC", "c6.5", "c6.56"],
),
);There are two rules that hold for all orthogonal contrasts:
Factorial designs (i.e., lab experiments) are traditionally analyzed with analysis of variance. The test statistics of main effects and interactions are based on an orthogonal set of contrasts. We specify them with HypothesisCoding.
An A(2) x B(2) design can be recast as an F(4) design with the levels (A1-B1, A1-B2, A2-B1, A2-B2). The following contrast specification returns estimates for the main effect of A, the main effect of B, and the interaction of A and B. In a figure With A on the x-axis and the levels of B shown as two lines, the interaction tests the null hypothesis that the two lines are parallel. A positive coefficient implies overadditivity (diverging lines toward the right) and a negative coefficient underadditivity (converging lines).
| Est. | SE | z | p | σ_Subj | |
|---|---|---|---|---|---|
| (Intercept) | 389.7336 | 7.0918 | 54.96 | <1e-99 | 55.2033 |
| CTR: A | 59.0052 | 5.1831 | 11.38 | <1e-29 | 36.2122 |
| CTR: B | 31.0348 | 4.6755 | 6.64 | <1e-10 | 31.7183 |
| CTR: AxB | -36.5287 | 3.0930 | -11.81 | <1e-31 | 16.0087 |
| Residual | 69.8348 |
It is also helpful to see the corresponding layout of the four means for the interaction of A and B (i.e., the third contrast)
B1 B2
A1 +1 -1
A2 -1 +1
Thus, interaction tests whether the difference between main diagonal and minor diagonal is different from zero.
Going beyond the four level factor; it is also helpful to see the corresponding layout of the eight means for the interaction of A and B and C.
C1 C2
B1 B2 B1 B2
A1 +1 -1 A1 -1 +1
A2 -1 +1 A2 +1 -1
Nested contrasts are often specified as follow up as post-hoc tests for ANOVA interactions. They are orthogonal. We specify them with HypothesisCoding.
An A(2) x B(2) design can be recast as an F(4) design with the levels (A1-B1, A1-B2, A2-B1, A2-B2). The following contrast specification returns an estimate for the main effect of A and the effects of B nested in the two levels of A. In a figure With A on the x-axis and the levels of B shown as two lines, the second contrast tests whether A1-B1 is different from A1-B2 and the third contrast tests whether A2-B1 is different from A2-B2.
| Est. | SE | z | p | σ_Subj | |
|---|---|---|---|---|---|
| (Intercept) | 389.7336 | 7.0913 | 54.96 | <1e-99 | 55.1998 |
| CTR: do_so | 59.0052 | 5.1824 | 11.39 | <1e-29 | 36.2064 |
| CTR: spt | 33.7817 | 3.2873 | 10.28 | <1e-24 | 23.2479 |
| CTR: grv | 2.7470 | 2.2142 | 1.24 | 0.2148 | 9.5100 |
| Residual | 69.8348 |
The three contrasts for one main effect and two nested contrasts are orthogonal. There is no test of the interaction (parallelism).
Three factors:
2 x 3 = 6 measures / subject
Arrow.Table with 396 rows, 5 columns, and schema:
:Subj String
:Group String
:A String
:B String
:dv Float64
We select an LMM supported by the data.
cntrst2 = Dict(
:Group => SeqDiffCoding(; levels=["G1", "G2"]),
:A => SeqDiffCoding(; levels=["A1", "A2"]),
:B => SeqDiffCoding(; levels=["B1", "B2", "B3"]),
)
f6_cpx = @formula(dv ~ 1 + Group*A*B + (1 + A+B | Subj));
f6_zcp = @formula(dv ~ 1 + Group*A*B + zerocorr(1 + A+B | Subj));
f6_ovi = @formula(dv ~ 1 + Group*A*B + (1 | Subj));| Est. | SE | z | p | σ_Subj | |
|---|---|---|---|---|---|
| (Intercept) | 29.5404 | 1.0697 | 27.62 | <1e-99 | 8.5500 |
| Group: G2 | -6.3605 | 2.1394 | -2.97 | 0.0029 | |
| A: A2 | -0.0074 | 0.3796 | -0.02 | 0.9844 | 0.3183 |
| B: B2 | 0.1856 | 0.5132 | 0.36 | 0.7175 | 1.8073 |
| B: B3 | -0.9345 | 0.4665 | -2.00 | 0.0451 | 0.5001 |
| Group: G2 & A: A2 | 0.2392 | 0.7591 | 0.32 | 0.7527 | |
| Group: G2 & B: B2 | 1.9668 | 1.0263 | 1.92 | 0.0553 | |
| Group: G2 & B: B3 | 0.0872 | 0.9330 | 0.09 | 0.9256 | |
| A: A2 & B: B2 | 3.5336 | 0.9248 | 3.82 | 0.0001 | |
| A: A2 & B: B3 | 0.5232 | 0.9248 | 0.57 | 0.5715 | |
| Group: G2 & A: A2 & B: B2 | 2.8335 | 1.8496 | 1.53 | 0.1255 | |
| Group: G2 & A: A2 & B: B3 | -2.3042 | 1.8496 | -1.25 | 0.2128 | |
| Residual | 3.7548 |
| Est. | SE | z | p | σ_Subj | |
|---|---|---|---|---|---|
| (Intercept) | 29.5404 | 1.0698 | 27.61 | <1e-99 | 8.5483 |
| Group: G2 | -6.3605 | 2.1395 | -2.97 | 0.0030 | |
| A: A2 | -0.0074 | 0.3805 | -0.02 | 0.9845 | 0.0000 |
| B: B2 | 0.1856 | 0.4992 | 0.37 | 0.7100 | 1.4527 |
| B: B3 | -0.9345 | 0.4660 | -2.01 | 0.0449 | 0.0000 |
| Group: G2 & A: A2 | 0.2392 | 0.7610 | 0.31 | 0.7533 | |
| Group: G2 & B: B2 | 1.9668 | 0.9984 | 1.97 | 0.0488 | |
| Group: G2 & B: B3 | 0.0872 | 0.9321 | 0.09 | 0.9255 | |
| A: A2 & B: B2 | 3.5336 | 0.9321 | 3.79 | 0.0001 | |
| A: A2 & B: B3 | 0.5232 | 0.9321 | 0.56 | 0.5745 | |
| Group: G2 & A: A2 & B: B2 | 2.8335 | 1.8641 | 1.52 | 0.1285 | |
| Group: G2 & A: A2 & B: B3 | -2.3042 | 1.8641 | -1.24 | 0.2164 | |
| Residual | 3.7843 |
| Est. | SE | z | p | σ_Subj | |
|---|---|---|---|---|---|
| (Intercept) | 29.5404 | 1.0698 | 27.61 | <1e-99 | 8.5428 |
| Group: G2 | -6.3605 | 2.1395 | -2.97 | 0.0030 | |
| A: A2 | -0.0074 | 0.3879 | -0.02 | 0.9848 | |
| B: B2 | 0.1856 | 0.4751 | 0.39 | 0.6960 | |
| B: B3 | -0.9345 | 0.4751 | -1.97 | 0.0492 | |
| Group: G2 & A: A2 | 0.2392 | 0.7758 | 0.31 | 0.7579 | |
| Group: G2 & B: B2 | 1.9668 | 0.9502 | 2.07 | 0.0385 | |
| Group: G2 & B: B3 | 0.0872 | 0.9502 | 0.09 | 0.9269 | |
| A: A2 & B: B2 | 3.5336 | 0.9502 | 3.72 | 0.0002 | |
| A: A2 & B: B3 | 0.5232 | 0.9502 | 0.55 | 0.5819 | |
| Group: G2 & A: A2 & B: B2 | 2.8335 | 1.9004 | 1.49 | 0.1360 | |
| Group: G2 & A: A2 & B: B3 | -2.3042 | 1.9004 | -1.21 | 0.2253 | |
| Residual | 3.8580 |
| model-dof | -2 logLik | χ² | χ²-dof | P(>χ²) | |
|---|---|---|---|---|---|
| dv ~ 1 + Group + A + B + Group & A + Group & B + A & B + Group & A & B + (1 | Subj) | 14 | -2419 | |||
| dv ~ 1 + Group + A + B + Group & A + Group & B + A & B + Group & A & B + zerocorr(1 + A + B | Subj) | 17 | -2418 | 1 | 3 | 0.8302 |
| dv ~ 1 + Group + A + B + Group & A + Group & B + A & B + Group & A & B + (1 + A + B | Subj) | 23 | -2416 | 1 | 6 | 0.9597 |
There is a significant interaction between A and the first contrast of B (i.e., B2 - B1). The interaction is not significant for A and the second contrast of B (i.e., B3 - B2). This implies that the left pair of lines in the following figure is statistically not parallel and that we do not have sufficient evidence that the right pair of lines is not parallel.
| Row | Name | Coef. | Std. Error | z | Pr(>|z|) |
|---|---|---|---|---|---|
| String | Float64 | Float64 | Float64 | Float64 | |
| 1 | A: A2 & B: B2 | 3.53363 | 0.950202 | 3.71882 | 0.000200157 |
| 2 | A: A2 & B: B3 | 0.523243 | 0.950202 | 0.550665 | 0.581863 |
using Chain
tbl1 = @chain DataFrame(dat2) begin
groupby(_, [:Subj, :A, :B])
combine(_, nrow => :n, :dv => mean => :dv)
groupby(_, [:A, :B])
combine(_,
:dv => mean => :dv_M,
:dv => std => :dv_SD,
:dv => sem => :dv_SE)
end
fig = Figure(; size=(300, 300))
plt = data(tbl1) * mapping(:B, :dv_M; color=:A) * (visual(Lines) + visual(Scatter))
draw!(fig, plt)
figDummyCoding and once with EffectsCoding for the four-level CTR factor. What does the intercept estimate in each case?With DummyCoding the intercept is the mean of the reference level; with EffectsCoding it is the grand mean (the unweighted average across levels). The slopes change meaning accordingly — differences from the reference level versus deviations from the grand mean.
HypothesisCoding that encodes the spatial, object-based, and attraction comparisons described in the text, and verify that the resulting coefficients match those comparisons.Supply a 3×4 hypothesis matrix whose rows correspond to the three theoretically motivated comparisons (in the appropriately ordered factor levels). After fitting, check that each estimated coefficient equals the corresponding difference of cell means — a good habit for confirming a custom coding does what you intended.
References
This page was rendered from git revision e563d55 using Quarto 1.9.38 and Julia 1.12.6.