A.3 O operador %>% e o encadeamento de ações

Notem que nada do que vimos até aqui parece ser muito relevante se comparamos com o que pode ser feito com o pacote base do R. Vejamos:

# Queremos selecionar colunas? Operadores `$` e `[[` dao conta
head(
  iris[, which(names(iris) == "Species")],
  10
)
##  [1] setosa setosa setosa setosa setosa setosa setosa setosa setosa setosa
## Levels: setosa versicolor virginica
head(
  iris[, "Sepal.Length"],
  10
)
##  [1] 5.1 4.9 4.7 4.6 5.0 5.4 4.6 5.0 4.4 4.9
# Filtrar linhas? Vetores lógicos em conjunto com o operador `[[` em um data.frame resolvem o problema
head(
  iris[iris$Species == "virginica" & iris$Sepal.Length > 7, ],
  10
)
Sepal.Length Sepal.Width Petal.Length Petal.Width Species
103 7.1 3.0 5.9 2.1 virginica
106 7.6 3.0 6.6 2.1 virginica
108 7.3 2.9 6.3 1.8 virginica
110 7.2 3.6 6.1 2.5 virginica
118 7.7 3.8 6.7 2.2 virginica
119 7.7 2.6 6.9 2.3 virginica
123 7.7 2.8 6.7 2.0 virginica
126 7.2 3.2 6.0 1.8 virginica
130 7.2 3.0 5.8 1.6 virginica
131 7.4 2.8 6.1 1.9 virginica
# Ou podemos filtrar também usando a função `subset`:
head(
  subset(iris, Species == "virginica" & iris$Sepal.Length > 7),
  10
)
Sepal.Length Sepal.Width Petal.Length Petal.Width Species
103 7.1 3.0 5.9 2.1 virginica
106 7.6 3.0 6.6 2.1 virginica
108 7.3 2.9 6.3 1.8 virginica
110 7.2 3.6 6.1 2.5 virginica
118 7.7 3.8 6.7 2.2 virginica
119 7.7 2.6 6.9 2.3 virginica
123 7.7 2.8 6.7 2.0 virginica
126 7.2 3.2 6.0 1.8 virginica
130 7.2 3.0 5.8 1.6 virginica
131 7.4 2.8 6.1 1.9 virginica
# Criar novas colunas? Podemos atribuir novas colunas a qualquer data.frame existente usando o operador `$` para criar uma nova coluna qualquer
iris_novo <- iris
iris_novo$razaopetsep <- iris_novo$Petal.Length / iris_novo$Sepal.Length
head(
  iris_novo,
  10
)
Sepal.Length Sepal.Width Petal.Length Petal.Width Species razaopetsep
5.1 3.5 1.4 0.2 setosa 0.2745098
4.9 3.0 1.4 0.2 setosa 0.2857143
4.7 3.2 1.3 0.2 setosa 0.2765957
4.6 3.1 1.5 0.2 setosa 0.3260870
5.0 3.6 1.4 0.2 setosa 0.2800000
5.4 3.9 1.7 0.4 setosa 0.3148148
4.6 3.4 1.4 0.3 setosa 0.3043478
5.0 3.4 1.5 0.2 setosa 0.3000000
4.4 2.9 1.4 0.2 setosa 0.3181818
4.9 3.1 1.5 0.1 setosa 0.3061224
# Sumariar resultados - Aqui temos um pouco mais de trabalho, porem nada muito complexo
iris_count <- as.data.frame(table(iris$Species))
names(iris_count) <- c("Species", "N")
iris_sumario2 <- cbind(iris_count, sepala_c_media = tapply(iris$Sepal.Length, iris$Species, "mean"), sepala_l_media = tapply(iris$Sepal.Width, iris$Species, "mean"), petala_c_media = tapply(iris$Petal.Length, iris$Species, "mean"), petala_l_media = tapply(iris$Petal.Width, iris$Species, "mean"))
head(
  iris_sumario2,
  10
) # comparem o resultado do objeto `iris_sumario2` com os de `iris_sumario` criado com as funcoes do pacote `dplyr`
Species N sepala_c_media sepala_l_media petala_c_media petala_l_media
setosa setosa 50 5.006 3.428 1.462 0.246
versicolor versicolor 50 5.936 2.770 4.260 1.326
virginica virginica 50 6.588 2.974 5.552 2.026

