Capítulo31 Temas Avanzados en Data Science con Tidyverse

Fecha de la última revisión

## [1] "2026-09-09"

31.1 Temas

  • Por qué una función normal no puede recibir un nombre de columna
  • {{ }}: la solución, y := para nombrar la columna de salida
  • Modelado reproducible con tidymodels: receta, modelo y flujo
  • Datos anidados: una tabla dentro de una tabla
  • Un modelo por grupo con nest(), map() y broom

Este capítulo supone que ya hiciste los de funciones e iteración. Todo lo que hay aquí es la combinación de esos dos: funciones que reciben columnas, y modelos aplicados a muchos grupos a la vez.

31.2 Introducción

Este documento presenta temas avanzados en el ecosistema tidyverse con ejemplos prácticos en R.

31.2.1 1. Manipulación avanzada con dplyr

Empecemos por el problema, porque si no se ve el problema la solución parece arbitraria.

En el capítulo de funciones aprendimos a escribir funciones propias. Pero si intentas escribir una que reciba el nombre de una columna, no funciona:

promedio_por <- function(datos, grupo, valor) {
  datos |> 
    group_by(grupo) |>              # <- esto está mal
    summarise(media = mean(valor))  # <- y esto también
}

promedio_por(millas, clase, autopista)
# Error: object 'clase' not found

El motivo es que dplyr hace algo poco común: te deja escribir clase sin comillas, buscando ese nombre dentro de la tabla en vez de en tu sesión. A eso se le llama evaluación tidy. Es comodísimo cuando escribes código a mano y se vuelve un problema en cuanto quieres envolverlo en una función, porque group_by(grupo) busca una columna que se llame literalmente grupo, no el contenido del argumento.

Evaluación tidy: {{ }} y :=: funciones que reciben nombres de columnas.

  • {{ var }} (“embrace”): permite pasar el nombre de una columna como argumento a una función que usa dplyr por dentro.
  • :=: se usa en mutate/summarise cuando el nombre de la nueva columna también es una variable.
  • Para qué: escribir tus propias funciones reutilizables sobre pipelines de dplyr.

La solución son las llaves dobles, {{ }}, que en inglés se llaman embrace (“abrazar”). Le dicen a dplyr: “no busques una columna con este nombre; mira qué me pasaron a mí y usa eso”.

Explicación: Uso de tidy evaluation para funciones personalizadas.

library(dplyr)

# Función personalizada con tidy evaluation
mean_by_group <- function(data, group_var, value_var) {
  data %>%
    group_by({{ group_var }}) %>%
    summarise(mean_value = mean({{ value_var }}, na.rm = TRUE))
}
mean_by_group
## function (data, group_var, value_var) 
## {
##     data %>% group_by({
##         {
##             group_var
##         }
##     }) %>% summarise(mean_value = mean({
##         {
##             value_var
##         }
##     }, na.rm = TRUE))
## }
df <- tibble(grupo = c("A", "A", "B", "B"), 
             valor = c(10, 20, 30, 40)) # los datos

mean_by_group(df, grupo, valor) # El uso de la función nueva
## # A tibble: 2 × 2
##   grupo mean_value
##   <chr>      <dbl>
## 1 A             15
## 2 B             35

Otro ejemplo

dfnew= tibble(especie = c("setosa", "setosa", "versicolor", "versicolor"),
              longitud_sepalo = c(5.1, 4.9, 7.0, 6.4))

mean_by_group(dfnew, especie, longitud_sepalo)
## # A tibble: 2 × 2
##   especie    mean_value
##   <chr>           <dbl>
## 1 setosa            5  
## 2 versicolor        6.7
mean_sd_by_group <- function(data, group_var, value_var) {
  data %>%
    group_by({{ group_var }}) %>%
    summarise(sd_value = sd({{ value_var }}, na.rm = TRUE),
              mean_value = mean({{ value_var }}, na.rm = TRUE))
}

mean_sd_by_group(dfnew, especie, longitud_sepalo)
## # A tibble: 2 × 3
##   especie    sd_value mean_value
##   <chr>         <dbl>      <dbl>
## 1 setosa        0.141        5  
## 2 versicolor    0.424        6.7

31.2.2 Componentes clave:

  1. Definición de la función
