3  Manipulação de Dados Tabulares

Nos dois capítulos anteriores conhecemos a linguagem R e aprendemos a calcular estatísticas sobre os dados que temos em mãos. Antes de chegar a esse ponto, porém, quase nenhuma base de dados real está pronta para ser resumida. Este capítulo trata do trabalho que antecede e sustenta toda análise, que é deixar os dados no formato em que a pergunta possa ser respondida. Começaremos pela leitura de arquivos em diferentes formatos e pelo operador que encadeia operações no R. Em seguida percorreremos os verbos que formam o vocabulário básico da manipulação, com os quais se filtram linhas, selecionam colunas, ordenam registros, criam variáveis, calculam resumos por grupo e combinam tabelas diferentes. Ao final, veremos como encadear tudo em uma única sequência legível, como guardar o resultado sem perder informação e como reorganizar uma tabela entre os formatos long e wide, decisão que determina o quão fácil ou será realizar certos tipos de tarefa de exploração de dados que veremos nos capítulos mais aplicados deste livro.

Neste capítulo, trabalharemos com dois conjuntos de dados: escolha_modal.csv e velocidades.xlsx. Faça o download de ambos pelos links abaixo e salve-os em uma pasta onde você irá criar o projeto no RStudio no qual executará esta aula. Assim que salvá-los, crie um novo script do R para iniciarmos o estudo.

3.1 Aspectos Introdutórios

3.1.1 O tidyverse

O tidyverse é uma coleção de pacotes do R desenvolvida principalmente pela equipe da Posit e projetada para facilitar a análise de dados. Todos os pacotes compartilham uma filosofia de design comum e estruturas de dados coerentes, o que torna a experiência de trabalho mais fluída e previsível. A documentação completa está disponível em tidyverse.org.

Ao instalar o tidyverse com install.packages("tidyverse"), você obtém um conjunto de pacotes para as principais etapas da análise de dados:

  • ggplot2: criação de gráficos declarativos baseados na Grammar of Graphics
  • dplyr: manipulação de dados tabulares (filtrar, selecionar, transformar, sumarizar e combinar tabelas)
  • tidyr: reestruturação de tabelas para o formato tidy (cada variável em uma coluna, cada observação em uma linha)
  • readr: leitura de arquivos de texto (CSV, TSV etc.) de forma rápida e previsível
  • purrr: programação funcional, facilitando a aplicação de funções a listas e vetores
  • tibble: versão modernizada do data frame, com comportamento mais consistente e impressão mais legível
  • stringr: manipulação de textos e expressões regulares
  • forcats: manipulação de variáveis categóricas (factors)
  • lubridate: manipulação de datas e horários

Neste capítulo, o foco está nos pacotes dplyr e readr, que cobrem as operações mais frequentes na etapa de manipulação de dados tabulares.

3.1.2 Leitura de arquivos CSV com read_csv()

Conforme já vimos anteriormente, o R base oferece a função read.csv() para leitura de arquivos CSV. O pacote readr, parte do tidyverse, oferece a alternativa read_csv(). O código abaixo mostra como usar cada uma delas para ler o arquivo escolha_modal.csv:

em1 <- read.csv("escolha_modal.csv")
em2 <- read_csv("escolha_modal.csv")

Embora ambas façam a leitura do mesmo arquivo, o comportamento ao imprimir os objetos é diferente. em1 é um data frame convencional e, caso a base de dados tenha muitas linhas, ao digitá-lo no console, o R imprimirá centenas delas, fazendo com que tenhamos de subir bastante a barra de rolagem à direita para verificar o nome das suas colunas. Já em2 é do tipo tibble. Ao imprimi-lo, aparecem apenas as primeiras dez linhas e apenas as colunas que cabem na largura do console, além dos tipos de cada variável logo abaixo dos nomes das colunas (como <chr> para texto, <dbl> para número real e <int> para inteiro). Observe abaixo:

em2
# A tibble: 1,000 × 7
   sexo  idade renda posse_auto modo_viagem    dist_viagem tempo_viagem
   <chr> <dbl> <dbl> <chr>      <chr>                <dbl>        <dbl>
 1 F        34  3797 N          "Carro ou App"        14.3           27
 2 F        43  3515 N          "Carro ou App"        36.1           55
 3 F        55  4791 S          "TP"                  26.9           64
 4 F        45  2749 N          "A p\xe9"              2.9           26
 5 M        45  2929 N          "Carro ou App"        20.6           44
 6 M        36  2011 N          "TP"                  22.6           58
 7 F        18  1084 N          "TP"                  29.6           83
 8 F        31  2338 N          "TP"                  26.9           63
 9 M        53  7473 S          "Carro ou App"        18.3           39
10 M        40  2786 N          "Carro ou App"        36.8           57
# ℹ 990 more rows
Nota

Por padrão, tibbles imprimem apenas as 10 primeiras linhas e data frames imprimem até 1000 elementos. É possível alterar esses comportamentos com a função options(), que configura parâmetros globais da sessão R. As opções mais úteis são:

  • options(max.print = n) — define o número máximo de elementos impressos (n) para data frames e outros objetos R base. O padrão é 1000.
  • options(tibble.print_max = n) — define o número máximo de linhas exibidas em tibbles. Quando o objeto tem mais linhas do que esse valor, somente as primeiras n são impressas.
  • options(tibble.print_min = n) — define quantas linhas são exibidas quando o limite máximo é ultrapassado.
  • options(tibble.width = Inf) — exibe todas as colunas, mesmo que o tibble seja mais largo do que a janela do console.

Para sempre exibir todas as linhas de um tibble, use options(tibble.print_max = Inf). Para restaurar os padrões, use options(max.print = 1000, tibble.print_max = 10, tibble.print_min = 10).

Ao imprimir em2, a coluna modo_viagem pode exibir o valor "A p\xe9" no lugar de "A pé". Isso ocorre porque o arquivo foi gravado com codificação Latin-1 (também chamada de ISO-8859-1), enquanto o R, por padrão, tenta ler arquivos em UTF-8. O caractere "é" não ocupa o mesmo byte nos dois sistemas de codificação, resultando na sequência de escape \xe9.

Para corrigir esse problema, é preciso informar a codificação do arquivo na leitura. Com read.csv(), usa-se o argumento fileEncoding:

em1 <- read.csv("escolha_modal.csv", fileEncoding = "Latin1")

Com read_csv(), usa-se o argumento locale:

em2 <- read_csv("escolha_modal.csv", locale = locale(encoding = "Latin1"))

Após essa correção, os acentos aparecerão corretamente. Podemos verificar a classe dos dois objetos com a função class():

class(em1)
class(em2)

O resultado será "data.frame" para em1 e c("tbl_df", "tbl", "data.frame") para em2, confirmando que o tibble é uma extensão do data frame.

3.1.3 Leitura de arquivos Excel com readxl

Para ler arquivos no formato Excel (.xlsx ou .xls), usamos o pacote readxl. Ele não faz parte do núcleo carregado por library(tidyverse), mas é instalado junto com o tidyverse. Para usá-lo, instale-o (se ainda não estiver instalado) e depois carregue-o:

