rguides

dplyr::mutate()

mutate(.data, ..., .by = NULL, .keep = c("all", "used", "unused", "none"), .before = NULL, .after = NULL)

mutate() is a core dplyr verb that adds new columns or modifies existing ones. It computes values column-wise using vectorised operations, meaning the expression is applied to each row independently.

Syntax

mutate(.data, ..., .by = NULL, .keep = c("all", "used", "unused", "none"), .before = NULL, .after = NULL)

Parameters

ParameterTypeDefaultDescription
.datatibble / data.frame,Input data frame or tibble
...name-value pairs,New column definitions using tidy select syntax
.bytidy-selectNULLOptional grouping column(s) for scoped operations
.keepstring”all”Which columns to keep: “all”, “used”, “unused”, “none”
.beforetidy-selectNULLPosition for new columns (column name or index)
.aftertidy-selectNULLPosition for new columns (column name or index)

Examples

Basic usage

The simplest form of mutate() adds a new column computed from existing ones. The expression z = x + y is evaluated element-wise across all rows, and the result is appended as a new column named z:

library(dplyr)

df <- tibble(
  x = 1:5,
  y = c(2, 4, 6, 8, 10)
)

mutate(df, z = x + y)

The output shows the original columns x and y preserved alongside the newly created z. Notice that the number of rows does not change — mutate() always returns the same row count as the input. This output block is simply the printed tibble, not a separate call, so you can reproduce it directly by running the code above in your own R session:

# A tibble: 5 × 3
      x     y     z
  <int> <dbl> <dbl>
1     1     2     3
2     2     4     6
3     3     6     9
4     4     8    12
5     5    10    15

Multiple new columns

You can create any number of columns in a single mutate() call, and columns defined earlier in the call are available to expressions defined later. This sequential evaluation means you can build complex derivations step by step within one pipeline stage:

df <- tibble(
  name = c("Alice", "Bob", "Charlie"),
  age = c(25, 30, 35)
)

mutate(df,
  age_next_year = age + 1,
  name_upper = toupper(name)
)

The result includes both the original columns alongside the two newly computed ones. Each expression references only the input columns, so age_next_year and name_upper are derived independently from age and name. Here is what the output looks like when printed:

# A tibble: 3 × 4
  name    age age_next_year name_upper
  <chr> <dbl>         <dbl> <chr>     
1 Alice    25            26 ALICE     
2 Bob      30            31 BOB       
3 Charlie 35            36 CHARLIE   

Using .by for grouped operations

The .by argument is a newer alternative to group_by() that scopes a computation to groups without leaving the data permanently grouped. It computes the summary within each group and broadcasts the result back to every row, preserving the original data structure:

df <- tibble(
  group = c("A", "A", "B", "B"),
  value = c(10, 20, 30, 40)
)

mutate(df, 
  group_mean = mean(value), 
  .by = group
)

Each row receives its group’s mean, giving you a column you can use for centering, computing ratios, or performing within-group comparisons without reshaping the data. The .by approach keeps the pipeline concise because there is no need for a trailing ungroup() call afterward:

# A tibble: 4 × 3
  group value group_mean
  <chr> <dbl>      <dbl>
1 A        10         15
2 A        20         15
3 B        30         35
4 B        40         35

Using .keep to control output

The .keep argument controls which columns appear in the result. "used" keeps only the grouping columns (when .by is set) plus the newly computed columns, dropping everything else. This is useful when you want to isolate the result of a transformation without carrying forward irrelevant source columns:

# Keep only the new columns (and grouping cols if using .by)
mutate(df, z = x + y, .keep = "used")

Common patterns

Conditional transformations: case_when() inside mutate() is the standard tidyverse pattern for creating categorical columns from numeric thresholds. Each branch specifies a condition and the value to assign when that condition is the first to match. This replaces long chains of nested if_else() calls and is easier to read and maintain:

mutate(df, 
  category = case_when(
    score >= 90 ~ "A",
    score >= 80 ~ "B",
    TRUE ~ "C"
  )
)

Cumulative calculations: Functions like cumsum(), cummean(), and cummax() compute running statistics row by row in their original order. These are useful for tracking progress, computing running totals, or building lagged features for time-series analysis. Each function preserves the input length, so no rows are dropped:

mutate(df, cum_sum = cumsum(value))

Ranking within groups: Combining .by with row_number() or dense_rank() assigns a rank to each row within its group. The .by = group pattern is cleaner than the older group_by() + mutate() + ungroup() triple for single-use group-aware computations. Use min_rank() for ties that share the same rank position:

mutate(df, 
  rank = row_number(value),
  .by = group
)

dplyr::mutate() in practice

mutate() adds or modifies columns in a data frame without changing the number of rows. New columns can reference both existing columns and other columns added in the same mutate() call: mutate(df, b = a * 2, c = b + 1) creates b and then uses it to compute c in the same step.

To add a column in a specific position, use .before and .after arguments: mutate(df, new_col = value, .before = existing_col) places new_col before existing_col. Without positioning arguments, new columns are appended at the right.

mutate() with across() applies a function to multiple columns at once: mutate(df, across(where(is.numeric), log)) applies log() to all numeric columns, replacing them with their log values. mutate(df, across(c(a, b), ~ . / sum(.), .names = "{.col}_pct")) creates proportion columns with a _pct suffix while keeping the originals.

For group-level calculations, mutate() after group_by() computes values within groups: df |> group_by(category) |> mutate(rank = rank(-score)) ranks each item within its category. This preserves all rows while adding the group-aware calculation, different from summarise() which collapses to one row per group.

mutate() adds or modifies columns while keeping all existing columns. Computations can reference columns created earlier in the same mutate() call. Use .keep = "used" to drop columns not referenced in the computation, .keep = "none" for a transmute-like result. mutate(across(where(is.numeric), scale)) applies a function to all numeric columns at once. For row-wise operations that reference multiple columns, wrap with rowwise() or use pmap().

Transforming multiple columns

For applying the same transformation to multiple columns, combine mutate() with across(). mutate(across(where(is.numeric), log)) log-transforms every numeric column. This is more maintainable than listing each column individually, especially when the column set changes. The .names argument to across() controls whether the transformation creates new columns or replaces existing ones — using "{.col}_log" creates new columns with a suffix, while omitting .names replaces in place.

See also