Filogenias datadas

Por eso, los árboles datados suelen incluir:

En R, al importar un árbol anotado desde BEAST, toda esta información se organiza en el objeto tree$data, que actúa como una tabla con variables clave para analizar, visualizar y anotar el árbol.

🧰 Preparación de los datos

Primero cargamos los metadatos de las muestras y renombramos la columna Sample a label para que coincida con las etiquetas de las puntas del árbol (tip labels). Luego leemos el árbol anotado por BEAST.

library(treeio)
library(ggtree)
library(tidyverse)
library(deeptime)

# Metadata de las muestras
df_stenopelmatus <- read.csv("../docs/stenopelmatus.csv") %>%
  rename(label = Sample)   # renombra Sample → label para que haga match

head(df_stenopelmatus)
     label    Lat    Long                    Species                    Text
1 CNIN3620 15.649 -92.809 Stenopelmatus_micropterous 40: S. sp. aff. chiapas
2 CNIN3621 15.649 -92.809 Stenopelmatus_micropterous                    <NA>
3 CNIN3624 15.722 -92.939 Stenopelmatus_macropterous      25: S. sartorianus
4 CNIN3625 15.722 -92.939 Stenopelmatus_macropterous                    <NA>
5 CNIN3626 15.722 -92.939 Stenopelmatus_macropterous                    <NA>
6 CNIN3630 19.631 -97.009     Stenopelmatus_apterous                    <NA>
    Color Presencia_alas                                      Region
1   negro             no          Mexico: Chiapas Highlands province
2   negro             no          Mexico: Chiapas Highlands province
3 naranja             si          Mexico: Chiapas Highlands province
4 naranja             si          Mexico: Chiapas Highlands province
5 naranja             si          Mexico: Chiapas Highlands province
6    rojo             no Mexico: Transmexican Volcanic Belt province
# Leer el árbol anotado por BEAST
beast_tree <- read.beast("../docs/Beast_Stenopelmatinae_cox1.tre")

head(beast_tree@phylo$tip.label)
[1] "CNIN3620_Cox1" "CNIN3621_Cox1" "CNIN3624_Cox1" "CNIN3625_Cox1"
[5] "CNIN3626_Cox1" "CNIN3630_Cox1"

Normalizamos las etiquetas del árbol de BEAST eliminando sufijos que no forman parte del identificador de la muestra (por ejemplo, el marcador _Cox1 o _Cox1p) y versiones agregadas automáticamente (como .1, .2).

# Quitar sufijos "_cox1" o "_cox1p" de los tips
beast_tree@phylo$tip.label <- beast_tree@phylo$tip.label %>%
    str_remove("_Cox1p?$") %>%      # quita _Cox1 o _Cox1p
    str_remove("\\.\\d+$")          # quita punto y dígitos al final (.1, .2, etc.)

head(beast_tree@phylo$tip.label)
[1] "CNIN3620" "CNIN3621" "CNIN3624" "CNIN3625" "CNIN3626" "CNIN3630"

Esta limpieza garantiza que los nombres en label y en los tip labels del árbol coincidan 1:1, lo cual es imprescindible para fusionar metadatos al árbol (p. ej., con %<+% en ggtree) y evitar empates fallidos o duplicados.

📊 El “data” de un objeto treedata

Para explorar y anotar un árbol datado, primero anclamos el eje temporal al presente (0) desplazando el árbol por la edad de la raíz; así el tiempo queda en [-root_age, 0]. Luego integramos los metadatos de las muestras y extraemos beast_p$data en beast_df.

# 1) Graficar el árbol BEAST + agregar metadatos por 'label'
beast_p <- ggtree(beast_tree, size = 1.3) %<+% df_stenopelmatus

# Anclar el eje temporal: presente en 0, pasado hacia la izquierda
# revts() re-ubica x para que max(x) = 0 (no cambia Y ni rota el árbol)
beast_p <- revts(beast_p)

# Extraer la tabla interna del plot, contiene coordenadas (-x, y)
beast_df <- beast_p$data

# Inspeccionar los nombres de columnas disponibles para mapear/visualizar
colnames(beast_df)
 [1] "parent"            "node"              "branch.length"    
 [4] "label"             "CAheight_0.95_HPD" "CAheight_mean"    
 [7] "CAheight_median"   "CAheight_range"    "height"           
[10] "height_0.95_HPD"   "height_median"     "height_range"     
[13] "length"            "length_0.95_HPD"   "length_median"    
[16] "length_range"      "posterior"         "rate"             
[19] "rate_0.95_HPD"     "rate_median"       "rate_range"       
[22] "isTip"             "x"                 "y"                
[25] "branch"            "angle"             "Lat"              
[28] "Long"              "Species"           "Text"             
[31] "Color"             "Presencia_alas"    "Region"           