mean_by_group <- function(data, group_var, value_var) { ... }
  • data: El data frame o tibble que contiene los datos.
  • group_var: La variable por la que se agruparán los datos (por ejemplo, categoría).
  • value_var: La variable numérica de la que se calculará la media.
  1. {{ }} (Curly-curly operator):
  • Es parte del tidy evaluation en dplyr.
  • Permite que los argumentos group_var y value_var se pasen como nombres de columna sin comillas.
  • Sin esto, la función no podría interpretar correctamente los nombres de columna.
  1. group_by({{ group_var }}):
  • Agrupa el conjunto de datos por la variable indicada.
  • Ejemplo: Si group_var = especie, agrupa por especie.
  1. summarise(mean_value = mean({{ value_var }}, na.rm = TRUE)):
  • Calcula la media de la columna indicada (value_var) dentro de cada grupo.
  • na.rm = TRUE elimina valores faltantes (NA) para evitar errores.

31.2.3 Nombrar la columna de salida: :=

Hay una limitación en la función de arriba: la columna nueva siempre se llama mean_value, diga lo que diga el usuario. Para que el nombre dependa del argumento hace falta := (dos puntos e igual) en vez del = normal:

promedio_con_nombre <- function(datos, grupo, valor, nombre = "media") {
  datos |> 
    group_by({{ grupo }}) |> 
    summarise({{ nombre }} := mean({{ valor }}, na.rm = TRUE))
}

Regla corta para no perderse: {{ }} a la derecha del igual, := en lugar del igual. Todo lo que sea “una columna que me pasaron” va abrazado; el := solo hace falta cuando el nombre de la columna nueva también es un argumento.

Si olvidas na.rm = TRUE, funciones como mean() o sd() devuelven NA en cuanto la columna tiene un solo valor faltante, en vez de calcular el promedio.

datos=c(1, NA, 3:8, NA, 10:12)
mean(datos, na.rm=TRUE)
## [1] 6.7

31.3 2. Modelado con tidymodels

Explicación: Flujo reproducible para regresión lineal.

En el capítulo de modelos se ajustaba un lm() de una sola línea. tidymodels es otra manera de hacerlo, más larga, y conviene entender qué se gana a cambio.

Las tres piezas de tidymodels: separar la preparación, el método y el ajuste.

  • La receta (recipe()): describe qué se va a modelar y qué hay que hacerle a los datos antes (centrar, escalar, convertir categorías, imputar faltantes). Es una descripción, no un cálculo: no toca los datos hasta que se aplica.
  • El modelo (linear_reg(), set_engine()): describe con qué método, separado del paquete concreto que lo calcula. El mismo modelo puede correr con lm, con glmnet o con stan cambiando una palabra.
  • El flujo (workflow()): junta receta y modelo en un solo objeto, que es el que se ajusta con fit() y el que se guarda.

¿Para qué tanta ceremonia si lm() cabía en una línea? Por tres razones, y todas aparecen en cuanto el análisis crece:

  1. La preparación viaja con el modelo. Si centraste una variable para ajustar, hay que centrarla igual al predecir. Con lm() eso lo tienes que recordar tú; con una receta, va incluido.
  2. Cambiar de método cuesta una palabra. Pasar de regresión lineal a bosque aleatorio no obliga a reescribir el resto.
  3. Evita la fuga de información. Al validar, la receta se aplica solo a la parte de entrenamiento, que es lo correcto y lo que casi nadie hace a mano.

Para un modelo suelto de un artículo, lm() sigue siendo la respuesta correcta y más clara. tidymodels se justifica cuando hay preparación de datos, varios modelos que comparar, o validación cruzada de por medio.

library(tidymodels)
data(mtcars)

# Convertir variables a factores
mtcars <- mtcars %>%
  mutate(cyl = factor(cyl),
         gear = factor(gear))


receta <- recipe(mpg ~ wt + hp+ cyl + gear, data = mtcars)
modelo <- linear_reg() %>%
          set_engine("lm")

workflow() %>%
  add_recipe(receta) %>%
  add_model(modelo) %>%
  fit(data = mtcars)
