cassionData Analysis

Lesson 1 of 8

Unit · A project that reopens

An analysis that reopens a year later

RStudio projects, here(), renv and the two habits — setwd() and a saved workspace — that make an R analysis run on exactly one machine on exactly one day.

R80 min

The failure this lesson prevents

Someone asks you to explain last quarter’s figure. You open the analysis, run it, and get a different number — or it does not run at all, because the package that produced it has moved on two versions.

That is not a bug in the analysis. It is the environment, and in R there are three specific habits that cause it.

The three habits to drop

setwd()

setwd("/Users/marie/Desktop/nutrition")

This runs on exactly one computer. It is the single most common reason a colleague’s script fails on your machine, and the fix is not to edit the path — it is to stop having one.

rm(list = ls()) at the top

It looks like starting clean and is not. It removes the objects in your workspace and leaves everything else: attached packages, options, the working directory, anything loaded from a .Rprofile. A script that needs it is a script that is not reproducible, and running it in a fresh session is the only real test.

Restart R instead — Ctrl/Cmd + Shift + F10 in RStudio. That is the clean start rm(list = ls()) pretends to be.

A saved workspace

RStudio offers to save .RData when you quit and to reload it when you start. Turn both off:

Tools -> Global Options -> General
  Restore .RData into workspace at startup    [ ]
  Save workspace to .RData on exit            Never

A reloaded workspace means your session contains objects whose code you cannot see. The analysis appears to work because something from last week is still in memory, and it stops working the moment someone else runs it.

The project

An RStudio project is a directory with a .Rproj file in it. Opening the project sets the working directory to that directory, and that is the whole mechanism — but it is enough to make every path in your code relative to the project instead of to your home folder.

muac-analysis/
  muac-analysis.Rproj
  data/
    raw/            the export exactly as it arrived, never edited
    interim/
  outputs/
    tables/
    figures/
  R/
    read_register.R
    indicator_table.R
  renv.lock
  README.md
  .gitignore

data/raw is read-only. The export as it arrived is the only thing you cannot reproduce; everything else is regenerated by running the code. So it is never edited, never sorted in Excel “just to look”, and a correction lands beside the original as a new versioned filename rather than replacing it.

here() rather than relative paths

A relative path like "data/raw/muac.csv" works from the project root and breaks in a notebook, in an R Markdown document knitted from a subdirectory, or when someone runs one line at a time from a different location.

library(here)

muac <- readr::read_csv(here("data", "raw", "muac-screening-artibonite-2024.v1.csv"))

here() finds the project root — the directory containing the .Rproj file, or a .here file, or a .git directory — and builds the path from there. The result is the same wherever the code is run from.

here()
#> [1] "/home/marie/muac-analysis"

Call here() once at the top of a script and use it everywhere. A path built by paste0() with a / in it will not work on Windows; here() handles the separator.

renv: the package versions, recorded

here() fixes paths. It does nothing about the packages, and the packages are where the number changes.

install.packages("renv")

renv::init()       # once per project

renv::init() gives the project its own library and writes renv.lock, a record of every package and its exact version. From then on:

renv::snapshot()   # after installing or upgrading anything
renv::restore()    # on another machine, or a year later

Commit renv.lock. It is the difference between “install the tidyverse” and “install the versions that produced this table”.

Installing where there is no internet

The part most tutorials skip, and the part that matters on a deployment.

On a connected machine, download the sources once:

renv::init()
renv::snapshot()

# Fill a local cache with everything the lockfile names.
renv::install()
renv::isolate()

Then copy the project directory — including renv/library — to the field laptop. renv::restore() will use what is already there rather than reaching for a repository it cannot see.

For a machine that will need packages you have not yet installed, the general mechanism is a local repository:

# On the connected machine
dir.create("pkgs")
download.packages(c("dplyr", "readr", "haven"), destdir = "pkgs", type = "source")

# On the offline machine
install.packages(
  c("dplyr", "readr", "haven"),
  repos = NULL,
  type = "source",
  contriburl = paste0("file://", normalizePath("pkgs"))
)

Source packages need a compiler for anything with C or C++ in it, which is common. If the field machines are Windows, download the binaries instead (type = "win.binary") on a Windows machine of the same R version.

Loading packages

library(readr)
library(dplyr)

Two things not to do:

Do not use require() in a script. It returns FALSE and continues when the package is missing, so the script fails later with a confusing error about an object that does not exist. library() stops there and tells you which package.

Do not call install.packages() from a script. A script that installs software as a side effect of being run is a script nobody can safely run twice. Installation is renv::restore(), once, deliberately.

Where two packages export the same name — dplyr::filter() and stats::filter(), dplyr::lag() and stats::lag() — say which you mean:

muac |> dplyr::filter(muac_mm < 125)

The :: form is worth the characters in a script that will outlive your memory of what was attached.

Verifying the environment before trusting it

sessionInfo()

For an analysis whose numbers matter, assert rather than inspect:

stopifnot(getRversion() >= "4.2.0")
stopifnot(requireNamespace("dplyr", quietly = TRUE))

|>, the native pipe used throughout this course, needs R 4.1 or later. If your team is on an older R, %>% from magrittr does the same job and the code in these lessons works unchanged with it.

What to hand to a colleague

Commit Do not commit
R/, .Rproj renv/library/
renv.lock data/raw/ — see below
README.md outputs/
.gitignore .Rhistory, .RData
.Rproj.user/
.Rhistory
.RData
renv/library/
data/raw/
outputs/

Never commit raw beneficiary data, identified or pseudonymised. Git keeps every version of every file forever, and a repository that was internal on Monday can be shared on Friday. Credentials — a DHIS2 token, a KoboToolbox key — never appear in a script:

token <- Sys.getenv("DHIS2_TOKEN")
stopifnot(nzchar(token))

Keep the value in a .Renviron file that .gitignore covers, and document the variable’s name — not its value — in the README.

What comes next

The project reopens and the packages are pinned. The next lesson reads an export into it: readr for CSV, readxl for Excel, and the column specification that stops a facility code from becoming a number.

Teach this lesson

The lesson as a slide deck, with the prose kept in the speaker notes rather than on the slide. Generated from this page, so it cannot fall out of step with it.

Start the slideshowRead the slides

The PDF needs no software and projects from any machine. The PowerPoint file is there to be edited — add your organisation's branding, cut a section for a shorter session, or merge two lessons into a workshop.