1 Introducción

Las curvas de rango–abundancia de Whittaker (RACs) ordenan las especies de una comunidad desde la más abundante (rango 1) a la menos abundante, graficando su abundancia relativa (típicamente en escala logarítmica) frente al rango.

Longitud de la curva → riqueza (más especies = curva más larga).

Pendiente → equidad (pendiente pronunciada = alta dominancia y baja equidad; pendiente suave = comunidad más pareja).

Este enfoque, originado en los años 60 (Whittaker, 1965), sigue vigente porque visualiza simultáneamente dominancia, rareza y estructura y facilita comparaciones entre comunidades, sitios y tiempos. Avances recientes sugieren (i) combinar RACs con normalización del esfuerzo (p. ej., rarefacción) para comparaciones justas; (ii) usar marcos de dinámica de comunidades que cuantifican cambios en riqueza, equidad y reordenamiento (Avolio et al., 2015; 2019); y (iii) contextualizar las RACs con la literatura de SADs (distribuciones de abundancia de especies), donde varios modelos (logserie, lognormal, etc.) pueden ajustarse de forma similar y conviene evaluar con métricas objetivas (McGill et al., 2007; Baldridge et al., 2016; Koffel et al., 2022; Callaghan et al., 2023).

2 Funciones auxiliares

# Convierte un vector de conteos (una comunidad) en tabla de rango–abundancia
rank_abundance <- function(x) {
  x <- x[x > 0]               # quita ceros
  x <- sort(x, decreasing = TRUE)
  tibble(
    rango = seq_along(x),
    abund = as.numeric(x),
    prop  = abund / sum(abund)
  )
}

# Gráfico RAC con ggplot2 (prop. o abundancia bruta; eje y opcional log10)
plot_rac <- function(df, y = c("prop","abund"), log10_y = TRUE, title = NULL) {
  y <- match.arg(y)
  g <- ggplot(df, aes(x = rango, y = .data[[y]])) +
    geom_line() +
    geom_point() +
    labs(x = "Rango (1 = más abundante)",
         y = ifelse(y == "prop", "Abundancia relativa", "Abundancia absoluta"),
         title = title) +
    theme_minimal(base_size = 12)
  if (log10_y) g <- g + scale_y_continuous(trans = "log10", labels = scales::label_number())
  g
}

# Ajuste rápido de pendiente (en log10) para interpretar equidad
pendiente_rac <- function(df) {
  fit <- lm(log10(prop) ~ rango, data = df)
  unname(coef(fit)[2])  # pendiente
}

3 Datos de ejemplo (vegan)

Usaremos conjuntos incorporados en vegan:

dune (vegetación de dunas; 20 sitios × 30 spp.)

varespec (comunidades de líquenes; 24 sitios × 44 spp.)

data(dune)      # 20 x 30
data(varespec)  # 24 x 44
dim(dune); dim(varespec)
## [1] 20 30
## [1] 24 44

4 RAC básica en un solo sitio

Tomemos un sitio de dune y construyamos su curva.

com1 <- as.numeric(dune[1, ])     # comunidad del sitio 1
rac1  <- rank_abundance(com1)
head(rac1, 10)
## # A tibble: 5 × 3
##   rango abund   prop
##   <int> <dbl>  <dbl>
## 1     1     7 0.389 
## 2     2     4 0.222 
## 3     3     4 0.222 
## 4     4     2 0.111 
## 5     5     1 0.0556
plot_rac(rac1, y = "prop", log10_y = TRUE, title = "RAC (dune, sitio 1)")

cat("Pendiente (log10 prop ~ rango):", round(pendiente_rac(rac1), 3), "\n")
## Pendiente (log10 prop ~ rango): -0.199

Lectura rápida: pendientes más negativas implican menor equidad (más dominancia).

5 Comparar dos comunidades “con juego limpio”

Las comparaciones deben estandarizarse por tamaño de muestra. Aquí igualamos el número total de individuos de dos sitios usando rarefacción (rrarefy) y luego comparamos sus RACs.