## ══ Workflow [trained] ═════════════════════════════
## Preprocessor: Recipe
## Model: linear_reg()
## 
## ── Preprocessor ───────────────────────────────────
## 0 Recipe Steps
## 
## ── Model ──────────────────────────────────────────
## 
## Call:
## stats::lm(formula = ..y ~ ., data = data)
## 
## Coefficients:
## (Intercept)           wt           hp         cyl6         cyl8        gear4  
##    34.46173     -2.79186     -0.03424     -2.93920     -1.33080      1.42125  
##       gear5  
##     2.08577

Ver los resultados del modelo y hacer predicciones.

# Ajustar el modelo
ajuste <- workflow() %>%
  add_recipe(receta) %>%
  add_model(modelo) %>%
  fit(data = mtcars)


# Extraer el modelo ajustado
modelo_ajustado <- ajuste %>% extract_fit_parsnip()


# Ver coeficientes

modelo_ajustado %>% tidy()      # Coeficientes en formato tibble
## # A tibble: 7 × 5
##   term        estimate std.error statistic  p.value
##   <chr>          <dbl>     <dbl>     <dbl>    <dbl>
## 1 (Intercept)  34.5       2.46      14.0   2.52e-13
## 2 wt           -2.79      0.856     -3.26  3.18e- 3
## 3 hp           -0.0342    0.0177    -1.93  6.44e- 2
## 4 cyl6         -2.94      1.47      -1.99  5.71e- 2
## 5 cyl8         -1.33      2.78      -0.479 6.36e- 1
## 6 gear4         1.42      1.51       0.941 3.56e- 1
## 7 gear5         2.09      2.14       0.972 3.40e- 1
modelo_ajustado %>% glance()    # Métricas globales (R², AIC, etc.)
## # A tibble: 1 × 12
##   r.squared adj.r.squared sigma statistic       p.value    df logLik   AIC   BIC
##       <dbl>         <dbl> <dbl>     <dbl>         <dbl> <dbl>  <dbl> <dbl> <dbl>
## 1     0.864         0.831  2.48      26.4 0.00000000113     6  -70.5  157.  169.
## # ℹ 3 more variables: deviance <dbl>, df.residual <int>, nobs <int>
summary(modelo_ajustado$fit)    # Resumen clásico del modelo lm
## 
## Call:
## stats::lm(formula = ..y ~ ., data = data)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -3.4215 -1.2428 -0.3401  0.8961  5.3657 
## 
## Coefficients:
##             Estimate Std. Error t value Pr(>|t|)    
## (Intercept) 34.46173    2.46379  13.987 2.52e-13 ***
## wt          -2.79186    0.85567  -3.263  0.00318 ** 
## hp          -0.03424    0.01770  -1.935  0.06444 .  
## cyl6        -2.93920    1.47362  -1.995  0.05710 .  
## cyl8        -1.33080    2.77885  -0.479  0.63617    
## gear4        1.42125    1.51065   0.941  0.35580    
## gear5        2.08577    2.14490   0.972  0.34015    
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 2.477 on 25 degrees of freedom
## Multiple R-squared:  0.8638, Adjusted R-squared:  0.8311 
## F-statistic: 26.42 on 6 and 25 DF,  p-value: 1.128e-09
# Predicciones y métricas
predicciones <- predict(modelo_ajustado, mtcars)

bind_cols(mtcars, predicciones) %>%
  metrics(truth = mpg, estimate = .pred)
## # A tibble: 3 × 3
##   .metric .estimator .estimate
##   <chr>   <chr>          <dbl>
## 1 rmse    standard       2.19 
## 2 rsq     standard       0.864
## 3 mae     standard       1.68
  • extract_fit_parsnip() obtiene el modelo ajustado.
  • tidy() muestra coeficientes y errores estándar.
  • metrics() calcula métricas como RMSE, R², MAE.

broom: convertir la salida de un modelo en tablas ordenadas.

  • tidy(modelo): una fila por coeficiente, con estimado, error estándar, estadístico y valor p.
  • glance(modelo): una sola fila con las métricas del modelo entero: \(R^2\), AIC, sigma, número de observaciones.
  • augment(modelo, datos): los datos originales más las predicciones y los residuales, que es lo que hacía add_predictions() y add_residuals() del capítulo de modelos.

