Filtros dinâmicos em uma função

Olá a todos.
Gostaria de criar uma função que usa seus argumentos para filtrar colunas em um data.frame. O ponto é que eu gostaria que nos casos onde o usuário não declare o parâmetro, a função não faça esse filtro.

Existe alguma forma elegante de se fazer isso que não seja uma serie de if(is.null(arg)){}... ou nem declarado como default do argumento todos os valores possíveis?

Exemplo simples:

library(magrittr)
filter_diamonds = function(what_cut = NULL, 
                           what_color = NULL, 
                           what_clarity= NULL){
  
  data = ggplot2::diamonds
  out = data %>% 
    dplyr::filter(cut %in%  what_cut) %>% 
    dplyr::filter(color %in%  what_color) %>% 
    dplyr::filter(clarity %in%  what_clarity) 
  return(out)
  
}

filter_diamonds(what_cut = 'Premium', what_color = 'E' ,what_clarity = 'SI2') # Funciona bem
#> # A tibble: 519 × 10
#>    carat cut     color clarity depth table price     x     y     z
#>    <dbl> <ord>   <ord> <ord>   <dbl> <dbl> <int> <dbl> <dbl> <dbl>
#>  1  0.2  Premium E     SI2      60.2    62   345  3.79  3.75  2.27
#>  2  0.82 Premium E     SI2      60.8    60  2824  6.05  6.03  3.67
#>  3  0.75 Premium E     SI2      61.9    57  2829  5.88  5.82  3.62
#>  4  0.8  Premium E     SI2      60.2    57  2829  6.05  6.01  3.63
#>  5  0.83 Premium E     SI2      59.2    60  2859  6.17  6.12  3.64
#>  6  0.82 Premium E     SI2      61.7    59  2861  6.01  5.98  3.7 
#>  7  0.79 Premium E     SI2      61      58  2868  5.96  5.9   3.62
#>  8  0.81 Premium E     SI2      62.5    59  2901  5.97  5.9   3.71
#>  9  0.31 Premium E     SI2      60.9    60   558  4.38  4.35  2.66
#> 10  0.8  Premium E     SI2      59.9    58  2939  6.03  5.96  3.59
#> # … with 509 more rows
filter_diamonds(what_cut = 'Premium') # Esperava ver todos os cut == Premium
#> # A tibble: 0 × 10
#> # … with 10 variables: carat <dbl>, cut <ord>, color <ord>, clarity <ord>,
#> #   depth <dbl>, table <dbl>, price <int>, x <dbl>, y <dbl>, z <dbl>

Created on 2022-04-08 by the reprex package (v2.0.1)

Leandro,

Eu não sei se existe uma forma mais simples de fazer isso, mas nesses casos eu normalmente me aproveito da reciclagem de vetores do R, a saber, a capacidade do R de repetir valores escalares até eles virarem um vetor do tamanho correto. Veja só um exemplo abaixo:

# Operação com vetores
c(1, 1, 1, 1) + c(2, 3, 4, 5)
#> [1] 3 4 5 6

# Reciclagem
1 + c(2, 3, 4, 5)
#> [1] 3 4 5 6

# Idem para testes lógicos
TRUE | c(FALSE, TRUE, FALSE, TRUE)
#> [1] TRUE TRUE TRUE TRUE

Created on 2022-04-10 by the reprex package (v2.0.1)

Agora vamos pensar no que é um filter(): nós passamos um teste lógico para ele e ele espera que esse teste retorne um TRUE ou um FALSE para cada linha; se aquela linha tiver um TRUE, ela fica, e o contrário para o FALSE. Podemos nos aproveitar da reciclagem para retornar TRUE para todas as linhas sem precisar de fato fazer um teste para cada linha:

# Exemplo (argumento com o valor padrão)
what <- NULL
is.null(what) | c("a", "b", "c") %in% what
#> [1] TRUE TRUE TRUE

# Exemplo 2 (argumento com valor não-padrão)
what <- "a"
is.null(what) | c("a", "b", "c") %in% what
#> [1]  TRUE FALSE FALSE

Created on 2022-04-10 by the reprex package (v2.0.1)

No código acima, quando what é NULL, por causa da reciclagem, o valor final do teste será TRUE independentemente do que acontecer do lado direito do |. Quando o what não for NULL, então o valor final do teste dependerá inteiramente do lado direito to |. Segue o código completo:

library(magrittr)

filter_diamonds <- function(what_cut = NULL,
                            what_color = NULL,
                            what_clarity = NULL) {
  ggplot2::diamonds %>%
    dplyr::filter(is.null(what_cut) | cut %in% what_cut) %>%
    dplyr::filter(is.null(what_color) | color %in% what_color) %>%
    dplyr::filter(is.null(what_clarity) | clarity %in% what_clarity)
}

filter_diamonds(what_cut = "Premium", what_color = "E", what_clarity = "SI2")
#> # A tibble: 519 × 10
#>    carat cut     color clarity depth table price     x     y     z
#>    <dbl> <ord>   <ord> <ord>   <dbl> <dbl> <int> <dbl> <dbl> <dbl>
#>  1  0.2  Premium E     SI2      60.2    62   345  3.79  3.75  2.27
#>  2  0.82 Premium E     SI2      60.8    60  2824  6.05  6.03  3.67
#>  3  0.75 Premium E     SI2      61.9    57  2829  5.88  5.82  3.62
#>  4  0.8  Premium E     SI2      60.2    57  2829  6.05  6.01  3.63
#>  5  0.83 Premium E     SI2      59.2    60  2859  6.17  6.12  3.64
#>  6  0.82 Premium E     SI2      61.7    59  2861  6.01  5.98  3.7 
#>  7  0.79 Premium E     SI2      61      58  2868  5.96  5.9   3.62
#>  8  0.81 Premium E     SI2      62.5    59  2901  5.97  5.9   3.71
#>  9  0.31 Premium E     SI2      60.9    60   558  4.38  4.35  2.66
#> 10  0.8  Premium E     SI2      59.9    58  2939  6.03  5.96  3.59
#> # … with 509 more rows

filter_diamonds(what_cut = "Premium")
#> # A tibble: 13,791 × 10
#>    carat cut     color clarity depth table price     x     y     z
#>    <dbl> <ord>   <ord> <ord>   <dbl> <dbl> <int> <dbl> <dbl> <dbl>
#>  1  0.21 Premium E     SI1      59.8    61   326  3.89  3.84  2.31
#>  2  0.29 Premium I     VS2      62.4    58   334  4.2   4.23  2.63
#>  3  0.22 Premium F     SI1      60.4    61   342  3.88  3.84  2.33
#>  4  0.2  Premium E     SI2      60.2    62   345  3.79  3.75  2.27
#>  5  0.32 Premium E     I1       60.9    58   345  4.38  4.42  2.68
#>  6  0.24 Premium I     VS1      62.5    57   355  3.97  3.94  2.47
#>  7  0.29 Premium F     SI1      62.4    58   403  4.24  4.26  2.65
#>  8  0.22 Premium E     VS2      61.6    58   404  3.93  3.89  2.41
#>  9  0.22 Premium D     VS2      59.3    62   404  3.91  3.88  2.31
#> 10  0.3  Premium J     SI2      59.3    61   405  4.43  4.38  2.61
#> # … with 13,781 more rows

Created on 2022-04-10 by the reprex package (v2.0.1)

Muito bom. Obrigado Caio. Isso vai ser muito útil.