K Y
K Y

Reputation: 378

How to use quasiquotation / tidy evaluation when doing `map` with column names

Could you help me understand how Quasiquotation works? I'm using map and count function but it doesn't seem to work properly.

First try:

map(names(starwars),~starwars %>% count(.x))
Error: Column `.x` is unknown

Second try:

map(names(starwars),~starwars %>% count(!!.x))
#not useful [[1]]
# A tibble: 1 x 2
  `"name"`     n
  <chr>    <int>
1 name        87

[[2]]
# A tibble: 1 x 2
  `"height"`     n
  <chr>      <int>
1 height        87

Third try:

map(names(starwars),~starwars %>% count(!!!.x))
# the same

Another example (working on functions):

If I want to make a function that takes a list and the changes each element of the list with regard to the previous element on that list:

my_list <- list("a" =1 , "b" = 2, "c" = 3)

# this obviously is not working (list + number)
> my_list+1
Error in my_list + 1 : non-numeric argument to binary operator
# this is a bit strange
my_list %>% map(~+1)
#this works fine
my_list %>% map(+1)
# as this
my_list %>% map(~.x+1)

# moving on to add the previous element to the next element
imap(my_list, my_list[[.y +1]] := .x %>% +1)
Error in `:=`(my_list[[.y + 1]], .x %>% +1) : could not find function ":="

# wrong eval 1?
imap(my_list, my_list[[.y +1]] <- .x %>% +1)
Error in eval(lhs, parent, parent) : object '.x' not found

# wrong eval 2?
imap(my_list, my_list[[.y +1]] <- !!.x %>% +1)
Error in eval(lhs, parent, parent) : object '.x' not found

# wrong symbol 1?
imap(my_list, my_list[[.y +1]] = .x %>% +1)
Error: unexpected '=' in "imap(my_list, my_list[[.y +1]] ="

Upvotes: 2

Views: 377

Answers (1)

Ari Anisfeld
Ari Anisfeld

Reputation: 816

I think this question can be decomposed into a section on quasi-quotation and anoth on map functions.

First, ~ starwars %>% count(.x)) is shorthand for and a slightly more complicated version of function(.x){starwars %>% count(.x)}. So I'm going to work with the functions directly.

Second, names(starwars) gives you a character vector.

So to avoid the confusion that map brings let's start with functions and pass them the character "eye_color".

Attempt 1: dplyr functions treat symbols as if they are columns in the tbl

dplyr functions are nice when doing interactive data analysis, because they allow us to refer to columns with symbols. I recommend reading: https://dplyr.tidyverse.org/articles/programming.html for more info.

func <- function(.x) { starwars %>% count(.x) }
func("eye_color")
Error: Column `.x` is unknown

In your first attempt, this leads to a problem, because .x is symbol, so R thinks .x is column in starwars.

Attempt 2/3: count() / group_by() expect symbols not character input.

!! takes .x and replaces it with "eye_color". But "eye_color" is not symbol/name but rather a character.

func_2 <- function(.x) { starwars %>% count(!!.x) }
func_2("eye_color")

# A tibble: 1 x 2
  `"eye_color"`     n
  <chr>         <int>
1 eye_color        87

This weird output is the result of grouping by a character. For whatever reason, dplyr groups the whole dataframe as "eye_color" and then tells you there are 87 rows. starwars %>% count("hooray") gives similar output.

Interlude: what we want is a symbol

A somewhat intuitive way to code dplyr functions is to pass symbols/names and use {{.x}} to evaluate the promise. (Less intuitively you can do !!enquo(.x).)

func_3 <- function(.x) {  starwars %>% count({{.x}}) }
func_3(eye_color)

# A tibble: 15 x 2
   eye_color         n
   <chr>         <int>
 1 black            10
 2 blue             19
 3 ...

This works!

A solution is to convert the characters to symbols

func_4 <- function(.x) { .x = as.symbol(.x)
                         starwars %>% count({{.x}}) }
func_4("eye_color")

# A tibble: 15 x 2
   eye_color         n
   <chr>         <int>
 1 black            10
 2 blue             19
 3 ...

This also works!

Bringing back map

Before I continue, I think nniloc's solution is better for your problem.

But you could use map as follows

starwars %>% 
  select_if(negate(is.list)) %>% 
  names() %>% 
  map(function(.x) {x = as.symbol(.x)
      starwars %>% count( {{ x }} )
      }) 

or

starwars %>% 
  select_if(negate(is.list)) %>% 
  names() %>% 
  map(as.symbol) %>%
  map(function(.x) { 
      starwars %>% count( {{  .x }} )
      }) 

When you use the ~ notation, .x is now a "pronoun" that refers to the symbols directly, so we can use !! to access the symbols directly. (I don't fully understand this).

starwars %>% 
  select_if(negate(is.list)) %>% 
  names() %>% 
  map(as.symbol) %>%
  map(~ starwars %>% count( !! .x  )) 

Regarding imap(), it looks like you want to code in python (or some other language with iteration). imap() is short hand for map2(.x, names(.x), ...) so is distinct from enumerate() in python. There are R functions like seq_along which give you position in an object, but I haven't used those with map.

Upvotes: 3

Related Questions