O que é o Tidyverse?

Postagem preparada em versão reduzida para a disciplina BOT-89 Introdução ao R e Preparação de Dados, ligada ao Programa de Pós-graduação em Ciências Biológicas (Botânica) (PPGBOT) do Instituto Nacional de Pesquisas da Amazônia (INPA), Amazonas, Brasil, e ministrada anualmente pelo Dr. Alberto Vicentini (INPA). A versão completa encontra-se aqui.

A linguagem R completou 20 anos de idade neste mês de março de 2020, e a cada dia se torna mais popular. Pesquisa recente publicada no mês de março feita pelo TIOBE Programming Community Index, um site que agrega indicadores sobre a popularidade de linguagens de programação, posiciona a linguagem como a 11º entre as mais populares no mundo A TIOBE agrega dados do Google, Yahoo, Bing, Wikipedia, Youtube, Badu, e Amazon, para gerar estes resultados, que são apresentados mensalmente.

Parte dessa popularidade é relativamente recente, podendo ser verificada a partir do ano de 2014. Coincidência ou não, 2014 é o ano de surgimento do pacote dplyr, o primeiro de uma série de pacotes que coletivamente vieram a se tornar conhecidos como Tidyverse.

Porcentagem de visitas às questões pertinentes a algumas linguagens de programação na plataforma Stack Overflow. Dados obtidos apenas de países desenvolvidos segundo o Banco Mundial. Nota-se o aumento quase linear referente à linguagem R a partir do ano 2014. Figura extraída desta postagem: https://stackoverflow.blog/2017/10/10/impressive-growth-r/

Porcentagem de visitas às questões pertinentes a algumas linguagens de programação na plataforma Stack Overflow. Dados obtidos apenas de países desenvolvidos segundo o Banco Mundial. Nota-se o aumento quase linear referente à linguagem R a partir do ano 2014. Figura extraída desta postagem: https://stackoverflow.blog/2017/10/10/impressive-growth-r/

O que é o Tidyverse?

O Tidyverse é um conjunto de pacotes de R desenvolvidos para a ciência de dados. Todos os pacotes compartilham uma mesma filosofia e gramática da linguagem. Por exemplo, a estrutura das funções é sempre a mesma:

  • o primeiro argumento sempre é data, isto é, você deve sempre apresentar os dados neste local. Já que o universo destes pacotes é focado em dados na forma de uma tabela, aqui sempre deve ser fornecido um data.frame;

  • Argumentos posteriores modificam o data.frame
    • por exemplo, na função select() do pacote dplyr, você deve fornecer os nomes das colunas que deseja selecionar no seu conjunto de dados;
    • na função separate_rows do pacote tidyr, você deve fornecer os nomes das colunas que se deseja separar em uma ou mais colunas além de indicar o separador (por exemplo, você pode ter uma coluna que possui os nomes Sapotaceae;Burseraceae e deseja separar isso em duas colunas; você deve indicar que o separador é ;).
  • A função sempre deve retornar um data.frame (existem algumas exceções feitas às funções de alguns pacotes voltados exclusivamente para lidar com vetores, como por exemplo o pacote purrr; porém o uso dessas funções é geralmente utilizado dentro de colunas do seu data.frame)

Pretendemos aqui apresentar apenas funcionalidades básicas de dois dos pacotes deste universo, e mostrar como se tornam poderosas quando integrados ao mesmo fluxo de trabalho.

dplyr e ggplot2, símbolos do Tidyverse

Talvez os pacotes mais conhecidos deste universo sejam o dplyr e o ggplot2. Enquanto o primeiro é especializado na manipulação de dados, o segundo é voltado para a produção de plots. O dplyr surgiu com o objetivo de fornecer um conjunto de ferramentas (suas funções!) para uma manipulação eficiente de conjuntos de dados sob a forma de data.frames, e rapidamente, ganhou muitos adeptos devido à facilidade de uso de suas funções dentro de uma nova gramática para a manipulação de dados (palavras do criador do pacote, Hadley Wickham, em sua postagem de introdução do pacote). Essa nova gramática incluiu tanto o uso de funções com nomes de verbos (em inglês, vale ressaltar) desenhados para executar muito bem apenas uma ação (Tabela 1), quanto o uso do que se convencionou chamar de pipe, criado para encadear ações da esquerda para a direita, resultando em menos objetos intermediários estocados na área de trabalho e facilitando a leitura do código. Com o uso de verbos como nome de funções e uma sintaxe diferente da tradicionalmente utilizada em R, o pacote ganhou muitos adeptos deste sua disponibilização no CRAN em janeiro de 2012. Seguindo o mesmo caminho, o pacote ggplot2 (Tabela 2), também do mesmo autor do pacote dplyr, porém já de muito mais idade (foi lançado oficialmente em 10 de junho de 2007) se tornou uma referência na produção de gráficos utilizando a linguagem R, ao propor a construção de gráficos por camadas, similar ao utilizado em programas de SIG. Dentro desta nova sintaxe em R, o operador + ganhou uma nova função. Nas próximas seções, vamos ver alguns exemplos práticos utilizando esses dois pacotes.

