Apêndice: manipular para enxergar

Este post revisa as principais funções para manipulação de dados do Tidyverse. O material serve mais como referência e apoio ao tutorial de ggplot2 do que como introdução ao assunto.
data-visualization
ggplot2
tutorial-R
Author

Vinicius Oike

Published

July 7, 2023

Introdução

Este tutorial, ao contrário da série “ggplot2: do básico ao intermediário” já assume que se tenha um entendimento razoável de R. O material aqui serve mais para consultar/relembrar ou aprender um truque novo. Da maneira como está escrito, não é adequado para uma primeira leitura. Grosso modo, a referência aqui é Hadley (2017) capítulos 4, 5, 10-12.

Limpeza de dados

O primeiro passo para montar uma visualização é ter os dados no formato certo. Em geral, isto envolve três etapas: (1) importar os dados no R; (2) limpar os dados; e (3) transformar os dados no formato apropriado. Neste tutorial vamos focar sobretudo nas últimas duas etapas.

Dados tabulares são armazenados dentro de objetos chamados data.frame. Todo gráfico de ggplot começa com um (ou mais) data.frame. Apesar de ser possível montar gráficos a partir de vetores de dados dispersos, recomendo fortemente que sempre se utilize dados dentro de data.frame. Isto garante um código mais organizado e menos propenso a erros.

Apesar de funcionar como um repositório de pacotes, o R já vem com diversas funções “de fábrica” que permitem a importação e manipulação de dados. Estas funções que já vem carregadas no R são chamadas de funções “base” ou “base-R”. Alguns pacotes foram criados para melhorar estas funções “base”.

Você tem um momento para falar sobre o tidyverse?

O tidyverse é um metapacote, ele instala vários pacotes simultaneamente quando instalado e carrega múltiplos pacotes quando chamado. Estes pacotes formam uma família de pacotes que são unidos por uma filosofia e um objetivos comuns: todos os pacotes têm como objetivo a limpeza de dados e, de maneira geral, seguem o princípio de “tidy” data.

# Instala o pacote tidyverse
install.packages("tidyverse")
# Carrega o pacote tidyverse
library("tidyverse")

Todos os pacotes e funções que vamos utilizar serão carregados nesta única linha de código.

  • readr - importação e exportação de dados.
  • dplyr - manipulação de dados.
  • tidyr - manipulação de dados.
  • stringr - manipulacao de strings (character)
  • forcats - manipulacao de factors
  • lubridate - manipulação de datas (Date)

O tidyverse inclui ainda mais pacotes, como rvest para webscrapping, ou dbplyr que permite utilizar comandos do dplyr em databases.

A filosofia do tidyverse

Numa primeira leitura, esta seção pode ser pulada sem grandes prejuízos; contudo, ela pode ser interessante numa segunda leitura ou para leitores que já tem alguma familiaridade com tidyverse.

A filosofia geral do tidyverse toma muito emprestado da gramática. As funções têm nomes de verbos que costumam ser intuitivos e são encadeados via “pipes”1 que funcionam como conectivos numa frase. Em tese, isto torna o código mais legível e até mais didático. A tarefa de renomear colunas, criar variáveis e calcular uma média nos grupos torna-se “linear” no mesmo sentido em que uma frase com sujeito-verbo-objeto é linear.

O pipe, essencialmente, carrega o resultado de uma função adiante numa cadeia de comandos: objeto |> função1 |> função2 |> função3. Isto tem duas vantagens: primeiro, evita que você use funções compostas que são lidas “de dentro para fora” como exp(mean(log(x))); e, segundo, dispensa a criação de objetos intermediários “inúteis” que estão ali somente para segurar um valor que não vai ser utilizado mais adiante.

model <- lm(log(AirPassengers) ~ time(AirPassengers))

#> Função composta
mean(exp(fitted(model)))
#> Usando pipes
model |> fitted() |> exp() |> mean()
#> Usando objetos intermediários
x1 <- fitted(model)
x2 <- exp(x1)
x3 <- mean(x2)

Há um tempo atrás argumentava-se contra o uso de “pipes”, pois estes dificultavam a tarefa de encontrar bugs no código. Isto continua sendo parcialmente verdade, mas as funções do tidyvserse atualmente têm mensagens de erro bastante ricas e permitem encontrar a fonte do erro com relativa facilidade. Ainda assim, não se recomenda encadear funções em excesso, i.e., pipes com 10 funções ou mais.

Outra filosofia do tidyverse é de que tarefas rotineiras devem ser transformadas em funções específicas. Neste sentido, os pacotes dplyr, tidyr e afins são recheados de funções, às vezes com nomes muito semelhantes e com usos redundantes. As funções starts_with e ends_with, por exemplo, são casos específicos da função matches. Há funções que permitem até duas formas de grafia como summarise e summarize. Outras como slice_min e slice_max são convenientes mas são literalmente: arrange + slice.

Somando somente os dois principais pacotes, dplyr e tidyr, há 360 funções disponíveis. Contraste isto com o data.table que permite fazer 95% das transformações de dados somente com dt[i, j, by = c(), .SDcols = cols].

Mesmo as funções base do R costumam ser mais sucintas do que códigos em tidyverse. No exemplo abaixo, a função tapply consegue o mesmo resultado que o código mais extenso feito com dplyr.

tapply(mtcars$mpg, mtcars$cyl, mean)

mtcars |> 
  group_by(cyl) |> 
  summarise(avg = mean(cyl))

As vantagens do tidyverse se tornam mais evidentes com o tempo. De fato, o pacote permite abstrações muito poderosas, e eventualmente, pode-se fazer um código centenas de vezes mais sucinto combinando as suas funções.

O lado negativo disto tudo é que para não-falantes de inglês muitas destas “vantagens gramaticais” são despercebidas2; e o resultado é somente um código “verborrágico”, cheio de funções. Atualmente, parece haver um consenso crescente de que a melhor forma de começar a aprender R é começando pelo tidyverse; esta visão não é livre de críticos como de Norm Matloff, professor de estatística da UC Davis.

Um fato particularmente irritante do tidyverse é a frequência com que os pacotes mudam. Na maior parte das vezes, as mudanças são positivas, mas isto faz com que o código escrito em tidyverse não seja sustentável ao longo do tempo.

Eu demorei um bom tempo para entender as funções tidyr::gather e tidyr::spread e, atualmente, ambas foram descontinuadas e substituídas pelas funções pivot_longer e pivot_wider. As funções mutate_if, mutate_at e similares do dplyr foram todas suprimidas pela sinataxe mais geral do across. A função tidyr::separate agora está sendo substituída por separate_wider_position e separate_wider_delim.

Mesmo um código bem escrito há poucos anos atrás tem grandes chances de não funcionar mais porque as funções foram alteradas ou descontinuadas. Em 2021, Hadley Wickham, a principal mente por trás do tidyverse, discutiu este problema abertamente numa palestra. Desde então, o tidyverse tem melhorado a sua política de manutenção de funções.

Tabelas

O objeto central da análise de dados é o data.frame. Um data.frame é uma tabela bidimensional que contém informações: em geral, cada coluna é uma variável diferente e cada linha é uma observação. Este objeto possui propriedades bastante simples:

  1. Comprimento fixo. O número de linhas de um data.frame é fixo, assim todas as colunas têm o mesmo comprimento.
  2. Homogeneidade. Cada coluna de um data.frame é homogênea, isto é, contém um dado de um único tipo. Assim, uma mesma coluna não pode misturar um string e um número, um factor e um string, etc.
  3. Nomes. Cada coluna tem um nome (único e idiomático). Este nome é utilizado para fazer refrência a esta coluna.

Estas três características garantem a funcionalidade e consistência de um data.frame. O comprimento fixo e a homogeneidade, em particular, tornam este tipo de objeto muito conveniente e previsível.

Construindo tabelas

Para construir um data.frame basta chamar a função homônima e declarar as suas colunas seguindo as três propriedades acima. Nos exemplos abaixo, ao invés da função data.frame vou utilizar a função tibble que é, essencialmente, equivalente, mas que possui algumas pequenas vantagens. No restante do texto as palavras tibble e data.frame serão utilizadas como sinônimas.

No primeiro exemplo crio uma tabela com três linhas e duas colunas.

dados <- tibble(
  cidade = c("Porto Alegre", "São Paulo", "Salvador"),
  pop22 = c(1.332, 11.451, 2.418)
)

Para visualizar o resultado basta chamar o objeto por nome ou usar a função print. Uma das vantanges do tibble é de mostrar a classe de cada coluna, onde chr indica character (caractere), isto é, um string e dbl indica double, isto é, um número3.

