StatProg2
  • Home
  • Syllabus
  • Group Project
  • Reflection Prompts
  • Setup

On this page

  • Quiz
  • Overview
  • Part 1: Sandbox — communicating palmerpenguins
    • Exercise 1.1: Table or plot?
    • Exercise 1.2: Build a gt summary table
    • Exercise 1.3: Polish a default plot
  • Part 2: Your project — communicate your results
    • Exercise 2.1: Table or plot?
    • Exercise 2.2: Build a gt table from your results
    • Exercise 2.3: Improve an existing project plot
    • Exercise 2.4: Write a caption that states the finding
    • Exercise 2.5: Share one improvement
  • Reflection log
    • End-of-practical checklist
  • Resources

Practical #10

Advanced Statistical Programming using R — Statistical Communication & Visualisation

Author

Leonhard Kestel, Lisa Bondo Andersen, Cynthia Huang

Published

June 26, 2026

Quiz

Before starting, work through this QUIZ to check your understanding of this week’s lecture on statistical communication & visualisation (with a short recap of last week).


Overview

In yesterday’s lecture you saw how to turn raw analysis output into something an audience can read: choosing between a table and a plot, building a clean table with {gt}, and applying design principles to a default ggplot2 figure. Today you put that into practice on real output — your project’s.

You do it twice. Part 1 is a sandbox: you rehearse the full workflow on the palmerpenguins dataset, where we give you solutions. Part 2 is the real thing: you produce a polished table and improve a plot from your own project, then share one improvement with the group.

ImportantThis session is an online drop-in

Work on the practical on your own and bring questions to the drop-in in this Zoom link. Lisa will be available on Friday from 2-4pm. We still recommend pairing up with your group members to work on the task. At least one person should have the project open in RStudio with a result to communicate when entering the call.

Budget roughly 2 hours: about 35 minutes on the sandbox and the rest on your own project.

Note

You will need gt and ggrepel. Install them inside your project (so renv records them) and snapshot afterwards:

install.packages(c("gt", "ggrepel"))
renv::snapshot()   # run in the R console

Part 1: Sandbox — communicating palmerpenguins

~35 min

{palmerpenguins} ships with penguins, measurements of 344 Antarctic penguins. You will make one table and polish one plot.

library(dplyr)
library(ggplot2)
library(gt)
library(palmerpenguins)

Exercise 1.1: Table or plot?

For each output below, decide whether a table or a plot communicates it better, and say why in a few words.

  1. The exact mean and SD of bill length for each of the three species
  2. Whether body mass and flipper length are related
  3. A preview of the first six rows of the raw data for a methods appendix
  4. How the distribution of bill length differs between species
NoteSolution
  • a — table. Exact values and a small comparison; readers want to read the numbers off.
  • b — plot. A relationship/pattern (a scatter plot) is read far better than a correlation buried in a table.
  • c — table. A data sample is about showing the values and structure, not a pattern.
  • d — plot. A distribution (histogram, density, or boxplot) shows shape and overlap that a table of summary stats hides.

Heuristic from the lecture: exact values or small comparisons → table; patterns, trends, or distributions → plot.

Exercise 1.2: Build a gt summary table

Summarise first, then pipe into gt(). Start from this and add the design layers:

penguins |>
  group_by(species) |>
  summarise(
    n         = n(),
    mean_bill = mean(bill_length_mm, na.rm = TRUE),
    sd_bill   = sd(bill_length_mm,   na.rm = TRUE),
    mean_mass = mean(body_mass_g,    na.rm = TRUE)
  ) |>
  gt()

Apply the table design rules from the lecture: format the numbers, put units in the column headers, right-align numbers, and write a subtitle that states the finding plus a source note.

