I am using dplyr::case_when() to match a complicated series of conditions based on several variables in a data frame. Being a defensive programmer I want to check that I have accounted for all possible conditions, but I can't find an elegant way of doing this.
Here is a motivating example:
library("tidyverse")
df <- expand_grid(x=c(NA_integer_, 1L, 2L), y=1L:2L)
df %>%
mutate(Case = case_when(
is.na(x) | is.na(y) ~ NA_character_,
x > y ~ "x>y",
x < y ~ "x<y"
))
The return value has a mixture of NA: the "good ones" where I am just propagating input NA, and the "bad ones" where I forgot the possible condition x==y.
Conceptually what I want is something like:
df %>%
mutate(Case = case_when(
is.na(x) | is.na(y) ~ NA_character_,
x > y ~ "x>y",
x < y ~ "x<y",
TRUE ~ stop("Unmatched condition")
))
But (as clearly documented in dplyr) this won't work as every RHS expression is always evaluated. The following does work:
library("magrittr")
df %>%
mutate(Case = case_when(
is.na(x) | is.na(y) ~ NA_character_,
x > y ~ "x>y",
x < y ~ "x<y",
TRUE ~ "UNMATCHED"
)) %T>%
with(., stopifnot(! "UNMATCHED" %in% Case))
But this (a) feels a bit hackish and (b) is harder to implement with non-character return types.
Does anybody have a (more elegant) solution to ensure that case_when() matches >0 of the specified conditions?
Side note:
It would actually be quite simple to add this functionality directly to dplyr e.g.:
@ -139,7 +139,7 @@
#' starwars %>%
#' mutate(type = case_character_type(height, mass, species, robots = FALSE)) %>%
#' pull(type)
case_when <- function(...) {
case_when <- function(..., require_match = FALSE) {
fs <- compact_null(list2(...))
n <- length(fs)
@ -173,6 +173,10 @@ case_when <- function(...) {
replaced <- replaced | (query[[i]] & !is.na(query[[i]]))
}
if (isTRUE(require_match) && !all(replaced)) {
abort(paste0("Unmatched cases for ", sum(!replaced), " element", if(sum(!replaced)>1) "s"))
}
out
}
So that we can do:
df %>%
mutate(Case = case_when(
is.na(x) | is.na(y) ~ NA_character_,
x > y ~ "x>y",
x < y ~ "x<y",
require_match = TRUE
))
I will submit a pull request for this patch (plus some roxygen stuff) unless of course my question gets shot down in flames :)