Capítulo27 Iteración: purrr
El tema proviene de los siguientes sitios.
English: https://r4ds.had.co.nz/iteration.html
Español: https://es.r4ds.hadley.nz/iteración.html
Fecha de la última revisión
## [1] "2026-09-09"
27.1 Temas: El paquete purrr
- La idea de iterar (repetir una acción)
- El bucle
forde base R - La familia
map()depurrr - Fórmulas anónimas con
~y.x - Iterar sobre dos listas con
map2() across(): iterar sobre columnas dentro dedplyrwalk(): iterar para hacer algo, no para calcularsafely(): cuando alguna vuelta puede fallar- Un modelo por grupo
- Cuándo un
forsigue siendo mejor
purrr es parte de tidyverse, así que se activa con library(tidyverse).
27.2 ¿Qué es iterar?
Iterar es repetir la misma acción sobre cada elemento de una lista, vector o columna. Igual que con las funciones, la meta es no repetir código. Si las funciones evitan copiar y pegar código dentro de una tarea, la iteración evita copiar y pegar la misma tarea muchas veces.
Supón que queremos la mediana de cada columna de este data frame:
df <- tibble(
a = rnorm(10),
b = rnorm(10),
c = rnorm(10),
d = rnorm(10)
)
# Repetir a mano es tedioso y propenso a errores:
median(df$a)## [1] -0.2364174
## [1] 0.2332814
## [1] 1.136037
## [1] 0.4082237
27.3 El bucle for de base R
La forma clásica de iterar es con un bucle for:
resultado <- vector("double", ncol(df)) # espacio para guardar la salida
for (i in seq_along(df)) { # para cada columna
resultado[[i]] <- median(df[[i]]) # calcular y guardar
}
resultado## [1] -0.2364174 0.2332814 1.1360375 0.4082237
Funciona, pero hay que escribir bastante “andamiaje” (crear el vector de salida,
manejar el índice i). purrr simplifica esto.
Crea el vector de salida con su tamaño final antes del bucle. Hacerlo crecer dentro del for (por ejemplo con c() en cada vuelta) obliga a R a copiarlo una y otra vez y vuelve el código muy lento.
27.4 La familia map()
La idea de map (aplicar una función a cada elemento) viene de la programación funcional y del lenguaje Lisp (años 1960). El paquete purrr, de Hadley Wickham y Lionel Henry, la trajo al tidyverse con una familia de funciones de tipos predecibles.
La familia map() (purrr): aplicar una función a cada elemento.
map(x, f): aplicafa cada elemento y devuelve una lista.- Variantes por tipo:
map_dbl(),map_int(),map_chr(),map_lgl()devuelven un vector del tipo indicado. - Fórmula anónima:
~ f(.x)define una función corta donde.xes cada elemento;map2()itera sobre dos listas a la vez.
Las funciones map() toman un vector o lista, aplican una función a cada
elemento, y devuelven un resultado. Hay una versión por tipo de salida:
map()devuelve una listamap_dbl()devuelve un vector de doblesmap_int()devuelve un vector de enterosmap_chr()devuelve un vector de textomap_lgl()devuelve un vector lógico
Esa lista de variantes no es un capricho. La diferencia importante es que
map_dbl() comprueba el tipo: si alguna vuelta devuelve algo que no es un
número, se detiene con un error. map() no comprueba nada y te devuelve una lista
con cosas de tipos distintos, que descubres tres pasos después.
Usa siempre la variante más específica que puedas: map_dbl() si esperas
números, map_chr() si esperas texto. El error inmediato vale mucho más que una
lista rara. Deja map() a secas para cuando cada vuelta devuelve algo complejo,
como un modelo o un data frame.
## a b c d
## -0.2364174 0.2332814 1.1360375 0.4082237
## a b c d
## -0.3534311 0.1467468 1.0998756 0.2888905
## a b c d
## 0.9753734 0.8503055 0.5548967 1.0877976
Compara: el bucle for completo de arriba se reduce a map_dbl(df, median).
27.5 Fórmulas anónimas con ~ y .x
A veces necesitas una pequeña operación que no tiene nombre de función. Puedes
escribirla como una fórmula empezando con ~, y usar .x para representar
“el elemento actual”.
## a b c d
## 3.042304 2.203092 1.832097 3.338961
## a b c d
## 3 5 10 6
Esto es muy útil con datos reales. Por ejemplo, el promedio de atraso de salida por mes en los vuelos:
vuelos %>%
split(.$mes) %>% # una lista: un data frame por mes
map_dbl(~ mean(.x$atraso_salida, na.rm = TRUE)) # promedio de atraso por mes## 1 2 3 4 5 6 7 8
## 10.036665 10.816843 13.227076 13.938038 12.986859 20.846332 21.727787 12.611040
## 9 10 11 12
## 6.722476 6.243988 5.435362 16.576688
27.6 Iterar sobre dos listas con map2()
Cuando necesitas recorrer dos listas en paralelo, usa map2(). Los elementos
se representan con .x (de la primera) y .y (de la segunda).
medias <- c(0, 10, 100)
desv <- c(1, 2, 3)
# Generar 5 números al azar de cada combinación de media y desviación
map2(medias, desv, ~ rnorm(5, mean = .x, sd = .y))## [[1]]
## [1] 1.01720062 -0.45401384 1.11807753 0.02410736 -0.30014527
##
## [[2]]
## [1] 13.09959 10.12764 10.11323 9.00243 10.06829
##
## [[3]]
## [1] 99.31260 99.88415 98.67224 100.17103 103.52231
27.7 across(): iterar sobre columnas dentro de dplyr
Para el caso concreto de “hacerle lo mismo a varias columnas de una tabla”,
dplyr tiene su propia herramienta y suele ser la mejor opción, porque el
resultado sale como tabla y encaja con group_by().
across() (dplyr): aplicar una función a varias columnas dentro de mutate() o summarise().
summarise(across(where(is.numeric), mean)): el promedio de todas las columnas numéricas.across(c(a, b), ~ .x / 100): columnas escogidas a mano, con una fórmula anónima.- Varias funciones a la vez:
across(where(is.numeric), list(media = mean, sd = sd)). - Con
group_by(): funciona por grupo, que es lo que casi nunca es cómodo hacer conmap().
## # A tibble: 1 × 5
## cilindrada anio cilindros ciudad autopista
## <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 3.47 2004. 5.89 16.9 23.4
## # A tibble: 7 × 3
## clase ciudad autopista
## <chr> <dbl> <dbl>
## 1 2asientos 15.4 24.8
## 2 compacto 20.1 28.3
## 3 mediano 18.8 27.3
## 4 minivan 15.8 22.4
## 5 pickup 13 16.9
## 6 subcompacto 20.4 28.1
## 7 suv 13.5 18.1
\(x) mean(x) es la manera moderna de escribir una función anónima en R, desde
la versión 4.1. Es lo mismo que function(x) mean(x), solo que más corto. En
purrr también se puede usar ~ mean(.x); las tres formas conviven y vas a
encontrarte las tres en código ajeno.
La regla para escoger: across() cuando trabajas dentro de una tabla, y
map() cuando trabajas con una lista de cosas que no son columnas, como una
lista de modelos, de archivos o de gráficas.
27.8 walk(): iterar para hacer algo
A veces no quieres un resultado: quieres que pase algo. Guardar diez archivos,
imprimir diez gráficas, escribir diez mensajes. Para eso está walk(), que hace
exactamente lo mismo que map() pero descarta el resultado y devuelve la
entrada sin cambios.
nombres <- c("compact", "suv", "pickup")
walk(nombres, \(cl) {
millas |>
filter(clase == cl) |>
write_csv(paste0("Datos/millas_", cl, ".csv"))
})Si usaras map() para eso, funcionaría igual pero además te imprimiría en la
consola una lista con los tres data frames enteros, que no le interesan a nadie.
walk() existe para decir “esto es por el efecto, no por el valor”.
27.9 safely(): cuando alguna vuelta puede fallar
Un map() sobre cuarenta grupos se detiene entero si uno solo falla, y no te
dice cuál. safely() envuelve una función para que en vez de fallar devuelva una
lista con dos partes: result y error. Una de las dos siempre es NULL.
safely(), possibly() y quietly() (purrr): iterar sin que un fallo lo pare todo.
safely(f): devuelvelist(result = ..., error = ...)en cada vuelta. La que falló trae el error yresult = NULL.possibly(f, otherwise = NA): más simple, devuelve el valor que le digas cuando falla.quietly(f): captura también los mensajes y las advertencias, en vez de dejarlos salir por la consola.
promedio_exigente <- function(x) {
if (nrow(x) < 10) stop("muy pocos datos")
mean(x$autopista)
}
resultados <- millas |>
split(millas$clase) |>
purrr::map(safely(promedio_exigente))
# ¿cuáles fallaron?
resultados |> purrr::map("error") |> compact() |> names()## [1] "2asientos"
## compacto mediano minivan pickup subcompacto suv
## 28.29787 27.29268 22.36364 16.87879 28.14286 18.12903
Fíjate en que aquí se escribe purrr::map() y no map() a secas. En el capítulo
de datos relacionados se activó el paquete maps (el de los mapas del mundo), y
ese paquete también tiene una función llamada map(). Como se activó después,
la suya tapa a la de purrr en todo lo que viene a continuación del libro, y una
llamada a map() sin apellido acaba en un error rarísimo sobre bases de datos
geográficas.
Es exactamente el enmascaramiento del que avisa library() con su mensaje “The
following object is masked from…”, el mismo del capítulo de opciones de knitr.
La solución es la de siempre: poner el apellido del paquete con ::.
compact() quita los NULL de una lista, así que las dos líneas de arriba
separan lo que funcionó de lo que no. Es el patrón estándar para procesar muchos
archivos o muchos grupos sabiendo de antemano que alguno va a dar problemas.
27.10 Un modelo por grupo
Un uso poderoso es ajustar un modelo por grupo. Aquí ajustamos una regresión del atraso de llegada contra el de salida, por separado para cada aerolínea.
Como el paquete maps también se carga en este libro, escribimos purrr::map() de forma explícita para no confundirla con maps::map().
modelos <- vuelos %>%
split(.$aerolinea) %>%
purrr::map(~ lm(atraso_llegada ~ atraso_salida, data = .x))
# Ver el R^2 de cada modelo
modelos %>%
purrr::map(summary) %>%
map_dbl("r.squared") %>%
head()## 9E AA AS B6 DL EV
## 0.8622935 0.7952061 0.7012039 0.8369837 0.8192728 0.9080101
Este patrón (dividir, aplicar un modelo a cada parte, y resumir) es la base del
tema de muchos modelos con purrr y broom.
map_dbl("r.squared") funciona porque, cuando en vez de una función le pasas un
texto, map() lo entiende como “sácame ese elemento de cada uno”. Es un atajo
muy usado y algo críptico la primera vez que se ve. Lo mismo con un número:
map_dbl(lista, 1) saca el primer elemento de cada uno.
27.11 Cuándo un for sigue siendo mejor
map() no sustituye al for en todos los casos. El bucle sigue siendo la
herramienta correcta cuando:
- cada vuelta depende de la anterior, como en una simulación de crecimiento
poblacional donde el año siguiente se calcula a partir del actual.
map()supone que las vueltas son independientes; - no hay nada que devolver y el código es largo, aunque para eso normalmente
walk()queda más claro; - estás depurando: dentro de un
forpuedes parar en la vuelta 17 y mirar el estado de todo. Dentro de unmap()eso es bastante más incómodo.
Y al revés: en cuanto haya que guardar un resultado por vuelta, map() gana,
porque se ocupa solo de crear el vector de salida del tamaño y el tipo correctos,
que es justo la parte del for donde se cometen los errores.
27.12 Ejercicios del libro R4DS
Hacer los ejercicios de las secciones 21.5 y 21.9 del libro en español.
27.13 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 el
df de números al azar, vuelos ni diamantes, que son los del capítulo. La hoja
completa para entregar está en Ejercicios/Ejercicios_Capitulo_27_iteracion.Rmd.
27.13.1 Ejercicio 1. Del for al map()
Escribe un bucle for que calcule el promedio de cada columna numérica de
millas, creando el vector de salida con su tamaño final antes del bucle. Después
escribe la misma cosa con map_dbl(), en una línea.
Verificación: millas tiene 5 columnas numéricas, y los dos caminos tienen
que dar exactamente los mismos cinco números.
En palabras: cuenta cuántas líneas ocupa cada versión. ¿Cuál de las partes del
for desapareció al usar map_dbl(), y por qué esa parte era justamente la que
más errores causa?
27.13.2 Ejercicio 2. El tipo importa
Cuenta los valores distintos de todas las columnas de millas con
n_distinct(), de tres maneras: con map_dbl(), con map_chr() y con map() a
secas. Pon el que falle en un bloque con error=TRUE.
Verificación: una de las tres da error y las otras dos funcionan, pero
devuelven cosas de clase distinta. Compruébalo con class().
En palabras: ¿por qué falla la que falla? ¿Cuál de las tres usarías y por qué? Relaciónalo con la idea de que un error inmediato vale más que un resultado raro.
27.13.3 Ejercicio 3. Fórmulas anónimas
Con millas, usa map_int() y una fórmula anónima con ~ y .x para contar,
en cada columna, cuántos valores distintos hay. Después usa map_lgl() para saber
cuáles columnas son numéricas.
Verificación: map_lgl(millas, is.numeric) tiene que dar 5 TRUE, y
clase tiene que salir con 7 valores distintos.
En palabras: escribe la misma fórmula anónima de las tres maneras que existen:
con ~ .x, con \(x) y con function(x). ¿Cuál prefieres y por qué?
27.13.4 Ejercicio 4. Un modelo por grupo
Divide millas por clase con split() y ajusta, para cada clase, una regresión
de autopista contra cilindrada. Saca después el \(R^2\) de cada una.
Verificación: tienen que salir 7 modelos y 7 valores de \(R^2\), todos entre 0 y 1.
En palabras: ¿en cuál clase explica mejor la cilindrada el consumo, y en cuál peor? Mira cuántos vehículos tiene cada clase antes de sacar conclusiones: ¿te fías igual del \(R^2\) de una clase con 5 vehículos que del de una con 62?
27.13.5 Ejercicio 5. across(), la alternativa dentro de la tabla
Consigue el resultado del Ejercicio 1 otra vez, pero ahora con across() dentro
de summarise(). Después hazlo por clase, con group_by().
Verificación: la primera versión da una tabla de 1 fila y 5 columnas; la segunda, una de 7 filas.
En palabras: compara la salida de map_dbl() con la de across(). ¿Qué te da
una que no te da la otra? ¿Cuál de las dos podrías haber usado para hacer la
versión por clase, y cuál no sin complicarte mucho?
27.13.6 Ejercicio 6. Cuando una vuelta falla
Escribe una función que calcule el promedio de autopista de un grupo pero que
dé error si el grupo tiene menos de 10 filas. Aplícala a los grupos de clase
de dos maneras: con map() a secas, en un bloque con error=TRUE, y con
safely().
Verificación: con map() a secas se detiene todo y no obtienes ningún
resultado. Con safely() obtienes los resultados de las clases que sí tenían
suficientes filas, y una lista con los nombres de las que fallaron.
En palabras: ¿cuál clase fue la culpable? ¿Por qué safely() es
imprescindible cuando procesas cien archivos y no sabes si alguno está corrupto?
27.13.7 Ejercicio 7 (BONO). walk() y map2()
Con walk(), guarda un archivo .csv por cada clase de millas dentro de una
carpeta Datos/por_clase/, con el nombre de la clase en el nombre del archivo.
Después usa map2() para hacer lo mismo pasando dos listas en paralelo: la de
tablas y la de nombres de archivo.
Verificación: tienen que quedar 7 archivos en la carpeta. Compruébalo con
list.files().
En palabras: ¿qué habría pasado si hubieras usado map() en vez de walk()?
Pruébalo. ¿Por qué existe walk() si map() hace lo mismo?