Part 1: R Workflows
Sam, a graduate student on your research team, just graduated and left for a new job, leaving an ongoing project to pass to you. Their analysis is “done.” You get an email:
“Here’s the script. It definitely works, just run it! The data’s attached, plus the Table 1 doc I’ve been updating. Good luck! – Sam”
Attached: final_analysis_FINAL_v2.R, births.csv, and Table1_DRAFT_manuscript.docx.
Today we’ll cover good R practices to avoid leaving any of your colleagues – and possibly most importantly, your future self – in this position
here()targetsWe’ll work in RStudio today – it’s probably what you already use and it’s great
But I’ll also mention Positron: Posit’s newer editor, built on VS Code. Multi-language (including R and Python), more extensible, and has some things we’ll discuss built in
Error in setwd(“/Users/sam/Desktop/myproject”) :
cannot change working directory
A researcher doing a meta-analysis reached out years later for some data…
setwd()setwd() points at one exact folder on one exact computer. The moment the project moves – a new laptop, a shared drive, your machine – it will break
Note
A classic explainer: https://www.tidyverse.org/blog/2017/12/workflow-vs-script/
my-project/
├─ my-project.Rproj
├─ README.md
├─ data/
│ ├─ raw/
│ └─ processed/
├─ R/
└─ results/
├─ figures/
└─ tables/
.Rproj file marks the project root and opens RStudio in that folder – no setwd() needed
.Rproj files, instead opening the folder as a project – but the idea is the same.Rproj fileYou can have multiple projects open at once in different RStudio sessions!
data folder (either through RStudio or your file explorer)The bad news is that there are still ways it might break (e.g., depending on your settings, if you are trying to read it in from an R Markdown or Quarto document in a subfolder)
Error in file(file, “rt”) : cannot open the connection
In addition: Warning message:
In file(file, “rt”) :
cannot open file ‘data/births.csv’: No such file or directory
here packagehere() builds paths from the project root – the folder with the .Rproj.
here("data/births.csv") becomes my full path (i.e., /Users/l.smith/.../myproject/data/births.csv); on yours it becomes yourshere()You can nest as many directories as exist in the path, and slashes are always forward:
here::here() vs library(here)is the same as
Note
You can do this with any function from any package (we’ll see another example in a bit)
Save a new, better version to work on.
setwd() line, and rewrite the read.csv() line to use here()
install.packages("here") firstWhile you work, R quietly accumulates:
A script doesn’t make this explicit will run for you, today, and break for anyone else – including future you
Sam’s script restricts to adult mothers:
####### smoking & birth weight analysis ########
####### *** this is the GOOD version, use this one!! *** ########
# install.packages("tidyverse") # run this if it doesn't work
setwd("/Users/sam/Desktop/myproject")
births<-read.csv("births.csv")
births <- clean_names(births)
# restrict to adult moms only
births = births[births$mat_age >= age_min,]Error: object ‘age_min’ not found
Where does age_min come from? It’s never defined in the script. Sam typed age_min <- 18 in the console months ago, and it’s been sitting in their environment ever since.
This is hidden state – the analysis depends on something that isn’t in the code.
This is also common when you are not running code top-to-bottom – this will error in a new session if age_min is defined below the code where it’s used.
The same trap catches functions:
Error in clean_names(births) : could not find function “clean_names”
If a package was loaded in Sam’s session but library() never made it into the script, or a helper function was defined in the console or by running code from a new script, your clean session has no idea what you mean.
base, methods, datasets, utils, grDevices, graphics, stats are all loaded by default