NoteSolution
penguins |>
  group_by(species) |>
  summarise(
    n         = n(),
    mean_bill = mean(bill_length_mm, na.rm = TRUE),
    sd_bill   = sd(bill_length_mm,   na.rm = TRUE),
    mean_mass = mean(body_mass_g,    na.rm = TRUE)
  ) |>
  gt() |>
  fmt_number(c(mean_bill, sd_bill, mean_mass), decimals = 1) |>
  cols_label(
    species   = "Species",
    n         = md("*n*"),
    mean_bill = html("Mean bill<br>(mm)"),
    sd_bill   = html("SD bill<br>(mm)"),
    mean_mass = html("Mean mass<br>(g)")
  ) |>
  cols_align(align = "right", columns = where(is.numeric)) |>
  tab_header(
    title    = "Penguin measurements by species",
    subtitle = "Chinstrap penguins have the longest bills on average"
  ) |>
  tab_source_note(md("Source: `palmerpenguins` package"))

The general gt workflow builds up layer by layer: gt() → fmt_number() → cols_label() → cols_align() → tab_header() → tab_source_note(). Units live in the headers (mm, g) so the cells stay clean, and the subtitle states the finding rather than restating the column names.

Exercise 1.3: Polish a default plot

Here is a default ggplot2 scatter. Run it, then apply at least three design improvements from the lecture.

ggplot(penguins, aes(x = bill_length_mm, y = body_mass_g, color = species)) +
  geom_point()

Improvements to choose from: a colorblind-safe palette, a minimal theme, a title + subtitle that states the finding, axis labels with units, and a source caption.

NoteSolution
ggplot(penguins, aes(x = bill_length_mm, y = body_mass_g, color = species)) +
  geom_point(alpha = 0.8) +
  scale_color_brewer(palette = "Dark2") +
  theme_minimal() +
  labs(
    title    = "Bill length and body mass by species",
    subtitle = "Gentoo penguins are heavier and longer-billed than Adelie and Chinstrap",
    x        = "Bill length (mm)",
    y        = "Body mass (g)",
    color    = "Species",
    caption  = "Source: palmerpenguins package"
  )

Why these choices:

  • Position carries the message. The two most important variables are on the x/y axes — the most accurately-read encoding (Cleveland & McGill). Colour is reserved for the secondary grouping (species).
  • Colour done right. scale_color_brewer(palette = "Dark2") is a qualitative, colourblind-safe palette — right for unordered categories. Use scale_color_brewer(type = "seq") or type = "div" instead for ordered or diverging values.
  • The subtitle states the finding, not just what the axes show.
TipDirect labelling instead of a legend

With ≤ 5 groups, labelling the data directly cuts the eye-travel to a legend:

library(ggrepel)
last_plot() +
  geom_text_repel(
    data = ~slice_max(.x, bill_length_mm, by = species),
    aes(label = species), show.legend = FALSE
  ) +
  guides(color = "none")

Part 2: Your project — communicate your results

~75 min

Now do the same on your own output. By the end you should have one polished gt table and one improved plot committed to your project repo.

Open your project and pick one analysis result worth communicating — a summary you have computed, a model output, or a figure you have already drafted.

Exercise 2.1: Table or plot?

For the result you picked, decide whether it is better shown as a table or a plot, using the heuristic from Ex 1.1. If it is a small set of exact values or a sample, make a table (Ex 2.2). If it is a pattern, trend, or distribution, improve a plot (Ex 2.3). Most groups will do both on different results.

TipMatching chart type to your message
Goal Chart type
Compare values across categories Bar chart (horizontal preferred)
Show distribution of one variable Histogram, density, boxplot, violin
Show relationship between two variables Scatter plot, line chart
Show composition or part-of-whole Stacked bar, treemap
Show change over time Line chart, area chart
Show many variables at once Faceted plots, paired plots

Exercise 2.2: Build a gt table from your results

Produce at least one gt table from your project — a summary-statistics table or a data sample. Follow the workflow from Ex 1.2.

library(dplyr)
library(gt)

# Summary-stats example — adapt the columns to your data
your_data |>
  group_by(your_group) |>
  summarise(n = n(), mean_x = mean(your_value, na.rm = TRUE)) |>
  gt() |>
  fmt_number(where(is.numeric), decimals = 1) |>
  cols_label(your_group = "Group", mean_x = html("Mean x<br>(units)")) |>
  cols_align(align = "right", columns = where(is.numeric)) |>
  tab_header(title = "...", subtitle = "<the finding>") |>
  tab_source_note(md("Source: ..."))