# elegimos dos sitios de dune
com2 <- as.numeric(dune[5, ])

# Tamaños totales
n1 <- sum(com1); n2 <- sum(com2); n1; n2
## [1] 18
## [1] 43
# Igualamos al mínimo
n_min <- min(n1, n2)

com1_std <- as.numeric(rrarefy(matrix(com1, nrow = 1), sample = n_min))
com2_std <- as.numeric(rrarefy(matrix(com2, nrow = 1), sample = n_min))

rac1_std <- rank_abundance(com1_std) %>% dplyr::mutate(comunidad = "Sitio 1 (std)")
rac2_std <- rank_abundance(com2_std) %>% dplyr::mutate(comunidad = "Sitio 5 (std)")

rac_comp <- dplyr::bind_rows(rac1_std, rac2_std)

ggplot(rac_comp, aes(rango, prop, color = comunidad)) +
  geom_line() + geom_point() +
  scale_y_continuous(trans = "log10") +
  labs(x = "Rango", y = "Abundancia relativa (log10)",
       title = "Comparación RAC con rarefacción (mismo esfuerzo)") +
  theme_minimal(base_size = 12)

data.frame(
  Comunidad = c("Sitio 1 (std)", "Sitio 5 (std)"),
  Pendiente = c(pendiente_rac(rac1_std), pendiente_rac(rac2_std))
) |>
  dplyr::mutate(Pendiente = round(Pendiente, 3))
##       Comunidad Pendiente
## 1 Sitio 1 (std)    -0.199
## 2 Sitio 5 (std)    -0.073

Interpretación: a igual esfuerzo, la curva más larga indica mayor riqueza, y la pendiente menos negativa sugiere mayor equidad.

6 Efecto de la equidad (ejemplo sintético)

Construimos dos comunidades con igual riqueza (S = 20) pero distinta equidad.

S <- 20
# Comunidad A: muy pareja
com_A <- rep(10, S)
# Comunidad B: dominante + raras
com_B <- c(100, rep(2, S-1))

rac_A <- rank_abundance(com_A) %>% dplyr::mutate(comunidad = "Pareja")
rac_B <- rank_abundance(com_B) %>% dplyr::mutate(comunidad = "Dominante")
rac_AB <- dplyr::bind_rows(rac_A, rac_B)

ggplot(rac_AB, aes(rango, prop, color = comunidad)) +
  geom_line() + geom_point() +
  scale_y_continuous(trans = "log10") +
  labs(x = "Rango", y = "Abundancia relativa (log10)",
       title = "Misma riqueza, distinta equidad") +
  theme_minimal(base_size = 12)

data.frame(
  Comunidad = c("Pareja", "Dominante"),
  Pendiente = c(pendiente_rac(rac_A), pendiente_rac(rac_B))
) |>
  dplyr::mutate(Pendiente = round(Pendiente, 3))
##   Comunidad Pendiente
## 1    Pareja     0.000
## 2 Dominante    -0.024

7 Ajuste de modelos SAD (logserie, lognormal, etc.)

El ajuste de distribuciones de abundancia de especies (SADs) complementa la lectura de las RACs. Con vegan::radfit obtenemos modelos clásicos y sus AIC.

fit <- radfit(com1)    # sobre la comunidad del sitio 1
fit
## 
## RAD models, family poisson 
## No. of species 5, total abundance 18
## 
##            par1     par2        par3        Deviance AIC      BIC     
## Null                                         0.89431 15.84710 15.84710
## Preemption  0.36537                          0.69044 17.64324 17.25267
## Lognormal   1.1125   0.72822                 0.38771 19.34050 18.55938
## Zipf        0.41068 -0.89683                 0.89538 19.84817 19.06705
## Mandelbrot    Inf   -3.8045e+07  9.1134e+07  0.44565 21.39844 20.22675
# Gráfico diagnóstico de radfit
plot(fit, main = "Ajustes SAD sobre la RAC (dune, sitio 1)")

