• Fernanda Kelly

Função: across

Muitas vezes me encontro em uma situação em que preciso realizar a mesma operação em várias colunas em um grande conjunto de dados. Para isso, recorro a nada menos que a função across do universo tidyverse e, mais especificamente, do pacote dplyr. Mas, como veremos, não apenas a função across nos ajuda quando lidamos com dados interativamente, pois há outras funções que também operam perfeitamente no R com este fim. A seguir, mostrarei alguns casos de uso simples da função dplyr::across e, "de quebra", contemple uma das imagens SUPER legais da Allison Horst.





Como usar essa função?

Em princípio, vamos instalar os pacotes de interesse para bom acompanhemento desse post. Instale os pacote dplyr e datasets e carregue-os como é demostrado abaixo. (Dica: Utilize o pacote pacman como ferramenta de gerenciamento de pacotes no R que combina a funcionalidade das funções relacionadas à biblioteca base em funções intuitivamente nomeadas, reduzindo o código para executar simultaneamente várias ações.)


library(dplyr) 
library(datasets)

Vejamos o uso mais básico da função across. Para este post, usarei o conjunto de dados mtcars do pacote datasets. Esse banco de dados foi extraído da revista Motor Trend US de 1974 e abrangem o consumo de combustível e 10 aspectos do design e desempenho do automóvel para 32 automóveis (modelos de 1973 a 1974).


base::names(mtcars)
 #[1] "mpg"  "cyl"  "disp" "hp"   "drat" "wt"   "qsec" "vs"   "am"   "gear" "carb"

Outras informações em relação a este banco de dados você encontra com a segunte linha de comando no console:


base::help(mtcars)

Existem duas colunas relacionadas ao bom desempenho do automóvel: mpg e cyl. Quero obter rapidamente a média dessas colunas para cada categoria de automóvel que temos na base de dados. Primeiro, aqui está como eu poderia fazer isso sem a função across, mas utilizando outras funções do universo tidyverse:


datasets::mtcars %>%   
dplyr::group_by(am) %>%   
dplyr::summarise(mpg = mean(mpg, na.rm = TRUE),  cyl = mean(cyl, na.rm = TRUE))

O que funciona bem, logo se não houvesse as funções do pacote dplyr, faríamos da forma pré-histórica do R que é demonstrada a seguir.


base::mean(mpg, na.rm = TRUE)
base::mean(cyl, na.rm = TRUE)

Ou ainda utilizando de formas de loopings como o for para aplicação da função mean em cada coluna.


Mas imagine se em vez de duas colunas houvesse 10 ou 20 ou 100! Seria rapidamente tedioso e demorado adicionar uma nova linha para cada coluna. Aqui é onde a função dplyr::across entra para BRILHAR. Veja o exemplo abaixo.


datasets::mtcars %>%   
dplyr::group_by(am) %>%   
dplyr::summarise(across(c(mpg,cyl), mean, na.rm = TRUE))

Muito mais eficiente e simples né? Fornecemos ao dplyr::across um vetor de nomes de colunas de interesse seguido pela função (neste caso mean) e por quaisquer outros argumentos que queremos aplicar à função que estamos utilizando. Há outras formas mais sofisticadas de se fornecer as funções de interesse para o dplyr::across.


Usando ~ e .x?

Se quisermos fornecer uma função lambda, usamos o que chamamos de purrr sintaxe (Que eu acho bem complicada inclusive). Neste exemplo, digamos que queremos dividir cada categoria de câmbio pelo máximo de milhas por galão em cada categoria. Usaremos o ~ para indicar que estamos fornecendo uma função lambda e usaremos .x para indicar onde a variável dplyr::across é utilizada.


datasets::mtcars %>%   
dplyr::group_by(am) %>% 
dplyr::mutate(across(c(mpg, cyl), ~ .x / max(.x, na.rm = TRUE))) 

O output deve ser como o demostrado abaixo.


# A tibble: 32 x 11
# Groups:   am [2]
     mpg   cyl  disp    hp  drat    wt  qsec    vs    am  gear  carb
   <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
 1 0.619  0.75  160    110  3.9   2.62  16.5     0     1     4     4
 2 0.619  0.75  160    110  3.9   2.88  17.0     0     1     4     4
 3 0.673  0.5   108     93  3.85  2.32  18.6     1     1     4     1
 4 0.877  0.75  258    110  3.08  3.22  19.4     1     0     3     1
 5 0.766  1     360    175  3.15  3.44  17.0     0     0     3     2
 6 0.742  0.75  225    105  2.76  3.46  20.2     1     0     3     1
 7 0.586  1     360    245  3.21  3.57  15.8     0     0     3     4
 8 1      0.5   147.    62  3.69  3.19  20       1     0     4     2
 9 0.934  0.5   141.    95  3.92  3.15  22.9     1     0     4     2
