O novo tidyverse: mutate

Neste post ensino abordagens diferentes para criar colunas de maneira eficiente. Apresento também as novidades que o dplyr trouxe nos últimos anos como a função across e novo argumento .by.
data-science
tutorial-R
tidyverse
Author

Vinicius Oike

Published

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

mutate

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 mutate serve para criar novas colunas a partir de colunas pré-existentes. Ela segue a seguinte sintaxe:

mutate(coluna_nova = f(coluna_velha))

onde f() designa algum transformação que é feita sobre os dados antigos. Em geral, esta transformação é alguma operação matemática (+, -, log, etc.), transformação de classe (e.g. as.numeric), ou função em geral. Abaixo mostra-se alguns exemplos de transformações simples. Como as saídas ocupam muito espaço, vou omiti-las.

A primeira linha cria uma coluna onde todas as entradas são iguais a 1. O segundo exemplo aplica a função log sobre a variável pib. O terceiro exemplo divide a coluna household por um milhão. Por fim, o quarto exemplo mostra como dropar uma coluna usando a função mutate.

#> 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)
#> Dropa a coluna pib
mutate(tbl, pib = NULL)

Vale notar que, assim como a função filter é mais eficiente juntar todas as operações dentro de um único mutate:

tbl |> 
  mutate(
    id = 1,
    lpib = log(pib),
    hh = household / 1e6,
    pib = NULL
  )

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; similarmente, pode-se criar a coluna lpibs a partir de pibserv, que é a soma de pib_services e pib_govmt_services

tbl |> 
  mutate(
    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,
    #> Criando uma variável a partir de duas colunas criadas anteriormente
    lpibs = log(pibserv)
  )

Grupos

A expressão mutate sempre é aplicada dentro de grupos. No caso em que não existe um grupo, a expressão é aplicada para todos os dados disponíveis. O código abaixo, por exemplo, encontra a participação percentual do PIB de cada município no PIB brasileiro.

tbl |> 
  select(name_muni, abbrev_state, pib) |> 
  mutate(pib_share = pib / sum(pib) * 100) |> 
  arrange(desc(pib_share))
# A tibble: 5,570 × 4
   name_muni      abbrev_state       pib pib_share
   <chr>          <chr>            <dbl>     <dbl>
 1 São Paulo      SP           748759007     9.84 
 2 Rio de Janeiro RJ           331279902     4.35 
 3 Brasília       DF           265847334     3.49 
 4 Belo Horizonte MG            97509893     1.28 
 5 Manaus         AM            91768773     1.21 
 6 Curitiba       PR            88308728     1.16 
 7 Osasco         SP            76311814     1.00 
 8 Porto Alegre   RS            76074563     1.00 
 9 Guarulhos      SP            65849311     0.865
10 Campinas       SP            65419717     0.860
# ℹ 5,560 more rows

Já este segundo código encontra a participação percentual do PIB de cada município dentro do seu respectivo estado.

tbl |> 
  select(name_muni, abbrev_state, pib) |> 
  mutate(pib_share = pib / sum(pib) * 100, .by = "abbrev_state") |> 
  arrange(desc(pib_share))
# A tibble: 5,570 × 4
   name_muni      abbrev_state       pib pib_share
   <chr>          <chr>            <dbl>     <dbl>
 1 Brasília       DF           265847334     100  
 2 Manaus         AM            91768773      79.1
 3 Boa Vista      RR            11826207      73.8
 4 Macapá         AP            11735557      63.5
 5 Rio Branco     AC             9579592      58.1
 6 Rio de Janeiro RJ           331279902      43.9
 7 Fortaleza      CE            65160893      39.0
 8 Teresina       PI            21578875      38.3
 9 Porto Velho    RO            19448762      37.7
10 Aracaju        SE            16447105      36.2
# ℹ 5,560 more rows

Vale notar que a sintaxe .by = "coluna" é nova e ainda está em fase experimental. Ela substitui a sintaxe mais antiga do group_by. O código acima é equivalente ao código abaixo.

tbl |> 
  select(name_muni, abbrev_state, pib) |> 
  group_by(abbre_state) |> 
  mutate(pib_share = pib / sum(pib) * 100) |> 
  ungroup()

