4  Listas y programación funcional

NoteResultados de aprendizaje
  • 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.

library(tidyverse)
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_libras es el nombre.
  • kg es 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)
Tip¿Por qué 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

ImportantEjercicio 4.1 — Desafío: ataque por tipo

¿Qué tipo de Pokémon tiene el mayor ataque promedio normalizado? Combina todo lo aprendido:

  1. Parte de poke_stats y une habilidad_principal con left_join().
  2. Lleva las 6 estadísticas a formato largo con pivot_longer().
  3. Normaliza valor dentro de cada stat con group_by() + mutate() + tu función normalizar().
  4. Filtra solo stat == "ataque".
  5. Calcula el promedio del valor normalizado por tipo1.
  6. Usa slice_max() para encontrar el tipo con el valor más alto.