Skip to Content
Course content

127: Non-Standard Evaluation in R

Click on the "Edit" button in the top corner of the screen to edit your slide content.

If you've spent any time with the tidyverse, you've already been using Non-Standard Evaluation (NSE) without realizing it. When you write filter(df, species == "Setosa"), you aren't passing a string or a variable called species; you're passing the symbol species, and filter is smart enough to look for that symbol inside the columns of df. It's a bit of R magic that makes code read like a sentence, but the moment you try to wrap that logic inside your own function, the magic usually breaks.

The "Column Not Found" Trap

I see this mistake constantly when people start writing helper functions. Let's say you want a function that filters a dataset based on a column name and a specific value. Your first instinct is probably to do something like this:

# This will not work as you expect
filter_by_value <- function(data, col_name, value) {
  dplyr::filter(data, col_name == value)
}

mtcars %>% filter_by_value("cyl", 6)

You'll likely get an error saying column 'col_name' not found. Here is why: filter() is designed for NSE. It expects a bare column name. When you pass col_name into the function, R looks at the expression col_name == value and thinks you are literally looking for a column named "col_name" in your data frame. It doesn't realize that col_name is actually a variable holding the string "cyl".

You might try to "fix" this using the old-school eval(parse(text = ...)) approach. I'll be honest: don't do that. It's a security nightmare, it's slow, and it's incredibly difficult to debug. You're essentially treating your code as a string and asking R to execute it, which is a recipe for disaster in any professional pipeline.

Quoting and Unquoting with Curly-Curly

The modern, "Tidy" way to handle this is through a process called "embedding." We want to tell R: "Don't evaluate this argument right now; wait until you're inside the filter call, and then treat it as a column name."

In recent versions of rlang and dplyr, we use the {{ }} (curly-curly) operator. It acts as a bridge between the standard evaluation of your function arguments and the non-standard evaluation of the dplyr verb.

# The professional way
filter_by_value <- function(data, col_name, value) {
  dplyr::filter(data, {{ col_name }} == value)
}

# Now we pass the column name WITHOUT quotes
mtcars %>% filter_by_value(cyl, 6)

Notice the subtle change in how the function is called. We no longer pass "cyl" as a string; we pass cyl as a symbol. The {{ }} operator captures that symbol and "unquotes" it into the filter call. It's cleaner, it's type-safe, and it maintains the declarative style that makes R powerful for data analysis.

The Trade-off: Flexibility vs. Predictability

So, why not use NSE for everything? Because NSE adds a layer of cognitive load. When you use {{ }}, you are explicitly changing the rules of how R evaluates code. If you're building a package for other developers, they might be confused why your function expects an unquoted symbol instead of a standard character string.

If you absolutely must accept a string (for example, if the column name is coming from a JSON API or a user-input text box), {{ }} won't work because the input is already a string, not a symbol. In those cases, you'd use .data[[col_name]]. I prefer curly-curly for internal analysis scripts because it feels native to the tidyverse, but I lean toward the .data pronoun when building robust production tools. It's a trade-off between "elegant syntax" and "explicit behavior."




📋 Practical Task

Building a Dynamic Column Summarizer

You need to create a helper function called summarize_column that takes a data frame and a column name, and returns the mean of that column. However, the function must be written using Non-Standard Evaluation (the curly-curly operator) so that the user can pass the column name without quotes.

Requirements:

  • The function should take two arguments: data and target_col.
  • Inside the function, use dplyr::summarize() and mean(..., na.rm = TRUE).
  • Ensure the result is a data frame containing the mean.
  • Test your function using the iris dataset, calculating the mean of the Sepal.Length column without putting the column name in quotes.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.