Prof. Marc Los Huertos • Pomona College
Why It Matters Create a Project Folder Structure File Paths The here Package Naming Files Workflow Verify
What this module covers. A reproducible analysis is one that anyone — including your future self — can re-run from scratch on a new machine and get identical results. The foundation of reproducibility is project structure: keeping your raw data, code, and outputs in predictable locations and referencing them with paths that work everywhere. This module builds that foundation using RStudio Projects and the
herepackage, with examples drawn from the EA030 server atrstudio.pomona.edu.
A common beginner workflow looks something like this: all files in
one folder (or worse, the Desktop), hardcoded paths like
C:/Users/Jamie/Desktop/my_stuff/data.csv, and a folder that
has accumulated names like final_final_v3_REAL.R. When you
open it six weeks later — or hand it to a collaborator — nothing
works.
A structured project solves this:
Desktop/
data.csv
analysis.R
analysis_v2.R
analysis_FINAL.R
plot.png
plot_good.png
notes.docx
ea030_streams/
data/raw/stream_sites.csv
data/processed/sites_clean.csv
scripts/01_clean.R
scripts/02_analyse.Rmd
output/figures/do_vs_temp.png
ea030_streams.Rproj
The problems with the unstructured approach are not cosmetic — they cause real failures:
.Rproj file means R does not know
where the working directory is📌 The reproducibility test
A project passes the reproducibility test if a colleague can clone or
download it, open the .Rproj file, and run every script in
order — with zero modifications — and reproduce all your figures and
tables. This is the standard you are aiming for in EA030.
coursecode_topic — for example:ea030_streams
~) — this places the project in your home
directory on the serverSteps 2–6 are identical. The project folder appears wherever you chose in step 5.
✅ What the .Rproj file does
The .Rproj file is the anchor of your project. When you
open it (by double-clicking in Finder/Explorer, or through File
→ Open Project in RStudio), three things happen
automatically:
You never need to call setwd() again.
| Scenario | Project name | Notes |
|---|---|---|
| EA030 lab on stream chemistry | ea030_stream_chemistry |
One project for each lab keeps labs independent |
| EA030 lab on bird survey | ea030_bird_survey |
Never mix two labs in one project |
| Separate independent research project | research_chaparral_birds |
Entirely separate from course work |
| Homework exercises | ea030_homework |
Reasonable to group all HW in one project with numbered subfolders |
ea030_streams.Rproj ←
open this to enter the project
data/
raw/ ← original files, NEVER edited
stream_sites.csv
water_quality_2024.xlsx
processed/ ← cleaned copies
produced by scripts
sites_clean.csv
scripts/
01_load_clean.R ← numbered in run
order
02_visualise.R
03_analysis.Rmd
output/
figures/ ← plots saved by scripts
tables/ ← CSV summaries, knitr
tables
reports/ ← knitted HTML / PDF files
README.md ← one paragraph: what
is this project?
stream_sites.csv, sensor_log.xlsx
scripts/. Always reproducible from raw.
sites_clean.csv, merged_2023_2024.csv
.R and .Rmd files, numbered in the order
they should be run. Each script has one clear purpose.
01_load_clean.R, 02_eda.Rmd
do_vs_temp.png, site_map.pdf
summary_stats.csv, model_coefs.csv
03_analysis.html, lab_report.pdf
Run this once in the Console after creating the project. It builds the entire skeleton instantly:
# Run once from the Console — creates all project folders
dirs <- c(
"data/raw", "data/processed",
"scripts",
"output/figures", "output/tables", "output/reports"
)
invisible(lapply(dirs, dir.create, recursive = TRUE, showWarnings = FALSE))
# Verify
list.dirs(recursive = TRUE)
Or create them manually in the Files pane: click New Folder, name it, then navigate inside and repeat for subfolders.
An absolute path encodes the full location of a file from the root of the file system:
read.csv(“/home/jsmith/ea030_streams/data/raw/stream_sites.csv”)
# On the server it becomes:
read.csv(“/home/mloshuertos/ea030_streams/…”)
# On a Mac:
read.csv(“/Users/jamie/Documents/…”)
read.csv(“data/raw/stream_sites.csv”)
# Works because RStudio
Projects set the
# working directory to the project root,
# so
all paths start from there.
A relative path is measured from the working
directory — and because .Rproj always sets the
working directory to the project root,
"data/raw/stream_sites.csv" resolves correctly whether you
are on the Pomona server, your laptop, or a collaborator’s machine.
⚠️ setwd() is a red flag
If you find yourself writing setwd("/some/long/path"),
stop. It means a file is being referenced from outside the project, or
the project is not open. The fix is to open the project via the
.Rproj file and use relative paths — never
setwd().
Relative paths like "data/raw/file.csv" work most of the
time, but they depend on the working directory being set correctly.
Inside a knitted .Rmd, the working directory is the folder
containing the .Rmd file — not necessarily the project
root. This causes subtle bugs when scripts are in a subfolder.
The here package always resolves paths from the
project root, regardless of where the script lives.
{#here}
library(here)
# here() returns the absolute path to the project root
here()
# "/home/jsmith/ea030_streams"
# Construct paths safely
read.csv(here("data", "raw", "stream_sites.csv"))
# Save output to a subfolder
ggsave(here("output", "figures", "do_vs_temp.png"),
width = 7, height = 4)
⚙️ here() vs. relative paths — when does it matter?
| Situation | Bare relative path | here() |
|---|---|---|
| Script in project root | ✅ Works | ✅ Works |
Script in scripts/ subfolder |
⚠️ May fail if WD not root | ✅ Always works |
Inside a knitted .Rmd |
⚠️ WD = .Rmd location |
✅ Always works |
| Shared with a collaborator | ✅ Works if paths are short | ✅ Works |
Use here() consistently and you will never debug a path
error again.
library(here)
here() # should print the path ending in your project name
01_load_clean.R
02_exploratory_plots.Rmd
03_linear_regression.R
stream_sites_raw_2024-05-12.csv
do_vs_temp_fig1.png
README.md
analysis.R
analysis FINAL (2).R
my data.csv
résultats.csv
Figure 1 (good one!).png
untitled.R
Number scripts with a two-digit prefix so they sort in run order in any file browser:
# Correct — sorts as 01, 02, 03 ... 10, 11
01_load_raw_data.R
02_clean_and_filter.R
03_exploratory_analysis.Rmd
04_linear_regression.R
05_report.Rmd
# Wrong — sorts as 1, 10, 11, 2, 3 (lexicographic order)
1_load.R
10_final.R
2_clean.R
When a file is versioned by date (raw data downloads, for example),
use ISO 8601 format (YYYY-MM-DD) — it
sorts chronologically:
# Good — sorts chronologically
stream_discharge_2024-03-15.csv
stream_discharge_2024-04-02.csv
stream_discharge_2024-11-01.csv
# Bad — ambiguous, sorts wrong
stream_discharge_3-15-24.csv
stream_discharge_Nov_data.csv
Every analysis starts the same way: load raw data, inspect it, clean it, save a processed copy. No output figures yet — just getting the data ready.
# scripts/01_load_clean.R
# Purpose: load raw stream data, check, clean, and save processed copy
# Author: your name
# Date: 2024-05-12
library(tidyverse)
library(here)
library(janitor)
# ── 1. Load ────────────────────────────────────────────────────────
raw <- read_csv(here("data", "raw", "stream_sites.csv"))
# ── 2. Inspect ─────────────────────────────────────────────────────
glimpse(raw)
summary(raw)
# ── 3. Clean ───────────────────────────────────────────────────────
clean <- raw %>%
clean_names() %>% # snake_case column names
filter(!is.na(dissolved_oxygen_mgl)) %>% # drop rows missing DO
mutate(
date = as.Date(date, "%Y-%m-%d"),
site_id = factor(site_id),
do_mgl = dissolved_oxygen_mgl,
temp_c = water_temp_c
) %>%
select(date, site_id, do_mgl, temp_c, flow_cms, conductivity_us)
# ── 4. Save processed copy ─────────────────────────────────────────
write_csv(clean, here("data", "processed", "stream_clean.csv"))
cat("Saved:", nrow(clean), "rows to data/processed/stream_clean.csv\n")
# scripts/02_exploratory_plots.R
# Purpose: initial visualisations of cleaned stream data
library(tidyverse)
library(here)
clean <- read_csv(here("data", "processed", "stream_clean.csv"))
# ── DO vs temperature ──────────────────────────────────────────────
p_do_temp <- ggplot(clean, aes(temp_c, do_mgl, colour = site_id)) +
geom_point(alpha = 0.55, size = 2) +
geom_smooth(method = "lm", se = FALSE, linewidth = 0.8) +
scale_colour_brewer(palette = "Dark2", name = "Site") +
labs(x = "Water temperature (°C)", y = "Dissolved oxygen (mg/L)",
title = "DO–Temperature Relationship by Site") +
theme_minimal(base_size = 13) +
theme(plot.title = element_text(family = "serif", face = "bold",
colour = "#1e3a2f"))
# Save — never use Export button for reproducible figures
ggsave(here("output", "figures", "do_vs_temp.png"),
plot = p_do_temp, width = 7, height = 4, dpi = 150)
cat("Figure saved to output/figures/do_vs_temp.png\n")
⚠️ Never use the RStudio Export button for final figures
The Export button in the Plots pane lets you save a figure
interactively — but the saved size and resolution depend on how you
manually resize the window. Use ggsave() with explicit
width, height, and dpi arguments
so figures are identical every time the script runs.
---
title: "Stream Chemistry Analysis"
author: "Your Name"
date: "`r Sys.Date()`"
output:
html_document:
self_contained: true
toc: true
---
```{r load, include=FALSE}
library(tidyverse)
library(here)
clean <- read_csv(here("data", "processed", "stream_clean.csv"))
```
```{r do-temp-plot, fig.cap="DO decreases with temperature across all sites.", echo=FALSE}
ggplot(clean, aes(temp_c, do_mgl, colour = site_id)) +
geom_point(alpha = 0.55) +
geom_smooth(method = "lm", se = FALSE) +
theme_minimal()
```
Knit with Ctrl/⌘+Shift+K.
The rendered HTML lands in the same folder as the .Rmd —
move it to output/reports/ manually, or set the
knit: option in the YAML to write there directly (as all
EA030 tutorials do).
Run this chunk to audit your project. It checks that all required folders exist, warns about any absolute paths in your scripts, and lists any raw data files.
# Audit the current project structure
required_dirs <- c("data/raw", "data/processed",
"scripts", "output/figures",
"output/tables", "output/reports")
cat("── Folder audit ──────────────────────────────\n")
## ── Folder audit ──────────────────────────────
for (d in required_dirs) {
exists <- dir.exists(d)
icon <- if (exists) "✅" else "❌ MISSING"
cat(sprintf(" %s %s\n", icon, d))
}
## ❌ MISSING data/raw
## ❌ MISSING data/processed
## ❌ MISSING scripts
## ❌ MISSING output/figures
## ❌ MISSING output/tables
## ❌ MISSING output/reports
cat("\n── Working directory ─────────────────────────\n")
##
## ── Working directory ─────────────────────────
cat(" ", getwd(), "\n")
## /home/mwl04747/RTricks/01_RGuides
cat("\n── R scripts found in scripts/ ───────────────\n")
##
## ── R scripts found in scripts/ ───────────────
if (dir.exists("scripts")) {
scripts <- list.files("scripts", pattern = "\\.(R|Rmd)$",
recursive = TRUE)
if (length(scripts) > 0) {
cat(paste(" •", scripts), sep = "\n")
} else {
cat(" (no scripts yet)\n")
}
} else {
cat(" scripts/ folder not found\n")
}
## scripts/ folder not found
.Rproj file — never navigate files outside a project.
data/raw/. Scripts read from it and write to
data/processed/.
setwd(), no
absolute paths. Use here() for robustness inside Rmd files
and subfolders.
01_,
02_, … so run order is unambiguous.
ggsave(), not the Export button, so figures are
reproducible.