| 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
- Use modern tidyverse patterns - Prioritize dplyr 1.1+ features, native pipe, and current APIs
- Profile before optimizing - Use profvis and bench to identify real bottlenecks
- Write readable code first - Optimize only when necessary and after profiling
- 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
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"))
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
inner_join(x, y, by = join_by(id),
relationship = "one-to-one")
left_join(transactions, companies, by = join_by(company == id),
relationship = "many-to-one")
inner_join(x, y, by = join_by(id),
unmatched = "error")
left_join(x, y, by = join_by(id),
na_matches = "never")
inner_join(x, y, by = join_by(id)
relationship
unmatched
na_matches
tidyloginner_joinx y by join_byid
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
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 = .by group
data
reframequantiles quantilex .by group
data
group_bycategory
summarisemean_value meanvalue
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+)
filter(data, !(value < 0))
filter_out(data, value < 0)
filter(data, when_any(x, y, z, \(col) col > 0))
filter(data, when_all(x, y, z, \(col) !is.na(col)))
filter(data, !value value
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
mutate(data, status = replace_when(status,
value < 0 ~ "negative",
value == 0 ~ "zero"
))
mutate(data, status = case_when(
value < 0 ~ "negative",
value == 0 ~ "zero",
.default = status
))
mutate(data, grade = case_when(
score >= 90 ~ "A",
score >= 80 ~ "B",
score >= 70 ~
.unmatched
Serialization
- Use
qs2 for fast serialization — successor to qs, not backwards-compatible
qs2::qs_save(object, "data/results.qs2")
object <- qs2::qs_read("data/results.qs2")
qs::qsave(object, "data/results.qs")
Modern purrr Patterns
- Use
map() |> list_rbind() instead of superseded map_dfr()
- Use
walk() for side effects (file writing, plotting)
- Use
in_parallel() for scaling across cores
models <- data_splits |>
map(\(split) train_model(split)) |>
list_rbind()
summaries <- data_list |>
map(\(df) get_summary_stats(df)) |>
list_cbind()
plots <- walk2(data_list, plot_names, \(df, name) {
p <- ggplot(df, aes(x, y)) + geom_point()
ggsave(name, p)
})
librarymirai
daemons
results large_datasets
mapin_parallelexpensive_computation
daemons
String Manipulation with stringr
- Use stringr over base R string functions
- Consistent
str_ prefix and string-first argument order
- Pipe-friendly and vectorized by design
text |>
str_to_lower() |>
str_trim() |>
str_replace_all("pattern", "replacement") |>
str_extract("\\d+")
str_detect(text, "pattern")
str_extract(text, "pattern")
str_replace_all(text, "a", "b")
str_split(text, ",")
str_length(text)
str_sub(text, 1, 5)
str_c("a"
str_glue
str_padtext
str_wraptext width
str_to_lowertext
str_to_uppertext
str_to_titletext
str_detecttext fixed
str_detecttext regex
str_detecttext coll locale
grepl text
regmatchestext regexpr...
gsub text
Vectorization and Performance
result <- x + y
map_dbl(data, mean)
map_chr(data, class)
sapply(data, mean)
result <- numeric(length(x))
for(i in seq_along(x)) {
result[i] <- x[i] + y[i]
}
Common Anti-Patterns to Avoid
Legacy Patterns
data %>% function()
inner_join(x, y, by = c("a" = "b"))
sapply()
mutate(data, !!paste0("new_", var) := value)
Performance Anti-Patterns
result <- c()
for(i in 1:n) {
result <- c(result, compute(i))
}
result <- vector("list", n)
for(i in 1:n) {
result[[i]] <- compute(i)
}
result <- map(1:n, compute)
Migration from Old Patterns
From Base R to Modern Tidyverse
subset(data, condition) -> filter(data, condition)
data[order(data$x), ] -> arrange(data, x)
aggregate(x ~ y, data, mean) -> summarise(data, mean(x), .by = y)
sapply(x, f) -> map(x, f)
lapply(x, f) -> map(x, f)
grepl("pattern", text) -> str_detect(text
gsub text str_replace_alltext
substrtext str_subtext
nchartext str_lengthtext
strsplittext str_splittext
paste0a b str_ca b
tolowertext str_to_lowertext
From Old to New Tidyverse Patterns
data %>% function() -> data |> function()
group_by(data, x) |>
summarise(mean(y)) |>
ungroup() -> summarise(data, mean(y), .by = x)
across(starts_with("x")) -> pick(starts_with("x"))
by = c("a" = "b") -> by = join_by(a == b)
summarisedata x .groups reframedata x
gatherspread pivot_longerpivot_wider
separatecol into separate_wider_delimcol delim
extractcol into regex separate_wider_regexcol patterns x regex
Superseded purrr Functions (purrr 1.0+)
map_dfr(x, f) -> map(x, f) |> list_rbind()
map_dfc(x, f) -> map(x, f) |> list_cbind()
map2_dfr(x, y, f) -> map2(x, y, f) |> list_rbind()
pmap_dfr(list, f) -> pmap(list, f) |> list_rbind()
imap_dfr(x, f) -> imap(x, f) |> list_rbind()
walk(x write_file
walk2data paths write_csv