viewof c1p = html`<input type="number" value="2" min="-3" max="3" step="1" style="width:52px">`
viewof c1t = html`<input type="number" value="-1" min="-3" max="3" step="1" style="width:52px">`
viewof c1i = html`<input type="number" value="-1" min="-3" max="3" step="1" style="width:52px">`
md`**Contrast 2** weights — Pizza / Thai / Indian`Planned Contrasts
Info sheet · Statistics for Psychology & Neuroscience
Warning✎ Editing notes — to do / to check
Working notes for the author — not shown to students once collapsed; remove before publishing.
- [ ]
What you’ll get from this sheet
An alternative to post-hoc testing that you plan before seeing the data. By the end you should be able to:
- Tell a planned (a priori) contrast from a post-hoc comparison.
- Build a contrast plan — and check it’s orthogonal.
- Run planned contrasts in R.
A planned contrast is a comparison you choose before seeing the results. Rules: each contrast compares two chunks of conditions, its weights sum to zero, and no group is reused. If the contrasts are orthogonal (their weights multiply to zero across groups), they’re independent — so you don’t need to correct for multiple comparisons.
Planned vs post-hoc
Post-hoc comparisons are inherently opportunistic. You run the ANOVA, something catches your eye — a gap between the 1850s and the 2000s, say — and you chase it with an extra t-test. That’s close to HARKing (hypothesising after the results are known), which is why post-hoc tests are penalised with multiple-comparison corrections.
Planned contrasts are the opposite: you decide which comparisons to test in advance, from your hypotheses — exactly what you’d put in a pre-registration — and then run them regardless of the omnibus result. Because they were planned, you generally don’t need to correct for multiple comparisons. You’re not penalised for tests you committed to up front.
Field’s guidelines for building a plan: if there’s a control condition (a healthy group, a gold-standard drug), contrast each test condition against it. Otherwise, follow three rules — each contrast compares only two chunks of variation; weights sum to zero; and once a group is singled out in one contrast it can’t appear in another. That last rule is what keeps the contrasts independent.
Build a contrast plan
Take the late-night food example: two new curry shops (Thai, Indian) against the gold-standard Pizza. The sensible plan is Pizza vs both curries, then Thai vs Indian — two contrasts that carry as much information as all three pairwise tests. Set the weights and watch the checks: each column must sum to zero, and the product column must sum to zero for the pair to be orthogonal.
The default plan is orthogonal: Contrast 1 = (2, −1, −1) pits Pizza against both curries, Contrast 2 = (0, 1, −1) compares the two curries, and no group is singled out twice. Try the “obvious” alternative instead — Pizza vs Thai = (1, −1, 0) and Pizza vs Indian = (1, 0, −1). Both still sum to zero, but Pizza is reused, the product column no longer sums to zero, and the checker flags it: those contrasts overlap, so you’d have to correct after all. Orthogonality is exactly the reused-group rule, made arithmetic.
More plans, same logic
The rules scale up. For four sci-fi shows (Star Wars + three Star Treks): start (3, −1, −1, −1) — Star Wars vs all Trek — then split the Treks, e.g. (0, 2, −1, −1) and (0, 0, 1, −1). For Decade of Dance, group by hypothesis rather than testing every pair: pre-disco (’50s, ’60s) vs post-disco (’80s–2010s), dropping the ’70s with a weight of 0, then a follow-up splitting 20th- from 21st-century post-disco. A group left out of a contrast always gets weight 0.
See it in code
Assign a contrast matrix to the factor, then read each contrast’s test straight off summary.lm:
import numpy as np, pandas as pd, statsmodels.formula.api as smf
# custom contrasts via a helper matrix; each column is one planned contrast
C = np.array([[2, 0], [-1, 1], [-1, -1]]) # rows: Pizza, Thai, Indian
df = pd.DataFrame({"rating": ratings, "food": food})
m = smf.ols("rating ~ C(food, contrasts.ContrastMatrix(C, ['c1','c2']))", df).fit()
print(m.summary())The R tab runs live; the Python tab is a static reference.
TipCheck your understanding
Why can’t your two contrasts be “Pizza vs Thai” and “Pizza vs Indian”?
Because Pizza is singled out in both — you’ve reused a group, so the two contrasts aren’t independent (their weight products won’t sum to zero). That overlap means they’re testing partly the same thing, and you’d lose the no-correction advantage of planned contrasts. The orthogonal plan tests Pizza against both curries in one contrast, then Thai against Indian in the other — each group carries its full weight exactly once.
Two traps. First, in R the contrast rows must line up with the factor’s level order, which is alphabetical by default — set levels = explicitly or you’ll silently test the wrong comparison. Second, the no-correction perk only applies to orthogonal planned contrasts; non-orthogonal ones (or more contrasts than k − 1 groups’ worth) still need a correction. And “planned” means genuinely planned — decide before you see the data, or it’s just HARKing with extra steps.
Where this shows up next
See Chapter (ANOVA) for the full worked example (planned contrasts on the country-of-origin data), and the next segment for a complete step-by-step build.