| name | r-scripting |
| description | R coding standards for scripts and data analysis. Use for writing R scripts, data analysis, tidyverse workflows, or exploratory analysis. Includes guidance for creating teaching demos with knitr::spin (literate R scripts that render to PDF/HTML). Not for package development. |
R Scripting
Core Principles
- Clear over clever - readability trumps brevity
- Explicit over implicit - all function inputs as arguments (no globals)
- Functional over imperative - prefer
map() over loops when clearer
Design Principles
Top-Down Design
Decompose problems before coding:
- Problem → high-level subtasks
- Subtasks → smaller pieces
- Code → implement bottom-up
Start with the big picture, then drill down.
DRY (Don't Repeat Yourself)
Extract repeated code into functions—errors fix in one place, changes propagate automatically.
result_a <- data_a |> filter(x > 0) |> mutate(y = log(x)) |> summarize(m = mean(y))
result_b <- data_b |> filter(x > 0) |> mutate(y = log(x)) |> summarize(m = mean(y))
compute_log_mean <- function(data) {
data |> filter(x > 0) |> mutate(y = log(x)) |> summarize(m = mean(y))
}
results <- map(list(data_a, data_b), compute_log_mean)
One Task Per Function
Keep functions short (~20-50 lines). If a function does multiple things, split it.
Naming
calculate_mean <- function(x) mean(x, na.rm = TRUE)
filter_outliers <- function(data, threshold = 3) { ... }
patient_ages <- c(25, 32, 41)
model_predictions <- predict(fit)
MAX_ITERATIONS <- 1000
DEFAULT_ALPHA <- 0.05
Formatting
result <- x + y
result <- raw_data |>
filter(age > 18) |>
mutate(log_income = log(income)) |>
summarize(mean = mean(log_income))
map(data, \(x) x^2 + 1)
- 2 spaces indentation, ~80 chars/line
- Use native pipe
|> (R ≥ 4.1) or %>%
- Run
air format . after every major edit (not just before commit). A pre-commit hook enforces this, so formatting early avoids wasted cycles.
Functions
my_function <- function(data, threshold = 0.05, verbose = FALSE) {
stopifnot(
"data must be data.frame" = is.data.frame(data),
"threshold must be positive" = threshold > 0
)
if (nrow(data) == 0) return(NULL)
process_data(data, threshold)
}
Pure Functions (Critical)
THRESHOLD <- 0.05
filter_data <- function(df) df |> filter(p < THRESHOLD)
filter_data <- function(df, threshold = 0.05) df |> filter(p < threshold)
All inputs must be explicit arguments. Same inputs → same outputs.
Control Flow
Return Early, Keep Happy Path Flat
Handle edge cases and errors first, then let the main logic flow with minimal indentation.
process <- function(x, method) {
if (!is.null(x)) {
if (is.numeric(x)) {
if (method == "mean") {
return(mean(x))
} else {
return(median(x))
}
}
}
return(NA)
}
process <- function(x, method = c("mean", "median")) {
if (is.null(x)) return(NA)
if (!is.numeric(x)) return(NA)
method <- match.arg(method)
switch(method,
mean = mean(x),
median = median(x)
)
}
Homogenize Inputs Early
Normalize variants at the start, then work with a single form:
method <- match.arg(method)
type <- tolower(type)
arg <- arg %||% default_value
Vectorized Conditions
result <- ifelse(x > 0, "pos", "neg")
category <- case_when(
score >= 90 ~ "A",
score >= 80 ~ "B",
TRUE ~ "F"
)
result <- switch(method,
"mean" = mean(x, na.rm = TRUE),
"median" = median(x, na.rm = TRUE),
stop("Unknown method: ", method)
)
Functional Programming (purrr)
map_dbl(data, compute_value)
map_lgl(data, is_valid)
map2(x, y, \(a, b) a + b)
pmap(list(x, y, z), \(a, b, c) a * b + c)
safe_mean <- possibly(mean, otherwise = NA)
Data Manipulation (tidyverse)
result <- raw_data |>
filter(!is.na(value)) |>
mutate(log_value = log(value), scaled = scale(value)) |>
group_by(category) |>
summarize(n = n(), mean = mean(value)) |>
arrange(desc(mean))
df |> mutate(across(where(is.numeric), scale))
df |> summarize(across(where(is.numeric), list(mean = mean, sd = sd)))
Error Handling
stopifnot(
"x must be numeric" = is.numeric(x),
"denom cannot be zero" = all(denom != 0)
)
if (x < 0) {
stop("x must be non-negative, got x = ", x,
"\nTry using abs(x) or filtering negatives")
}
result <- tryCatch(
risky_computation(data),
error = function(e) { message("Failed: ", e$message); NULL }
)
Script Structure
library(tidyverse)
DATA_PATH <- "data/input.csv"
compute_stats <- function(x) { ... }
data <- read_csv(DATA_PATH)
results <- compute_stats(data)
write_csv(results, "output/results.csv")
Section headers: Use # Section Name ---- or # Section Name ------ format (dashes to ~col 80). RStudio recognizes these for code folding. Never use multi-line # ==== block headers.
Anti-Patterns
Code Examples
significant <- filter(results, p < 0.05)
ALPHA <- 0.05
significant <- filter(results, p < ALPHA)
summary_a <- data_a |> filter(valid) |> summarize(m = mean(val))
summary_b <- data_b |> filter(valid) |> summarize(m = mean(val))
calc_summary <- function(d) d |> filter(valid) |> summarize(m = mean(val))
summaries <- map(list(data_a, data_b), calc_summary)
try(risky(x), silent = TRUE)
tryCatch(risky(x), error = function(e) { message("Failed: ", e$message); NA })
Code Smells & Fixes
| Smell | Problem | Fix |
|---|
| Long functions (>50 lines) | Hard to understand/test | Extract subtasks into helpers |
| Long parameter lists | Unwieldy API | Group related args into list |
| Data clumps | Variables that always appear together | Unify into single object/list |
| Dead/commented-out code | Confusion, maintenance burden | Delete (Git has history) |
| Deep nesting | Hard to follow | Return early, flatten logic |
| High cyclomatic complexity | Many paths = many bugs | Target < 10 per function |
Cyclomatic Complexity
Measure with cyclocomp package—target < 10 per function:
cyclocomp::cyclocomp(my_function)
Comments
data <- mutate(data, log_val = log(value + 1))
rolling <- zoo::rollmean(values, k = 7)
Teaching Demos with knitr::spin
For literate R scripts that render to PDF/HTML, see knitr-spin-reference.md in this skill folder.
Key points: use #' prefix for prose, #+ for chunk options, interleave code and explanation.
Tools
lintr::lint("script.R")
air format script.R
air format .
Quick Checklist