You can use the packages pane to see what packages you have loaded or their versions, but don’t load them this way – make sure to library() them in your script so anyone else can run it too
Works because janitor is loaded:
Works without loading all janitor functions, which is fine if this is all you need:
But another janitor function, like get_dupes(births) will error
Error: object ‘Age_Min’ not found
Error in clean.names(births) : could not find function “clean.names”
You are also going to get these same errors if you spell something wrong, so check for typos before going on a wild goose chase for hidden state! Similarly:
Warning message: package ‘jantor’ is not available for this version of R
A version of this package for your version of R might be available elsewhere, see the ideas at https://cran.r-project.org/doc/manuals/r-patched/R-admin.html#Installing-packages
Tell R to never save or restore your workspace:
(In Positron, this is automatic!)
rm(list = ls()) is not a clean slateYou’ll see scripts open with this to “clear everything”:
It deletes objects – but it does not unload packages, reset options, or change the working directory. It’s a false sense of safety.
Tip
The real reset: Session → Restart R (Cmd/Ctrl + Shift + F10). Do it early and do it often. (I actually map my keyboard shortcut to Cmd + Shift + R so it’s easier to reach/I remember it!).
attach()Sam’s script does this:
attach() dumps a copy of every column into your search path so you can type birth_weight instead of births$birth_weight. Tempting. Avoid!
attach() goes wrongThe attached copy is a snapshot. Edit births and the attached birth_weight is now stale – you’re working with two versions and won’t be told which.
detach() and the clutter follows you all sessionRefer to columns explicitly (births$x, or with(), or stay inside dplyr functions).
Sam’s environment was full of things the script quietly depends on. Find as many as you can:
library()Just make a list for now – we’ll fix them next.
…not the order you happened to run things in. Sam’s script draws a figure near the top:
The library() call exists – it’s just in the wrong place. In Sam’s session the package was already loaded, so the figure ran. In a fresh session it errors: could not find function "ggplot".
Load every package at the top.
A script should read top to bottom like a recipe.
For example: load → read → clean → model → report.
A reproducible analysis can be re-run from nothing and give the same result – by you, by a reviewer, by you in two years.
That needs:
here()),library() calls to the top; move the figure after the data it plots.min_age to 18, bad_ids to 1:3, bw_cutoff to 2500, and replace make_or_table() with tidy()Cmd/Ctrl + Shift + F10) for a clean slate.The code runs either way. Style is what makes it readable, reviewable, and hard to break – by collaborators, reviewers, and future you.
This is separate from code that’s wrong or unsafe. We’re talking about formatting and naming here, not correctness.
This is painful to look at and difficult to understand. One line, three statements, cryptic names, no spaces, mixed assignment.
Instead you might write:
# inconsistent naming, spacing, and assignment
MatAge <- d$mat_age
birth.weight<-d$birthWeight
n_Obs = nrow( d )
# magic numbers with no explanation
d <- d[d$v3 > 2500 & d$v7 < 37, ]
# the commented-out graveyard
# m1 <- lm(y ~ x)
# m2 <- lm(y ~ x + z)
# m3 <- lm(y ~ x + z + w) # this one? maybe?
# T/F abbreviations
T <- 34
if (x == T) {...}x + 1, not x+1<- for assignment (the tidyverse convention), reserve = for argumentsNote
Reference: https://r4ds.hadley.nz/workflow-style
styler (R package)
styler::style_file("script.R")Air (newer, very fast)
styler::style_file() on your script (Addins → Style active file), or install Air and format document.Sam’s script reads, cleans, plots, models, and saves – all in one file, top to bottom. For a small analysis, honestly, that’s fine!
The strain shows up as it grows:
There’s no single right answer. Here are four options, lightest to heaviest – pick what works for you
R/00_setup.R # packages, options, source functions
R/01_clean.R # raw data -> analysis data
R/02_analysis.R # fit models
R/03_outputs.R # tables, figures
run_all.R # source() them in order
A great default for many projects
Everything – cleaning, models, write-up – in a single .qmd.
Makes a lot of sense when the analysis is the deliverable: a report, a paper, a homework assignment. It also encourages good habits.
Sample size was n = `r nrow(births)`Note
Familiar with R Markdown? You can use basically everything you know about R Markdown in Quarto, and more!
A parent document pulls in section files.
Good when a document gets unwieldy, or co-authors each own a section without scrolling through the whole manuscript.
Note
This is similar to how I wrote my dissertation (with R Markdown)!
targets pipelinetargets is a package that tracks what each step depends on and only re-runs what changed.
Instead of a sequence of R scripts, you have a sequence of targets that may or may not depend on each other:
targets?Edit the figure and re-run – a slow model does not need to be refit. Change the raw data file and everything downstream knows it’s out of date.
It’s overkill for today’s tiny analysis, but worth it when steps are slow, numerous, or re-run constantly (I use it for simulation studies and for more complex analyses where the data is often changing)
targetsI gave a talk introducing targets a few years ago: https://www.louisahsmith.com/talks/2023-05-31/slides#/title-slide
The documentation is very thorough: https://books.ropensci.org/targets/
Carve your working script into numbered pieces and save them into the appropriate directory:
R/00_setup.R – library() calls, read + clean the dataR/01_model.R – fit the modelsR/02_figures-tables.R – analyze the resultsrun_all.R – source() them in order
Restart R and source run_all.R. Does it run cleanly?
Before: one file, hard-coded paths, hidden state, code out of order, results typed into Word by hand. Broke on the first line.
After: an R Project anyone can open and run from a clean session – paths that travel, code in order, tables and numbers that rebuild themselves, and a choice of ways to organize and report it.
here()Your project runs and reports itself.
But code still breaks. Part 2: how to debug it systematically, and how to ask for help that actually gets you unstuck.