O novo tidyverse: select

Neste post ensino abordagens diferentes para selecionar colunas de maneira eficiente. Apresento também as novidades que o dplyr trouxe nos últimos anos como as funções tidyselectors, que ajudam a selecionar colunas com base em padrões lógicos.
data-science
tutorial-R
tidyverse
Author

Vinicius Oike

Published

January 6, 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.

select

A função select serve para selecionar colunas e reduzir a complexidade de uma tabela. Esta função mudou consideravelmente nos últimos anos após a criação de algumas funções auxiliares conhecidas como selection helpers, que fazem parte do tidyselect. Estas funções poderosas permitem selecionar colunas usando regras lógicas e segundo padrões de texto; isto facilita consideravelmente a tarefa de limpeza de dados. Além disso, a lógica do tidyselect foi extendida para outros pacotes como tidymodels, gt, entre outros.

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.

tbl <- readr::read_csv(
  "https://github.com/viniciusoike/restateinsight/raw/main/static/data/cities_brazil.csv"
  )

A função select serve para selecionar colunas. Adicionalmente, ela também pode renomear as colunas selecionadas. A sintaxe da função é a seguinte

select(dados, coluna1, coluna2, nome_coluna = coluna3)

O código abaixo seleciona três colunas: name_muni, population, pib.

select(tbl, name_muni, population, pib)
# A tibble: 5,570 × 3
   name_muni             population     pib
   <chr>                      <dbl>   <dbl>
 1 Alta Floresta D'Oeste      21495  570272
 2 Ariquemes                  96833 2818049
 3 Cabixi                      5363  167190
 4 Cacoal                     86895 2519353
 5 Cerejeiras                 15890  600670
 6 Colorado do Oeste          15663  366931
 7 Corumbiara                  7519  268381
 8 Costa Marques              12627  261978
 9 Espigão D'Oeste            29397  666331
10 Guajará-Mirim              39386  984586
# ℹ 5,560 more rows

Pode-se selecionar colunas de três maneiras gerais: (1) como expressões (escrevendo o nome delas como se elas fossem objetos); (2) strings; (3) índices (que indicam a sua posição na tabela).

# Selecionando colunas de modo geral
select(tbl, name_muni, population, pib)

# Selecionando colunas usando strings
select(tbl, "name_muni", "population", "pib")
# Selecionando colunas usando vetor de texto
sel_cols <- c("name_muni", "population", "pib")
select(tbl, sel_cols)

# Selecionando colunas usando índices

# Usando índices explicitamente
select(tbl, 2, 8, 15)
# Usando um vetor numérico

# Encontra a posição de todas as colunas que começam com 'pib'
inds <- grep("^pib_", names(tbl))
select(tbl, inds)

Para remover uma coluna usa-se o sinal de menos (-) ou o operador lógico de negação (!)1.

select(tbl, !name_muni)
select(tbl, -name_muni)

De modo geral, para facilitar a seleção de colunas, pode-se usar os operadores lógicos convencionais (&, |, !). Por fim, existe também o operador : que serve para selecionar colunas contíguas.

select(tbl, code_muni:name_region)

Tidyselectors

Básico

Existe um conjunto de funções auxiliares que facilita a seleção de colunas. Estas funções retornam índices a partir de alguma regra. Isto é, elas permitem selecionar colunas com base em algum padrão. O caso mais geral é da função matches, que seleciona colunas com base em um regex.

# Seleciona as colunas que começam com 'pib_'
select(tbl, matches("^pib_"))
# Seleciona as colunas que terminam com 'muni'
select(tbl, matches("muni$"))
# Seleciona as colunas que contém o termo '_share'
select(tbl, matches("_share"))

A função matches retorna colunas a partir de algum padrão de texto no nome da coluna. Na linha da filosofia do tidyverse, de transformar tarefas rotineiras em funções específicas e com nomes “intuitivos” há uma série de funções auxiliares que imitam a função matches:

  • starts_with() - seleciona colunas que começam com algum string

  • ends_with() - seleciona colunas que terminam com algum string

  • contains() - seleciona colunas que contêm algum string

Isto é, podemos reescrever os códigos acima da seguinte maneira

# Seleciona as colunas que começam com 'pib_'
select(tbl, starts_with("pib_"))
# Seleciona as colunas que terminam com 'muni'
select(tbl, ends_with("muni"))
# Seleciona as colunas que contém o termo '_share'
select(tbl, contains("_share"))

all_of e any_of

Como visto acima, pode-se selecionar colunas com base em um vetor de texto. Nos casos em que é necessário maior controle sobre a seleção, há duas funções auxiliares: any_of e all_of. A primeira função faz o match entre o vetor de texto e o nome das colunas e retorna todos os casos positivos; já a segunda função faz o match entre o vetor de texto e o nome das colunas e retorna um resultado somente no caso de todos os matches terem sucesso.

Estas funções também são úteis para evitar potenciais ambiguidades entre o nome de objetos criados com o nome de colunas.

A diferença entre as funções fica mais evidente num exemplo. Considere o caso em que colocamos uma coluna adicional pib_per_capita que não existe na base de dados. Quando se usa a função all_of retorna-se todas as colunas onde o match teve sucesso.

sel_cols <- c("code_muni", "name_muni", "population", "pib", "pib_per_capita")