Esto es lo que cierra el círculo con los capítulos anteriores: summary() imprime un texto bonito que no se puede usar, y tidy() y glance() devuelven tibbles, que sí se pueden filtrar, unir, graficar y guardar. Es la misma lección del Ejercicio 2 del capítulo de funciones: devuelve una tabla, no un adorno.

metrics() calculado sobre los mismos datos con los que se ajustó siempre sale demasiado optimista: el modelo ya vio esas observaciones. Para saber de verdad qué tan bien predice hace falta separar los datos en entrenamiento y prueba, con initial_split(). En el ejemplo de arriba no se hace, y por eso las métricas hay que leerlas como descripción del ajuste, no como capacidad de predicción.


31.4 3. Visualización avanzada con ggplot2

Explicación: Facetas y escalas personalizadas.

library(ggplot2)

ggplot(mtcars, aes(x = wt, y = mpg)) +
  geom_point() +
  facet_wrap(~ cyl) +
  scale_y_log10() +
  theme_minimal()


31.5 4. Datos anidados y listas con purrr

Explicación: Ajustar modelos por grupo y extraer coeficientes.

Esta sección es la continuación directa del capítulo de iteración, con una idea nueva: una columna de un tibble puede contener cosas que no son números. Puede contener tablas enteras, o modelos.

nest() y unnest() (tidyr): meter tablas dentro de una tabla.

  • group_by(g) |> nest(): deja una fila por grupo y una columna data donde cada celda es el tibble completo de ese grupo.
  • Con mutate() y map() se le puede añadir una columna modelo, donde cada celda es un objeto lm, y otra con sus coeficientes.
  • unnest(columna): deshace el anidamiento y devuelve una tabla plana.

La ventaja sobre el split() del capítulo de iteración es que todo se queda en una sola tabla: los datos, el modelo y los resultados de cada grupo viajan juntos en la misma fila, y no hay que ir emparejando listas por su nombre.

El patrón completo, que vale la pena aprenderse de memoria porque resuelve una enorme cantidad de análisis reales:

datos |> 
  group_by(grupo) |> 
  nest() |> 
  mutate(
    modelo   = purrr::map(data, \(d) lm(y ~ x, data = d)),
    resumen  = purrr::map(modelo, broom::glance),
    coefs    = purrr::map(modelo, broom::tidy)
  ) |> 
  unnest(resumen)
library(tidyr)
library(purrr)
library(broom)

nested <- mtcars %>%
  group_by(cyl) %>%
  nest()

nested <- nested %>%
  mutate(modelo = purrr::map(data, ~ lm(mpg ~ wt, data = .x)),
         coeficientes = purrr::map(modelo, tidy))

