Transformando dados com across( )

Fazendo mais com menos.
Data de Publicação

8 de outubro de 2024

Dentre as inúmeras ferramentas disponíveis no tidyverse, a função across() pode ser um recurso de grande utilidade para simplificar transformações aplicadas em múltiplas variáveis.

Usando across( )

Quando estamos inicando na ciência de dados em R e conhecendo a sintaxe do tidyverse, é muito comum escrevermos códigos parecidos como estes:

library(tidyverse)

penguins <- palmerpenguins::penguins

penguins |> 
    summarise(
        mean_bill_length = mean(bill_length_mm, na.rm = TRUE),
        mean_bill_depth = mean(bill_depth_mm, na.rm = TRUE),
        mean_flipper_length = mean(flipper_length_mm, na.rm = TRUE),
        mean_body_mass = mean(body_mass_g, na.rm = TRUE)
    )
#> # A tibble: 1 × 4
#>   mean_bill_length mean_bill_depth mean_flipper_length mean_body_mass
#>              <dbl>           <dbl>               <dbl>          <dbl>
#> 1             43.9            17.2                201.          4202.

Neste caso, estamos computando a mesma função mean() em diversas colunas. Porém, há uma maneira mais eficiente de fazer esta mesma operação usando a função across(), onde apenas devemos selecionar as variáveis nas quais desejamos trabalhar no parâmetro .cols e a função desejada no parâmetro .fns que, neste caso, é a função de média, mean().

penguins |> 
    summarise(
        across(
            .cols = c(
                bill_length_mm, bill_depth_mm,
                flipper_length_mm, body_mass_g
            ),
            .fns = \(.x) mean(.x, na.rm = TRUE)
        )
    )
#> # A tibble: 1 × 4
#>   bill_length_mm bill_depth_mm flipper_length_mm body_mass_g
#>            <dbl>         <dbl>             <dbl>       <dbl>
#> 1           43.9          17.2              201.       4202.

Note que a função de média é aplicada em cada variável através da função anônima \(.x) mean(.x, na.rm = TRUE), onde \() equivale a function() e o .x corresponde as colunas listadas em .cols que serão utilizadas para calcular suas médias com mean().

across() também nos permite aplicar diversas funções ao mesmo tempo para as variáveis escolhidas em .cols. Para aplicar mais de uma função em across(), apenas é necessário inserir as funções num vetor, como vemos abaixo, calculamos a variâcia das variáveis em conjunto com suas médias no parâmetro .fns:

penguins |> 
    summarise(
        across(
            .cols = c(
                bill_length_mm, bill_depth_mm,
                flipper_length_mm, body_mass_g
            ),
            .fns = c(
                mean = \(.x) mean(.x, na.rm = TRUE),
                var = \(.x) mean(.x, na.rm = TRUE)
            )
        )
    )
#> # A tibble: 1 × 8
#>   bill_length_mm_mean bill_length_mm_var bill_depth_mm_mean bill_depth_mm_var
#>                 <dbl>              <dbl>              <dbl>             <dbl>
#> 1                43.9               43.9               17.2              17.2
#> # ℹ 4 more variables: flipper_length_mm_mean <dbl>,
#> #   flipper_length_mm_var <dbl>, body_mass_g_mean <dbl>, …

Obverse o output do código, onde cada variável resultou em duas variáveis, uma para cada estatística: média (mean()) e variância (var()). Infelizmente, o output deste código gera uma tibble em um formato largo (wide tibble), com oito variáveis e apenas uma observação, o que não é ideal para podermos trabalhar com ela. Felizmente, podemos usar mais um parâmetro do across() para facilitar nossas vidas, .names:

penguins |> 
    summarise(
        across(
            .cols = c(
                bill_length_mm, bill_depth_mm,
                flipper_length_mm, body_mass_g
            ),
            .fns = c(
                mean = \(.x) mean(.x, na.rm = TRUE),
                var = \(.x) mean(.x, na.rm = TRUE)
            ),
            .names = "{.col}----{.fn}"
        )
    ) 
#> # A tibble: 1 × 8
#>   `bill_length_mm----mean` `bill_length_mm----var` `bill_depth_mm----mean`
#>                      <dbl>                   <dbl>                   <dbl>
#> 1                     43.9                    43.9                    17.2
#> # ℹ 5 more variables: `bill_depth_mm----var` <dbl>,
#> #   `flipper_length_mm----mean` <dbl>, `flipper_length_mm----var` <dbl>, …

.names entra em cena para auxiliar nosso trabalho ao nos permitir modificar os nomes das variáveis inclusas em .cols. O parâmetro requer uma string, onde é possível adicionar {.col} para sinalizar o uso do nome da variável e {.fn} que indica o uso do nome da função (neste caso, ou mean, ou var). Entre eles, adicionei quatro traços ----, que servirá como separador e irá facilitar nossas vidas na transformação de uma wide tibble para uma long tibble, através do pivot_longer():

penguins |> 
    summarise(
        across(
            .cols = c(
                bill_length_mm, bill_depth_mm,
                flipper_length_mm, body_mass_g
            ),
            .fns = c(
                mean = \(.x) mean(.x, na.rm = TRUE),
                var = \(.x) mean(.x, na.rm = TRUE)
            ),
            .names = "{.col}----{.fn}"
        )
    ) |> 
    pivot_longer(
        cols = everything(),
        names_sep = "----",
        names_to = c("variable", "stat")
    )
