R Development
Follow these guidelines when writing or reviewing R code. Prioritize modern tidyverse patterns (dplyr 1.1+), native pipe, explicit namespacing, and performance-aware practices.
Core Principles
- Use modern tidyverse patterns — Prefer dplyr 1.1+ features, native pipe, and current APIs
- Profile before optimizing — Use profvis and bench to identify real bottlenecks
- Write readable code first — Optimize only when necessary and after profiling
- Follow tidyverse style guide — Consistent naming, spacing, and structure
- Use explicit namespacing — Write
package::function()(e.g.,dplyr::filter(),stringr::str_detect()). Never rely onlibrary()calls. This makes code self-documenting about where each function comes from and avoids namespace conflicts - No
\dontrun{}in examples — All roxygen2@examplesmust be runnable. Never wrap examples in\dontrun{}— this hides broken code fromR CMD check. If an example needs external resources, use\donttest{}instead or make the example self-contained
Modern Tidyverse Essentials
Native Pipe (|> not %>%)
Use native pipe |> instead of magrittr %>% (R 4.1+):
# Modern
data |>
dplyr::filter(year >= 2020) |>
dplyr::summarise(mean_value = mean(value))
# Avoid legacy pipe
data %>% dplyr::filter(year >= 2020)
Join Syntax (dplyr 1.1+)
Use dplyr::join_by() for all joins:
# Equality join
transactions |>
dplyr::inner_join(companies, by = dplyr::join_by(company == id))
# Inequality join
transactions |>
dplyr::inner_join(companies, dplyr::join_by(company == id, year >= since))
# Rolling join (closest match)
transactions |>
dplyr::inner_join(companies, dplyr::join_by(company == id, closest(year >= since)))
Control match behavior:
# Expect 1:1 matches
dplyr::inner_join(x, y, by = dplyr::join_by(id), multiple = "error")
# Ensure all rows match
dplyr::inner_join(x, y, by = dplyr::join_by(id), unmatched = "error")
Per-Operation Grouping with .by
Use .by instead of dplyr::group_by() |> ... |> dplyr::ungroup():
# Modern approach (always returns ungrouped)
data |>
dplyr::summarise(mean_value = mean(value), .by = category)
# Multiple grouping variables
data |>
dplyr::summarise(total = sum(revenue), .by = c(company, year))
Column Operations
Use modern column selection and transformation:
# pick() for column selection in data-masking contexts
data |>
dplyr::summarise(
n_x_cols = ncol(dplyr::pick(starts_with("x"))),
n_y_cols = ncol(dplyr::pick(starts_with("y")))
)
# across() for applying functions to multiple columns
data |>
dplyr::summarise(
dplyr::across(where(is.numeric), mean, .names = "mean_{.col}"),
.by = group
)
# reframe() for multi-row results per group
data |>
dplyr::reframe(quantiles = quantile(x, c(0.25, 0.5, 0.75)), .by = group)
Data Reshaping
Use tidyr::pivot_longer() and tidyr::pivot_wider() for reshaping:
# Wide to long
data |>
tidyr::pivot_longer(
cols = starts_with("year_"),
names_to = "year",
names_prefix = "year_",
values_to = "value"
)
# Long to wide
data |>
tidyr::pivot_wider(
names_from = category,
values_from = value,
values_fill = 0
)
Reading and Writing Data
Use readr for text-based formats and readxl for Excel:
data <- readr::read_csv("data.csv")
readr::write_csv(data, "output.csv")
# Excel files
data <- readxl::read_excel("data.xlsx", sheet = "Sheet1")
# R-native format for intermediate results
readr::write_rds(data, "cached.rds")
data <- readr::read_rds("cached.rds")
String Operations
Prefer stringr for consistent, pipe-friendly string manipulation:
text |>
stringr::str_to_lower() |>
stringr::str_trim() |>
stringr::str_replace_all("old", "new")
# Pattern matching
stringr::str_detect(text, "pattern")
stringr::str_extract_all(text, "\\d+")
# String interpolation
stringr::str_glue("Column {col} has {n} values")
Functional Programming with purrr
Use type-stable map variants and modern purrr 1.0+ patterns:
# Type-stable mapping
purrr::map_dbl(data_list, \(df) mean(df$value))
purrr::map_chr(data_list, \(df) df$name[[1]])
# Row-binding results (purrr 1.0+, replaces map_dfr)
results <- data_splits |>
purrr::map(\(split) process(split)) |>
purrr::list_rbind()
# Walking for side effects
purrr::walk2(plots, filenames, \(p, f) ggplot2::ggsave(f, p))
# Safely handling errors
safe_read <- purrr::safely(readr::read_csv)
results <- purrr::map(file_paths, safe_read)
successes <- purrr::map(results, "result") |> purrr::compact()
ggplot2 Essentials
Build plots with the layered grammar of graphics:
data |>
ggplot2::ggplot(ggplot2::aes(x = year, y = value, color = group)) +
ggplot2::geom_point() +
ggplot2::geom_smooth(method = "lm") +
ggplot2::facet_wrap(~category) +
ggplot2::labs(title = "Title", x = "Year", y = "Value") +
ggplot2::theme_minimal()
Apply consistent theming across an analysis by defining a custom theme function or setting ggplot2::theme_set() at the top of the script. Use ggplot2::ggsave() to export plots with explicit dimensions and DPI.
rlang Metaprogramming
For comprehensive rlang patterns, see references/rlang-patterns.md.
Quick Reference
{{}}— Forward function arguments to data-masking functions!!— Inject single expressions or values!!!— Inject multiple arguments from a list.data[[]]— Access columns by name (character vectors)dplyr::pick()— Select columns inside data-masking functions
Example function with embracing:
my_summary <- function(data, group_var, summary_var) {
data |>
dplyr::summarise(mean_val = mean({{ summary_var }}), .by = {{ group_var }})
}
Performance Optimization
For detailed performance guidance, see references/performance.md.
Key Strategies
- Profile first: Use
profvis::profvis()andbench::mark() - Vectorize operations: Avoid loops when vectorized alternatives exist
- Use dtplyr: For large data operations (lazy evaluation with data.table backend)
- Parallel processing: Use
furrr::future_map()for parallelizable work - Memory efficiency: Pre-allocate, use appropriate data types
# Profile code
profvis::profvis({
result <- data |>
complex_operation() |>
another_operation()
})
# Benchmark alternatives
bench::mark(
approach_1 = method1(data),
approach_2 = method2(data),
check = FALSE
)
Common Migration Patterns
Base R to Tidyverse
# Data manipulation
subset(data, condition) # -> dplyr::filter(data, condition)
data[order(data$x), ] # -> dplyr::arrange(data, x)
aggregate(x ~ y, data, mean) # -> dplyr::summarise(data, mean(x), .by = y)
# Functional programming
sapply(x, f) # -> purrr::map(x, f) (type-stable)
lapply(x, f) # -> purrr::map(x, f)
# Strings
grepl("pattern", text) # -> stringr::str_detect(text, "pattern")
gsub("old", "new", text) # -> stringr::str_replace_all(text, "old", "new")
Old to New Tidyverse
# Pipes
%>% # -> |>
# Grouping
group_by() |> ... |> ungroup() # -> dplyr::summarise(..., .by = x)
# Joins
by = c("a" = "b") # -> by = dplyr::join_by(a == b)
# Reshaping
gather() / spread() # -> tidyr::pivot_longer() / tidyr::pivot_wider()
Additional Resources
For detailed guidance beyond the essentials above, consult these reference files:
- references/rlang-patterns.md — Comprehensive data-masking and metaprogramming patterns including embracing, injection, dynamic dots, and pronouns
- references/performance.md — Profiling with profvis, benchmarking with bench, vectorization, dtplyr for large data, and memory optimization
- references/package-development.md — API design patterns for tidyverse-style package functions: dependency strategy, input validation, error handling, and naming conventions. For devtools workflow, testing commands, roxygen2 documentation, and NEWS.md conventions, defer to the r-package-development skill
- references/object-systems.md — S3, S4, S7, R6, and vctrs: decision matrix for choosing an object system, class definitions, and migration strategies
Improve this skill from use
After completing a task with this skill, reflect on whether its instructions or resources revealed a
gap, ambiguity, stale instruction, avoidable friction, or error. If concrete evidence surfaced, include
a brief Skill feedback note in the handoff or final response that names the affected file or section
and proposes the smallest useful correction. Do not invent feedback when no issue surfaced, and do not
edit the skill during an unrelated task without the user's authorization.