The Epi R Handbook is a R reference manual for applied epidemiology and public health.
Go to www.epiRhandbook.com to see the latest version of the online handbook.
This book strives to:
- Serve as a quick epi R code reference manual
- Provide task-centered examples addressing common epidemiological problems
- Assist epidemiologists transitioning to R
- Be accessible in settings with low internet-connectivity via an offline version
Written by epis, for epis We are applied epis from around the world, writing in our spare time to offer this resource to the community. Your encouragement and feedback is most welcome:
- Structured feedback form
- Email epiRhandbook@gmail.com or tweet @epiRhandbook
- Submit issues to our Github repository
Upgrading from an earlier version? STAKEHOLDERS.md explains what changed for readers and authors in 2.7 — which chapters were edited, what readers will notice, and the decisions behind two chapters that are no longer included.
- Browse the pages in the Table of Contents, or use the search box
- Click the "copy" icons to copy code
- You can follow-along with [the example data][Download handbook and data]
- See the "Resources" section of each page for further material
Offline version
See instructions in the [Download handbook and data] page.
Languages
We want to translate this into languages other than English. If you can help, please contact us.
This section explains how the handbook is built, translated, and published. It is for anyone changing a chapter, adding a language, or debugging a broken build.
Two repositories build this handbook, and each owns a different part:
- appliedepi/aedockerpublic owns the R packages, the Docker images, and the render scripts.
- This repository owns the
.qmdcontent, in every language, and the choice of which image renders each chapter.
Neither repository fetches from the other at build time.
They are split because a chapter's prose and a chapter's package set change on different schedules, and by different people. An author can fix a sentence today without waiting on a package upgrade. A package upgrade can happen without touching a single word of text.
docker-images.yml, in this repository, maps each chapter to a Docker image.
Each chapter gets one entry, called a "stem" (e.g. time_series). One entry
covers all of that chapter's languages, because a translation runs the same R code
as the English original, so it needs the same packages.
To move one chapter to a different image version, change that one line.
The language list is not in this manifest. It lives in languages.yml.
languages.yml declares each language: its code, its BCP-47 lang tag, its display label and its book title. Every
check in checks/check-sync.sh reads it. English is the main language.
Each language owns one folder, content/<lang>/. That folder holds the language's chapter
files, named <stem>.qmd, and one Quarto book project, content/<lang>/_quarto.yaml. The
project file declares the language, the title and the chapter list. Each language renders under
its own site path: /en/, /fr/, and so on.
The eight project files declare the same 52 stems in the same order. Check 9 of
checks/check-sync.sh reports a language that drifts from that. Three of the 52 are not
chapters: index.qmd, about.qmd and acknowledgements.qmd. That is why the landing page
hero counts 49 chapters.
Old /new_pages/... URLs still work. Every chapter file except index.qmd carries an
aliases: entry in its front matter. Quarto turns that into a redirect stub at the old path:
---
aliases:
- /new_pages/time_series.html
---
English writes /new_pages/<stem>.html. Every other language writes
/new_pages/<stem>.<lang>.html, so French writes /new_pages/time_series.fr.html. Check 9 of
checks/check-sync.sh enforces it. A chapter file that lacks the alias, or carries the wrong
one, is a drift.
A chapter that also replaces a second old URL carries a second alias line.
content/en/transition_to_r.qmd is today's one example. It carries
/new_pages/transition_to_R.html as well, the old spelling with a capital R.
The alias value must be root-relative: it must start with a leading /. Without the leading
slash, Quarto writes the redirect stub in the wrong place, and the old link stays broken.
Three environments publish from this repository:
preview— one build per pull request.staging— builds on every push tomain.production— updated only when a release is published, by promoting staging. Nothing is rendered at release time.
Production is a promotion of staging, not a rebuild. Cutting a release force-pushes
the already-built staging artifact to production. Nothing is rebuilt at release
time. What goes live is exactly what was already reviewed on staging, not a fresh
render that might behave differently.
production has not been used yet. The branch does not exist on the remote and
production.yml has never run. It is kept ready for the first promotion.
A fork pull request now renders, but still cannot publish a preview. Be clear about which half of that changed.
The 2.9 images are public, so the build authenticates to nothing and every image pull is anonymous. A fork PR therefore gets all the way through the render, where it previously died at a login step it could never pass. That is genuinely useful: it proves the contributor's chapters build.
It stops at the deploy. assemble-deploy force-pushes the preview branch, and GitHub gives
a pull request opened from a fork a read-only GITHUB_TOKEN no matter what permissions:
the workflow asks for. So the push fails and the run goes red, even though nothing is wrong
with the contribution. A red check on a fork PR does not mean the change is broken — look
at whether the render jobs passed. Publishing a preview for a fork needs a different design
(a pull_request_target deploy, or an upload-artifact-and-comment flow); we have not built it.
Removing the login did remove a whole class of failure: a registry or DNS hiccup during authentication can no longer kill a 25-minute render. If an image is ever made private again, the pull fails naming the image it wanted, which is where you want to find out.
You do not need to install R, Quarto, or any package to work on this handbook. The environment ships as a container.
Open the repository in a dev container. .devcontainer.json at the repository root
names the image:
{
"name": "epirhandbook",
"image": "ghcr.io/appliedepi/aedockerpublic/epirhandbook-monolith:2.9"
}- Install Positron (or VS Code) and Docker.
- Clone this repository and open the folder.
- Accept the "Reopen in Container" prompt. See Positron's dev containers guide.
The image is public — no docker login, no token, no Applied Epi account.
Why the monolith. CI renders each chapter in its own group image, holding only that group's packages. The monolith holds all of them at once. It is generated from the six group images rather than maintained by hand, so it cannot drift from what CI uses. One container therefore renders any chapter. That is what you want while editing. It is what you do not want in CI, where a smaller image is faster.
To render a single chapter inside the container, work from that chapter's language folder:
cd content/en
quarto render epicurves.qmdQuarto reads content/en/_quarto.yaml from that folder, so every relative path in the chapter
resolves. Rendering the whole book in all eight languages is CI's job, not something to do
locally.
If you add a package, that is a change to
appliedepi/aedockerpublic, not to this
repository. Add the package to the chapter's group list there, rerun
epirhandbook/2.9/generate_groups.py, and commit — the group images and the monolith
are regenerated from the same source, so they cannot disagree.
This is the same flow as appliedepi/websitetimecourse. Content changes, translations included, all go through it.
- Create a branch off
mainand make your edits. - Check they render locally.
- Open a pull request into
main. - That triggers a build to the
previewbranch. View it at the preview website URL. Check it before merging. - Merge the pull request into
main. That triggers a build to thestagingbranch. Check the build succeeded. - If staging is good, create a GitHub release, following the versioning conventions.
That pulls
stagingintoproduction, and a webhook updates the live site.
Only rendered HTML reaches preview, staging and production. They are orphan
branches: CI force-pushes them, nothing is ever merged into them, and they contain no
.qmd source. The source lives on main and nowhere else.
Nothing is re-rendered at release time. Step 6 copies the artifact you already checked in step 5, so what goes live is exactly what was reviewed.
| To change... | Edit... |
|---|---|
| A chapter's prose | this repository, in content/<lang>/ |
| Which languages ship | languages.yml, in this repository |
| A chapter's R packages | appliedepi/aedockerpublic |
| Which image a chapter uses | docker-images.yml, in this repository |
| The landing page's hero text | landing.yml, in this repository. Its markup is utils/landing-hero.R |
Bump one chapter's image. Edit that chapter's row in docker-images.yml to
point at the new image tag. Every other chapter keeps its own tag and is
unaffected.
Add a new chapter. Five things, across both repositories:
- A
.qmdfile incontent/en/, and a translated file in each othercontent/<lang>/. - An image in aedockerpublic: a new one, or an existing one that already has the right packages.
- A new row in
docker-images.yml, in this repository. - A new entry under
book.chaptersin everycontent/<lang>/_quarto.yaml, at the same position in each. - The standard alias in the chapter's own front matter:
/new_pages/<stem>.htmlundercontent/en/,/new_pages/<stem>.<lang>.htmlelsewhere. The value needs a leading/.
Run checks/check-sync.sh afterwards. Check 9 reports a language you missed.
Add a new language. Six things, all in this repository:
- An entry in
languages.ymlwith four fields:code,lang,labelandtitle.codenames the folder and the site path.langis the BCP-47 tag, and it can differ from the code: Japanese iscode: jpwithlang: ja. - A folder
content/<code>/with a translated<stem>.qmdfor each of the 52 stems thatcontent/en/holds. - A
content/<code>/_quarto.yaml, copied fromcontent/en/_quarto.yaml. Setlang:to thelangvalue andbook.titleto thetitlevalue fromlanguages.yml. Keep the chapter list and its order unchanged. - The alias in each chapter's front matter,
/new_pages/<stem>.<code>.html. Inindex.qmdthe alias is/index.<code>.htmlinstead. Check 9 does not checkindex.qmd, so get that one right by hand. - A
<code>:block inlanding.ymlfor the landing page strings. A missing string falls back to English. Check 15 names every translatable key the block lacks, apart from the two omissions it allows on purpose. - For a script that is not Latin, check two places. The
subset=list in the webfont link inbanner.htmlsets which scripts the webfonts serve. The:lang()gate near the top oftheme-ael.scssturns off uppercase headings, and it needs the new tag if uppercase does not suit the script.
No image or docker-images.yml change is needed: a translation runs the same R code as the English chapter. Run checks/check-sync.sh afterwards. Check 9 reports a missing folder, project file, chapter or alias.
Start by identifying three things: which job failed, which language, and which chapter.
A render that exits 0 is not proof the chapter is correct. A chapter can render successfully and still be wrong — stale output, a broken cross-reference, a computed value that silently changed. The build validates its own output as a separate step; check what that validation reports, not just whether the render job's exit code was 0.
One chapter is excluded from the build: epidemic_models. Every
content/<lang>/_quarto.yaml comments it out under book.chapters. Its source files sit in
_excluded/.
The chapter fails on a recorded EpiNow2 API break, an xy.coords() error. See
aedockerpublic's epirhandbook/2.7/BREAKAGE.tsv.
Its old URL will stop working. /new_pages/epidemic_models.html returns HTTP 200
today, after a redirect to /en/new_pages/epidemic_models.html. It serves the version
built before the exclusion, and it will stop resolving once this deploys. A chapter
absent from book.chapters never renders, so it never emits the alias redirect stub
that keeps the old URL alive.
No chapter links to epidemic_models in any language. A search for the string across every
.qmd file under content/ returns nothing, so the exclusion breaks no cross-reference.
What it would take to bring it back. Rewrite the chapter's EpiNow2 code against the current API. The chapter uses result accessors that EpiNow2 removed. Then render the chapter and verify the output.
This handbook is produced by a collaboration of epidemiologists from around the world drawing upon experience with organizations including local, state, provincial, and national health agencies, the World Health Organization (WHO), Médecins Sans Frontières / Doctors without Borders (MSF), hospital systems, and academic institutions.
This handbook is not an approved product of any specific organization. Although we strive for accuracy, we provide no guarantee of the content in this book.
Editor: Neale Batra
Project core team: Neale Batra, Alex Spina, Amrish Baidjoe, Pat Keating, Henry Laurenson-Schafer, Finlay Campbell
Authors: Neale Batra, Alex Spina, Paula Blomquist, Finlay Campbell, Henry Laurenson-Schafer, Isaac Florence, Natalie Fischer, Aminata Ndiaye, Liza Coyer, Jonathan Polonsky, Yurie Izawa, Chris Bailey, Daniel Molling, Isha Berry, Emma Buajitti, Mathilde Mousset, Sara Hollis, Wen Lin
Reviewers: Pat Keating, Annick Lenglet, Margot Charette, Daniely Xavier, Esther Kukielka, Michelle Sloan, Aybüke Koyuncu, Rachel Burke, Kate Kelsey, Berhe Etsay, John Rossow, Mackenzie Zendt, James Wright, Laura Haskins, Flavio Finger, Tim Taylor, Jae Hyoung Tim Lee, Brianna Bradley, Wayne Enanoria, Manual Albela Miranda, Molly Mantus, Pattama Ulrich, Joseph Timothy, Adam Vaughan, Olivia Varsaneux, Lionel Monteiro, Joao Muianga
Illustrations: Calder Fong
The handbook received supportive funding via a COVID-19 emergency capacity-building grant from TEPHINET, the global network of Field Epidemiology Training Programs (FETPs).
Administrative support was provided by the EPIET Alumni Network (EAN), with special thanks to Annika Wendland. EPIET is the European Programme for Intervention Epidemiology Training.
Special thanks to Médecins Sans Frontières (MSF) Operational Centre Amsterdam (OCA) for their support during the development of this handbook.
This publication was supported by Cooperative Agreement number NU2GGH001873, funded by the Centers for Disease Control and Prevention through TEPHINET, a program of The Task Force for Global Health. Its contents are solely the responsibility of the authors and do not necessarily represent the official views of the Centers for Disease Control and Prevention, the Department of Health and Human Services, The Task Force for Global Health, Inc. or TEPHINET.
The multitude of tutorials and vignettes that provided knowledge for development of handbook content are credited within their respective pages.
More generally, the following sources provided inspiration for this handbook:
The "R4Epis" project (a collaboration between MSF and RECON)
R Epidemics Consortium (RECON)
R for Data Science book (R4DS)
bookdown: Authoring Books and Technical Documents with R Markdown
Netlify hosts this website
This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
Academic courses and epidemiologist training programs are welcome to use this handbook with their students. If you have questions about your intended use, email epirhandbook@gmail.com.
Batra, Neale et al. (2021), The Epidemiologist R Handbook.
If you would like to make a content contribution, please contact with us first via Github issues or by email. We are implementing a schedule for updates and are creating a contributor guide.
Please note that the epiRhandbook project is released with a Contributor Code of Conduct. By contributing to this project, you agree to abide by its terms.


