En muchas situaciones nos gustaría usar una función de dplyr en varias columnas a la vez. Por ejemplo, si queremos pasar todas las variables numéricas a la vez usando la función mutate(), o si queremos proporcionar el mismo summary para tres de nuestras variables. A pesar de que ya es posible realizar este tipo de tareas utilizando llamadas de alcance, como mutate_all(), select_if(), summarise_at(), ahora es aún más fácil, y más coherente, utilizando la nueva función across() de dplyr. Para expicar across(), usaré el el dataset starwars.

Cargando tidyverse

library(tidyverse)
starwars$height <- as.numeric(starwars$height)

glimpse(starwars)
## Rows: 87
## Columns: 14
## $ name       <chr> "Luke Skywalker", "C-3PO", "R2-D2", "Darth Vader", "Leia...
## $ height     <dbl> 172, 167, 96, 202, 150, 178, 165, 97, 183, 182, 188, 180...
## $ mass       <dbl> 77.0, 75.0, 32.0, 136.0, 49.0, 120.0, 75.0, 32.0, 84.0, ...
## $ hair_color <chr> "blond", NA, NA, "none", "brown", "brown, grey", "brown"...
## $ skin_color <chr> "fair", "gold", "white, blue", "white", "light", "light"...
## $ eye_color  <chr> "blue", "yellow", "red", "yellow", "brown", "blue", "blu...
## $ birth_year <dbl> 19.0, 112.0, 33.0, 41.9, 19.0, 52.0, 47.0, NA, 24.0, 57....
## $ sex        <chr> "male", "none", "none", "male", "female", "male", "femal...
## $ gender     <chr> "masculine", "masculine", "masculine", "masculine", "fem...
## $ homeworld  <chr> "Tatooine", "Tatooine", "Naboo", "Tatooine", "Alderaan",...
## $ species    <chr> "Human", "Droid", "Droid", "Human", "Human", "Human", "H...
## $ films      <list> [<"The Empire Strikes Back", "Revenge of the Sith", "Re...
## $ vehicles   <list> [<"Snowspeeder", "Imperial Speeder Bike">, <>, <>, <>, ...
## $ starships  <list> [<"X-wing", "Imperial shuttle">, <>, <>, "TIE Advanced ...

Normalmente, si queremos hacer un sumario de una variable categórica como species, calculando el número de entradas diferentes, (usando n_distinct()) que contiene, deberíamos escribir:

starwars %>%
  summarise(distinct_species = n_distinct(species))
## # A tibble: 1 x 1
##   distinct_species
##              <int>
## 1               38

Si quisiéramos calcular n_distinct(), no solo para diferentes species, sino también entre mundos de origen (homeworld) y color de piel (skin_color), necesitaríamos escribir la función n_distinct() 3 veces por separado, dentro de summarise:

starwars%>%
summarise(distinct_species = n_distinct(species),
          distinct_homeworld = n_distinct(homeworld),
          distinct_skin_color = n_distinct(skin_color))
## # A tibble: 1 x 3
##   distinct_species distinct_homeworld distinct_skin_color
##              <int>              <int>               <int>
## 1               38                 49                  31

Podría ser bueno pues indicar a qué columnas queremos aplicar n_distinct() y luego especificar n_distinct() una vez, en lugar de tener que aplicar n_distinct a cada columna por separado

Aquí es donde entra across(), ya que se utiliza dentro de su función dplyr, y la sintaxis es across(.cols, .fnd), donde .cols especifica las columnas sobre las que desea que actúe la función de dplyr.

Cuando las funciones de dplyr involucran funciones externas que se están aplicando a columnas, p.ex. n_distinct() como en el ejemplo anterior, esta función externa se coloca en el argumento .fnd. Por ejemplo, aplicaríamos n_distinct() a species, a homeworld y a skin_color, escribiríamos (across(c(species,homeworld,skin_color), n_distinct)) dentro de las paréntesis de summarise().
Tener en cuenta que estamos especificando qué variables queremos involucrar en summarise() usando c(), como si estuviéramos listando los nombres de las variables en un vector, pero como estamos en dplyr, no necesitamos ponerlos entre comillas:

starwars %>%
  summarise(across(c(species,homeworld,skin_color), 
                   n_distinct))
## # A tibble: 1 x 3
##   species homeworld skin_color
##     <int>     <int>      <int>
## 1      38        49         31

Cabe destacar que la función n_distinct() es un argumento de across(), en lugar de ser un argumento de la función dplyr summarise().

Seleccionar soportes: la selección de columnas para aplicar la función

Hasta ahora, hemos visto cómo aplicar una función dplyr a un conjunto de columnas usando una notación vectorial c(col1, col2, col3, …). Sin embargo, hay muchas otras formas de especificar las columnas a las que desea aplicar la función dplyr.
everything(): aplica la función a todas las columnas.

starwars %>%
  summarise(across(everything(), n_distinct))
## # A tibble: 1 x 14
##    name height  mass hair_color skin_color eye_color birth_year   sex gender
##   <int>  <int> <int>      <int>      <int>     <int>      <int> <int>  <int>
## 1    87     46    39         13         31        15         37     5      3
## # ... with 5 more variables: homeworld <int>, species <int>, films <int>,
## #   vehicles <int>, starships <int>

starts_with(): aplica la función a todas las columnas cuyo nombre empiece con la cadena especificada:

starwars %>%
    summarise(across(starts_with("home"), n_distinct))