install.packages("readxl")
library(readxl)

O pacote oferece as funções read_excel(), que detecta o formato automaticamente, e read_xlsx() e read_xls, específicas para .xlsx e .xls, respectivamente. Ambas opções abaixo valem para lermos nosso arquivo “velocidades.xlsx”:

vel <- read_excel("velocidades.xlsx")
vel <- read_xlsx("velocidades.xlsx")

Outro aspecto interessante quando queremos utilizar funções de bibliotecas já instaladas, mas não carregadas com o library() no R, é que podemos executá-las usando a notação pacote::função(). Observe abaixo:

vel <- readxl::read_xlsx("velocidades.xlsx")

Essa notação é útil quando se quer usar uma função de um pacote de forma pontual, sem carregar o pacote inteiro na sessão.

3.1.4 O operador pipe (|>)

O operador pipe é uma das ferramentas mais úteis do ecossistema tidyverse. Ele permite encadear operações de forma legível, passando o resultado de uma expressão como o primeiro argumento da expressão seguinte.

Durante muito tempo, o pipe mais usado no R foi o %>%, proveniente do pacote magrittr. A partir do R 4.1.0, lançado em 2021, o R passou a oferecer um pipe nativo, |>, que funciona de forma similar sem exigir nenhum pacote adicional. Neste livro, usamos o pipe nativo |>.

No RStudio, o atalho de teclado Ctrl+Shift+M (Windows e Linux) ou Cmd+Shift+M (Mac) insere o pipe automaticamente. Para garantir que o atalho insira |> e não %>%, vá em Tools > Global Options > Code e marque a opção Use native pipe operator.

A lógica do pipe é simples: em vez de escrever f(x), escrevemos x |> f(). Veja alguns exemplos com a base vel:

vel |>
  head()
# A tibble: 6 × 3
  faixa tipo_veic       velocidade
  <chr> <chr>                <dbl>
1 Cen   Passeio                 54
2 Cen   Caminhão/Ônibus         50
3 Cen   Passeio                 56
4 Dir   Moto                    43
5 Esq   SUV/Pickup              57
6 Cen   Caminhão/Ônibus         50
vel$velocidade |>
  mean()
[1] 53.804

No primeiro exemplo, vel é passado como argumento para head(), que retorna as primeiras seis linhas. No segundo, o vetor vel$velocidade é passado para mean(), que calcula a média. O pipe se torna ainda mais poderoso quando encadeamos várias operações, como veremos nas seções mais adiante.

3.1.5 A função summary() e variáveis factor

Vamos agora fazer a leitura definitiva da base escolha_modal.csv, informando a codificação correta:

em <- read_csv("escolha_modal.csv", locale = locale(encoding = "Latin1"))

Podemos obter um resumo rápido de todas as variáveis com a função summary():

em |>
  summary()
        sexo          idade           renda           posse_auto  
 Length   :1000   Min.   :18.00   Min.   :  327   Length   :1000  
 N.unique :   2   1st Qu.:32.00   1st Qu.: 2142   N.unique :   2  
 N.blank  :   0   Median :43.00   Median : 3017   N.blank  :   0  
 Min.nchar:   1   Mean   :42.84   Mean   : 3993   Min.nchar:   1  
 Max.nchar:   1   3rd Qu.:54.00   3rd Qu.: 5398   Max.nchar:   1  
                  Max.   :80.00   Max.   :14049                   
    modo_viagem    dist_viagem     tempo_viagem   
 Length   :1000   Min.   : 0.10   Min.   :  9.00  
 N.unique :   4   1st Qu.:10.70   1st Qu.: 34.00  
 N.blank  :   0   Median :22.15   Median : 44.00  
 Min.nchar:   2   Mean   :19.65   Mean   : 45.96  
 Max.nchar:  12   3rd Qu.:27.80   3rd Qu.: 56.00  
                  Max.   :45.20   Max.   :105.00  

Para variáveis numéricas, summary() retorna mínimo, primeiro quartil, mediana, média, terceiro quartil e máximo, além do número de valores ausentes. Para variáveis do tipo character (texto), porém, o resultado é pouco informativo, pois o R indica apenas o comprimento e a classe, sem qualquer informação sobre os valores que aparecem.

Para variáveis categóricas, é mais adequado usar o tipo factor. Um factor é uma representação especial para variáveis categóricas no R que armazena os valores distintos como níveis (levels) e permite que o summary() mostre a contagem de cada categoria, entre outras vantagens que veremos mais adiante.

Vamos transformar as variáveis sexo, posse_auto e modo_viagem em factor:

em <- em |>
  mutate(
    sexo        = factor(sexo),
    posse_auto  = factor(posse_auto),
    modo_viagem = factor(modo_viagem)
  )

Agora summary() mostrará a contagem de cada nível:

em |>
  summary()
 sexo        idade           renda       posse_auto       modo_viagem 
 F:478   Min.   :18.00   Min.   :  327   N:613      A pé        :150  
 M:522   1st Qu.:32.00   1st Qu.: 2142   S:387      Bicicleta   :141  
         Median :43.00   Median : 3017              Carro ou App:380  
         Mean   :42.84   Mean   : 3993              TP          :329  
         3rd Qu.:54.00   3rd Qu.: 5398                                
         Max.   :80.00   Max.   :14049                                
  dist_viagem     tempo_viagem   
 Min.   : 0.10   Min.   :  9.00  
 1st Qu.:10.70   1st Qu.: 34.00  
 Median :22.15   Median : 44.00  
 Mean   :19.65   Mean   : 45.96  
 3rd Qu.:27.80   3rd Qu.: 56.00  
 Max.   :45.20   Max.   :105.00  

Para verificar os níveis de um factor, usamos a função levels():

levels(em$modo_viagem)
[1] "A pé"         "Bicicleta"    "Carro ou App" "TP"          
levels(em$sexo)
[1] "F" "M"

3.2 Filtragem de Observações

A filtragem de observações consiste na seleção de um subconjunto de linhas de uma tabela com base em algum critério. O dplyr oferece duas funções principais para isso.

A função slice() seleciona linhas por posição. Para obter as cinco primeiras linhas de vel:

vel |>
  slice(1:5)
# A tibble: 5 × 3
  faixa tipo_veic       velocidade
  <chr> <chr>                <dbl>
1 Cen   Passeio                 54
2 Cen   Caminhão/Ônibus         50
3 Cen   Passeio                 56
4 Dir   Moto                    43
5 Esq   SUV/Pickup              57

A função filter() realiza a filtragem com base em condições lógicas. Os exemplos abaixo usam a base vel com diferentes operadores condicionais sobre a variável velocidade:

vel |>
  filter(velocidade >= 50)
# A tibble: 383 × 3
   faixa tipo_veic       velocidade
   <chr> <chr>                <dbl>
 1 Cen   Passeio                 54
 2 Cen   Caminhão/Ônibus         50
 3 Cen   Passeio                 56
 4 Esq   SUV/Pickup              57
 5 Cen   Caminhão/Ônibus         50
 6 Esq   Passeio                 55
 7 Esq   SUV/Pickup              64
 8 Esq   Passeio                 53
 9 Cen   Passeio                 58