O operador %>% foi introduzido no R por meio do pacote magrittr, de autoria de Stefan Milton Bache, com o intuito de encadear ações na manipulação de data.frames e facilitar a leitura do código. Segundo palavras do próprio autor, o operador %>% modifica semanticamente o código em R e o torna mais intuitivo tanto na escrita quanto na leitura. Será? Vamos tentar entender isso na prática. Vamos retomar os exemplos acima com a introdução do operador %>% e usá-lo para efetuar dois conjuntos de comandos, expostos abaixo:

A.3.1 Conjunto de comandos 1

# Chamar o data.frame `iris`, então...
# Selecionar as colunas `Species`, `Petal.Length`, e `Sepal.Length`, então ...
# Agrupar os dados em função de `Species`, então ...
# Sumariar os dados para obter o número de observações por grupo, nomeando esta variável como `N`; obter o comprimento médio de pétalas, nomeando esta variável como `petala_l_media`, e o comprimento médio de sépalas, nomeando esta variável como `sepala_l_media`, então ...
# Atribui o resultado dessa operação a um objeto chamado `res1`

A.3.2 Conjunto de comandos 2

# Chamar o data.frame `iris`, então...
# Selecionar as colunas `Species`, `Petal.Length`, e `Sepal.Length`, então ...
# Filtrar os dados para conter apenas a espécie `virginica` e espécimes com comprimento de sépala maior que 7 cm, então ...
# Criar uma nova coluna chamada `razaopetsep` que contenha a razão entre os comprimentos de pétala e sépala, então ...
# Sumariar os dados para obter o número total de observações, nomeando esta variável como `N`; obter o comprimento médio de pétalas, nomeando esta variável como `petala_l_media`, o comprimento médio de sépalas, nomeando esta variável como `sepala_l_media`, e a média do índice da razão entre o comprimento de pétalas e o comprimento de sépalas, nomeando-a como `media_razaopetsep`, então ...
# Atribui o resultado dessa operação a um objeto chamado `res2`

Primeiramente, carreguemos o pacote magrittr:

library("magrittr")

Executando o conjunto de comandos 1, temos:

# Chamar o data.frame `iris`, então...
res1 <-
  iris %>%
  # Selecionar as colunas `Species`, `Petal.Length`, e `Sepal.Length`, então ...
  select(Species, Petal.Length, Sepal.Length) %>%
  # Agrupar os dados em função de `Species`, então ...
  group_by(Species) %>%
  # Sumariar os dados para obter o número de observações por grupo, nomeando esta variável como `N`; obter o comprimento médio de pétalas, nomeando esta variável como `petala_l_media`, e o comprimento médio de sépalas, nomeando esta variável como `sepala_l_media`
  summarise(
    N = n(),
    petala_l_media = mean(Petal.Length, na.rm = TRUE),
    sepala_l_media = mean(Sepal.Length, na.rm = TRUE)
  )
res1
Species N petala_l_media sepala_l_media
setosa 50 1.462 5.006
versicolor 50 4.260 5.936
virginica 50 5.552 6.588

Fazendo o mesmo com o conjunto de comandos 2, temos:

# Chamar o data.frame `iris`, então...
res2 <-
  iris %>%
  # Selecionar as colunas `Species`, `Petal.Length`, e `Sepal.Length`, então ...
  select(Species, Petal.Length, Sepal.Length) %>%
  # Filtrar os dados para conter apenas a espécie `virginica` e espécimes com comprimento de sépala maior que 7 cm, então ...
  filter(Species == "virginica" & Sepal.Length > 7) %>%
  # Criar uma nova coluna chamada `razaopetsep` que contenha a razão entre os comprimentos de pétala e sépala, então ...
  mutate(
    razaopetsep = Petal.Length / Sepal.Length
  ) %>%
  # Sumariar os dados para obter o número total de observações, nomeando esta variável como `N`; obter o comprimento médio de pétalas, nomeando esta variável como `petala_l_media`, o comprimento médio de sépalas, nomeando esta variável como `sepala_l_media`, e a média do índice da razão entre o comprimento de pétalas e o comprimento de sépalas, nomeando-a como `media_razaopetsep`
  summarise(
    N = n(),
    petala_l_media = mean(Petal.Length, na.rm = TRUE),
    sepala_l_media = mean(Sepal.Length, na.rm = TRUE)
  )