10 0.787  0.75  168.   123  3.92  3.44  18.3     1     0     4     4


Parece mágica, mas é somente o universo tidyverse brilhando (e muito) em data wrangling. Agora imagina que é de seu interesse alterar o nome das novas colunas de uma forma eficaz e rápida também. Como fazer? Vamos para a próxima abordagem do post.


Alterando os nomes das colunas

E se quisermos alterar os nomes de nossas novas colunas? Podemos fazer isso usando o argumento/parâmetro .names.


datasets::mtcars %>%   
dplyr::group_by(am) %>% 
dplyr::mutate(across(c(mpg, cyl), ~ .x / max(.x, na.rm = TRUE), .names = "{col}_prop")) %>% 
dplyr::select(am, ends_with("prop"))

O argumento .names usa a chama glue sintaxe, em que qualquer coisa dentro das chaves é uma variável. No caso, o nome de cada coluna que fornecemos é substituído por {col}.


E se quisermos não apenas a média, mas o desvio padrão?

A função dplyr::across pode ter várias funções como uma lista. Veja aqui:


datasets::mtcars %>%
dplyr::group_by(am) %>%
dplyr::summarise(across(c(mpg, cyl), list(mean = mean, sd = sd), na.rm = TRUE, .names = "{col}_{fn}"))

O output deve ser como o demostrado abaixo.


# A tibble: 2 x 5
     am mpg_mean mpg_sd cyl_mean cyl_sd
  <dbl>    <dbl>  <dbl>    <dbl>  <dbl>
1     0     17.1   3.83     6.95   1.54
2     1     24.4   6.17     5.08   1.55

E se eu tiver muitas colunas?

Corresponder os nomes das colunas com tidyselect é uma opção vantajosa, então se tivermos muitas colunas para operar, pode ser complicado soletrar cada nome. Podemos aproveitar tidyselect auxiliares para corresponder as colunas por nome ou tipo. Continuando com nosso exemplo, vamos calcular novamente o valor médio de mpg e cyl por categoria de câmbio, mas usaremos ends_with para buscar colunas que terminam com a string "prop" (e há muitas outros argumentos (help(dplyr_tidy_select )).


datasets::mtcars %>%   
dplyr::group_by(am) %>% 
dplyr::mutate(across(c(mpg, cyl), ~ .x / max(.x, na.rm = TRUE), .names = "{col}_prop")) %>% 
dplyr::summarise(across(ends_with("prop"), sd, na.rm = TRUE, .names = "col}_"))

Da mesma forma, se quisermos fazer isso para qualquer coluna numérica nos dados, usamos where(is.numeric).


datasets::mtcars %>%   
dplyr::group_by(am) %>% 
dplyr::summarise(across(where(is.numeric), mean, na.rm = TRUE, .names = "{col}_{fn}"))

Usando across programaticamente (e dificilmente rs)

Os exemplos acima destacam por que a função across é tão útil nos fluxos de trabalho analíticos do dia-a-dia. Mas a verdadeira razão pela qual a função across é PERFEITA é que essa função torna a programação dplyr muito mais fácil e intuitiva. Aqui vamos literalmente "abraçar" a função across (e por "abraçar" quero dizer usar {{}}).


Neste exemplo, criaremos uma função que solicita que o usuário forneça qualquer número de colunas numéricas em seus dados, e a função calculará a média, o desvio padrão e os quantis de 0,05% a 95%. Também permitiremos que o usuário forneça uma variável de agrupamento, se desejar.


summarizer <- function(data, numeric_cols = NULL, ...) {
data %>%
group_by(...) %>%
summarise(across({{numeric_cols}}, list(
mean = ~mean(.x, na.rm = TRUE),
sd = ~sd(.x, na.rm = TRUE), 
q05 = ~quantile(.x, 0.05, na.rm = TRUE),
q95 = ~quantile(.x, 0.95, na.rm = TRUE)),
.names = "{col}_{fn}")) }

Agora testamos a função.


summarizer(mtcars, numeric_cols = c(mpg, cyl), am)

Espero que você possa apreciar a versatilidade que a função dplyr::across oferece. Ela não apenas reduz a digitação, mas pode tornar a programação muito mais fácil e sofisticada.


Fiquem bem e bons estudos :))

Posts recentes

Ver tudo