📑 Campos principales en tree$data

Columna Qué representa Notas prácticas
CAheight_0.95_HPD Intervalo HPD 95% de la edad (CA) Rango de credibilidad bayesiano para la edad de ese nodo bajo el método CA.
CAheight_mean Edad del nodo (método Common Ancestor, media) “CA” proviene de Common Ancestor heights (opción de TreeAnnotator). Ajusta edades para ser consistentes entre clados al resumir muchas muestras de árboles.
CAheight_median Edad del nodo (CA, mediana) Métrica robusta para reportar edad de clados.
CAheight_range Rango min–max (CA) Útil como referencia rápida; prefiere HPD para reportes.
height Edad del nodo (método estándar de TreeAnnotator) Si no usaste “CA heights”, esta suele ser la edad principal.
height_0.95_HPD HPD 95% de la edad (no-CA) El intervalo de credibilidad más usado en figuras y tablas.
height_median Edad (mediana) Igual que arriba, pero explícita la mediana del resumen.
height_range Rango min–max (no-CA) Referencia rápida; reporta HPD en manuscritos.
length Longitud de rama (resumen) Suele ser la misma magnitud que branch.length pero resumida (media/mediana) por TreeAnnotator sobre el conjunto de árboles.
length_0.95_HPD HPD 95% de la longitud de rama Incertidumbre por rama (muy útil con reloj relajado).
length_median Longitud de rama (mediana) En árboles datados: tiempo de rama; en no calibrados: sustituciones por sitio.
length_range Rango min–max de la longitud Exploratorio.
posterior Probabilidad posterior del clado Soporte de clado (0–1). Muy usado para colorear o filtrar nodos.
rate Tasa molecular de sustituciones por sitio/tiempo. Solo si usaste reloj relajado: tasa específica de la rama (multiplicador respecto a la media). En reloj estricto, esta columna puede no variar.
rate_0.95_HPD HPD 95% de la tasa Incertidumbre de la tasa por rama.
rate_median Tasa (mediana) por rama Útil para colorear ramas por tasa estimada.
rate_range Rango min–max de la tasa Exploratorio.

🖼️ Ajustar el lienzo: ejes, límites y márgenes

📐 Cómo funcionan los ejes en ggtree

  • Eje X (x) → representa la distancia acumulada desde la raíz.

    • Si tu árbol es de Máxima Verosimilitud o inferencia bayesiana, X suele ser sustituciones por sitio.

    • Si es ultramétrico (ej. BEAST), X puede interpretarse como tiempo.

  • Eje Y (y) → es un índice que asigna ggtree automáticamente a cada terminal, de arriba a abajo.

    • No es biológico, solo indica la posición vertical de cada taxón.
# Rango en X (distancia/tiempo)
# xr → te dice de dónde a dónde llega el árbol en horizontal.
xr <- range(beast_df$x, na.rm = TRUE)

# Rango en Y (posición de tips/nodos)
# yr → te da el número mínimo y máximo de posiciones verticales (básicamente cuántos tips).
yr <- range(beast_df$y, na.rm = TRUE)

# 5% de aire a izquierda/derecha
pad_left  <- 0.05 * diff(xr)
pad_right <- 0.05 * diff(xr)

beast_p  +
  # Eje X visible y ajustado
  scale_x_continuous(
    name   = "Tiempo antes del presente (Ma)", # cambia el nombre si es tiempo
    breaks = pretty(xr, n = 6),
    limits = c(xr[1] - pad_left, xr[2] + pad_right),
    expand = c(0, 0)  # usamos limits, no expand
  ) +
  # Eje Y visible con ticks
  scale_y_continuous(
    name   = "Número de taxa",
    breaks = pretty(yr, n = 5),
    expand = expansion(mult = c(0.02, 0.04))
  ) + 
  # Mostrar ticks y líneas (ggtree los oculta por default)
  theme(
    axis.line.y  = element_line(),
    axis.ticks.y = element_line(),
    axis.text.y  = element_text(size = 8),
    axis.title.y = element_text(size = 10),
    axis.line.x  = element_line(),
    axis.ticks.x = element_line(),
    axis.text.x  = element_text(size = 8),
    axis.title.x = element_text(size = 10)
    ) 
Scale for y is already present.
Adding another scale for y, which will replace the existing scale.

👉 Para que las etiquetas no se corten, el árbol sea legible y la figura mantenga proporciones al exportar.

