Tools for a Reproducible, Shareable & Communicable Science

DiplΓ΄me Universitaire Public Health Data Science

Author
Published

October 25, 2026

Foreword and Instructions

Note🎯 Learning Objectives

By the end of this class you will be able to:

  1. Explain why reproducibility matters in science and identify the main barriers.
  2. Organize a data-analysis project so that any colleague (including your future self) can re-run it from scratch.
  3. Write a self-contained, reproducible dynamic document with Quarto that blends narrative text, code, and output together.
  4. Produce publication-quality visualizations with ggplot2 () or plotnine (), and save them programmatically.
  5. Audit your own project against the standard reproducibility checklist before sharing it.

Instructions: how to navigate this class

This class walks you through 4 Parts (augmented by this introduction and a concluding summary). Along the way, you will meet three different types of activity, identified by the icons below:

Icon Type Purpose
πŸ“– Targeted reading A short external resource to read (~5–10 min). Follow the links β€” don’t skip them.
πŸ’‘ Concept definition Key ideas with illustrations and worked examples.
πŸ§‘β€πŸ’» Hands-on exercise YOU write and run code yourself. Blocks are not pre-executed here.

Choose R or python , both languages are shown in the exercises via tabs but you only need to do one (according to your initial preferences and familiarity).

Total time needed: approx. ~3 hours. Do not hesitate to take a short break between each Part.


βš™οΈ Setup (to be completed before starting the class)

Install the latest version of β†’ https://cran.r-project.org/
Install the latest version of RStudio β†’ https://posit.co/download/rstudio-desktop/

Then paste the following lines into your console:

install.packages(c(
  "tidyverse",    # ggplot2, dplyr, readr, …
  "gapminder",    # our main dataset
  "renv",         # dependency management
  "here",         # portable file paths
  "quarto",       # render .qmd from R
  "knitr",        # knitting engine
  "sessioninfo"   # document your environment
))

Verify that Quarto is installed and available from the terminal: in the RStudio Terminal tab, type quarto --version.
The command should return a version number if everything works correctly (e.g.Β β‰₯ 1.4).

Install Python β‰₯ 3.10 β†’ https://www.python.org/downloads/
Install VS Code β†’ https://code.visualstudio.com/ (with the Python extension)
or use JupyterLab: pip install jupyterlab

Create and activate a virtual environment, then install packages:

# Create project folder and virtual environment
mkdir repro-phds && cd repro-phds
python -m venv .venv

# Activate (choose the one line right for your OS)
source .venv/bin/activate       # for macOS or Linux
# .venv\Scripts\activate       # for Windows (PowerShell)

# Install python libraries
pip install pandas plotnine jupyterlab quarto-cli pyprojroot

Verify Quarto is indeed available by running quarto --version in your terminal (it should return a version number).

Warning⏱ Before you start

If you run into installation errors, search the error message on the web: troubleshooting is a core data-science skill! Most errors have a quickly identifiable solution on Stack Overflow for instance… GenAI chatbots such as Vibe from Mistral AI can also be very helpful.


1 Reproducibility: what it is and why it matters?

⏱ ~45 minutes

1.1 πŸ“– Readings: the reproducibility crisis

NoteπŸ“– Targeted readings (~10 min)

Before you read, think about: Have you ever tried to re-run an analysis and gotten different numbers? Have you read a Methods section and thought β€œI have no idea how they actually did this”?

  1. Read the Nature News Feature β€œ1,500 scientists lift the lid on reproducibility” (Baker, 2016).
    Focus on the survey bar chart titled β€œWhat Factors Contribute to Irreproducible Research?” and the section β€œThe cause”:
    πŸ‘‰ https://www.nature.com/news/1-500-scientists-lift-the-lid-on-reproducibility-1.19970
    (~5 min)

  2. Skim the Turing Way overview page. Read β€œWhy this is important” and the bullet list of barriers:
    πŸ‘‰ https://book.the-turing-way.org/reproducible-research/overview
    (~5 min)

Reflection: In public health the stakes are especially high. Irreproducible findings can influence clinical guidelines or policy decisions. Can you recall one published COVID-19 study from 2020 that had to be retracted or substantially corrected? (Hint: look up β€œSurgisphere hydroxychloroquine Lancet” or β€œHydroxychloroquine and azithromycin Gautret Raoult International Journal of Antimicrobial Agents”)


1.2 πŸ’‘ The Reproducibility Spectrum

β€œReproducible” is used loosely, and several definitions co-exist in different scientific fields. In this class, we will use the definition below focusing on computational reproducibility:

Same data? Same code/method? What does it test?
Reproducible βœ… βœ… Can you re-run the analysis and get the exact same numbers?
Replicable ❌ βœ… Does the same method generalise to new data?
Robust βœ… ❌ Do different analytical choices reach the same conclusion?
Generalisable ❌ ❌ Does the finding hold beyond the original context?

The diagram below (from The Turing Way community) relates these 4 levels to one another:

A 2x2 matrix showing reproducible, replicable, robust and generalisable research across axes of same/different data and same/different code

Reproducibility matrix. The Turing Way / Scriberia β€” CC-BY 4.0. Source: https://book.the-turing-way.org

1.3 πŸ’‘ Why reproducibility matters

What is the value of a non reproducible article?
If it isn’t reproducible, is it science?
Should we just trust each other?

