A reproducible research project

From explicit inputs to a result another researcher can rebuild

Before reading: Basic R functions and file paths; no prior workflow system is required.

1 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:

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 for identifiers and transformation records.

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

Code
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
  n_people estimate        se confidence    lower    upper
1       80 2.021953 0.1162457       0.95 1.790572 2.253335

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.

2.1 Rebuild in a fresh R process

From the unpacked project directory, the documented entry point is:

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:

Code
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))
              run estimate        SE
1 Current process 2.021953 0.1162457
2 Fresh process 1 2.021953 0.1162457
3 Fresh process 2 2.021953 0.1162457

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.

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

Code
input_manifest <- read.csv(file.path(project_root, "results", "run-01",
                                      "input-manifest.csv"))
knitr::kable(input_manifest, row.names = FALSE)
path role checksum
config/config.csv configuration e8784c6e9aeafd8d014d28ceb7386a17
R/analysis.R functions fa68c28933b1a691f6cbf27e9b48c61f
run.R entry point 90a574b941d9a1fbba17f627cc9f9460

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.

3 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:

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

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

5 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 and caching 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:

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.

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

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

7 Exercises

Try each checkpoint before opening its hint or solution.

Checkpoint · REP-01 — Specify a complete rerun contract

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?

Hint

Follow the dependencies of fit and the exclusion decision all the way to the reported table.

Solution outline — open after trying

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.

Additional practice (optional)

  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.

8 Reading

Back to top