dplyr & tidyr Cheat Sheet
Covers dplyr's data manipulation verbs, table joins, and tidyr's pivot_longer/pivot_wider reshaping functions for tidy data workflows.
Core dplyr Verbs
Filter, select, mutate, and summarize a data frame.
library(dplyr)sales %>% filter(region == "US", amount > 0) %>% select(customer_id, order_date, amount) %>% mutate(amount_usd = amount * 1.0) %>% arrange(desc(amount_usd))sales %>% group_by(region) %>% summarise( total_sales = sum(amount), avg_sales = mean(amount), n_orders = n() )
Joins
Combine tables using dplyr's join family.
orders %>% left_join(customers, by = "customer_id")orders %>% inner_join(products, by = c("product_id" = "id"))# anti_join: rows in orders with no match in customersorders %>% anti_join(customers, by = "customer_id")
Reshaping with tidyr
Pivot between long and wide formats and clean up columns.
library(tidyr)# Wide to longlong_df <- wide_df %>% pivot_longer(cols = jan:dec, names_to = "month", values_to = "sales")# Long to widewide_df <- long_df %>% pivot_wider(names_from = month, values_from = sales)# Split/combine columnsdf %>% separate(full_name, into = c("first", "last"), sep = " ")df %>% unite(full_name, first, last, sep = " ")df %>% drop_na(amount) # remove rows with NA in amountdf %>% replace_na(list(amount = 0))
Key Concepts
The verbs and pipes that define tidyverse-style data manipulation.
- filter()- Keeps rows matching a logical condition
- select()- Chooses/reorders columns by name or helper (starts_with(), everything())
- mutate()- Creates or modifies columns, computed row-wise
- summarise()/summarize()- Collapses grouped data into one row per group with aggregate statistics
- group_by()- Groups rows so subsequent verbs (summarise, mutate) operate per group
- pivot_longer/pivot_wider- tidyr's modern reshaping functions, replacing the older gather()/spread()
- %>% / |>- Pipe operator passing the left-hand result as the first argument to the right-hand function
across() for Multi-Column Operations
Apply the same transformation or summary to many columns at once, replacing the deprecated *_at/*_if/*_all variants.
library(dplyr)# Scale every numeric columndf %>% mutate(across(where(is.numeric), ~ scale(.x)[, 1]))# Summarise multiple stats for selected columns, with custom namesdf %>% group_by(region) %>% summarise(across(c(sales, profit), list(mean = mean, sd = sd), .names = "{.col}_{.fn}"))# Conditionally recode across a set of columns matched by name patterndf %>% mutate(across(starts_with("flag_"), ~ if_else(.x == 1, "yes", "no")))# if_any()/if_all() for row-wise filtering across many columnsdf %>% filter(if_any(starts_with("score_"), ~ .x > 90))df %>% filter(if_all(starts_with("score_"), ~ !is.na(.x)))
Window Functions & Ranking
Rank, lag/lead, and select top rows per group without collapsing the data frame.
df %>% group_by(customer_id) %>% arrange(order_date) %>% mutate( prev_amount = lag(amount), next_amount = lead(amount), running_total = cumsum(amount), order_rank = row_number(), dense_rnk = dense_rank(desc(amount)) ) %>% ungroup()# Top-N rows per group without a full sort + headdf %>% group_by(region) %>% slice_max(amount, n = 3, with_ties = FALSE)df %>% group_by(region) %>% slice_min(amount, n = 1)# Sample rows per groupdf %>% group_by(region) %>% slice_sample(n = 2)
Programming with dplyr (Tidy Eval)
Write reusable functions that take column names as arguments using embracing and the .data pronoun.
library(rlang)# Embrace {{ }} to forward a data-masked argument into a verbsummarise_by <- function(data, group_var, value_var) { data %>% group_by({{ group_var }}) %>% summarise(total = sum({{ value_var }}, na.rm = TRUE), .groups = "drop")}summarise_by(sales, region, amount)# Programmatic column access with strings via .datacol_name <- "amount"df %>% summarise(total = sum(.data[[col_name]]))# Dynamic naming with the walrus (:=) operatormake_flag <- function(data, var, threshold) { data %>% mutate("{{ var }}_flag" := {{ var }} > threshold)}make_flag(df, amount, 100)
tidyr: complete(), fill(), and Nested Data
Fill implicit gaps in panel data and work with list-columns of nested data frames.
library(tidyr)# Make implicit missing combinations explicit (e.g. every customer x month)df %>% complete(customer_id, month, fill = list(sales = 0))# Carry the last non-NA value forward/backward down a columndf %>% arrange(date) %>% fill(status, .direction = "down")# Fill every combination of two columns, useful before a joincrossing(region = c("US", "EU"), quarter = 1:4)# Nest per-group data into list-columns, then map a model over eachnested <- df %>% group_by(region) %>% nest()nested <- nested %>% mutate(model = purrr::map(data, ~ lm(sales ~ month, data = .x)))# Reverse: expand list-columns back into rowsunnested <- nested %>% select(region, data) %>% unnest(data)
Advanced Gotchas & Performance Notes
Behaviors that trip up experienced users moving beyond basic verb chains.
- .groups argument in summarise()- Since dplyr 1.0, omitting it emits a message about grouping structure after summarise(); set .groups = "drop" explicitly in pipelines/scripts
- rowwise()- Creates a special grouped structure of one group per row, needed for row-wise aggregation of list-columns or vectors (e.g. mean(c_across(...)))
- join relationship checks- Pass relationship = "one-to-many" (or similar) to *_join() to fail fast instead of silently producing a row explosion on unexpected duplicate keys
- join_by() for inequality/rolling joins- Modern dplyr joins support join_by(x >= y) or closest() for non-equi and rolling joins, replacing manual fuzzyjoin workarounds
- data.table/dtplyr for scale- dtplyr::lazy_dt() lets you write dplyr syntax that compiles to data.table for large in-memory data without rewriting pipelines
- distinct() column subset trap- distinct(df, a, b) drops all other columns by default (keeps first occurrence); pass .keep_all = TRUE to retain them
- arrange() and locale- String sorting in arrange() depends on locale; use arrange(df, stringr::str_sort(x, locale = "en")) for reproducible ordering across machines
Always call ungroup() after a group_by() %>% summarise() chain if you plan further row-wise mutate() calls - a lingering grouping silently changes how later verbs compute results.