Skip to content
AgileLogics
6 min read

Programming with dplyr: Tidy Evaluation

Prerequisites: Environments, scoping, and closures, dplyr: mutate, summarise, and group_by

dplyr lets you write bare column names — filter(df, age > 30) — instead of df$age. That convenience, called data masking, needs special handling when you write your own functions around dplyr.

The problem

library(dplyr, warn.conflicts = FALSE)

summarise_by <- function(data, group, value) {
  data |>
    summarise(mean = mean(value), .by = group)
}

Calling summarise_by(mtcars, cyl, mpg) fails: dplyr looks for columns literally called group and value.

The fix: embrace with {{ }}

Wrapping an argument in double braces tells dplyr to use the column the caller named:

library(dplyr, warn.conflicts = FALSE)

summarise_by <- function(data, group, value) {
  data |>
    summarise(mean = mean({{ value }}), n = n(), .by = {{ group }}) |>
    arrange({{ group }})
}

summarise_by(mtcars, cyl, mpg)
summarise_by(mtcars, gear, hp)
  cyl     mean  n
1   4 26.66364 11
2   6 19.74286  7
3   8 15.10000 14
  gear     mean  n
1    3 176.1333 15
2    4  89.5000 12
3    5 195.6000  5

(mtcars is a plain data frame, so results print without tibble formatting.)

Column names as text

When column names arrive as strings — from a Shiny input or a configuration file — use the .data pronoun:

library(dplyr, warn.conflicts = FALSE)

top_by <- function(data, column, n = 3) {
  data |>
    arrange(desc(.data[[column]])) |>
    head(n) |>
    select(all_of(column))
}

top_by(mtcars, "hp", 2)
                hp
Maserati Bora  335
Ford Pantera L 264
  • .data[[column]] looks up a column by name inside data-masking verbs.
  • all_of(column) does the same for column-selecting verbs like select.

Naming new columns dynamically

library(dplyr, warn.conflicts = FALSE)

add_double <- function(data, column) {
  data |> mutate("{{ column }}_x2" := {{ column }} * 2)
}

tibble(price = c(10, 20)) |> add_double(price)
# A tibble: 2 × 2
  price price_x2
  <dbl>    <dbl>
1    10       20
2    20       40

The := operator allows a computed name on the left-hand side.

Why bother?

Once repeated analysis steps live in functions, you can apply them to new datasets and columns without copying code — and test them.