Skip to content
Development
Skill

/tidyverse-patterns

Modern tidyverse patterns for R including pipes, joins, grouping, purrr, and stringr. Use when writing tidyverse R code.

From plugin
r-skills
1898 skills3 agents4 commands4 hooks
Install
$ npx -y skills add ab604/claude-code-r-skills --skill tidyverse-patterns --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/tidyverse-patterns

Context preview

The summary Claude sees to decide when to auto-load this skill.

Modern tidyverse patterns for R including pipes, joins, grouping, purrr, and stringr. Use when writing tidyverse R code.

SKILL.md

tidyverse-patterns.SKILL.md
name: tidyverse-patterns
description: Modern tidyverse patterns for R including pipes, joins, grouping, purrr, and stringr. Use when writing tidyverse R code.

Modern Tidyverse Patterns

*Best practices for modern tidyverse development with dplyr 1.1+ and R 4.3+*

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

Pipe Usage (`|>` not `%>%`)

  • **Always use native pipe `|>` instead of magrittr `%>%`**
  • R 4.3+ provides all needed features
# 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**
# 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"))

Join Quality Control

  • **Declare cardinality with `relationship` to validate join assumptions**
  • **Use `unmatched = "error"` to catch unexpected non-matches**
  • **Use `na_matches = "never"` to prevent silent NA joins**
  • **Use `tidylog::` prefix interactively to verify join results**
# Validate 1:1 relationship — errors if violated
inner_join(x, y, by = join_by(id),
  relationship = "one-to-one")

# Validate many-to-one (left has duplicates, right does not)
left_join(transactions, companies, by = join_by(company == id),
  relationship = "many-to-one")

# Ensure all rows from left match something in right
inner_join(x, y, by = join_by(id),
  unmatched = "error")

# Prevent NA values from matching each other silently
left_join(x, y, by = join_by(id),
  na_matches = "never")

# Combine for strict joins
inner_join(x, y, by = join_by(id),
  relationship = "one-to-one",
  unmatched = "error",
  na_matches = "never")

# Interactive verification with tidylog
# tidylog prints a summary of rows matched/dropped
tidylog::inner_join(x, y, by = join_by(id))

Data Masking and Tidy Selection

  • **Understand the difference between data masking and tidy selection**
  • **Use `{{}}` (embrace) for function arguments**
  • **Use `.data[[]]` for character vectors**
# 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**
# 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()

NA-Safe Row Filtering

  • **Use `filter_out()` instead of negating conditions** — negation (`!condition`) silently drops NAs
  • **Use `when_any()` and `when_all()` for multi-column OR/AND filters (dplyr 1.2+)**
# Problem: negation silently drops rows where condition is NA
filter(data, !(value < 0))       # drops rows where value is NA — silent!

# Good - filter_out() passes NAs through safely
filter_out(data, value < 0)      # rows where value is NA are kept

# Good - when_any() for OR across columns (dplyr 1.2+)
filter(data, when_any(x, y, z, \(col) col > 0))  # any column > 0

# Good - when_all() for AND across columns
filter(data, when_all(x, y, z, \(col) !is.na(col)))  # no NAs in any

# Avoid - verbose base patterns
filter(data, !(value < 0) | is.na(value))   # workaround, not idiomatic

Recoding and Conditional Updates

  • **Use `replace_when()` for in-place conditional updates** — avoids `case_when()` with `.default = x`
  • **Use `case_when()` with `.unmatched = "error"` when all cases should be handled**
# Good - replace_when() for in-place updates (type-stable, NAs unaffected)
mutate(data, status = replace_when(status,
  value < 0  ~ "negative",
  value == 0 ~ "zero"
))

# Avoid - case_when() requires restating the variable in .default
mutate(data, status = case_when(
  value < 0  ~ "negative",
  value == 0 ~ "zero",
  .default   = status    # repetitive
))

# Good - case_when() with strict exhaustiveness check
mutate(data, grade = case_when(
  score >= 90 ~ "A",
  score >= 80 ~ "B",
  s
Read more
Ships withr-skills

A curated collection of Claude Code configurations for modern R use. These skills, rules, commands, and agents help Claude Code understand R best practices and generate idiomatic, high-quality R code.

Get the whole plugin