select(tbl, any_of(sel_cols))
# A tibble: 5,570 × 4
   code_muni name_muni             population     pib
       <dbl> <chr>                      <dbl>   <dbl>
 1   1100015 Alta Floresta D'Oeste      21495  570272
 2   1100023 Ariquemes                  96833 2818049
 3   1100031 Cabixi                      5363  167190
 4   1100049 Cacoal                     86895 2519353
 5   1100056 Cerejeiras                 15890  600670
 6   1100064 Colorado do Oeste          15663  366931
 7   1100072 Corumbiara                  7519  268381
 8   1100080 Costa Marques              12627  261978
 9   1100098 Espigão D'Oeste            29397  666331
10   1100106 Guajará-Mirim              39386  984586
# ℹ 5,560 more rows

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

select(tbl, all_of(sel_cols))
Error in `all_of()`:
! Can't subset columns that don't exist.
✖ Column `pib_per_capita` doesn't exist.

Note que, neste caso, isto é equivalente a simplesmente usar o vetor. Esta sintaxe, contudo, não é recomendado e futuramente será descontinuada.

select(tbl, sel_cols)
Error in `select()`:
! Can't subset columns that don't exist.
✖ Column `pib_per_capita` doesn't exist.

Conflitos de nomes

A função select faz um bom trabalho em resolver situações onde há alguma ambiguidade sobre qual o environment em que se deve avaliar uma expressão. O uso de all_of e any_of é recomendado justamente para evitar potenciais ambiguidades. Vale tirar um tempo para entender os exemplos abaixo.

Note que no primeiro caso, a função ncol é aplicada sobre x dentro da função select. A função ncol é avaliada no environment geral, isto é, ela considera x como o data.frame criado no espaço geral e não como uma coluna específica.

No segundo caso, a expressão y dentro da função select é interpretada como uma data-expression, isto é, como uma expressão que se refere ao nome de coluna do data.frame x.

No último caso, a função all_of indica que a expressão y deve ser avaliada como uma env-expression, isto é, como uma variável no environment geral, como o vetor de texto criado anteriormente.

x <- data.frame(x = 1, y = 2)
y <- c("x", "y")

# Retorna as duas colunas, 'x', 'y'
select(x, 1:ncol(x))
# Retorna a segunda coluna, 'y'
select(x, y)
# Retorna as duas colunas, 'x', 'y'
select(x, all_of(y))

Helpers de posição

Há também algumas funções auxilares mais gerais:

  • everything() - seleciona todas as colunas

  • last_col() - seleciona a última coluna

  • group_cols() - seleciona todas as colunas que compõem o group.

A função everything() tem um comportamento particular quando combinada com outras colunas. A função seleciona todas as colunas, exceto as que foram explicitamente chamadas. Isto facilita bastante o trabalho de rearranjar as colunas dentro de uma mesma base de dados.

# Coloca as colunas pib e pib_industrial na frente das demais colunas
select(tbl, pib, pib_industrial, everything())
# Dropa todas as variáveis
select(tbl, -everything())

Helpers de tipo

Por fim, pode-se selecionar as colunas pela sua classe. Vale lembrar que num data.frame cada coluna tem uma classe específica. Os exemplos abaixo mostram os casos de aplicação mais simples.

# Seleciona todas as colunas numéricas
select(tbl, where(is.numeric))
# Seleciona todas as colunas tipo character
select(tbl, where(is.character))
# Seleciona todas as colunas tipo factor
select(tbl, where(is.factor))
# Selciona todas as colunas lógicas (i.e. TRUE, FALSE, NA)
select(tbl, where(is.logical))
# Seleciona todas as colunas
select(tbl, where(is.Date))

Essencialmente, o que o código acima faz é aplicar a função selecionada em cada uma das colunas e retornar os casos positivos. É possível criar condições lógicas mais complexas com auxílio do operador ~ (tilde). Os exemplos abaixo mostram como dropar todas as colunas que contém somente NA e selecionar as colunas com datas (Date).

# eval: true
dat <- tibble(
  lgl = NA,
  missing = NA,
  lglT = sample(c(TRUE, FALSE), size = 10, replace = TRUE),
  dia_mes = seq(as.Date("2000-01-01"), by = "month", length.out = 10),
  val = rnorm(10)
)

# "Remove" as colunas que contêm somente NA
select(dat, !where(~all(is.na(.x))))
# Seleciona apenas as colunas de data
select(dat, where(~all(inherits(.x, "Date"))))

Resumindo

Em resumo, temos quatro grupos gerais de tidyselectors.

  1. Seleção com base num padrão de texto. (matches, starts_with, ends_with, contains)
  2. Seleção com base num vetor de texto. (all_of, any_of)
  3. Seleção com base na “posição”. (last_col, everything, group_cols)
  4. Seleção com base na “classe”, i.e., numa função que retorna um valor lógico. (where)

Estas funções auxiliares são muito importantes pois elas funcionam não somente com o select mas também com outras funções do pacote dplyr como mutate, rename, summarise e outras. Estas funções são relativamente recentes e marcam uma mudança considerável em relação às versões <1 do dplyr, que utilizam sufixos (all, if, at) para diferenciar as funções como select_if, ou mutate_at.

Outros posts da série

Veja também

Footnotes

  1. Apesar de ambas as opções serem válidas, recomenda-se utilizar os operadores booleanos ao invés do opreadores de conjuntos. Isto é, deve-se usar ! ao invés de -.↩︎