10 Esq   Passeio                 53
# ℹ 373 more rows
vel |>
  filter(velocidade < 45)
# A tibble: 32 × 3
   faixa tipo_veic       velocidade
   <chr> <chr>                <dbl>
 1 Dir   Moto                    43
 2 Dir   Moto                    42
 3 Dir   Caminhão/Ônibus         42
 4 Dir   Caminhão/Ônibus         44
 5 Dir   Moto                    43
 6 Dir   Caminhão/Ônibus         41
 7 Dir   Caminhão/Ônibus         40
 8 Dir   Caminhão/Ônibus         44
 9 Dir   Moto                    43
10 Dir   Caminhão/Ônibus         43
# ℹ 22 more rows

Para combinar condições, usamos os operadores & (E lógico) e | (OU lógico). O exemplo abaixo seleciona registros com velocidade acima de 50 km/h e do tipo de veículo “Moto”:

vel |>
  filter(velocidade > 50 & tipo_veic == "Moto")
# A tibble: 7 × 3
  faixa tipo_veic velocidade
  <chr> <chr>          <dbl>
1 Cen   Moto              51
2 Cen   Moto              52
3 Cen   Moto              55
4 Esq   Moto              51
5 Dir   Moto              55
6 Cen   Moto              51
7 Cen   Moto              54

Já o exemplo seguinte seleciona registros com velocidade entre 40 e 50 km/h:

vel |>
  filter(velocidade > 40 & velocidade < 50)
# A tibble: 114 × 3
   faixa tipo_veic       velocidade
   <chr> <chr>                <dbl>
 1 Dir   Moto                    43
 2 Dir   Moto                    42
 3 Dir   Moto                    46
 4 Dir   Caminhão/Ônibus         48
 5 Dir   Caminhão/Ônibus         47
 6 Dir   Moto                    46
 7 Dir   Caminhão/Ônibus         42
 8 Dir   Moto                    46
 9 Dir   Passeio                 46
10 Dir   Caminhão/Ônibus         47
# ℹ 104 more rows

Na base em, podemos combinar filtros em variáveis diferentes. O exemplo abaixo seleciona viagens realizadas por pessoas do sexo feminino que usaram transporte público (“TP”) ou foram a pé. Note que o operador %in% verifica se o valor está contido em um vetor de opções, sendo uma alternativa mais limpa a múltiplas condições com | quando temos dados do tipo character.

em |>
  filter(sexo == "F" & modo_viagem %in% c("TP", "A pé"))
# A tibble: 277 × 7
   sexo  idade renda posse_auto modo_viagem dist_viagem tempo_viagem
   <fct> <dbl> <dbl> <fct>      <fct>             <dbl>        <dbl>
 1 F        55  4791 S          TP                 26.9           64
 2 F        45  2749 N          A pé                2.9           26
 3 F        18  1084 N          TP                 29.6           83
 4 F        31  2338 N          TP                 26.9           63
 5 F        32  1170 N          TP                 33.7           79
 6 F        20  1292 N          TP                 31.8           70
 7 F        34  2695 N          A pé                2.7           29
 8 F        66  7295 S          TP                 28.4           62
 9 F        25  1963 N          TP                 28.2           62
10 F        45  3037 N          TP                 23.7           67
# ℹ 267 more rows

3.3 Seleção de Colunas

A seleção de colunas permite manter apenas as variáveis relevantes para a análise, facilitando a leitura do código e reduzindo o uso de memória. No dplyr, usamos a função select().

Para selecionar apenas as colunas tipo_veic e velocidade de vel:

vel |>
  select(tipo_veic, velocidade)
# A tibble: 500 × 2
   tipo_veic       velocidade
   <chr>                <dbl>
 1 Passeio                 54
 2 Caminhão/Ônibus         50
 3 Passeio                 56
 4 Moto                    43
 5 SUV/Pickup              57
 6 Caminhão/Ônibus         50
 7 Passeio                 55
 8 SUV/Pickup              64
 9 Passeio                 53
10 Passeio                 58
# ℹ 490 more rows

Para remover uma coluna, basta preceder o nome com -:

vel |>
  select(-velocidade)
# A tibble: 500 × 2
   faixa tipo_veic      
   <chr> <chr>          
 1 Cen   Passeio        
 2 Cen   Caminhão/Ônibus
 3 Cen   Passeio        
 4 Dir   Moto           
 5 Esq   SUV/Pickup     
 6 Cen   Caminhão/Ônibus
 7 Esq   Passeio        
 8 Esq   SUV/Pickup     
 9 Esq   Passeio        
10 Cen   Passeio        
# ℹ 490 more rows

O select() também pode ser usado para reordenar colunas. Ao listar as colunas na nova ordem desejada, o resultado terá as colunas nessa sequência:

vel |>
  select(velocidade, tipo_veic, faixa)
# A tibble: 500 × 3
   velocidade tipo_veic       faixa
        <dbl> <chr>           <chr>
 1         54 Passeio         Cen  
 2         50 Caminhão/Ônibus Cen  
 3         56 Passeio         Cen  
 4         43 Moto            Dir  
 5         57 SUV/Pickup      Esq  
 6         50 Caminhão/Ônibus Cen  
 7         55 Passeio         Esq  
 8         64 SUV/Pickup      Esq  
 9         53 Passeio         Esq  
10         58 Passeio         Cen  
# ℹ 490 more rows

Eventualmente, nós podemos obter os nomes das variáveis de interesse a serem analisadas a partir de alguma operação anterior que as retorne em formato character. Como a função select() não aceita nomes de colunas nesse formato, podemos usamos a função auxiliar all_of(), conforme o exemplo abaixo:

colunas <- c("tipo_veic", "velocidade")
vel |>
  select(all_of(colunas))
# A tibble: 500 × 2
   tipo_veic       velocidade
   <chr>                <dbl>
 1 Passeio                 54
 2 Caminhão/Ônibus         50
 3 Passeio                 56
 4 Moto                    43
 5 SUV/Pickup              57
 6 Caminhão/Ônibus         50
 7 Passeio                 55
 8 SUV/Pickup              64
 9 Passeio                 53
10 Passeio                 58
# ℹ 490 more rows

3.4 Ordenação de Linhas

A ordenação de linhas reordena as observações de uma tabela com base nos valores de uma ou mais colunas. No dplyr, usamos a função arrange().

Para ordenar vel pela velocidade em ordem crescente:

vel |>
  arrange(velocidade)
# A tibble: 500 × 3
   faixa tipo_veic       velocidade
   <chr> <chr>                <dbl>
 1 Dir   Caminhão/Ônibus         38
 2 Dir   Caminhão/Ônibus         40
 3 Dir   Moto                    40
 4 Dir   Caminhão/Ônibus         41
 5 Dir   Moto                    41
 6 Dir   Caminhão/Ônibus         41
 7 Dir   Moto                    42
 8 Dir   Caminhão/Ônibus         42
 9 Dir   Caminhão/Ônibus         42
