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:

  1. Every script starts with a standard header (Purpose / Inputs / Outputs / Depends)
  2. Names alone should convey meaning (no abbreviations, no generic names)
  3. No hardcoding (absolute paths and magic numbers become named constants)
  4. Keep scripts small (200 lines per file, pipes of 5 steps or fewer)
  5. 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 hardcoded setwd() 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 to data/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; always YYYYMMDD)
  • File names are snake_case nouns describing the content
  • Names like final.R, new2.R, and test_copy.R are 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 / Depends are 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

ItemTargetHard limit
Line width80 characters100 characters
Lines per file~200 lines300 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).

FormatUseExample
snake_casevariables, functions, file namespatient_age, clean_outcomes(), 01_clean.R
SCREAMING_SNAKE_CASEconstantsN_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, x2 are 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 own library() call
  • For packages used only once or twice in a script, call them as package::function() instead of adding a library() 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 / FALSE must 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.

ForbiddenDo 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() to outputs/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_RATE means “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:

  1. Hand over a single file: the header (Purpose / Inputs / Outputs / Depends) carries the context — just paste the file
  2. Include dependencies when they exist: attach the files listed under Depends: (usually 00_prepare.R)
  3. 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)
  4. 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.

  1. Is the header (Purpose / Inputs / Outputs / Depends) present?
  2. Does the code respect the 80-character target (100 hard limit) and 200-line guideline?
  3. Are names specific, unabbreviated, and correctly formatted (snake_case / SCREAMING_SNAKE_CASE; functions are verbs, variables are nouns; no PascalCase functions)?
  4. Any hardcoding (absolute paths, magic numbers, setwd())?
  5. Pipes ≤ 5 steps and nesting ≤ 3 levels?
  6. Does the layout follow header → libraries → constants → functions → executed statements?
  7. tidyverse over base R; meta packages over individual library() calls?
  8. Are arguments passed by name?
  9. Is set.seed() present before stochastic steps?
  10. 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