dados
# A tibble: 3 × 2
  cidade       pop22
  <chr>        <dbl>
1 Porto Alegre  1.33
2 São Paulo    11.5 
3 Salvador      2.42

Pode-se também criar a tabela a partir de vetores/objetos previamente declarados.

cidades <- c("Porto Alegre", "São Paulo", "Salvador")
populacao <- c(1.332, 11.451, 2.418)

dados <- tibble(
  nome_cidade = cidades,
  pop22 = populacao
)

Quando alguma das colunas não tiver o mesmo comprimento das demais, o R vai tentar “reciclar” os valores desta coluna. Em geral, isto vai causar um erro, mas em alguns casos pode funcionar. No caso abaixo o valor "Brasil" (de comprimento unitário) é repetido três vezes para “caber” dentro da tabela.

dados <- tibble(
  cidade = c("Porto Alegre", "São Paulo", "Salvador"),
  pop22 = c(1.332, 11.451, 2.418),
  pais = "Brasil"
)

dados
# A tibble: 3 × 3
  cidade       pop22 pais  
  <chr>        <dbl> <chr> 
1 Porto Alegre  1.33 Brasil
2 São Paulo    11.5  Brasil
3 Salvador      2.42 Brasil

Propriedades de tabelas

Toda coluna de um data.frame possui nomes. Para acessar os nomes usa-se names.

names(dados)
#> [1] "cidade" "pop22"  "pais"

Os nomes das colunas sempre devem ser únicos. Aqui, há uma pequena vantagem em utilizar o tibble. Mesmo no caso em que se tenta criar uma tabela com nomes idênticos, a função data.frame evita que isto acontece, mas emite nenhum tipo de alerta sobre o que está acontecendo.

tab <- data.frame(
  a = c(1, 2, 3),
  a = c("a", "b", "c")
)

tab
  a a.1
1 1   a
2 2   b
3 3   c

A função tibble é um pouco mais exigente e retorna um erro neste caso.

tab <- tibble(
  a = c(1, 2, 3),
  a = c("a", "b", "c")
)
Error in `tibble()`:
! Column name `a` must not be duplicated.
Use `.name_repair` to specify repair.
Caused by error in `repaired_names()`:
! Names must be unique.
✖ These names are duplicated:
  * "a" at locations 1 and 2.