🔑 Pasos básicos:

  • 📐 Calcular rangos en x y y desde ggtree(tr)$data.

  • ➕ Agregar un poco de aire a la derecha (pad_x) para las etiquetas.

  • 📏 Usar scale_x_continuous y scale_y_continuous con expand=0 para fijar límites exacto

  • ✂️ coord_cartesian(clip="off") → deja que sobresalgan etiquetas largas.

  • ↔︎️ plot.margin → agrega margen extra si se cortan textos.

  • 📊 geom_treescale → coloca una barra de escala en coordenadas reales.

👉 En pocas líneas: controlas el espacio visible, evitas recortes y aseguras consistencia en tus figuras.

# Extraer información de nodos con edades e intervalos HPD 
hpd <- as_tibble(beast_tree) %>% 
    select(node, height, height_0.95_HPD) %>%  # Nos quedamos con nodo, edad y HPD95
    tidyr::unnest_wider(height_0.95_HPD, names_sep = "_") %>% # Desempaqueta la columna list 'height_0.95_HPD' en dos columnas: _1 (mínimo) y _2 (máximo)
    filter(!is.na(height_0.95_HPD_1), !is.na(height)) # Filtra para quedarnos solo con nodos internos (los tips no tienen HPD)

# x: toma el máximo valor del límite superior del HPD 
x <- max(hpd$height_0.95_HPD_2, na.rm = TRUE)

# Redondea "hacia arriba" al múltiplo de 5 más cercano.
hdp_max <- ceiling(x / 5) * 5

# obtener el numero de terminales
ntips_s <- beast_df %>%
    filter(isTip == "TRUE") %>%
    nrow()

# Aire extra en X (2% del rango) para no cortar etiquetas
pad_x <- 0.05 * diff(xr)

beast_p_adj <- beast_p +
  scale_x_continuous(name   = "Time before present (Ma)",
                     breaks = -seq(0, hdp_max, by = 20),  # negativos en el dato
                     labels = abs,   #pero se ven positivos
                     limits = c(-hdp_max - pad_x, xr[2] + pad_x),
                     expand = c(0, 0)) +   # Limites exactos sin padding automático
  scale_y_continuous(limits = c(-5, ntips_s + 3),
                     expand = c(0, 0)) +
  coord_cartesian(clip = "off") +      # Permitir etiquetas largas fuera del panel
     theme_tree2()  +   # dibuja la línea del eje, los ticks y los textos del eje X.
  theme(plot.margin = margin(5.5, 25, 5.5, 5.5), # Más aire a la derecha
        axis.text.x  = element_text(size = 14, face = "bold"),    # tamaño de letra del eje x
        axis.title.x = element_text(size = 15, face = "bold"))    # tamaño de letra del titulo del eje x
 

beast_p_adj

🛡️Soportes de nodos

En un árbol bayesiano (BEAST, MrBayes), los nodos internos llevan probabilidades posteriores (PP) que indican qué tan confiable es cada clado:

  • Para figuras más limpias, se puede resumir gráficamente los soportes con un criterio común en filogenia bayesiana de acuerdo a Alfaro y Holder (2006):

    • PP ≥ 0.95 → círculos negros (alta confianza).

    • PP 0.90–0.95 → círculos grises (confianza moderada).

    • PP < 0.90 → círculos blancos (confianza baja).

👉 Esto permite pasar de una visualización informativa (números) a una visualización clara y estética basada en categorías de soporte.

# Puntos por umbral de PP (0–1)
beast_p_adj <- beast_p_adj +
  # Alta: PP ≥ 0.95 → negro
  geom_point2(aes(subset = !isTip & !is.na(posterior) & posterior >= 0.95),
              size = 4.5, shape = 16, color = "black") +
  # Moderada: 0.90–0.95 → gris
  geom_point2(aes(subset = !isTip & !is.na(posterior) & posterior >= 0.90 & posterior < 0.95),
              size = 4.5, shape = 16, color = "grey40") +
  # Baja: PP < 0.90 → blanco
  geom_point2(aes(subset = !isTip & !is.na(posterior) & posterior < 0.90),
              size = 4.5, shape = 21, fill = "white", color = "black")

beast_p_adj

🕰️ Árbol datado con escala temporal y referencia estratigráfica

  • Para facilitar la lectura cronológica del árbol, añadimos dos elementos:

    1. Faja geológica al pie del gráfico (eras, períodos y epochs) usando deeptime::coord_geo(). Esto da un contexto estratigráfico claro para interpretar las edades de divergencia.

    2. Líneas verticales punteadas en los límites de los epochs, alineadas con el eje temporal (0 en el presente y valores negativos hacia el pasado).
      Estas guías ayudan a ubicar rápidamente cada nodo respecto a cambios geológicos mayores.