nested$data
## [[1]]
## # A tibble: 7 × 10
##     mpg  disp    hp  drat    wt  qsec    vs    am gear   carb
##   <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <fct> <dbl>
## 1  21    160    110  3.9   2.62  16.5     0     1 4         4
## 2  21    160    110  3.9   2.88  17.0     0     1 4         4
## 3  21.4  258    110  3.08  3.22  19.4     1     0 3         1
## 4  18.1  225    105  2.76  3.46  20.2     1     0 3         1
## 5  19.2  168.   123  3.92  3.44  18.3     1     0 4         4
## 6  17.8  168.   123  3.92  3.44  18.9     1     0 4         4
## 7  19.7  145    175  3.62  2.77  15.5     0     1 5         6
## 
## [[2]]
## # A tibble: 11 × 10
##      mpg  disp    hp  drat    wt  qsec    vs    am gear   carb
##    <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <fct> <dbl>
##  1  22.8 108      93  3.85  2.32  18.6     1     1 4         1
##  2  24.4 147.     62  3.69  3.19  20       1     0 4         2
##  3  22.8 141.     95  3.92  3.15  22.9     1     0 4         2
##  4  32.4  78.7    66  4.08  2.2   19.5     1     1 4         1
##  5  30.4  75.7    52  4.93  1.62  18.5     1     1 4         2
##  6  33.9  71.1    65  4.22  1.84  19.9     1     1 4         1
##  7  21.5 120.     97  3.7   2.46  20.0     1     0 3         1
##  8  27.3  79      66  4.08  1.94  18.9     1     1 4         1
##  9  26   120.     91  4.43  2.14  16.7     0     1 5         2
## 10  30.4  95.1   113  3.77  1.51  16.9     1     1 5         2
## 11  21.4 121     109  4.11  2.78  18.6     1     1 4         2
## 
## [[3]]
## # A tibble: 14 × 10
##      mpg  disp    hp  drat    wt  qsec    vs    am gear   carb
##    <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <fct> <dbl>
##  1  18.7  360    175  3.15  3.44  17.0     0     0 3         2
##  2  14.3  360    245  3.21  3.57  15.8     0     0 3         4
##  3  16.4  276.   180  3.07  4.07  17.4     0     0 3         3
##  4  17.3  276.   180  3.07  3.73  17.6     0     0 3         3
##  5  15.2  276.   180  3.07  3.78  18       0     0 3         3
##  6  10.4  472    205  2.93  5.25  18.0     0     0 3         4
##  7  10.4  460    215  3     5.42  17.8     0     0 3         4
##  8  14.7  440    230  3.23  5.34  17.4     0     0 3         4
##  9  15.5  318    150  2.76  3.52  16.9     0     0 3         2
## 10  15.2  304    150  3.15  3.44  17.3     0     0 3         2
## 11  13.3  350    245  3.73  3.84  15.4     0     0 3         4
## 12  19.2  400    175  3.08  3.84  17.0     0     0 3         2
## 13  15.8  351    264  4.22  3.17  14.5     0     1 5         4
## 14  15    301    335  3.54  3.57  14.6     0     1 5         8
nested$modelo
## [[1]]
## 
## Call:
## lm(formula = mpg ~ wt, data = .x)
## 
## Coefficients:
## (Intercept)           wt  
##       28.41        -2.78  
## 
## 
## [[2]]
## 
## Call:
## lm(formula = mpg ~ wt, data = .x)
## 
## Coefficients:
## (Intercept)           wt  
##      39.571       -5.647  
## 
## 
## [[3]]
## 
## Call:
## lm(formula = mpg ~ wt, data = .x)
## 
## Coefficients:
## (Intercept)           wt  
##      23.868       -2.192
nested$coeficientes
## [[1]]
## # A tibble: 2 × 5
##   term        estimate std.error statistic p.value
##   <chr>          <dbl>     <dbl>     <dbl>   <dbl>
## 1 (Intercept)    28.4       4.18      6.79 0.00105
## 2 wt             -2.78      1.33     -2.08 0.0918 
## 
## [[2]]
## # A tibble: 2 × 5
##   term        estimate std.error statistic    p.value
##   <chr>          <dbl>     <dbl>     <dbl>      <dbl>
## 1 (Intercept)    39.6       4.35      9.10 0.00000777
## 2 wt             -5.65      1.85     -3.05 0.0137    
## 
## [[3]]
## # A tibble: 2 × 5
##   term        estimate std.error statistic    p.value
##   <chr>          <dbl>     <dbl>     <dbl>      <dbl>
## 1 (Intercept)    23.9      3.01       7.94 0.00000405
## 2 wt             -2.19     0.739     -2.97 0.0118

La alternativa es hacer cada factor individualmente

  • Hacemos un modelo lm(mpg ~ wt) para cada subconjunto de datos agrupados por cyl == 6
modelo_cyl6 <- lm(mpg ~ wt, data = filter(mtcars, cyl == 6))

summary(modelo_cyl6)
## 
## Call:
## lm(formula = mpg ~ wt, data = filter(mtcars, cyl == 6))
## 
## Residuals:
##      Mazda RX4  Mazda RX4 Wag Hornet 4 Drive        Valiant       Merc 280 
##        -0.1250         0.5840         1.9292        -0.6897         0.3547 
##      Merc 280C   Ferrari Dino 
##        -1.0453        -1.0080 
## 
## Coefficients:
##             Estimate Std. Error t value Pr(>|t|)   
## (Intercept)   28.409      4.184   6.789  0.00105 **
## wt            -2.780      1.335  -2.083  0.09176 . 
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 1.165 on 5 degrees of freedom
## Multiple R-squared:  0.4645, Adjusted R-squared:  0.3574 
## F-statistic: 4.337 on 1 and 5 DF,  p-value: 0.09176


