Operational reference for R project workflow: environment isolation, dependency management, reproducible reports, code style, pipelines, testing, and the base-R vs tidyverse decision.
.Rproj)Create one project per analysis. Every .Rproj file sets the working
directory to its own folder on open — no manual path wrangling needed.
# Start fresh: File > New Project > New/Existing Directory in RStudio
# Or from R:
usethis::create_project("my-analysis")
The project root becomes here::here() automatically.
setwd() with absolute pathsAbsolute paths break on any other machine, in CI, or after a folder rename.
# BAD — ties code to one machine
setwd("/Users/mack/projects/analysis")
data <- read.csv("data/raw.csv")
# GOOD — works everywhere the .Rproj exists
library(here)
data <- read.csv(here("data", "raw.csv"))
here::here() walks up the directory tree to find the project root
(.Rproj, .git, DESCRIPTION, .here). Pass path components as
separate strings; here handles the OS separator.
Accumulated state in a live session hides bugs. Bind Cmd/Ctrl+Shift+F10 to "Restart R" and use it between major steps. If your script doesn't run clean from a fresh session, it's broken.
.RDataThe hidden .RData file silently reloads stale objects. Disable it globally:
# In ~/.Rprofile or via Tools > Global Options in RStudio
usethis::use_blank_slate() # sets save.defaults and restore defaults
# Equivalent manual toggle (in .Rprofile):
# RStudio GUI: Tools > Global Options > Workspace > Never save/restore
Set in project .Rprofile too if sharing with collaborators who may
have different global defaults.
renv — project-local libraryrenv records exact package versions in a lockfile and restores them
on any machine. The gold standard for reproducible R environments.
# Initialise in a new or existing project
renv::init()
# After installing or updating packages, snapshot the lockfile
renv::snapshot()
# On a collaborator's machine or in CI
renv::restore()
# Check for out-of-sync state
renv::status()
Key files committed to version control:
renv.lock — exact versions (JSON, human-readable).Rprofile — sources renv/activate.R automaticallyrenv/activate.R — bootstraps renv on clone.renv/library/ goes in .gitignore (large, platform-specific).
pak — fast, reliable installspak resolves dependencies in parallel and handles CRAN, GitHub,
Bioconductor, and local packages uniformly. Use it inside renv
workflows for faster installs.
# Install pak (once)
install.packages("pak")
# Install from CRAN
pak::pak("dplyr")
# Install from GitHub (owner/repo)
pak::pak("tidyverse/dplyr")
# Install multiple at once
pak::pak(c("dplyr", "ggplot2", "tidyr"))
# Within renv: pak integrates transparently
options(renv.config.pak.enabled = TRUE) # in .Rprofile
pak is faster than install.packages() for initial setup; renv
owns reproducibility; the two compose well.
Quarto (.qmd) is the current standard for reproducible documents.
It supersedes R Markdown for new work while remaining compatible with
the same knitr/pandoc backend. R Markdown (.Rmd) is still
maintained and supported.
---
title: "Analysis Title"
author: "Your Name"
date: today
format: html # or pdf, docx, revealjs, dashboard, …
execute:
echo: true
warning: false
---
Code chunks use #| YAML-style options:
```{r}
#| label: load-data
#| message: false
#| echo: false # hide code, show output
library(tidyverse)
df <- read_csv(here::here("data", "raw.csv"))
```
```{r}
#| label: plot-dist
#| fig-width: 8
#| fig-height: 4
#| fig-cap: "Distribution of values"
ggplot(df, aes(x = value)) + geom_histogram()
```
| Option | Values | Effect |
|---|---|---|
echo |
true/false/fenced |
Show source code |
eval |
true/false |
Run the chunk |
include |
true/false |
Include output (false suppresses everything) |
message |
true/false |
Show package messages |
warning |
true/false |
Show warnings |
cache |
true/false |
Cache results (invalidated on code change) |
fig-width / fig-height |
numeric (inches) | Figure dimensions |
label |
string (no spaces) | Chunk identifier (required for cross-refs) |
Set document-wide defaults in the YAML execute: block; override
per-chunk with #| options.
# CLI render
quarto render report.qmd
quarto render report.qmd --to pdf
quarto render report.qmd --to docx
# From R
quarto::quarto_render("report.qmd", output_format = "html")
# Preview with live reload
quarto preview report.qmd
| Format | YAML format: value |
Notes |
|---|---|---|
| HTML (default) | html |
Self-contained with embed-resources: true |
pdf |
Requires LaTeX (tinytex::install_tinytex()) |
|
| Word | docx |
Use reference doc for corporate styles |
| Slides | revealjs |
HTML slideshow |
| Dashboard | dashboard |
shinylive or shiny for interactivity |
| Website | website (in _quarto.yml) |
Multi-page projects |
_quarto.ymlproject:
type: website
output-dir: _site
website:
title: "My Analysis"
navbar:
left:
- href: index.qmd
text: Home
- analysis.qmd
format:
html:
theme: cosmo
toc: true
Follow the tidyverse style guide. Key rules:
# snake_case for variables and functions
daily_revenue <- df |> group_by(date) |> summarise(rev = sum(amount))
compute_rate <- function(x, n) x / n
# No camelCase, no dots (dots reserved for S3 methods)
x <- 10 # use <- for assignment
mean(x = 10) # = is fine for function arguments
# Spaces around <- and binary operators (except ^ and :)
z <- (a + b)^2 / d
# Space after comma, not before
mean(x, na.rm = TRUE)
# No space before parenthesis in function calls
mean(x) # not mean (x)
# |> (native, R >= 4.1) — prefer over magrittr %>% for new code
# space before pipe, pipe at end of line
flights |>
filter(!is.na(arr_delay)) |>
group_by(carrier) |>
summarise(mean_delay = mean(arr_delay))
# Keep pipelines vertical when > 2 steps
# Break function args onto new lines when > ~80 chars
flights |>
mutate(
speed = distance / air_time * 60,
dep_hour = dep_time %/% 100
)
# Auto-format a file or selection
styler::style_file("analysis.R")
styler::style_dir("R/") # whole directory
# Lint for style + common bugs
lintr::lint("analysis.R")
lintr::lint_dir("R/")
# RStudio: Cmd/Ctrl+Shift+P → "styler" for palette shortcuts
Both tools are CI-friendly:
# In CI (GitHub Actions etc.)
Rscript -e "lintr::lint_dir('R/', linters = lintr::linters_with_defaults())"
targetsFor analyses where intermediate steps are slow, targets gives you
Make-like dependency tracking in R: reruns only what changed.
# _targets.R (project root)
library(targets)
tar_option_set(packages = c("tidyverse", "here"))
list(
tar_target(raw_data, read_csv(here("data", "raw.csv"))),
tar_target(clean_data, clean(raw_data)),
tar_target(model, fit_model(clean_data)),
tar_target(report, render_report(model),
format = "file")
)
# Run the pipeline
targets::tar_make()
# Visualise dependency graph
targets::tar_visnetwork()
# Check what's out of date
targets::tar_outdated()
targets integrates with renv and Quarto. Reach for it when
source("analysis.R") takes minutes and reruns waste your time.
testthat (3rd edition)# Scaffold a package or analysis project test suite
usethis::use_testthat()
# tests/testthat/test-clean.R
test_that("remove_outliers drops values beyond 3 SD", {
x <- c(1, 2, 3, 100)
result <- remove_outliers(x, sd_threshold = 3)
expect_length(result, 3)
expect_false(100 %in% result)
})
# Run all tests
devtools::test() # inside a package
testthat::test_dir("tests/testthat/") # standalone
Use expect_snapshot() for output that's hard to specify precisely
(regression tests on printed output, ggplot objects via vdiffr).
usethis scaffoldingusethis::create_project("my-pkg") # analysis project
usethis::create_package("mypkg") # R package
usethis::use_r("helpers") # R/helpers.R + tests/testthat/test-helpers.R
usethis::use_github_actions() # R-CMD-check / lintr CI
usethis::use_renv() # add renv to existing project
reprex — reproducible examplesBefore posting a question, produce a minimal reproducible example:
# Copy failing code to clipboard, then:
reprex::reprex() # formats for GitHub/Stack Overflow
reprex::reprex(venue = "so") # Stack Overflow formatting
reprex::reprex(venue = "slack") # Slack-friendly
reprex() runs your code in a clean session, captures output/errors,
and copies markdown to your clipboard. If it fails inside reprex,
your example is not self-contained — fix that first.
Include minimal data:
# Inline small data with dput()
dput(head(my_df, 10))
# Paste the output into your reprex as my_df <- <pasted output>
# Or use built-in data
reprex::reprex({
library(dplyr)
mtcars |> filter(cyl == 4) |> summarise(mpg = mean(mpg))
})
| Channel | Best for |
|---|---|
| Posit Community | Tidyverse, RStudio, Shiny, Quarto |
Stack Overflow [r] |
General R questions with reprex |
| GitHub Issues (package repo) | Confirmed bugs, feature requests |
#rstats on Mastodon/Twitter |
Community discussion |
?dplyr::mutate # function docs
vignette("dplyr") # package vignettes
browseVignettes("ggplot2") # all vignettes in browser
# pkgdown sites: https://dplyr.tidyverse.org
Both are valid. The native pipe |> works in either world with no
dependencies (R >= 4.1).
| Situation | Reach for | Why |
|---|---|---|
| Interactive analysis, EDA | tidyverse | Readable pipelines, consistent API across dplyr/tidyr/ggplot2 |
| Team projects, code review | tidyverse | Shared vocabulary lowers onboarding cost |
| Package development (public) | base R or selective imports | Minimise user-facing Imports; CRAN policy discourages heavy dep trees |
| Minimal-dep scripts / system tools | base R | No install requirements beyond R itself |
| Very large data (> memory pressure) | data.table | 2–10× faster than dplyr on multi-GB data; lower memory copies |
| Performance-critical inner loops | base R / data.table | Avoid tidyverse overhead in tight iteration |
| Subsetting / indexing gymnastics | base R [ [[ $ |
More expressive for non-rectangular access patterns |
| Apply-family parallelism | base R lapply / parallel |
No extra dependency; composes with future |
| Everything else | Your preference | Mix freely — tidyverse and base R interoperate |
Native pipe |> notes:
magrittr dependency required_ (R >= 4.2): x |> lm(y ~ ., data = _). as implicit first argument (magrittr feature)%>% in microbenchmarks (negligible in practice)# This crashes on every other machine
read_csv("/Users/mack/Desktop/data.csv")
# Use here::here() relative to project root
read_csv(here::here("data", "raw.csv"))
.RData persistence corrupts reproducibilityIf save.image() or .RData auto-restore is on, stale objects
accumulate. Scripts that "work" in your session may fail for anyone
else. Disable at project and global level — see "Do not save .RData"
above.
library() calls inside packages (vs scripts)In scripts / analysis: library(pkg) at the top is correct.
In package code (R/*.R): NEVER call library() or require().
Use pkg::function() (recommended) or declare in DESCRIPTION under
Imports: and call the function unqualified. library() in package
code modifies the user's search path silently.
# Package code — correct
clean <- function(df) {
df |> dplyr::filter(!is.na(value))
}
# Package code — wrong (affects the user's session)
library(dplyr)
clean <- function(df) df |> filter(!is.na(value))
renv + pak interactionpak must be enabled before renv::init() if you want it as the
installer. Set options(renv.config.pak.enabled = TRUE) in
.Rprofile (before renv sources its activation script) or in
renv/settings.json:
{ "package.install.backend": "pak" }
#| cache: true caches on code hash, but not on upstream data changes.
If your source data changes, manually bust:
targets::tar_invalidate("affected_target") # if using targets
# Or delete the _cache/ directory for the affected chunk
Use cache: false (the default) unless render time is genuinely painful.
here::here() root detection orderhere finds the root via (in priority order): .here file,
DESCRIPTION, .Rproj, .git, .svn. If your project has
nested git repos or unusual layouts, place an explicit .here file
at the true root with here::set_here().
| Package | Install | Purpose |
|---|---|---|
here |
CRAN | Portable paths from project root |
renv |
CRAN | Project-local library + lockfile |
pak |
CRAN | Fast, unified package installer |
quarto |
CRAN (R pkg) + quarto.org CLI | Render .qmd from R |
styler |
CRAN | Auto-format R code (tidyverse style) |
lintr |
CRAN | Static analysis / linting |
targets |
CRAN | Make-like reproducible pipelines |
testthat |
CRAN | Unit testing (3rd edition) |
usethis |
CRAN | Project / package scaffolding |
reprex |
CRAN | Minimal reproducible examples |
devtools |
CRAN | Package development workflow |
# Install the whole workflow toolkit at once
pak::pak(c(
"here", "renv", "pak", "quarto",
"styler", "lintr", "targets",
"testthat", "usethis", "reprex", "devtools"
))