Nota: Diferencias pequeñas en AIC entre modelos indican que varios modelos explican patrones similares; conviene usar criterios adicionales (p. ej., rasgos funcionales, dispersión, o evidencia multi-sitio).

8 RACs en serie temporal (mini-demo)

Simulamos un “antes/después” alterando levemente abundancias para ilustrar cómo cambia la pendiente/equidad.

antes  <- com1_std
despues <- com1_std
# Aumentamos la especie dominante y reducimos varias raras
despues[which.max(despues)] <- despues[which.max(despues)] + 10
pos <- which(despues > 0)
raras <- if (length(pos) > 5) tail(pos, 5) else pos
despues[raras] <- pmax(0, despues[raras] - 1)

rac_antes  <- rank_abundance(antes)  %>% dplyr::mutate(tiempo = "Antes")
rac_desp   <- rank_abundance(despues) %>% dplyr::mutate(tiempo = "Después")
rac_td <- dplyr::bind_rows(rac_antes, rac_desp)

ggplot(rac_td, aes(rango, prop, color = tiempo)) +
  geom_line() + geom_point() +
  scale_y_continuous(trans = "log10") +
  labs(x = "Rango", y = "Abundancia relativa (log10)",
       title = "Cambio temporal en la RAC (simulación)") +
  theme_minimal(base_size = 12)

data.frame(
  Tiempo    = c("Antes","Después"),
  Pendiente = c(pendiente_rac(rac_antes), pendiente_rac(rac_desp))
) |>
  dplyr::mutate(Pendiente = round(Pendiente, 3))
##    Tiempo Pendiente
## 1   Antes    -0.199
## 2 Después    -0.361

9 Recomendaciones prácticas

  • Usa rarefacción (o cobertura muestral equivalente) antes de comparar RACs entre sitios.

  • Reporta riqueza (longitud) y equidad (pendiente) conjuntamente.

  • Complementa con ajuste de SADs (radfit) y justifica el modelo con criterios ecológicos, no solo AIC.

  • Si tienes muchas muestras/tiempos, considera métricas de dinámica de comunidades (p. ej., cambios en rango/turnover) además de la forma de la RAC.

10 Ejercicio “Estudio de Caso-Trichopteros en tres gradientes de altura”

Los siguientes son datos de abundancia de genros de trichopteros recolectados en el río Ranchería sobre tres gradientes de altura

Género Alta Baja Media
Atanatolica 1 0 0
Atopsyche 3 0 0
Betrichia 0 1 0
Chimarra 89 39 33
Culoptila 0 0 2
Helicopsyche 0 5 6
Leptonema 87 1 7
Marilia 0 1 0
Mayatrichia 2 0 0
Nectopsyche 0 3 0
Neotrichia 7 19 0
Oecetis 20 0 0
Oxyethira 8 2 3
Phylloicus 4 0 3
Protoptila 5 31 16
Smicridea 573 378 219
# Cargar librerías necesarias
library(ggplot2)
library(dplyr)
library(tidyr)
library(gridExtra)
library(ggrepel)
library(cowplot)

# Crear los datos
datos <- data.frame(
  Generos = c("Atanatolica", "Atopsyche", "Betrichia", "Chimarra", "Culoptila", 
              "Helicopsyche", "Leptonema", "Marilia", "Mayatrichia", "Nectopsyche", 
              "Neotrichia", "Oecetis", "Oxyethira", "Phylloicus", "Protoptila", "Smicridea"),
  Alta = c(1, 3, 0, 89, 0, 0, 87, 0, 2, 0, 7, 20, 8, 4, 5, 573),
  Baja = c(0, 0, 1, 39, 0, 5, 1, 1, 0, 3, 19, 0, 2, 0, 31, 378),
  Media = c(0, 0, 0, 33, 2, 6, 7, 0, 0, 0, 0, 0, 3, 3, 16, 219)
)

