library(tidyverse)4 Listas y programación funcional
- R2. Importar y manipular bases de datos en R.
Hasta ahora usamos funciones que otros escribieron. En este capítulo aprendemos a escribir nuestras propias funciones y a aplicarlas repetidamente sobre colecciones de datos usando map(). Trabajaremos con una Pokédex.
atributos <- read_csv("data/atributos.csv")
stats <- read_csv("data/stats.csv")
tipo <- read_csv("data/tipo.csv")
habilidades <- read_csv("data/habilidades.csv")4.1 Escribir funciones
Una función encapsula una operación que queremos reutilizar. Su anatomía:
kilos_a_libras <- function(kg) {
kg * 2.20462
}
kilos_a_libras(70)[1] 154.3234
kilos_a_librases el nombre.kges el argumento (la entrada).- Lo que está entre llaves
{ }es el cuerpo; R devuelve el valor de la última línea.
Una función puede recibir varios argumentos:
calcular_bmi <- function(peso, altura) {
peso / (altura^2)
}
calcular_bmi(peso = 70, altura = 1.75)[1] 22.85714
Y se integra naturalmente con mutate():
atributos <- atributos %>%
mutate(
peso_lbs = kilos_a_libras(peso_kg),
bmi = calcular_bmi(peso_kg, altura_m)
)4.2 Listas
Una lista puede contener elementos de distinto tipo y tamaño. Es más flexible que un vector:
lista_ataques <- list(Pikachu = 55, Charizard = 84, Bulbasaur = 49)
lista_ataques$Pikachu
[1] 55
$Charizard
[1] 84
$Bulbasaur
[1] 49
4.3 Iterar con map()
map() aplica una función a cada elemento de una lista y devuelve una nueva lista con los resultados:
ataques_mejorados <- map(lista_ataques, function(x) x + 10)
ataques_mejorados$Pikachu
[1] 65
$Charizard
[1] 94
$Bulbasaur
[1] 59
split() divide un data frame en una lista según una variable. Combinado con map(), podemos operar grupo por grupo:
# Dividir por generación y contar Pokémon en cada una
lista_por_gen <- split(atributos, atributos$Gen)
conteos_gen <- map(lista_por_gen, nrow)map() y no un bucle for?
Ambos sirven para repetir operaciones, pero map() produce código más corto, predecible y fácil de leer. Es el enfoque que favorece el tidyverse (paquete purrr).
4.4 Un ejemplo completo: normalización por grupos
La normalización min-max reescala valores entre 0 y 1. La escribimos como función y la aplicamos dentro de cada generación:
normalizar <- function(x) {
(x - min(x, na.rm = TRUE)) / (max(x, na.rm = TRUE) - min(x, na.rm = TRUE))
}
normalizar(c(10, 20, 30, 40, 50))[1] 0.00 0.25 0.50 0.75 1.00
poke_completo <- atributos %>%
left_join(stats, by = "ndex")
lista_por_gen <- split(poke_completo, poke_completo$Gen)
velocidad_norm <- map(lista_por_gen, function(df) {
df %>% mutate(velocidad_norm = normalizar(velocidad))
})4.5 Pivot aplicado a estadísticas
Primero construimos una tabla base uniendo nombre, tipo y estadísticas:
poke_stats <- atributos %>%
select(ndex, nombre, Gen) %>%
left_join(tipo, by = "ndex") %>%
left_join(stats, by = "ndex")pivot_longer() también es clave en programación funcional: lleva las 6 estadísticas de combate a formato largo para resumirlas de un golpe:
poke_largo <- poke_stats %>%
pivot_longer(
cols = c(hp, ataque, defensa, at_esp, def_esp, velocidad),
names_to = "stat",
values_to = "valor"
)
# Promedio de cada estadística por generación
poke_largo %>%
group_by(Gen, stat) %>%
summarise(promedio = mean(valor, na.rm = TRUE), .groups = "drop")Ejercicios
¿Qué tipo de Pokémon tiene el mayor ataque promedio normalizado? Combina todo lo aprendido:
- Parte de
poke_statsy unehabilidad_principalconleft_join(). - Lleva las 6 estadísticas a formato largo con
pivot_longer(). - Normaliza
valordentro de cadastatcongroup_by()+mutate()+ tu funciónnormalizar(). - Filtra solo
stat == "ataque". - Calcula el promedio del valor normalizado por
tipo1. - Usa
slice_max()para encontrar el tipo con el valor más alto.