Non-standard evaluation em R

Eu estou tentando entender o conceito de NSE em R. Num primeiro momento, tentei replicar a função select() do dplyr e subset() do R base (ou, o vetor de subset [, mais comumente usado).

Considere estas duas funções (f_1 e f_2):

f_1 <- function(dados, mudar) {

  changed <- substitute(expr = mudar) # parse tree

  avaliar <- eval(expr = changed, envir = dados) # quero avaliar a expressão "changed" dentro do ambiente "dados"

  dados[,avaliar]  # o que quero fazer

}

f_2 <- function(dados, mudar) {

  dados[,mudar]  # o que quero fazer

}

Elas funcionam de forma semelhante à função select do dplyr, subset do base e ao operador de subset ([).

O que ocorre é que f_1() tem o mesmo comportamento de f_2(). Pra mim f_1 é que deveria funcionar, não f_2().

Então, quais as diferenças entre f_1() e f_2() e como f1() se relaciona com o conceito de Non-standard evaluation em R?

Giovani,

Eu não sei se estou fazendo alguma coisa errada, mas nenhuma das duas está funcionando pra mim. Será que você pode passar um exemplo de execução das duas funções, talvez com reprex? No meu computador, o objeto avaliar de f_1() traz a coluna corretamente, mas passar isso para [ quebra tudo. Remover a linha dados[,avaliar] de f_1() resolve o problema. Já f_2() (como você mesmo previu), não funciona mesmo.

f_1 <- function(dados, mudar) {
  changed <- substitute(expr = mudar)
  avaliar <- eval(expr = changed, envir = dados)
  dados[,avaliar]
}

f_1_ <- function(dados, mudar) {
  changed <- substitute(expr = mudar)
  eval(expr = changed, envir = dados)
}

f_2 <- function(dados, mudar) {
  dados[,mudar]
}

f_1(mtcars, mpg)
#> Error in `[.data.frame`(dados, , avaliar): undefined columns selected
f_1_(mtcars, mpg)
#>  [1] 21.0 21.0 22.8 21.4 18.7 18.1 14.3 24.4 22.8 19.2 17.8 16.4 17.3 15.2 10.4
#> [16] 10.4 14.7 32.4 30.4 33.9 21.5 15.5 15.2 13.3 19.2 27.3 26.0 30.4 15.8 19.7
#> [31] 15.0 21.4
f_2(mtcars, mpg)
#> Error in `[.data.frame`(dados, , mudar): object 'mpg' not found

Created on 2022-05-16 by the reprex package (v2.0.1)

1 Curtida

Oi, Caio. Obrigado por responder. Peço perdão por não fornecer os dados, eu me esqueci. Aqui estão:

for (i in 1:6) {
  assign(
    x = paste(
      "var", i, sep = "_"
    ), 
    value = runif(
      n = 30, min = 20, max = 100
    )
  )
}

df_1 <- do.call(what = cbind.data.frame, args = mget(x = ls(pattern = "*var")))

Estas são as funções, que retornam o mesmo:

f_1(dados = df_1, 3)

f_2(dados = df_1, 3)

subset(df_1, select = 3)

dplyr::select(.data = df_1, 3)

Eu gostaria de entender o conceito de NSE relacionado à função f_1(). É isso.

A função f_1() eu tirei do livro Advanced R, do Hadley, neste link. Eu apenas mudei os nomes dos argumentos.

Agora entendi. Ambos os código estão certos, é só que o jeito como você está usando eles faz com que o NSE seja desnecessário, por isso a f_2() funciona. Vamos por partes:

  • NSE é útil quando queremos usar o nome de um objeto que não existe no ambiente global. Por exemplo, no dplyr nós usamos os nomes das colunas como elas existissem fora da tabela. Veja o código abaixo.
# Objeto `mpg` não existe
mean(mpg)
#> Error in mean(mpg): object 'mpg' not found

# Mas aqui ele existe, por causa da NSE
dplyr::summarise(mtcars, media = mean(mpg))
#>      media
#> 1 20.09062

Created on 2022-05-16 by the reprex package (v2.0.1)

  • Note que o seu exemplo não usa o nome de nenhuma coluna! Você está usando um índice, que não depende do ambiente para funcionar. Para entender isso, veja os dois cenários abaixo.
# Onde NSE seria útil -----------------------------------------------

# Não existe
mpg
#> Error in eval(expr, envir, enclos): object 'mpg' not found

# Capturar a expressão (dentro de uma função usaríamos substitute())
changed <- expression(mpg)
print(changed)
#> expression(mpg)

# Dentro de mtcars, `mpg` existe e o valor dele é a coluna
eval(expr = changed, envir = mtcars)
#>  [1] 21.0 21.0 22.8 21.4 18.7 18.1 14.3 24.4 22.8 19.2 17.8 16.4 17.3 15.2 10.4
#> [16] 10.4 14.7 32.4 30.4 33.9 21.5 15.5 15.2 13.3 19.2 27.3 26.0 30.4 15.8 19.7
#> [31] 15.0 21.4

# O que você está fazendo -------------------------------------------

# Já existe
3
#> [1] 3

# Capturar a expressão (dentro de uma função usaríamos substitute())
changed <- expression(3)
print(changed)
#> expression(3)

# Dentro de mtcars, 3 continua valendo 3. Nada muda!
eval(expr = changed, envir = mtcars)
#> [1] 3

Created on 2022-05-16 by the reprex package (v2.0.1)

  • Respondendo a sua pergunta, ambas as suas funções funcionam porque, no fundo, você não precisaria estar usando NSE. Ao escolher trabalhar com índices numéricos, eval() simplesmente vai retornar a mesma coisa que sem o eval(). Um cenário em que f_1() funciona e f_2() não funciona ocorre se replicarmos o exemplo que eu dei na resposta anterior.
f_1 <- function(dados, mudar) {
  changed <- substitute(expr = mudar)
  eval(expr = changed, envir = dados)
}

f_2 <- function(dados, mudar) {
  dados[,mudar]
}

f_1(dados = mtcars, mpg)
#>  [1] 21.0 21.0 22.8 21.4 18.7 18.1 14.3 24.4 22.8 19.2 17.8 16.4 17.3 15.2 10.4
#> [16] 10.4 14.7 32.4 30.4 33.9 21.5 15.5 15.2 13.3 19.2 27.3 26.0 30.4 15.8 19.7
#> [31] 15.0 21.4
f_2(dados = mtcars, mpg)
#> Error in `[.data.frame`(dados, , mudar): object 'mpg' not found

Created on 2022-05-16 by the reprex package (v2.0.1)

Fez sentido?

1 Curtida

P.S.: Não estou escrevendo isso na minha resposta principal para não poluir o conteúdo, mas tenho dois comentários extras bem importantes.

  1. Ao que parece, você está lendo a versão desatualizada do Advanced R. A versão mais recente tem exemplos melhores, mais completos e mais claros: Introduction | Advanced R

  2. Eu não sei onde você viu esse código para criar uma base exemplo, mas eu recomendo fortemente que você não use assign() e mget() dessa forma. O problema é igual ao do attach(): com essas funções, você está criando objetos implicitamente, o que pode gerar problemas significativos para o seu ambiente e para o de outras pessoas. Por exemplo, se eu tivesse um objeto var_1 na minha sessão do R, o seu código ia ter sobrescrito ele sem nenhum alerta; adicionalmente, se eu tivesse um objeto var_7 na minha sessão, o seu código ia tentar colocar ele no data frame sem pensar duas vezes. Eu sugiro duas alternativas:

    1. Usar uma base exemplo conhecida, como mtcars. Ela já vem embutida no R e funciona perfeitamente para o seu caso de uso.

    2. Criar uma base de forma reprodutível e que não afete o ambiente global silenciosamente. Abaixo eu trago um exemplo de como eu reproduziria o seu código sem o assign() e o mget(). Fiz questão de não usar tidyverse assim como você, mas seria ainda mais fácil se pudéssemos usar o purrr.

# Dimensões
n_cols <- 6
n_rows <- 30

# Criar base aleatória
valores <- runif(n_cols * n_rows, min = 20, max = 100)
matriz <- matrix(valores, n_rows, n_cols)
df_1 <- as.data.frame(matriz)
df_1 <- setNames(df_1, paste0("var_", 1:n_cols))

df_1
#>       var_1    var_2    var_3    var_4    var_5    var_6
#> 1  23.99319 83.08135 44.91609 88.23554 53.45876 81.12524
#> 2  42.72483 81.87513 36.30606 47.61565 92.76582 68.05732
#> 3  53.21126 43.99220 54.63698 29.17323 43.37121 93.60560
#> 4  33.85476 53.21079 74.66623 26.78936 84.56716 72.92723
#> 5  67.92665 55.22817 44.40981 26.69334 94.11133 75.30311
#> 6  62.49503 97.02495 33.10143 87.83024 85.91052 92.77075
#> 7  26.54523 34.12147 34.07012 65.01604 22.64772 74.04184
#> 8  54.59410 86.51683 75.26457 53.54947 93.62651 61.44068
#> 9  90.84324 35.27832 43.25647 91.19247 36.90249 75.74303
#> 10 58.61632 95.78962 73.55183 75.20845 50.16715 64.64136
#> 11 91.13351 43.45912 98.29957 57.70420 45.59759 53.28298
#> 12 33.31359 87.86609 56.91151 74.26511 42.85071 41.79602
#> 13 64.08158 59.69306 27.07186 55.91688 89.65977 50.72007
#> 14 36.83089 28.75231 85.03205 69.73358 50.04300 22.87607
#> 15 53.19492 81.05688 62.48769 65.63798 23.91084 40.65742
#> 16 51.85673 44.24135 35.27178 50.00914 22.53244 41.42426
#> 17 97.75247 75.11192 80.79885 70.24780 80.49677 82.37019
#> 18 80.81312 94.89223 82.88712 91.60798 72.78799 57.01062
#> 19 85.42045 29.01018 26.60212 68.30809 59.18077 28.26270
#> 20 63.65476 43.48596 83.42579 51.81648 62.43914 46.10564
#> 21 74.36493 78.27764 57.15617 29.66410 20.54996 24.14920
#> 22 74.08149 57.89709 37.81221 84.46472 20.00241 82.12642
#> 23 72.52565 58.47717 98.73068 72.63361 84.89136 37.91157
#> 24 24.86654 75.28698 59.75531 51.25147 88.24850 99.19943
#> 25 72.41611 69.34763 60.51925 73.77818 87.62513 81.37722
#> 26 61.23868 33.05886 95.78225 66.43609 52.63549 41.18476
#> 27 66.00657 71.81542 62.51243 99.60803 77.30090 48.96223
#> 28 77.44212 78.86053 25.86510 61.33261 40.92850 31.60303
#> 29 72.30087 63.31598 39.29860 63.12097 73.30271 77.78095
#> 30 20.22254 41.63650 65.22488 31.35565 67.79861 54.87141

Created on 2022-05-16 by the reprex package (v2.0.1)

1 Curtida

Opa, Caio. Muitíssimo obrigado. Na verdade eu acho que o exemplo do livro que usei como referência induz ao erro ao usar o subset ([) dentro da função. Aí eu coloquei o índice e fiquei confuso quando ambas - f_1() e f_2() - retornaram a mesma coisa.

Agora sei que o NSE é aplicável apenas em nomes. E também agradeço as dicas sobre assign e mget, muito úteis.

Nota
Só uma curiosidade, Caio. Então o NSE nada mais é do que uma fábrica de acúcares sintáticos (syntax sugar), como a função with e within presentes no R?

Abraço.