#> # A tibble: 8 × 3
#>   variable          stat  value
#>   <chr>             <chr> <dbl>
#> 1 bill_length_mm    mean   43.9
#> 2 bill_length_mm    var    43.9
#> 3 bill_depth_mm     mean   17.2
#> 4 bill_depth_mm     var    17.2
#> 5 flipper_length_mm mean  201. 
#> 6 flipper_length_mm var   201. 
#> # ℹ 2 more rows

Assim, a tibble fica muito mais amigável para criação de novas variáveis e para trabalhar com ela.

Usando tidyselect helpers em conjunto com across( )

Os helpers tidyselect são um grupo de verbos do tidyverse disponibilizadas para a seleção de variáveis. São úteis para podermos lidar com tibbles que possuem um número expressivo de variáveis, evitando digitação de muito código. Por exemplo, a tibble abaixo possui 74 variáveis:

ames <- modeldata::ames

ames
#> # A tibble: 2,930 × 74
#>   MS_SubClass                    MS_Zoning       Lot_Frontage Lot_Area Street
#> * <fct>                          <fct>                  <dbl>    <int> <fct> 
#> 1 One_Story_1946_and_Newer_All_… Residential_Lo…          141    31770 Pave  
#> 2 One_Story_1946_and_Newer_All_… Residential_Hi…           80    11622 Pave  
#> 3 One_Story_1946_and_Newer_All_… Residential_Lo…           81    14267 Pave  
#> 4 One_Story_1946_and_Newer_All_… Residential_Lo…           93    11160 Pave  
#> 5 Two_Story_1946_and_Newer       Residential_Lo…           74    13830 Pave  
#> 6 Two_Story_1946_and_Newer       Residential_Lo…           78     9978 Pave  
#> # ℹ 2,924 more rows
#> # ℹ 69 more variables: Alley <fct>, Lot_Shape <fct>, Land_Contour <fct>, …

Digamos que queremos trabalhar com todas as variáveis numéricas do dataset ames. Ao invés de digitarmos todas as colunas, podemos utilizar os helpers tidyselect, como o where() em conjunto com is.numeric para selecionar todas as variáveis onde os resultados de is.numeric é igual a TRUE.

ames |> 
    summarise(
        across(
            .cols = where(is.numeric),
            .fns = c(
                mean = \(.x) mean(.x, na.rm = TRUE),
                var = \(.x) mean(.x, na.rm = TRUE)
            ),
            .names = "{.col}----{.fn}"
        )
    ) |> 
    pivot_longer(
        cols = everything(),
        names_sep = "----",
        names_to = c("variable", "stat")
    )
#> # A tibble: 68 × 3
#>   variable     stat    value
#>   <chr>        <chr>   <dbl>
#> 1 Lot_Frontage mean     57.6
#> 2 Lot_Frontage var      57.6
#> 3 Lot_Area     mean  10148. 
#> 4 Lot_Area     var   10148. 
#> 5 Year_Built   mean   1971. 
#> 6 Year_Built   var    1971. 
#> # ℹ 62 more rows

Também é possível usar mais helpers do tidyselect para filtrar ainda mais as colunas desejadas. Por exemplo, podemos combinar where(is.numeric) e starts_with("Lot") para aplicar transformações apenas para as variáveis numéricas que iniciam com o padrão “Lot”.

ames |> 
    summarise(
        across(
            .cols = where(is.numeric) & starts_with("Lot"),
            .fns = c(
                mean = \(.x) mean(.x, na.rm = TRUE),
                var = \(.x) mean(.x, na.rm = TRUE)
            ),
            .names = "{.col}----{.fn}"
        )
    ) |> 
    pivot_longer(
        cols = everything(),
        names_sep = "----",
        names_to = c("variable", "stat")
    )
#> # A tibble: 4 × 3
#>   variable     stat    value
#>   <chr>        <chr>   <dbl>
#> 1 Lot_Frontage mean     57.6
#> 2 Lot_Frontage var      57.6
#> 3 Lot_Area     mean  10148. 
#> 4 Lot_Area     var   10148.

Outras dicas

Podemos reescrever a função across() de diversas maneiras:

Também é possível utilizar o across() com mutate():

modeldata::ames |> 
    mutate(across(matches("(Y|y)ear"), ~ make_date(year = .x))) |> 
    select(where(is.Date))
#> # A tibble: 2,930 × 3
#>   Year_Built Year_Remod_Add Year_Sold 
#>   <date>     <date>         <date>    
#> 1 1960-01-01 1960-01-01     2010-01-01
#> 2 1961-01-01 1961-01-01     2010-01-01
#> 3 1958-01-01 1958-01-01     2010-01-01
#> 4 1968-01-01 1968-01-01     2010-01-01
#> 5 1997-01-01 1998-01-01     2010-01-01
#> 6 1998-01-01 1998-01-01     2010-01-01
#> # ℹ 2,924 more rows

Este é somente um exemplo dos mais diversos de como é possível combinar across() com mutate() ou summarise() para fazer bastante progresso em uma análise de dados, digitando bem menos e otimizando consideravelmente o código.