--- title: "Two-Way Comparison Figures with ggcompare()" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Two-Way Comparison Figures with ggcompare()} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include = FALSE} # Every computed chunk needs datarium (the example data) and emmeans (the # pooled-model simple comparisons). Skip gracefully if either is absent so the # vignette still builds. has_deps <- requireNamespace("datarium", quietly = TRUE) && requireNamespace("emmeans", quietly = TRUE) knitr::opts_chunk$set( collapse = TRUE, comment = "#>", fig.width = 7, fig.height = 5, dpi = 96, eval = has_deps ) ``` ```{r not-available, echo = FALSE, eval = !has_deps, results = "asis"} cat("> This vignette needs the **datarium** and **emmeans** packages, which are", "not installed, so its output is not shown here.") ``` A common publication figure compares an outcome across **two** categorical factors at once -- for example a score across *gender* and *education level* -- and annotates it with the interaction from a two-way ANOVA plus pairwise comparisons within each group. `ggcompare()` assembles this figure in a single call. ## The one-call two-way figure Map the first factor to `x` and the second factor to `color` (or `fill`). That second mapping switches `ggcompare()` into its two-way mode. ```{r two-way} library(ggpubr) library(rstatix) data("jobsatisfaction", package = "datarium") ggcompare( jobsatisfaction, x = "gender", y = "score", color = "education_level" ) ``` The figure carries: - **dodged boxes** for the two factors, with jittered points and group means; - **pairwise comparison brackets** of the education levels *within each gender*; - a **subtitle** naming the two-way ANOVA interaction; and - a **caption** describing the pairwise test. In two-way mode the pairwise `method` defaults to `"emmeans_test"` (simple comparisons from the pooled model), `p.adjust.method` to `"bonferroni"`, and `hide.ns` to `TRUE`. Each of these is a default only -- pass the argument to override it. ## It matches the by-hand analysis The annotation reproduces the numbers you would compute by hand. The subtitle is the interaction row of the two-way ANOVA: ```{r anova} jobsatisfaction %>% anova_test(score ~ gender * education_level) ``` and the brackets are the pooled-model emmeans comparisons within each gender: ```{r pwc} jobsatisfaction %>% group_by(gender) %>% emmeans_test(score ~ education_level, p.adjust.method = "bonferroni") %>% select(gender, group1, group2, p.adj, p.adj.signif) ``` ## Simple main effects on the plot When the interaction is significant, interpret it by breaking it into **simple main effects** -- the effect of the focal factor *within each level* of the other. Group by the moderator and run a one-way `anova_test()`, passing the full two-way model as `error` so the test borrows the **pooled error term** from the whole design: ```{r simple-effects} model <- lm(score ~ gender * education_level, data = jobsatisfaction) sme <- jobsatisfaction %>% group_by(gender) %>% anova_test(score ~ education_level, error = model) sme ``` The pooled error is what keeps the denominator degrees of freedom at the full-model residual df -- here `F(2, 52)` for both genders. Dropping `error = model` would instead use each subset's own residuals, giving a different (smaller) df and a different F, so keep the `error` argument when you report a simple main effect. Layer those per-group F values onto the `ggcompare()` figure with a `geom_text()`, placing each label above its cluster's brackets: ```{r simple-effects-plot} p <- ggcompare( jobsatisfaction, x = "gender", y = "score", color = "education_level" ) # position each label just above that group's tallest bracket pwc <- jobsatisfaction %>% group_by(gender) %>% emmeans_test(score ~ education_level, p.adjust.method = "bonferroni") %>% add_xy_position(x = "gender") tops <- tapply(pwc$y.position, pwc$gender, max) sme_lab <- transform( sme, x = as.integer(factor(gender, levels = levels(jobsatisfaction$gender))), y = tops[as.character(gender)] + 0.4, label = sprintf( "F(%g,%g) = %.3g, %s", DFn, DFd, F, ifelse(p < 0.0001, "p < 0.0001", paste0("p = ", signif(p, 2))) ) ) p + geom_text( data = sme_lab, inherit.aes = FALSE, aes(x = x, y = y, label = label), size = 3, fontface = "italic" ) + scale_y_continuous(expand = expansion(mult = c(0.05, 0.28))) ``` The figure now reports itself completely: the interaction on top, each gender's simple main effect of education above its boxes, and which education levels differ within each gender marked by the brackets. ## Customizing Every element is overridable. For instance, compare the *genders within each education level* instead (swap the comparison direction with `pwc.group.by`), switch the base geometry, and show non-significant brackets: ```{r customize} ggcompare( jobsatisfaction, x = "gender", y = "score", color = "education_level", base = "violin", pwc.group.by = "legend.var", hide.ns = FALSE ) ``` You can also compare each group to a reference level (`ref.group`), choose a different pairwise `method`/`p.adjust.method`, or pass extra styling through `pwc.args`, `add.params` and `...` (forwarded to the base builder). See `?ggcompare` for the full argument list. ## Naming an effect on any plot The subtitle machinery is also available directly through `add_test_label()`. Given a plot with `x` and a second factor, `group.by` computes the two-way ANOVA and `effect` chooses which effect to report -- the interaction (default), a named term, or `"all"` for a multi-line label of both main effects and the interaction: ```{r add-test-label} p <- ggboxplot(jobsatisfaction, x = "gender", y = "score", color = "education_level") add_test_label(p, group.by = "education_level", effect = "all", type = "text") ```