O novo tidyverse: filter

Neste post ensino abordagens diferentes para filtrar linhas de uma tabela de maneira eficiente. Apresento também algumas das inovações que o pacote dplyr lançou nos últimos anos como as funções auxiliares if_any e if_all.
data-science
tutorial-R
tidyverse
Author

Vinicius Oike

Published

January 10, 2024

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.

library(dplyr)
library(readr)

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
  population_density households dwellers_per_household   pib pib_taxes
               <dbl>      <dbl>                  <dbl> <dbl>     <dbl>
1               1.52       2.24                   2.25  2.80      2.81
2               2.10       2.97                   1.04  3.27      3.31
  pib_added_value pib_industrial pib_services pib_govmt_services
            <dbl>          <dbl>        <dbl>              <dbl>
1            2.76           2.36         2.71               2.57
2            3.18           2.76         2.92               3.17

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

Outros posts da série