
A Reproducible R Workflow for Data Analysis
Build a reproducible R data analysis workflow with explicit imports, validation checks, recorded exclusions and script-generated outputs. Follow a synthetic training-score example from source file to handover.
On this page
- 1. Start with a question and a data dictionary
- 2. Use a project folder with clear boundaries
- 3. Create a separate teaching file without overwriting data
- 4. Import with explicit expectations
- 5. Turn critical assumptions into checks
- 6. Make missing-pair handling visible
- 7. Calculate and export a descriptive summary
- 8. Record the environment and decisions
- 9. Rerun from a fresh session
- 10. Share the workflow without exposing private data
- A practical handover exercise
- Questions readers ask
- Official references and further reading
A reproducible R data analysis workflow keeps the question, data checks, transformations and outputs connected in a script. Another person should be able to understand where the numbers came from and, where access permits, rerun the analysis without guessing which console commands you used.
This guide builds a small project with synthetic training scores. The workflow intentionally stops when important checks fail. That is useful behavior: an informative error before analysis is better than a polished report built from the wrong file, duplicated records or misunderstood missing values.
1. Start with a question and a data dictionary
Write the question before importing a spreadsheet. For the teaching example: “What are the recorded pre-training and post-training scores, and what descriptive change appears among complete pairs?” This is a descriptive question. It does not ask whether training caused the change.
Define one row as one participant’s pair of scores. Document the identifier, score scale, missing-value convention and source. If a participant can legitimately appear more than once, a different key may be needed. A duplicate identifier is not always an error; it depends on the unit of observation.
Our data collection methods guide explains how the question and observation should align. A reproducible script preserves decisions, but it cannot make an unsuitable measurement valid.
2. Use a project folder with clear boundaries
Create a project with separate folders for inputs, scripts and generated outputs. Keep the source file unchanged. When cleaning is necessary, make the transformation in code and write a different output file rather than editing the only source copy.
dir.create("data", showWarnings = FALSE)
dir.create("scripts", showWarnings = FALSE)
dir.create("outputs", showWarnings = FALSE)Run paths relative to the project folder so the project does not depend on your personal desktop location. Avoid switching working directories repeatedly in the middle of a script. Give files descriptive names that explain their role, not just “new,” “final” or “final2.”
If R is not installed, begin with R and RStudio setup. The code below uses functions supplied with R and does not require extra packages.
3. Create a separate teaching file without overwriting data
The following synthetic values make the workflow easy to inspect. The file is written only if it does not already exist. Use a dedicated practice project; do not paste this setup into a folder containing real participant records.
demo <- data.frame(
id = c("P01", "P02", "P03", "P04", "P05", "P06"),
pre = c(50, 60, 55, 70, 65, 58),
post = c(60, 68, 64, 75, 74, NA_real_)
)
input_file <- file.path("data", "training-demo.csv")
if (file.exists(input_file)) {
message("Existing demo file retained: ", input_file)
} else {
write.csv(demo, input_file,
row.names = FALSE, na = "")
}The explicit no-overwrite behavior protects an existing practice file. It also means rerunning setup will not silently erase edits you made to test a validation rule. If you want to reset the practice data, make that a deliberate action after inspecting the file.
R’s data output reference documents write.csv(). Choosing row.names = FALSE avoids adding a row-name column that can later be mistaken for a participant identifier.
4. Import with explicit expectations
d <- read.csv(
input_file,
stringsAsFactors = FALSE,
na.strings = c("", "NA"),
colClasses = c(id = "character",
pre = "numeric",
post = "numeric")
)
str(d)
summary(d)
colSums(is.na(d))The identifier is kept as text. This is important for identifiers with leading zeroes or characters that should not be treated as quantities. The score fields are expected to be numeric, so an unexpected text entry should prompt investigation rather than an unnoticed change of meaning.
Specify missing-value codes that genuinely belong to the dataset. Do not assume that 0, 99 or “not applicable” has the same meaning in every source. The official R data input reference explains import options and defaults.
5. Turn critical assumptions into checks
required <- c("id", "pre", "post")
if (!identical(names(d), required)) {
stop("Unexpected columns or column order.")
}
if (anyNA(d$id) || any(trimws(d$id) == "")) {
stop("Participant identifier is missing.")
}
if (anyDuplicated(d$id)) {
stop("Duplicate participant identifiers: investigate first.")
}
if (any(d$pre < 0 | d$pre > 100, na.rm = TRUE) ||
any(d$post < 0 | d$post > 100, na.rm = TRUE)) {
stop("A score is outside the defined 0–100 scale.")
}These checks encode this example’s data contract. They are not universal rules for every training dataset. The column-order check is deliberately strict; in another project, checking names without requiring a particular order may be more appropriate.
Do not automatically remove duplicated rows before defining what makes a record unique. Two genuine events can share the same values. Conversely, two records with different timestamps may still represent an accidental repeat submission. Investigate the process that generated the records.
6. Make missing-pair handling visible
complete_pair <- complete.cases(d[c("pre", "post")])
excluded <- d[!complete_pair, , drop = FALSE]
analysis_data <- d[complete_pair, , drop = FALSE]
if (nrow(analysis_data) == 0L) {
stop("No complete pairs are available.")
}
analysis_data$change <- analysis_data$post - analysis_data$pre
write.csv(excluded,
file.path("outputs", "excluded-demo-records.csv"),
row.names = FALSE)The example retains five complete pairs and records the incomplete case separately. The excluded file is synthetic here. In a real project, an exclusion log can contain sensitive information, so keep it in an appropriately restricted location and include only necessary fields.
Complete-case analysis is not automatically unbiased. Ask why the post-score is missing and whether people with incomplete data differ from those retained. Record the reason for the decision rather than describing the reduced dataset as if every original participant supplied complete results.
7. Calculate and export a descriptive summary
result <- data.frame(
input_rows = nrow(d),
complete_pairs = nrow(analysis_data),
excluded_rows = nrow(excluded),
mean_pre = mean(analysis_data$pre),
mean_post = mean(analysis_data$post),
mean_change = mean(analysis_data$change)
)
result
write.csv(result,
file.path("outputs", "training-demo-summary.csv"),
row.names = FALSE)
png(file.path("outputs", "training-demo-change.png"),
width = 1000, height = 700)
plot(analysis_data$pre, analysis_data$post,
xlab = "Pre-training score", ylab = "Post-training score",
pch = 16, col = "#1658b9")
abline(a = 0, b = 1, lty = 2)
dev.off()For the unchanged teaching file, mean pre-score is 60.0, mean post-score is 68.2 and mean paired change is 8.2 points. The dashed line marks equal pre and post values. These are descriptive outputs from constructed data, not evidence of a training effect.
Without a suitable comparison design and other relevant information, a pre/post difference cannot isolate training from practice effects, other changes or selection. For planning learning evaluation, read the employee training module example.
8. Record the environment and decisions
capture.output(sessionInfo(),
file = file.path("outputs", "session-info.txt"))
writeLines(
c("Dataset: synthetic teaching example.",
"Unit: one participant with a pre/post pair.",
"Score scale: 0–100.",
"Analysis: descriptive summary of complete pairs.",
"No causal training-effect claim is made."),
file.path("outputs", "analysis-notes.txt")
)sessionInfo() records useful R, system and package information; it is not a complete frozen environment. For long-lived projects, review appropriate dependency-management practices and retain the instructions needed to obtain permitted inputs.
A readme should explain the question, files, execution order and expected outputs. Document decisions that code alone does not explain: why a measure was chosen, why a group was excluded or why a particular report is intended for a limited audience.
9. Rerun from a fresh session
Restart R and run the saved script from the first line. The workflow should not require an object left behind by yesterday’s console commands. Check that expected files are generated and the numerical summary matches the teaching values before modifying the example.
Then deliberately test one failure at a time in a copy of the practice file. Add a duplicate identifier, rename a column or enter a score outside the defined range. Confirm that the script stops with a useful message. Restore the valid teaching file before using the example for another purpose.
These checks help detect predictable mistakes, but they do not certify that every assumption is correct. A script can run reproducibly while asking the wrong question. Interpretation remains part of the analyst’s responsibility.
10. Share the workflow without exposing private data
Reproducibility and unrestricted public access are different matters. If the real source cannot be shared, provide a data dictionary, permitted access instructions and a synthetic example that exercises the code. Remove identifiers from public reports and follow the institution’s authorization requirements.
Keep original inputs, transformation scripts and report outputs distinguishable. If you later add regression or ANOVA, document the model question and assumptions instead of treating a successful function call as an analysis plan.
- The question and unit of observation are explicit.
- Source data are retained without silent overwriting.
- Types, identifiers, missing values and ranges are checked.
- Exclusions are recorded and justified.
- Results are written by the script, not manually copied.
- Software/session information and decision notes are saved.
- The project runs from a fresh session.
- Shared materials respect confidentiality and access permissions.
The result is a workflow another person can inspect, question and repeat where permitted. That is more valuable than a long collection of clever commands with no clear connection between the input file, the analysis choices and the conclusion.
A practical handover exercise
Give a colleague an appropriately shareable copy of the practice project without explaining the file layout verbally. Ask them to locate the question, identify the input file and run the saved script. Note where they need information that is missing from the readme.
Then ask them to explain what the value 8.2 means. A correct answer should identify it as the mean difference in score points among the five complete synthetic pairs. It should not describe 8.2 as a percentage improvement, a causal training effect or the change for every participant.
Review the generated summary and exclusion file together. Can the colleague tell that one input record did not contribute a complete pair? Can they find the reason this matters to interpretation? If those answers depend on remembering a conversation, improve the notes rather than adding more unexplained code.
For a real handover, also check access permissions and ownership. A project can be technically organized but unusable to a colleague who cannot obtain the approved input. Record the authorized access process without embedding credentials in the script. The objective is an understandable and responsible analysis package, not unrestricted distribution of confidential records.
Questions readers ask
Does a script that runs without errors guarantee correct analysis?
No. A reproducible script can still use unsuitable measurements or an inappropriate method. Review the question, design and interpretation as well as execution.
Should duplicated records always be deleted?
No. Define the observational unit and investigate how records were generated. A duplicate value pattern may represent either a genuine repeat or a data problem.
Why keep original data separate from outputs?
It preserves the source and makes transformations traceable. Generated files should not silently overwrite the only original input.
Is complete-case analysis always unbiased?
No. Its suitability depends on the missing-data process and analysis. Record excluded observations and explain the limitations of the decision.
Does sessionInfo freeze the entire project environment?
No. It records useful session information but is not a complete frozen environment. Longer-lived projects may require additional dependency-management practices.
Must confidential data be published to make a workflow reproducible?
No. Sharing must respect permissions and confidentiality. Provide suitable documentation, authorized access instructions and a synthetic example where appropriate.
Official references and further reading
- R: reading tabular data
- R: CSV and tabular output
- R: recording session information
- R: checking expressions with stopifnot
Sources checked 19 October 2026. Follow the linked official documents for current software requirements and method details. The examples are educational and do not establish results from an actual study.
Editorial update: Oxford Training and Research. Prepared with AI-assisted drafting; the original author record is retained. Worked examples are illustrative. No independent field study or specialist review is claimed.