10 Dir   Moto                    42
# ℹ 490 more rows

Para ordenar em ordem decrescente, usamos a função auxiliar desc():

vel |>
  arrange(desc(velocidade))
# A tibble: 500 × 3
   faixa tipo_veic  velocidade
   <chr> <chr>           <dbl>
 1 Esq   SUV/Pickup         66
 2 Esq   Passeio            65
 3 Esq   Passeio            65
 4 Esq   Passeio            65
 5 Esq   SUV/Pickup         64
 6 Esq   Passeio            64
 7 Esq   Passeio            64
 8 Esq   Passeio            63
 9 Esq   Passeio            63
10 Esq   Passeio            63
# ℹ 490 more rows

Para variáveis numéricas, uma alternativa mais compacta é usar o sinal de negação diretamente:

vel |>
  arrange(-velocidade)
# A tibble: 500 × 3
   faixa tipo_veic  velocidade
   <chr> <chr>           <dbl>
 1 Esq   SUV/Pickup         66
 2 Esq   Passeio            65
 3 Esq   Passeio            65
 4 Esq   Passeio            65
 5 Esq   SUV/Pickup         64
 6 Esq   Passeio            64
 7 Esq   Passeio            64
 8 Esq   Passeio            63
 9 Esq   Passeio            63
10 Esq   Passeio            63
# ℹ 490 more rows

O resultado é idêntico. Atente-se, no entanto, que a notação com - não funciona para variáveis de texto ou factor, situações em que desc() é obrigatório.

A ordenação também é possível por múltiplas colunas. O código abaixo ordena primeiro por tipo_veic (em ordem alfabética) e, dentro de cada tipo, por velocidade em ordem decrescente:

vel |>
  arrange(tipo_veic, desc(velocidade))
# A tibble: 500 × 3
   faixa tipo_veic       velocidade
   <chr> <chr>                <dbl>
 1 Cen   Caminhão/Ônibus         56
 2 Cen   Caminhão/Ônibus         55
 3 Cen   Caminhão/Ônibus         54
 4 Cen   Caminhão/Ônibus         53
 5 Esq   Caminhão/Ônibus         53
 6 Cen   Caminhão/Ônibus         52
 7 Cen   Caminhão/Ônibus         52
 8 Dir   Caminhão/Ônibus         52
 9 Cen   Caminhão/Ônibus         52
10 Cen   Caminhão/Ônibus         52
# ℹ 490 more rows

Observe que a ordenação de tipo_veic segue a ordem alfabética, o que não reflete uma sequência lógica em termos de porte do veículo. A ordem natural seria Moto, Passeio, SUV/Pickup e Caminhão/Ônibus. Para impor essa ordem, convertemos tipo_veic em factor com os níveis na sequência desejada:

vel <- vel |>
  mutate(
    tipo_veic = factor(
      tipo_veic,
      levels = c("Moto", "Passeio", "SUV/Pickup", "Caminhão/Ônibus")
    )
  )

Quando a variável categórica tem uma ordem intrínseca com significado (como o porte do veículo), podemos declará-la explicitamente com o argumento ordered = TRUE, fazendo com que esta se torne uma variável categórica ordinal:

vel <- vel |>
  mutate(
    tipo_veic = factor(
      tipo_veic,
      levels  = c("Moto", "Passeio", "SUV/Pickup", "Caminhão/Ônibus"),
      ordered = TRUE
    )
  )

Um factor ordenado permite comparações lógicas entre categorias. Por exemplo, tipo_veic > "Passeio" retornará TRUE para “SUV/Pickup” e “Caminhão/Ônibus” e FALSE para “Moto” e “Passeio”. Ao ordenar novamente por tipo_veic, o resultado seguirá a ordem dos níveis definidos:

vel |>
  arrange(tipo_veic)
# A tibble: 500 × 3
   faixa tipo_veic velocidade
   <chr> <fct>          <dbl>
 1 Dir   Moto              43
 2 Dir   Moto              42
 3 Dir   Moto              46
 4 Dir   Moto              46
 5 Dir   Moto              46
 6 Cen   Moto              51
 7 Dir   Moto              43
 8 Dir   Moto              45
 9 Cen   Moto              48
10 Dir   Moto              43
# ℹ 490 more rows

Podemos confirmar os níveis com levels():

levels(vel$tipo_veic)
[1] "Moto"            "Passeio"         "SUV/Pickup"      "Caminhão/Ônibus"

3.5 Transformação e Criação de Variáveis

A função mutate() do dplyr permite criar novas variáveis ou modificar variáveis existentes. Ela adiciona (ou substitui) colunas na tabela sem alterar o número de linhas.

A variável tempo_viagem na base em está originalmente em minutos. Vamos convertê-la para horas, sobrescrevendo a coluna original:

em <- em |>
  mutate(tempo_viagem = tempo_viagem / 60)

Com o tempo agora em horas, podemos criar uma nova variável com a velocidade média da viagem, dividindo a distância pelo tempo:

em <- em |>
  mutate(vel_media_viagem = dist_viagem / tempo_viagem)

Para criar variáveis dicotômicas, a função ifelse() é útil. O código abaixo cria uma variável lógica indicando se o veículo estava acima de 60 km/h:

vel <- vel |>
  mutate(acim60 = ifelse(velocidade > 60, TRUE, FALSE))

Para classificações com mais de duas categorias, costumamos utilizar o ifelse() de forma aninhada, ou, de maneira mais simples, usando a função case_when(). O exemplo abaixo cria a variável veic_pesado, listando explicitamente quais categorias pertencem a cada grupo por meio do operador %in%:

vel <- vel |>
  mutate(
    veic_pesado = case_when(
      tipo_veic %in% c("Caminhão/Ônibus")               ~ "S",
      tipo_veic %in% c("Moto", "Passeio", "SUV/Pickup") ~ "N"
    )
  )

Quando nem todas as combinações precisam ser enumeradas, podemos usar .default para definir o valor dos casos restantes, tornando o código mais conciso:

vel <- vel |>
  mutate(
    veic_pesado = case_when(
      tipo_veic == "Caminhão/Ônibus" ~ "S",
      .default = "N"
    )
  )

A sintaxe do case_when() segue o padrão condição ~ valor, com .default definindo o valor para os casos que não atendem a nenhuma condição explícita. Uma alternativa equivalente ao .default é usar TRUE ~ "N", notação mais antiga que ainda é amplamente encontrada em códigos R.

Há situações em que o interesse não está em acrescentar colunas à base original, mas em produzir uma tabela nova contendo apenas as variáveis derivadas. Para isso existe o transmute(), que calcula exatamente como o mutate() e descarta todas as colunas que não forem mencionadas. O exemplo abaixo monta uma tabela compacta com o modo de transporte, a distância em quilômetros, o tempo convertido de volta para minutos e a velocidade média, deixando de fora as demais variáveis de em.

em |>
  transmute(
    modo_viagem,
    dist_km   = dist_viagem,
    tempo_min = tempo_viagem * 60,
    vel_kmh   = vel_media_viagem
  )
