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()ybroom
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 foundEl 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 enmutate/summarisecuando 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:
- 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.
- {{ }} (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.
- group_by({{ group_var }}):
- Agrupa el conjunto de datos por la variable indicada.
- Ejemplo: Si group_var = especie, agrupa por especie.
- 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.
## [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 conlm, conglmneto constancambiando una palabra. - El flujo (
workflow()): junta receta y modelo en un solo objeto, que es el que se ajusta confit()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:
- 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. - Cambiar de método cuesta una palabra. Pasar de regresión lineal a bosque aleatorio no obliga a reescribir el resto.
- 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
## # 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>
##
## 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íaadd_predictions()yadd_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 columnadatadonde cada celda es el tibble completo de ese grupo.- Con
mutate()ymap()se le puede añadir una columnamodelo, donde cada celda es un objetolm, 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:
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
## [[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
## [[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
##
## 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 sí existe en millas. Explica entonces
dónde la fue a buscar R y por qué no la encontró.
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.
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 =?
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”.
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?
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().
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.