# Crear abreviaciones apropiadas para cada género
abreviaciones <- c(
  "Atanatolica" = "Atan",
  "Atopsyche" = "Atop", 
  "Betrichia" = "Betr",
  "Chimarra" = "Chim",
  "Culoptila" = "Culo",
  "Helicopsyche" = "Heli",
  "Leptonema" = "Lept",
  "Marilia" = "Mari",
  "Mayatrichia" = "Maya",
  "Nectopsyche" = "Nect",
  "Neotrichia" = "Neot",
  "Oecetis" = "Oece",
  "Oxyethira" = "Oxye",
  "Phylloicus" = "Phyl",
  "Protoptila" = "Prot",
  "Smicridea" = "Smic"
)

# Función para calcular abundancia relativa y crear datos para gráfico de Whittaker
calcular_whittaker <- function(abundancias, localidad_nombre) {
  # Filtrar especies con abundancia > 0
  datos_filtrados <- data.frame(
    genero = datos$Generos,
    abundancia = abundancias
  ) %>%
    filter(abundancia > 0) %>%
    arrange(desc(abundancia))
  
  # Calcular abundancia total
  abundancia_total <- sum(datos_filtrados$abundancia)
  
  # Calcular abundancia relativa
  datos_filtrados$abundancia_relativa <- datos_filtrados$abundancia / abundancia_total
  
  # Asignar rangos
  datos_filtrados$rango <- 1:nrow(datos_filtrados)
  
  # Añadir abreviaciones
  datos_filtrados$abreviacion <- abreviaciones[datos_filtrados$genero]
  
  # Añadir información de localidad
  datos_filtrados$localidad <- localidad_nombre
  
  return(datos_filtrados)
}

# Calcular datos de Whittaker para cada localidad
datos_alta <- calcular_whittaker(datos$Alta, "Alta")
datos_baja <- calcular_whittaker(datos$Baja, "Baja") 
datos_media <- calcular_whittaker(datos$Media, "Media")

# Combinar todos los datos
datos_whittaker <- rbind(datos_alta, datos_baja, datos_media)

# Crear colores ultra-vibrantes y altamente contrastantes (SIN transparencias)
colores_vibrantes <- c(
  "Smicridea" = "#FF0000",      # Rojo puro
  "Chimarra" = "#FF8C00",       # Naranja vibrante  
  "Leptonema" = "#00FF00",      # Verde lima puro
  "Protoptila" = "#00BFFF",     # Azul cielo profundo
  "Neotrichia" = "#8A2BE2",     # Violeta azulado
  "Oecetis" = "#0000FF",        # Azul puro
  "Helicopsyche" = "#FF1493",   # Rosa fucsia
  "Oxyethira" = "#8B4513",      # Marrón silla
  "Phylloicus" = "#9ACD32",     # Verde amarillo
  "Atopsyche" = "#FF6347",      # Tomate
  "Nectopsyche" = "#4682B4",    # Azul acero
  "Mayatrichia" = "#9370DB",    # Púrpura medio
  "Atanatolica" = "#FFB6C1",    # Rosa claro
  "Betrichia" = "#A9A9A9",      # Gris oscuro
  "Culoptila" = "#ADFF2F",      # Verde amarillo
  "Marilia" = "#DEB887"         # Madera clara
)