Researchers are accountable for their scientific results, while public funding demands accountability. Scientific credibility and each researcher’s professionalism rest on this. Working reproducibly, by making one’s methods and results verifiable, helps meet these expectations.

  • πŸ“™ Scientific journals require some level of reproducibility: peer reviewers no longer just read and assess manuscripts, they now also check the code, the data and the computational workflows/pipelines before final acceptance for publication.
  • πŸ›‘οΈ Reproducibility acts as a methodological shield: it reduces the likelihood of undetected errors and spurious findings.
  • πŸ‡ͺπŸ‡ΊπŸ‡«πŸ‡· Institutional law: the European Union, the French National Research Agency (ANR) and the French High Council for the Evaluation of Research and Higher Education (HcΓ©res), among others, all require some level of reproducibility for the research they fund.
  • 🧱 Increased impact: reproducible articles are cited and reused more (they provide more trustworthy foundations for future works).

In public health, reproducibility carries an additional importance:

  • Policy decisions are made from published findings. A wrong or irreproducible result can harm patients and populations at scale.
  • Publicly-funded research should be accountable: if the public paid for the study, the code and data should (where possible) be publicly accessible. This idea is very much related to open-science, a concept connected to reproducibility, but different (➑️🌐 more details here)

Barriers, motivations and solutions

Barrier Concrete example Practical solution
Cultural β€œMy PI never does this” Frame it as a career investment, not overhead
Technical Simulation takes 3 days to run Store pre-computed intermediate results; provide a fast reduced-run mode
Legal Patient data under GDPR Generate synthetic data of same structure; grant temporary restricted auditor access
Time β€œI’ll clean up the code later” Start early: it costs extra work and time upfront, but saves a lot of time during later revisions

Reproducibility is a safeguard, not a burden

Reproducibility and its testimonies strengthen trust. Indeed, scientists who care about the reproducibility of their work are more efficient in the long term: they can build on their own past work more easily.

Reproducibility benefits science at several levels:

Benefits of reproducibility at 3 different levels
🌍 Research field πŸ₯ Research group πŸ§‘β€πŸ”¬ Yourself
Stronger methodological credibility Faster transmission to collaborators Faster article completion & revisions
Cumulative, extendable knowledge Reduced technical debt Transparent & trustable for audience
Lower risk of published errors Clear, defensible archival Hard skills & efficient workflow
More citations
ImportantπŸ”‘ Key takeaways
  • Reproducibility is a scientific requirement: a scientific result that cannot be reproduced has very little (if any) added value. In this class, we focus on computational reproducibility, which is only one of the layers needed to reproduce science (think also data generation for instance).
  • Reproducibility is a continuum: universal, absolute, forever reproducibility is an ideal that is out of reach. Nonetheless, we should strive for improving practical reproducibility thinking about future reuse of our work and how we establish trust in scientific results.

1.4 πŸ§‘β€πŸ’» Exercise 1: Create Your Reproducible Project Skeleton

TipπŸ§‘β€πŸ’» Exercise 1: Project Setup (25 min)

Goal: Create a well-organised, portable project that you will build on throughout this class.


Step 1: Create a project

  1. Open RStudio β†’ File β†’ New Project… β†’ New Directory β†’ New Project

  2. Name it repro-phds, choose a folder you will remember

  3. βœ… Tick β€œUse renv with this project” β€” this sets up dependency tracking automatically

  4. Click Create Project

  5. In the console of the new project, make the packages installed during the setup available to it (the project’s renv library is empty at first):

    renv::hydrate(c("tidyverse", "gapminder", "here", "quarto", "knitr", "rmarkdown", "sessioninfo"))

Notice the .Rproj file in the Files pane. Whenever you open this file, RStudio automatically sets the working directory to your project root. This alone eliminates the vast majority of path-related bugs.

# If you have not done so previously, create the directory and the environment below
mkdir repro-phds && cd repro-phds

# Create and activate a virtual environment
python -m venv .venv
source .venv/bin/activate          # macOS/Linux – run the line corresponding to your OS
.venv\Scripts\activate           # Windows – run the line corresponding to your OS

# Start Python
python

Step 2: Create a standard folder structure

Run this in your console (inside the project):

dirs <- c(
  "data/raw",           # Original data β€” NEVER overwrite
  "data/processed",     # Cleaned / transformed data
  "R",                  # Reusable R functions
  "analysis",           # .qmd scripts go here
  "output/figures",     # Saved plots  β†’ fig01_..., fig02_...
  "output/tables"       # Exported tables β†’ tab01_..., tab02_...
)

invisible(lapply(dirs, dir.create, recursive = TRUE, showWarnings = FALSE))
cat("βœ… Folders created!\n")

Run the following python code (with .venv active, inside repro-phds/):

import os

dirs = [
    "data/raw",
    "data/processed",
    "src",               # Python source files / modules
    "analysis",          # Notebooks or scripts
    "output/figures",
    "output/tables"
]

for d in dirs:
    os.makedirs(d, exist_ok=True)

print("βœ… Folders created!")

Your project’s structure should now look like this:

repro-phds/
β”œβ”€β”€ data/
β”‚   β”œβ”€β”€ raw/           ← original data  (treat as read-only)
β”‚   └── processed/     ← cleaned data
β”œβ”€β”€ R/  (or src/)      ← reusable functions / modules
β”œβ”€β”€ analysis/          ← your scripts and notebooks
β”œβ”€β”€ output/
β”‚   β”œβ”€β”€ figures/       ← fig01_name.pdf, fig01_name.png, …
β”‚   └── tables/        ← tab01_name.csv, …
└── README.md          ← you'll write this next

Step 3: Write a README.md

