Excluding rows
## 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 usesnot
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
it should be quo(not(UQ(q))). Don't change the env, it makes sure that `not` is scoped in your NS, where you know what `not` is → tidy eval
— lionel (@_lionelhenry) July 2, 2017
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