Data Wrangling with dplyr

x |> f(y) is f(x, y).

library(dplyr)
library(palmerpenguins)   # penguins dataset

18.1 The pipe

x |> f(y) is f(x, y). The native pipe supports a _ placeholder for named arguments (R 4.2+): df |> lm(y ~ x, data = _).

penguins |> filter(species == "Adelie") |> summarise(mean_mass = mean(body_mass_g, na.rm = TRUE))
penguins %>% head()      # magrittr pipe (supports the . placeholder)

18.2 Core verbs

keep rows matching conditions choose/rename columns create/modify columns reduce to summary rows group_by() / .by count() / tally() rename() / relocate() rename / reorder columns rows by position (slice_max, slice_sample,...) extract a column as a vector

penguins |>
  filter(!is.na(sex), island %in% c("Biscoe", "Dream"), body_mass_g > 3500) |>  # rows
  select(species, island, sex, bill_length_mm, body_mass_g) |>                   # columns
  mutate(mass_kg = body_mass_g / 1000,                                           # new columns
         big = if_else(mass_kg > 4.5, "big", "small")) |>
  arrange(desc(mass_kg)) |>                                                      # sort
  slice_head(n = 10)

18.3 Grouped summaries

penguins |>
  group_by(species, sex) |>
  summarise(
    n = n(),
    mean_mass = mean(body_mass_g, na.rm = TRUE),
    sd_mass = sd(body_mass_g, na.rm = TRUE),
    max_bill = max(bill_length_mm, na.rm = TRUE),
    .groups = "drop"
  )

penguins |> summarise(mean_mass = mean(body_mass_g, na.rm = TRUE), .by = species)   # per-operation grouping
penguins |> count(species, island, sort = TRUE)
penguins |> group_by(species) |> mutate(z = (body_mass_g - mean(body_mass_g, na.rm = TRUE)) / sd(body_mass_g, na.rm = TRUE))

18.4 Selection helpers & across()

select(penguins, starts_with("bill"), ends_with("_mm"), contains("mass"), where(is.numeric))
select(penguins, -year, species:island, any_of(c("sex", "nope")))
penguins |> summarise(across(where(is.numeric), \(x) mean(x, na.rm = TRUE)))
penguins |> mutate(across(c(bill_length_mm, bill_depth_mm), ~ .x / 10, .names = "{.col}_cm"))
penguins |> filter(if_any(ends_with("_mm"), \(x) x > 50))
penguins |> rowwise() |> mutate(total = sum(c_across(bill_length_mm:flipper_length_mm)))

18.5 Useful helpers

if_else(x > 0, "pos", "neg")
case_when(x < 0 ~ "neg", x == 0 ~ "zero", .default = "pos")
case_match(code, 1 ~ "one", 2 ~ "two")
coalesce(x, 0)                       # replace NA
na_if(x, -99)
lag(x); lead(x); cumsum(x); row_number(); min_rank(desc(x)); ntile(x, 4); percent_rank(x)
between(x, 1, 10); near(0.1 + 0.2, 0.3)
n_distinct(x); first(x); last(x); nth(x, 2)
consecutive_id(x)

18.6 Joins

band <- tibble(name = c("Mick", "John", "Paul"), band = c("Stones", "Beatles", "Beatles"))
plays <- tibble(name = c("John", "Paul", "Keith"), plays = c("guitar", "bass", "guitar"))

left_join(band, plays, by = "name")       # all rows of band
inner_join(band, plays, by = "name")      # matches only
full_join(band, plays, by = "name")       # all rows of both
right_join(band, plays, by = "name")
semi_join(band, plays, by = "name")       # band rows with a match (filtering join)
anti_join(band, plays, by = "name")       # band rows without a match
left_join(x, y, by = join_by(id == key, date >= start))   # different names, inequality/rolling joins
cross_join(x, y)
bind_rows(df1, df2, .id = "source"); bind_cols(df1, df2)
union(), intersect(), setdiff()           # set operations on rows

18.7 dplyr backends

The same dplyr code runs on databases (dbplyr), data.table (dtplyr), Arrow (arrow), DuckDB (duckplyr), and Spark (sparklyr) — translated to SQL or native operations.