## # A tibble: 1 x 1
##   homeworld
##       <int>
## 1        49

contains(): aplica la función a todas las columnas cuyo nombre contenga la cadena especificada.

starwars %>%
  summarise(across(contains("color"), n_distinct))
## # A tibble: 1 x 3
##   hair_color skin_color eye_color
##        <int>      <int>     <int>
## 1         13         31        15

where() aplica la función a todas las columnas que satisfagan una condición lógica como, por ejemplo, !is.numeric().

starwars %>%
    summarise(across(where(is.numeric), ~sum(!is.na(.))))
## # A tibble: 1 x 3
##   height  mass birth_year
##    <int> <int>      <int>
## 1     81    59         43

Notar que hemos tenido que insertar un tilde “~” delante de sum porque “is.na()” no es una función admitida dentro de across(). El punto entre paréntesis indica “todas las columnas”.

En este mismo sentido, para identificar cuántos valores faltantes hay en cada columna, podríamos especificar la función ~ sum (is.na (.)), que calcula cuántos valores NA hay en cada columna (donde la columna está representada por “.”) y los agrega:

starwars %>%
  summarise(across(everything(), 
                   ~sum(is.na(.))))
## # A tibble: 1 x 14
##    name height  mass hair_color skin_color eye_color birth_year   sex gender
##   <int>  <int> <int>      <int>      <int>     <int>      <int> <int>  <int>
## 1     0      6    28          5          0         0         44     4      4
## # ... with 5 more variables: homeworld <int>, species <int>, films <int>,
## #   vehicles <int>, starships <int>

Con mutate()

¿Qué pasa si queremos reemplazar los valores faltantes en las columnas numéricas con 0? Sin la función across(), aplicaríamos una función if_else () por separado a cada columna numérica, que reemplazaría todos los valores NA con 0 y dejaría todos los valores que no sean NA como están:

# 1. Solución sin across() i sin usar una función.
starwars%>%
  mutate(height = if_else(is.na(height), 0, height)) %>%
  mutate(mass = if_else(is.na(mass),0, mass)) %>%
  mutate(birth_year = if_else(is.na(birth_year),0, birth_year)) %>%
  summarise(height_m = mean(height),
            mass_m = mean(mass),
            b_year_m = mean(birth_year))
## # A tibble: 1 x 3
##   height_m mass_m b_year_m
##      <dbl>  <dbl>    <dbl>
## 1     162.   66.0     43.3
na_repl_0 <- function(x) {
  if_else(condition = is.na(x), 
          true = 0, 
          false = (x))
}

# 2. Sin across y con función.
starwars %>%
    mutate(height = na_repl_0(height),
         mass = na_repl_0(mass),
         birth_year = na_repl_0(birth_year))%>%
    summarise(height_m2 = mean(height),
              mass_m2 = mean(mass),
              b_year_m2 = mean(birth_year))
## # A tibble: 1 x 3
##   height_m2 mass_m2 b_year_m2
##       <dbl>   <dbl>     <dbl>
## 1      162.    66.0      43.3

Por suerte, esto ya lo podemos hacer con across():

# 3. Con across() y con función.
starwars %>%
  mutate(across(where(is.numeric), na_repl_0)) %>%
    summarise(across(where(is.numeric), mean))
## # A tibble: 1 x 3
##   height  mass birth_year
##    <dbl> <dbl>      <dbl>
## 1   162.  66.0       43.3

Obviamente, 0 no es una gran opción, entonces quizás podamos reemplazar los valores faltantes con el valor medio de la columna. Esta vez, en lugar de definir una nueva función (en lugar de na_repl_0(), seremos un poco más concisos y usaremos la notación de tilde-punto para especificar la función que queremos aplicar.

starwars %>%
  mutate(across(where(is.numeric),
                ~if_else(is.na(.),
                         mean(., na.rm = T),
                         as.numeric(.)))) %>%
  summarise(across(where(is.numeric), mean))
## # A tibble: 1 x 3
##   height  mass birth_year
##    <dbl> <dbl>      <dbl>
## 1   174.  97.3       87.6

Notar como el hecho de substituir los valores con el promedio de la columna y no con 0, conlleva un aumento de los valores medios de todas las variables.

O mejor aún, quizás podemos remplazar los NA con los valores medios dentro de species (o más variables).

starwars %>%
  group_by(species) %>%
  mutate(across(where(is.numeric), 
                ~if_else(condition = is.na(.), 
                         true = mean(., na.rm = T), 
                         false = as.numeric(.)))) %>%
  summarise(across(where(is.numeric), mean)) %>%
  ungroup()
## `summarise()` ungrouping output (override with `.groups` argument)
## # A tibble: 38 x 4
##    species   height  mass birth_year
##    <chr>      <dbl> <dbl>      <dbl>
##  1 Aleena       79   15        NaN  
##  2 Besalisk    198  102        NaN  
##  3 Cerean      198   82         92  
##  4 Chagrian    196  NaN        NaN  
##  5 Clawdite    168   55        NaN  
##  6 Droid       131.  69.8       53.3
##  7 Dug         112   40        NaN  
##  8 Ewok         88   20          8  
##  9 Geonosian   183   80        NaN  
## 10 Gungan      209.  74         52  
## # ... with 28 more rows

Notar que allá donde no se pueden calcular valores medios porque solo hay NA, summarise() devuelve un NaN.