Contrasts from factors
Witold E. Wolski
2026-02-25
Source:vignettes/Contrasts_from_factors.Rmd
Contrasts_from_factors.RmdPurpose
When working with factorial experimental designs (e.g. two conditions
crossed with three time points), specifying all the relevant contrasts
by hand is tedious and error-prone. The generate_contrasts
family of functions in prolfqua automates this by generating contrast
specifications from factor levels. These contrast strings can then be
passed directly to the Contrasts class for statistical
testing.
This vignette demonstrates how to:
- Generate main effect, level-specific, and interaction contrasts for a two-factor design
- Use
annotation_add_contraststo produce a combined annotation and contrast table ready for analysis
Group labelling convention
All contrast generation functions assume that the group levels in the
fitted model follow the naming convention
G_<primary>_<secondary>, which is produced by
group_label():
library(prolfqua)
group_label("MI", "T0")## [1] "G_MI_T0"
group_label("MINOCA", "T300")## [1] "G_MINOCA_T300"
This means that before fitting a model, the data must contain a
grouping column with levels in this format. The
annotation_add_contrasts function creates such a column
automatically using tidyr::unite.
Building contrasts step by step
Consider a two-factor design with disease type (MI, MINOCA) and time point (T0, T150, T300):
Main effect contrasts
Main effects average across all levels of the secondary factor. For example, the main effect of MINOCA vs MI is the average difference across all time points:
me <- main_effect_contrasts(primary_levels, secondary_levels)
data.frame(ContrastName = names(me), Contrast = unlist(me))## ContrastName
## MINOCA_vs_MI MINOCA_vs_MI
## Contrast
## MINOCA_vs_MI ( (G_MINOCA_T0 + G_MINOCA_T150 + G_MINOCA_T300)/3 - (G_MI_T0 + G_MI_T150 + G_MI_T300)/3 )
Swapping the roles of primary and secondary gives main effects for time points averaged across disease types:
me2 <- main_effect_contrasts(secondary_levels, primary_levels)
data.frame(ContrastName = names(me2), Contrast = unlist(me2))## ContrastName
## T150_vs_T0 T150_vs_T0
## T300_vs_T0 T300_vs_T0
## T300_vs_T150 T300_vs_T150
## Contrast
## T150_vs_T0 ( (G_T150_MI + G_T150_MINOCA)/2 - (G_T0_MI + G_T0_MINOCA)/2 )
## T300_vs_T0 ( (G_T300_MI + G_T300_MINOCA)/2 - (G_T0_MI + G_T0_MINOCA)/2 )
## T300_vs_T150 ( (G_T300_MI + G_T300_MINOCA)/2 - (G_T150_MI + G_T150_MINOCA)/2 )
Level-specific contrasts
These compare primary factor levels at each individual level of the secondary factor:
ls <- level_specific_contrasts(primary_levels, secondary_levels)
data.frame(ContrastName = names(ls), Contrast = unlist(ls))## ContrastName Contrast
## MINOCA_vs_MI_at_T0 MINOCA_vs_MI_at_T0 G_MINOCA_T0 - G_MI_T0
## MINOCA_vs_MI_at_T150 MINOCA_vs_MI_at_T150 G_MINOCA_T150 - G_MI_T150
## MINOCA_vs_MI_at_T300 MINOCA_vs_MI_at_T300 G_MINOCA_T300 - G_MI_T300
Interaction contrasts
Interaction contrasts test whether the difference between primary levels changes across secondary levels (difference of differences):
ic <- interaction_contrasts(primary_levels, secondary_levels)
data.frame(ContrastName = names(ic), Contrast = unlist(ic))## ContrastName
## interaction_MINOCA_vs_MI_at_T150_vs_T0 interaction_MINOCA_vs_MI_at_T150_vs_T0
## interaction_MINOCA_vs_MI_at_T300_vs_T0 interaction_MINOCA_vs_MI_at_T300_vs_T0
## interaction_MINOCA_vs_MI_at_T300_vs_T150 interaction_MINOCA_vs_MI_at_T300_vs_T150
## Contrast
## interaction_MINOCA_vs_MI_at_T150_vs_T0 (G_MINOCA_T150 - G_MI_T150) - (G_MINOCA_T0 - G_MI_T0)
## interaction_MINOCA_vs_MI_at_T300_vs_T0 (G_MINOCA_T300 - G_MI_T300) - (G_MINOCA_T0 - G_MI_T0)
## interaction_MINOCA_vs_MI_at_T300_vs_T150 (G_MINOCA_T300 - G_MI_T300) - (G_MINOCA_T150 - G_MI_T150)
Single-factor contrasts
For a one-factor design, generate_contrasts_for_factor
generates all pairwise comparisons:
group_levels <- c("CondA", "CondB", "CondC")
sf <- generate_contrasts_for_factor(group_levels)
data.frame(ContrastName = names(sf), Contrast = unlist(sf))## ContrastName Contrast
## CondB_vs_CondA CondB_vs_CondA CondB - CondA
## CondC_vs_CondA CondC_vs_CondA CondC - CondA
## CondC_vs_CondB CondC_vs_CondB CondC - CondB
Generating all contrasts at once
generate_contrasts combines main effects,
level-specific, and interaction contrasts into a single data frame:
all_contrasts <- generate_contrasts(primary_levels, secondary_levels)
knitr::kable(all_contrasts, row.names = FALSE)| ContrastName | Contrast |
|---|---|
| MINOCA_vs_MI | ( (G_MINOCA_T0 + G_MINOCA_T150 + G_MINOCA_T300)/3 - (G_MI_T0 + G_MI_T150 + G_MI_T300)/3 ) |
| MINOCA_vs_MI_at_T0 | G_MINOCA_T0 - G_MI_T0 |
| MINOCA_vs_MI_at_T150 | G_MINOCA_T150 - G_MI_T150 |
| MINOCA_vs_MI_at_T300 | G_MINOCA_T300 - G_MI_T300 |
| interaction_MINOCA_vs_MI_at_T150_vs_T0 | (G_MINOCA_T150 - G_MI_T150) - (G_MINOCA_T0 - G_MI_T0) |
| interaction_MINOCA_vs_MI_at_T300_vs_T0 | (G_MINOCA_T300 - G_MI_T300) - (G_MINOCA_T0 - G_MI_T0) |
| interaction_MINOCA_vs_MI_at_T300_vs_T150 | (G_MINOCA_T300 - G_MI_T300) - (G_MINOCA_T150 - G_MI_T150) |
To exclude interaction contrasts:
no_int <- generate_contrasts(primary_levels, secondary_levels, interactions = FALSE)
knitr::kable(no_int, row.names = FALSE)| ContrastName | Contrast |
|---|---|
| MINOCA_vs_MI | ( (G_MINOCA_T0 + G_MINOCA_T150 + G_MINOCA_T300)/3 - (G_MI_T0 + G_MI_T150 + G_MI_T300)/3 ) |
| MINOCA_vs_MI_at_T0 | G_MINOCA_T0 - G_MI_T0 |
| MINOCA_vs_MI_at_T150 | G_MINOCA_T150 - G_MI_T150 |
| MINOCA_vs_MI_at_T300 | G_MINOCA_T300 - G_MI_T300 |
Working with annotation tables
In a typical prolfquapp workflow, you start with a sample annotation
table that has columns for the two factors.
annotation_add_contrasts creates a united Group column,
generates all contrasts, and binds them alongside the annotation:
# x5463yzwer453bbb is a bundled example annotation table
# with factor_A (MI / MINOCA) and factor_B (T0 / T150 / T300)
head(prolfqua::x5463yzwer453bbb[, c("Name", "Group", "factor_A", "factor_B")])## # A tibble: 6 × 4
## Name Group factor_A factor_B
## <chr> <chr> <chr> <chr>
## 1 MI_150_6 MI_T150 MI T150
## 2 MINOCA_0_3 MINOCA_T0 MINOCA T0
## 3 MINOCA_300_5 MINOCA_T300 MINOCA T300
## 4 MI_150_5 MI_T150 MI T150
## 5 MI_150_1 MI_T150 MI T150
## 6 MI_0_3 MI_T0 MI T0
result <- annotation_add_contrasts(
prolfqua::x5463yzwer453bbb,
primary_col = "factor_A",
secondary_col = "factor_B",
prefix = "primary"
)
# The annotation with united Group and contrast columns
knitr::kable(head(result$annot[, c("Name", "Group", "ContrastName", "Contrast")], 10),
row.names = FALSE)| Name | Group | ContrastName | Contrast |
|---|---|---|---|
| MI_150_6 | MI_T150 | MINOCA_vs_MI | ( (G_MINOCA_T0 + G_MINOCA_T150 + G_MINOCA_T300)/3 - (G_MI_T0 + G_MI_T150 + G_MI_T300)/3 ) |
| MINOCA_0_3 | MINOCA_T0 | MINOCA_vs_MI_at_T0 | G_MINOCA_T0 - G_MI_T0 |
| MINOCA_300_5 | MINOCA_T300 | MINOCA_vs_MI_at_T150 | G_MINOCA_T150 - G_MI_T150 |
| MI_150_5 | MI_T150 | MINOCA_vs_MI_at_T300 | G_MINOCA_T300 - G_MI_T300 |
| MI_150_1 | MI_T150 | interaction_MINOCA_vs_MI_at_T150_vs_T0 | (G_MINOCA_T150 - G_MI_T150) - (G_MINOCA_T0 - G_MI_T0) |
| MI_0_3 | MI_T0 | interaction_MINOCA_vs_MI_at_T300_vs_T0 | (G_MINOCA_T300 - G_MI_T300) - (G_MINOCA_T0 - G_MI_T0) |
| MINOCA_150_1 | MINOCA_T150 | interaction_MINOCA_vs_MI_at_T300_vs_T150 | (G_MINOCA_T300 - G_MI_T300) - (G_MINOCA_T150 - G_MI_T150) |
| MI_300_2 | MI_T300 | NA | NA |
| MINOCA_150_5 | MINOCA_T150 | NA | NA |
| MI_300_3 | MI_T300 | NA | NA |
# Suggested output file name
result$name## [1] "DEA_primary_dataset.csv"
Swapping primary and secondary factors gives contrasts from the other perspective:
result2 <- annotation_add_contrasts(
prolfqua::x5463yzwer453bbb,
primary_col = "factor_B",
secondary_col = "factor_A",
prefix = "secondary"
)
knitr::kable(head(result2$annot[, c("Name", "Group", "ContrastName", "Contrast")], 10),
row.names = FALSE)| Name | Group | ContrastName | Contrast |
|---|---|---|---|
| MI_150_6 | T150_MI | T150_vs_T0 | ( (G_T150_MI + G_T150_MINOCA)/2 - (G_T0_MI + G_T0_MINOCA)/2 ) |
| MINOCA_0_3 | T0_MINOCA | T300_vs_T0 | ( (G_T300_MI + G_T300_MINOCA)/2 - (G_T0_MI + G_T0_MINOCA)/2 ) |
| MINOCA_300_5 | T300_MINOCA | T300_vs_T150 | ( (G_T300_MI + G_T300_MINOCA)/2 - (G_T150_MI + G_T150_MINOCA)/2 ) |
| MI_150_5 | T150_MI | T150_vs_T0_at_MI | G_T150_MI - G_T0_MI |
| MI_150_1 | T150_MI | T150_vs_T0_at_MINOCA | G_T150_MINOCA - G_T0_MINOCA |
| MI_0_3 | T0_MI | T300_vs_T0_at_MI | G_T300_MI - G_T0_MI |
| MINOCA_150_1 | T150_MINOCA | T300_vs_T0_at_MINOCA | G_T300_MINOCA - G_T0_MINOCA |
| MI_300_2 | T300_MI | T300_vs_T150_at_MI | G_T300_MI - G_T150_MI |
| MINOCA_150_5 | T150_MINOCA | T300_vs_T150_at_MINOCA | G_T300_MINOCA - G_T150_MINOCA |
| MI_300_3 | T300_MI | interaction_T150_vs_T0_at_MINOCA_vs_MI | (G_T150_MINOCA - G_T0_MINOCA) - (G_T150_MI - G_T0_MI) |