---
title: "A reproducible research project"
subtitle: "From explicit inputs to a result another researcher can rebuild"
guide-status: "Available"
---
**Before reading:** Basic R functions and file paths; no prior workflow system is required.
## A result should have an executable route back to its inputs
A reproducible project records what another researcher needs to regenerate a stated result: inputs, acquisition and inclusion rules, code, configuration, software environment, and a documented execution command. This makes an argument inspectable. It does not establish that its target is meaningful or its assumptions are true.
For a first project, use a small structure whose responsibilities are clear:
```text
project/
README.md # question, inputs, environment, execution instructions
data/raw/ # original inputs, or documented controlled acquisition
data/metadata/ # manifests, identifiers, acquisition and decision logs
R/ # functions with explicit inputs and return values
config/ # analysis settings and simulation scenarios
results/ # derived tables, figures, and replicate-level records
reports/ # Quarto sources connecting results to claims
renv.lock # R package environment, if using renv
```
Sensitive or restricted inputs need not be committed to a repository. Record how an authorized collaborator obtains the correct version, and provide synthetic data for exercising the pipeline where appropriate. Keep raw inputs unchanged; regenerate derived objects through code. Follow [Data provenance and an analysis-ready dataset](data-provenance.qmd) for identifiers and transformation records.
## Separate a function from an analysis run
A function should expose its statistical inputs. Avoid an estimator that silently reads a variable from an interactive workspace, changes its target according to a filename, or reuses a previously fitted model. An analysis run supplies the data and settings, calls functions, and saves labeled results.
A function is the statistical recipe; a run is a particular application of that recipe. For example, `SummarizePeople(y, confidence)` computes a mean interval from supplied values. `GeneratePeople(config)` creates synthetic inputs. The runner reads settings, calls those functions, and writes outputs. The estimator does not receive the generator's true population mean.
The [downloadable teaching project](../examples/research-project.zip) contains four files: `R/analysis.R`, `config/config.csv`, `run.R`, and `README.md`. It uses only standard R packages and invented people. The example below unpacks that actual archive into a temporary directory, reads its configuration, and computes the result. Students should keep an unpacked project in a persistent location when working through it.
```{r}
work_dir <- tempfile("teaching project-"); dir.create(work_dir)
unzip("../examples/research-project.zip", exdir = work_dir)
project_root <- normalizePath(file.path(work_dir, "research-project"))
source(file.path(project_root, "R", "analysis.R"))
config <- ReadConfig(file.path(project_root, "config", "config.csv"))
people <- GeneratePeople(config)
result <- SummarizePeople(people$outcome, config$confidence)
result
```
The configuration specifies 80 independent normal outcomes, population mean 2, seed 71, and confidence level 0.95. The interval is a normal-population t interval for the mean, in synthetic measurement units. Its assumptions come from this generator; running the code on dependent cells would require a different uncertainty argument.
### Rebuild in a fresh R process
From the unpacked project directory, the documented entry point is:
```{.bash}
Rscript --vanilla run.R . results/run-01
Rscript --vanilla run.R . results/run-02
```
The dot names the project root, and the final argument names a new output directory relative to it. Each command starts a separate R process. The runner finds its functions and configuration from the explicit root, so it can also be called from elsewhere using the appropriate script and root paths. It refuses an existing output directory, keeping the two attempts separate.
This page performs those fresh-process runs rather than merely repeating a function in its existing workspace:
```{r}
FreshRun <- function(root, output_relative) {
command_output <- system2(file.path(R.home("bin"), "Rscript"),
args = c("--vanilla", shQuote(file.path(root, "run.R")),
shQuote(root), shQuote(output_relative)),
stdout = TRUE, stderr = TRUE)
dir.create(file.path(root, "logs"), showWarnings = FALSE)
writeLines(command_output,
file.path(root, "logs", paste0(basename(output_relative), ".txt")))
status <- attr(command_output, "status")
if (!is.null(status) && status != 0L) stop(paste(command_output, collapse = "\n"))
out <- file.path(root, output_relative)
stopifnot(file.exists(file.path(out, "COMPLETE")))
readRDS(file.path(out, "summary.rds"))
}
run_01 <- FreshRun(project_root, "results/run-01")
run_02 <- FreshRun(project_root, "results/run-02")
stopifnot(isTRUE(all.equal(result, run_01, tolerance = 1e-12)),
isTRUE(all.equal(run_01, run_02, tolerance = 1e-12)))
data.frame(run = c("Current process", "Fresh process 1", "Fresh process 2"),
estimate = c(result$estimate, run_01$estimate, run_02$estimate),
SE = c(result$se, run_01$se, run_02$se))
```
Agreement shows that these inputs and code reproduce the numeric result without relying on a fitted object left in the first process. It does not establish correctness of the statistical question or guarantee identical file bytes on every platform. State which outputs must agree and at what tolerance. Exact ID sets, row counts, and categorical decisions often deserve exact checks; floating-point estimates may need a justified numeric tolerance. “The conclusions looked similar” is too vague to serve as an output check.
<span id="fig-project-execution"></span>
<span id="fig-project-execution-title"></span>
### Inspect what the run retained
Each completed run saves the synthetic people, summary table and R object, configuration used, input/code manifest, output checksums, R session details, and RNG setup. Its final `COMPLETE` file is written after the listed outputs. An interrupted directory without that marker is retained as an incomplete attempt, rather than mistaken for a successful analysis. The marker describes execution status, not statistical validity.
::: {.table-responsive}
```{r}
input_manifest <- read.csv(file.path(project_root, "results", "run-01",
"input-manifest.csv"))
knitr::kable(input_manifest, row.names = FALSE)
```
:::
Read the manifest as a statement of dependencies. Changing `config/config.csv` changes the requested run even when the functions are identical. Changing `R/analysis.R` changes the procedure even when the settings are identical. The checksum identifies each saved file's contents; the role column says why it matters. A real omics project additionally needs acquired data versions, annotation references, preprocessing, and exclusion rules. An analysis notebook's filename alone does not identify those dependencies.
## Record the environment and randomness
A seed controls a random-number stream under a given setup; it does not identify the software, input data, or entire parallel execution schedule. Retain R and package versions, operating-system details when relevant, code revision, configuration, and seeds or per-replicate streams. Use stable replicate IDs when splitting simulation work across workers. Two jobs that each reset the same seed can accidentally generate duplicate replicates.
For projects using **renv**, its documented workflow includes initialization, recording package versions with a snapshot, and restoration from the lockfile. These are commands for a deliberately configured project, not commands automatically executed by this page:
```{r}
#| eval: false
renv::init()
renv::snapshot()
# In a checkout that already contains the intended lockfile:
renv::restore()
```
A lockfile covers an important part of the R environment, not every external system dependency or input dataset. Document those separately. Check the restored project in a fresh environment before describing it as reproducible.
The base-R example uses `--vanilla` to skip startup files and saved workspaces, as described in [R's startup documentation](https://stat.ethz.ch/R-manual/R-devel/library/base/html/Startup.html). In a project using renv, however, the normal automatic activation is in `.Rprofile`, which that flag also skips. Activate the documented project environment explicitly, for example by sourcing `renv/activate.R`, or use the project's documented startup route. A fresh session must still use the intended environment.
Keep randomness tied to the unit being reproduced. An analysis with learned nuisances may need fold assignments and tuning seeds; a simulation may need one recorded stream per replicate. Saving only the first seed can leave results dependent on method order or worker scheduling. The run record should let a collaborator recover the same dataset and fitting choices for a particular replicate, not merely obtain a similar overall rejection rate.
## Keep code changes reviewable
Use version control for code, configuration, report sources, and documentation. Make focused commits with messages explaining the analysis decision, and record the code revision used for a run. If results were generated with uncommitted changes, retain the corresponding changes as well; a commit identifier alone does not describe that state. Review a change together with its affected results and assumptions.
Keep credentials and restricted data outside the repository, with documented acquisition paths and appropriate ignore rules. Large derived outputs can be stored separately when they remain linked to the run manifest and can be regenerated. Collaborators should agree on which files are authoritative inputs and which are rebuildable outputs.
A protocol and a run manifest answer different questions. The protocol records the planned target, methods, exclusions, and comparisons, with reasons for amendments. A run manifest records the actual inputs, code, settings, and outputs of one attempt. Retain both: reproducing an exploratory revision does not make it prespecified. New outputs should receive a new run ID, rather than silently replacing the evidence for an earlier draft.
## Use caching deliberately
Quarto's `freeze` feature can reuse prior computation during project renders; it makes repeated builds faster. A rendered page is therefore not evidence that its code was executed in that build. A data file or helper function may change without the report source itself changing.
Record a clean execution procedure that recomputes the analysis when inputs, settings, functions, or dependencies change. Check the run log and output timestamps or run identifiers. Treat cached/frozen results as outputs with provenance, and review Quarto's [execution](https://quarto.org/docs/projects/code-execution.html) and [caching](https://quarto.org/docs/computations/caching.html) documentation when defining that procedure.
For example, `exclusions.csv` changes while the report source is untouched. A global project render using `freeze: auto` may reuse the old computation. Knitr caching is another layer: unchanged chunk text does not automatically detect every changed external input. For an executable R report, explicitly rendering the file bypasses project-level freeze, and `--cache-refresh` refreshes chunk caches:
```{.bash}
quarto render reports/analysis.qmd --cache-refresh
```
Run the project's analysis entry point first if the report reads previously saved results rather than fitting the model itself. Refreshing a report cache cannot regenerate a result file that no report chunk creates. Confirm that the displayed result belongs to the new input manifest and run ID; a recent HTML timestamp alone only establishes that a page was written.
## Decide what to retain
For a simple analysis, keep the result table, diagnostics, input manifest, configuration, session information, code revision, and report source. For a simulation, keep one record per attempted replicate and method, including failures and seeds. Avoid relying on a manually edited spreadsheet as the only record of a result.
A useful README answers: What scientific target is studied? What files or access permissions are required? Which environment is supported? What command runs the project? Which outputs should appear? What is intentionally excluded from a public release? A collaborator should not need your interactive command history to discover these answers.
::: {#rerun-procedure .callout-note icon=false title="Procedure: audit a collaborator's rerun"}
1. Start from the documented code revision and acquire the recorded input versions through the permitted route.
2. Establish the stated environment and a fresh analysis process, with project activation made explicit.
3. Execute the documented entry point into a new run directory, retaining its status and logs.
4. Check expected files, identifiers, dimensions, diagnostics, and numeric results against the stated comparison criteria.
5. Resolve any discrepancy by tracing changed inputs, settings, code, software, or randomness. Record the resolution and any remaining limitation.
**Completion means:** the declared result is recoverable through the documented route. Its scientific assumptions remain part of the research argument.
:::
## Exercises
Try each checkpoint before opening its hint or solution.
::: {#checkpoint-rep-01 .checkpoint}
### Checkpoint · REP-01 — Specify a complete rerun contract {.unnumbered}
**Task type:** Construction and diagnosis.
A report renders successfully on its author’s laptop but fails in a fresh R session because `fit` is absent. Its frozen output also predates a changed sample-exclusion file. Write a minimal project structure and rerun procedure that resolves both problems. Identify what a seed and a package lockfile each fail to record.
Use the downloadable teaching project to perform two fresh-process runs. State which inputs and outputs you compare, how you distinguish an incomplete run, and whether each comparison needs byte identity, exact records, or numeric agreement.
**Ready to continue when:** another reader can identify the inputs, configuration, environment, entry point, and expected outputs, and you can verify a rerun without your interactive workspace.
**Transfer:** How would you preserve reproducibility if raw data could only be accessed inside a controlled computing environment?
<details class="checkpoint-help">
<summary>Hint</summary>
Follow the dependencies of `fit` and the exclusion decision all the way to the reported table.
</details>
<details class="checkpoint-help">
<summary>Solution outline — open after trying</summary>
Create fit from explicit data and settings in a function called by the entry script or report. Record the exclusion file/version in an input manifest, regenerate the analysis after it changes, and rebuild the report with recomputation verified. Document acquisition, environment restoration, execution, and output checks. A seed does not specify inputs or package versions; a package lockfile does not specify biological inclusion rules, all system dependencies, or acquired data. Test the instructions in a fresh session.
For the teaching project, run the same configuration into two new directories, inspect their input manifests and completion markers, and compare the numeric summaries at the stated tolerance. A completion marker without matching inputs is insufficient. A renamed output directory need not make every recorded path identical; the comparison should concern the scientific output and the dependencies that produced it.
</details>
:::
### Additional practice (optional) {.unnumbered}
1. Apply the README questions to a small existing project and ask a collaborator to perform the rerun.
2. Split a toy simulation into two batches with distinct replicate IDs and verify that the combined record has no duplicate replicates.
## Reading
- Wilson et al., [Good enough practices in scientific computing](https://journals.plos.org/ploscompbiol/article?id=10.1371/journal.pcbi.1005510).
- Official [renv introduction](https://rstudio.github.io/renv/articles/renv.html) and [Quarto project execution](https://quarto.org/docs/projects/code-execution.html).
<span id="continue-through-the-collection"></span>