# Instala o pacote tidyverse
install.packages("tidyverse")
# Carrega o pacote tidyverse
library("tidyverse")
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.
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 defactors
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.
<- lm(log(AirPassengers) ~ time(AirPassengers))
model
#> Função composta
mean(exp(fitted(model)))
#> Usando pipes
|> fitted() |> exp() |> mean()
model #> Usando objetos intermediários
<- fitted(model)
x1 <- exp(x1)
x2 <- mean(x2) x3
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:
- Comprimento fixo. O número de linhas de um
data.frame
é fixo, assim todas as colunas têm o mesmo comprimento. - 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, umfactor
e um string, etc. - 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.
<- tibble(
dados 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.
<- c("Porto Alegre", "São Paulo", "Salvador")
cidades <- c(1.332, 11.451, 2.418)
populacao
<- tibble(
dados 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.
<- tibble(
dados 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.
<- data.frame(
tab 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.
<- tibble(
tab 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
$cidade
dados#> [1] "Porto Alegre" "São Paulo" "Salvador"
"cidade"]]
dados[[#> [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:
- Há muitos arquivos para se importar.
- É difícil fazer o
R
encontrar o arquivo. - 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
<- read_csv("/Users/viniciusoike/Documents/GitHub/projeto/data/income.csv")
dat #> Bom
<- read_csv("data/income.csv")
dat #> Ainda melhor
<- read_csv(here::here("data/income.csv")) dat
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
ouname_repair
: O primeiro permite que se declare explicitamente o nome que cada coluna vai ter dentro doR
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-selocale = 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
<- read_delim(
df #> 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.
<- read_csv("...") tbl
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
<- rename(tbl, codigo_municipio = code_muni, pop = population) tbl_renamed
Os nomes das colunas de um data.frame
devem
- Ser únicos (não-duplicados) e evitar caracteres maiúsculos.
- Não devem incluir caracteres especiais (e.g. !*&@%), nem começar com um número ou caractere especial.
- Evitar espaços em branco, que devem ser substituídos por
_
ou omitidos (e.g.PIB Agro
deve ser reescrito comopibAgro
oupib_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
.
<- c(
new_names "codigo_municipio" = "code_muni",
"pop" = "population",
"pop_rate" = "population_growth_rate"
)
<- rename(tbl, all_of(new_names)) tbl_renamed
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
.
<- c(
new_names "codigo_municipio" = "code_muni",
"pop" = "population",
"pop_rate" = "population_growth_rate",
"unidade" = "unit"
)
<- rename(tbl, any_of(new_names)) tbl_renamed
Já a função all_of
é mais rigorosa e retorna um erro indicando que a coluna unit
não existe.
<- rename(tbl, all_of(new_names)) tbl_renamed
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
.
<- rename_with(tbl, toupper)
tbl_renamed 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.
<- as.data.frame(matrix(ncol = 6))
test_df names(test_df) <- c("firstName", "ábc@!*", "% successful (2009)",
"REPEAT VALUE", "REPEAT VALUE", "")
::clean_names(test_df)
janitor#> 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
<- select(tbl, code_muni, pib, pib_agriculture)
sel_tbl
#> Cria um vetor de nomes
<- c("code_muni", "pib", "pib_agriculture")
colunas #> Seleciona as colunas baseado no vetor
<- select(tbl, all_of(colunas))
sel_tbl
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
<- select(tbl, -pib, -city_area, -population_density)
sel_tbl
#> Cria um vetor de nomes
<- c("pib", "city_area", "population_density")
colunas #> Seleciona as colunas baseado no vetor
<- select(tbl, -all_of(colunas))
sel_tbl
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).
<- select(tbl, code_muni, pib:pib_added_value)
sel_tbl
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.
<- select(tbl, codigo_municipio = code_muni, pib)
sel_tbl
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.
<- filter(tbl, population_growth < 0)
filtered_tbl
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,== "Nordeste",
name_region !(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.
<- arrange(tbl, pib)
tbl_arranged
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 summarise
5 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 |>
tbl_share_pop 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 |>
tbl_pop_grouped group_by(name_state) |>
filter(population > mean(population))
#> Filtra cidades que possuem população acima da média do país
<- tbl |>
tbl_pop_ungrouped 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 |>
tbl_summary 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.
<- filter(tbl_summary, name_state == "São Paulo")[["reg"]]
reg_sp <- reg_sp[[1]]
reg_sp
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 |>
tbl_share_pop 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 |>
tbl_share_pop 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):
- Toda variável é uma coluna.
- Cada observação é uma linha.
- 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.
<- tibble(
dat 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:
Cada coluna não é uma variável. A maior parte das colunas são datas, que deveriam estar todas numa única coluna.
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.
<- tibble(
dat2 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:
- O nome das colunas mistura variáveis e valores.
- 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.
<- tibble(
dat3 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:
- 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:
<- tibble(
dat31 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.
<- tibble(
dat4 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:
- 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.
<- tibble(
data 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:
|> pivot_longer(cols = -date)
dat |> pivot_longer(cols = starts_with("pib"))
dat |> pivot_longer(cols = c("x1", "x2")) dat
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
oureshape2::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.
<- tibble(
tab 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.
::melt(tab, id.vars = "data", measure.vars = c("PETR4", "CYRE3")) reshape2
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
::melt(tab, id.vars = "data") reshape2
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
oureshape2::dcast
, cuja sintaxe é um pouco mais estranha, mas bastante intuitiva.
<- reshape2::melt(tab, id.vars = "data")
long
::dcast(long, data ~ variable) reshape2
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 depivot_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
<- gather(tab, "ticker", "price", -data)
long 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:
- Cada linha é uma observação: 900 é o valor de apartamentos vendidos em 2022-01.
- 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
Wickham, H. Centinkaya-Rundel, M. Grolemund, G. R for Data Science. 2ed. - Uma introdução didática ao tidyverse com aplicações para ciências de dados.
Centinkaya-Rundel, M. Teaching the tidyverse in 2023 - Uma visão geral de como ensinar o
tidyverse
na sua versão mais atual. Discute um pouco dos desafios de ensinar tantos pacotes e funções.Wickham, H. Tidy Data. Journal of Statistical Software. 2014. - Artigo seminal com o conceito de tidy. Este artigo foi acompanhado dos pacotes
plyr
ereshape2
que foram os embriões dodplyr
etidyr
.
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:
Matloff, N. Teach Base-R, Not Just the Tidyverse - Um famoso argumento contra o ensino do
tidyverse
. Essencialmente, argumenta-se que o tidyverse além de ser bastante complexo, esconde alguns aspectos fundamentais do R como vetores e operadores$
, o que dificulta o aprendizado da linguagem.Matloff, N. Greatly Revised Edition of Tidyverse Skeptic - Atualização do texto anterior.
Footnotes
Para saber mais sobre pipes e a diferença entre o novo pipe nativo
|>
e o pipe%>%
domagrittr
veja meu post sobre o assunto.↩︎No fundo, isto é ainda mais um incentivo para aprender inglês.↩︎
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.↩︎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 usandotypeof
em um objetodata.frame
.↩︎O
dplyr
também aceita a funçãosummarize
com ‘z’ ao invés de ‘s’. As funções são exatamente iguais e podem ser intercambiadas sem maiores problemas.↩︎