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.
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.