/tidyverse-patterns
Modern tidyverse patterns for R including pipes, joins, grouping, purrr, and stringr. Use when writing tidyverse R code.
$ npx -y skills add ab604/claude-code-r-skills --skill tidyverse-patterns --agent claude-codeHow 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.mdname: 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
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
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.
Repo: ab604/claude-code-r-skills
Other skills on r-skills.
- /r-bayes
Patterns for Bayesian inference in R using brms, including multilevel models, DAG validation, and marginal effects. Use when performing Bayesian analysis.
Open skill - /r-oop
R object-oriented programming guide for S7, S3, S4, and vctrs. Use when designing R classes or choosing an OOP system.
Open skill - /r-package-development
R package development guide covering dependencies, API design, testing, and documentation. Use when developing R packages.
Open skill - /r-performance
R performance best practices including profiling, benchmarking, vctrs, and optimization strategies. Use when optimizing R code.
Open skill - /r-style-guide
R style guide covering naming conventions, spacing, layout, and function design best practices. Use when writing R code.
Open skill - /rlang-patterns
rlang metaprogramming patterns for data-masking, injection operators, and dynamic dots. Use when writing functions that use tidy evaluation.
Open skill