# A tibble: 1,000 × 4
   modo_viagem  dist_km tempo_min vel_kmh
   <fct>          <dbl>     <dbl>   <dbl>
 1 Carro ou App    14.3        27   31.8 
 2 Carro ou App    36.1        55   39.4 
 3 TP              26.9        64   25.2 
 4 A pé             2.9        26    6.69
 5 Carro ou App    20.6        44   28.1 
 6 TP              22.6        58   23.4 
 7 TP              29.6        83   21.4 
 8 TP              26.9        63   25.6 
 9 Carro ou App    18.3        39   28.2 
10 Carro ou App    36.8        57   38.7 
# ℹ 990 more rows

Observe que colunas já existentes podem ser mantidas apenas citando seu nome, como modo_viagem, ou renomeadas no caminho, como dist_viagem, que passou a se chamar dist_km. Note também que nada foi atribuído a um objeto, de modo que a tabela em original permanece intacta e o resultado apenas foi impresso no console.

Nota

A partir da versão 1.1.0 do dplyr, o transmute() passou à condição de superseded, ou seja, continua funcionando e não será removido, mas deixou de receber novos desenvolvimentos. A recomendação atual é obter o mesmo resultado com o argumento .keep = "none" do próprio mutate(). A única diferença de escrita está nas colunas já existentes, que precisam ser atribuídas a si mesmas.

em |>
  mutate(
    modo_viagem = modo_viagem,
    dist_km     = dist_viagem,
    tempo_min   = tempo_viagem * 60,
    vel_kmh     = vel_media_viagem,
    .keep = "none"
  )

Vale conhecer as duas formas, já que o transmute() aparece com frequência em códigos escritos antes dessa mudança.

3.6 Sumarização de Dados

A sumarização reduz uma tabela a um conjunto de estatísticas calculadas a partir dos valores das colunas. No dplyr, a combinação de group_by() e summarize()1 é o principal mecanismo para isso.

Vamos calcular a velocidade média de viagem por modo de transporte na base em:

em |>
  group_by(modo_viagem) |>
  summarize(vel_media = mean(vel_media_viagem, na.rm = TRUE))
# A tibble: 4 × 2
  modo_viagem  vel_media
  <fct>            <dbl>
1 A pé              4.19
2 Bicicleta        15.2 
3 Carro ou App     35.0 
4 TP               24.7 

O argumento na.rm = TRUE instrui a função a ignorar os valores ausentes (NA) no cálculo. Sem ele, qualquer NA na coluna faria com que o resultado fosse NA para o grupo inteiro.

Podemos calcular várias estatísticas de uma vez no mesmo summarize():

em |>
  group_by(modo_viagem) |>
  summarize(
    vel_media   = mean(vel_media_viagem,            na.rm = TRUE),
    vel_mediana = median(vel_media_viagem,          na.rm = TRUE),
    q1          = quantile(vel_media_viagem, 0.25,  na.rm = TRUE),
    q3          = quantile(vel_media_viagem, 0.75,  na.rm = TRUE),
    desvio_pad  = sd(vel_media_viagem,              na.rm = TRUE)
  )
# A tibble: 4 × 6
  modo_viagem  vel_media vel_mediana    q1    q3 desvio_pad
  <fct>            <dbl>       <dbl> <dbl> <dbl>      <dbl>
1 A pé              4.19        4.25  3.39  4.97       1.13
2 Bicicleta        15.2        15.1  13.0  17.2        3.55
3 Carro ou App     35.0        34.8  32.3  37.7        4.32
4 TP               24.7        24.7  22.7  26.8        3.19

Um outro uso bastante interessante é o de contagem de observações em níveis diferentes de variáveis categóricas. Para tanto, usamos a função n() dentro de summarize(), que não recebe argumentos e conta o número de linhas em cada grupo. O exemplo abaixo conta o número de viagens por combinação de sexo e posse de automóvel:

em |>
  group_by(sexo, posse_auto) |>
  summarize(quant = n())
`summarise()` has regrouped the output.
ℹ Summaries were computed grouped by sexo and posse_auto.
ℹ Output is grouped by sexo.
ℹ Use `summarise(.groups = "drop_last")` to silence this message.
ℹ Use `summarise(.by = c(sexo, posse_auto))` for per-operation grouping
  (`?dplyr::dplyr_by`) instead.
# A tibble: 4 × 3
# Groups:   sexo [2]
  sexo  posse_auto quant
  <fct> <fct>      <int>
1 F     N            328
2 F     S            150
3 M     N            285
4 M     S            237

Para esse tipo de contagem simples, o dplyr oferece a função count() como atalho conveniente:

em |>
  count(sexo, posse_auto)
# A tibble: 4 × 3
  sexo  posse_auto     n
  <fct> <fct>      <int>
1 F     N            328
2 F     S            150
3 M     N            285
4 M     S            237

Os valores são equivalentes aos do exemplo anterior, mas há uma diferença importante no objeto retornado. O group_by() seguido de summarize() retorna um tibble ainda agrupado, com o agrupamento por sexo permanecendo ativo após o summarize(). Já o count() retorna um tibble comum, sem agrupamento. Essa distinção é relevante quando se encadeiam operações após a contagem. Com o resultado do group_by() |> summarize(), um mutate() subsequente opera dentro dos grupos remanescentes. Já com o resultado do count(), ele opera sobre o tibble inteiro.

Esse é o caso de quando queremos analisar a proporção dentro de cada grupo, em vez de números absolutos, o que pode ser mais informativo. O código abaixo calcula a proporção de cada combinação dentro de cada sexo, encadeando um mutate() após o summarize():

em |>
  group_by(sexo, posse_auto) |>
  summarize(quant = n()) |>
  mutate(prop = quant / sum(quant))
`summarise()` has regrouped the output.
ℹ Summaries were computed grouped by sexo and posse_auto.
ℹ Output is grouped by sexo.
ℹ Use `summarise(.groups = "drop_last")` to silence this message.
ℹ Use `summarise(.by = c(sexo, posse_auto))` for per-operation grouping
  (`?dplyr::dplyr_by`) instead.
# A tibble: 4 × 4
# Groups:   sexo [2]
  sexo  posse_auto quant  prop
  <fct> <fct>      <int> <dbl>
1 F     N            328 0.686
2 F     S            150 0.314
3 M     N            285 0.546
4 M     S            237 0.454

O mutate() é aplicado com o agrupamento por sexo ainda ativo, então sum(quant) é calculado dentro de cada categoria de sexo, gerando proporções que somam 1 dentro de cada grupo. Repare que o dplyr imprime uma mensagem informativa antes do resultado, avisando que a saída permaneceu agrupada por sexo. Não é erro nem sinal de problema, é o pacote tornando explícito um comportamento que passaria despercebido. A mensagem, porém, denuncia algo incômodo, pois o resultado depende de uma regra que não está escrita em lugar nenhum do código.

Para contrastar, veja o que acontece ao usar count() e encadear o mesmo mutate():