Para extrair uma coluna de um data.frame temos duas opções. A mais simples e direta é utilizar o operador $ e chamar o nome da coluna como se fosse um objeto. A segunda opção é utilizar [[ e chamar o nome da coluna como um string4.

#> Extraindo uma coluna 

dados$cidade
#> [1] "Porto Alegre" "São Paulo"    "Salvador"

dados[["cidade"]]
#> [1] "Porto Alegre" "São Paulo"    "Salvador"

Importando tabelas

Raramente vamos declarar todas as observações de uma tabela. Na prática, é muito mais comum importar uma tabela de alguma fonte externa como de uma planilha de Excel ou de um arquivo csv. Para cada tipo de arquivo existe uma função read_* diferente. Importar dados costuma ser uma tarefa frustrante por três motivos:

  1. Há muitos arquivos para se importar.
  2. É difícil fazer o R encontrar o arquivo.
  3. Os arquivos têm problemas (valores corrompidos, linhas vazias, etc.)

Os dois primeiros problemas são simples de se resolver. Pode-se importar múltiplos arquivos ao mesmo tempo usando um loop; importar todos os arquivos dentro de uma mesma pasta é trivial, desde que os arquivos sigam o mesmo padrão.

Garantir que o R consiga encontrar os arquivos também é simples. Idealmente, todos os arquivos externos devem estar organizados dentro de uma pasta chamada dados ou data e deve-se chamar estes dados usando funções read_*. Uma boa prática é sempre usar “caminhos relativos” ao invés de caminhos absolutos.

#> Ruim
dat <- read_csv("/Users/viniciusoike/Documents/GitHub/projeto/data/income.csv")
#> Bom 
dat <- read_csv("data/income.csv")
#> Ainda melhor
dat <- read_csv(here::here("data/income.csv"))

O terceiro problema é muito mais complexo e vai exigir mais conhecimento e prática. Em geral, resolve-se a maior parte dos problemas usando algum dos argumentos dentro da função read_* como:

  • skip: Pula as primeiras k linhas.

  • na: Define quais valores devem ser interpretados como valores ausentes.

  • col_types: Permite que se declare explicitamente qual o tipo de dado (numérico, data, texto) que está armazenado em cada coluna.

  • col_names ou name_repair: O primeiro permite que se declare explicitamente o nome que cada coluna vai ter dentro do R enquanto o segundo permite que se use uma função que renomeia as colunas.

  • locale: Permite selecionar diferentes tipos de padrão de local. Em geral, usa-se locale = locale("pt_BR").

  • range: Este argumento só vale no caso de planilhas de Excel e permite que se importe uma seleção específica da planilha (e.g. “D4:H115”)

O código abaixo mostra um exemplo particularmente tenebroso. O título da segunda coluna inclui símbolos como $ e /; as datas estão em português com o mês escrito por extenso e em formato dia-mês-ano; os números usam a vírgula (ao invés do ponto) para separar o decimal e o valor ausente é sinalizado com “X”.

#> Input de um csv sujo
dados <-
'Data; Valor (R$/m2)
"01-maio-2020";22,3
"01-junho-2020";21,5
"06-julho-2021";X
"07-novembro-2022";22'

#> Lendo o arquivo
df <- read_delim(
  #> Substitui esta linha pelo 'path' até o csv
  I(dados),
  delim = ";",
  #> Usa , como separador decimal; lê meses em português (e.g. maio, junho, etc.)
  locale = locale(decimal_mark = ",", date_names = "pt", date_format = "%d-%B-%Y"),
  #> Interpreta X como valores ausentes (NA)
  na = "x",
  #> Renomeia as colunas
  name_repair = janitor::clean_names
  )

Manipulando dados

O pacote dplyr é uma das ferramentas mais populares e úteis para manipulação de dados no R. Ele fornece uma série de funções simples e poderosas para filtrar, agrupar, modificar e resumir dados. Neste tutorial, vamos explorar algumas dessas funções e ver como elas podem ser usadas para realizar tarefas comuns de manipulação de dados.

Agora, vamos ver algumas das funções mais úteis do dplyr:

Nome da Função Tradução O que faz
rename Renomear Modifica o nome das colunas.
select Selecionar Seleciona as colunas.
filter Filtrar Filtra/seleciona as linhas segundo alguma condição.
arrange Arranjar/ordenar Ordena as linhas (crescente/decrescente) segundo alguma variável.
mutate Mutar/transformar Cria uma nova coluna a partir de outras colunas ou dados.
summarise Sumarizar/resumir Aplica alguma função sobre as linhas. Cria uma tabela “resumo”.
group by Agrupar Agrupa as linhas segundo alguma variável.

As funções rename, mutate, select e filter são utilizadas para preparar e limpar os dados, enquanto as funções group_by e summarise são utilizadas para transformar/resumir os dados. Estas são as seis principais funções do pacote e veremos cada uma delas em maior detalhes.

Para praticar as funções vamos utilizar uma tabela que traz informações sobre as cidades do Brasil.

tbl <- read_csv("...")

rename

Para renomear as colunas de data.frame usa-se a função rename (renomear) com rename(tbl, novo_nome = velho_nome). Vale lembrar que para checar os nomes da tabela usa-se names(tbl).

#> Renomear colunas
tbl_renamed <- rename(tbl, codigo_municipio = code_muni, pop = population)

Os nomes das colunas de um data.frame devem

  1. Ser únicos (não-duplicados) e evitar caracteres maiúsculos.
  2. Não devem incluir caracteres especiais (e.g. !*&@%), nem começar com um número ou caractere especial.
  3. Evitar espaços em branco, que devem ser substituídos por _ ou omitidos (e.g. PIB Agro deve ser reescrito como pibAgro ou pib_agro.

Também é possível renomear colunas com auxílio de um vetor ou lista e usando ou all_of (todos) ou any_of (algum/alguns). O exemplo abaixo mostra a lógica geral: temos um vetor que indica o novo nome e o antigo nome de cada coluna que se quer trocar. Caso se queira trocar exatamente todos os nomes indicados no vetor usa-se all_of (mais rigoroso), caso se queira trocar todos os nomes indicados no vetor, ignorando os casos que não batem com nomes de colunas existentes, usa-se any_of.

new_names <- c(
  "codigo_municipio" = "code_muni",
  "pop" = "population",
  "pop_rate" = "population_growth_rate"
  )

tbl_renamed <- rename(tbl, all_of(new_names))

No exemplo abaixo incluo uma “nova coluna” chamada unit que desejo renomear para unidade. Usando any_of o retorno é exatamente igual ao caso acima, pois a função ignora a coluna inexistente unit.

new_names <- c(
  "codigo_municipio" = "code_muni",
  "pop" = "population",
  "pop_rate" = "population_growth_rate",
  "unidade" = "unit"
)

tbl_renamed <- rename(tbl, any_of(new_names))

Já a função all_of é mais rigorosa e retorna um erro indicando que a coluna unit não existe.

tbl_renamed <- rename(tbl, all_of(new_names))
Error in `all_of()`:
! Can't rename columns that don't exist.
✖ Column `unit` doesn't exist.

O uso de um vetor externo para renomear colunas é conveniente não somente porque permite melhor organizar o código; na prática, este vetor externo funciona como um dicionário de variáveis que pode ser inclusive utilizado para tratar várias bases de dados ou mesmo em outros códigos. Além disso, tratar o nome das colunas como strings é útil pois nos permite transformar este dado mais facilmente.

Por fim, pode-se aplicar uma função para renomear as colunas usando rename_with.

tbl_renamed <- rename_with(tbl, toupper)
names(tbl_renamed)
 [1] "CODE_MUNI"              "NAME_MUNI"              "CODE_STATE"            
 [4] "NAME_STATE"             "ABBREV_STATE"           "CODE_REGION"           
 [7] "NAME_REGION"            "POPULATION"             "POPULATION_GROWTH"     
[10] "POPULATION_GROWTH_RATE" "CITY_AREA"              "POPULATION_DENSITY"    
[13] "HOUSEHOLDS"             "DWELLERS_PER_HOUSEHOLD" "PIB"                   
[16] "PIB_SHARE_UF"           "PIB_TAXES"              "PIB_ADDED_VALUE"       
[19] "PIB_AGRICULTURE"        "PIB_INDUSTRIAL"         "PIB_SERVICES"          
[22] "PIB_GOVMT_SERVICES"    

Uma dica final para rapidamente renomear colunas é a função janitor::clean_names que obedece aos três princípios elencados acima. Ela pode ser utilizada diretamente num data.frame como se vê no exemplo abaixo.

test_df <- as.data.frame(matrix(ncol = 6))
names(test_df) <- c("firstName", "ábc@!*", "% successful (2009)",
                    "REPEAT VALUE", "REPEAT VALUE", "")
janitor::clean_names(test_df)
#>   first_name abc percent_successful_2009 repeat_value repeat_value_2  x
#> 1         NA  NA                      NA           NA             NA NA

select

A função select serve para selecionar colunas num data.frame, permitindo-se trabalhar com uma versão menor dos dados. Similarmente à função rename é possível selecionar colunas diretamente select(tbl, coluna_1, coluna_2, …) ou selecionando os nomes via um vetor de strings com auxílio de any_of of all_of.

#> Seleciona diretamente as colunas
sel_tbl <- select(tbl, code_muni, pib, pib_agriculture)

#> Cria um vetor de nomes
colunas <- c("code_muni", "pib", "pib_agriculture")
#> Seleciona as colunas baseado no vetor
sel_tbl <- select(tbl, all_of(colunas))

sel_tbl
# A tibble: 5,570 × 3
   code_muni     pib pib_agriculture
       <dbl>   <dbl>           <dbl>
 1   1100015  570272          203394
 2   1100023 2818049          199723
 3   1100031  167190           81177
 4   1100049 2519353          236215
 5   1100056  600670           94758
 6   1100064  366931           88923
 7   1100072  268381          155648
 8   1100080  261978           80684
 9   1100098  666331          137802
10   1100106  984586           59384
# ℹ 5,560 more rows

Para remover uma coluna, basta usar o sinal de menos na frente do nome.

#> Seleciona diretamente as colunas
sel_tbl <- select(tbl, -pib, -city_area, -population_density)

#> Cria um vetor de nomes
colunas <- c("pib", "city_area", "population_density")
#> Seleciona as colunas baseado no vetor
sel_tbl <- select(tbl, -all_of(colunas))

sel_tbl
# A tibble: 5,570 × 19
   code_muni name_muni             code_state name_state abbrev_state
       <dbl> <chr>                      <dbl> <chr>      <chr>       
 1   1100015 Alta Floresta D'Oeste         11 Rondônia   RO          
 2   1100023 Ariquemes                     11 Rondônia   RO          
 3   1100031 Cabixi                        11 Rondônia   RO          
 4   1100049 Cacoal                        11 Rondônia   RO          
 5   1100056 Cerejeiras                    11 Rondônia   RO          
 6   1100064 Colorado do Oeste             11 Rondônia   RO          
 7   1100072 Corumbiara                    11 Rondônia   RO          
 8   1100080 Costa Marques                 11 Rondônia   RO          
 9   1100098 Espigão D'Oeste               11 Rondônia   RO          
10   1100106 Guajará-Mirim                 11 Rondônia   RO          
   code_region name_region population population_growth population_growth_rate
         <dbl> <chr>            <dbl>             <dbl>                  <dbl>
 1           1 Norte            21495             -2897                  -1.05
 2           1 Norte            96833              6480                   0.58
 3           1 Norte             5363              -950                  -1.35
 4           1 Norte            86895              8321                   0.84
 5           1 Norte            15890             -1139                  -0.58
 6           1 Norte            15663             -2928                  -1.42
 7           1 Norte             7519             -1264                  -1.29
 8           1 Norte            12627             -1051                  -0.66
 9           1 Norte            29397               668                   0.19
10           1 Norte            39386             -2270                  -0.47
   households dwellers_per_household pib_share_uf pib_taxes pib_added_value
        <dbl>                  <dbl>        <dbl>     <dbl>           <dbl>
 1       7695                   2.79         1.11     35109          535163
 2      34768                   2.77         5.46    295656         2522393
 3       1967                   2.73         0.32      7237          159953
 4      31919                   2.71         4.88    274451         2244902
 5       5873                   2.69         1.16     89923          510747
 6       5991                   2.61         0.71     24075          342856
 7       2840                   2.64         0.52     10200          258181
 8       4161                   3.01         0.51      9276          252702
 9      10463                   2.8          1.29     58285          608046
10      11803                   3.3          1.91    139461          845125
   pib_agriculture pib_industrial pib_services pib_govmt_services
             <dbl>          <dbl>        <dbl>              <dbl>
 1          203394          20716       150192             160860
 2          199723         404752      1207405             710513
 3           81177           5438        28667              44671
 4          236215         275537      1157344             575806
 5           94758          23582       276755             115652
 6           88923          24322       118529             111082
 7          155648          10847        34749              56937
 8           80684           6205        48318             117495
 9          137802          54521       213513             202211
10           59384          41650       443005             301086
# ℹ 5,560 more rows

Pode-se também selecionar várias colunas ao mesmo tempo se elas estiverem em sequência (uma ao lado da outra). O código abaixo, por exemplo, seleciona a coluna code_muni e todas as colunas entre pib e pib_added_value (inclusive).

sel_tbl <- select(tbl, code_muni, pib:pib_added_value)

sel_tbl
# A tibble: 5,570 × 5
   code_muni     pib pib_share_uf pib_taxes pib_added_value
       <dbl>   <dbl>        <dbl>     <dbl>           <dbl>
 1   1100015  570272         1.11     35109          535163
 2   1100023 2818049         5.46    295656         2522393
 3   1100031  167190         0.32      7237          159953
 4   1100049 2519353         4.88    274451         2244902
 5   1100056  600670         1.16     89923          510747
 6   1100064  366931         0.71     24075          342856
 7   1100072  268381         0.52     10200          258181
 8   1100080  261978         0.51      9276          252702
 9   1100098  666331         1.29     58285          608046
10   1100106  984586         1.91    139461          845125
# ℹ 5,560 more rows

Também é possível renomear e selecionar ao mesmo tempo.

sel_tbl <- select(tbl, codigo_municipio = code_muni, pib)

sel_tbl
# A tibble: 5,570 × 2
   codigo_municipio     pib
              <dbl>   <dbl>
 1          1100015  570272
 2          1100023 2818049
 3          1100031  167190
 4          1100049 2519353
 5          1100056  600670
 6          1100064  366931
 7          1100072  268381
 8          1100080  261978
 9          1100098  666331
10          1100106  984586
# ℹ 5,560 more rows

Por fim, existem algumas funções auxiliares que facilitam a seleção de múltiplas colunas. Vou apresentar apenas três destas funções:

  • starts_with/ends_with - selecionam colunas que começam ou terminam com determindo string.

  • where - seleciona colunas de uma determinada classe (numeric, factor, etc.)

  • matches - seleciona colunas com base num match, usando regex. Esta função é mais geral e engloba as duas primeiras.

O código abaixo mostra alguns exemplos simples. Vou omitir as saídas do código, max experimente reproduzir o resultado.

#> Seleciona code_muni mais as colunas que começam com 'pib'
select(tbl, code_muni, starts_with("pib"))
#> Seleciona as colunas que terminam com 'muni'
select(tbl, ends_with("muni"))
#> Seleciona code_muni mais as colunas que contêm números
select(tbl, code_muni, where(is.numeric))

#> Seleciona as colunas que tem o padrão '_texto_'
select(tbl, matches("_[a-z].+_"))
#> Selciona todas as colunas que começam com 'pib'
select(tbl, matches("^pib"))
#> Seleciona todas as colunas que terminam com 'muni'
select(tbl, matches("muni$"))

filter

A função filter serve para filtrar as linhas de um data.frame segundo alguma condição lógica.

filtered_tbl <- filter(tbl, population_growth < 0)

filtered_tbl
# A tibble: 2,399 × 22
   code_muni name_muni                code_state name_state abbrev_state
       <dbl> <chr>                         <dbl> <chr>      <chr>       
 1   1100015 Alta Floresta D'Oeste            11 Rondônia   RO          
 2   1100031 Cabixi                           11 Rondônia   RO          
 3   1100056 Cerejeiras                       11 Rondônia   RO          
 4   1100064 Colorado do Oeste                11 Rondônia   RO          
 5   1100072 Corumbiara                       11 Rondônia   RO          
 6   1100080 Costa Marques                    11 Rondônia   RO          
 7   1100106 Guajará-Mirim                    11 Rondônia   RO          
 8   1100114 Jaru                             11 Rondônia   RO          
 9   1100130 Machadinho D'Oeste               11 Rondônia   RO          
10   1100148 Nova Brasilândia D'Oeste         11 Rondônia   RO          
   code_region name_region population population_growth population_growth_rate
         <dbl> <chr>            <dbl>             <dbl>                  <dbl>
 1           1 Norte            21495             -2897                  -1.05
 2           1 Norte             5363              -950                  -1.35
 3           1 Norte            15890             -1139                  -0.58
 4           1 Norte            15663             -2928                  -1.42
 5           1 Norte             7519             -1264                  -1.29
 6           1 Norte            12627             -1051                  -0.66
 7           1 Norte            39386             -2270                  -0.47
 8           1 Norte            50591             -1414                  -0.23
 9           1 Norte            30707              -428                  -0.12
10           1 Norte            15679             -4195                  -1.96
   city_area population_density households dwellers_per_household     pib
       <dbl>              <dbl>      <dbl>                  <dbl>   <dbl>
 1      7067               3.04       7695                   2.79  570272
 2      1314               4.08       1967                   2.73  167190
 3      2783               5.71       5873                   2.69  600670
 4      1451              10.8        5991                   2.61  366931
 5      3060               2.46       2840                   2.64  268381
 6      4987               2.53       4161                   3.01  261978
 7     24857               1.58      11803                   3.3   984586
 8      2944              17.2       18947                   2.66 1665068
 9      8509               3.61      10841                   2.81  700317
10      1703               9.21       5798                   2.7   403370
   pib_share_uf pib_taxes pib_added_value pib_agriculture pib_industrial
          <dbl>     <dbl>           <dbl>           <dbl>          <dbl>
 1         1.11     35109          535163          203394          20716
 2         0.32      7237          159953           81177           5438
 3         1.16     89923          510747           94758          23582
 4         0.71     24075          342856           88923          24322
 5         0.52     10200          258181          155648          10847
 6         0.51      9276          252702           80684           6205
 7         1.91    139461          845125           59384          41650
 8         3.23    189006         1476062          223881         195776
 9         1.36     35830          664487          226106          35782
10         0.78     26898          376472          123270          22561
   pib_services pib_govmt_services
          <dbl>              <dbl>
 1       150192             160860
 2        28667              44671
 3       276755             115652
 4       118529             111082
 5        34749              56937
 6        48318             117495
 7       443005             301086
 8       712461             343944
 9       150365             252233
10       100806             129836
# ℹ 2,389 more rows

Os principais opereadores lógicos no R:

  • “Maior que”, “Menor que”: >, <, >=, <=

  • E/ou: &, |

  • “Negação”: !

  • “Igual a”: ==

  • “Dentro de”: %in%

Existem alguns outros operadores, mas estes costumam resolver 95% dos casos. O exemplo abaixo mostra como filtrar linhas baseado num string. Note que quando se usa múltiplos strings é preciso usar o %in%.

filter(tbl, name_muni == "São Paulo")
# A tibble: 1 × 22
  code_muni name_muni code_state name_state abbrev_state code_region name_region
      <dbl> <chr>          <dbl> <chr>      <chr>              <dbl> <chr>      
1   3550308 São Paulo         35 São Paulo  SP                     3 Sudeste    
  population population_growth population_growth_rate city_area
       <dbl>             <dbl>                  <dbl>     <dbl>
1   11451245            197742                   0.15      1521
  population_density households dwellers_per_household       pib pib_share_uf
               <dbl>      <dbl>                  <dbl>     <dbl>        <dbl>
1              7528.    4307693                   2.65 748759007         31.5
  pib_taxes pib_added_value pib_agriculture pib_industrial pib_services
      <dbl>           <dbl>           <dbl>          <dbl>        <dbl>
1 124349146       624409861           61896       58077784    520357969
  pib_govmt_services
               <dbl>
1           45912212
filter(tbl, name_muni %in% c("São Paulo", "Rio de Janeiro"))
# A tibble: 2 × 22
  code_muni name_muni      code_state name_state     abbrev_state code_region
      <dbl> <chr>               <dbl> <chr>          <chr>              <dbl>
1   3304557 Rio de Janeiro         33 Rio De Janeiro RJ                     3
2   3550308 São Paulo              35 São Paulo      SP                     3
  name_region population population_growth population_growth_rate city_area
  <chr>            <dbl>             <dbl>                  <dbl>     <dbl>
1 Sudeste        6211423           -109023                  -0.14      1200
2 Sudeste       11451245            197742                   0.15      1521
  population_density households dwellers_per_household       pib pib_share_uf
               <dbl>      <dbl>                  <dbl>     <dbl>        <dbl>
1              5175.    2437059                   2.53 331279902         44.0
2              7528.    4307693                   2.65 748759007         31.5
  pib_taxes pib_added_value pib_agriculture pib_industrial pib_services
      <dbl>           <dbl>           <dbl>          <dbl>        <dbl>
1  59975014       271304888          105065       36666723    180098159
2 124349146       624409861           61896       58077784    520357969
  pib_govmt_services
               <dbl>
1           54434942
2           45912212

Para negar a igualdade, basta usar o operador !. No caso do operador %in% há duas maneiras válidas de negá-lo: pode-se colocar o ! no começo da expressão ou colocar a expressão inteira dentro de um parêntesis. Eu tendo a preferir a segunda sintaxe.

#> Remove todas as cidades da região Sudeste
filter(tbl, name_region != "Sudeste")
#> Remove todas as cidades das regiões Sudeste e Norte
filter(tbl, !name_region %in% c("Sudeste", "Norte"))
#> Remove todas as cidades das regiões Sudeste e Norte
filter(tbl, !(name_region %in% c("Sudeste", "Norte")))

Em geral, não é preciso utilizar o E (&), já que pode-se colocar várias condições lógicas dentro de uma mesma chamada para função filter.

filter(
  tbl,
  name_region == "Nordeste",
  !(name_state %in% c("Pernambuco", "Piauí")),
  !(name_muni %in% c("Natal", "Fortaleza", "Maceió"))
  )

No caso de relações de grandeza, pode-se colocar um número absoluto, mas também pode-se usar alguma função. No exemplo abaixo filtra-se apenas os municípios com PIB acima da média, por exemplo.

filter(tbl, pib > mean(pib))
filter(tbl, population >= 1000000)

arrange

A função arrange é talvez a mais simples e serve para rearranjar as linhas de um data.frame. Em geral, ela é utilizada mais para fins estéticos ou exploratórios, como para ordenar as cidades pelo maior PIB, ou menor população. Contudo, no caso de uma tabela que contenha séries de tempo, pode ser importante validar que as observações estão na ordem correta.

tbl_arranged <- arrange(tbl, pib)

tbl_arranged
# A tibble: 5,570 × 22
   code_muni name_muni                  code_state name_state         
       <dbl> <chr>                           <dbl> <chr>              
 1   2209450 Santo Antônio dos Milagres         22 Piauí              
 2   2510659 Parari                             25 Paraíba            
 3   3166600 Serra da Saudade                   31 Minas Gerais       
 4   2414902 Viçosa                             24 Rio Grande Do Norte
 5   2206308 Miguel Leão                        22 Piauí              
 6   3147501 Passabém                           31 Minas Gerais       
 7   2501153 Areia de Baraúnas                  25 Paraíba            
 8   3115607 Cedro do Abaeté                    31 Minas Gerais       
 9   5201207 Anhanguera                         52 Goiás              
10   2504850 Coxixola                           25 Paraíba            
   abbrev_state code_region name_region  population population_growth
   <chr>              <dbl> <chr>             <dbl>             <dbl>
 1 PI                     2 Nordeste           2138                79
 2 PB                     2 Nordeste           1720              -122
 3 MG                     3 Sudeste             833                18
 4 RN                     2 Nordeste           1822               204
 5 PI                     2 Nordeste           1318                65
 6 MG                     3 Sudeste            1600              -166
 7 PB                     2 Nordeste           2005              -178
 8 MG                     3 Sudeste            1081              -129
 9 GO                     5 Centro Oeste        924               -96
10 PB                     2 Nordeste           1824                53
   population_growth_rate city_area population_density households
                    <dbl>     <dbl>              <dbl>      <dbl>
 1                   0.31        34              63.6         606
 2                  -0.57       208               8.28        644
 3                   0.18       336               2.48        337
 4                   0.99        38              48.1         636
 5                   0.42        93              14.1         406
 6                  -0.82        94              17.0         610
 7                  -0.71       114              17.6         673
 8                  -0.94       283               3.82        449
 9                  -0.82        56              16.6         358
10                   0.25       174              10.5         734
   dwellers_per_household   pib pib_share_uf pib_taxes pib_added_value
                    <dbl> <dbl>        <dbl>     <dbl>           <dbl>
 1                   3.53 16741         0.03       396           16345
 2                   2.67 20839         0.03       903           19936
 3                   2.46 21055         0          555           20500
 4                   2.86 21254         0.03       739           20515
 5                   3.23 21627         0.04      1454           20173
 6                   2.61 21854         0          817           21037
 7                   2.98 22128         0.03      1047           21082
 8                   2.41 22133         0          636           21497
 9                   2.58 22362         0.01      1312           21050
10                   2.49 22544         0.03       742           21801
   pib_agriculture pib_industrial pib_services pib_govmt_services
             <dbl>          <dbl>        <dbl>              <dbl>
 1             471            776         2545              12552
 2            1888            769         4692              12587
 3            5970            908         4249               9373
 4             768            738         5355              13654
 5             614           2042         5336              12181
 6            2582            873         5838              11745
 7             982            893         3735              15472
 8            4116            875         5633              10872
 9            2768            964         7164              10154
10            1907           1218         4849              13827
# ℹ 5,560 more rows

O padrão da função é de sempre ordenar de maneira crescente (do menor para o maior). Para inverter este comportamento pode-se usar a função desc ou o sinal de menos.

#> Ordena as cidades por PIB em ordem decrescente (maior ao menor)
arrange(tbl, desc(pib))
#> Ordena as cidades por PIB em ordem decrescente (maior ao menor)
arrange(tbl, -pib)

A função arrange ordena strings em ordem alfabética e Datas em ordem cronológica.

mutate

A função mutate cria novas colunas. Em geral, cria-se uma nova coluna com base nas colunas pré-existentes, mas a expressão é bastante geral na forma mutate(tbl, nova_coluna = …). Novamente, vou omitir as saídas para poupar espaço.

#> Cria uma coluna onde todas as entradas são iguais a 1
mutate(tbl, id = 1)
#> Cria a coluna 'lpib' igual ao logaritmo natural do 'pib'
mutate(tbl, lpib = log(pib))
#> Cria a coluna hh igual a 'household' dividido por 1 milhão
mutate(tbl, hh = household / 1e6)

Um fato conveniente da função mutate é que ela vai criando as colunas sequencialmente, assim é possível fazer diversas transformações numa mesma chamada à função. No caso abaixo, pode-se criar a variável lpibpc a partir das colunas lpib e lpop.

mutate(tbl,
  lpib = log(pib),
  lpop = log(population),
  #> Criando uma variável a partir de duas colunas criadas anteriormente
  lpibpc = lpib - lpop,
  pibserv = pib_services + pib_govmt_services,
  lpibs = log(pibserv)
  )

Por fim, é possível transformar múltiplas colunas simultaneamente usando a função across da seguinte maneira: across(colunas, função). Para indicar quais colunas quer-se transformar podemos usar a mesma lógica da função select: isto é, declarando o nome das colunas, usando col1:col2, ou mesmo uma função como starts_with/matches, etc.

tbl |> 
  mutate(across(pib:pib_services, log)) |> 
  select(pib:pib_services)
# A tibble: 5,570 × 7
     pib pib_share_uf pib_taxes pib_added_value pib_agriculture pib_industrial
   <dbl>        <dbl>     <dbl>           <dbl>           <dbl>          <dbl>
 1  13.3        0.104     10.5             13.2            12.2           9.94
 2  14.9        1.70      12.6             14.7            12.2          12.9 
 3  12.0       -1.14       8.89            12.0            11.3           8.60
 4  14.7        1.59      12.5             14.6            12.4          12.5 
 5  13.3        0.148     11.4             13.1            11.5          10.1 
 6  12.8       -0.342     10.1             12.7            11.4          10.1 
 7  12.5       -0.654      9.23            12.5            12.0           9.29
 8  12.5       -0.673      9.14            12.4            11.3           8.73
 9  13.4        0.255     11.0             13.3            11.8          10.9 
10  13.8        0.647     11.8             13.6            11.0          10.6 
   pib_services
          <dbl>
 1         11.9
 2         14.0
 3         10.3
 4         14.0
 5         12.5
 6         11.7
 7         10.5
 8         10.8
 9         12.3
10         13.0
# ℹ 5,560 more rows
#> Aplica uma transformação log em todas as colunas entre pib e pib_services
mutate(tbl, across(pib:pib_services, log))
#> Aplica uma transformação log em todas as colunas que começam com pib
mutate(tbl, across(starts_with("pib"), log))
#> Divide por pib e multiplica por 100 todas as colunas entre pib_taxes e
#> pib_govmt_services
mutate(tbl, across(pib_taxes:pib_govmt_services, ~.x / pib * 100))

summarise e group_by

Uso básico

As funções summarise5 e group_by são (quase) sempre utilizadas em conjunto e servem para resumir ou “sumarizar” os dados. A função group_by agrupa os dados segundo alguma coluna. No caso da nossa base de cidades, poderíamos agrupar os dados por estado ou região, por exemplo. A função summarise aplica transformações nestes dados agrupados: pode-se, por exemplo, calcular a população total de cada estado, o PIB per capita médio de cada região, etc.

A tabela abaixo calcula a população total de cada região e ordena os dados, de maneira decrescente, segundo a população. A partir de agora começo a usar mais o pipe nos códigos.

tbl |> 
  #> Agrupa por região
  group_by(name_region) |> 
  #> Soma o total da população (dentro de cada região)
  summarise(pop = sum(population)) |> 
  #> Rearranja o resultado final
  arrange(desc(pop))
# A tibble: 5 × 2
  name_region       pop
  <chr>           <dbl>
1 Sudeste      84847187
2 Nordeste     54644582
3 Sul          29933315
4 Norte        17349619
5 Centro Oeste 16287809

Um pouco mais de group_by

A função group_by agrupa os dados de um tibble e permite que se faça operações sobre estes grupos. Pode-se, por exemplo, calcular o share da população de cada cidade, dentro do seu estado usando mutate. No caso abaixo, eu calculo o share percentual e mostro o resultado para a capital paulista. Vê-se que a capital tem cerca de 11,45 milhão de habitantes, equivalente a 25,78% da população do estado.

tbl_share_pop <- tbl |> 
  group_by(name_state) |> 
  mutate(pop_share = population / sum(population) * 100)

tbl_share_pop |> 
  filter(name_muni == "São Paulo") |> 
  select(name_muni, population, pop_share)
# A tibble: 1 × 4
# Groups:   name_state [1]
  name_state name_muni population pop_share
  <chr>      <chr>          <dbl>     <dbl>
1 São Paulo  São Paulo   11451245      25.8

Similarmente, pode-se combinar outras funções como filter. O código abaixo filtra somente as cidades que possuem população acima da média do seu estado. Vale comparar os resultados e entender as diferenças entre cada uma das tabelas

#> Filtra cidades que possuem população acima da média do seu estado
tbl_pop_grouped <- tbl |> 
  group_by(name_state) |> 
  filter(population > mean(population))

#> Filtra cidades que possuem população acima da média do país
tbl_pop_ungrouped <- tbl |> 
  filter(population > mean(population))

Um pouco mais de summarise

É possível fazer várias novas colunas num mesmo summarise. Assim como em mutate também é possível fazer transformações com colunas que foram criadas anteriormente na mesma função. No caso abaixo eu calculo o PIB e população totais de cada estado do nordeste e depois calculo o PIB per capita baseado nestes valores agregados.

tbl |> 
  filter(name_region == "Nordeste") |> 
  group_by(name_state) |> 
  summarise(
    pib_uf = sum(pib) * 1000,
    pop_uf = sum(population),
    pibpc_uf = pib_uf / pop_uf
    ) |> 
  arrange(pibpc_uf)
# A tibble: 9 × 4
  name_state                pib_uf   pop_uf pibpc_uf
  <chr>                      <dbl>    <dbl>    <dbl>
1 Maranhão            106915961000  6775152   15781.
2 Piauí                56391259000  3269200   17249.
3 Paraíba              70292036000  3974495   17686.
4 Ceará               166914529000  8791688   18985.
5 Alagoas              63202350000  3127511   20209.
6 Sergipe              45409659000  2209558   20551.
7 Pernambuco          193307324000  9058155   21341.
8 Bahia               305320808000 14136417   21598.
9 Rio Grande Do Norte  71577110000  3302406   21674.

A função summarise é bastante potente. O exemplo abaixo filtra as cidades de médio-grande porte (acima de 100.000 habitantes), agrupa os dados por estado e faz:

  • O total (soma) da população (cidades com mais de 100.000 habitantes, por estado).

  • Calcula a média da população (entre as cidades com mais de 100.000 habitantes, por estado).

  • Calcula a população máxima (idem).

  • Calcula os quintis da distribuição da população (idem).

  • Faz uma regressão linear entre a população e o PIB (idem).

  • Faz uma regressão linear entre a população e o PIB e extrai o R2 (idem).

  • Conta o número de cidades (idem).

tbl_summary <- tbl |> 
  filter(population > 100000) |> 
  group_by(name_state) |> 
  summarise(
    pop_uf = sum(population),
    pop_avg = mean(population),
    pop_max = max(population),
    pop_ntile = list(quantile(population, probs = c(0.2, 0.4, 0.6, 0.8))),
    reg = list(lm(population ~ pib)),
    reg_r2 = summary(lm(population ~ pib))$r.squared,
    count = n()
    ) |> 
  arrange(desc(count))

Nem tudo o que fiz acima faz muito sentido, mas ilustra a capacidade da função summarise de gerar informação e a flexibilidade de um tibble para armazenar diferentes tipos de output. Note que as funções foram todas executadas dentro dos respectivos grupos.

No código abaixo pode-se verificar os quintis da população das cidades de Minas Gerais.

tbl_summary |> 
  filter(name_state == "Minas Gerais") |> 
  pull(pop_ntile)
[[1]]
     20%      40%      60%      80% 
111694.6 129821.8 169058.0 333014.8 

Já no código abaixo pode-se verificar o resultado da regressão entre população e PIB feita somente nos municípios grandes de São Paulo.

reg_sp <- filter(tbl_summary, name_state == "São Paulo")[["reg"]]
reg_sp <- reg_sp[[1]]

summary(reg_sp)

Call:
lm(formula = population ~ pib)

Residuals:
    Min      1Q  Median      3Q     Max 
-535435  -28068    8337   55289  240897 

Coefficients:
             Estimate Std. Error t value Pr(>|t|)    
(Intercept) 6.330e+04  1.619e+04   3.911 0.000199 ***
pib         1.511e-02  1.851e-04  81.617  < 2e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 137300 on 76 degrees of freedom
Multiple R-squared:  0.9887,    Adjusted R-squared:  0.9886 
F-statistic:  6661 on 1 and 76 DF,  p-value: < 2.2e-16

Adendos técnicos

Vale notar que a função group_by trasnforma um tibble num grouped_df. Para desfazer esta transformação é preciso usar ungroup. De fato, é uma boa prática sempre utilizar ungroup depois de usar um group_by. Por exemplo, no caso em que se calcula o share percentual da população de cada município em seu respectivo estado, é importante desagrupar os dados.

tbl_share_pop <- tbl |> 
  group_by(name_state) |> 
  mutate(pop_share = population / sum(population) * 100) |> 
  ungroup()

Esta prática serve para evitar erros potenciais e, infelizmente, é necessária em alguns casos, como quando se quer combinar duas bases distintas. Atualmente, existe uma sintaxe experimental, fortemente inspirada na sintaxe do data.table, que desagrupa os dados por padrão. No caso do código abaixo não é preciso utilizar ungroup().

Pessoalmente, gosto bastante desta sintaxe, mas como ela ainda está em fase experimental, é melhor esperar um pouco para utilizá-la.

tbl_share_pop <- tbl |> 
  mutate(
    pop_share = population / sum(population) * 100,
    .by = "name_state")

Este mesmo .by pode ser utilizado dentro de summarise, filter, etc.

Formato dos dados

O princípio geral da formatação dos dados, dentro do tidyverse, segue a lógica exposta em Wickham (2014):

  1. Toda variável é uma coluna.
  2. Cada observação é uma linha.
  3. Todo tipo de “unidade observacional” é uma tabela única.

Vale adicionar que o nome da coluna deve ser o nome de uma variável e não de um valor.

É importante frisar este ponto: uma tabela é uma coleção organizada de valores (números ou texto). Cada valor pertence a uma única variável e uma única observação. Por exemplo: 171 é a altura (variável) de João (observação) em centímetros. Uma variável contém todos os valores que mensuram o mesmo atributo (e.g. altura, peso, IMC, etc.). Uma observação contém todos os valores mensurados no mesmo sujeito/objeto (e.g. um indivíduo, um dia específico, uma rodada de um experimento, etc.).

Contra-exemplos

Vamos começar explorando alguns contra-exemplos de dados que não estão em formato “tidy”.

Vendas de casas e apartamentos

A tabela abaixo segue um formato tipicamente encontrando em planilhas de Excel. A primeira coluna define: (vendas de) apartamentos, casas e o total. Cada coluna subsequente representa um mês diferente; os valores de cada linha representam o número de vendas de cada tipo em cada mês.

dat <- tibble(
  nome = c("Apartamentos", "Casas", "Total"),
  `2022-01` = c(900, 100, 1000),
  `2022-02` = c(850, 120, 970),
  `2022-03` = c(875, 125, 1000),
  `2022-04` = c(920, 100, 1020),
)

dat
# A tibble: 3 × 5
  nome         `2022-01` `2022-02` `2022-03` `2022-04`
  <chr>            <dbl>     <dbl>     <dbl>     <dbl>
1 Apartamentos       900       850       875       920
2 Casas              100       120       125       100
3 Total             1000       970      1000      1020

Note que:

  1. Cada coluna não é uma variável. A maior parte das colunas são datas, que deveriam estar todas numa única coluna.

  2. Cada linha não é uma observação. Cada linha é uma série de valores de observações que varia mês a mês.

Vendas e Alugueis de apartamentos e casas

Esta segunda tabela é uma versão piorada da versão acima.

dat2 <- tibble(
  nome = c("Apartamentos", "Casas", "Total", "Apartamentos", "Casas", "Total"),
  tipo = c("Venda", "Venda", "Venda", "Aluguel", "Aluguel", "Aluguel"),
  `2022-01` = c(900, 100, 1000, 50, 100, 150),
  `2022-02` = c(850, 120, 970, 60, 80, 140),
  `2022-03` = c(875, 125, 1000, 70, 90, 160),
  `2022-04` = c(920, 100, 1020, 50, 50, 100),
)

dat2
# A tibble: 6 × 6
  nome         tipo    `2022-01` `2022-02` `2022-03` `2022-04`
  <chr>        <chr>       <dbl>     <dbl>     <dbl>     <dbl>
1 Apartamentos Venda         900       850       875       920
2 Casas        Venda         100       120       125       100
3 Total        Venda        1000       970      1000      1020
4 Apartamentos Aluguel        50        60        70        50
5 Casas        Aluguel       100        80        90        50
6 Total        Aluguel       150       140       160       100

Note que:

  1. O nome das colunas mistura variáveis e valores.
  2. A linha continua não sendo uma observação.

Teste AB

A tabela abaixo mostra um teste AB num formato (bem) problemático. No experimento, Bernardo e Álvares estão no grupo de tratamento, enquanto Fernando e Ricardo estão no grupo controle.

dat3 <- tibble(
  id = c("Bernardo", "Álvares", "Fernando", "Ricardo"),
  controle = c(NA, NA, 7.2, 5.1),
  tratamento = c(6.4, 5.5, NA, NA)
)

dat3
# A tibble: 4 × 3
  id       controle tratamento
  <chr>       <dbl>      <dbl>
1 Bernardo     NA          6.4
2 Álvares      NA          5.5
3 Fernando      7.2       NA  
4 Ricardo       5.1       NA  

Note que:

  1. Nem ‘controle’ e nem ‘tratamento’ são variáveis, já que são valores de uma mesma variável “qual grupo que está o indivíduo”.

Alternativamente, pode-se ter:

dat31 <- tibble(
  grupo = c("controle", "tratamento"),
  `Bernardo` = c(NA, 6.4),
  `Álvares` = c(NA, 5.5),
  `Fernando` = c(7.2, NA),
  `Ricardo` = c(5.1, NA)
)

dat31
# A tibble: 2 × 5
  grupo      Bernardo Álvares Fernando Ricardo
  <chr>         <dbl>   <dbl>    <dbl>   <dbl>
1 controle       NA      NA        7.2     5.1
2 tratamento      6.4     5.5     NA      NA  

Agora ‘controle’ e ‘tratamento’ estão corretamente dentro de uma mesma coluna, mas cada coluna representa um indivíduo diferente e não uma variável. “Bernardo” não é uma variável e sim um valor (nome do indivíduo).

Cidades

A tabela abaixo mostra dados hipóteticos de PIB e população de duas cidades.

dat4 <- tibble(
  nome_cidade = c("São Paulo", "Porto Alegre"),
  pib_2020 = c(1000, 500),
  pib_2021 = c(1200, 700),
  pop_2020 = c(1100, 110),
  pop_2021 = c(1200, 120)
)

dat4
# A tibble: 2 × 5
  nome_cidade  pib_2020 pib_2021 pop_2020 pop_2021
  <chr>           <dbl>    <dbl>    <dbl>    <dbl>
1 São Paulo        1000     1200     1100     1200
2 Porto Alegre      500      700      110      120

Note que:

  1. Há mais de uma variável por coluna: a coluna pib_2020 indica duas informações: o ano da observação e o que está sendo mensurado (PIB).

Casas e Apartamentos

Nesta tabela o nome das colunas mistura variáveis e valores.

data <- tibble(
  cidade = c("A", "B", "C"),
  financiado_apto_2020 = c(1, 2, 3),
  vista_apto_2020 = c(2, 2, 2),
  permuta_casa_2020 = c(3, 1, 2),
  vista_casa_2020 = c(1, 1, 1)
)

data
# A tibble: 3 × 5
  cidade financiado_apto_2020 vista_apto_2020 permuta_casa_2020 vista_casa_2020
  <chr>                 <dbl>           <dbl>             <dbl>           <dbl>
1 A                         1               2                 3               1
2 B                         2               2                 1               1
3 C                         3               2                 2               1

Organizando

Sendo bastante franco, acho difícil explicar a intuição por trás das funções pivot_longer e pivot_wider. No caso da primeira função tem-se:

dat |> 
  pivot_longer(
    cols = ...,
    #> Argumentos opcionais
    names_to = "name",
    values_to = "value"
  )

onde cols indica quais colunas devem ser convertidas em formato longitudinal. Este argumento é bastante flexível e segue as mesmas regras da função select. Por exemplo:

dat |> pivot_longer(cols = -date)
dat |> pivot_longer(cols = starts_with("pib"))
dat |> pivot_longer(cols = c("x1", "x2"))

Já a função pivot_wider é mais exigente:

dat |> 
  pivot_wider(
    id_cols = ...,
    names_from = ...,
    values_from = ...
  )

O primeiro argumento indica qual coluna identifica unicamente os valores; o segundo argumento indica quais valores devem ser convertidos em colunas (variáveis); o terceiro argumento indica quais valores devem ser convertido em valores (sim, é isto mesmo). A função “desfaz” o que a pivot_longer “faz”, mas também pode fazer novas tabelas e também condensar informação. Pra piorar a situação, ambas as funções tem vários argumentos opcionais, que muitas vezes são super úteis.

Como num jogo, explicar as regras em voz-alta parece torná-lo mais complicado do que é. A melhor dica que posso dar é que se pratique bastante. Alternativamente, considere também as funções:

  • data.table::melt ou reshape2::melt. É preciso escolher as colunas “identificadoras” e as colunas de “mensuração”. Eu penso no “id” como o que identifica unicamente cada linha e “measure” como o que identifica o que está sendo mensurado. Por exemplo: na tabela abaixo temos o preços de duas ações em dois dias distintos.
tab <- tibble(
  data = c(as.Date("2023-05-04"), as.Date("2023-05-05")),
  PETR4 = c(23.02, 24),
  CYRE3 = c(15.54, 15.97)
)

tab
# A tibble: 2 × 3
  data       PETR4 CYRE3
  <date>     <dbl> <dbl>
1 2023-05-04  23.0  15.5
2 2023-05-05  24    16.0

Para converter em formato “tidy” ou longitudinal, declara-se quais as colunas identificam a observação e quais as colunas indicam o que está sendo mensurado.

reshape2::melt(tab, id.vars = "data", measure.vars = c("PETR4", "CYRE3"))
        data variable value
1 2023-05-04    PETR4 23.02
2 2023-05-05    PETR4 24.00
3 2023-05-04    CYRE3 15.54
4 2023-05-05    CYRE3 15.97

Neste caso particular, é útil saber que o argumento measure.vars pode ser omitido

reshape2::melt(tab, id.vars = "data")
        data variable value
1 2023-05-04    PETR4 23.02
2 2023-05-05    PETR4 24.00
3 2023-05-04    CYRE3 15.54
4 2023-05-05    CYRE3 15.97
  • O contrário destas funções é data.table::dcast ou reshape2::dcast, cuja sintaxe é um pouco mais estranha, mas bastante intuitiva.
long <- reshape2::melt(tab, id.vars = "data")

reshape2::dcast(long, data ~ variable)
        data PETR4 CYRE3
1 2023-05-04 23.02 15.54
2 2023-05-05 24.00 15.97
  • tidyr::gather que é a versão antiga de pivot_longer. Pessoalmente, sempre achei esta função bastante confusa, mas, ela talvez seja mais intuitiva para você.
gather(tab, "ticker", "price", -data)
# A tibble: 4 × 3
  data       ticker price
  <date>     <chr>  <dbl>
1 2023-05-04 PETR4   23.0
2 2023-05-05 PETR4   24  
3 2023-05-04 CYRE3   15.5
4 2023-05-05 CYRE3   16.0
long <- gather(tab, "ticker", "price", -data)
spread(long, "ticker", "price")
# A tibble: 2 × 3
  data       CYRE3 PETR4
  <date>     <dbl> <dbl>
1 2023-05-04  15.5  23.0
2 2023-05-05  16.0  24  

Neste caso simples, o par gather/spread parece bastante conveniente, mas em casos mais complexos esta sintaxe limitada torna-se bastante problemática.

Exemplo 1

dat
# A tibble: 3 × 5
  nome         `2022-01` `2022-02` `2022-03` `2022-04`
  <chr>            <dbl>     <dbl>     <dbl>     <dbl>
1 Apartamentos       900       850       875       920
2 Casas              100       120       125       100
3 Total             1000       970      1000      1020

Este caso é bem simples, pois todas as colunas, exceto a primeira, são uma única variável (as datas do mês). A função rename é usada somente para deixar mais evidente que a primeira coluna contém a tipologia do que foi vendido. O argumento values_to também é opcional.

dat |> 
  rename(tipologia = nome) |> 
  pivot_longer(cols = -tipologia, names_to = "data", values_to = "unidades")
# A tibble: 12 × 3
   tipologia    data    unidades
   <chr>        <chr>      <dbl>
 1 Apartamentos 2022-01      900
 2 Apartamentos 2022-02      850
 3 Apartamentos 2022-03      875
 4 Apartamentos 2022-04      920
 5 Casas        2022-01      100
 6 Casas        2022-02      120
 7 Casas        2022-03      125
 8 Casas        2022-04      100
 9 Total        2022-01     1000
10 Total        2022-02      970
11 Total        2022-03     1000
12 Total        2022-04     1020

Note que agora:

  1. Cada linha é uma observação: 900 é o valor de apartamentos vendidos em 2022-01.
  2. Cada coluna é uma variável: a primeira coluna define a ‘tipologia’, a segunda coluna define a ‘data’ e a terceira coluna é o número de ‘unidades’.

Por fim, para ser mais completo pode-se converter a data num formato padrão.

dat |> 
  rename(tipologia = nome) |> 
  pivot_longer(cols = -tipologia, names_to = "data", values_to = "unidades") |> 
  mutate(data = readr::parse_date(data, format = "%Y-%m"))
# A tibble: 12 × 3
   tipologia    data       unidades
   <chr>        <date>        <dbl>
 1 Apartamentos 2022-01-01      900
 2 Apartamentos 2022-02-01      850
 3 Apartamentos 2022-03-01      875
 4 Apartamentos 2022-04-01      920
 5 Casas        2022-01-01      100
 6 Casas        2022-02-01      120
 7 Casas        2022-03-01      125
 8 Casas        2022-04-01      100
 9 Total        2022-01-01     1000
10 Total        2022-02-01      970
11 Total        2022-03-01     1000
12 Total        2022-04-01     1020

Exemplo 2

dat2
# A tibble: 6 × 6
  nome         tipo    `2022-01` `2022-02` `2022-03` `2022-04`
  <chr>        <chr>       <dbl>     <dbl>     <dbl>     <dbl>
1 Apartamentos Venda         900       850       875       920
2 Casas        Venda         100       120       125       100
3 Total        Venda        1000       970      1000      1020
4 Apartamentos Aluguel        50        60        70        50
5 Casas        Aluguel       100        80        90        50
6 Total        Aluguel       150       140       160       100

O segundo exemplo é quase idêntico ao primeiro.

dat2 |> 
  rename(tipologia = nome, mercado = tipo) |> 
  pivot_longer(
    cols = -c(tipologia, mercado),
    names_to = "data",
    values_to = "unidades"
    ) |> 
  print(n = 24)
# A tibble: 24 × 4
   tipologia    mercado data    unidades
   <chr>        <chr>   <chr>      <dbl>
 1 Apartamentos Venda   2022-01      900
 2 Apartamentos Venda   2022-02      850
 3 Apartamentos Venda   2022-03      875
 4 Apartamentos Venda   2022-04      920
 5 Casas        Venda   2022-01      100
 6 Casas        Venda   2022-02      120
 7 Casas        Venda   2022-03      125
 8 Casas        Venda   2022-04      100
 9 Total        Venda   2022-01     1000
10 Total        Venda   2022-02      970
11 Total        Venda   2022-03     1000
12 Total        Venda   2022-04     1020
13 Apartamentos Aluguel 2022-01       50
14 Apartamentos Aluguel 2022-02       60
15 Apartamentos Aluguel 2022-03       70
16 Apartamentos Aluguel 2022-04       50
17 Casas        Aluguel 2022-01      100
18 Casas        Aluguel 2022-02       80
19 Casas        Aluguel 2022-03       90
20 Casas        Aluguel 2022-04       50
21 Total        Aluguel 2022-01      150
22 Total        Aluguel 2022-02      140
23 Total        Aluguel 2022-03      160
24 Total        Aluguel 2022-04      100

Exemplo 3

dat3
# A tibble: 4 × 3
  id       controle tratamento
  <chr>       <dbl>      <dbl>
1 Bernardo     NA          6.4
2 Álvares      NA          5.5
3 Fernando      7.2       NA  
4 Ricardo       5.1       NA  
dat3 |> 
  pivot_longer(-id, names_to = "grupo", values_to = "valor")
# A tibble: 8 × 3
  id       grupo      valor
  <chr>    <chr>      <dbl>
1 Bernardo controle    NA  
2 Bernardo tratamento   6.4
3 Álvares  controle    NA  
4 Álvares  tratamento   5.5
5 Fernando controle     7.2
6 Fernando tratamento  NA  
7 Ricardo  controle     5.1
8 Ricardo  tratamento  NA  

Exemplo 4

dat4
# A tibble: 2 × 5
  nome_cidade  pib_2020 pib_2021 pop_2020 pop_2021
  <chr>           <dbl>    <dbl>    <dbl>    <dbl>
1 São Paulo        1000     1200     1100     1200
2 Porto Alegre      500      700      110      120

Neste exemplo pode-se separar as colunas facilmente usando names_sep e names_to.

dat4 |> 
  pivot_longer(
    cols = -nome_cidade,
    names_sep = "_",
    names_to = c("variavel", "ano"),
    values_to = "valor"
    )
# A tibble: 8 × 4
  nome_cidade  variavel ano   valor
  <chr>        <chr>    <chr> <dbl>
1 São Paulo    pib      2020   1000
2 São Paulo    pib      2021   1200
3 São Paulo    pop      2020   1100
4 São Paulo    pop      2021   1200
5 Porto Alegre pib      2020    500
6 Porto Alegre pib      2021    700
7 Porto Alegre pop      2020    110
8 Porto Alegre pop      2021    120

Exemplo 5

data
# A tibble: 3 × 5
  cidade financiado_apto_2020 vista_apto_2020 permuta_casa_2020 vista_casa_2020
  <chr>                 <dbl>           <dbl>             <dbl>           <dbl>
1 A                         1               2                 3               1
2 B                         2               2                 1               1
3 C                         3               2                 2               1

Confesso que só coloquei este exemplo aqui para mostrar que é possível usar dois pivot_longer em sequência para chegar num resultado útil.

data |> 
  pivot_longer(
    cols = -cidade,
    names_sep = "_",
    names_to = c(".value", "tipologia", "ano")
  ) |> 
  pivot_longer(
    cols = financiado:permuta,
    names_to = "forma_pagamento",
    values_to = "unidades"
  )
# A tibble: 18 × 5
   cidade tipologia ano   forma_pagamento unidades
   <chr>  <chr>     <chr> <chr>              <dbl>
 1 A      apto      2020  financiado             1
 2 A      apto      2020  vista                  2
 3 A      apto      2020  permuta               NA
 4 A      casa      2020  financiado            NA
 5 A      casa      2020  vista                  1
 6 A      casa      2020  permuta                3
 7 B      apto      2020  financiado             2
 8 B      apto      2020  vista                  2
 9 B      apto      2020  permuta               NA
10 B      casa      2020  financiado            NA
11 B      casa      2020  vista                  1
12 B      casa      2020  permuta                1
13 C      apto      2020  financiado             3
14 C      apto      2020  vista                  2
15 C      apto      2020  permuta               NA
16 C      casa      2020  financiado            NA
17 C      casa      2020  vista                  1
18 C      casa      2020  permuta                2

Referências e Comentários

Como comentei no texto, a popularidade do tidyverse criou um certo consenso de que esta é a melhor forma de começar a aprender R. Ainda assim, acho que vale a pena considerar alguns dos contra-argumentos:

Footnotes

  1. Para saber mais sobre pipes e a diferença entre o novo pipe nativo |> e o pipe %>% do magrittr veja meu post sobre o assunto.↩︎

  2. No fundo, isto é ainda mais um incentivo para aprender inglês.↩︎

  3. A classe mais geral de números do R é a numeric. Aqui, “double” faz referência à precisão do número, que é um “double-precision value”, que equivale a uma precisão de 53 bits, com aplitude de \(2\times 10^{-308}\) a \(2\times 10^{-308}\). Em geral, a maioria dos números (com exceção de inteiros) é armazenada desta forma.↩︎

  4. De fato, esta é a mesma sintaxe que se utiliza para extrair um elemento de uma lista. Isto acontece pois um data.frame é essencialmente, uma lista com um pouco mais de estrutura. Isto pode ser verificado usando typeof em um objeto data.frame.↩︎

  5. O dplyr também aceita a função summarize com ‘z’ ao invés de ‘s’. As funções são exatamente iguais e podem ser intercambiadas sem maiores problemas.↩︎