Create README.md at the project root. A README is the first thing anyone β€” including future-you β€” reads. It must answer: What is this? How do I run it?

Copy the template below into your README.md and fill in the blanks:

# repro-phds β€” Reproducibility Practice (Public Health)

## Description
A self-contained data analysis project produced during the
"Tools for Reproducible Science" class (DU Public Health Data Science, [Year]).

## How to reproduce all results

1. Open `repro-phds.Rproj` in RStudio
   (or, in Python, create then activate the virtual environment:
   `python -m venv .venv` then `source .venv/bin/activate`).
2. Restore dependencies:
   - R: run `renv::restore()` in the console
   - Python: `pip install -r requirements.txt`
3. Render the main report:
   open `analysis/report.qmd` and click **Render**,
   or run `quarto render analysis/report.qmd`.

## Expected runtime
< 1 minute on a standard laptop.

## Session information
(Paste here the output of `sessioninfo::session_info()` for R, or the output of `pip freeze` for python, **after** completing Exercise 4 in Part 4.)

Step 4: Lock your dependencies

renv was already initialised when you created the project.
After installing packages, record their exact versions:

renv::snapshot()
# β†’ "renv.lock has been updated."

This file is your insurance against β€œit worked last year” bugs.
A collaborator who runs renv::restore() will get the exact same packages.

In the terminal, execute the following commands:

pip freeze > requirements.txt

# Verify it was created
cat requirements.txt

A collaborator can restore this exact environment with:

pip install -r requirements.txt

βœ… Self-check

Before moving on to the next part, check that you have completed the following steps, and that you understand each of their purpose and role:

  • getwd() () or os.getcwd() () returns the path to repro-phds/
  • The six sub-folders exist in the files explorer / pane
  • README.md is at the project root and has all three sections
  • renv.lock () or requirements.txt () has been created
  • Closing and reopening the project works cleanly

Question to think about: Why should data/raw/ be treated as strictly read-only? What would happen if you accidentally saved a modified version over your original data file?


2 Literate Programming with Quarto

⏱ ~45 minutes

2.1 πŸ“– Reading: One Document to Rule Them All

NoteπŸ“– Targeted Readings (5 min)

Donald Knuth introduced literate programming in the 1980s: instead of code with comments intended to be read primarily by computers, write a narrative document that contains the code intended for humans. The resulting document is both the analysis and its report.

  1. Read the Quarto β€œHello, Quarto” guide β€” try to render the example document if you can:
    πŸ‘‰ https://quarto.org/docs/get-started/hello/rstudio.html

  2. Keep the Quarto cheatsheet open as a reference throughout this block:
    πŸ‘‰ https://rstudio.github.io/cheatsheets/quarto.pdf

The central idea: When figures and numbers are computed inside your .qmd file, there is no copy-paste step where a stale number can slip through. Re-run the document and every value updates automatically.


2.2 πŸ’‘ Anatomy of a .qmd File

A Quarto file has three building blocks:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  1. YAML header     -> title, author, output type...    β”‚
│─────────────────────────────────────────────────────────│
β”‚  2. Markdown text   -> prose, headings, lists, links    β”‚
│─────────────────────────────────────────────────────────│
β”‚  3. Code chunks     -> R or Python code + its output    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Quarto schematics

A colourful illustration showing wizards casting spells to turn R code into beautiful output documents

FigureΒ 1: Quarto (like its predecessor, Rmarkdown) lets you blend code and narrative text into a single dynamic document. Artwork by Allison Horst β€” CC-BY 4.0.

1. The YAML header

A YAML header sits at the very beginning of the document, between two lines containing only ---. It defines, among other things, the output settings and the overall format of the document. Here is an example. Be careful: indentation matters in YAML.

---
title: "Global Health Trends β€” Gapminder Data"
author: "Your Name"
date: today                   # auto-fills today's date
format:
  html:
    toc: true                 # table of contents
    code-fold: true           # hide code by default (reader can expand)
    embed-resources: true     # self-contained `HTML` file (portable)
execute:
  echo: true                  # show code in output by default
  warning: true               # keep showing warnings
  message: false              # suppress messages (eg when loading packages)
---

2. Markdown text

Between code chunks, narrative text is written in Markdown (.md), a lightweight markup language. Here are a few syntax basics:

# Heading level 1
## Heading level 2

A paragraph. Make text **bold**, *italic*, or `code-styled`.

- Bullet list item one
- Bullet list item two

1. Numbered item one
2. Numbered item two