res2
N petala_l_media sepala_l_media
12 6.3 7.475

Notem que o código fica formatado da maneira que funciona nosso pensamento sobre as ações a serem executadas: pegamos os dados, efetuamos transformações, e agregamos os resultados, praticamente da mesma maneira que o código é executado. Como diz o autor na vinheta de introdução ao operador %>%, é como uma receita, fácil de ler, fácil de seguir (It’s like a recipe – easy to read, easy to follow!). Em conformidade com este entendimento, sugere-se que leiamos o operador %>% como ENTÃO, implicando em uma passagem do resultado da ação à esquerda para a função à direita. Por isso, eu fiz questão de incluir em ambos os conjuntos de comandos, 1 e 2, a palavra então... ao fim de cada sentença.

Um ponto importante que deve ser levado em consideração é que o uso do operador %>% permite que escondamos o data.frame de entrada nas funções. Vejamos na prática para entender. Suponha que nós queiramos selecionar apenas as colunas Species e Petal.Length de iris. Podemos executar isso de duas maneiras, todas com o mesmo resultado:

# podemos representar iris de três maneiras utilizando o operador `%>%`
iris %>% select(Species, Petal.Length) # como temos feito ate aqui
iris %>% select(., Species, Petal.Length) # explicitamos que `iris` esta dentro de select por meio do `.`

Isso pode ficar mais fácil de entender com outro exemplo. Suponha que tenhamos o vetor meuvetor <- c(1:20) e queiramos obter o somatório deste vetor. Podemos executar isso de três maneiras utilizando o operador %>%:

meuvetor <- c(1:20)
meuvetor %>% sum(.) # representando o vetor na forma de um `.`
meuvetor %>% sum() # deixando a funcao vazia
meuvetor %>% sum() # sem parenteses e sem o `.`. O que?????

Todas as maneiras acima executam e geram o mesmo resultado, 210. Essa multiplicidade de maneiras de expor o data.frame (ou o vetor no exemplo acima) é alvo de críticas por parte de alguns estudiosos, devido ao pacote magrittr não exigir que o argumento seja explícito quando usamos o operador %>% (vejam uma boa argumentação nesta postagem de John Mount).

Vale ressaltar que poderíamos muito bem encadear todas as ações executadas acima sem o operador %>%, porém perderíamos a chance de ler o código da esquerda para a direita, oportunidade ofertada pelo uso do operador. Vejamos, usando o conjunto de comandos 2:

summarise(
  mutate(
    filter(
      select(iris, Species, Petal.Length, Sepal.Length),
      Species == "virginica" & Sepal.Length > 7
    ),
    razaopetsep = Petal.Length / Sepal.Length
  ),
  N = n(),
  petala_l_media = mean(Petal.Length, na.rm = TRUE),
  sepala_l_media = mean(Sepal.Length, na.rm = TRUE)
)

Reparem que o código fica mais difícil de ser lido, pois temos de identificar primeiro quem é o data.frame que serve de entrada para a função summarise. Depois, há outros desafios, como entender o que cada função faz, e em qual ordem. Por fim, o código é lido de dentro para fora, um sentido nada intuitivo. Foi pensando em tornar a leitura do código mais fácil que o autor decidiu criar este operador na linguagem R, uma vez que essa lógica já é extensivamente utilizada em algumas outras linguagens de programação, como F# (representada como |> e o bash (e similares) (representada como |).

A.3.3 Resumo do operador %>%:

  • transforma a leitura do código da esquerda para a direita;

  • evita a criação de muitos objetos intermediários na sessão de trabalho;

  • facilita a leitura do código, pois transforma a própria escrita em uma receita.