Tip

Use tab_spanner() to group related columns under a shared header instead of repeating units in every label, and choose decimal places deliberately — match the precision that actually matters for your variable.

Exercise 2.3: Improve an existing project plot

Take a plot from your project (or draft one now) and apply at least two design improvements. Work through this output checklist:

TipPerceptual hierarchy — what readers read most accurately

Visual encodings ranked from most to least accurately perceived (Cleveland & McGill):

  1. Position on a common scale — the most accurately read
  2. Length
  3. Angle / slope
  4. Area
  5. Colour saturation / intensity — the least accurate

Put the most important variable on an axis. Reserve colour for a secondary grouping, and avoid using area or colour alone to encode a key comparison.

Exercise 2.4: Write a caption that states the finding

  1. Rewrite your table/figure caption so it states the conclusion, not the contents.
NoteFinding, not contents
  • ✗ “Bill length by species”
  • ✓ “Chinstrap penguins have longer bills on average”

The reader should be able to take the message from the caption alone. If you cannot write a one-sentence finding, the figure may be trying to say too much — split it (“one figure, one message”).

  1. Add an alternative text: make sure that your plot is accessible to visually impaired people, by providing all necessary information as alt-text. In Quarto, set it with the fig-alt chunk option:
```{r}
#| fig-alt: "A scatter plot of ..."
ggplot(...) + ...
```
TipDrafting alt text with an LLM

Writing good alt text is time-consuming. Emil Hvitfeldt shows how to use Claude Code to auto-generate fig-alt for all figures in a Quarto document in one pass — including running it directly on your project files.

See: emilhvitfeldt.com/post/claude-code-alt-text-quarto

Always review and adjust the generated alt text to make sure it matches your message.

Exercise 2.5: Share one improvement

Pick the single improvement you are most pleased with and share it — post the before/after in your README.md, and say in one sentence what changed and why. Then commit the table and plot to your project repo.

TipOral exam tip

In the oral exam we will likely ask you to walk us through a visualisation or table from your project and explain the choices you made. “I used a bar chart because I wanted to compare exact counts across groups” or “I moved species to position because colour is the least accurately-read encoding” is exactly what we are looking for — a design decision linked to a reason.


Reflection log

~10 min

Take a few minutes to add this week’s entry to your reflection log. Then commit and push everything — the new gt table, the improved plot, and the reflection log entry can all go in one commit.


End-of-practical checklist

Before you leave today, make sure your group has:


Resources

Visualisation

  • Fundamentals of Data Visualization (Wilke) — the design principles behind this week
  • ColorBrewer palettes — qualitative / sequential / diverging palette chooser
  • viridis color scales — perceptually-uniform, colourblind-safe scales

Tables

  • gt package documentation — the table workflow this practical follows
  • gtsummary package — publication-ready summary tables with less boilerplate
Source Code
---
title: "Practical #10"
subtitle: "Advanced Statistical Programming using R — Statistical Communication & Visualisation"
author: "Leonhard Kestel, Lisa Bondo Andersen, Cynthia Huang"
date: "June 26, 2026"
format:
  html:
    theme: default
    toc: true
    toc-depth: 2
    code-tools: true
    highlight-style: github
execute:
  eval: false
  message: false
  warning: false
---

# Quiz

Before starting, work through this [QUIZ](quiz.qmd){target="_blank"} to check your understanding of this week's lecture on statistical communication & visualisation (with a short recap of last week).

---

# Overview

In yesterday's lecture you saw how to turn raw analysis output into something an
audience can read: choosing between a table and a plot, building a clean table with
`{gt}`, and applying design principles to a default ggplot2 figure. Today you put
that into practice on real output — your project's.