# Límites de epochs (Ma) en eje negativo --------------------------------
# deeptime::epochs trae min_age/max_age (Ma). Convertimos a eje negativo (pasado < 0).
epochs_df <- deeptime::epochs %>%
  select(min_age, max_age)

epoch_limits <- sort(unique(c(-epochs_df$min_age, -epochs_df$max_age, 0)))

beast_p_adj <- beast_p_adj +
     geom_vline(xintercept = epoch_limits,
             linetype = "dashed", linewidth = 0.5, color = "grey70") +
  coord_geo(
    dat   = list("epochs", "periods", "eras"),   # tres niveles
    pos   = list("b", "b", "b"),                 # colocar en la parte inferior
    neg   = TRUE,                                 # tiempo negativo hacia el pasado
    abbrv = list(TRUE, TRUE, FALSE)              # abreviados: epochs/periods; eras completas
  )

beast_p_adj

🎨 Etiquetas de las terminales

En esta parte vamos a enriquecerlo con nuestros metadatos —que añadimos en el paso previo con el operador %<+% de ggtree— para poder representar información extra en el árbol.

  • 🔤 Pintar los nombres de las terminales por categorías (ej. S. apterous, S. micropterous…).
# Paleta por categoría funcional (ajústala si quieres)
pal <- c(
  "Stenopelmatus_apterous"     = "#56ae6c",
  "Stenopelmatus_micropterous" = "#a24f99",
  "Stenopelmatus_macropterous" = "#af953c",
  "Ammopelmatus_apterous"     = "#6971c9",
  "Outgroup"        = "#ba4a4f"
)

beast_p_adj <- beast_p_adj +
    geom_tiplab(
    aes(fill = Species, label = label),  # fill por especie
    color = "grey15",
    geom = "label",                      # cajita detrás del texto
    label.size = 0,                      # sin borde en la cajita
    label.padding = unit(0.12, "lines"), # relleno interno de la cajita
    size = 2.6,
    offset = 0.003,
    show.legend = TRUE
  ) +
  scale_fill_manual(
    values = pal,
    na.value = "black",
    # orden explícito de los grupos
    breaks = c("Ammopelmatus_apterous",
               "Stenopelmatus_apterous", 
               "Stenopelmatus_macropterous",
               "Stenopelmatus_micropterous",
               "Outgroup"),
    labels = c(Ammopelmatus_apterous = expression(bolditalic("A. apterous")),
               Stenopelmatus_apterous = expression(bolditalic("S. apterous")),
               Stenopelmatus_macropterous = expression(bolditalic("S. macropterous")),
               Stenopelmatus_micropterous = expression(bolditalic("S. micropterous")),
               Outgroup = expression(bold("Outgroup")))) +
  guides(fill = guide_legend(
    title = "Species",
    # símbolo cuadrado, sin letra y un poco más pequeño para que el gap sea menor
    override.aes = list(shape = 15, size = 4, label = NULL, colour = NA, stroke = 0),
    label.hjust = 0, title.hjust = 0)) +
  theme(
    legend.position      = c(0.02, 0.98),
    legend.justification = c("left", "top"),
    legend.background    = element_rect(fill = "white", color = "white"),
              # menos espacio entre key y texto
    legend.title         = element_text(size = 16, face = "bold"),
    legend.text          = element_text(size = 14, margin = margin(l = 0))
  )

beast_p_adj

🌡️ Colorear ramas por la tasa molecular

  • En un análisis de BEAST con reloj relajado, cada rama puede tener una tasa de sustitución distinta (subs/sitio/tiempo).

  • Colorear el árbol con la paleta viridis permite visualizar qué linajes evolucionan más lento (azules/oscuros) o más rápido (amarillos/claros) a nivel molecular, resaltando la heterogeneidad en la velocidad de acumulación de mutaciones.

# Rango de tasas sin considerar el valor de la raíz = 1 
rate_rng <- range(beast_p$data$rate[beast_p$data$rate != 1], na.rm = TRUE)

# Árbol con ramas coloreadas por tasa molecular
beast_p_rate <- beast_p_adj +
    aes(color = rate) +   # asigna el color al valor de 'rate' por rama
    scale_color_viridis_c(
        limits   = rate_rng,   # recorta la escala ignorando la raíz = 1 
        na.value = "grey80",   # color neutro para valores faltantes
        name     = "Rate")      # título de la leyenda