# Función para crear gráfico individual optimizado
crear_grafico_whittaker <- function(datos_localidad, titulo_localidad, mostrar_eje_y = TRUE) {
  
  p <- ggplot(datos_localidad, aes(x = rango, y = abundancia_relativa)) +
    geom_line(color = "black", size = 2, alpha = 1) +  # Línea más gruesa y sin transparencia
    geom_point(aes(color = genero), size = 4, alpha = 1) +  # Puntos más grandes y opacos
    geom_text_repel(
      aes(label = abreviacion, color = genero),
      size = 4,
      fontface = "italic",
      point.padding = 0.3,
      box.padding = 0.6,
      segment.color = NA,
      max.overlaps = Inf,
      force = 2,
      nudge_x = 0.6,
      nudge_y = 0.01,
      show.legend = FALSE,
      min.segment.length = 0
    ) +
    scale_y_log10(
      breaks = c(0.001, 0.01, 0.1, 1),
      labels = c("0.001", "0.01", "0.100", "1"),
      limits = c(0.0008, 1.3)
    ) +
    scale_x_continuous(
      breaks = seq(0, 12, by = 2),
      limits = c(0, max(datos_localidad$rango) + 3)
    ) +
    scale_color_manual(values = colores_vibrantes, name = "Taxones") +
    labs(
      title = titulo_localidad,
      x = "Rango de las especies"
    ) +
    theme_minimal() +
    theme(
      panel.border = element_rect(color = "black", fill = NA, size = 1.3),
      plot.title = element_text(hjust = 0.5, size = 16, face = "bold", 
                                margin = margin(b = 12)),
      axis.title.x = element_text(size = 12, margin = margin(t = 10)),
      axis.text = element_text(size = 11, color = "black", face = "bold"),
      legend.position = "none",
      panel.grid.minor = element_blank(),
      panel.grid.major = element_line(color = "grey92", size = 0.3),
      plot.margin = margin(12, 15, 12, 12),
      panel.background = element_rect(fill = "white", color = NA)
    )
  
  # Añadir etiqueta Y solo si se requiere
  if(mostrar_eje_y) {
    p <- p + labs(y = "log10(Abundancia relativa)") +
      theme(axis.title.y = element_text(size = 12, margin = margin(r = 10)))
  } else {
    p <- p + labs(y = NULL) +
      theme(axis.title.y = element_blank())
  }
  
  return(p)
}

# Crear gráficos individuales - SOLO el primero tendrá etiqueta Y
grafico_alta <- crear_grafico_whittaker(datos_alta, "Alta", mostrar_eje_y = TRUE)
grafico_baja <- crear_grafico_whittaker(datos_baja, "Baja", mostrar_eje_y = FALSE)
grafico_media <- crear_grafico_whittaker(datos_media, "Media", mostrar_eje_y = FALSE)

# Crear leyenda compacta con colores vibrantes
crear_leyenda_compacta <- function() {
  # Obtener géneros únicos presentes en los datos
  generos_presentes <- unique(datos_whittaker$genero)
  generos_ordenados <- sort(generos_presentes)
  
  # Crear data frame para la leyenda
  df_leyenda <- data.frame(
    x = rep(1, length(generos_ordenados)),
    y = length(generos_ordenados):1,
    genero = generos_ordenados,
    stringsAsFactors = FALSE
  )
  
  ggplot(df_leyenda, aes(x = x, y = y)) +
    geom_point(aes(color = genero), size = 4.5, alpha = 1) +  # Puntos más grandes y opacos
    geom_text(aes(label = genero, color = genero), 
              x = 1.3, 
              fontface = "italic", 
              size = 4.2,  # Texto más grande
              hjust = 0,
              show.legend = FALSE) +
    scale_color_manual(values = colores_vibrantes) +
    xlim(0.8, 2.8) +
    ylim(0.3, length(generos_ordenados) + 0.7) +
    labs(title = "Taxones") +
    theme_void() +
    theme(
      plot.title = element_text(size = 14, face = "bold", hjust = 0, margin = margin(b = 8)),
      plot.margin = margin(8, 2, 8, 2),
      legend.position = "none"
    )
}

# Crear la leyenda compacta
leyenda_compacta <- crear_leyenda_compacta()

# Crear figura combinada MÁS COMPACTA
figura_combinada <- plot_grid(
  grafico_alta, grafico_baja, grafico_media, leyenda_compacta,
  ncol = 4,
  rel_widths = c(1.1, 0.9, 0.9, 0.65),  # Más compacto
  align = "hv",
  axis = "tb"
)

# Crear título principal más compacto
titulo_principal <- ggdraw() + 
  draw_label("Figura 2: Patrón de rango abundancia para las localidades", 
             fontface = 'bold', 
             size = 18,
             y = 0.5) +
  theme(plot.margin = margin(8, 0, 8, 0))

# Figura final más compacta
figura_final <- plot_grid(
  titulo_principal, 
  figura_combinada, 
  ncol = 1, 
  rel_heights = c(0.08, 1)  # Título más pequeño
)