Usando o dplyr

Vamos utilizar o famoso conjunto iris para aprender a manipular os dados com as ferramentas do dplyr.

O famoso conjunto de dados iris consiste de observações de comprimentos e larguras de sépalas e pétalas de 3 espécies de Iris, um gênero de plantas herbáceas da família Iridaceae. O conjunto de dados possui 150 linhas e 5 colunas. Para quem quiser saber mais sobre esses dados, leia aqui.

Vamos aprender brevemente como funcionam as principais funções deste pacote (Tabela 1). Primeiro vamos carregar o pacote para a sessão de trabalho:

library("dplyr")
## 
## Attaching package: 'dplyr'
## The following objects are masked from 'package:stats':
## 
##     filter, lag
## The following objects are masked from 'package:base':
## 
##     intersect, setdiff, setequal, union

Para não encher esta postagem com linhas desnecessárias, vamos utilizar a função head() para apresentar apenas as primeiras 10 linhas.

Tabela 1: Principais funções do pacote R dplyr.
Função O que faz
select() seleciona colunas dos dados
filter() filtra linhas específicas dos dados
arrange() ordena as linhas do data.frame
mutate() cria novas colunas no data.frame
summarise() sumariza os dados de acordo com grupos
group_by() agrupa os dados segundo grupos

Selecionando colunas com select()

Agora, vamos utilizar a função select() para selecionar colunas. Ela funciona da seguinte maneira:

  • primeiro, utiliza-se como primeiro argumento o nome do data.frame que se deseja trabalhar; em nosso caso, o data.frame se chama iris: select(iris, ...);

  • depois, colocamos no lugar de ... o nome das colunas que desejamos selecionar, sem aspas. Por exemplo, se quisermos selecionar a coluna das espécie, fazemos assim:

head(
  select(iris, Species),
  10)
##    Species
## 1   setosa
## 2   setosa
## 3   setosa
## 4   setosa
## 5   setosa
## 6   setosa
## 7   setosa
## 8   setosa
## 9   setosa
## 10  setosa

Ou se quisermos a coluna de comprimento de pétala mais a coluna das espécies:

head(
  select(iris, Petal.Length, Species),
  10)
##    Petal.Length Species
## 1           1.4  setosa
## 2           1.4  setosa
## 3           1.3  setosa
## 4           1.5  setosa
## 5           1.4  setosa
## 6           1.7  setosa
## 7           1.4  setosa
## 8           1.5  setosa
## 9           1.4  setosa
## 10          1.5  setosa

Também podemos utilizar funções auxiliares para executar buscas nos nomes das colunas segundo determinados padrões. Entre essas funções auxiliares, destacamos a função contains(). Por exemplo, se quisermos selecionar todas as variáveis que contêm “Petal” em seus nomes:

head(
  select(iris, contains("Petal")),
  10)
##    Petal.Length Petal.Width
## 1           1.4         0.2
## 2           1.4         0.2
## 3           1.3         0.2
## 4           1.5         0.2
## 5           1.4         0.2
## 6           1.7         0.4
## 7           1.4         0.3
## 8           1.5         0.2
## 9           1.4         0.2
## 10          1.5         0.1

Filtrando dados com filter()

Se desejamos filtrar os dados segundo alguma informação, devemos utilizar a função filter(). Por exemplo, se quiser checar os dados de pétalas apenas para a espécie setosa, fazemos assim:

head(
  filter(iris, Species == "setosa"),
  10)
##    Sepal.Length Sepal.Width Petal.Length Petal.Width Species
## 1           5.1         3.5          1.4         0.2  setosa
## 2           4.9         3.0          1.4         0.2  setosa
## 3           4.7         3.2          1.3         0.2  setosa
## 4           4.6         3.1          1.5         0.2  setosa
## 5           5.0         3.6          1.4         0.2  setosa
## 6           5.4         3.9          1.7         0.4  setosa
## 7           4.6         3.4          1.4         0.3  setosa
## 8           5.0         3.4          1.5         0.2  setosa
## 9           4.4         2.9          1.4         0.2  setosa
## 10          4.9         3.1          1.5         0.1  setosa

Ou então quais amostras da espécie virginica possuem comprimento de sépala maior que 7 cm:

