Reproducible workflows in R

King’s Open Research Summer School

QR code
Dr Ewan Carr

Department of Biostatistics & Health Informatics
King’s College London

July 21, 2026

🖼️

  • My aim is to surface key tools and habits — you’ll need to explore further on your own.

  • Reproducibility is necessary for open science, but not sufficient. Broader change in motivations, incentives, and culture is essential.

  • Reset academic publishing models.
  • Reward high quality team science regardless of null findings.
  • Stop chasing small, noisy effects in tiny samples.

“Research can be open and reproducible and still completely and obviously wrong.”

Richard McElreath (2025)

This talk

1. Code and data hygiene

2. Version control with Git

3. Managing your environment

4. Workflow automation

5. Dynamic reporting with Quarto

6. Wrap-up

🫣

Not doing horrible things with your data or code

🧼 Data hygiene

  1. Keep raw and processed separate
  2. Never edit raw data; automate instead.
    • Avoid final_final_v3.csv
  3. Set raw data as read only
  4. Version your data
    • e.g., data/raw/YYYY-MM-DD
  5. Back up regularly; test your backups.

Use a consistent project structure

🧨 Absolute file paths break easily

They only work on your computer, right now.

df <- read_csv(
  "/Users/ewan/Documents/data/study.csv"
)
  • If you move this project, or share it with someone else, paths may break.

  • Your collaborator does not have your computer.

Never set the working directory inside a script.

🎒 Use here for relative paths

Set a project root (i.e., a top-level directory) from:

  • An RStudio project
  • A Git repository
  • Manually (with a .here file)

Then, construct relative paths with here():

library(here)
data <- read_csv(
  here("data", "raw", "study.csv")
)

This works well with RStudio projects.