31.6 Ejercicios del capítulo

Sobre los ejercicios de este capítulo. Cada ejercicio se entrega con tres cosas: el script, el resultado (que el bloque corra) y una explicación en palabras. Los ejercicios usan millas y paises del paquete datos, no mtcars ni los tibbles pequeños del capítulo. La hoja completa para entregar está en Ejercicios/Ejercicios_Capitulo_31_avanzado.Rmd.

31.6.1 Ejercicio 1. El problema, antes de la solución

Escribe la función promedio_por(datos, grupo, valor) sin las llaves dobles, tal como saldría de lo aprendido en el capítulo de funciones, y llámala con millas, clase y autopista. Ponla en un bloque con error=TRUE.

Verificación: tiene que dar object 'clase' not found.

En palabras: la columna clase existe en millas. Explica entonces dónde la fue a buscar R y por qué no la encontró.

# Escribe tu código aquí

31.6.2 Ejercicio 2. Las llaves dobles

Arregla la función anterior con {{ }}. Pruébala con millas (por clase, el promedio de autopista) y después con paises (por continente, el promedio de esperanza_de_vida).

Verificación: la primera llamada devuelve 7 filas y la segunda 5. La misma función sirve para las dos tablas sin cambiar nada.

En palabras: ¿por qué datos no necesita llaves dobles y grupo sí? Fíjate en qué tipo de cosa es cada argumento.

# Escribe tu código aquí

31.6.3 Ejercicio 3. El nombre de la columna de salida

Añádele a tu función un cuarto argumento, nombre, que decida cómo se llama la columna del resultado. Usa :=.

Verificación: con nombre = "consumo_medio", la tabla que sale tiene que tener una columna llamada exactamente así. Compruébalo con names().

En palabras: prueba primero con = normal en vez de := y copia el error. ¿Qué intentaba hacer R con ese =?

# Escribe tu código aquí

31.6.4 Ejercicio 4. Una tabla dentro de una tabla

Con millas, agrupa por clase y usa nest(). Mira el resultado, y después mira el contenido de una sola celda de la columna data.

Verificación: la tabla anidada tiene 7 filas y 2 columnas. La celda de data de la clase suv contiene un tibble de 62 filas.

En palabras: ¿cuántas filas tiene la tabla anidada y cuántas tenía millas? ¿Dónde se fueron las demás? Explica qué es una “columna lista”.

# Escribe tu código aquí

31.6.5 Ejercicio 5. Un modelo por grupo, todo en una tabla

Sobre la tabla anidada, añade con mutate() y map() una columna con el modelo lm(autopista ~ cilindrada) de cada clase, y otra con broom::glance() de cada modelo. Después haz unnest() de esa última y ordena por \(R^2\).

Verificación: la tabla final tiene 7 filas y una columna r.squared.

En palabras: compara esto con lo que hiciste en el capítulo de iteración con split() y map(). ¿Qué ventaja tiene tener los datos, el modelo y las métricas en la misma fila?

# Escribe tu código aquí

31.6.6 Ejercicio 6. tidy(), glance() y augment()

Ajusta lm(autopista ~ cilindrada + clase, data = millas) y aplícale las tres funciones de broom. Compara cada salida con la de summary().

Verificación: tidy() devuelve una fila por coeficiente, glance() devuelve una sola fila, y augment() devuelve 234 filas.

En palabras: summary() enseña la misma información. ¿Por qué preferir broom? Contesta con algo que puedas hacer con la salida de tidy() y no con la de summary().

# Escribe tu código aquí

31.6.7 Ejercicio 7 (BONO). Un flujo de tidymodels

Monta con tidymodels el mismo modelo del ejercicio anterior: una receta, un modelo linear_reg() con motor lm, y un workflow(). Ajústalo y saca los coeficientes con extract_fit_parsnip() y tidy().

Verificación: los coeficientes tienen que ser idénticos a los del lm() del Ejercicio 6.

En palabras: si dan exactamente lo mismo y ocupa cinco veces más código, ¿en qué situación vale la pena tidymodels? Da un ejemplo concreto de tu propio trabajo.

# Escribe tu código aquí