2  Una Introducción Tidyverse

¿Qué es tidyverse y dónde encaja el marco de tidymodels? tidyverse es una colección de paquetes R para análisis de datos que se desarrollan con ideas y normas comunes. De Wickham et al. (2019):

“En un nivel alto, tidyverse es un lenguaje para resolver desafíos de ciencia de datos con código R. Su objetivo principal es facilitar una conversación entre un humano y una computadora sobre datos. De manera menos abstracta, tidyverse es una colección de paquetes de R que comparten una filosofía de diseño de alto nivel y estructuras de datos y gramática de bajo nivel, de modo que aprender un paquete hace que sea más fácil aprender el siguiente.”

En este capítulo, analizamos brevemente principios importantes de la filosofía de diseño de tidyverse y cómo se aplican en el contexto del software de modelado para que sea fácil de usar correctamente y respalde las buenas prácticas estadísticas, como lo describimos en el Capítulo 1. El siguiente capítulo cubre las convenciones de modelado del lenguaje principal R. Juntos, pueden utilizar estas discusiones para comprender las relaciones entre tidyverse, tidymodels y el lenguaje principal o básico, R. Tanto tidymodels como tidyverse se basan en el lenguaje R, y tidymodels aplica los principios de tidyverse para construir modelos.

2.1 Principios De Tidyverse

El conjunto completo de estrategias y tácticas para escribir código R en el estilo tidyverse se puede encontrar en el sitio web https://design.tidyverse.org. Aquí podemos describir brevemente varios de los principios generales de diseño de tidyverse, su motivación y cómo pensamos sobre el modelado como una aplicación de estos principios.

2.1.1 Diseño para humanos

Tidyverse se centra en diseñar paquetes y funciones de R que puedan ser fácilmente comprendidos y utilizados por una amplia gama de personas. Tanto históricamente como hoy, un porcentaje sustancial de usuarios de R no son personas que crean software o herramientas, sino personas que crean análisis o modelos. Como tal, los usuarios de R normalmente no tienen (ni necesitan) experiencia en informática, y muchos no están interesados ​​en escribir sus propios paquetes R.

Por esta razón, es fundamental que sea fácil trabajar con el código R para lograr sus objetivos. La documentación, la formación, la accesibilidad y otros factores juegan un papel importante para lograrlo. Sin embargo, si la sintaxis en sí es difícil de comprender para las personas, la documentación es una mala solución. El software en sí debe ser intuitivo.

Para contrastar el enfoque tidyverse con la semántica R más tradicional, considere ordenar un marco de datos. Los marcos de datos pueden representar diferentes tipos de datos en cada columna y varios valores en cada fila. Usando solo el lenguaje central, podemos ordenar un marco de datos usando una o más columnas reordenando las filas mediante las reglas de subíndice de R junto con order(); no puedes utilizar con éxito una función que podrías sentirte tentado a probar en tal situación debido a su nombre, sort(). Para ordenar los datos de mtcars por dos de sus columnas, la llamada podría verse así:

mtcars[order(mtcars$gear, mtcars$mpg), ]

Si bien es muy eficiente desde el punto de vista computacional, sería difícil argumentar que se trata de una interfaz de usuario intuitiva. En dplyr, por el contrario, la función tidyverse arrange() toma un conjunto de nombres de variables como argumentos de entrada directamente:

library(dplyr)
arrange(.data = mtcars, gear, mpg)

Los nombres de variables utilizados aquí están “sin comillas”; muchas funciones tradicionales de R requieren una cadena de caracteres para especificar variables, pero las funciones tidyverse toman nombres sin comillas o funciones de selección. Los selectores permiten una o más reglas legibles que se aplican a los nombres de las columnas. Por ejemplo, ends_with("t") seleccionaría las columnas drat y wt del marco de datos mtcars.

Además, el nombramiento es crucial. Si era nuevo en R y estaba escribiendo análisis de datos o código de modelado que involucra álgebra lineal, es posible que se sienta bloqueado al buscar una función que calcule la matriz inversa. El uso de apropos("inv") no produce candidatos. Resulta que la función R base para esta tarea es solve(), para resolver sistemas de ecuaciones lineales. Para una matriz X, usarías solve(X) para invertir X (sin vector para el lado derecho de la ecuación). Esto sólo está documentado en la descripción de uno de los argumentos en el archivo de ayuda. En esencia, necesita saber el nombre de la solución para poder encontrarla.

