Excluding rows

4 min read

dplyr tidyeval bangbang

## Warning: package 'purrr' was built under R version 3.4.1

This is another post about tidy evaluation, the new cool thing that appeared in the dplyr 0.7 series and is likely to be used everywhere in the tidyverse. In this post, we want to create a new verb exclude that kind of does the opposite of filter, i.e. the call would extract the tibble on the right.

exclude( data, a == 1, b == 2 )

So the condition we give to exclude control what we don’t want to see in the result, this is equivalent to these.

# more likely we would type this
filter( data, a != 1, b != 2 )

# but this will be easier to generate programmatically
filter( data, ! (a == 1), !(b == 2) )

# ... and using not instead spares us on bang
filter( data, not(a == 1), not(b == 2) )

We can do something similar to this previous post and use Reduce and !!! :

exclude1 <- function(data, ...){
  dots <- quos(...)
  filter( data, Reduce("&", map(list(!!!dots), ~not(.))) )
}
  • First we get a logical vector for each condition by splicing the dots: list(!!!dots)
  • We use purrr::map to negate them. (see how this uses not instaed of !) because we have enough bangs in the expression
  • Then we iteratively reduce them into a single logical vector with Reduce("&").
exclude1(  data, a == 1, b == 2 )
##   a b
## 1 2 1
## 2 2 3

This works fine, but it’s kind of hacky and asks dplyr to evaluate this complicated expression:

filter( data, Reduce("&", map(list(a==1, b==2), ~not(.))) )
##   a b
## 1 2 1
## 2 2 3

tidy eval lets us manipulate the expressions before we splice them. Let’s have a lot at the mysterious dots object that quos gives us:

curious <- function(...) quos(...)
curious( a == 1, b == 2)
## [[1]]
## <quosure: global>
## ~a == 1
## 
## [[2]]
## <quosure: global>
## ~b == 2
## 
## attr(,"class")
## [1] "quosures"

We get a list of quosure, so we can manipulate each of them with purrr::map. We just need a function that takes a quosure, and return a new quosure that wraps the previous expression in a not call and uses the same environment. This is what I came up with, perhaps there is a better way:

negate_quosure <- function(q){
  set_env( quo(not(!!get_expr(q))), get_env(q))
}
q <- quo(a==1)
negate_quosure( q )
## <quosure: global>
## ~not(a == 1)
dots <- curious(a == 1, b == 2)
map( dots, negate_quosure )
## [[1]]
## <quosure: global>
## ~not(a == 1)
## 
## [[2]]
## <quosure: global>
## ~not(b == 2)

Finally we can splice those modified quosures into a filter call:

exclude <- function(data, ...) {
  ndots <- map( quos(...), negate_quosure )
  filter( data, !!!ndots )
}
exclude( data, a == 1, b == 2 )
##   a b
## 1 2 1
## 2 2 3

And celebrate our !!! (aka bang bang bang) skills:

Lionel came to the rescue on twitter for a better implementation of negate_quosure

negate_quosure <- function(q){
  quo(not(UQ(q)))
}
exclude <- function(data, ...) {
  ndots <- map( quos(...), negate_quosure )
  filter( data, !!!ndots )
}
exclude( data, a == 1, b == 2 )
##   a b
## 1 2 1
## 2 2 3

Actually while we are here, we can make this all thing purrr:

exclude <- function(data, ...) {
  ndots <- map( quos(...), ~ quo(not(!!.x)) )
  filter( data, !!!ndots )
}
exclude( data, a == 1, b == 2 )
##   a b
## 1 2 1
## 2 2 3