em |>
  count(sexo, posse_auto) |>
  mutate(prop = n / sum(n))
# A tibble: 4 × 4
  sexo  posse_auto     n  prop
  <fct> <fct>      <int> <dbl>
1 F     N            328 0.328
2 F     S            150 0.15 
3 M     N            285 0.285
4 M     S            237 0.237

Como count() retorna um tibble sem agrupamento, sum(n) é calculado sobre todas as linhas da tabela, gerando proporções em relação ao total geral, não dentro de cada sexo. As quatro proporções agora somam 1 entre si.

Nenhum dos dois resultados está errado, eles simplesmente respondem a perguntas diferentes. O problema do primeiro é de outra natureza. Quem lê aquele código precisa saber de cor que o summarize() descarta apenas o último nível de agrupamento para conseguir prever o que o mutate() seguinte vai fazer. Para este cálculo de proporção dentro de cada grupo existe uma terceira forma, que produz exatamente os mesmos números da primeira sem depender dessa memória e sem gerar mensagem nenhuma. Ela consiste em contar com count(), que devolve a tabela já limpa, e declarar explicitamente o agrupamento desejado antes do mutate():

em |>
  count(sexo, posse_auto) |>
  group_by(sexo) |>
  mutate(prop = n / sum(n))
# A tibble: 4 × 4
# Groups:   sexo [2]
  sexo  posse_auto     n  prop
  <fct> <fct>      <int> <dbl>
1 F     N            328 0.686
2 F     S            150 0.314
3 M     N            285 0.546
4 M     S            237 0.454

Os valores são idênticos aos da primeira versão. A diferença está em que o agrupamento por sexo aparece escrito no código, em vez de sobrar de uma operação anterior. Quem lê não precisa reconstruir mentalmente o que restou do group_by() inicial, e quem escreve pode trocar group_by(sexo) por group_by(posse_auto) para inverter a pergunta sem mexer em mais nada. É a forma que recomendamos como padrão sempre que o objetivo for calcular proporções dentro de grupos.

O resultado continua agrupado por sexo, como indica a linha # Groups: sexo [2] logo abaixo do cabeçalho. Quando as etapas seguintes não dependerem desse agrupamento, encerre a cadeia com ungroup() para devolver um tibble comum.

em |>
  count(sexo, posse_auto) |>
  group_by(sexo) |>
  mutate(prop = n / sum(n)) |>
  ungroup()
# A tibble: 4 × 4
  sexo  posse_auto     n  prop
  <fct> <fct>      <int> <dbl>
1 F     N            328 0.686
2 F     S            150 0.314
3 M     N            285 0.546
4 M     S            237 0.454
Nota

A própria mensagem do summarize() sugere dois caminhos para silenciá-la. O argumento .groups controla o que acontece com o agrupamento na saída, sendo .groups = "drop" a opção que remove todos os níveis e .groups = "drop_last" a que apenas confirma o comportamento padrão de descartar o último. Já o argumento .by permite declarar o agrupamento dentro da própria função, como em summarize(quant = n(), .by = c(sexo, posse_auto)), dispensando o group_by() anterior e devolvendo sempre um resultado sem agrupamento. Vale conhecer as duas alternativas, porque aparecem com frequência, mas nenhuma delas deixa o código mais legível do que a sequência com count() apresentada acima.

3.7 Junção de Tabelas

Em muitas situações, as informações relevantes para uma análise estão distribuídas em tabelas diferentes, que precisam ser combinadas. Essa operação é chamada de junção (join) e é realizada com base em uma ou mais colunas que identificam a correspondência entre as linhas das tabelas.

Suponha que, além dos dados de velocidade, tenhamos uma tabela com o peso médio de cada tipo de veículo:

pesos <- tibble(
  tipo_veic = c("Moto", "Passeio", "SUV/Pickup", "Caminhão/Ônibus"),
  peso_kg   = c(120, 1300, 2000, 16000)
)
pesos
# A tibble: 4 × 2
  tipo_veic       peso_kg
  <chr>             <dbl>
1 Moto                120
2 Passeio            1300
3 SUV/Pickup         2000
4 Caminhão/Ônibus   16000

Para incorporar essa informação à base vel, usamos left_join(), função do dplyr que mantém todas as linhas da tabela à esquerda (vel) e adiciona as colunas da tabela à direita (pesos) para as linhas com correspondência:

vel |>
  left_join(pesos, by = "tipo_veic")
# A tibble: 500 × 6
   faixa tipo_veic       velocidade acim60 veic_pesado peso_kg
   <chr> <chr>                <dbl> <lgl>  <chr>         <dbl>
 1 Cen   Passeio                 54 FALSE  N              1300
 2 Cen   Caminhão/Ônibus         50 FALSE  S             16000
 3 Cen   Passeio                 56 FALSE  N              1300
 4 Dir   Moto                    43 FALSE  N               120
 5 Esq   SUV/Pickup              57 FALSE  N              2000
 6 Cen   Caminhão/Ônibus         50 FALSE  S             16000
 7 Esq   Passeio                 55 FALSE  N              1300
 8 Esq   SUV/Pickup              64 TRUE   N              2000
 9 Esq   Passeio                 53 FALSE  N              1300
10 Cen   Passeio                 58 FALSE  N              1300
# ℹ 490 more rows

O argumento by indica a coluna usada como chave de junção. Neste caso, tipo_veic está presente nas duas tabelas e identifica cada tipo de veículo. Para cada linha de vel, o left_join() busca o peso correspondente em pesos e adiciona a coluna peso_kg ao resultado.

Dica

O dplyr oferece outros tipos de junção para diferentes situações. O right_join() mantém todas as linhas da tabela à direita. O inner_join() mantém apenas as linhas com correspondência nas duas tabelas. O full_join() mantém todas as linhas de ambas as tabelas.

Na prática, é comum que as duas tabelas usem nomes diferentes para a mesma coluna-chave. Suponha que a tabela pesos tivesse a coluna chamada veiculo em vez de tipo_veic:

pesos2 <- tibble(
  veiculo = c("Moto", "Passeio", "SUV/Pickup", "Caminhão/Ônibus"),
  peso_kg = c(120, 1300, 2000, 16000)
)

Nesse caso, o argumento by recebe um vetor nomeado, onde o nome é a coluna da tabela da esquerda e o valor é a coluna da tabela da direita:

vel |>
  left_join(pesos2, by = c("tipo_veic" = "veiculo"))
# A tibble: 500 × 6
   faixa tipo_veic       velocidade acim60 veic_pesado peso_kg
   <chr> <chr>                <dbl> <lgl>  <chr>         <dbl>
 1 Cen   Passeio                 54 FALSE  N              1300
 2 Cen   Caminhão/Ônibus         50 FALSE  S             16000
 3 Cen   Passeio                 56 FALSE  N              1300
 4 Dir   Moto                    43 FALSE  N               120
 5 Esq   SUV/Pickup              57 FALSE  N              2000
 6 Cen   Caminhão/Ônibus         50 FALSE  S             16000
 7 Esq   Passeio                 55 FALSE  N              1300
 8 Esq   SUV/Pickup              64 TRUE   N              2000
 9 Esq   Passeio                 53 FALSE  N              1300
