cd /news/developer-tools/claude-r-tidyverse-expert · home topics developer-tools article
[ARTICLE · art-7791] src=gist.github.com ↗ pub= topic=developer-tools verified=true sentiment=· neutral

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.

read8 min views15 publishedAug 21, 2025

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
data |> 
  filter(year >= 2020) |>
  summarise(mean_value = mean(value))

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
transactions |> 
  inner_join(companies, by = join_by(company == id))

transactions |>
  inner_join(companies, join_by(company == id, year >= since))

transactions |>
  inner_join(companies, join_by(company == id, closest(year >= since)))

transactions |> 
  inner_join(companies, by = c("company" = "id"))

Multiple Match Handling

  • Use multiple and unmatched arguments for quality control
inner_join(x, y, by = join_by(id), multiple = "error")

inner_join(x, y, by = join_by(id), multiple = "all")

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

my_summary <- function(data, group_var, summary_var) {
  data |>
    group_by({{ group_var }}) |>
    summarise(mean_val = mean({{ summary_var }}))
}

for (var in names(mtcars)) {
  mtcars |> count(.data[[var]]) |> print()
}

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
data |>
  summarise(mean_value = mean(value), .by = category)

data |>
  summarise(total = sum(revenue), .by = c(company, year))

data |>
  summarise(
    n_x_cols = ncol(pick(starts_with("x"))),
    n_y_cols = ncol(pick(starts_with("y")))
  )

data |>
  summarise(across(where(is.numeric), mean, .names = "mean_{.col}"), .by = group)

data |>
  reframe(quantiles = quantile(x, c(0.25, 0.5, 0.75)), .by = group)

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:

my_summarise <- function(data, var) {
  data |> dplyr::summarise(mean = mean({{ var }}))
}

mtcars |> my_summarise(cyl)
mtcars |> my_summarise(cyl * am)
mtcars |> my_summarise(.data$cyl)  # pronoun syntax supported

Forwarding ... (No Special Syntax Needed)

my_group_by <- function(.data, ...) {
  .data |> dplyr::group_by(...)
}

my_select <- function(.data, ...) {
  .data |> dplyr::select(...)
}

my_pivot_longer <- function(.data, ...) {
  .data |> tidyr::pivot_longer(c(...))
}

Names Patterns with .data

Use .data pronoun for programmatic column access:

my_mean <- function(data, var) {
  data |> dplyr::summarise(mean = mean(.data[[var]]))
}

mtcars |> my_mean("cyl")  # No ambiguity, works like regular function

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 !!

var <- "cyl"
mtcars |> dplyr::summarise(mean = mean(!!sym(var)))

df <- data.frame(x = 1:3)
x <- 100
df |> dplyr::mutate(scaled = x / !!x)  # Uses both data and env x

mtcars |> dplyr::summarise(mean = mean(!!data_sym(var)))

Splicing with !!!

vars <- c("cyl", "am")
mtcars |> dplyr::group_by(!!!syms(vars))

mtcars |> dplyr::group_by(!!!data_syms(vars))

args <- list(na.rm = TRUE, trim = 0.1)
mtcars |> dplyr::summarise(mean = mean(cyl, !!!args))

Dynamic Dots Patterns

Using list2() for Dynamic Dots Support

my_function <- function(...) {
  dots <- list2(...)
}

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

name <- "result"
list2("{name}" := 1)  # Creates list(result = 1)

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"

my_mean <- function(data, var, name = englue("mean_{{ var }}")) {
  data |> dplyr::summarise("{name}" := mean({{ var }}))
}

mtcars |> my_mean(cyl, name = "cylinder_mean")

Pronouns for Disambiguation

.data and .env Best Practices

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)
)

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:

my_group_by <- function(data, vars) {
  data |> dplyr::group_by(across({{ vars }}))
}

mtcars |> my_group_by(starts_with("c"))

my_group_by <- function(data, vars) {
  data |> dplyr::group_by(across(all_of(vars)))
}

mtcars |> my_group_by(c("cyl", "am"))

Transformation Patterns

my_mean <- function(data, var) {
  data |> dplyr::summarise(mean = mean({{ var }}, na.rm = TRUE))
}

my_means <- function(data, ...) {
  data |> dplyr::summarise(across(c(...), ~ mean(.x, na.rm = TRUE)))
}

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

var <- "cyl" 
code <- paste("mean(", var, ")")
eval(parse(text = code))  # Dangerous!

!!sym(var)  # Safe symbol injection

with(mtcars, mean(get(var)))  # Collision-prone

with(mtcars, mean(!!sym(var)))  # Safe
mtcars |> summarise(mean(.data[[var]]))  # Even safer

Common Mistakes

my_func <- function(x) {
  x <- force(x)  # x is now a value, not an argument
  quo(mean({{ x }}))  # Wrong! Captures value, not expression
}

my_func <- function(data, var) data |> summarise(mean = mean({{ var }}))
my_func <- function(data, var) {
  var <- enquo(var)
  data |> summarise(mean = mean(!!var))
}

Package Development with rlang

Import Strategy

Imports: rlang

importFrom(rlang, enquo, enquos, expr, !!!, :=)

#' @importFrom rlang := enquo enquos

Documentation Tags

#' @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

test_that("function supports data masking", {
  result <- my_function(mtcars, cyl)
  expect_equal(names(result), "mean_cyl")
  
  result2 <- my_function(mtcars, cyl * 2)
  expect_true("mean_cyl * 2" %in% names(result2))
})

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
── more in #developer-tools 4 stories · sorted by recency
── more on @tidyverse 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/claude-r-tidyverse-e…] indexed:0 read:8min 2025-08-21 ·