Uma das vantagens de usar .by é que não é necessário usar ungroup já que os dados são desagrupados automaticamente.

Transformando múltiplas colunas

A função mutate tem um par importante na função across, que permite aplicar uma mesma função a múltiplas colunas com facilidade. Imagine o seguinte caso, onde quer-se aplicar a função scale, que serve para “normalizar” vetores numéricos, em todas as colunas de uma base. Tipicamente, seria necessário escrever e nomear cada coluna

tbl |> 
  mutate(
    scaled_pib = scale(pib),
    scaled_pop = scale(population),
    scaled_agriculture = scale(pib_agriculture),
    scaled_industrial = scale(pib_industrial),
    ...
  )

Em linhas gerais, o resultado do código acima pode ser replicado simplesmente com:

tbl |> 
  mutate(
    across(where(is.numeric), scale)
  )

A função across serve para aplicar uma função sobre um subconjunto de colunas seguindo: across(colunas, funcao). Ela funciona com os tidyselectors1, facilitando a seleção de colunas a ser transformadas. Funções mais complexas podem ser utilizadas via função anônima usando o operador ~2.

O primeiro exemplo abaixo mostra como aplicar a função log em todas as colunas cujo nome começa com pib. Já o segundo exemplo mostra como converter todas as colunas do tipo character para factor. O terceiro exemplo mostra como converter as colunas de factor para numeric utilizando o operador ~. Os últimos dois exemplos mostram outras aplicações do mesmo operador.

#> Aplica uma transformação log em todas as colunas que começam com pib
mutate(tbl, across(starts_with("pib"), log))
#> Converte todas as colunas de strings para factors
mutate(tbl, across(where(is.character), as.factor))
#> Converte as colunas de factors para numeric
mutate(tbl, across(where(is.factor), ~ as.numeric(as.character(.x))))
#> 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))
#> Normaliza todas as colunas numéricas
mutate(tbl, across(where(is.numeric), ~ as.numeric(scale(.x))))

Por fim, existe um argumento opcional .names que permite renomear as novas colunas usando uma sintaxe estilo glue3. Esta sintaxe tem dois tipos especiais importantes: {.col}, que faz referência ao nome original da coluna, e {.fn}, que faz referência ao nome da função utilizada. O exemplo abaixo refina o primeiro caso que vimos acima. Agora aplica-se a função as.numeric(scale(x)) sobre cada uma das colunas numéricas. As novas colunas têm o nome "scaled_NomeOriginalDaColuna".

tbl |> 
  mutate(
    across(
      where(is.numeric),
      ~ as.numeric(scale(.x)),
      .names = "scaled_{.col}"
      )
  )

O tipo especial {.fn} é bastante útil com a função summarise, que permite aplicar uma lista de múltiplas funções simultaneamente. Ainda assim, é possível utilizá-lo com a função mutate. A sintaxe tem de ser adaptada, pois {.fn} espera que a função tenha sido passada como uma lista com nomes. No exemplo abaixo, aplica-se a função log sobre todas as colunas númericas e as colunas resultantes são renomeadas. Vale notar que, na maioria dos casos, não vale a pena utilizar {.fn} no contexto do mutate.

tbl |> 
  mutate(
    across(
      where(is.numeric),
      list("ln" = log),
      .names = "{.fn}_{.col}"
      )
  )

Outros argumentos

A função mutate tem alguns outros argumentos, de uso diverso. Os argumentos .before e .after permitem selecionar a posição das novas colunas. O padrão da função é de sempre adicionar as novas colunas ao final do data.frame. Estes argumentos aceitam o nome de alguma das colunas ou mesmo funções tidyselect. No caso abaixo, cria-se a coluna lpib que é posta no início do data.frame.

tbl |> 
  mutate(
    lpib = log(pib),
    .before = everything()
  ) |> 
  select(1:5)
