library(dplyr)
library(readr)O novo tidyverse: filter
dplyr lançou nos últimos anos como as funções auxiliares if_any e if_all.
Tidyverse
O tidyverse é uma coleção poderosa de pacotes, voltados para a manipulação e limpeza de dados. Num outro post, discuti alguns aspectos gerais da filosofia destes pacotes que incluem a sua consistência sintática e o uso de pipes. 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” que funcionam como conectivos numa frase. Em tese, isto torna o código mais legível e até mais didático.
O tidyverse está em constante expansão, novas funcionalidades são criadas para melhorar a performance e capabilidade de suas funções. Assim, é importante atualizar nosso conhecimento destes pacotes periodicamente. Nesta série de posts vou focar nas funções principais dos pacotes dplyr e tidyr, voltados para a limpeza de dados.
Alguns verbos
Essencialmente, o dplyr gira em torno de quatro grandes funções: filter, select, mutate e summarise. Estas funções fazem o grosso do trabalho de limpeza de dados: filtram linhas, selecionam colunas e transformam os dados. A tabela abaixo resume as principais funções do pacote.
| 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. |
filter
O básico
Os pacotes utilizados neste tutorial são listados abaixo.
Para praticar as funções vamos utilizar uma tabela que traz informações sobre as cidades do Brasil.
dat <- readr::read_csv(
"https://github.com/viniciusoike/restateinsight/raw/main/static/data/cities_brazil.csv"
)
# dat <- select(dat, 1:7, population, population_growth, pib)A função filter é talvez uma das que menos mudou ao longo do desenvolvimento do pacote dplyr. A função serve para filtrar as linhas de um data.frame segundo alguma condição lógica.
filtered_dat <- filter(dat, population_growth < 0)
nrow(filtered_dat)[1] 2399
Os principais operadores lógicos no R:
“Maior que”, “Menor que”:
>,<,>=,<=E/ou:
&,|“Negação”:
!“Igual a”:
==“Dentro de”:
%in%
As funções is_* também são bastante importantes; em particular a função is.na() é útil para encontrar ou remover observações ausentes.
O exemplo abaixo mostra como filtrar linhas baseado num string. Note que quando se usa múltiplos strings é preciso usar o %in%.
filter(dat, name_muni == "São Paulo")
filter(dat, name_muni %in% c("São Paulo", "Rio de Janeiro"))
cities <- c("São Paulo", "Rio de Janeiro")
filter(dat, name_muni %in% cities)filter(dat, name_muni %in% c("São Paulo", "Rio de Janeiro")) |>
print_table()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(dat, name_region != "Sudeste")
#> Remove todas as cidades das regiões Sudeste e Norte
filter(dat, !name_region %in% c("Sudeste", "Norte"))
#> Remove todas as cidades das regiões Sudeste e Norte
filter(dat, !(name_region %in% c("Sudeste", "Norte")))Em geral, pode-se omitir o operador E (&), já que se pode concatenar várias condições lógicas dentro uma mesma chamada para a função filter, separando as condições por vírgulas. Esta sintaxe costuma ser preferida pois ela é mais eficiente do que chamar a função a função filter múltiplas vezes. Além disso, a escrita do código fica mais limpa, pois é fácil separar as condições em linhas distintas. As três versões do código abaixo geram o mesmo resultado.
# Mais eficiente e mais fácil de ler
d1 <- dat |>
filter(
name_region == "Nordeste",
!(name_state %in% c("Pernambuco", "Piauí")),
!(name_muni %in% c("Natal", "Fortaleza", "Maceió"))
)
# Igualmente eficiente, leitura fica um pouco pior
d2 <- dat |>
filter(
name_region == "Nordeste" &
!(name_state %in% c("Pernambuco", "Piauí")) &
!(name_muni %in% c("Natal", "Fortaleza", "Maceió"))
)
# Menos eficiente
d3 <- dat |>
filter(name_region == "Nordeste") |>
filter(!(name_state %in% c("Pernambuco", "Piauí"))) |>
filter(!(name_muni %in% c("Natal", "Fortaleza", "Maceió")))
all.equal(d1, d2)
all.equal(d2, d3)
all.equal(d3, d1)Relações de grandeza funcionam naturalmente com números. A tabela abaixo mostra todos os municípios com mais do que um milhão de habitantes.
filter(dat, population > 1e6)| name_muni | abbrev_state | population | population_density |
|---|---|---|---|
| São Paulo | SP | 11.451.245 | 7.528 |
| Rio de Janeiro | RJ | 6.211.423 | 5.175 |
| Brasília | DF | 2.817.068 | 489 |
| Fortaleza | CE | 2.428.678 | 7.775 |
| Salvador | BA | 2.418.005 | 3.487 |
| Belo Horizonte | MG | 2.315.560 | 6.988 |
| Manaus | AM | 2.063.547 | 181 |
| Curitiba | PR | 1.773.733 | 4.079 |
| Recife | PE | 1.488.920 | 6.804 |
| Goiânia | GO | 1.437.237 | 1.971 |
| Porto Alegre | RS | 1.332.570 | 2.690 |
| Belém | PA | 1.303.389 | 1.230 |
| Guarulhos | SP | 1.291.784 | 4.054 |
| Campinas | SP | 1.138.309 | 1.433 |
| São Luís | MA | 1.037.775 | 1.780 |
Também pode-se usar alguma função que retorne um valor numérico. Nos exemplos abaixo filtra-se apenas os municípios com PIB acima da média e os municípios no top 1% da distribuição do PIB.
filter(dat, pib > mean(pib))
filter(dat, pib > quantile(pib, probs = 0.99))| name_muni | abbrev_state | pib | pib_share_uf |
|---|---|---|---|
| São Paulo | SP | 748.759.007 | 31 |
| Rio de Janeiro | RJ | 331.279.902 | 44 |
| Brasília | DF | 265.847.334 | 100 |
| Belo Horizonte | MG | 97.509.893 | 14 |
| Manaus | AM | 91.768.773 | 79 |
| Curitiba | PR | 88.308.728 | 18 |
| Osasco | SP | 76.311.814 | 3 |
| Porto Alegre | RS | 76.074.563 | 16 |
| Guarulhos | SP | 65.849.311 | 3 |
| Campinas | SP | 65.419.717 | 3 |
| Fortaleza | CE | 65.160.893 | 39 |
| Salvador | BA | 58.938.115 | 19 |
| Goiânia | GO | 51.961.311 | 23 |
| Barueri | SP | 51.254.572 | 2 |
| Jundiaí | SP | 51.235.050 | 2 |
| Recife | PE | 50.311.002 | 26 |
| São Bernardo do Campo | SP | 48.614.342 | 2 |
| Duque de Caxias | RJ | 47.153.673 | 6 |
| Niterói | RJ | 40.949.495 | 5 |
| São José dos Campos | SP | 39.148.012 | 2 |
| Paulínia | SP | 38.572.766 | 2 |
| Parauapebas | PA | 38.014.863 | 18 |
| Uberlândia | MG | 37.631.537 | 6 |
| Sorocaba | SP | 36.723.769 | 2 |
| Joinville | SC | 36.391.912 | 10 |
| Maricá | RJ | 35.618.327 | 5 |
| Ribeirão Preto | SP | 35.218.869 | 1 |
| Itajaí | SC | 33.084.145 | 9 |
| São Luís | MA | 33.074.010 | 31 |
| Belém | PA | 30.835.763 | 14 |
| Campo Grande | MS | 30.121.789 | 25 |
| Contagem | MG | 29.558.094 | 4 |
| Santo André | SP | 29.440.477 | 1 |
| Piracicaba | SP | 27.172.817 | 1 |
| Cuiabá | MT | 26.528.839 | 15 |
| Betim | MG | 26.185.005 | 4 |
| Caxias do Sul | RS | 25.965.161 | 6 |
| Camaçari | BA | 25.697.266 | 8 |
| Vitória | ES | 25.473.898 | 18 |
| Serra | ES | 25.079.657 | 18 |
| Campos dos Goytacazes | RJ | 23.841.837 | 3 |
| Maceió | AL | 22.872.756 | 36 |
| Natal | RN | 22.729.773 | 32 |
| Canaã dos Carajás | PA | 22.522.725 | 10 |
| Santos | SP | 22.073.535 | 1 |
| São José dos Pinhais | PR | 21.975.612 | 4 |
| Londrina | PR | 21.729.852 | 4 |
| Teresina | PI | 21.578.875 | 38 |
| Florianópolis | SC | 21.312.447 | 6 |
| Cajamar | SP | 20.798.646 | 1 |
| João Pessoa | PB | 20.766.551 | 30 |
| Maringá | PR | 20.005.630 | 4 |
| Araucária | PR | 19.724.416 | 4 |
| Porto Velho | RO | 19.448.762 | 38 |
| São Gonçalo | RJ | 19.002.883 | 3 |
| São José do Rio Preto | SP | 18.694.213 | 1 |
Grupos
A função de filtro segue uma regra lógica que é aplicada sobre a tabela como um todo. É possível filtrar dentro de grupos usando o argumento .by = "nome_do_grupo".
No código abaixo, novamente filtra-se os municípios com PIB acima da média. No segundo exemplo, contudo, este filtro é aplicado dentro de cada região, segundo a coluna/grupo name_region. A regra lógica pib > mean(pib) é aplicada dentro de cada região, isto é, filtra-se todos os municípios que têm PIB superior à média do PIB da sua região.
dat |> filter(pib > mean(pib))
dat |> filter(pib > mean(pib), .by = "name_region")Vale notar que a a sintaxe .by = "grupo" ainda está em fase experimental. Ela oferece um substituto mais sucinto à antiga sintaxe que usava a função group_by() com a vantagem de sempre aplicar a função ungroup() ao final do processo, isto é, o resultado final da função acima será uma tabela sem grupos. O código acima é equivalente ao código abaixo.
dat |>
group_by(name_region) |>
filter(pib > mean(pib)) |>
ungroup()Este outro exemplo enfatiza como o resultado da função filter muda quando é aplicada em diferentes grupos.
dat |> filter(pib == max(pib))
dat |> filter(pib == max(pib), .by = "name_state")| name_muni | abbrev_state | pib | pib_share_uf |
|---|---|---|---|
| Porto Velho | RO | 19.448.762 | 38 |
| Rio Branco | AC | 9.579.592 | 58 |
| Manaus | AM | 91.768.773 | 79 |
| Boa Vista | RR | 11.826.207 | 74 |
| Parauapebas | PA | 38.014.863 | 18 |
| Macapá | AP | 11.735.557 | 64 |
| Palmas | TO | 9.940.091 | 23 |
| São Luís | MA | 33.074.010 | 31 |
| Teresina | PI | 21.578.875 | 38 |
| Fortaleza | CE | 65.160.893 | 39 |
| Natal | RN | 22.729.773 | 32 |
| João Pessoa | PB | 20.766.551 | 30 |
| Recife | PE | 50.311.002 | 26 |
| Maceió | AL | 22.872.756 | 36 |
| Aracaju | SE | 16.447.105 | 36 |
| Salvador | BA | 58.938.115 | 19 |
| Belo Horizonte | MG | 97.509.893 | 14 |
| Vitória | ES | 25.473.898 | 18 |
| Rio de Janeiro | RJ | 331.279.902 | 44 |
| São Paulo | SP | 748.759.007 | 31 |
| Curitiba | PR | 88.308.728 | 18 |
| Joinville | SC | 36.391.912 | 10 |
| Porto Alegre | RS | 76.074.563 | 16 |
| Campo Grande | MS | 30.121.789 | 25 |
| Cuiabá | MT | 26.528.839 | 15 |
| Goiânia | GO | 51.961.311 | 23 |
| Brasília | DF | 265.847.334 | 100 |
if_any e if_all
A função filter não funciona em conjunção com a função across(). Esta função foi desenvolvida para funcionar apenas com mutate e summarise e aplica uma mesma regra/função sobre múltiplas colunas.
Já a função filter recebeu duas funções auxiliares: if_any e if_all. Elas seguem o mesmo padrão das funções base any e all. Estas funções servem para agregar condições lógicas. A função any, por exemplo, testa múltiplas condições lógicas e retorna um único TRUE se houver ao menos um TRUE entre as condições lógicas. Já a função all retorna um único TRUE se absolutamente todas as condições lógicas testadas também retornaram TRUE.
A função if_any aplica uma mesma regra em múltiplas colunas e retorna todas as linhas que atendem esta regra. No exemplo abaixo
dat |> filter(if_any(starts_with("pib"), ~ . > 100000))O exemplo seguinte é mais interessante. Neste caso, todas as variáveis numéricas da tabela são normalizadas (por região) e retorna-se apenas os municípios onde o valor de cada coluna é superior a 1. Como as variáveis estão normalizadas isto é equivalente a retornar os municípios que estão 1 desvio-padrão acima da média da sua região em todos os atributos numéricos considerados.
dat |>
select(-contains("code")) |>
select(where(~all(.x > 0))) |>
mutate(across(where(is.numeric), ~as.numeric(scale(log(.x)))), .by = "name_region") |>
filter(if_all(everything(), ~ . > 1))# A tibble: 2 × 15
name_muni name_state abbrev_state name_region population city_area
<chr> <chr> <chr> <chr> <dbl> <dbl>
1 Paranaguá Paraná PR Sul 2.32 1.13
2 São José dos Pinhais Paraná PR Sul 3.00 1.28
# ℹ 9 more variables: population_density <dbl>, households <dbl>,
# dwellers_per_household <dbl>, pib <dbl>, pib_taxes <dbl>,
# pib_added_value <dbl>, pib_industrial <dbl>, pib_services <dbl>,
# pib_govmt_services <dbl>
O último exemplo é similar ao anterior. As variáveis numéricas novamente são normalizadas mas desta vez busca-se somente os municípios que estão 3 desvios-padrão, acima da média do seu estado, ou na população ou no PIB.
dat |>
select(-contains("code")) |>
select(where(~all(.x > 0))) |>
mutate(across(where(is.numeric), ~as.numeric(scale(log(.x)))), .by = "name_state") |>
filter(if_any(c(population, pib), ~ . > 3))| name_muni | abbrev_state | population | pib | population_density | pib_services | city_area |
|---|---|---|---|---|---|---|
| Porto Velho | RO | 3,249 | 3,508 | 1,129 | 2,954 | 2,622 |
| Rio Branco | AC | 3,254 | 3,337 | 2,525 | 3,112 | 0,628 |
| Manaus | AM | 5,214 | 5,439 | 3,420 | 5,136 | -0,239 |
| Boa Vista | RR | 3,299 | 3,310 | 3,059 | 3,172 | -0,709 |
| Belém | PA | 4,041 | 3,410 | 3,122 | 3,670 | -0,672 |
| Canaã dos Carajás | PA | 0,950 | 3,151 | 0,511 | 2,260 | 0,069 |
| Parauapebas | PA | 2,305 | 3,582 | 0,816 | 2,795 | 0,602 |
| Araguaína | TO | 3,837 | 3,459 | 2,184 | 3,492 | 1,201 |
| Gurupi | TO | 3,062 | 2,857 | 2,255 | 3,018 | 0,381 |
| Palmas | TO | 4,467 | 4,166 | 3,269 | 4,073 | 0,580 |
| Balsas | MA | 2,045 | 3,190 | -0,898 | 3,117 | 2,899 |
| Imperatriz | MA | 3,236 | 3,599 | 2,353 | 3,654 | 0,373 |
| São José de Ribamar | MA | 3,103 | 2,412 | 4,270 | 2,681 | -1,894 |
| São Luís | MA | 4,845 | 5,096 | 4,541 | 4,903 | -0,581 |
| Parnaíba | PI | 3,676 | 3,413 | 3,580 | 3,577 | -0,564 |
| Picos | PI | 2,881 | 3,018 | 2,643 | 3,340 | -0,266 |
| Teresina | PI | 5,667 | 5,526 | 4,092 | 5,417 | 0,676 |
| Uruçuí | PI | 1,464 | 3,101 | -1,185 | 2,756 | 2,604 |
| Caucaia | CE | 3,041 | 2,955 | 1,882 | 2,814 | 0,878 |
| Fortaleza | CE | 5,212 | 4,963 | 5,137 | 4,922 | -0,641 |
| Maracanaú | CE | 2,569 | 3,239 | 3,900 | 3,146 | -1,851 |
| Parnamirim | RN | 3,428 | 3,265 | 4,138 | 3,371 | -0,674 |
| Mossoró | RN | 3,475 | 3,434 | 1,213 | 3,512 | 2,658 |
| Natal | RN | 4,538 | 4,421 | 4,968 | 4,488 | -0,323 |
| Cabedelo | PB | 2,206 | 3,090 | 3,771 | 3,125 | -2,247 |
| Campina Grande | PB | 4,163 | 4,287 | 2,668 | 4,093 | 1,407 |
| João Pessoa | PB | 4,893 | 4,951 | 4,328 | 4,721 | 0,137 |
| Santa Rita | PB | 3,070 | 3,029 | 1,490 | 2,768 | 1,645 |
| Ipojuca | PE | 1,478 | 3,168 | 0,696 | 2,718 | 0,530 |
| Jaboatão dos Guararapes | PE | 3,469 | 3,148 | 2,836 | 3,085 | -0,142 |
| Recife | PE | 4,360 | 4,261 | 3,673 | 4,202 | -0,303 |
| Arapiraca | AL | 3,021 | 2,913 | 3,084 | 3,187 | 0,645 |
| Maceió | AL | 4,588 | 4,318 | 4,478 | 4,537 | 1,190 |
| Aracaju | SE | 3,638 | 3,676 | 4,258 | 3,886 | -0,094 |
| Camaçari | BA | 3,357 | 4,191 | 2,547 | 3,774 | 0,001 |
| Feira de Santana | BA | 4,229 | 3,722 | 2,744 | 3,800 | 0,506 |
| Juazeiro | BA | 3,068 | 2,623 | 0,357 | 2,778 | 2,135 |
| Luís Eduardo Magalhães | BA | 2,123 | 3,040 | 0,108 | 3,046 | 1,628 |
| Salvador | BA | 5,882 | 4,927 | 4,579 | 5,007 | -0,122 |
| São Francisco do Conde | BA | 0,853 | 3,510 | 1,627 | 2,947 | -1,059 |
| Vitória da Conquista | BA | 3,615 | 3,054 | 1,439 | 3,230 | 1,414 |
| Belo Horizonte | MG | 5,103 | 4,613 | 5,077 | 4,620 | -0,186 |
| Betim | MG | 3,489 | 3,638 | 3,490 | 3,365 | -0,148 |
| Contagem | MG | 3,874 | 3,728 | 4,373 | 3,712 | -0,715 |
| Extrema | MG | 1,581 | 3,027 | 1,961 | 3,055 | -0,487 |
| Governador Valadares | MG | 3,049 | 2,630 | 1,342 | 2,768 | 1,768 |
| Ipatinga | MG | 2,935 | 3,004 | 3,619 | 2,862 | -0,882 |
| Juiz de Fora | MG | 3,744 | 3,312 | 2,450 | 3,412 | 1,280 |
| Montes Claros | MG | 3,495 | 2,900 | 1,386 | 2,955 | 2,195 |
| Nova Lima | MG | 2,270 | 3,072 | 2,118 | 2,792 | 0,073 |
| Ribeirão das Neves | MG | 3,282 | 2,320 | 4,007 | 2,358 | -0,944 |
| Uberaba | MG | 3,304 | 3,326 | 0,995 | 3,208 | 2,426 |
| Uberlândia | MG | 4,003 | 3,907 | 1,752 | 3,795 | 2,332 |
| Serra | ES | 3,040 | 2,923 | 2,726 | 2,891 | 0,284 |
| Rio de Janeiro | RJ | 3,591 | 3,414 | 2,301 | 3,286 | 1,393 |
| Guarulhos | SP | 3,055 | 2,881 | 2,875 | 2,802 | 0,155 |
| São Paulo | SP | 4,578 | 4,338 | 3,294 | 4,267 | 2,022 |
| Araucária | PR | 2,360 | 3,294 | 2,508 | 2,752 | 0,310 |
| Cascavel | PR | 3,108 | 3,016 | 1,810 | 2,989 | 2,310 |
| Curitiba | PR | 4,576 | 4,554 | 5,174 | 4,384 | 0,210 |
| Foz do Iguaçu | PR | 2,930 | 3,212 | 2,899 | 2,666 | 0,660 |
| Londrina | PR | 3,531 | 3,376 | 2,550 | 3,365 | 1,995 |
| Maringá | PR | 3,255 | 3,306 | 3,514 | 3,330 | 0,361 |
| Ponta Grossa | PR | 3,135 | 3,183 | 1,859 | 2,982 | 2,286 |
| São José dos Pinhais | PR | 3,058 | 3,385 | 2,585 | 3,081 | 1,249 |
| Florianópolis | SC | 3,322 | 3,046 | 2,472 | 3,052 | 1,339 |
| Itajaí | SC | 2,731 | 3,372 | 2,587 | 3,097 | 0,268 |
| Joinville | SC | 3,436 | 3,443 | 2,155 | 3,134 | 1,988 |
| Canoas | RS | 3,136 | 3,231 | 3,730 | 2,942 | -0,689 |
| Caxias do Sul | RS | 3,370 | 3,492 | 1,901 | 3,237 | 1,694 |
| Pelotas | RS | 3,083 | 2,722 | 1,637 | 2,718 | 1,669 |
| Porto Alegre | RS | 4,230 | 4,316 | 3,739 | 4,169 | 0,561 |
| Campo Grande | MS | 4,131 | 3,470 | 2,785 | 3,555 | 1,112 |
| Cuiabá | MT | 3,653 | 2,967 | 3,573 | 3,093 | 0,040 |
| Anápolis | GO | 3,070 | 3,060 | 2,746 | 3,038 | 0,151 |
| Aparecida de Goiânia | GO | 3,297 | 3,039 | 3,874 | 3,094 | -0,966 |
| Goiânia | GO | 4,110 | 3,976 | 3,908 | 4,062 | -0,081 |