gtsummary and ggpubr for hypothesis testing
Produce Table 1, Table 2, and figures-with-p-values for a randomized cohort using gtsummary and ggpubr.
Slides
Exercise 1: Demographics and Table 1
Mode setup. The room is split into Group A and Group B. For Exercise 1, Group A works agentic — Claude Code reads your notebook, writes code, and runs it. Group B works manual — you write each step yourself; you may ask a chat LLM conceptual questions, but it does not act in your environment. Exercise 2 flips the modes.
Open notebooks/06-table-one.qmd in your Codespace. The wrapper question across both exercises: can you defend the analysis you would put in a paper?
Part 1 — Predict (no code)
For each non-pid column of table_01 (arm, smoker, age, education, sex), predict — in plain words, before running anything:
- How will
tbl_summary(by = arm)summarise it? Mean (SD)? Median (IQR)? Counts (%)? - Is the column skewed or symmetric, sparse or dense? That same judgment is what decides which test
add_p()reaches for.
Part 2 — Build
Build a publication-ready Table 1.
- Start with
table_01 |> select(-pid, -sex) |> tbl_summary(by = arm). Read the defaults. - Add
|> add_p(). Confirmadd_p()’s test choices against your Part 1 predictions. - Override
ageto use"{mean} ({sd})"andt.test. Run again and see what changed. - Polish: add
|> add_overall() |> bold_labels() |> modify_header(label ~ "**Characteristic**").
After each step, compare what changed in the output.
Part 3 — Reach (optional)
Re-do the Table 1 with by = smoker instead of by = arm. Same data, different grouping.
Discussion question: what question is this Table 1 now answering, and is that a question the paper needs answered?
Discussion
Once the room is back together:
- What was easy in your mode? What was hard?
- Did
add_p()pick what you predicted? Where was it wrong? - What would you change about gtsummary’s defaults?
Exercise 2: Figures with p-values
Mode flip. If you were agentic in Exercise 1, you are manual now; if you were manual, you are agentic. Same definitions as before — manual means you may ask a chat LLM conceptual questions but it does not touch your notebook.
Open notebooks/06-figures-with-pvalues.qmd in your Codespace.
Part 1 — Read three plots (no code)
The notebook shows three pre-built plots: the single-outcome ggboxplot (pH by arm at week 7), the six-cytokine facet_wrap panel, and the paired pre-post line plot. For each plot:
- What is the mark?
- What is the comparison being tested?
- What test is on it, and how do you know?
- How many rows of data made each mark?
Part 2 — Build
Build the four analyses, growing toward a publication-ready set.
ggboxplot(table_02 |> filter(time_point == "week_7"), x = "arm", y = "ph", fill = "arm") + stat_compare_means(method = "wilcox.test"). The visual form of Table 2 row “ph”.- Build the same plot in plain ggplot:
ggplot(...) + geom_boxplot() + stat_compare_means(method = "wilcox.test"). Confirm both give the same p-value. - The cytokine panel: filter
table_03to six chosen cytokines andlimits == "within limits", jointable_00to attacharmandtime_point, filter toweek_7, thenggplot + geom_boxplot + facet_wrap(~ cytokine, scales = "free_y") + stat_compare_means(method = "wilcox.test"). - The paired pre-post: filter
table_02to baseline and week_7,mutate(time_point = fct_relevel(time_point, "baseline", "week_7")), thenggplot + geom_line(aes(group = pid)) + geom_point + facet_wrap(~ arm) + stat_compare_means(method = "wilcox.test", paired = TRUE, comparisons = list(c("baseline", "week_7"))). - The adjusted analysis: join
table_01onto a week_7 slice oftable_02, fitlm(ph ~ arm + age + smoker), pipe totbl_regression().
Part 3 — Reach (optional)
Pick one of the four analyses and polish it for publication (axis labels, theme, panel ordering, p-value formatting, color palette).
Or, repeat the full analysis arc on nugent_score or crp_blood instead of ph. The pattern is identical; only the column name changes.
Discussion
Once the room is back together: pick one result from the five analyses you built that you would be willing to put in a paper. What is the test behind it, and how would you defend that choice to a skeptical reviewer?