El enfoque de tidyverse consiste en utilizar nombres de funciones que sean descriptivos y explícitos en lugar de aquellos que sean breves e implícitos. Hay un enfoque en los verbos (por ejemplo, “adaptar”, “arreglar”, etc.) para los métodos generales. Los pares verbo-sustantivo son particularmente eficaces; considere invert_matrix() como un nombre de función hipotético. En el contexto del modelado, también es importante evitar jergas muy técnicas, como letras griegas o términos oscuros. Los nombres deben ser lo más autodocumentados posible.

Cuando hay funciones similares en un paquete, los nombres de las funciones están diseñados para optimizarse para completarse con tabulaciones. Por ejemplo, el paquete glue tiene una colección de funciones que comienzan con un prefijo común (glue_) que permite a los usuarios encontrar rápidamente la función que buscan.

2.1.2 Reutilizar estructuras de datos existentes

Siempre que sea posible, las funciones deben evitar devolver una estructura de datos novedosa. Si los resultados son propicios para una estructura de datos existente, se debe utilizar. Esto reduce la carga cognitiva al utilizar software; no se requieren sintaxis ni métodos adicionales.

El marco de datos es la estructura de datos preferida en los paquetes tidyverse y tidymodels, porque su estructura se adapta bien a una gama tan amplia de tareas de ciencia de datos. Específicamente, los modelos tidyverse y tidy favorecen el tibble, una reinvención moderna del marco de datos de R que describimos en la siguiente sección sobre el ejemplo de sintaxis de tidyverse.

Como ejemplo, el paquete rsample se puede utilizar para crear resamples de un conjunto de datos, como la validación cruzada o el bootstrap (descrito en el Capítulo 10). Las funciones de remuestreo devuelven un tibble con una columna llamada “divisiones” de objetos que definen los conjuntos de datos remuestreados. Tres muestras de arranque de un conjunto de datos podrían verse así:

boot_samp <- rsample::bootstraps(mtcars, times = 3)
boot_samp
## # Bootstrap sampling 
## # A tibble: 3 × 2
##   splits          id        
##   <list>          <chr>     
## 1 <split [32/10]> Bootstrap1
## 2 <split [32/12]> Bootstrap2
## 3 <split [32/7]>  Bootstrap3
class(boot_samp)
## [1] "bootstraps" "rset"       "tbl_df"     "tbl"        "data.frame"

Con este enfoque, se pueden usar funciones basadas en vectores con estas columnas, como vapply() o purrr::map().1 Este objeto boot_samp tiene múltiples clases pero hereda métodos para marcos de datos ("data.frame") y tibbles ("tbl_df"). Además, se pueden agregar nuevas columnas a los resultados sin afectar la clase de los datos. Es mucho más fácil y versátil para los usuarios trabajar con esto que un tipo de objeto completamente nuevo que no hace que su estructura de datos sea obvia.

Una desventaja de depender de estructuras de datos comunes es la posible pérdida de rendimiento computacional. En algunas situaciones, los datos se pueden codificar en formatos especializados que son representaciones más eficientes de los datos. Por ejemplo:

  • En química computacional, el formato de archivo de datos estructurales (SDF) es una herramienta para tomar estructuras químicas y codificarlas en un formato con el que sea computacionalmente eficiente trabajar.

  • Los datos que tienen una gran cantidad de valores iguales (como ceros para datos binarios) se pueden almacenar en un formato de matriz dispersa. Este formato puede reducir el tamaño de los datos y permitir técnicas computacionales más eficientes.

Estos formatos son ventajosos cuando el problema tiene un alcance adecuado y los posibles métodos de procesamiento de datos están bien definidos y son adecuados para dicho formato.2 Sin embargo, una vez que se violan dichas restricciones, los formatos de datos especializados son menos útiles. Por ejemplo, si realizamos una transformación de los datos que los convierte en números fraccionarios, la salida ya no es escasa; La representación matricial dispersa es útil para un paso algorítmico específico en el modelado, pero a menudo esto no es cierto antes o después de ese paso específico.

Una estructura de datos especializada no es lo suficientemente flexible para un flujo de trabajo de modelado completo como lo es una estructura de datos común.