head(
  filter(iris, Species == "virginica", Sepal.Length > 7),
  10)
##    Sepal.Length Sepal.Width Petal.Length Petal.Width   Species
## 1           7.1         3.0          5.9         2.1 virginica
## 2           7.6         3.0          6.6         2.1 virginica
## 3           7.3         2.9          6.3         1.8 virginica
## 4           7.2         3.6          6.1         2.5 virginica
## 5           7.7         3.8          6.7         2.2 virginica
## 6           7.7         2.6          6.9         2.3 virginica
## 7           7.7         2.8          6.7         2.0 virginica
## 8           7.2         3.2          6.0         1.8 virginica
## 9           7.2         3.0          5.8         1.6 virginica
## 10          7.4         2.8          6.1         1.9 virginica

E se quisermos adicionar uma coluna em iris que consiste na razão entre o comprimento da pétala pelo comprimento da sépala? Chamaremos nossa nova coluna de razaopetsep:

head(
  mutate(iris, razaopetsep = Petal.Length/Sepal.Length),
  10)
##    Sepal.Length Sepal.Width Petal.Length Petal.Width Species razaopetsep
## 1           5.1         3.5          1.4         0.2  setosa   0.2745098
## 2           4.9         3.0          1.4         0.2  setosa   0.2857143
## 3           4.7         3.2          1.3         0.2  setosa   0.2765957
## 4           4.6         3.1          1.5         0.2  setosa   0.3260870
## 5           5.0         3.6          1.4         0.2  setosa   0.2800000
## 6           5.4         3.9          1.7         0.4  setosa   0.3148148
## 7           4.6         3.4          1.4         0.3  setosa   0.3043478
## 8           5.0         3.4          1.5         0.2  setosa   0.3000000
## 9           4.4         2.9          1.4         0.2  setosa   0.3181818
## 10          4.9         3.1          1.5         0.1  setosa   0.3061224

Destrinchando as funções group_by e summarise

As funções group_by e summarise resumem o propósito do pacote dplyr, pois permitem em poucas linhas de comando sumariar os dados, e partem do princípio, muito presente no R através das funções da família apply, chamado split-apply-combine que, em tradução livre, pode ser entendido como uma sequência lógica de ação: quebre em grupos, aplique uma função, e combine os resultados. Vamos partir para o uso dessas funções agrupando os dados em função da coluna Species e calculando a média do comprimento das pétalas (variável Petal.Length):

iris_grouped <- group_by(iris, Species)
iris_sumario <- summarise(iris_grouped, petala_l_media = mean(Petal.Length, na.rm = TRUE))
iris_sumario
## # A tibble: 3 x 2
##   Species    petala_l_media
##   <fct>               <dbl>
## 1 setosa               1.46
## 2 versicolor           4.26
## 3 virginica            5.55

Vamos destrinchar o que fizemos acima. A função group_by os dados em função de alguma ou algumas variáveis. Essa função geralmente é utilizada em conjunto com a função summarise para gerar sumários estatísticos de uma ou mais variáveis.

head(
  group_by(iris, Species),
  10)
## # A tibble: 10 x 5
## # Groups:   Species [1]
##    Sepal.Length Sepal.Width Petal.Length Petal.Width Species
##           <dbl>       <dbl>        <dbl>       <dbl> <fct>  
##  1          5.1         3.5          1.4         0.2 setosa 
##  2          4.9         3            1.4         0.2 setosa 
##  3          4.7         3.2          1.3         0.2 setosa 
##  4          4.6         3.1          1.5         0.2 setosa 
##  5          5           3.6          1.4         0.2 setosa 
##  6          5.4         3.9          1.7         0.4 setosa 
##  7          4.6         3.4          1.4         0.3 setosa 
##  8          5           3.4          1.5         0.2 setosa 
##  9          4.4         2.9          1.4         0.2 setosa 
## 10          4.9         3.1          1.5         0.1 setosa

Vejam que, acima das linhas do conjunto de dados, há a seguinte sentença:

## # Groups:   Species [1]

Ela informa que o objeto gerado a partir de group_by está agrupado ao redor da variável Species. Pensando no pacote base, é como pensar que a variável Species é o argumento INDEX da função tapply(): todos os cálculos a partir desse objeto ocorrerão em função dessa variável. Após agrupar os dados, nós colocamos esse data.frame agrupado via group_by(), iris_grouped <- group_by(iris, Species), como primeiro argumento da função summarise() para então calcular a média do comprimento de pétala:

iris_grouped <- group_by(iris, Species)
summarise(iris_grouped, petala_l_media = mean(Petal.Length, na.rm = TRUE))
## # A tibble: 3 x 2
##   Species    petala_l_media
##   <fct>               <dbl>
## 1 setosa               1.46
## 2 versicolor           4.26
## 3 virginica            5.55

