Claude R Tidyverse Expert This document outlines current best practices for R development using modern tidyverse patterns, emphasizing the use of the native pipe (`|>`) over the legacy magrittr pipe, and recommending `join_by()` for joins. It also covers key techniques such as embracing function arguments with `{{ }}`, using `.by` for per-operation grouping, and employing `pick()`, `across()`, and `reframe()` for efficient and readable code. Modern R Development Guide This document captures current best practices for R development, emphasizing modern tidyverse patterns, performance, and style. Last updated: August 2025 Core Principles 1. Use modern tidyverse patterns - Prioritize dplyr 1.1+ features, native pipe, and current APIs 2. Profile before optimizing - Use profvis and bench to identify real bottlenecks 3. Write readable code first - Optimize only when necessary and after profiling 4. Follow tidyverse style guide - Consistent naming, spacing, and structure Modern Tidyverse Patterns Pipe Usage | not % % - Always use native pipe | instead of magrittr % % - R 4.3+ provides all needed features r Good - Modern native pipe data | filter year = 2020 | summarise mean value = mean value Avoid - Legacy magrittr pipe data % % filter year = 2020 % % summarise mean value = mean value Join Syntax dplyr 1.1+ - Use join by instead of character vectors for joins - Support for inequality, rolling, and overlap joins r Good - Modern join syntax transactions | inner join companies, by = join by company == id Good - Inequality joins transactions | inner join companies, join by company == id, year = since Good - Rolling joins closest match transactions | inner join companies, join by company == id, closest year = since Avoid - Old character vector syntax transactions | inner join companies, by = c "company" = "id" Multiple Match Handling - Use multiple and unmatched arguments for quality control r Expect 1:1 matches, error on multiple inner join x, y, by = join by id , multiple = "error" Allow multiple matches explicitly inner join x, y, by = join by id , multiple = "all" Ensure all rows match inner join x, y, by = join by id , unmatched = "error" Data Masking and Tidy Selection - Understand the difference between data masking and tidy selection - Use {{}} embrace for function arguments - Use .data for character vectors r Data masking functions: arrange , filter , mutate , summarise Tidy selection functions: select , relocate , across Function arguments - embrace with {{}} my summary <- function data, group var, summary var { data | group by {{ group var }} | summarise mean val = mean {{ summary var }} } Character vectors - use .data for var in names mtcars { mtcars | count .data var | print } Multiple columns - use across data | summarise across {{ summary vars }}, ~ mean .x, na.rm = TRUE Modern Grouping and Column Operations - Use .by for per-operation grouping dplyr 1.1+ - Use pick for column selection inside data-masking functions - Use across for applying functions to multiple columns - Use reframe for multi-row summaries r Good - Per-operation grouping always returns ungrouped data | summarise mean value = mean value , .by = category Good - Multiple grouping variables data | summarise total = sum revenue , .by = c company, year Good - pick for column selection data | summarise n x cols = ncol pick starts with "x" , n y cols = ncol pick starts with "y" Good - across for applying functions data | summarise across where is.numeric , mean, .names = "mean {.col}" , .by = group Good - reframe for multi-row results data | reframe quantiles = quantile x, c 0.25, 0.5, 0.75 , .by = group Avoid - Old persistent grouping pattern data | group by category | summarise mean value = mean value | ungroup Modern rlang Patterns for Data-Masking Core Concepts Data-masking allows R expressions to refer to data frame columns as if they were variables in the environment. rlang provides the metaprogramming framework that powers tidyverse data-masking. Key rlang Tools - Embracing {{}} - Forward function arguments to data-masking functions - Injection - Inject single expressions or values - Splicing - Inject multiple arguments from a list - Dynamic dots - Programmable ... with injection support - Pronouns .data / .env - Explicit disambiguation between data and environment variables Function Argument Patterns Forwarding with {{}} Use {{}} to forward function arguments to data-masking functions: r Single argument forwarding my summarise <- function data, var { data | dplyr::summarise mean = mean {{ var }} } Works with any data-masking expression mtcars | my summarise cyl mtcars | my summarise cyl am mtcars | my summarise .data$cyl pronoun syntax supported Forwarding ... No Special Syntax Needed r Simple dots forwarding my group by <- function .data, ... { .data | dplyr::group by ... } Works with tidy selections too my select <- function .data, ... { .data | dplyr::select ... } For single-argument tidy selections, wrap in c my pivot longer <- function .data, ... { .data | tidyr::pivot longer c ... } Names Patterns with .data Use .data pronoun for programmatic column access: r Single column by name my mean <- function data, var { data | dplyr::summarise mean = mean .data var } Usage - completely insulated from data-masking mtcars | my mean "cyl" No ambiguity, works like regular function Multiple columns with all of my select vars <- function data, vars { data | dplyr::select all of vars } mtcars | my select vars c "cyl", "am" Injection Operators When to Use Each Operator | Operator | Use Case | Example | |----------|----------|---------| | {{ }} | Forward function arguments | summarise mean = mean {{ var }} | | | Inject single expression/value | summarise mean = mean sym var | | | Inject multiple arguments | group by syms vars | | .data | Access columns by name | mean .data var | Advanced Injection with r Create symbols from strings var <- "cyl" mtcars | dplyr::summarise mean = mean sym var Inject values to avoid name collisions df <- data.frame x = 1:3 x <- 100 df | dplyr::mutate scaled = x / x Uses both data and env x Use data sym for tidyeval contexts more robust mtcars | dplyr::summarise mean = mean data sym var Splicing with r Multiple symbols from character vector vars <- c "cyl", "am" mtcars | dplyr::group by syms vars Or use data syms for tidy contexts mtcars | dplyr::group by data syms vars Splice lists of arguments args <- list na.rm = TRUE, trim = 0.1 mtcars | dplyr::summarise mean = mean cyl, args Dynamic Dots Patterns Using list2 for Dynamic Dots Support r my function <- function ... { Collect with list2 instead of list for dynamic features dots <- list2 ... Process dots... } Enables these features: my function a = 1, b = 2 Normal usage my function list a = 1, b = 2 Splice a list my function "{name}" := value Name injection my function a = 1, Trailing commas OK Name Injection with Glue Syntax r Basic name injection name <- "result" list2 "{name}" := 1 Creates list result = 1 In function arguments with {{ my mean <- function data, var { data | dplyr::summarise "mean {{ var }}" := mean {{ var }} } mtcars | my mean cyl Creates column "mean cyl" mtcars | my mean cyl am Creates column "mean cyl am" Allow custom names with englue my mean <- function data, var, name = englue "mean {{ var }}" { data | dplyr::summarise "{name}" := mean {{ var }} } User can override default mtcars | my mean cyl, name = "cylinder mean" Pronouns for Disambiguation .data and .env Best Practices r Explicit disambiguation prevents masking issues cyl <- 1000 Environment variable mtcars | dplyr::summarise data cyl = mean .data$cyl , Data frame column env cyl = mean .env$cyl , Environment variable ambiguous = mean cyl Could be either usually data wins Use in loops and programmatic contexts vars <- c "cyl", "am" for var in vars { result <- mtcars | dplyr::summarise mean = mean .data var print result } Programming Patterns Bridge Patterns Converting between data-masking and tidy selection behaviors: r across as selection-to-data-mask bridge my group by <- function data, vars { data | dplyr::group by across {{ vars }} } Works with tidy selection mtcars | my group by starts with "c" across all of as names-to-data-mask bridge my group by <- function data, vars { data | dplyr::group by across all of vars } mtcars | my group by c "cyl", "am" Transformation Patterns r Transform single arguments by wrapping my mean <- function data, var { data | dplyr::summarise mean = mean {{ var }}, na.rm = TRUE } Transform dots with across my means <- function data, ... { data | dplyr::summarise across c ... , ~ mean .x, na.rm = TRUE } Manual transformation advanced my means manual <- function .data, ... { vars <- enquos ..., .named = TRUE vars <- purrr::map vars, ~ expr mean .x, na.rm = TRUE .data | dplyr::summarise vars } Error-Prone Patterns to Avoid Don't Use These Deprecated/Dangerous Patterns r Avoid - String parsing and eval security risk var <- "cyl" code <- paste "mean ", var, " " eval parse text = code Dangerous Good - Symbol creation and injection sym var Safe symbol injection Avoid - get in data mask name collisions with mtcars, mean get var Collision-prone Good - Explicit injection or .data with mtcars, mean sym var Safe or mtcars | summarise mean .data var Even safer Common Mistakes r Don't use {{ }} on non-arguments my func <- function x { x <- force x x is now a value, not an argument quo mean {{ x }} Wrong Captures value, not expression } Don't mix injection styles unnecessarily Pick one approach and stick with it: Either: embrace pattern my func <- function data, var data | summarise mean = mean {{ var }} Or: defuse-and-inject pattern my func <- function data, var { var <- enquo var data | summarise mean = mean var } Package Development with rlang Import Strategy r In DESCRIPTION: Imports: rlang In NAMESPACE, import specific functions: importFrom rlang, enquo, enquos, expr, , := Or import key functions: ' @importFrom rlang := enquo enquos Documentation Tags r ' @param var < data-masked dplyr::dplyr data masking Column to summarize ' @param ... < dynamic-dots rlang::dyn-dots Additional grouping variables ' @param cols < tidy-select dplyr::dplyr tidy select Columns to select Testing rlang Functions r Test data-masking behavior test that "function supports data masking", { result <- my function mtcars, cyl expect equal names result , "mean cyl" Test with expressions result2 <- my function mtcars, cyl 2 expect true "mean cyl 2" %in% names result2 } Test injection behavior test that "function supports injection", { var <- "cyl" result <- my function mtcars, sym var expect true nrow result 0 } This modern rlang approach enables clean, safe metaprogramming while maintaining the intuitive data-masking experience users expect from tidyverse functions. Performance Best Practices Performance Tool Selection Guide When to Use Each Performance Tool Profiling Tools Decision Matrix | Tool | Use When | Don't Use When | What It Shows | |------|----------|----------------|---------------| | profvis | Complex code, unknown bott