# Mostrar la figura
#print(figura_final)
_
_
# Guardar la figura COMPACTA con colores vibrantes
ggsave("whittaker_plots_vibrante_compacto.png", figura_final, 
       width = 15, height = 6.5, dpi = 300, units = "in", bg = "white")

# Guardar versión en PDF compacta
ggsave("whittaker_plots_vibrante_compacto.pdf", figura_final, 
       width = 15, height = 6.5, units = "in", device = "pdf")


# Tabla comparativa simple
análisis_compacto <- data.frame(
  Localidad = c("Alta", "Baja", "Media"),
  Riqueza = c(11, 10, 8),
  Abundancia = c(799, 480, 289),
  Dominancia_Smic = c("71.7%", "78.8%", "75.8%"),
  Shannon_H = c(1.007, 0.838, 0.918),
  Equitabilidad_J = c(0.420, 0.364, 0.441)
)

11 interpretación de resultados

Concepto principal: Las curvas de Whittaker muestran la estructura de dominancia en comunidades ecológicas, ordenando especies de mayor a menor abundancia en escala logarítmica. Patrón observado: Las tres localidades estudiadas presentan el patrón típico de “J invertida”, donde pocas especies dominan mientras muchas otras son raras. Esto es característico de ecosistemas acuáticos maduros. Diferencias entre localidades:

Localidad Baja: Curva muy empinada con dominancia extrema del género Smicridea (78.8%). Indica distribución muy desigual de recursos que favorece intensamente a pocas especies. Localidad Alta: Curva con pendiente más suave, mayor equilibrio en las abundancias y capacidad para mantener 11 géneros. Representa una comunidad más madura y diversificada. Localidad Media: Curva intermedia con solo 8 géneros, mostrando una comunidad compacta pero con distribución más equitativa que Baja.

Interpretación ecológica: Alta soporta mayor diversidad con más especies raras (mayor heterogeneidad ambiental), Baja está estructurada por fuerte competencia dominante, y Media representa una comunidad eficiente pero ambientalmente limitada. Conclusión: Las curvas revelan diferentes estados de madurez y estabilidad comunitaria, fundamentales para entender la dinámica de estas comunidades de macroinvertebrados acuáticos.

12 Referencias

Avolio, M. L., La Pierre, K. J., Houseman, G. R., Koerner, S. E., Grman, E., Isbell, F., et al. (2015). A framework for quantifying the magnitude and variability of community responses to global change drivers. Ecosphere, 6(12), 1–18. https://doi.org/10.1890/ES15-00317.1

Avolio, M. L., Forrestel, E. J., La Pierre, K. J., et al. (2019). A comprehensive approach to analyzing community dynamics using rank abundance curves. Ecosphere, 10(10), e02881. https://doi.org/10.1002/ecs2.2881

Baldridge, E., Harris, D. J., Xiao, X., & White, E. P. (2016). An extensive comparison of species‐abundance distribution models. PeerJ, 4, e2823. https://doi.org/10.7717/peerj.2823

Callaghan, C. T., Nakagawa, S., & Cornwell, W. K. (2023). Unveiling global species abundance distributions. Nature Ecology & Evolution, 7, 1188–1199. https://doi.org/10.1038/s41559-023-02173-y

Koffel, T., Umemura, K., Litchman, E., & Klausmeier, C. A. (2022). A general framework for species-abundance distributions: Linking traits and dispersal to explain commonness and rarity. Ecology Letters, 25(11), 2359–2371. https://doi.org/10.1111/ele.14094

McGill, B. J., Etienne, R. S., Gray, J. S., et al. (2007). Species abundance distributions: Moving beyond single prediction theories to integration within an ecological framework. Ecology Letters, 10(10), 995–1015. https://doi.org/10.1111/j.1461-0248.2007.01094.x

Whittaker, R. H. (1965). Dominance and diversity in land plant communities. Science, 147(3655), 250–260. https://doi.org/10.1126/science.147.3655.250