Una característica importante del tibble producido por rsample es que la columna splits es una lista. En este caso, cada elemento de la lista tiene el mismo tipo de objeto: un objeto rsplit que contiene información sobre qué filas de mtcars pertenecen a la muestra de arranque. Las columnas de lista pueden ser muy útiles en el análisis de datos y, como se verá a lo largo de este libro, son importantes para los modelos ordenados.

2.1.3 Diseño para la tubería y programación funcional.

El operador de canalización magrittr (%>%) es una herramienta para encadenar una secuencia de funciones R.3 Para demostrarlo, considere los siguientes comandos que ordenan un marco de datos y luego conserve las primeras 10 filas:

small_mtcars <- arrange(mtcars, gear)
small_mtcars <- slice(small_mtcars, 1:10)

# or more compactly: 
small_mtcars <- slice(arrange(mtcars, gear), 1:10)

El operador de tubería sustituye el valor del lado izquierdo del operador como primer argumento del lado derecho, por lo que podemos implementar el mismo resultado que antes con:

small_mtcars <- 
  mtcars %>% 
  arrange(gear) %>% 
  slice(1:10)

La versión canalizada de esta secuencia es más legible; esta legibilidad aumenta a medida que se agregan más operaciones a una secuencia. Este enfoque de programación funciona en este ejemplo porque todas las funciones que utilizamos devuelven la misma estructura de datos (un marco de datos) que luego es el primer argumento de la siguiente función. Esto es por diseño. Cuando sea posible, cree funciones que puedan incorporarse a un conjunto de operaciones.

Si ha utilizado ggplot2, esto no es diferente a la superposición de componentes de la trama en un objeto ggplot con el operador +. Para hacer un diagrama de dispersión con una línea de regresión, la llamada inicial a ggplot() se aumenta con dos operaciones adicionales:

library(ggplot2)
ggplot(mtcars, aes(x = wt, y = mpg)) +
  geom_point() + 
  geom_smooth(method = lm)

Si bien es similar a la canalización dplyr, tenga en cuenta que el primer argumento de esta canalización es un conjunto de datos (mtcars) y que cada llamada de función devuelve un objeto ggplot. No todas las canalizaciones necesitan mantener los valores devueltos (objetos de trazado) iguales que el valor inicial (un marco de datos). El uso del operador de canalización con operaciones dplyr ha hecho que muchos usuarios de R esperen devolver un marco de datos cuando se utilizan canalizaciones; como se muestra con ggplot2, no tiene por qué ser así. Las canalizaciones son increíblemente útiles para modelar flujos de trabajo, pero el modelado de canalizaciones puede devolver, en lugar de un marco de datos, objetos como componentes del modelo.

R tiene excelentes herramientas para crear, cambiar y operar funciones, lo que lo convierte en un excelente lenguaje para la programación funcional. Este enfoque puede reemplazar los bucles iterativos en muchas situaciones, como cuando una función devuelve un valor sin otros efectos secundarios.4

Veamos un ejemplo. Suponga que está interesado en el logaritmo de la relación entre la eficiencia del combustible y el peso del automóvil. Para aquellos nuevos en R y/o que vienen de otros lenguajes de programación, un bucle puede parecer una buena opción:

n <- nrow(mtcars)
ratios <- rep(NA_real_, n)
for (car in 1:n) {
  ratios[car] <- log(mtcars$mpg[car]/mtcars$wt[car])
}
head(ratios)
## [1] 2.081 1.988 2.285 1.896 1.693 1.655

Aquellos con más experiencia en R sabrán que existe una versión vectorizada mucho más simple y rápida que se puede calcular mediante:

ratios <- log(mtcars$mpg/mtcars$wt)

Sin embargo, en muchos casos del mundo real, la operación de interés por elementos es demasiado compleja para una solución vectorizada. En tal caso, un buen enfoque es escribir una función para realizar los cálculos. Cuando diseñamos para programación funcional, es importante que la salida dependa sólo de las entradas y que la función no tenga efectos secundarios. Las violaciones de estas ideas en la siguiente función se muestran con comentarios:

compute_log_ratio <- function(mpg, wt) {
  log_base <- getOption("log_base", default = exp(1)) # obtiene datos externos
  results <- log(mpg/wt, base = log_base)
  print(mean(results))                                # imprime en la consola
  done <<- TRUE                                       # establece datos externos
  results
}