# Visualizar
beast_p_rate

📊 Barras de incertidumbre (HPD) en árboles datados

En los árboles datados obtenidos con BEAST, además de las edades estimadas de los nodos, es fundamental mostrar la incertidumbre asociada.
Las barras HPD (Highest Posterior Density) al 95% representan el rango de edades más creíble para cada divergencia según la inferencia bayesiana.

Visualizar estas barras junto con la filogenia:

  • Da una idea clara de la precisión temporal de las estimaciones.

  • Permite identificar clados con mayor incertidumbre en comparación con otros.

  • Es una práctica estándar en publicaciones, ya que comunica tanto el valor puntual (mediana) como el intervalo de credibilidad.

# Extraer coordenadas Y de cada nodo a partir del objeto gráfico ---------------
coords <- beast_p_adj %>% 
    select(node, y)   # Cada nodo tiene asignada una coordenada 'y' en la gráfica

# Unir coordenadas Y con las edades HPD de cada nodo ---------------------------
hpddf <- left_join(coords, hpd, by = "node")

# Dibujar barras horizontales representando los intervalos HPD -----------------
beast_p_adj +
    geom_segment(
        data = hpddf,
        aes(x = height_0.95_HPD_1 * -1,      # Límite inferior de la barra (invertido a negativo para el eje tiempo)
            xend = height_0.95_HPD_2 * -1,   # Límite superior de la barra
            y    = y, 
            yend = y),       # Mantiene la barra horizontal a la altura del nodo
        linewidth = 4,       # Grosor de la barra
        alpha     = 0.3,     # Transparencia
        color     = "red"    # Color de las barras HPD
    )

🎯 Barras HPD en nodos clave

En algunos casos, mostrar todas las barras HPD del árbol puede generar saturación visual y dificultar la interpretación.
Una alternativa útil es resaltar únicamente los nodos de interés biológico o evolutivo, como los que marcan orígenes de clados principales.

👉 Aquí destacamos cuatro nodos clave en la historia de Stenopelmatinae, mostrando sus intervalos de credibilidad (HPD 95%) y la mediana de sus edades estimadas:

  • Nodo 98: raíz (117.16–80.13 Ma; mediana 97.52 Ma).

  • Nodo 99: origen de Stenopelmatinae (65.24–33.06 Ma; mediana 48.60 Ma).

  • Nodo 114: origen del clado mesoamericano de Stenopelmatus (56.13–28.49 Ma; mediana 41.63 Ma).

  • Nodo 148: divergencia entre grupos faulkneri y talpa (≈47–23 Ma).

Esto permite enfocar la atención en los eventos macroevolutivos más relevantes, manteniendo el gráfico limpio y comprensible.

# Nodos de interés y etiquetas en inglés
nodes_target <- tibble::tibble(
  node = c(98, 99, 114, 148),
  label_node = c("",
    "Origin of Stenopelmatinae",
    "Mesoamerican Stenopelmatus clade",
    "Faulkneri vs. Talpa divergence"
  )
)

# 1) Extraer HPD y mediana SOLO para esos nodos
hpd_sel <- as_tibble(beast_tree) %>% 
  dplyr::select(node, height, height_0.95_HPD, height_median) %>% 
  tidyr::unnest_wider(height_0.95_HPD, names_sep = "_") %>% 
  dplyr::filter(node %in% nodes_target$node, !is.na(height_0.95_HPD_1))

# 2) Coordenadas Y desde el plot
coords_sel <- beast_p_adj$data %>% 
  dplyr::select(node, y) %>% 
  dplyr::filter(node %in% nodes_target$node)

# 3) Unir todo + etiquetas
hpddf_sel <- dplyr::left_join(coords_sel, hpd_sel, by = "node") %>% 
  dplyr::left_join(nodes_target, by = "node")

# 4) Graficar con barras HPD, mediana y etiquetas
beast_p_focus <- beast_p_adj +
  # Barras HPD (turquesa claro)
  geom_segment(
    data = hpddf_sel,
    aes(x = -height_0.95_HPD_1, xend = -height_0.95_HPD_2, y = y, yend = y),
    linewidth = 4, alpha = 0.5, color = "#48AAAD"
  ) +
  # Etiquetas con los nombres de clados/eventos
  geom_label(
    data = hpddf_sel,
    aes(x = -height_median, y = y, label = label_node),
    hjust = 1, vjust = -0.4,
    size = 6, label.size = 0, 
    fill = scales::alpha("white", 0.15),
    color = "#016064"
  )

beast_p_focus