🧹 Write clean, organised code

  • Write modular code:

    • Break tasks into separate functions or scripts (e.g., separate scripts for cleaning, analysis, visualisation.
    • Move geneic functions into functions.R
    • Avoid very long scripts.
  • Follow a style guide and use a code formatter.

  • Document with inline comments and README.md files.

  1. Be consistent

  2. Use clear snake_case object names

  3. Objects are nouns; functions are verbs

  4. Give files meaningful, sortable names

  5. Load packages together at the start

 

  1. Use whitespace to show structure

  2. Organise scripts into clear sections

  3. Break up long, complex expressions

  4. Turn repeated work into functions

  5. Comment the why, not the what

See style.tidyverse.org for details.

Use the code formatter in RStudio

From the Code menu, select “Reformat Selection”:

🔁

Version control

Use git.

Git
Software on your computer that records your project’s history.
Repository
A project folder that Git is watching.
GitHub
A website that hosts repositories online for backup, sharing, and collaboration.

Local vs. remote repositories

Local repository

The working copy on your computer. You can edit files and commit offline.

Remote repository

A linked copy hosted online (e.g., GitHub), holding the full history.


  • We send and receive changes between them — this is pushing and pulling.

  • You can use Git entirely locally, without GitHub.

How does it work?

Initialise the repository:

git init
git add README.md cleaning.R
git commit -m "Initial commit"


Then on GitHub, create a new repository and connect the remote repository to your local one:

git remote add origin \
  https://github.com/your-username/your-repo.git

Push your changes to GitHub

git push origin main


Then, repeat:

1git add .
2git commit -m "Describe your changes"
3git push
1
Add recent changes
2
‘Commit’ them to the local repository
3
‘Push’ them to GitHub

Some tips for working with Git…

Include a README.md file

A good README.md helps others (and your future self) understand your project.

  • What and who is the project for?
  • Describe the folder structure and key files.
  • Step-by-step instructions, including software dependencies and steps to reproduce.


Write good commit messages

Your message should explain what changed and why.

🚀 Good

  • Add 10-fold cross-validation to the model pipeline
  • Check for missing values before fitting the mixed model
  • Drop participants with no baseline PHQ-9 measure
  • Recode sex to a factor so lm() doesn’t treat it as numeric

🔥 Bad

  • Changes
  • Update
  • Fix, properly this time

Your message should complete the sentence “If applied, this commit will…”

Use .gitignore to exclude files

Create a .gitignore to keep files out of version control:

data/         # Ignore the "data" folder
outputs/
*.csv         # Ignore any file ending .csv
*.sav
.DS_Store
.Rhistory
*.Rproj
  • Yes: scripts, README, config files.
  • No: data, model files, credentials, API keys.

Never commit patient data

Researchers using UK Biobank data committed data folders locally, pushed them to GitHub, and then exposed them publicly.

The Guardian, 14 March 2026

  • Add data folders to .gitignore file before any data files are added.

  • Deleting a file later is not enough — the earlier version stays in the Git history.

What to learn next?

  • 🌿 Branching and pull requests
  • 🤝 Collaborating in real-world projects
  • ⚔️ Handling merge conflicts
  • 🔄 Continuous integration (e.g., GitHub Actions)
  • 🧪 Automated testing

📦

Environment control

Your entire environment should be easily reproducible

We need a way of capturing the state of your computing environment, such that you or others can recreate it later.

  • Software (e.g., R, Python, command line tools)
  • R packages
  • Operating system

  1. Report session information in your scripts:

    sessionInfo()
    sessioninfo::session_info()
  2. Set a seed:

    set.seed(42)
  3. Use a dated CRAN repository:

    options(repos = c(
      CRAN = "packagemanager.posit.co/cran/2026-07-21"
    ))
  4. Use pak or renv to control package versions.

Freeze your packages to a date

renv

renv is an R package to manage and reproduce the exact package versions used in a project.

Setting up renv

First, install the package (once):

install.packages("renv")

Then, from the top-level directory, initialise the project:

renv::init()

Install the required packages:

renv::install()             # All required packages
renv::install("tidyverse")  # A specific package

Save the current state:

renv::snapshot()


Then share renv.lock with collaborator (via Git).

Restoring from a renv lockfile

When re-initialising a project (e.g., on a new computer, or as a collaborator):

renv::restore()
  • Compares the lockfile to the current project library.
  • Installs any missing or mismatched packages.

R packages are just one part of
your computing environment.

Containers

Containers package your entire computing environment so it runs consistently everywhere.

This typically involves Docker or Singularity.

Your computer — Mac, Windows or Linux
Docker — runs containers
A container — everything your analysis needs
Linux
R 4.5.1
Your packages
Your code
The green box is the bit you share — it runs the same on anyone's machine.

Where do containers come from?

  1. Build them yourself.
  2. Download a container made by someone else.

A Dockerfile is a recipe

FROM rocker/r-ver:4.5.1                          # 1
RUN apt-get update && apt-get install -y \       # 2
    libcurl4-openssl-dev libxml2-dev
WORKDIR /project                                 # 3
COPY renv.lock .                                 # 4
RUN R -e 'renv::restore()'                       # 5
COPY . .                                         # 6
CMD ["Rscript", "run.R"]                         # 7
Line by line
  1. Start from an image that already has R 4.5.1 on Ubuntu.
  2. Install system libraries R needs to compile.
  3. Set where inside the container your project lives.
  4. Copy renv.lock into the container to set package versions.
  5. Restore the renv environment to install those versions.
  6. Copy your scripts into the container.
  7. Set what runs when someone starts the container.
  • You build the image once. This is when R and your packages get installed.
  • You (or anyone else) can then run it as many times as you like — nothing is installed again, so it works the same in five years’ time.

🤖

Workflow automation

Level 1: source()

Create a script that runs your other scripts:

run.R
source("01-cleaning.R")
source("02-analysis.R")
source("03-plots.R")
  • Supports basic automation.
  • Runs everything unconditionally, no awareness of dependencies, scales poorly.

Level 2: Make

Make runs only the parts of your code that need updating.

Makefile
clean.csv: raw.csv cleaning.R
    Rscript cleaning.R

plot.png: clean.csv analysis.R
    Rscript analysis.R

A Makefile declares targets and dependencies.

  • For example, clean.csv is a target with dependencies (raw.csv, cleaning.R).
  • If either change, the code (Rscript cleaning.R) is run.

Once your Makefile is defined, run:

make plot.png

Make checks timestamps and re-runs only the steps whose inputs have changed.



Level 3: targets

  • targets builds reproducible workflows from R functions and the objects they return, rather than scripts and files.

  • It tracks changes by content (rather than timestamps) so nothing is recomputed unnecessarily.

📄

Dynamic reporting with Quarto

What is Quarto?

Combine code, results, and prose into one document.

  • Figures and tables are generated from code — never pasted in by hand.
  • Re-render when the data changes, and the document updates itself.
  • One source → many outputs: reports, papers, dashboards, websites — even these slides.

An example

---
title: "Bill length and body mass in Adélie penguins"
author: "Ewan Carr"
date: today
format:
  html:
    toc: true
  docx: default
bibliography: references.bib
---

```{r}
#| label: setup
#| include: false
library(tidyverse)
library(palmerpenguins)
adelie <- filter(penguins, species == "Adelie")
```

## Introduction

Bill morphology varies with body size across the *Pygoscelis* genus
[@gorman2014]. Here we look at Adélie penguins only.

## Methods

We fitted a linear model to `r nrow(adelie)` birds.

```{r}
#| label: fig-scatter
#| fig-cap: "Bill length against body mass."
#| warning: false
ggplot(adelie, aes(bill_length_mm, body_mass_g)) +
  geom_point(alpha = 0.6) +
  geom_smooth(method = "lm") +
  labs(x = "Bill length (mm)", y = "Body mass (g)")
```

## Results

Heavier birds had longer bills (@fig-scatter). The full model is
reported in @tbl-model.

```{r}
#| label: tbl-model
#| tbl-cap: "Linear model of body mass on bill length."
#| echo: false
lm(body_mass_g ~ bill_length_mm, data = adelie) |>
  broom::tidy() |>
  knitr::kable(digits = 2)
```

## References

::: {#refs}
:::

What can you put in a Quarto document?

Writing

  • Headings, lists, footnotes
  • Bold, italic, code, links
  • Equations, in LaTeX

Figures and tables

  • Images, with captions
  • Tables from code or written by hand
  • Multi-column and grid layouts

Scholarly features

  • Citations and a bibliography, from a .bib file
  • Cross-references to figures, tables and sections
  • Journal templates

Output formats

  • HTML, PDF, Word
  • Websites, books, dashboards
  • Slides — including these ones

Wrapping up



Phew, that was a lot.

  • 🌱 Start small, build incrementally.
  • 🤝 Talk to colleagues about how they organise their code; establish shared practices.
  • 🌀 Use version control early — it saves time (and headaches) later.
  • 🧠 Tools help — but culture and communication are key.

Thank you for listening.

Slides and practical materials