/tidyverse-patterns
Modern tidyverse patterns for R including pipes, joins, grouping, purrr, and stringr. Use when writing tidyverse R code.
Claude Code
$ npx -y skills add ab604/claude-code-r-skills --skill tidyverse-patterns --agent claude-code
Codex
$ npx skills add ab604/claude-code-r-skills --skill tidyverse-patterns -a codex -y
Cursor
$ npx skills add ab604/claude-code-r-skills --skill tidyverse-patterns -a cursor -y
Gemini CLI
$ npx skills add ab604/claude-code-r-skills --skill tidyverse-patterns -a gemini-cli -y
GitHub Copilot
$ npx skills add ab604/claude-code-r-skills --skill tidyverse-patterns -a github-copilot -y
OpenCode
$ npx skills add ab604/claude-code-r-skills --skill tidyverse-patterns -a opencode -y
Windsurf
$ npx skills add ab604/claude-code-r-skills --skill tidyverse-patterns -a windsurf -y
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.
Other skills on r-skills.
More skills from other plugins.
brainstorming
You MUST use this before any creative work - creating features, building components, adding functionality, modifying behavior, or…
ask-matt
Ask which skill or flow fits your situation. A router over the skills in this repo.
Questions about tidyverse-patterns.
- Which coding agents can use tidyverse-patterns?
- tidyverse-patterns installs in Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, OpenCode and Windsurf with the skills CLI, using each agent's flag shown on this page. Flowy's auto-invocation, which fires a skill when your prompt matches, runs in Claude Code only.

