An R Style Guide for Research Teams — Copy-Paste It as an LLM Prompt
You open an R script from a collaborator and stall on the first line: what is d2, and which file are you supposed to run first? Or you open a script you yourself wrote three months ago — and have the same experience.
This article publishes, in full, the R coding style guide our research team actually uses. It is written so that beginners and self-taught R users can use it directly for self-study. At the end, you will also find a condensed version you can copy-paste into ChatGPT or Claude — tell the model to follow it, and it will write code under the same conventions your team uses.
In short, the guide rests on five pillars:
- Every script starts with a standard header (Purpose / Inputs / Outputs / Depends)
- Names alone should convey meaning (no abbreviations, no generic names)
- No hardcoding (absolute paths and magic numbers become named constants)
- Keep scripts small (200 lines per file, pipes of 5 steps or fewer)
- Full compliance with the tidyverse style guide
The rest of this article explains why each rule exists, with before/after examples.
目次
Why a Style Guide? Code Is a Communication Problem
The fundamental theorem of readable code
The Art of Readable Code (Boswell & Foucher, 2012) states the principle that runs through this entire guide:
Code should be written to minimize the time it would take for someone else to understand it.
The Art of Readable Code (Boswell & Foucher, 2012)
“Someone else” includes yourself three months from now — and the LLM you will hand the code to. The goal is not merely code that runs; it is code that minimizes the reader’s time-to-understanding.
Programming as communication: four virtues
Economist Chishio Furukawa (Yokohama National University) frames programming in his lecture materials as a communication problem, and names four virtues of good code:
- Clear — the intent shows through
- Effective — the computer executes it correctly
- Consistent — the reader can predict what comes next
- Flexible — collaborators can modify it easily
Two of his points are especially important. First, good names replace comments: if variable and function names accurately describe what they do, most comments become unnecessary. Second, don’t start by typing code — write pseudocode first (a plain-language sketch of the logic), settle the logic, and only then translate it into syntax.
The idea that the code itself is the documentation underlies every rule below.
Project Setup: Let the Environment Prevent the Errors It Can
- Always use an R Project (
.Rproj). Working-directory problems disappear, and hardcodedsetwd()calls become unnecessary - Write all paths relative to the project root (
here::here()is recommended) - Delegate formatting to
{styler}and static checks to{lintr}(both available as RStudio Addins) - For projects that need frozen package versions — e.g., analyses attached to a manuscript submission — use
{renv}
For everything stylistic, we defer to the Tidyverse Style Guide. The point is to let tools enforce style instead of fixing indentation by eye.
Directory Structure: Never Wonder Where Anything Lives
R_project_directory/
├─ project_name.Rproj
├─ README.md # Purpose, how to run, dependencies
├─ data/
│ ├─ raw/ # Original data (read-only; never overwrite)
│ └─ processed/ # Intermediate data generated by scripts
├─ utils/ # Scripts
│ ├─ 00_prepare.R
│ ├─ 01_clean.R
│ ├─ 02_analyze.R
│ └─ appendix.R
└─ outputs/ # Figures/tables/results (always regenerable by script)
Four principles:
data/raw/is read-only. Processed results go todata/processed/- Everything in
outputs/must be regenerable by re-running the scripts - Directory names are plural or collective nouns (
outputs,utils) - Never commit clinical data (
data/raw/, etc.) to Git — add it to.gitignore
File Naming: final.R Is Forbidden
- Prefix files that have an execution order with numbers (
00_prepare.R,01_clean.R,02_analyze.R) - Suffix files that need version tracking with a date (
analysis_20260708.R; alwaysYYYYMMDD) - File names are
snake_casenouns describing the content - Names like
final.R,new2.R, andtest_copy.Rare forbidden
The moment you name a file “final,” final_v2.R is born — a universal law of research labs. Use numbers and dates instead.
Script Headers: One File Should Carry Its Own Context
Every script starts with the following header. With it, a single file handed to an LLM carries its own context.
#1. Copyright statement -----
# © 2026 Example Research Team. All rights reserved.
#2. Author -----
# Author: Taro Yamada
# Date: 2026-08-08
# Contact: taro.yamada@example.com
#3. File description -----
# Purpose: Run the primary analysis (random-effects meta-analysis)
# Inputs: data/processed/outcomes_clean.csv
# Outputs: outputs/forest_plot.png, outputs/main_results.csv
# Depends: utils/00_prepare.R (run first)
# Style: conforms to r_script_style_guide.md
#4. source() and library() -----
source("./utils/00_prepare.R")
library(tidyverse)
#5. Function definitions -----
#6. Executed statements -----
Purpose / Inputs / Outputs / Dependsare mandatory. Humans and LLMs alike read these first to grasp the script’s role- Section dividers use the
#X. Heading -----format consistently (this feeds both RStudio’s outline pane and an LLM’s section recognition)
This six-part header layout (Copyright → Author → File description → source/library → Function definitions → Executed statements) is based on the structure Chishio Furukawa uses in his research supervision.
The standard 00_prepare.R:
Sys.setenv(LANGUAGE = "en_US.UTF-8")
rm(list = ls()) # clear memory
cat("\014") # clear console
if (!is.null(dev.list())) dev.off() # clear plots
Script Layout and Size Limits
Write in this order: header → libraries → constants → function definitions → executed statements. Keep executed statements at the end of the script; never interleave them with function definitions.
#5. Function definitions -----
run_all <- function() {
outcomes <- read_outcomes()
outcomes_clean <- clean_outcomes(outcomes)
analyze_outcomes(outcomes_clean)
}
read_outcomes <- function() { ... }
clean_outcomes <- function(outcomes) { ... }
analyze_outcomes <- function(outcomes_clean) { ... }
#6. Executed statements -----
run_all()
The payoff: reading run_all() alone gives you the entire shape of the analysis.
Size guidelines
| Item | Target | Hard limit |
|---|---|---|
| Line width | 80 characters | 100 characters |
| Lines per file | ~200 lines | 300 lines |
| Pipe steps | ≤ 5 steps | — |
| Nesting depth | ≤ 3 levels | — |
- Past 200 lines, consider splitting the file or extracting functions
- The 300-line hard limit is also the practical ceiling for handing a file to an LLM (review accuracy drops for longer files)
- If a pipe exceeds 5 steps, split it into meaningfully named intermediate variables
- If nesting gets deep, extract a named function
Naming Conventions: The Name Alone Should Tell the Story
Both functions and variables use snake_case (PascalCase is the convention for S4/R6 class names, so it is not used for functions).
| Format | Use | Example |
|---|---|---|
snake_case | variables, functions, file names | patient_age, clean_outcomes(), 01_clean.R |
SCREAMING_SNAKE_CASE | constants | N_BOOTSTRAP, SEED |
The tidyverse style guide has no explicit rule for constants, so we adopt SCREAMING_SNAKE_CASE as the team convention — it is recognized across languages. {lintr} flags uppercase constants by default, so configure .lintr:
linters: linters_with_defaults(
object_name_linter = object_name_linter(
styles = c("snake_case", "SNAKE_CASE", "symbols")
)
)
The principles we adopted from The Art of Readable Code:
- Do not abbreviate:
patient_age✓ /pt_age✗ - Choose specific verbs:
fetch_trial_data()✓ /get()✗ - Avoid generic names:
tmp,retval,data,df,x2are forbidden - Encode units and types in names:
duration_ms,size_mb,is_eligible - Variables and file names are nouns; functions are verbs; directories and tables are plural; columns are singular
- The wider the scope, the more descriptive the name
- Boolean variables take an
is_/has_/use_prefix
# Good
patient_data <- read_csv(here::here("data", "raw", "patients.csv"))
MIN_FOLLOWUP_WEEKS <- 4
clean_outcomes <- function(outcomes) { ... }
# Bad
d <- read_csv("patients.csv")
weeks <- 4
clean <- function(x) { ... }
CleanOutcomes <- function(outcomes) { ... } # no PascalCase functions
Coding Priorities
Prefer tidyverse over base R
{tidyverse} is the first choice for data manipulation: purrr::map() over the apply() family, filter() over subset(). Uniformity across the team is itself worth paying for.
Load meta packages, not individual packages
# Good
library(tidyverse)
# Bad
library(dplyr)
library(ggplot2)
library(readr)
library(tidyr)
- Exception: packages not in the tidyverse (
{meta},{metafor}, etc.) get their ownlibrary()call - For packages used only once or twice in a script, call them as
package::function()instead of adding alibrary()line (e.g.,here::here(),janitor::clean_names())
Name your arguments
Readers — human or LLM — should not need to memorize a function’s signature to understand the call. Pass arguments by name.
# Good
metagen(
TE = log_rr,
seTE = se_log_rr,
data = outcomes_clean,
sm = "RR",
method.tau = "REML"
)
# Bad
metagen(log_rr, se_log_rr, outcomes_clean)
- Only the first argument (the data or main input) may be passed by position
- Arguments taking
TRUE/FALSEmust always be named (na.rm = TRUE)
Use %>% for pipes
Use the {magrittr} pipe %>% (preferred over base R’s |> for consistency with the tidyverse). Never mix the two within a project.
No Hardcoding: Give Every Value a Name
The following must never be written directly in code.
| Forbidden | Do this instead |
|---|---|
Absolute paths (/Users/xxx/..., C:\...) | .Rproj + relative paths, or here::here() |
setwd() | unnecessary with .Rproj |
Magic numbers (a bare 4 or 5000) | define as uppercase constants at the top of the file |
| Repeated strings (column names, group names) | constants or function arguments |
| API keys / passwords | .Renviron / environment variables (never commit to Git) |
# Good
MIN_FOLLOWUP_WEEKS <- 4
INCLUDED_ARMS <- c("CBT-I", "waitlist")
eligible_trials <- trials %>%
filter(followup_weeks >= MIN_FOLLOWUP_WEEKS, arm %in% INCLUDED_ARMS)
# Bad
eligible_trials <- trials %>%
filter(followup_weeks >= 4, arm %in% c("CBT-I", "waitlist"))
- If the same value appears in two or more places, it must become a constant
- Define constants together right after the header (between sections 4 and 5)
A bare 4 forces the reader to guess: four weeks? four trials? the fourth column? MIN_FOLLOWUP_WEEKS requires no guessing. Naming an eligibility threshold also documents an analytic decision.
Reproducibility: Script All the Way to the Result
- Call
set.seed()immediately before any stochastic step (bootstrap, multiple imputation, simulation). Define the seed as a constant (SEED) - When producing final outputs, save the result of
sessionInfo()tooutputs/session_info.txt - Never include manual steps (editing in Excel, etc.) in the analysis pipeline. The entire path from raw data to final output must be reproducible by script alone
Beyond the Rules: More Practices from The Art of Readable Code
You can follow every rule above and still write hard-to-read code. Here are four additional practices from The Art of Readable Code that pay off immediately in analysis code.
Comments explain why, never what
A comment that restates the code makes the reader pay twice. Comments exist to record background, reasons, and warnings the code cannot express.
# Bad: a translation of the code
# read the data
outcomes <- read_csv("data/raw/outcomes.csv")
# Good: something the code cannot tell you
# some source files record missing values as "-", so na must be set explicitly
outcomes <- read_csv("data/raw/outcomes.csv", na = c("", "NA", "-"))
When naming is done well, most comments can be deleted. The team motto: if you feel the urge to write a comment, first suspect the name.
Use explaining variables for complex conditions
Give the condition itself a name.
# Bad: unreadable in one pass
trials_included <- trials %>%
filter(n_total >= 10, followup_weeks >= MIN_FOLLOWUP_WEEKS, !is_crossover)
# Good: the eligibility definition gets a single name
trials <- trials %>%
mutate(
is_eligible = n_total >= MIN_SAMPLE_SIZE &
followup_weeks >= MIN_FOLLOWUP_WEEKS &
!is_crossover
)
trials_included <- trials %>% filter(is_eligible)
A side benefit: the is_eligible column persists, so you can later inspect why any given trial was excluded.
Flatten nesting with early returns
When branching gets deep, handle the exceptions first and exit (guard clauses), keeping the main logic at the left margin.
# Bad: the real work is pushed to the right
summarize_outcome <- function(outcomes) {
if (nrow(outcomes) > 0) {
if (all(c("mean", "sd") %in% names(outcomes))) {
# the actual work starts here
}
}
}
# Good: reject the exceptions early, keep the main path flat
summarize_outcome <- function(outcomes) {
if (nrow(outcomes) == 0) {
stop("outcomes has no rows")
}
if (!all(c("mean", "sd") %in% names(outcomes))) {
stop("outcomes must contain columns: mean, sd")
}
# the actual work starts here
}
Choose names that can’t be misread
Before finalizing a name, ask: could this be interpreted another way?
- Make boundary semantics explicit: use
max_for an inclusive upper bound (e.g.,MAX_MISSING_RATEmeans “up to and including this value”) - Keep booleans positive:
is_eligible✓ /is_not_excluded✗ (double negatives force the reader to flip logic in their head) - Name the result of a
filter()after what remains (trials_included,outcomes_complete)
Handing Scripts to an LLM: Where This Guide Pays Off
Scripts written under these rules can be handed directly to an LLM (Claude, ChatGPT, etc.) for review, modification, and extension. Four steps:
- Hand over a single file: the header (Purpose / Inputs / Outputs / Depends) carries the context — just paste the file
- Include dependencies when they exist: attach the files listed under
Depends:(usually00_prepare.R) - Describe the data, don’t paste it: attach the output of
dplyr::glimpse()instead of the data itself (column names, types, and examples in one shot — and it keeps clinical data out of external services) - Attach the guide: tell the LLM “conform to this style guide” and it will write code under the same conventions
Copy-paste prompt for LLMs (condensed version)
Copy the block below and paste it into your request whenever you ask an LLM for R code.
You are an assistant that writes R code. Always follow this style guide.
## Principles
- Conform to the Tidyverse Style Guide (https://style.tidyverse.org/)
- Write code to minimize the reader's time-to-understanding (the fundamental
theorem of readable code)
- Aim for: Clear (intent shows), Consistent (predictable), Flexible (easy to modify)
## Script structure
- Every script starts with this header:
#1. Copyright / #2. Author / #3. File description (Purpose, Inputs, Outputs,
Depends) / #4. source() and library() / #5. Function definitions /
#6. Executed statements
- Section dividers use the "#X. Heading -----" format
- Order: header -> libraries -> constants -> function definitions -> executed
statements (executed statements go last, never interleaved with functions)
- 80 characters per line as the target (100 hard limit); ~200 lines per file
(300 hard limit); pipes <= 5 steps; nesting <= 3 levels
## Naming
- Variables, functions, file names: snake_case (no PascalCase functions)
- Constants: SCREAMING_SNAKE_CASE, defined together at the top of the file
- No abbreviations (pt_age -> patient_age). tmp / retval / data / df / x2
are forbidden
- Functions are verbs; variables are nouns. Booleans take is_ / has_ / use_
prefixes and are phrased positively
- Encode units in names (duration_ms, followup_weeks)
## Coding
- Prefer tidyverse over base R (apply family -> purrr::map, subset -> filter)
- Load library(tidyverse), not individual tidyverse packages.
Non-tidyverse packages get their own library() call; packages used once or
twice are called as package::function()
- Use %>% for pipes (never mix with |>)
- Pass arguments by name (positional only for the first argument;
TRUE/FALSE arguments must always be named)
## Forbidden
- Absolute paths and setwd() (use here::here() and relative paths)
- Magic numbers (bare numbers become uppercase constants; any value appearing
twice must become a constant)
- Hardcoded API keys or passwords (use .Renviron)
- Comments that restate the code (comments explain why, never what)
## Reproducibility
- Call set.seed() immediately before stochastic steps (seed defined as the
constant SEED)
- The full path from raw data to final output must run by script alone,
with no manual steps
## When asked to review code
- For each violation, state which rule is violated and show a corrected example
Review Checklist
In code review — by a human or an LLM — check in this order. For each violation, state which rule is violated and show a corrected example.
- Is the header (Purpose / Inputs / Outputs / Depends) present?
- Does the code respect the 80-character target (100 hard limit) and 200-line guideline?
- Are names specific, unabbreviated, and correctly formatted (snake_case / SCREAMING_SNAKE_CASE; functions are verbs, variables are nouns; no PascalCase functions)?
- Any hardcoding (absolute paths, magic numbers,
setwd())? - Pipes ≤ 5 steps and nesting ≤ 3 levels?
- Does the layout follow header → libraries → constants → functions → executed statements?
- tidyverse over base R; meta packages over individual
library()calls? - Are arguments passed by name?
- Is
set.seed()present before stochastic steps? - Have
{styler}and{lintr}been run?
FAQ
What should I do with my existing, self-taught scripts?
You don’t need to rewrite them by hand. Paste the condensed prompt above together with your script into an LLM and ask it to “refactor this to conform to the style guide, without changing behavior.” One non-negotiable step remains yours: verify that the refactored script produces the same output as the original (compare the numbers).
I learned base R. Do I really have to relearn things the tidyverse way?
For shared team code, yes. What you do in your private analyses is your business, but the readability of shared code depends heavily on everyone writing the same way. Start with just three things — filter(), mutate(), and %>% — and you will be able to read most of the team’s shared code.
How should I split an analysis that exceeds 200 lines?
Split by processing stage — loading, cleaning, analysis, plotting — into numbered files (01_clean.R, 02_analyze.R, …). Pass data between files via data/processed/; each file then becomes an independent unit you can hand to an LLM on its own.
Why %>% instead of the native |>?
Functionally the difference is minor; the deciding factor is consistency. Our existing code base and most tidyverse teaching materials use %>%, so we standardize on it to avoid mixing. Standardizing a brand-new project on |> is a defensible choice too — the one hard rule is never mix the two within a project.
Closing: A Style Guide Is Consideration, Systematized
If this guide had to fit in one sentence: minimize the time it takes your readers — future you, your collaborators, and LLMs — to understand your code.
- Start every script with the header (Purpose / Inputs / Outputs / Depends)
- Make names carry the meaning (no abbreviations, no generic names, no magic numbers)
- Stay within 200 lines per file, 5 pipe steps, 3 nesting levels
- Let
{styler}and{lintr}do the formatting and checking - Keep the whole pipeline reproducible by script, with zero manual steps
Start with the next script you write — even just the header and the naming rules. Then share this article with your collaborators, hand the condensed prompt to your LLM, and watch the whole team’s code converge on the same readable shape.
References
- Boswell D, Foucher T. The Art of Readable Code. O’Reilly Media; 2012.
- Wickham H. The tidyverse style guide
- Furukawa C. Micro data science lecture materials (Yokohama National University) — programming as a communication problem; the four virtues (Clear / Effective / Consistent / Flexible); writing pseudocode before code

名古屋市立大学医学部卒業後、南生協病院での初期研修を経て、東京大学医学部附属病院精神神経科、東京武蔵野病院で専攻研修。日本専門医機構認定精神科専門医、精神保健指定医。臨床と並行してメタアナリシスを中心とした臨床研究を主導。筆頭著者として、JAMA Psychiatry, British Journal of Psychiatry, Schizophrenia Bulletin, Psychiatry and Clinical Neuroscienceなどのトップジャーナルに論文を発表。不眠の認知行動療法 (CBT-I) などの心理療法や、精神科疾患の薬物療法について、臨床で抱いた疑問に取り組んでいる。メディア報道・講演など。
免責事項:当ウェブサイトは所属団体の意見を代表するものではありません。管理人は、細心の注意を払って当ウェブサイトに情報を作成していますが、情報の正確性および完全性を保証するものではありません。当ウェブサイトの情報もしくはリンク先の情報を利用したことで直接・間接的に生じた損失に関し、管理人は一切責任を負いません。