10 Cen   Passeio                 58 FALSE  N              1300
# ℹ 490 more rows

Uma sintaxe alternativa e mais legível é usar a função join_by(), disponível a partir do dplyr 1.1.0. Ela recebe uma expressão de igualdade entre os nomes das colunas:

vel |>
  left_join(pesos2, by = join_by(tipo_veic == veiculo))
# A tibble: 500 × 6
   faixa tipo_veic       velocidade acim60 veic_pesado peso_kg
   <chr> <chr>                <dbl> <lgl>  <chr>         <dbl>
 1 Cen   Passeio                 54 FALSE  N              1300
 2 Cen   Caminhão/Ônibus         50 FALSE  S             16000
 3 Cen   Passeio                 56 FALSE  N              1300
 4 Dir   Moto                    43 FALSE  N               120
 5 Esq   SUV/Pickup              57 FALSE  N              2000
 6 Cen   Caminhão/Ônibus         50 FALSE  S             16000
 7 Esq   Passeio                 55 FALSE  N              1300
 8 Esq   SUV/Pickup              64 TRUE   N              2000
 9 Esq   Passeio                 53 FALSE  N              1300
10 Cen   Passeio                 58 FALSE  N              1300
# ℹ 490 more rows

O resultado é idêntico. A vantagem do join_by() é que os nomes das colunas são tratados como variáveis, sem necessidade de aspas, tornando o código mais próximo da sintaxe usada no restante do dplyr.

3.8 Fazendo tudo de uma só vez

Nas seções anteriores, cada operação foi apresentada de forma independente. Em uma análise real, essas operações quase sempre aparecem encadeadas, e é exatamente para isso que o operador pipe (|>) foi projetado.

Partindo do resultado do left_join() entre vel e pesos, queremos executar quatro operações em sequência. Primeiro, criaremos uma nova variável convertendo o peso de quilogramas para toneladas, o que torna os valores mais legíveis. Depois, filtraremos os registros para manter apenas veículos leves, excluindo caminhões e ônibus. Em seguida, selecionaremos apenas as colunas relevantes para a análise final. Por último, ordenaremos os registros pela velocidade em ordem decrescente.

Sem o pipe, seria necessário criar um objeto intermediário a cada passo:

vel_com_peso <- left_join(vel, pesos, by = "tipo_veic")
vel_com_peso <- mutate(vel_com_peso, peso_ton = peso_kg / 1000)
vel_com_peso <- filter(vel_com_peso, tipo_veic != "Caminhão/Ônibus")
vel_com_peso <- select(vel_com_peso, tipo_veic, velocidade, peso_ton)
vel_com_peso <- arrange(vel_com_peso, desc(velocidade))
vel_com_peso

O resultado é correto, mas o código é repetitivo, com o nome vel_com_peso aparecendo dez vezes. Cada linha reatribui o objeto ao mesmo nome, e qualquer erro em uma etapa intermediária exige depurar toda a cadeia manualmente. Com o pipe, o mesmo resultado é obtido de forma muito mais clara:

vel |>
  left_join(pesos, by = "tipo_veic") |>
  mutate(peso_ton = peso_kg / 1000) |>
  filter(tipo_veic != "Caminhão/Ônibus") |>
  select(tipo_veic, velocidade, peso_ton) |>
  arrange(desc(velocidade))
# A tibble: 424 × 3
   tipo_veic  velocidade peso_ton
   <chr>           <dbl>    <dbl>
 1 SUV/Pickup         66      2  
 2 Passeio            65      1.3
 3 Passeio            65      1.3
 4 Passeio            65      1.3
 5 SUV/Pickup         64      2  
 6 Passeio            64      1.3
 7 Passeio            64      1.3
 8 Passeio            63      1.3
 9 Passeio            63      1.3
10 Passeio            63      1.3
# ℹ 414 more rows

O raciocínio é linear: começamos com vel, adicionamos o peso com left_join(), convertemos para toneladas com mutate(), excluímos os veículos pesados com filter(), mantemos apenas as colunas de interesse com select() e ordenamos os registros por velocidade decrescente com arrange(). Cada linha descreve um passo da análise, e a leitura de cima para baixo espelha a sequência lógica do raciocínio.

Para armazenar o resultado em um novo objeto, basta atribuir a cadeia completa:

vel_leve <- vel |>
  left_join(pesos, by = "tipo_veic") |>
  mutate(peso_ton = peso_kg / 1000) |>
  filter(tipo_veic != "Caminhão/Ônibus") |>
  select(tipo_veic, velocidade, peso_ton) |>
  arrange(desc(velocidade))

3.9 Salvando Arquivos em Formato .rds

O formato .rds é específico do R e permite salvar um único objeto com todos os seus metadados preservados. Isso inclui os tipos das variáveis e a ordem dos níveis de factors, características que se perderiam ao salvar em formatos como .csv ou .xlsx.

Por exemplo, ao longo deste capítulo transformamos tipo_veic em um factor com os níveis na ordem “Moto”, “Passeio”, “SUV/Pickup” e “Caminhão/Ônibus”. Se salvarmos vel como .csv e depois lermos novamente o arquivo, essa ordem será perdida e tipo_veic voltará a ser uma variável character.

Para salvar vel no formato .rds:

saveRDS(vel, "vel.rds")

Para carregar o arquivo em uma sessão futura:

vel <- readRDS("vel.rds")

Ao carregar com readRDS(), o objeto retorna exatamente no estado em que foi salvo, com todos os tipos e ordenamentos preservados. Isso torna o .rds especialmente útil para armazenar bases de trabalho intermediárias, evitando reprocessar as transformações a cada sessão.

3.10 Dados em formato long e wide

Uma mesma informação pode ser organizada de maneiras diferentes em uma tabela, e a escolha entre elas determina quais operações ficam fáceis e quais ficam trabalhosas. As duas organizações mais comuns são o formato wide (largo) e o formato long (longo). No formato wide, cada unidade de análise ocupa uma única linha e as demais colunas guardam suas medições. No formato long, cada linha corresponde a uma combinação entre a unidade de análise e uma medição, de modo que a tabela fica mais alta e mais estreita.

Para tornar a diferença concreta, vamos criar uma pequena tabela fictícia com o número de viagens diárias por modo de transporte em três municípios.

viagens <- tibble(
  municipio = c("mun_a", "mun_b", "mun_c"),
  a_pe      = c(120,  90,  60),
  bicicleta = c( 30,  45,  20),
  carro     = c(200, 150, 260),
  tp        = c(150, 110,  80)
)
viagens
# A tibble: 3 × 5
  municipio  a_pe bicicleta carro    tp
  <chr>     <dbl>     <dbl> <dbl> <dbl>
1 mun_a       120        30   200   150
2 mun_b        90        45   150   110
3 mun_c        60        20   260    80