Una mejor versión sería:

compute_log_ratio <- function(mpg, wt, log_base = exp(1)) {
  log(mpg/wt, base = log_base)
}

The purrr package contains tools for functional programming. Let’s focus on the map() family of functions, which operates on vectors and always returns the same type of output. The most basic function, map(), always returns a list and uses the basic syntax of map(vector, function). For example, to take the square root of our data, we could:

map(head(mtcars$mpg, 3), sqrt)
## [[1]]
## [1] 4.583
## 
## [[2]]
## [1] 4.583
## 
## [[3]]
## [1] 4.775

Existen variantes especializadas de map() que devuelven valores cuando sabemos o esperamos que la función genere uno de los tipos de vectores básicos. Por ejemplo, dado que la raíz cuadrada devuelve un número de doble precisión:

map_dbl(head(mtcars$mpg, 3), sqrt)
## [1] 4.583 4.583 4.775

También hay funciones de mapeo que operan en múltiples vectores:

log_ratios <- map2_dbl(mtcars$mpg, mtcars$wt, compute_log_ratio)
head(log_ratios)
## [1] 2.081 1.988 2.285 1.896 1.693 1.655

Las funciones map() también permiten funciones temporales y anónimas definidas usando el carácter de tilde. Los valores de los argumentos son .x y .y para map2():

map2_dbl(mtcars$mpg, mtcars$wt, ~ log(.x/.y)) %>% 
  head()
## [1] 2.081 1.988 2.285 1.896 1.693 1.655

Estos ejemplos han sido triviales pero, en secciones posteriores, se aplicarán a problemas más complejos.

Para la programación funcional en el modelado ordenado, las funciones deben definirse de modo que funciones como map() puedan usarse para cálculos iterativos.

2.2 Ejemplos De Sintaxis De Tidyverse

Comencemos nuestra discusión sobre la sintaxis de tidyverse explorando más profundamente qué es un tibble y cómo funcionan. Los tibbles tienen reglas ligeramente diferentes a los marcos de datos básicos en R. Por ejemplo, los tibbles funcionan naturalmente con nombres de columnas que no son nombres de variables sintácticamente válidos:

# Quiere nombres válidos:
data.frame(`variable 1` = 1:2, two = 3:4)
##   variable.1 two
## 1          1   3
## 2          2   4
# Pero se puede obligar a utilizarlos con una opción adicional:
df <- data.frame(`variable 1` = 1:2, two = 3:4, check.names = FALSE)
df
##   variable 1 two
## 1          1   3
## 2          2   4

# Pero los tibbles simplemente funcionan:
tbbl <- tibble(`variable 1` = 1:2, two = 3:4)
tbbl
## # A tibble: 2 × 2
##   `variable 1`   two
##          <int> <int>
## 1            1     3
## 2            2     4

Los marcos de datos estándar permiten coincidencia parcial de argumentos para que el código que utiliza solo una parte de los nombres de las columnas siga funcionando. Tibbles evita que esto suceda, ya que puede provocar errores accidentales.

df$tw
## [1] 3 4

tbbl$tw
## Warning: Unknown or uninitialised column: `tw`.
## NULL

Tibbles también previene uno de los errores de R más comunes: eliminar dimensiones. Si un marco de datos estándar subconjunto de columnas en una sola columna, el objeto se convierte en un vector. Tibbles nunca hace esto:

df[, "two"]
## [1] 3 4

tbbl[, "two"]
## # A tibble: 2 × 1
##     two
##   <int>
## 1     3
## 2     4

Hay otras ventajas al usar tibbles en lugar de marcos de datos, como una mejor impresión y más.5

Para demostrar algo de sintaxis, usemos funciones tidyverse para leer datos que podrían usarse en el modelado. El conjunto de datos proviene del portal de datos de la ciudad de Chicago y contiene datos diarios sobre el número de pasajeros de las estaciones de trenes elevados de la ciudad. El conjunto de datos tiene columnas para:

  • el identificador de la estación (númerico)
  • el nombre de la estación (texto)
  • la fecha (texto en formato mm/dd/yyyy)
  • el día de la semana (texto)
  • el número de pasajeros (númerico)