A partir desse mesmo data.frame agrupado, iris_grouped, podemos responder várias outras perguntas:

Quantas amostras por espécies existem nesse conjunto de dados?

Utilizaremos a função n(), que pertence ao mesmo pacote dplyr, para contar o número de grupos. Vejamos:

summarise(iris_grouped, n())
## # A tibble: 3 x 2
##   Species    `n()`
##   <fct>      <int>
## 1 setosa        50
## 2 versicolor    50
## 3 virginica     50

Médias de comprimento de sépalas e pétalas

summarise(iris_grouped, sepala_l_media = mean(Sepal.Length, na.rm = TRUE))
## # A tibble: 3 x 2
##   Species    sepala_l_media
##   <fct>               <dbl>
## 1 setosa               5.01
## 2 versicolor           5.94
## 3 virginica            6.59
summarise(iris_grouped, petala_l_media = mean(Petal.Length, na.rm = TRUE))
## # A tibble: 3 x 2
##   Species    petala_l_media
##   <fct>               <dbl>
## 1 setosa               1.46
## 2 versicolor           4.26
## 3 virginica            5.55

Todas as operações anteriores na mesma linha de comando:

iris_sumario <- summarise(iris_grouped, N = n(), sepala_l_media = mean(Sepal.Length, na.rm = TRUE), petala_l_media = mean(Petal.Length, na.rm = TRUE))
head(iris_sumario, 10)
## # A tibble: 3 x 4
##   Species        N sepala_l_media petala_l_media
##   <fct>      <int>          <dbl>          <dbl>
## 1 setosa        50           5.01           1.46
## 2 versicolor    50           5.94           4.26
## 3 virginica     50           6.59           5.55

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
## 1           5.1         3.5          1.4         0.2  setosa   0.2745098
## 2           4.9         3.0          1.4         0.2  setosa   0.2857143
## 3           4.7         3.2          1.3         0.2  setosa   0.2765957
## 4           4.6         3.1          1.5         0.2  setosa   0.3260870
## 5           5.0         3.6          1.4         0.2  setosa   0.2800000
## 6           5.4         3.9          1.7         0.4  setosa   0.3148148
## 7           4.6         3.4          1.4         0.3  setosa   0.3043478
## 8           5.0         3.4          1.5         0.2  setosa   0.3000000
## 9           4.4         2.9          1.4         0.2  setosa   0.3181818
## 10          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
## setosa         setosa 50          5.006          3.428          1.462
## versicolor versicolor 50          5.936          2.770          4.260
## virginica   virginica 50          6.588          2.974          5.552
##            petala_l_media
## setosa              0.246
## versicolor          1.326
## virginica           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:

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`

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
## # A tibble: 3 x 4
##   Species        N petala_l_media sepala_l_media
##   <fct>      <int>          <dbl>          <dbl>
## 1 setosa        50           1.46           5.01
## 2 versicolor    50           4.26           5.94
## 3 virginica     50           5.55           6.59

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
## 1 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 |).

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.

Usando o ggplot2

O pacote ggplot2 funciona de maneira diferente da função plot() do pacote base do R, pois trabalha em camadas. Similarmente ao pacote dplyr, começamos com o data.frame que desejamos plotar, contudo, passos posteriores são bem diferentes, e se assemelham mais ao uso do operador %>% do pacote magrittr. No ggplot2, utilizamos o operador + para adicionar as camadas.

Tabela 2: Principais funções do pacote R ggplot2.
Função O que faz
ggplot() Recebe os dados a serem plotados
geom_point() Plota um gráfico de barra
geom_boxplot() Plota um diagrama de caixa
aes() Estética do gráfico
xlab() Modifica o texto do eixo X
ylab() Modifica o texto do eixo Y
ggtitle() Adiciona o título do gráfico
facet_wrap() Divide os gráficos segundo categoria especificada

As principais funções do pacote estão exemplificadas na tabela 2. A função básica do pacote é ggplot(): nela, informamos nosso conjunto de dados no primeiro argumento. Após o primeiro passo, fazemos uso de funções para plotar dados em forma de um espalhamento (scatterplots usando a função geom_point()), gráficos de barra (geom_bar), diagramas de caixa (geom_boxplot()), entre outras. Vejamos na prática como funciona:

library("ggplot2")
# um grafico de espalhamento da variavel Sepal.Length no eixo X e Petal.Length no eixo Y utilizando o conjunto de dados iris
ggplot(iris) + geom_point(aes(x = Sepal.Length, y = Petal.Length))

Dentro das funções que plotam os dados efetivamente (e.g., geom_point(), geom_boxplot()), devemos sempre usar a função aes(): nela inserimos os eixos x e y, informando sempre o nome das colunas sem aspas. Se quisermos colorir os pontos em função das espécies, fazemos:

ggplot(iris) + geom_point(aes(x = Sepal.Length, y = Petal.Length, color = Species))

Por trabalhar em camadas, podemos atribuir os resultados dessas operações a objetos. Por exemplo, vamos passar o resultado da ação acima para um objeto meugrafico e mudar os temas do gráfico:

meugrafico <- ggplot(iris) + geom_point(aes(x = Sepal.Length, y = Petal.Length, color = Species))
meugrafico + theme_bw() # Existem varios outros temas pre-definidos no ggplot2

meugrafico + theme_minimal() # Para utilizar os outros temas, e so verificar o help de funcoes que comecam com theme_OUTROSnomes

meugrafico + theme_void () # - existem, por exemplo, os temas theme_grey, theme_classic, theme_light etc

Podemos facilmente também gerar um gráfico para cada espécie utilizando a função facet_wrap():

meugrafico + facet_wrap(~Species)

Não temos a intenção de cobrir todo o uso do pacote ggplot2 nesta postagem. Existem muitas páginas com excelentes tutoriais na internet que podem ser visitadas para um maior aprofundamento nas ferramentas deste pacote (vejam abaixo na seção Para saber mais). Queremos aqui demonstrar o uso concomitante do pacote ggplot2 dentro de uma linha de trabalho associada ao pacote dplyr. Passemos para a seção abaixo.

dplyr e ggplot2 em conjunto

Durante uma análise exploratória de dados, muitas perguntas surgem com a análise de gráficos simples, que podemos criar com poucas linhas de comando. Com os comandos ensinados nos passos anteriores, e novamente utilizando o conjunto de dados iris, vamos fazer uma exploração muito breve nesses dados.

Gráfico de espalhamento

iris %>% 
  select(Species, Sepal.Width, Sepal.Length) %>% 
  ggplot(.) + # lembrem-se que o data.frame com colunas selecionadas acima aqui e representado por um `.`
  geom_point(aes(x = Sepal.Width, y = Sepal.Length, color = Species)) +
  xlab("Largura de sépala (cm)") +
  ylab("Comprimento de sépala (cm)")

Diagrama de caixas

iris %>% 
  select(Species, Sepal.Length) %>% 
  ggplot(.) +
  geom_boxplot(aes(x = Species, y = Sepal.Length, fill = Species)) +
  xlab("Espécies de Iris") +
  ylab("Comprimento de sépala (cm)")

Histograma

iris %>% 
  select(Species, Sepal.Width) %>% 
  ggplot(.) + 
  geom_histogram(aes(x = Sepal.Width, fill = Species)) + 
  xlab("Largura de sépala (cm)") +
  ylab("Frequência") +
  facet_wrap(~Species)
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.

Comparando o mesmo conjunto de ações entre base R e dplyr

Abaixo, para efeitos de comparação, executamos as mesmas ações usando os pacotes base e dplyr; ao utilizar o dplyr, também fizemos uso do operador %>%.

# para avaliar os objetos criados no ambiente de trabalho, vamos apagar tudo da area de trabalho e comecar do zero
rm(list=ls())
# filtra os dados em iris
## quem tem sepala menor que 6 cm e tem petala maior que 5 cm?
iris2 <-  subset(iris, Sepal.Length < 6 & Petal.Length < 5)
head(iris2, 10)
##    Sepal.Length Sepal.Width Petal.Length Petal.Width Species
## 1           5.1         3.5          1.4         0.2  setosa
## 2           4.9         3.0          1.4         0.2  setosa
## 3           4.7         3.2          1.3         0.2  setosa
## 4           4.6         3.1          1.5         0.2  setosa
## 5           5.0         3.6          1.4         0.2  setosa
## 6           5.4         3.9          1.7         0.4  setosa
## 7           4.6         3.4          1.4         0.3  setosa
## 8           5.0         3.4          1.5         0.2  setosa
## 9           4.4         2.9          1.4         0.2  setosa
## 10          4.9         3.1          1.5         0.1  setosa
# checa as dimensoes do novo objeto
dim(iris2)
## [1] 78  5
# ordena os dados segundo comprimento da petala
iris_ord <- iris2[order(iris2$Petal.Length),]
head(iris_ord, 10)
##    Sepal.Length Sepal.Width Petal.Length Petal.Width Species
## 23          4.6         3.6          1.0         0.2  setosa
## 14          4.3         3.0          1.1         0.1  setosa
## 15          5.8         4.0          1.2         0.2  setosa
## 36          5.0         3.2          1.2         0.2  setosa
## 3           4.7         3.2          1.3         0.2  setosa
## 17          5.4         3.9          1.3         0.4  setosa
## 37          5.5         3.5          1.3         0.2  setosa
## 39          4.4         3.0          1.3         0.2  setosa
## 41          5.0         3.5          1.3         0.3  setosa
## 42          4.5         2.3          1.3         0.3  setosa
# muda a ordem das colunas, colocando a coluna Species primeiro
colsp <- which(colnames(iris_ord) == "Species") # qual a posicao da coluna Species?
irisf <- iris_ord[, c(colsp, (which(!(1:ncol(iris_ord))  %in% colsp)))]
# cria uma nova coluna de razao entre comprimento da sepala e comprimento da petala
irisf$razao_sepall_petall <- irisf$Sepal.Length/irisf$Petal.Length
head(irisf, 10)
##    Species Sepal.Length Sepal.Width Petal.Length Petal.Width
## 23  setosa          4.6         3.6          1.0         0.2
## 14  setosa          4.3         3.0          1.1         0.1
## 15  setosa          5.8         4.0          1.2         0.2
## 36  setosa          5.0         3.2          1.2         0.2
## 3   setosa          4.7         3.2          1.3         0.2
## 17  setosa          5.4         3.9          1.3         0.4
## 37  setosa          5.5         3.5          1.3         0.2
## 39  setosa          4.4         3.0          1.3         0.2
## 41  setosa          5.0         3.5          1.3         0.3
## 42  setosa          4.5         2.3          1.3         0.3
##    razao_sepall_petall
## 23            4.600000
## 14            3.909091
## 15            4.833333
## 36            4.166667
## 3             3.615385
## 17            4.153846
## 37            4.230769
## 39            3.384615
## 41            3.846154
## 42            3.461538
# faz um sumario estatisco agrupando as especies e checando min, max, desvio, media e mediana de cada variavel
mean_pl <- aggregate(irisf$Petal.Length, list(Species = irisf$Species), FUN = "mean")
mean_raz <- aggregate(irisf$razao_sepall_petall, list(Species = irisf$Species), FUN = "mean")
sd_pl <- aggregate(irisf$Petal.Length, list(Species = irisf$Species), FUN = "sd")
sd_raz <- aggregate(irisf$razao_sepall_petall, list(Species = irisf$Species), FUN = "sd")
res <- as.data.frame(mean_pl)
res2 <- cbind(res, media_razao = mean_raz[, -1], desvio_pl = sd_pl[, -1], desvio_raz = sd_raz[, -1])
names(res2)[2] <- "media_pl"
res2
##      Species media_pl media_razao desvio_pl desvio_raz
## 1     setosa 1.462000    3.464906 0.1736640 0.43021683
## 2 versicolor 3.969231    1.404475 0.4249887 0.11954571
## 3  virginica 4.700000    1.115873 0.2828427 0.03816132
# plota grafico de pontos com eixo X correspondendo ao comprimento da sepala, e eixo Y ao comprimento da petala
plot(irisf$Sepal.Length, irisf$Petal.Length, col = irisf$Species)

# para avaliar os objetos criados no ambiente de trabalho, vamos apagar tudo da area de trabalho e comecar do zero
rm(list=ls())
# chama os pacotes tidyverse
library("dplyr") # o pacote para manipular dados
library("ggplot2") # o pacote para plotar graficos
# utilizando a filosofia tidyverse, utilizarei o pipe (" %>% ")
iris2t <- # salvamos o objeto como `iris2t` para diferenciar do salvo acima
  iris %>% # chama os dados e passa para a funcao abaixo
  filter(Sepal.Length < 6 & Petal.Length < 5) %>% # quem tem sepala menor que 6 cm E petala maior que 5 cm?
  arrange(Petal.Length) %>% 
  mutate(razao_sepall_petall = Sepal.Length/Petal.Length) %>% 
  select(Species, everything()) # reordena as colunas
head(iris2t, 10)
##    Species Sepal.Length Sepal.Width Petal.Length Petal.Width
## 1   setosa          4.6         3.6          1.0         0.2
## 2   setosa          4.3         3.0          1.1         0.1
## 3   setosa          5.8         4.0          1.2         0.2
## 4   setosa          5.0         3.2          1.2         0.2
## 5   setosa          4.7         3.2          1.3         0.2
## 6   setosa          5.4         3.9          1.3         0.4
## 7   setosa          5.5         3.5          1.3         0.2
## 8   setosa          4.4         3.0          1.3         0.2
## 9   setosa          5.0         3.5          1.3         0.3
## 10  setosa          4.5         2.3          1.3         0.3
##    razao_sepall_petall
## 1             4.600000
## 2             3.909091
## 3             4.833333
## 4             4.166667
## 5             3.615385
## 6             4.153846
## 7             4.230769
## 8             3.384615
## 9             3.846154
## 10            3.461538
# faz um sumario estatisco agrupando as especies e checando min, max, desvio, media e mediana de cada variavel
res_t <- iris2t %>% 
  group_by(Species) %>% # agrupa dados pela coluna Species
  summarise( # sumarisa dados de acordo com os grupo estabelecidos acima
    mean_pl = mean(Petal.Length), # media da coluna comp de petala
    mean_raz = mean(razao_sepall_petall), # media da coluna razao de comp de sepala/petala
    sd_pl = sd(Petal.Length), # desvio do comp da petala
    sd_raz = sd(razao_sepall_petall)) # desvio do comp da sepala
head(res_t, 10)
## # A tibble: 3 x 5
##   Species    mean_pl mean_raz sd_pl sd_raz
##   <fct>        <dbl>    <dbl> <dbl>  <dbl>
## 1 setosa        1.46     3.46 0.174 0.430 
## 2 versicolor    3.97     1.40 0.425 0.120 
## 3 virginica     4.7      1.12 0.283 0.0382
# plota grafico de pontos com eixo X correspondendo ao comprimento da sepala, e eixo Y ao comprimento da petala
tgraf <- ggplot(data = iris2t) + geom_point(aes(x = Sepal.Length, y = Petal.Length, color = Species))
tgraf

Vejam que os resultados são iguais, com exceção da estética diferente proporcionada pelo pacote ggplot2. Porém, há uma mudança notável em como a manipulação dos dados é feita quando utilizamos o operador %>% dentro do fluxo de trabalho, como resumido acima: a leitura é feita da esquerda para a direita, podemos reduzir a criação de objetos intermediários na área de trabalho, e o código pode ser lido com mais clareza. Postagens futuras abordarão com mais detalhes outros usos deste operador e de alguns outros inclusos no mesmo pacote, porém com funções levemente diferentes.

E o uso do operador %>% com o pacote base?

Apesar de termos explorado até aqui o uso do operador %>% apenas com o pacote dplyr, ele pode ser utilizado com qualquer função no R. Vamos retormar o exemplo da seção acima com comandos do pacote base do R, porém adicionando o operador %>% na manipulação dos dados:

iris_ord <- 
  iris %>% 
  subset(Sepal.Length < 6 & Petal.Length < 5) %>%  # filtramos os dados com a funcao subset()
  .[order(.$Petal.Length),] # usamos o `.` para representar o data.frame filtrado no passo anterior
colsp <- which(colnames(iris_ord) == "Species")
irisf <- iris_ord[, c(colsp, (which(!(1:ncol(iris_ord))  %in% colsp)))] 
irisf$razao_sepall_petall <- irisf$Sepal.Length/irisf$Petal.Length
head(irisf, 10)
##    Species Sepal.Length Sepal.Width Petal.Length Petal.Width
## 23  setosa          4.6         3.6          1.0         0.2
## 14  setosa          4.3         3.0          1.1         0.1
## 15  setosa          5.8         4.0          1.2         0.2
## 36  setosa          5.0         3.2          1.2         0.2
## 3   setosa          4.7         3.2          1.3         0.2
## 17  setosa          5.4         3.9          1.3         0.4
## 37  setosa          5.5         3.5          1.3         0.2
## 39  setosa          4.4         3.0          1.3         0.2
## 41  setosa          5.0         3.5          1.3         0.3
## 42  setosa          4.5         2.3          1.3         0.3
##    razao_sepall_petall
## 23            4.600000
## 14            3.909091
## 15            4.833333
## 36            4.166667
## 3             3.615385
## 17            4.153846
## 37            4.230769
## 39            3.384615
## 41            3.846154
## 42            3.461538
mean_pl <- aggregate(irisf$Petal.Length, list(Species = irisf$Species), FUN = "mean")
mean_raz <- aggregate(irisf$razao_sepall_petall, list(Species = irisf$Species), FUN = "mean")
sd_pl <- aggregate(irisf$Petal.Length, list(Species = irisf$Species), FUN = "sd")
sd_raz <- aggregate(irisf$razao_sepall_petall, list(Species = irisf$Species), FUN = "sd")
res <- as.data.frame(mean_pl)
res2 <- cbind(res, media_razao = mean_raz[, -1], desvio_pl = sd_pl[, -1], desvio_raz = sd_raz[, -1])
names(res2)[2] <- "media_pl"
res2
##      Species media_pl media_razao desvio_pl desvio_raz
## 1     setosa 1.462000    3.464906 0.1736640 0.43021683
## 2 versicolor 3.969231    1.404475 0.4249887 0.11954571
## 3  virginica 4.700000    1.115873 0.2828427 0.03816132

Reparem que, mesmo utilizando o operador %>%, algumas transformações realizadas com poucas linhas de código com o pacote dplyr em uma cadeia de comandos (o chamado pipeline em inglês) não são possíveis com o uso do pacote base do R, notadamente a criação de colunas, o rearranjo de colunas, e o sumário de dados.

Tidyverse, um resumo

David Robinson, autor do pacote broom, um dos membros do tidyverse, explica em sua postagem Teach the tidyverse to beginners que sua preferência por ensinar as ferramentas dos pacotes pertencentes ao tidyverse primeiramente a seus alunos se deve à compatibilidade da filosofia de ensino deste universo com o que ele acredita que seja ideal em uma filosofia de ensino: deve haver uma, preferencialmente apenas uma, maneira óbvia de se fazer, mote emprestado do guia de 19 princípios da linguagem de programação Python, conhecido como Zen of Python. Essa filosofia se contrapõe, por exemplo, ao mote da linguagem Perl, que é Há sempre mais de uma maneira de se fazer (nota pessoal: Isso pode ser observado na linguagem R, são sempre múltiplos os caminhos para se chegar a um resultado).

O mesmo autor também afirma nesta postagem que talvez um fluxo de trabalho comum aos alunos de cursos de ciência de dados seja:

  • trabalhar com dados interessantes e reais;

  • Criar gráficos informativos e atrativos;

  • Chegar a conclusões úteis.

Finalizando, ele conclui dizendo que o tidyverse oferece ferramentas a seus usuários que tornam esse caminho mais suave.

O uso do %>% oferece outros aspectos a serem considerados no uso dessas ferramentas:

  • Cada passo do operador resolve um problema;
  • Cada passo ensina uma função;
  • Acelera o uso da linguagem para análise exploratória de dados.

Nem tudo são flores em nenhum dos pontos abordados acima. Há muita controvérsia sobre o caminho a ser tomado em aulas da linguagem R nos dias atuais, que passam pela pergunta: ensinar primeiro o pacote base do R e suas funcionalidades básicas, ou começar pelas ferramentas do tidyverse (ver mais na seção Para saber mais. Não vamos entrar nesse embate nesta postagem. O que podemos afirmar é que qualquer pacote de R, com exceção do pacote base, foi construído em cima das funcionalidades deste último, logo, sem este pacote, não estaríamos nem aqui falando de funcionalidades do dplyr e afins.

Quisemos aqui apresentar funcionalidades básicas de alguns pacotes que podem ser adotadas ou não pelos alunos, e mostrar como podem ser incorporadas no fluxo diário das manipulações de dados. Caso queiram ver manipulações mais complexas em tabelas utilizando as ferramentas do tidyverse, convido-os a checarem estas duas postagens: importando e manipulando dados no R, e um tutorial para gerar o mapa de distribuição de Macrolobium aracaense Farroñay.

Para saber mais:

As postagens abaixo variam entre posições favoráveis ou desfavoráveis ao Tidyverse. Abrangem desde opiniões pessoais sobre o porquê de não ensinar essas ferramentas aos iniciantes na linguagem a questões de performance computacional desse universo quando comparado com o base R.

Leiam e tirem suas próprias conclusões.

  1. A Thousand Gadgets: My Thoughts on the R Tidyverse - - Opinião pessoal do autor da postagem sobre o porquê de ele não simpatizar nem utilizar nenhuma ferramenta do tidyverse.

  2. Tidyverse - Página principal deste universo de pacotes;

  3. R for Data Science - Livro disponível gratuitamente na internet, escrito por Garrett Grolemund e Hadley Wickham, este criador e mente criativa por muitos dos pacotes deste universo

  4. What is the tidyverse - Postagem introdutória sobre o tidyverse e algumas de suas funcionalidades.

  5. Why I don’t use the tidyverse - opinião pessoal do autor sobre o porquê do mesmo não utilizar nenhum pacote do tidyverse.

  6. Don’t teach built-in plotting to beginners (teach ggplot2) - postagem do autor do pacote broom, David Robinson, sobre o porquê de ele ensinar seus alunos a manipularem primeiro o ggplot2 em detrimento das funções gráficas do pacote base do R.

  7. Teach the tidyverse to beginners - postagem de David Robinson sobre sua opinião pessoal de se ensinar primeiro o tidyverse aos alunos de cursos de introdução ao R.

  8. Introducing magrittr - Vinheta de introdução às funcionalidades do operador %>%