You do it twice. **Part 1** is a sandbox: you rehearse the full workflow on the
`palmerpenguins` dataset, where we give you solutions. **Part 2** is the real thing:
you produce a polished table and improve a plot from your own project, then share one
improvement with the group.

::: {.callout-important}
## This session is an online drop-in
Work on the practical on your own and bring questions to the drop-in in this [Zoom link](https://lmu-munich.zoom-x.de/j/65274156117?pwd=heRGyyRHdtCcnzyZcyKjv69HchWxKW.1). Lisa will be available on Friday from 2-4pm. We still recommend pairing up with your group members to work on the task. At least one person should have the project open in RStudio with a result to communicate when entering the call.
:::

Budget roughly **2 hours**: about 35 minutes on the sandbox and the rest on your own
project.

::: {.callout-note}
You will need `gt` and `ggrepel`. Install them inside your project (so
`renv` records them) and snapshot afterwards:

```r
install.packages(c("gt", "ggrepel"))
renv::snapshot()   # run in the R console
```
:::

---

# Part 1: Sandbox — communicating `palmerpenguins`
*~35 min*

`{palmerpenguins}` ships with `penguins`, measurements of 344 Antarctic penguins.
You will make one table and polish one plot.

```r
library(dplyr)
library(ggplot2)
library(gt)
library(palmerpenguins)
```

## Exercise 1.1: Table or plot?

For each output below, decide whether a **table** or a **plot** communicates it
better, and say why in a few words.

a. The exact mean and SD of bill length for each of the three species
b. Whether body mass and flipper length are related
c. A preview of the first six rows of the raw data for a methods appendix
d. How the distribution of bill length differs between species

::: {.callout-note title="Solution" collapse="true"}
- **a — table.** Exact values and a small comparison; readers want to read the numbers off.
- **b — plot.** A relationship/pattern (a scatter plot) is read far better than a correlation buried in a table.
- **c — table.** A data sample is about showing the values and structure, not a pattern.
- **d — plot.** A distribution (histogram, density, or boxplot) shows shape and overlap that a table of summary stats hides.

Heuristic from the lecture: **exact values or small comparisons → table; patterns,
trends, or distributions → plot.**
:::

## Exercise 1.2: Build a `gt` summary table

Summarise first, then pipe into `gt()`. Start from this and add the design layers:

```r
penguins |>
  group_by(species) |>
  summarise(
    n         = n(),
    mean_bill = mean(bill_length_mm, na.rm = TRUE),
    sd_bill   = sd(bill_length_mm,   na.rm = TRUE),
    mean_mass = mean(body_mass_g,    na.rm = TRUE)
  ) |>
  gt()
```

Apply the table design rules from the lecture: format the numbers, put **units in the
column headers**, right-align numbers, and write a **subtitle that states the
finding** plus a source note.

::: {.callout-note title="Solution" collapse="true"}
```r
penguins |>
  group_by(species) |>
  summarise(
    n         = n(),
    mean_bill = mean(bill_length_mm, na.rm = TRUE),
    sd_bill   = sd(bill_length_mm,   na.rm = TRUE),
    mean_mass = mean(body_mass_g,    na.rm = TRUE)
  ) |>
  gt() |>
  fmt_number(c(mean_bill, sd_bill, mean_mass), decimals = 1) |>
  cols_label(
    species   = "Species",
    n         = md("*n*"),
    mean_bill = html("Mean bill<br>(mm)"),
    sd_bill   = html("SD bill<br>(mm)"),
    mean_mass = html("Mean mass<br>(g)")
  ) |>
  cols_align(align = "right", columns = where(is.numeric)) |>
  tab_header(
    title    = "Penguin measurements by species",
    subtitle = "Chinstrap penguins have the longest bills on average"
  ) |>
  tab_source_note(md("Source: `palmerpenguins` package"))
``` 


The general `gt` workflow builds up layer by layer:
`gt() → fmt_number() → cols_label() → cols_align() → tab_header() → tab_source_note()`.
Units live in the headers (`mm`, `g`) so the cells stay clean, and the subtitle
states the finding rather than restating the column names.
:::

## Exercise 1.3: Polish a default plot

Here is a default ggplot2 scatter. Run it, then apply **at least three** design
improvements from the lecture.

```r
ggplot(penguins, aes(x = bill_length_mm, y = body_mass_g, color = species)) +
  geom_point()
```

Improvements to choose from: a colorblind-safe palette, a minimal theme, a title +
**subtitle that states the finding**, axis labels with units, and a source caption.

::: {.callout-note title="Solution" collapse="true"}
```r
ggplot(penguins, aes(x = bill_length_mm, y = body_mass_g, color = species)) +
  geom_point(alpha = 0.8) +
  scale_color_brewer(palette = "Dark2") +
  theme_minimal() +
  labs(
    title    = "Bill length and body mass by species",
    subtitle = "Gentoo penguins are heavier and longer-billed than Adelie and Chinstrap",
    x        = "Bill length (mm)",
    y        = "Body mass (g)",
    color    = "Species",
    caption  = "Source: palmerpenguins package"
  )
```

Why these choices:

- **Position carries the message.** The two most important variables are on the x/y
  axes — the most accurately-read encoding (Cleveland & McGill). Colour is reserved
  for the secondary grouping (`species`).
- **Colour done right.** `scale_color_brewer(palette = "Dark2")` is a
  qualitative, colourblind-safe palette — right for unordered categories. Use
  `scale_color_brewer(type = "seq")` or `type = "div"` instead for ordered or diverging values.
- **The subtitle states the finding**, not just what the axes show.
:::

::: {.callout-tip title="Direct labelling instead of a legend" collapse="true"}
With ≤ 5 groups, labelling the data directly cuts the eye-travel to a legend:

```r
library(ggrepel)
last_plot() +
  geom_text_repel(
    data = ~slice_max(.x, bill_length_mm, by = species),
    aes(label = species), show.legend = FALSE
  ) +
  guides(color = "none")
```
:::

---

# Part 2: Your project — communicate your results
*~75 min*

Now do the same on your own output. By the end you should have **one polished `gt`
table** and **one improved plot** committed to your project repo.

Open your project and pick one analysis result worth communicating — a summary you
have computed, a model output, or a figure you have already drafted.

## Exercise 2.1: Table or plot?

For the result you picked, decide whether it is better shown as a table or a plot,
using the heuristic from Ex 1.1. If it is a small set of exact values or a sample,
make a table (Ex 2.2). If it is a pattern, trend, or distribution, improve a plot
(Ex 2.3). Most groups will do **both** on different results.

::: {.callout-tip title="Matching chart type to your message" collapse="true"}
| Goal | Chart type |
|---|---|
| Compare values across categories | Bar chart (horizontal preferred) |
| Show distribution of one variable | Histogram, density, boxplot, violin |
| Show relationship between two variables | Scatter plot, line chart |
| Show composition or part-of-whole | Stacked bar, treemap |
| Show change over time | Line chart, area chart |
| Show many variables at once | Faceted plots, paired plots |
:::

## Exercise 2.2: Build a `gt` table from your results

Produce at least one `gt` table from your project — a **summary-statistics** table or
a **data sample**. Follow the workflow from Ex 1.2.

```r
library(dplyr)
library(gt)

# Summary-stats example — adapt the columns to your data
your_data |>
  group_by(your_group) |>
  summarise(n = n(), mean_x = mean(your_value, na.rm = TRUE)) |>
  gt() |>
  fmt_number(where(is.numeric), decimals = 1) |>
  cols_label(your_group = "Group", mean_x = html("Mean x<br>(units)")) |>
  cols_align(align = "right", columns = where(is.numeric)) |>
  tab_header(title = "...", subtitle = "<the finding>") |>
  tab_source_note(md("Source: ..."))
```

::: {.callout-tip}
Use `tab_spanner()` to group related columns under a shared header instead of
repeating units in every label, and choose decimal places deliberately — match the
precision that actually matters for your variable.
:::

## Exercise 2.3: Improve an existing project plot

Take a plot from your project (or draft one now) and apply **at least two** design
improvements. Work through this output checklist:

- [ ] **Title** and a **subtitle that states the finding** (not just the axes)
- [ ] **Axis labels** with units; drop a redundant one with `y = NULL` if the title makes it clear
- [ ] **Data source caption**
- [ ] **Colour chosen for the variable type** — qualitative / sequential / diverging — and colourblind-safe
- [ ] **A clean theme** (`theme_minimal()` / `theme_bw()`)

::: {.callout-tip title="Perceptual hierarchy — what readers read most accurately" collapse="true"}
Visual encodings ranked from most to least accurately perceived (Cleveland & McGill):

1. **Position** on a common scale — the most accurately read
2. **Length**
3. **Angle / slope**
4. **Area**
5. **Colour saturation / intensity** — the least accurate

Put the most important variable on an axis. Reserve colour for a secondary grouping, and avoid using area or colour alone to encode a key comparison.
:::

## Exercise 2.4: Write a caption that states the finding

1. Rewrite your table/figure caption so it states the **conclusion**, not the contents.

::: {.callout-note title="Finding, not contents" collapse="true"}
- ✗ "Bill length by species"
- ✓ "Chinstrap penguins have longer bills on average"

The reader should be able to take the message from the caption alone. If you cannot
write a one-sentence finding, the figure may be trying to say too much — split it
("one figure, one message").
:::

2. Add an alternative text: make sure that your plot is accessible to visually impaired people, by providing all necessary information as alt-text. In Quarto, set it with the `fig-alt` chunk option:

````md
```{r}
#| fig-alt: "A scatter plot of ..."
ggplot(...) + ...
```
````

::: {.callout-tip title="Drafting alt text with an LLM" collapse="true"}
Writing good alt text is time-consuming. Emil Hvitfeldt shows how to use Claude Code to auto-generate `fig-alt` for all figures in a Quarto document in one pass — including running it directly on your project files.

See: [emilhvitfeldt.com/post/claude-code-alt-text-quarto](https://emilhvitfeldt.com/post/claude-code-alt-text-quarto/)

Always review and adjust the generated alt text to make sure it matches your message.
:::

## Exercise 2.5: Share one improvement

Pick the single improvement you are most pleased with and share it — post the
before/after in your README.md, and say in one sentence
what changed and why. Then commit the table and plot to your project repo.

::: {.callout-tip}
## Oral exam tip
In the oral exam we will likely ask you to walk us through a visualisation or table from your project and explain the choices you made. "I used a bar chart because I wanted to compare exact counts across groups" or "I moved species to position because colour is the least accurately-read encoding" is exactly what we are looking for — a design decision linked to a reason.
:::

---

# Reflection log
*~10 min*

Take a few minutes to add this week's entry to your
[reflection log](_reflection-prompts.qmd). Then commit and push everything — the new
`gt` table, the improved plot, and the reflection log entry can all go in one commit.

---

## End-of-practical checklist

Before you leave today, make sure your group has:

- [ ] Decided table-vs-plot for at least one project result
- [ ] At least one `gt` table from your project results, with units in headers, a finding subtitle, and a source note
- [ ] At least two design improvements applied to a project plot
- [ ] A caption that states the finding, not just the contents
- [ ] Shared one before/after improvement with the group
- [ ] This week's reflection log entry committed and pushed

---

# Resources

**Visualisation**

- [Fundamentals of Data Visualization](https://clauswilke.com/dataviz/) (Wilke) — the design principles behind this week
- [ColorBrewer palettes](https://colorbrewer2.org/) — qualitative / sequential / diverging palette chooser
- [viridis color scales](https://sjmgarnier.github.io/viridis/) — perceptually-uniform, colourblind-safe scales

**Tables**

- [`gt` package documentation](https://gt.rstudio.com/) — the table workflow this practical follows
- [`gtsummary` package](https://www.danieldsjoberg.com/gtsummary/) — publication-ready summary tables with less boilerplate