Nuestro canalización tidyverse llevará a cabo las siguientes tareas, en orden:

  1. Utilice el paquete tidyverse readr para leer los datos del sitio web de origen y convertirlos en un tibble. Para hacer esto, la función read_csv() puede determinar el tipo de datos leyendo un número inicial de filas. Alternativamente, si los nombres y tipos de las columnas ya se conocen, se puede crear una especificación de columna en R y pasarla a read_csv().

  2. Filtre los datos para eliminar algunas columnas que no son necesarias (como el ID de la estación) y cambie la columna “nombre de la estación” a “estación”. Para esto se utiliza la función select(). Al filtrar, utilice los nombres de las columnas o una función selectora dplyr. Al seleccionar nombres, se puede declarar un nuevo nombre de variable utilizando el formato de argumento nuevo_nombre = antiguo_nombre.

  3. Convierta el campo de fecha al formato de fecha R usando la función mdy() del paquete lubridate. También convertimos los números de pasajeros a miles. Ambos cálculos se ejecutan utilizando la función dplyr::mutate().

  4. Utilice el número máximo de viajes para cada estación y combinación de días. Esto mitiga el problema de una pequeña cantidad de días que tienen más de un registro de número de pasajeros en determinadas estaciones. Agrupamos los datos de número de pasajeros por estación y día, y luego resumimos dentro de cada una de las combinaciones únicas 1999 con la estadística máxima.

El código tidyverse para estos pasos es:

library(tidyverse)
library(lubridate)

url <- "https://data.cityofchicago.org/api/views/5neh-572f/rows.csv?accessType=DOWNLOAD&bom=true&format=true"

all_stations <- 
  # Paso 1: leer los datos.
  read_csv(url) %>% 
  # Paso 2: filtrar columnas y cambiar el nombre de la estación
  dplyr::select(station = stationname, date, rides) %>% 
  # Paso 3: convierta el campo de fecha de caracteres a una codificación de fecha.
  # Además, coloque los datos en unidades de 1K viajes.
  mutate(date = mdy(date), rides = rides / 1000) %>% 
  # Paso 4: resuma los múltiples registros utilizando el máximo.
  group_by(date, station) %>% 
  summarize(rides = max(rides), .groups = "drop")

Esta canalización de operaciones ilustra por qué tidyverse es popular. Se utiliza una serie de manipulaciones de datos que tienen funciones simples y fáciles de entender para cada transformación; La serie se presenta de forma simplificada y legible. La atención se centra en cómo el usuario interactúa con el software. Este enfoque permite que más personas aprendan R y alcancen sus objetivos de análisis, y adoptar estos mismos principios para modelar en R tiene los mismos beneficios.

2.3 Resumen Del Capítulo

Este capítulo presentó tidyverse, centrándose en las aplicaciones para modelado y cómo los principios de diseño de tidyverse informan el marco de trabajo de tidymodels. Piense en el marco de tidymodels como una aplicación de los principios de tidyverse al dominio de la construcción de modelos. Describimos las diferencias en las convenciones entre tidyverse y base R, e introdujimos dos componentes importantes del sistema tidyverse, tibbles y el operador de canalización %>%. La limpieza y el procesamiento de datos pueden parecer mundanos a veces, pero estas tareas son importantes para el modelado en el mundo real; Ilustramos cómo usar las funciones tibbles, pipe y tidyverse en un ejercicio de ejemplo de importación y procesamiento de datos.


  1. Si nunca antes ha visto :: en el código R, es un método explícito para llamar a una función. El valor del lado izquierdo es el espacio de nombres donde reside la función (normalmente un nombre de paquete). El lado derecho es el nombre de la función. En los casos en que dos paquetes usan el mismo nombre de función, esta sintaxis garantiza que se llame a la función correcta.↩︎

  2. No todos los algoritmos pueden aprovechar representaciones dispersas de datos. En tales casos, una matriz dispersa se debe convertir a un formato más convencional antes de continuar.↩︎

  3. En R 4.1.0, también se introdujo un operador de canalización nativo |>. En este libro, utilizamos la canalización magrittr ya que los usuarios de versiones anteriores de R no tendrán la nueva canalización nativa.↩︎

  4. Ejemplos de efectos secundarios de funciones podrían incluir cambiar datos globales o imprimir un valor.↩︎

  5. El capítulo 10 de Wickham y Grolemund (2016) tiene más detalles sobre tibbles.↩︎