[Descriptive link text](https://url.com)
![Image alt text](path/to/image.png){width=50%}

3. Code chunks

These chunks are enclosed in triple back-ticks ```, followed by {r} or {python} (for instance) to specify the programming language interpreter to use. Chunk-specific options are indicated by setting values to specific keywords at the beginning of the code chunk with the following syntax: #| keyword: value (which can override the defaults set in the header if needed). Below is a code chunk example, followed by its output:

```{r}
#| label: summary-table-r
#| echo: true
#| eval: true

library(gapminder)
head(gapminder)
```
# A tibble: 6 Γ— 6
  country     continent  year lifeExp      pop gdpPercap
  <fct>       <fct>     <int>   <dbl>    <int>     <dbl>
1 Afghanistan Asia       1952    28.8  8425333      779.
2 Afghanistan Asia       1957    30.3  9240934      821.
3 Afghanistan Asia       1962    32.0 10267083      853.
4 Afghanistan Asia       1967    34.0 11537966      836.
5 Afghanistan Asia       1972    36.1 13079460      740.
6 Afghanistan Asia       1977    38.4 14880372      786.

Key chunk options (written after #| inside the chunk):

Option Default Effect
echo true Show the code in output
eval true Run the code
include true Include the chunk output
message true Show package-load messages
warning true Show warnings
cache false Cache results (useful for slow code)
label β€” Name for cross-referencing figures/tables
fig-cap β€” Figure caption
fig-width / fig-height β€” Figure dimensions in inches

Inline code

You can also insert automatically computed values inline, directly into the text, using `r ` or `{python} `:

The dataset covers `r length(unique(gapminder$country))` countries from `r min(gapminder$year)` to `r max(gapminder$year)`

When rendered, this becomes:

The dataset covers 142 countries from 1952 to 2007

✨ No copy-paste, no stale numbers, no manual update!


2.3 πŸ§‘β€πŸ’» Exercise 2: Your First Reproducible Quarto Report

TipπŸ§‘β€πŸ’» Exercise 2: First Quarto report (25 min)

Goal: Write a reproducible .qmd report that loads real global health data, computes summaries with inline values, and renders to a self-contained HTML file.

We use the Gapminder dataset: life expectancy, population, and GDP per capita for 142 countries from 1952 to 2007. Life expectancy is a central metric in public health β€” it integrates mortality at all ages and reflects overall health system performance.


Step 1: Create the file

In RStudio: File β†’ New File β†’ Quarto Document….
Delete all template content. Save as analysis/report.qmd.

Paste the YAML header below that matches the language you use ( or ):

---
title: "Global Health Trends β€” Gapminder Analysis"
author: "Your Name"
date: today
format:
  html:
    toc: true
    code-fold: true
    theme: flatly
    embed-resources: true
execute:
  echo: true
  warning: false
  message: false
---
---
title: "Global Health Trends β€” Gapminder Analysis"
author: "Your Name"
date: today
format:
  html:
    toc: true
    code-fold: true
    theme: flatly
    embed-resources: true
execute:
  echo: true
  warning: false
  message: false
jupyter: python3
---

Step 2: The setup chunk (always the very first chunk!)

A setup chunk runs silently before anything else. It is a good place to set your random seed for instance.

```{r}
#| label: setup-r
#| include: false

set.seed(20260401)      # ← ensures the reproducibility of any downstream random operations

library(ggplot2)
library(dplyr)
library(gapminder)
library(here)
library(knitr)
```
```{python}
#| label: setup-py
#| include: false

import urllib.request
import numpy as np
import pandas as pd
from pyprojroot import here

np.random.seed(20260401)

# Download Gapminder into data/raw/ (then read it from that folder)
url = "https://raw.githubusercontent.com/jennybc/gapminder/main/inst/extdata/gapminder.tsv"
raw_file = here("data/raw/gapminder.tsv")
if not raw_file.exists():
    urllib.request.urlretrieve(url, raw_file)
gapminder = pd.read_csv(raw_file, sep="\t")
```

Step 3: Data section with inline values

## The Data

The **Gapminder** dataset is a global development database compiled 
by the Gapminder Foundation. It covers health, population and wealth 
indicators for **`r length(unique(gapminder$country))` countries** 
across **`r length(unique(gapminder$year))` time points**
(from `r min(gapminder$year)` to `r max(gapminder$year)`).

```{r}
#| label: tbl-preview-r
#| tbl-cap: "First 6 rows of the Gapminder dataset."

head(gapminder) |> knitr::kable()
```
## The Data

The **Gapminder** dataset is a global development database compiled 
by the Gapminder Foundation. It covers health, population and wealth 
indicators for **`{python} gapminder["country"].nunique()` countries** 
across **`{python} len(sorted(gapminder["year"].unique()))` time points**
(from `{python} int(gapminder["year"].min())` to `{python} int(gapminder["year"].max())`).

```{python}
#| label: tbl-preview-py

gapminder.head(6)
```

Step 4: Summary statistics section

## Summary Statistics

```{r}
#| label: tbl-continent-2007-r
#| tbl-cap: "Life expectancy and GDP per capita by continent in 2007."

gapminder |>
  filter(year == 2007) |>
  group_by(continent) |>
  summarise(
    `N countries`         = n(),
    `Mean life exp.`      = round(mean(lifeExp), 1),
    `Median life exp.`    = round(median(lifeExp), 1),
    `Mean GDP/capita`     = round(mean(gdpPercap), 0)
  ) |>
  knitr::kable(format.args = list(big.mark = ","))
```

In 2007, the global mean life expectancy was
**`r round(mean(dplyr::filter(gapminder, year == 2007)$lifeExp), 1)` years**,
with values ranging from
`r round(min(dplyr::filter(gapminder, year == 2007)$lifeExp), 1)` to
`r round(max(dplyr::filter(gapminder, year == 2007)$lifeExp), 1)` years.
## Summary Statistics

```{python}
#| label: tbl-continent-2007-py

gapminder.query("year == 2007") \
.groupby("continent") \
.agg(
    n_countries=("country", "nunique"),
    mean_lifeExp=("lifeExp", "mean"),
    median_lifeExp=("lifeExp", "median"),
    mean_gdpPercap=("gdpPercap", "mean")
) \
.round(1) \
.reset_index() \
.rename(columns={
    "continent": "Continent",
    "n_countries": "N Countries",
    "mean_lifeExp": "Mean Life Exp.",
    "median_lifeExp": "Median Life Exp.",
    "mean_gdpPercap": "Mean GDP/capita"
})
```

Step 5: Render!

Click the blue Render button (top of the editor pane), or press Ctrl+Shift+K / Cmd+Shift+K.

Or from the console:

quarto::quarto_render("analysis/report.qmd")

In the terminal

quarto render analysis/report.qmd

Open the resulting report.html in your browser. Check that:

  • Inline values (number of countries, year range) appear correctly in the prose
  • The summary table has a caption
  • The β€œCode” toggle (from code-fold: true) works for each chunk

βœ… Self-check

  • The HTML file renders without errors
  • The inline numbers in the text match the table values
  • Changing year == 2007 to year == 1997 and re-rendering updates the values automatically
  • You can open report.html without RStudio or any software (it is self-contained)

Bonus: Add a ## Introduction section above ## The Data with two or three sentences on why life expectancy is a meaningful public health indicator. Try adding a footnote using [^1] syntax and a citation using a bibliography .bib file together with a .csl style file (controlling the citation format).


3 Data Visualisation

⏱ ~40 minutes

3.1 πŸ“– Reading: The Grammar of Graphics

NoteπŸ“– Targeted Readings (5 min)

Rather than memorizing dozens of plot types, you learn a grammar β€” a set of composable rules. Any statistical graphic can be described as data mapped to visual properties (position, colour, size, shape), rendered through geometric objects, on a coordinate system.

  1. Skim the ggplot2 homepage and the first β€œGetting Started” example:
    πŸ‘‰ https://ggplot2.tidyverse.org/
    (For users, read the introduction to plotnine instead, which follows the same grammar:
    πŸ‘‰ https://plotnine.org/guide/introduction.html)

  2. Read Hadley Wickham’s summary of Wilkinson’s The Grammar of Graphics presented here:
    πŸ‘‰ https://vita.had.co.nz/papers/layered-grammar.html (read only the β€œAbstract” and β€œIntroduction” sections)

Key idea: Once you understand the grammar, you can quickly build almost any chart by composing (modular) layers. You no longer need to search for β€œhow do I make a violin plot in ?” at every turn.


3.2 πŸ’‘ Building Plots Layer by Layer

The seven layers

Component In ggplot2 Example
Data ggplot(data = ...) gapminder |> filter(year == 2007)
Aesthetics aes(x, y, colour, size, shape) aes(x = gdpPercap, y = lifeExp, colour = continent)
Geometry geom_*() geom_point(), geom_line(), geom_boxplot()
Scales scale_*() scale_x_log10(), scale_colour_viridis_d()
Facets facet_wrap() / facet_grid() facet_wrap(~ continent)
Theme theme_*() theme_minimal()
Labels labs() labs(title = "...", x = "...", caption = "...")

An artistic illustration of a person building a layered ggplot2 graphic

Every ggplot2 plot is built by layering these components. Artwork by Allison Horst β€” CC-BY 4.0.

A complete worked example

library(ggplot2)
library(gapminder)
library(dplyr)

gap_2007 <- gapminder |> filter(year == 2007)

ggplot(
  data = gap_2007,
  aes(x = gdpPercap, y = lifeExp,
      colour = continent,
      size   = pop)          # bubble size = population
) +
  geom_point(alpha = 0.7) +
  scale_x_log10(labels = scales::dollar_format()) +   # log GDP axis
  scale_colour_viridis_d(option = "plasma") +         # colour-blind safe
  scale_size(range = c(2, 14), guide = "none") +
  labs(
    title    = "Wealth and Health in 2007",
    subtitle = expression("Each bubble is a country; size" %prop% "population"),
    x        = "GDP per capita (log scale, USD)",
    y        = "Life expectancy (years)",
    colour   = "Continent",
    caption  = "Source: Gapminder Foundation"
  ) +
  theme_minimal(base_size = 13)
from plotnine import (ggplot, aes, geom_point, scale_x_log10, scale_size,
                      scale_colour_cmap_d, labs, theme_minimal)
from mizani.labels import label_dollar

gap_2007 = gapminder[gapminder["year"] == 2007]

(
    ggplot(gap_2007,
           aes(x="gdpPercap", y="lifeExp",
               colour="continent",
               size="pop"))                                    # bubble size = population
    + geom_point(alpha=0.7)
    + scale_x_log10(labels=label_dollar(precision=0, big_mark=","))  # log GDP axis
    + scale_colour_cmap_d("plasma")                            # colour-blind safe
    + scale_size(range=(2, 14), guide=None)
    + labs(
        title="Wealth and Health in 2007",
        subtitle="Each bubble is a country; size ∝ population",
        x="GDP per capita (log scale, USD)",
        y="Life expectancy (years)",
        colour="Continent",
        caption="Source: Gapminder Foundation"
    )
    + theme_minimal(base_size=13)
)

Saving figures programmatically

Warning⚠️ Never use the β€œExport” button

Saving a figure manually (by right-clicking or clicking β€œExport” in the Plots pane) is not reproducible: the size, resolution, and format vary each time, and the step is not recorded in your code (it remains invisible).

Always save figures inside your script, with fixed dimensions and resolution.

# 1. Assign your plot to a named variable
p <- ggplot(gap_2007, aes(...)) + ...

# 2. Save: provide either a vector format (e.g. `.PDF`) or a raster format (e.g. `.PNG`) depending on the nature of your graph
ggsave(
  filename = here::here("output", "figures", "fig01_wealth_health_2007.pdf"),
  plot     = p,
  width    = 10, height = 6, units = "in"
)
ggsave(
  filename = here::here("output", "figures", "fig01_wealth_health_2007.png"),
  plot     = p,
  width    = 10, height = 6, units = "in",
  dpi      = 300          # 300 dpi = publication quality
)
from pyprojroot import here

# 1. Assign your plot to a named variable
p = ggplot(gap_2007, aes(...)) + ...
    ... your plotting code ...

# 2. Save in a vector format (e.g. `.PDF`) or a raster format (e.g. `.PNG`) depending on the nature of your graph
p.save(
    here("output/figures/fig01_wealth_health_2007.pdf"),
    width=10, height=6, units="in", verbose=False
)
p.save(
    here("output/figures/fig01_wealth_health_2007.png"),
    width=10, height=6, units="in",
    dpi=300, verbose=False      # 300 dpi = publication quality
)

File naming convention: use a numeric prefix consistent with the order of figures and tables in your document β€” fig01_, fig02_, …, tab01_, tab02_, …

Choosing accessible colour palettes

About 8 % of men in France have some form of colour-vision deficiency (according to the SociΓ©tΓ© FranΓ§aise d’Ophtalmologie). Here are a few palettes designed to be perceptually uniform and colour-blind safe:

Palette Package /library Use case
viridis, plasma, magma viridis / built-in ggplot2 Sequential / continuous
scale_colour_viridis_d() ggplot2 Discrete categorical
scale_colour_brewer(palette = "Set2") ggplot2 Categorical (up to 8 groups)
scale_colour_cmap_d("viridis") plotnine Discrete categorical
scale_colour_cmap("viridis") plotnine Continuous colour mapping

3.3 πŸ§‘β€πŸ’» Exercise 3: Figures

TipπŸ§‘β€πŸ’» Exercise 3: Your first figure (20 min)

Goal: Add two publication-quality figures to analysis/report.qmd and save them programmatically to output/figures/.


Figure 2: Distribution of life expectancy in 2007

Add a new section ## Distribution in 2007 to your .qmd file:

## Distribution in 2007

As of 2007, the distribution of life expectancy varies widely within
each continent (see @fig-dist-r).

```{r}
#| label: fig-dist-r
#| fig-cap: "Life expectancy by continent in 2007. Violin = density; points = individual countries; white bar = median."
#| fig-width: 9
#| fig-height: 5

p_dist <- gapminder |>
  filter(year == 2007) |>
  ggplot(aes(
    x      = reorder(continent, lifeExp, FUN = median),
    y      = lifeExp,
    fill   = continent,
    colour = continent
  )) +
  geom_violin(alpha = 0.35, linewidth = 0.4) +
  geom_jitter(width = 0.12, alpha = 0.8, size = 2.2) +
  stat_summary(fun = median, geom = "crossbar",
               width = 0.45, colour = "white", linewidth = 0.9) +
  scale_fill_viridis_d(option = "plasma") +
  scale_colour_viridis_d(option = "plasma") +
  labs(
    title   = "Life Expectancy Distribution by Continent in 2007",
    x       = "Continent (ordered by median life expectancy)",
    y       = "Life expectancy (years)",
    caption = "Source: Gapminder. Each point = one country."
  ) +
  theme_minimal(base_size = 13) +
  theme(legend.position = "none")

p_dist

ggsave(
  here::here("output", "figures", "fig03_distribution_2007.pdf"),
  plot = p_dist, width = 9, height = 5
)
```
## Distribution in 2007

As of 2007, the distribution of life expectancy varies widely within
each continent (see @fig-dist-py).

```{python}
#| label: fig-dist-py
#| fig-cap: "Life expectancy by continent in 2007."

import numpy as np
from plotnine import (ggplot, aes, geom_violin, geom_jitter, stat_summary,
                      scale_fill_cmap_d, scale_colour_cmap_d, labs,
                      theme_minimal, theme)

gap_2007 = gapminder[gapminder["year"] == 2007]

p_dist = (
    ggplot(gap_2007, aes(
        x="reorder(continent, lifeExp, np.median)",
        y="lifeExp",
        fill="continent",
        colour="continent"
    ))
    + geom_violin(alpha=0.35, size=0.4)
    + geom_jitter(width=0.12, alpha=0.8, size=2.2)
    + stat_summary(fun_y=np.median, fun_ymin=np.median, fun_ymax=np.median,
                   geom="crossbar", width=0.45, colour="white", size=0.9)
    + scale_fill_cmap_d("plasma")
    + scale_colour_cmap_d("plasma")
    + labs(
        title="Life Expectancy Distribution by Continent in 2007",
        x="Continent (ordered by median life expectancy)",
        y="Life expectancy (years)",
        caption="Source: Gapminder. Each point = one country."
    )
    + theme_minimal(base_size=13)
    + theme(legend_position="none", figure_size=(9, 5))
)

p_dist.save(
    here("output/figures/fig03_distribution_2007.pdf"),
    width=9, height=5, verbose=False
)

p_dist
```

Cross-referencing in the narrative

In your report, reference figures by their label (replace @fig-trends-r with @fig-trends-py if you are using ):

Life expectancy has grown dramatically since 1952 (see @fig-trends-r),
although large disparities persist between and within continents
(@fig-dist-r).

βœ… Self-check

  • Both figures appear in the rendered HTML
  • Two PDF files exist in output/figures/ with descriptive names
  • File names use the fig01_, fig02_ prefix convention
  • Both figures use a colour-blind-safe palette
  • Each figure has an informative fig-cap

Bonus challenge: Add a 3rd figure: a scatter plot for Africa only showing how GDP per capita relates to life expectancy in each decade. Is the relationship the same as globally? Does it change over time?


4 Sharing & the Reproducibility Checklist

⏱ ~35 minutes

4.1 πŸ“– Reading β€” The Journal Audit

NoteπŸ“– Targeted Readings (5 min)

The Biometrical Journal, a leading biostatistics journal, performs reproducibility audits on every accepted paper. Manuscripts receive a checklist and cannot be published until all items pass (otherwise they lose their β€œFully reproducible” badge). This is increasingly common across health and life-science scientific journals.

  1. Read Box 1 and the Introduction of:
    Hejblum et al.Β (2020). Realistic and robust reproducible research for biostatistics.
    πŸ‘‰ https://www.biorxiv.org/content/10.1101/2020.01.15.907485v1
    (~5 min β€” focus on the 10 checklist items)

  2. Browse the Turing Way section on β€œMaking Research Reproducible”:
    πŸ‘‰ https://book.the-turing-way.org/reproducible-research/reproducible-research

Ask yourself: If you submitted your repro-phds project for a journal audit right now, which of the 10 checklist items would you fail?

4.2 πŸ’‘ Tools for Sharing and Archiving

The full reproducibility toolchain

Getting from β€œworks on my machine” to β€œanyone can run it” requires stacking a few layers:

πŸ“  Project organisation     RStudio Projects / standard folder structure
πŸ”’  Dependency management    `renv` (R) / `venv` + requirements.txt  (Python)
πŸ“  Literate document        `Quarto` (.qmd)
🎲  Random seeds             `set.seed()` / `np.random.seed()`
πŸ—‚οΈ  Version control          `git`
🌐  Remote hosting           GitHub
πŸ“¦  Permanent archiving      Zenodo  β†’  DOI you can cite in a paper

renv in and virtual environments in

renv creates a project-local library and records exact package versions in renv.lock.

renv::snapshot()    # update lockfile after installing / changing packages
renv::restore()     # collaborator command: install exact same versions
renv::status()      # check whether lockfile and current state agree

At the end of your report, always record your session environment:

sessioninfo::session_info()

This prints version, OS, and the exact version of every loaded package.

In the terminal, as mentioned in SectionΒ 1.

pip freeze > requirements.txt       # save exact versions

# Collaborator setup
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

At the end of your notebook:

## Requirements

```{python}
import sys, importlib.metadata
print(f"Python {sys.version}")
for pkg in ["pandas", "numpy", "plotnine"]:
    print(f"  {pkg}: {importlib.metadata.version(pkg)}")
```

Git and GitHub : the essentials

Why use git and GitHub ?

πŸŽ– Off-site backup: if your laptop dies, nothing is lost
πŸŽ– Full history: you can go back in time to any previous version, without worrying that you inadvertently deleted some code that you might end up using later in time
πŸŽ– Collaboration: others can contribute easily, without emailing files
πŸŽ– Integration with Zenodo for citable DOIs

  • git is a version control system that allows to track changes to all the files within a project, and synchronize those changes across multiple computers and contributors.
  • GitHub is a web platform from Microsoft providing git integration with cloud hosting.

You do not need to be a git expert. Three commands cover most of everyday use:

git add .                                                 # stage all changes
git commit -m "Add life expectancy analysis and figures"  # message describing the staged changes
git push                                                  # send to GitHub
NoteπŸ™ One-time git setup
git config --global user.name  "Your chosen user name"
git config --global user.email "you@domain.ext"

In RStudio: Tools β†’ Global Options… β†’ Git/SVN β€” point to the git executable. For trouble shooting in setting-up git in RStudio, you can refer to the Happy git with R online book.

Zenodo: a permanent DOI for your code (and optionally data)

Zenodo is a CERN-hosted (European Organization for Nuclear Research) archive that gives your code or data a DOI (which stands for Digital Object Identifier), a permanent, citable identifier. This is what goes in your paper’s Data Availability statement.

How it works in three steps:

  1. Push your project to a public GitHub repository
  2. Connect GitHub at https://zenodo.org/account/settings/github
  3. Create a GitHub release (e.g.Β v1.0) β€” Zenodo archives it automatically and issues a DOI

Your paper then cites (by replacing XXXXXXX with the actual DOI numbers): > β€œAll analysis code is available at https://doi.org/10.5281/zenodo.XXXXXXX”

The fact that it has a DOI and is hosted in Europe makes it a lot more future proof than just having a GitHub repo (especially as they are not exclusive).

4.3 πŸ§‘β€πŸ’» Exercise 4: Self-Audit and Preparing to Share

TipπŸ§‘β€πŸ’» Exercise 4: Check the RR Checklist (20 min)

Goal: Systematically audit your repro-phds project against the standard checklist, fix any gaps, and prepare it for sharing.


Step 1: The checklist

Work through every item. For anything not yet in place, add it now:

πŸ—‚οΈ README & execution

  • README.md describes the project in one paragraph
  • README gives exact instructions to reproduce all results (which file, which command)
  • A single command re-runs everything (quarto render analysis/report.qmd)
  • Runtime is documented (e.g.Β β€œ< 1 minute on a standard laptop”)

πŸ“¦ Dependencies

  • renv.lock () or requirements.txt () is present and up to date
  • sessioninfo::session_info() () or equivalent Python output is printed in the report

πŸ“Š Data & code

  • All scripts needed to reproduce results are inside the project
  • Data is either in data/raw/ or downloaded via a stable URL in the code
  • No absolute paths anywhere in the code (/home/alice/… or C:…)

🎲 Randomness

  • set.seed() () or np.random.seed() () is set in the setup chunk

πŸ“ Output

  • All figures are saved programmatically to output/figures/
  • Figure file names use the fig01_, fig02_ prefix convention matching the report
  • At least one table is saved to output/tables/

Step 2: Add session info to the README

Add the output of the session-info command to the bottom of your README.md:

# Run in the console and paste the output into README.md
sessioninfo::session_info()
# Run in the terminal and paste into README.md
python -c "import sys; print(sys.version)"
python -m pip list

Step 3: Export a results table from the report

Add a new section ## Export of results to your analysis/report.qmd file, just before the software environment information. The table is thus regenerated at every render, like the figures.

## Export of results

```{r}
#| label: export-tab01-r

library(readr)

summary_2007 <- gapminder |>
  filter(year == 2007) |>
  group_by(continent) |>
  summarise(
    n_countries    = n(),
    mean_lifeExp   = round(mean(lifeExp), 2),
    median_lifeExp = round(median(lifeExp), 2),
    mean_gdpPercap = round(mean(gdpPercap), 0)
  )

write_csv(summary_2007,
          here("output", "tables", "tab01_continent_summary_2007.csv"))
```
## Export of results

```{python}
#| label: export-tab01-py

summary_2007 = (
    gapminder.query("year == 2007") \
    .groupby("continent") \
    .agg(
        n_countries=("country", "nunique"),
        mean_lifeExp=("lifeExp", "mean"),
        median_lifeExp=("lifeExp", "median"),
        mean_gdpPercap=("gdpPercap", "mean")
    ) \
    .round(2) \
    .reset_index()
)

summary_2007.to_csv(here("output/tables/tab01_continent_summary_2007.csv"),
                    index=False)
```

Step 4: Final dependency snapshot and clean render

# Update the lockfile
renv::snapshot()

# The ultimate test β€” restart R completely, then render from scratch:
# Session β†’ Restart R  (or Ctrl+Shift+F10)
renv::restore()
quarto::quarto_render("analysis/report.qmd")
pip freeze > requirements.txt

# Deactivate and reactivate the environment, then render
deactivate
source .venv/bin/activate
pip install -r requirements.txt
quarto render analysis/report.qmd

Step 5 (Bonus) β€” Push to GitHub and archive on Zenodo

# 1. Initialise Git (skip if you already did this)
git init
git add .
git commit -m "Initial reproducible analysis β€” Gapminder life expectancy"

# 2. Create a new repo at https://github.com/new, then (replace YOUR_USERNAME in the url below by your actual GitHub username):
git remote add origin https://github.com/YOUR_USERNAME/repro-phds.git
git branch -M main
git push -u origin main


# 3. Go to https://zenodo.org/account/settings/github
#    Enable the repo β†’ a new release triggers automatic DOI creation

# 4. Create a release on GitHub (click "Releases" β†’ "Create a new release")
#    Tag it: v1.0.0

βœ… Final self-check

The ultimate reproducibility test:
Ask a classmate to clone or download your repository, follow your README instructions exactly, and try to reproduce your figures without asking you anything.

If they can β€” you have a reproducible project. πŸŽ‰

  • All 13 checklist items above are ticked
  • The project renders from a clean /Python session
  • A classmate can reproduce your figures from the README instructions alone
  • (Bonus) The project is on GitHub with a Zenodo DOI

Reflection questions:

  1. What was the hardest checklist item to satisfy β€” and why?
  2. Roughly how much extra time did the reproducible setup cost you, compared to β€œjust writing code”?
  3. Imagine one real-world public health scenario where the extra cost would clearly be worth it.

πŸ† Summary & wrap-up

What we learned:

πŸŽ– Adopting a clean, portable structure for easy navigation inside your project
πŸŽ– Running everything end-to-end with a single command
πŸŽ– Programmatically saving figures and result tables
πŸŽ– Locking your dependencies, so the environment is reproducible
πŸŽ– Including inline computed values directly in the text
πŸŽ– Passing the standard of a journal reproducibility checklist

The one rule

Start small, start now. Reproducibility is an ideal, each small step forward brings you closer.

Key tools

Tool Problem it solves One command to remember
RStudio Project No more absolute paths; self-contained workspace Open .Rproj
Quarto Code + prose in one document; no copy-paste of values quarto render
renv / venv Exact package versions β€” β€œit worked last year” insurance renv::snapshot()
here / pyprojroot Portable file paths that work on any OS here::here(β€œoutput”,β€œfig.pdf”) / here(β€œoutput/fig.pdf”)
ggplot2 / plotnine Programmatic, consistent, exportable figures ggsave() / p.save()
Git + GitHub Version history, collaboration, off-site backup git add . && git commit && git push
Zenodo Permanent DOI for code citation in papers Connect GitHub in Zenodo settings

Going further

Resource What for
The Turing Way Comprehensive, community-maintained reproducible research guide
R for Data Science (2e) Tidyverse, ggplot2, Quarto β€” free online book
What They Forgot to Teach You About R Project organisation, naming, file paths
Quarto documentation Everything Quarto
ggplot2 book (3e) Deep dive into the grammar of graphics
Python Data Science Handbook NumPy, Pandas, Matplotlib, scikit-learn
Hejblum et al.Β (2020) Reproducible research for biostatistics
Hornung et al.Β (2026) Overcoming computational reproducibility barriers in scientific publishing
Ouvrir la Science (in English) French open science guides and passports (MESRI)