Essa é a apresentação wide, e ela é confortável para leitura humana. Relatórios impressos e planilhas de divulgação quase sempre chegam nesse formato, porque a comparação entre municípios se faz percorrendo a coluna e a comparação entre modos se faz percorrendo a linha. Do ponto de vista da manipulação, porém, há um problema. Os nomes a_pe, bicicleta, carro e tp não são variáveis, são valores de uma variável que poderíamos chamar de modo de transporte. O número de viagens, única medição de fato registrada, está espalhado por quatro colunas.

Foi essa distinção que motivou o princípio tidy2 mencionado no início do capítulo, segundo o qual cada variável deve ocupar uma coluna e cada observação deve ocupar uma linha. A tabela acima viola esse princípio, e a consequência prática é que operações simples ficam trabalhosas. Calcular o total de viagens por município exige somar quatro colunas nomeadas uma a uma, e a inclusão de um quinto modo na pesquisa obrigaria a reescrever esse código.

A função pivot_longer(), do pacote tidyr, reorganiza a tabela para o formato long. Geralmente, são necesários informar 3 aspectos:

  • Quais colunas devem ser empilhadas (cols).
  • Qual será o nome da nova coluna que receberá os nomes antigos (names_to), que corresponderão aos níveis dessa nova variável.
  • Qual será o nome da nova coluna que receberá os valores (values_to) que existiam na tabela original.

Observe no exemplo para os nossos dados abaixo:

viagens_long <- viagens |>
  pivot_longer(
    cols      = c(a_pe, bicicleta, carro), # poderia ser somente cols = -municipio
    names_to  = "modo_viagem",
    values_to = "viagens"
  )
viagens_long
# A tibble: 9 × 4
  municipio    tp modo_viagem viagens
  <chr>     <dbl> <chr>         <dbl>
1 mun_a       150 a_pe            120
2 mun_a       150 bicicleta        30
3 mun_a       150 carro           200
4 mun_b       110 a_pe             90
5 mun_b       110 bicicleta        45
6 mun_b       110 carro           150
7 mun_c        80 a_pe             60
8 mun_c        80 bicicleta        20
9 mun_c        80 carro           260

As três linhas com quatro colunas de medição viraram doze linhas. Nenhuma informação foi criada ou perdida, apenas redistribuída. Agora existe uma coluna modo_viagem que guarda a categoria e uma coluna viagens que guarda a medição, e ambas podem ser usadas diretamente pelas funções estudadas nas seções anteriores.

Veja que o ganho aparece de imediato. O total de viagens por modo, que na tabela wide exigiria somar colunas manualmente, sai de um group_by() com summarize().

viagens_long |>
  group_by(modo_viagem) |>
  summarize(total = sum(viagens))
# A tibble: 3 × 2
  modo_viagem total
  <chr>       <dbl>
1 a_pe          270
2 bicicleta      95
3 carro         610

Da mesma forma, a repartição modal de cada município sai de um group_by() com mutate(), exatamente como fizemos na seção de sumarização.

viagens_long |>
  group_by(municipio) |>
  mutate(prop = viagens / sum(viagens))
# A tibble: 9 × 5
# Groups:   municipio [3]
  municipio    tp modo_viagem viagens   prop
  <chr>     <dbl> <chr>         <dbl>  <dbl>
1 mun_a       150 a_pe            120 0.343 
2 mun_a       150 bicicleta        30 0.0857
3 mun_a       150 carro           200 0.571 
4 mun_b       110 a_pe             90 0.316 
5 mun_b       110 bicicleta        45 0.158 
6 mun_b       110 carro           150 0.526 
7 mun_c        80 a_pe             60 0.176 
8 mun_c        80 bicicleta        20 0.0588
9 mun_c        80 carro           260 0.765 

Nenhum desses dois códigos menciona os nomes dos modos. Se a pesquisa passasse a registrar viagens de motocicleta, bastaria acrescentar as linhas correspondentes e ambos continuariam funcionando sem qualquer alteração. Essa independência em relação aos valores das categorias é a principal razão pela qual o formato long domina o trabalho de análise no R. É também o formato esperado pelo ggplot2 para a elaboração de gráficos, o que veremos em detalhe no próximo capítulo.

A operação inversa é feita por pivot_wider(), que espalha uma coluna de categorias por várias colunas novas. Os argumentos centrais são names_from, que indica de qual coluna sairão os nomes das novas colunas, e values_from, que indica de qual coluna sairão os valores que as preencherão. Aplicada sobre viagens_long, a função devolve exatamente a tabela original.

viagens_long |>
  pivot_wider(
    names_from  = modo_viagem,
    values_from = viagens
  )
# A tibble: 3 × 5
  municipio    tp  a_pe bicicleta carro
  <chr>     <dbl> <dbl>     <dbl> <dbl>
1 mun_a       150   120        30   200
2 mun_b       110    90        45   150
3 mun_c        80    60        20   260
Dica

Nem toda tabela em formato long contém todas as combinações possíveis. Quando uma combinação está ausente, o pivot_wider() preenche a célula correspondente com NA. Se o valor ausente significar zero, como costuma ocorrer em contagens, o argumento values_fill = 0 resolve a questão em uma linha.

A alternância entre os dois formatos reaparecerá com frequência na segunda parte do livro. As tabelas do Censo Demográfico costumam ser divulgadas em formato wide, com uma coluna para cada faixa etária, cada classe de rendimento ou cada tipo de domicílio, e a primeira providência ao analisá-las quase sempre é empilhá-las com pivot_longer().

3.11 Exercícios

Antes de iniciar os exercícios, baixe os arquivos abaixo para uma pasta onde você irá processá-los. Crie um projeto do R dentro dessa pasta e um novo script .R dentro desse projeto para responder às questões abaixo.

  1. Leia o arquivo velocidades.xlsx como um tibble e armazene-o em um objeto denominado vel e converta as variáveis faixa e tipo_veic para factor. Em seguida, obtenha as quantidades e proporções para cada combinação de níveis de faixa e tipo_veic. Na sequência filtre todas as observações com proporção acima de 0.1 e ordene-as de forma decrescente de acordo com a frequência absoluta de observações.

  2. Crie uma nova variável na base vel chamada cat_vel que classifique as velocidades em três níveis usando case_when():

    • "Lenta": velocidade menor que 40 km/h
    • "Normal": velocidade entre 40 e 60 km/h (incluindo esses limites)
    • "Alta": velocidade acima de 60 km/h
  3. Leia o arquivo escolha_modal.csv como um tibble (com a codificação de caracteres correta) e armazene-o em um objeto denominado em. Posteriormente, crie um dataframe com valores médios de emissão de CO2 por km rodado para cada um dos modos presentes na coluna modo_viagem (A pé = 0 g/km, Bicicleta = 0 g/km, Carro ou App = 120 g/km e TP = 50 g/km). Na sequência faça um left_join de em com essa nova tabela para incorporar os valores de emissão a em.

  4. Salve os objetos vel e em após todas as operações realizadas em formato .rds para entrega.


  1. Não confunda com a função summary(). Também pode ser escrita com “s”, ou seja, summarise(), em aderência ao inglês britânico.↩︎

  2. também presente no tidyverse↩︎