# A tibble: 5,570 × 5
    lpib code_muni name_muni             code_state name_state
   <dbl>     <dbl> <chr>                      <dbl> <chr>     
 1  13.3   1100015 Alta Floresta D'Oeste         11 Rondônia  
 2  14.9   1100023 Ariquemes                     11 Rondônia  
 3  12.0   1100031 Cabixi                        11 Rondônia  
 4  14.7   1100049 Cacoal                        11 Rondônia  
 5  13.3   1100056 Cerejeiras                    11 Rondônia  
 6  12.8   1100064 Colorado do Oeste             11 Rondônia  
 7  12.5   1100072 Corumbiara                    11 Rondônia  
 8  12.5   1100080 Costa Marques                 11 Rondônia  
 9  13.4   1100098 Espigão D'Oeste               11 Rondônia  
10  13.8   1100106 Guajará-Mirim                 11 Rondônia  
# ℹ 5,560 more rows

O outro argumento opcional é o .keep que permite controlar quais colunas devem ser preservadas após a aplicação da função mutate. O padrão da função, naturalmente, é de preservar todas as colunas, isto é, .keep = "all". Contudo, pode-se usar .keep = "used" para manter somente as colunas que foram utilizadas.

tbl |> 
  mutate(
    code_muni = as.character(code_muni),
    lpib = log(pib),
    .keep = "used"
    )
# A tibble: 5,570 × 3
   code_muni     pib  lpib
   <chr>       <dbl> <dbl>
 1 1100015    570272  13.3
 2 1100023   2818049  14.9
 3 1100031    167190  12.0
 4 1100049   2519353  14.7
 5 1100056    600670  13.3
 6 1100064    366931  12.8
 7 1100072    268381  12.5
 8 1100080    261978  12.5
 9 1100098    666331  13.4
10 1100106    984586  13.8
# ℹ 5,560 more rows

Vale notar que .keep = "used" sempre preserva as colunas “agrupadoras”.

tbl |> 
  mutate(
    code_muni = as.character(code_muni),
    lpib = log(pib),
    .by = "code_state",
    .keep = "used"
    )
# A tibble: 5,570 × 4
   code_muni code_state     pib  lpib
   <chr>          <dbl>   <dbl> <dbl>
 1 1100015           11  570272  13.3
 2 1100023           11 2818049  14.9
 3 1100031           11  167190  12.0
 4 1100049           11 2519353  14.7
 5 1100056           11  600670  13.3
 6 1100064           11  366931  12.8
 7 1100072           11  268381  12.5
 8 1100080           11  261978  12.5
 9 1100098           11  666331  13.4
10 1100106           11  984586  13.8
# ℹ 5,560 more rows

Funções úteis

Abaixo segue uma lista de funções úteis.

tbl |> 
  mutate(
    #> Cria um ranking da variável
    rank_pib = rank(pib),
    #> Cria um rakning (em percentil) da variável
    rank_perc_pib = percent_rank(pib),
    #> Agrupa em decis 
    group_decile_pib = ntile(pib, 10),
    #> Cria um id
    id = row_number(),
    #> Aplica uma transformação condicional a uma condição lógica
    lpib = ifelse(pib > 0, log(pib), 1),
    #> Aplica uma transformação condicional a múltiplas condições lógicas
    type = case_when(
      code_state %in% c(11, 12, 13) ~ "grupo_1",
      code_state %in% c(14, 15, 16) ~ "grupo_2",
      TRUE ~ "outros"
    ),
    #> Soma cumulativa
    spib = cumsum(pib),
    #> Diferença percentual usando o valor imediatamente anterior
    diff_pib = pib / lag(pib) - 1,
    #> Participação relativa da variável
    share_pib = pib / sum(pib, na.rm = TRUE) * 100,
    #> Normalizar variável
    scaled_pib = as.numeric(scale(pib))
  )

Outros posts da série

Footnotes

  1. Para mais detalhes sobre os tidyselectors veja o post sobre a função select.↩︎

  2. A função across foi uma mudança significativa de paradigma na evolução do dplyr. Esta função tornou obsoletas diversas funções que eram distinguidas pelos sufixos (_at, _all, _if). Para mais detalhes veja o post do blog do dplyr.↩︎

  3. Do pacote glue. Veja mais aqui.↩︎