Resumen. Medimos el área de 60 hojas de 6 árboles del campus de la Universidad del Valle, 3 creciendo a pleno sol y 3 bajo el dosel de otros árboles o la sombra de edificios, para evaluar si las hojas de sombra son más grandes. Con el árbol como unidad de análisis, los árboles de sombra promediaron 48,0 cm² y los de sol 31,0 cm² (permutación exacta, p = 0,400; ANOVA anidado, p = 0,295). No encontramos una diferencia detectable entre sol y sombra. Los árboles difirieron entre sí (ANOVA, p < 0,001): cerca del 85 % de la variación del área se debe a diferencias entre árboles, con G1_T2 y G1_T5 en los extremos. Con solo 3 árboles en la condición menos representada, el diseño no podía detectar un efecto al 5 %, sin importar su tamaño. Para responder la pregunta haría falta comparar más árboles de una misma especie.

1 Introducción

El tamaño de una hoja resulta de un compromiso. Una lámina grande capta más luz, pero también pierde más agua por transpiración y, bajo sol directo, se calienta más, porque la capa de aire quieto que la rodea es más gruesa y disipa peor el calor (Givnish, 1988; Wright et al., 2017). Por eso, dentro de una misma especie, las hojas que se desarrollan con mucha luz suelen ser más pequeñas y gruesas, y las que crecen con poca luz más grandes y delgadas, lo que maximiza la luz que interceptan (Valladares y Niinemets, 2008).

El campus universitario ofrece un contraste natural para poner a prueba esta idea: hay árboles aislados en zonas de pasto, expuestos al sol todo el día, y árboles que crecen dentro de bosquetes, bajo la copa de otros.

Pregunta. ¿Cambia el área de las hojas según la exposición a la luz del lugar donde crece el árbol?

Hipótesis. Los árboles que crecen a la sombra tienen hojas de mayor área promedio que los que crecen a pleno sol.

Predicción. Si la hipótesis es cierta, el promedio de área foliar de los árboles de sombra será mayor que el de los árboles de sol, y la diferencia se mantendrá al usar el árbol, y no la hoja, como unidad de comparación.

2 Materiales y métodos

2.1 Área de estudio

# Todo se lee como texto: así Excel no "adivina" tipos (una de las áreas quedó guardada como fecha)
crudo <- as.data.frame(read_excel(params$archivo, col_types = "text"))
esperadas <- c("grupo", "id_arbol", "lat", "lon", "n_hoja",
               "area_cm2", "exposicion", "fecha", "observaciones")
names(crudo)                                   # nombres tal como vienen en el Excel
## [1] "grupo 1"       "id_arbol"      "lat"           "lon"          
## [5] "n_hoja"        "area_cm2"      "exposicion"    "fecha"        
## [9] "observaciones"
names(crudo)[1:9] <- esperadas                 # la 1.ª columna dice "grupo 1": se renombra por posición
crudo <- crudo[!is.na(crudo$id_arbol), esperadas]
fechas  <- desde_excel(crudo$fecha)
n_arbol <- length(unique(toupper(trimws(crudo$id_arbol))))

ficha <- data.frame(
  Aspecto = c("Lugar", "Fechas de muestreo", "Tipo de estudio", "Factor", "Variable respuesta",
              "Unidad experimental", "Unidad de muestreo", "Tamaño de muestra", "Medición"),
  Detalle = c("Campus Meléndez, Universidad del Valle, Cali (≈ 1 000 m s.n.m.)",
              paste(format(sort(unique(fechas)), "%d/%m/%Y"), collapse = " y "),
              "Observacional: los árboles ya estaban al sol o a la sombra",
              "Exposición del árbol, con dos niveles: sol y sombra",
              "Área de la lámina foliar (cm²)",
              "Árbol",
              "Hoja (10 por árbol)",
              paste0(n_arbol, " árboles, ", nrow(crudo), " hojas"),
              "Contorno trazado en papel, foto dentro de un marco de lados conocidos y cálculo del área en Google Colab"))
tabla(ficha, caption = "Ficha del estudio.", align = "ll")
Ficha del estudio.
Aspecto Detalle
Lugar Campus Meléndez, Universidad del Valle, Cali (≈ 1 000 m s.n.m.)
Fechas de muestreo 22/10/2026 y 26/10/2026
Tipo de estudio Observacional: los árboles ya estaban al sol o a la sombra
Factor Exposición del árbol, con dos niveles: sol y sombra
Variable respuesta Área de la lámina foliar (cm²)
Unidad experimental Árbol
Unidad de muestreo Hoja (10 por árbol)
Tamaño de muestra 6 árboles, 60 hojas
Medición Contorno trazado en papel, foto dentro de un marco de lados conocidos y cálculo del área en Google Colab

El campus está en el valle geográfico del río Cauca, en una zona de clima cálido (temperatura media cercana a 24 °C) con dos temporadas de lluvias al año. Los árboles muestreados se encuentran en el sector arborizado del campus, donde alternan potreros abiertos, bosquetes con dosel continuo y zonas pavimentadas. De cada árbol se anotaron las coordenadas y se tomó una foto de él y de su ambiente. Se utilizaron hojas de papel, regla y lápiz para trazar el contorno de las hojas recolectadas, que se guardaron en sobres; el área de cada hoja se calculó después con un script de Google Colab.

2.2 Ubicación de los árboles

sitios <- unique(data.frame(id_arbol   = toupper(trimws(crudo$id_arbol)),
                            lat        = a_numero(crudo$lat),
                            lon        = -abs(a_numero(crudo$lon)),   # Cali está al oeste: longitud negativa (sección 3)
                            exposicion = tolower(trimws(crudo$exposicion)),
                            stringsAsFactors = FALSE))

buscar_foto <- function(id) {
  candidatas <- file.path(params$carpeta_fotos, paste0(id, c(".jpeg", ".jpg", ".png", ".JPG", ".JPEG")))
  existentes <- candidatas[file.exists(candidatas)]
  if (length(existentes) > 0) existentes[1] else NA_character_
}
sitios$foto <- vapply(sitios$id_arbol, buscar_foto, character(1))

sitios$popup <- vapply(seq_len(nrow(sitios)), function(i) {
  s <- sitios[i, ]
  img <- if (is.na(s$foto)) "" else paste0("<img src='", knitr::image_uri(s$foto), "' width='180'><br>")
  paste0(img, "<b>", s$id_arbol, "</b> (", s$exposicion, ")<br>", fmt(s$lat, 5), "; ", fmt(s$lon, 5))
}, character(1))

tabla(sitios[, c("id_arbol", "exposicion", "lat", "lon")], digits = 6,
      col.names = c("Árbol", "Exposición", "Latitud", "Longitud"),
      caption = "Coordenadas de los árboles (grados decimales, WGS84).")
Coordenadas de los árboles (grados decimales, WGS84).
Árbol Exposición Latitud Longitud
G1_T1 sombra 3,375537 -76,53550
G1_T2 sol 3,375512 -76,53571
G1_T3 sombra 3,377788 -76,53324
G1_T4 sol 3,375408 -76,53562
G1_T5 sombra 3,375517 -76,53560
G1_T6 sol 3,375541 -76,53542
if (requireNamespace("leaflet", quietly = TRUE)) {
  leaflet::leaflet(sitios) |>
    leaflet::addProviderTiles(leaflet::providers$Esri.WorldImagery, group = "Satélite") |>
    leaflet::addTiles(group = "Calles") |>
    leaflet::addCircleMarkers(lng = ~lon, lat = ~lat, radius = 10, weight = 2, color = "white",
                              fillColor = ~unname(col_expo[exposicion]), fillOpacity = 0.95,
                              label = ~id_arbol, popup = ~popup) |>
    leaflet::addLayersControl(baseGroups = c("Satélite", "Calles"),
                              options = leaflet::layersControlOptions(collapsed = FALSE)) |>
    leaflet::addLegend("bottomright", colors = unname(col_expo), labels = c("Sol", "Sombra"),
                       title = "Exposición", opacity = 1) |>
    leaflet::addScaleBar("bottomleft")
} else {
  ggplot(sitios, aes(lon, lat, fill = exposicion)) +
    geom_point(shape = 21, size = 6, color = "white") +
    geom_text(aes(label = id_arbol), vjust = -1.4, size = 3.5) +
    scale_fill_manual(values = col_expo, name = "Exposición") +
    coord_quickmap() +
    labs(title = "Ubicación de los árboles", x = "Longitud", y = "Latitud",
         caption = "Para el mapa interactivo: install.packages(\"leaflet\")")
}

Al hacer clic en cada punto se ve la foto del árbol. La distancia entre árboles va de 9 m (el par más cercano) a 374 m (el par más alejado).

2.3 Los árboles muestreados

# Galería de fotos: busca G1_T#.jpeg / .jpg / .png en la carpeta indicada en params$carpeta_fotos
galeria <- function(rutas, pies, clase = "") {
  tarjetas <- vapply(seq_along(rutas), function(i) {
    img <- if (is.na(rutas[i]) || !file.exists(rutas[i])) "<div class='sinfoto'>sin imagen</div>"
           else paste0("<img class='", clase, "' src='", knitr::image_uri(rutas[i]), "'>")
    paste0("<figure>", img, "<figcaption>", pies[i], "</figcaption></figure>")
  }, character(1))
  cat("\n\n<div class='galeria'>\n", paste(tarjetas, collapse = "\n"), "\n</div>\n\n")
}
pies <- paste0("<b>", sitios$id_arbol, "</b> · ", sitios$exposicion)

Fotografías de campo de los 6 árboles, tomadas el día del muestreo:

galeria(sitios$foto, pies, clase = "foto")
G1_T1 · sombra
G1_T2 · sol
G1_T3 · sombra
G1_T4 · sol
G1_T5 · sombra
G1_T6 · sol

Los árboles clasificados como de sol son G1_T2, G1_T4, G1_T6 y los de sombra son G1_T1, G1_T3, G1_T5. Las fotografías permiten contrastar esa clasificación y ver que los árboles no tienen la misma forma ni, probablemente, la misma edad:

  • G1_T1 y G1_T3 crecen dentro de un bosquete, bajo un dosel continuo que filtra la luz y deja el suelo cubierto de hojarasca.
  • G1_T2 es bajo y arbustivo, ramificado desde la base, y crece entre pasto alto.
  • G1_T4 parece joven, de tronco delgado, con brotes rojizos y el cielo despejado sobre la copa.
  • G1_T5 tiene una copa densa de hojas grandes y oscuras, en una zona de pasto bajo la sombra de otros árboles.
  • G1_T6 es de tronco delgado y crece en el borde de un parqueadero, con cielo despejado y suelo pavimentado cerca.

2.4 Recolección y medición de las hojas

  • Selección de árboles. Se eligieron árboles adultos, sanos y de hoja simple, y se clasificaron como de sol cuando recibían luz directa la mayor parte del día sin estar cubiertos por otros árboles o edificios, y como de sombra en caso contrario. A cada uno se le asignó un código G1_T# y se registraron sus coordenadas con el GPS del celular.
  • Selección de hojas. De cada árbol se tomaron 10 hojas adultas, completamente expandidas y sin daño visible, de distintas ramas, alturas y orientaciones. Se guardaron numeradas en bolsas rotuladas y se procesaron el mismo día para evitar que se enrollaran al secarse.
  • Medición. El contorno de cada hoja se trazó a lápiz sobre papel blanco y se encerró en un marco rectangular dibujado con regla, cuyos lados se midieron en milímetros. Cada papel se fotografió desde arriba. Un script de Google Colab detecta el marco, corrige la inclinación de la foto (homografía) y, como conoce las medidas reales del marco, convierte el área encerrada por el trazo a cm².

2.5 Análisis de datos

Todo el análisis se hizo en R version 4.3.3 (2024-02-29 ucrt), con los paquetes readxl, ggplot2, kableExtra y leaflet. Se siguieron estos pasos:

  1. Revisión y depuración de la base, dejando registro de cada corrección.
  2. Estadística descriptiva por árbol (n, media, mediana, desviación estándar y coeficiente de variación) y diagramas de caja por árbol y por exposición.
  3. Identificación de hojas atípicas con la regla de Tukey dentro de cada árbol y análisis de sensibilidad.
  4. Comparación sol–sombra usando el promedio de cada árbol como dato, con una prueba de permutación exacta, la prueba t de Welch (en escala original y logarítmica) y Wilcoxon.
  5. ANOVA de un factor entre árboles sobre log(área), con comparaciones de Tukey, y ANOVA anidado (hojas dentro de árboles dentro de exposición), en el que la exposición se prueba contra la variación entre árboles.

3 Revisión de la base de datos

Antes de analizar, se revisó la base en busca de errores de digitación y de formato. Todas las correcciones se hacen sobre una copia (datos); el archivo original no se modifica.

datos <- crudo
datos$fila <- seq_len(nrow(datos)) + 1          # fila en el Excel (la 1 es el encabezado)
datos$id_arbol   <- toupper(trimws(datos$id_arbol))
datos$exposicion <- tolower(trimws(datos$exposicion))

# Formato del ID, valores de exposición y una sola exposición por árbol
c(id_correcto      = all(grepl("^G[0-9]+_T[0-9]+$", datos$id_arbol)),
  expo_valida      = all(datos$exposicion %in% c("sol", "sombra")),
  una_expo_x_arbol = all(tapply(datos$exposicion, datos$id_arbol, function(x) length(unique(x))) == 1))
##      id_correcto      expo_valida una_expo_x_arbol 
##             TRUE             TRUE             TRUE
# ¿Qué hay en la columna grupo?
table(datos$grupo)
## 
##  1  2  3  4  5  6 
## 10 10 10 10 10 10

La columna grupo debería decir 1 en todas las filas, pero contiene 1, 2, 3, 4, 5, 6: en esos casos se digitó el número del árbol. Se reemplaza por el número de grupo que aparece en el ID.

datos$grupo <- as.integer(sub("^G([0-9]+)_.*", "\\1", datos$id_arbol))

datos$lat <- a_numero(datos$lat)
datos$lon <- a_numero(datos$lon)
range(datos$lon)
## [1] 76,53324 76,53571
datos$lon <- -abs(datos$lon)

datos$fecha  <- desde_excel(datos$fecha)
datos$n_hoja <- as.integer(datos$n_hoja)

Las longitudes se anotaron sin signo (76,53). Como Cali está al oeste del meridiano de Greenwich, la longitud es negativa (−76,53), y así se corrigió.

datos$area_texto <- trimws(datos$area_cm2)
datos$area_cm2   <- a_numero(datos$area_texto)
mediana_arbol    <- ave(datos$area_cm2, datos$id_arbol, FUN = function(x) median(x, na.rm = TRUE))
datos$razon_mediana <- datos$area_cm2 / mediana_arbol

# Hojas sin área o con un valor absurdo frente a su propio árbol
sospechosas <- datos[which(is.na(datos$area_cm2) | datos$razon_mediana > 10 | datos$razon_mediana < 0.1),
                     c("fila", "id_arbol", "n_hoja", "area_texto")]
tabla(sospechosas, caption = "Áreas sospechosas.", col.names = c("Fila", "Árbol", "Hoja", "Valor leído"))
Áreas sospechosas.
Fila Árbol Hoja Valor leído
11 G1_T1 10 46295

El área de la hoja 10 de G1_T1 llega como un número de varias decenas de miles: es una fecha. En el Excel esa celda tiene formato dd.mm y muestra 30.09. Al escribir 30.9, Excel lo interpretó como el 30 de septiembre. El valor medido era 30,9 cm², coherente con las demás hojas de ese árbol (entre 22 y 40 cm²). Se recupera a partir de la fecha:

es_fecha <- (!is.na(datos$area_cm2) & datos$area_cm2 > 10000) |
            grepl("^[0-9]{4}-[0-9]{2}-[0-9]{2}", datos$area_texto)
f_area <- desde_excel(datos$area_texto[es_fecha])
datos$area_cm2[es_fecha] <- a_numero(paste0(as.integer(format(f_area, "%d")), ".",
                                            as.integer(format(f_area, "%m"))))
datos$corregida <- es_fecha
datos[es_fecha, c("id_arbol", "n_hoja", "area_cm2")]
##    id_arbol n_hoja area_cm2
## 10    G1_T1     10     30,9
# Controles finales
c(areas_faltantes  = sum(is.na(datos$area_cm2) | datos$area_cm2 <= 0),
  filas_repetidas  = sum(duplicated(datos[, c("id_arbol", "n_hoja")])),
  con_observacion  = sum(!is.na(datos$observaciones)))
## areas_faltantes filas_repetidas con_observacion 
##               0               0               4
table(datos$id_arbol)
## 
## G1_T1 G1_T2 G1_T3 G1_T4 G1_T5 G1_T6 
##    10    10    10    10    10    10
decisiones <- data.frame(
  Problema = c("1.ª columna llamada «grupo 1»",
               "Columna grupo con el número del árbol",
               "Longitud sin signo negativo",
               "Área de G1_T1 hoja 10 convertida en fecha (30/09)"),
  Filas = c(nrow(datos), nrow(datos), nrow(datos), sum(datos$corregida)),
  Solución = c("Renombrar según la plantilla",
               "Reemplazar por 1, tomado del ID",
               "Cambiar a negativo",
               "Recuperar el valor digitado: 30,9 cm²"))
tabla(decisiones, caption = "Problemas encontrados y decisión tomada.", align = "lcl")
Problemas encontrados y decisión tomada.
Problema Filas Solución
1.ª columna llamada «grupo 1» 60 Renombrar según la plantilla
Columna grupo con el número del árbol 60 Reemplazar por 1, tomado del ID
Longitud sin signo negativo 60 Cambiar a negativo
Área de G1_T1 hoja 10 convertida en fecha (30/09) 1 Recuperar el valor digitado: 30,9 cm²
limpios <- datos[order(datos$id_arbol, datos$n_hoja),
                 c("grupo", "id_arbol", "lat", "lon", "n_hoja", "area_cm2",
                   "exposicion", "fecha", "observaciones", "corregida")]
if (params$guardar_limpios) write.csv(limpios, "datos_limpios_area_foliar_G1.csv", row.names = FALSE, na = "")

No hubo áreas faltantes ni filas repetidas. Cuatro hojas tienen observaciones de campo (forma menos ovoide, hoja levemente rota, un pequeño roto en el marco y papel arrugado); sus áreas están dentro del rango normal de su árbol, por lo que se conservaron. La base revisada tiene 60 hojas de 6 árboles y se guarda como datos_limpios_area_foliar_G1.csv.

4 Resultados descriptivos

dg <- limpios
dg$id_arbol <- factor(dg$id_arbol)
dg$expo     <- factor(dg$exposicion, levels = c("sol", "sombra"))

4.1 Área foliar de cada árbol

resumen <- do.call(rbind, lapply(split(dg, dg$id_arbol), function(d) {
  data.frame(arbol = as.character(d$id_arbol[1]), exposicion = d$exposicion[1], n = nrow(d),
             media = mean(d$area_cm2), mediana = median(d$area_cm2), de = sd(d$area_cm2),
             cv_pct = cv(d$area_cm2), minimo = min(d$area_cm2), maximo = max(d$area_cm2))
}))

tabla(resumen, digits = c(0, 0, 0, 1, 1, 1, 1, 1, 1),
      col.names = c("Árbol", "Exposición", "n", "Media", "Mediana", "DE", "CV (%)", "Mín.", "Máx."),
      caption = "Área foliar (cm²) por árbol. DE: desviación estándar; CV: coeficiente de variación.") |>
  column_spec(2, background = ifelse(resumen$exposicion == "sol", "#FBE8CC", "#D5E8DF")) |>
  column_spec(4, bold = TRUE)
Área foliar (cm²) por árbol. DE: desviación estándar; CV: coeficiente de variación.
Árbol Exposición n Media Mediana DE CV (%) Mín. Máx.
G1_T1 sombra 10 31,2 32,1 6,2 19,8 22,2 39,8
G1_T2 sol 10 16,8 16,0 2,8 16,6 13,9 22,9
G1_T3 sombra 10 42,0 41,8 9,3 22,1 30,2 61,8
G1_T4 sol 10 52,1 54,8 10,4 19,9 31,4 67,4
G1_T5 sombra 10 70,8 68,7 15,8 22,3 42,7 106,2
G1_T6 sol 10 24,1 23,1 5,6 23,3 14,5 34,4

El tamaño de hoja varía mucho de un árbol a otro. La media va de 16,8 cm² en G1_T2 (sol) a 70,8 cm² en G1_T5 (sombra): el árbol de hojas más grandes tiene hojas unas 4,2 veces más grandes que el de hojas más pequeñas. Dentro de cada árbol, la media y la mediana difieren como máximo en 5 %.

Los coeficientes de variación de los 6 árboles van de 17 a 23 %. La correlación entre la media y la desviación estándar de los árboles es de 0,99. Las hojas de los árboles con hojas grandes varían más en cm², pero en proporción parecida (CV similar), y eso justifica trabajar con el logaritmo del área en el ANOVA.

set.seed(4)
ggplot(dg, aes(id_arbol, area_cm2, fill = expo)) +
  geom_boxplot(alpha = 0.55, width = 0.6, outlier.shape = NA) +
  geom_jitter(width = 0.12, height = 0, shape = 21, size = 2.3, fill = "white", alpha = 0.9) +
  stat_summary(fun = mean, geom = "point", shape = 23, size = 3.5, fill = "black") +
  scale_fill_manual(values = col_expo, name = "Exposición") +
  scale_y_continuous(labels = coma) +
  labs(title = "Área foliar por árbol", subtitle = "Cada punto es una hoja (10 por árbol)",
       x = NULL, y = "Área foliar (cm²)")
Figura 1. Área de cada hoja según el árbol. El rombo negro marca la media.

Figura 1. Área de cada hoja según el árbol. El rombo negro marca la media.

Las cajas de G1_T2 y G1_T5 (los árboles de hojas más pequeñas y más grandes) no se solapan en absoluto. Entre árboles se concentra cerca del 85 % de la variación del área (ver ANOVA más adelante).

4.2 Hojas atípicas

Para decidir qué hacer con una hoja extrema seguimos dos preguntas, en este orden:

  1. ¿Hay una razón de medición que la explique? Por ejemplo, un error de digitación, un trazo defectuoso o una hoja dañada. Si la hay, se corrige o se excluye. Así se trató la hoja 10 de G1_T1.
  2. Si no la hay, ¿cae fuera de la regla de Tukey de su árbol (menor que Q1 − 1,5·RIC o mayor que Q3 + 1,5·RIC)? En ese caso la consideramos una hoja realmente distinta: se conserva, y se revisa cuánto cambiaría el resultado sin ella.
fuera <- do.call(rbind, lapply(split(dg, dg$id_arbol), function(d) {
  out <- boxplot.stats(d$area_cm2)$out
  if (length(out) == 0) return(NULL)
  data.frame(arbol = as.character(d$id_arbol[1]), n_hoja = d$n_hoja[d$area_cm2 %in% out],
             area = d$area_cm2[d$area_cm2 %in% out], mediana_arbol = median(d$area_cm2))
}))
tabla(fuera, digits = c(0, 0, 2, 1), col.names = c("Árbol", "Hoja", "Área (cm²)", "Mediana del árbol (cm²)"),
      caption = "Hojas fuera de los límites de Tukey de su árbol.")
Hojas fuera de los límites de Tukey de su árbol.
Árbol Hoja Área (cm²) Mediana del árbol (cm²)
G1_T2 2 22,85 16,0
G1_T5 1 106,20 68,7
G1_T5 4 42,66 68,7
sens <- do.call(rbind, lapply(split(dg, dg$id_arbol), function(d) {
  out <- boxplot.stats(d$area_cm2)$out
  data.frame(exposicion = d$exposicion[1], media = mean(d$area_cm2), mediana = median(d$area_cm2),
             media_sin = mean(d$area_cm2[!d$area_cm2 %in% out]))
}))
dif_sens <- sapply(c("media", "mediana", "media_sin"), function(v)
  mean(sens[[v]][sens$exposicion == "sombra"]) - mean(sens[[v]][sens$exposicion == "sol"]))
tabla(data.frame(c("Media de todas las hojas", "Mediana", "Media sin hojas atípicas"), unname(dif_sens)),
      digits = 2, col.names = c("Resumen de cada árbol", "Diferencia sombra − sol (cm²)"),
      caption = "Sensibilidad del resultado a la hoja atípica.")
Sensibilidad del resultado a la hoja atípica.
Resumen de cada árbol Diferencia sombra − sol (cm²)
Media de todas las hojas 17,00
Mediana 16,25
Media sin hojas atípicas 16,93

3 hojas quedan fuera de los límites de Tukey de su árbol (tabla anterior), en G1_T2 y G1_T5. Ninguna tiene una anotación de campo que sugiera un problema de medición y sus tamaños son plausibles para esos árboles, así que se conservan. Además, quitarlas no cambia la conclusión: la diferencia sombra − sol pasa de 17,00 a 16,93 cm².

4.3 Hojas de sol frente a hojas de sombra

set.seed(7)
ggplot(dg, aes(expo, area_cm2)) +
  geom_boxplot(aes(fill = expo), alpha = 0.45, width = 0.5, outlier.shape = NA, show.legend = FALSE) +
  geom_jitter(aes(shape = id_arbol), width = 0.15, height = 0, size = 2.6, alpha = 0.8) +
  scale_fill_manual(values = col_expo) +
  scale_shape_manual(values = c(16, 17, 15, 18, 3, 8, 4, 7, 10, 12)[seq_len(nlevels(dg$id_arbol))], name = "Árbol") +
  scale_y_continuous(labels = coma) +
  labs(title = "Área foliar por exposición", subtitle = "Descriptivo: las hojas de un árbol no son independientes",
       x = "Exposición", y = "Área foliar (cm²)")
Figura 2. Hojas agrupadas por exposición. La forma del punto indica el árbol.

Figura 2. Hojas agrupadas por exposición. La forma del punto indica el árbol.

por_expo <- split(dg, dg$expo)
tabla(data.frame(Exposición = names(por_expo),
                 Árboles = sapply(por_expo, function(d) length(unique(d$id_arbol))),
                 Hojas = sapply(por_expo, nrow),
                 Media = fmt(sapply(por_expo, function(d) mean(d$area_cm2)), 1),
                 Mediana = fmt(sapply(por_expo, function(d) median(d$area_cm2)), 1),
                 DE = fmt(sapply(por_expo, function(d) sd(d$area_cm2)), 1)),
      digits = 0, caption = "Resumen de las hojas por exposición.")
Resumen de las hojas por exposición.
Exposición Árboles Hojas Media Mediana DE
sol 3 30 31,0 23,5 16,9
sombra 3 30 48,0 41,8 20,2

Cada punto es una hoja y la forma indica el árbol. La mediana de las hojas de sol es 23,5 cm² y la de las hojas de sombra, 41,8 cm². Los puntos del mismo árbol se agrupan entre sí y cada caja junta varios árboles, por lo que las 60 hojas no pueden tratarse como 60 observaciones independientes de la exposición.

5 Sol frente a sombra: el árbol como unidad

Lo que se clasificó como de sol o de sombra fue el árbol. Sus 10 hojas comparten genética, edad, suelo y luz, así que representan a un solo individuo. Para comparar las exposiciones se resume cada árbol con el promedio de sus hojas. Así quedan 6 datos, no 60.

medias <- aggregate(area_cm2 ~ id_arbol + exposicion, data = dg, FUN = mean)
medias$id_arbol   <- as.character(medias$id_arbol)
medias$exposicion <- factor(medias$exposicion, levels = c("sol", "sombra"))
medias <- medias[order(medias$exposicion, medias$id_arbol), ]
n_expo <- table(medias$exposicion)

prom_expo <- tapply(medias$area_cm2, medias$exposicion, mean)
dif_obs   <- unname(prom_expo["sombra"] - prom_expo["sol"])

tabla(medias, digits = c(0, 0, 1), col.names = c("Árbol", "Exposición", "Área media (cm²)"),
      caption = "Promedio de área foliar de cada árbol.") |>
  column_spec(2, background = ifelse(medias$exposicion == "sol", "#FBE8CC", "#D5E8DF"))
Promedio de área foliar de cada árbol.
Árbol Exposición Área media (cm²)
G1_T2 sol 16,8
G1_T4 sol 52,1
G1_T6 sol 24,1
G1_T1 sombra 31,2
G1_T3 sombra 42,0
G1_T5 sombra 70,8
ggplot(medias, aes(exposicion, area_cm2, fill = exposicion)) +
  geom_boxplot(alpha = 0.3, width = 0.45, outlier.shape = NA, show.legend = FALSE) +
  stat_summary(fun = mean, fun.min = mean, fun.max = mean, geom = "crossbar",
               width = 0.45, color = "black", linewidth = 0.4, show.legend = FALSE) +
  geom_point(shape = 21, size = 7, color = "white", stroke = 1.2, show.legend = FALSE) +
  geom_text(aes(label = id_arbol), nudge_x = 0.32, size = 3.6) +
  scale_fill_manual(values = col_expo) +
  scale_y_continuous(labels = coma, limits = c(0, NA)) +
  labs(title = "Área foliar media por árbol",
       subtitle = paste0(n_expo["sol"], " árboles de sol y ", n_expo["sombra"], " de sombra"),
       x = "Exposición", y = "Área foliar media del árbol (cm²)")
Figura 3. Un punto por árbol (promedio de sus 10 hojas). La línea horizontal es el promedio de cada exposición.

Figura 3. Un punto por árbol (promedio de sus 10 hojas). La línea horizontal es el promedio de cada exposición.

En promedio, los árboles de sombra tienen hojas de 48,0 cm² y los de sol de 31,0 cm². La diferencia sombra − sol es de 17,0 cm² (positiva si las hojas de sombra son más grandes). Para ponerla en contexto: las medias de los árboles de sol van de 16,8 a 52,1 cm² y las de sombra de 31,2 a 70,8 cm². Los dos rangos se solapan: la exposición, por sí sola, no ordena a los árboles.

5.1 Distribución espacial del tamaño de hoja

En este mapa el tamaño de cada círculo es proporcional al área foliar media del árbol. Sirve para ver si hay una zona del campus con hojas grandes que pudiera confundirse con el efecto de la exposición.

mapa_res <- merge(medias, sitios[, c("id_arbol", "lat", "lon", "foto")], by = "id_arbol")
mapa_res$popup <- vapply(seq_len(nrow(mapa_res)), function(i) {
  s <- mapa_res[i, ]
  img <- if (is.na(s$foto)) "" else paste0("<img src='", knitr::image_uri(s$foto), "' width='180'><br>")
  paste0(img, "<b>", s$id_arbol, "</b> (", s$exposicion, ")<br>Área media: ", fmt(s$area_cm2), " cm²")
}, character(1))
mapa_res$expo_txt <- as.character(mapa_res$exposicion)

if (requireNamespace("leaflet", quietly = TRUE)) {
  leaflet::leaflet(mapa_res) |>
    leaflet::addProviderTiles(leaflet::providers$Esri.WorldImagery, group = "Satélite") |>
    leaflet::addTiles(group = "Calles") |>
    leaflet::addCircleMarkers(lng = ~lon, lat = ~lat, radius = ~sqrt(area_cm2) * 2.2, weight = 2,
                              color = "white", fillColor = ~unname(col_expo[expo_txt]), fillOpacity = 0.85,
                              label = ~paste0(id_arbol, ": ", fmt(area_cm2), " cm²"), popup = ~popup) |>
    leaflet::addLayersControl(baseGroups = c("Satélite", "Calles"),
                              options = leaflet::layersControlOptions(collapsed = FALSE)) |>
    leaflet::addLegend("bottomright", colors = unname(col_expo), labels = c("Sol", "Sombra"),
                       title = "Exposición (tamaño = área media)", opacity = 1) |>
    leaflet::addScaleBar("bottomleft")
} else {
  ggplot(mapa_res, aes(lon, lat, fill = expo_txt, size = area_cm2)) +
    geom_point(shape = 21, color = "white") +
    geom_text(aes(label = id_arbol), size = 3.5, vjust = -2) +
    scale_fill_manual(values = col_expo, name = "Exposición") +
    scale_size_area(max_size = 16, name = "Área media (cm²)") +
    coord_quickmap() +
    labs(title = "Área foliar media por árbol", x = "Longitud", y = "Latitud")
}

Los árboles con las hojas más pequeñas (G1_T2) y más grandes (G1_T5) están separados por unos 12 m, así que el tamaño de hoja no parece organizarse por zonas del campus. El árbol G1_T3 queda aparte, a más de 340 m de cualquiera de los demás, mientras que los otros 5 árboles están a menos de 40 m entre sí.

5.2 ¿Podría deberse al azar? Prueba de permutación

Si la exposición no tuviera ningún efecto, las etiquetas “sol” y “sombra” podrían haberse repartido de cualquier forma entre los 6 árboles. La prueba calcula la diferencia sombra − sol para todos los repartos posibles y compara con ellos la diferencia observada.

y         <- medias$area_cm2
es_sombra <- medias$exposicion == "sombra"
repartos  <- combn(length(y), sum(es_sombra))
dif_perm  <- apply(repartos, 2, function(i) mean(y[i]) - mean(y[-i]))
p_perm    <- mean(abs(dif_perm) >= abs(dif_obs) - 1e-9)
p_min     <- mean(abs(dif_perm) >= max(abs(dif_perm)) - 1e-9)   # el menor p que este diseño puede dar

perm_df <- data.frame(dif = dif_perm, tipo = ifelse(abs(dif_perm - dif_obs) < 1e-9, "Observado", "Otro reparto"))
ggplot(perm_df, aes(dif, 0, fill = tipo)) +
  geom_vline(xintercept = 0, linetype = 3) +
  geom_point(shape = 21, size = 8, color = "grey30") +
  scale_fill_manual(values = c(Observado = col_expo[["sombra"]], `Otro reparto` = "grey85"), name = NULL) +
  scale_y_continuous(NULL, breaks = NULL) +
  scale_x_continuous(labels = coma) +
  labs(x = "Diferencia sombra − sol (cm²)", title = paste(ncol(repartos), "repartos posibles"))
Figura 4. Diferencia sombra − sol en cada uno de los repartos posibles de las etiquetas.

Figura 4. Diferencia sombra − sol en cada uno de los repartos posibles de las etiquetas.

Solo hay 20 repartos posibles de las etiquetas, y la diferencia observada da p = 0,400. Además, con 3 árboles de sol y 3 de sombra, el menor p que podría obtenerse, fueran cuales fueran los datos, es 0,100. El diseño no tenía capacidad para detectar una diferencia al 5 %, por grande que fuera.

5.3 Otras pruebas sobre los promedios

tt  <- t.test(area_cm2 ~ exposicion, data = medias)
tl  <- t.test(log(area_cm2) ~ exposicion, data = medias)
wil <- wilcox.test(area_cm2 ~ exposicion, data = medias)
ic     <- -rev(tt$conf.int)                                  # sombra − sol
razon  <- unname(exp(tl$estimate[2] - tl$estimate[1]))
ic_raz <- exp(-rev(tl$conf.int))

tabla(data.frame(
  Prueba = c("Permutación exacta", "t de Welch (cm²)", "t de Welch sobre log: razón sombra/sol", "Wilcoxon"),
  Efecto = c(paste(fmt(dif_obs), "cm²"), paste(fmt(dif_obs), "cm²"), paste0(fmt(razon, 2), " veces"), "—"),
  IC95 = c("—", paste0("[", fmt(ic[1]), "; ", fmt(ic[2]), "]"),
           paste0("[", fmt(ic_raz[1], 2), "; ", fmt(ic_raz[2], 2), "]"), "—"),
  p = c(fmt_p(p_perm), fmt_p(tt$p.value), fmt_p(tl$p.value), fmt_p(wil$p.value))),
  align = "lccc", col.names = c("Prueba", "Efecto estimado", "IC 95 %", "p"),
  caption = paste0("Comparación sombra − sol con un dato por árbol (", n_expo["sol"], " de sol y ", n_expo["sombra"], " de sombra)."))
Comparación sombra − sol con un dato por árbol (3 de sol y 3 de sombra).
Prueba Efecto estimado IC 95 % p
Permutación exacta 17,0 cm² — 0,400
t de Welch (cm²) 17,0 cm² [-27,6; 61,6] 0,348
t de Welch sobre log: razón sombra/sol 1,64 veces [0,50; 5,39] 0,302
Wilcoxon — — 0,400

Ninguna prueba detecta diferencia (p entre 0,302 y 0,400). Los intervalos de confianza muestran la incertidumbre de la estimación con tan pocos árboles. Que no encontremos diferencia no significa que no exista: significa que, con estos datos, no se puede saber.

5.4 ¿Depende el resultado de un solo árbol?

Con solo 3 árboles por condición, un árbol puede mover mucho el promedio. Para comprobarlo se repite la diferencia sombra − sol quitando, uno a uno, cada árbol.

loo <- do.call(rbind, lapply(medias$id_arbol, function(a) {
  m <- medias[medias$id_arbol != a, ]
  data.frame(quitado = a,
             exposicion = as.character(medias$exposicion[medias$id_arbol == a]),
             dif = mean(m$area_cm2[m$exposicion == "sombra"]) - mean(m$area_cm2[m$exposicion == "sol"]))
}))
tabla(loo, digits = c(0, 0, 1),
      col.names = c("Árbol quitado", "Su exposición", "Diferencia sombra − sol (cm²)"),
      caption = "Diferencia sombra − sol al quitar un árbol a la vez.")
Diferencia sombra − sol al quitar un árbol a la vez.
Árbol quitado Su exposición Diferencia sombra − sol (cm²)
G1_T2 sol 9,9
G1_T4 sol 27,6
G1_T6 sol 13,6
G1_T1 sombra 25,4
G1_T3 sombra 20,0
G1_T5 sombra 5,6
loo_min <- min(loo$dif); loo_max <- max(loo$dif)
loo_mismo_signo <- all(sign(loo$dif) == sign(dif_obs))
arb_clave <- loo$quitado[which.min(abs(loo$dif))]

La diferencia conserva el mismo signo en todos los casos, pero su tamaño va de 5,6 a 27,6 cm² (con todos los árboles es 17,0 cm²). El árbol que más la reduce al quitarlo es G1_T5.

5.5 ¿Cuántos árboles harían falta?

sd_p  <- sqrt(mean(tapply(medias$area_cm2, medias$exposicion, var)))   # DE entre árboles dentro de cada exposición
n_nec <- ceiling(power.t.test(delta = abs(dif_obs), sd = sd_p, power = 0.8, sig.level = 0.05)$n)

Con la desviación estándar entre árboles observada (19,6 cm²), detectar una diferencia como la observada (17,0 cm²) con 80 % de potencia requeriría unos 22 árboles por condición. Es una estimación aproximada: con tan pocos árboles, tanto la diferencia como la variación pueden estar mal estimadas.

5.6 Qué pasa si se ignora el árbol

t_hojas <- t.test(area_cm2 ~ expo, data = dg)
tabla(data.frame(Análisis = c("Hojas como datos independientes (incorrecto)", "Un promedio por árbol (correcto)"),
                 Datos = c(nrow(dg), nrow(medias)),
                 p = c(fmt_p(t_hojas$p.value), fmt_p(p_perm))),
      align = "lcc", col.names = c("Análisis", "Datos usados", "p"),
      caption = "Efecto de la unidad de análisis sobre la prueba.") |>
  row_spec(1, color = "#A33B2B")
Efecto de la unidad de análisis sobre la prueba.
Análisis Datos usados p
Hojas como datos independientes (incorrecto) 60 < 0,001
Un promedio por árbol (correcto) 6 0,400

Usar las hojas como réplicas de la exposición se conoce como pseudorreplicación (Hurlbert, 1984): se multiplica por diez el tamaño de muestra sin agregar información sobre la exposición. Aquí el valor p pasa de < 0,001 (hojas como réplicas) a 0,400 (árbol como unidad). Ignorar el árbol produce un resultado falsamente significativo.

6 Diseño experimental y ANOVA

6.1 Estructura del diseño

tabla(data.frame(
  Elemento = c("Factor (fijo)", "Unidad experimental", "Unidad de muestreo", "Réplicas", "Asignación"),
  `En este estudio` = c("Exposición: sol y sombra", "Árbol", "Hoja (submuestra)",
                        paste0(n_expo["sol"], " árboles de sol y ", n_expo["sombra"], " de sombra; 10 hojas por árbol"),
                        "No aleatoria (estudio observacional)"),
  check.names = FALSE),
  align = "ll", caption = "Elementos del diseño.")
Elementos del diseño.
Elemento En este estudio
Factor (fijo) Exposición: sol y sombra
Unidad experimental Árbol
Unidad de muestreo Hoja (submuestra)
Réplicas 3 árboles de sol y 3 de sombra; 10 hojas por árbol
Asignación No aleatoria (estudio observacional)

Las hojas están anidadas en los árboles, y cada árbol pertenece a una sola exposición. El modelo es

\[\log(\text{área}_{ijk}) = \mu + E_i + A_{j(i)} + \varepsilon_{k(ij)},\]

donde \(E_i\) es el efecto de la exposición, \(A_{j(i)}\) el del árbol \(j\) dentro de la exposición \(i\) y \(\varepsilon_{k(ij)}\) la variación entre hojas de un mismo árbol. El término del árbol es el error experimental: la exposición debe compararse contra él, y no contra la variación entre hojas.

¿Por qué usar el logaritmo?

de_orig <- tapply(dg$area_cm2, dg$id_arbol, sd)
de_log  <- tapply(log(dg$area_cm2), dg$id_arbol, sd)
p_flig_orig <- fligner.test(area_cm2 ~ id_arbol, data = dg)$p.value
p_flig_log  <- fligner.test(log(area_cm2) ~ id_arbol, data = dg)$p.value

tabla(data.frame(Escala = c("Original (cm²)", "Logaritmo"),
                 DE_min = c(min(de_orig), min(de_log)), DE_max = c(max(de_orig), max(de_log)),
                 Cociente = c(max(de_orig) / min(de_orig), max(de_log) / min(de_log)),
                 p = c(fmt_p(p_flig_orig), fmt_p(p_flig_log))),
      digits = c(0, 3, 3, 2, 0),
      col.names = c("Escala", "DE mínima", "DE máxima", "DE máx / DE mín", "p (Fligner-Killeen)"),
      caption = "Variabilidad entre árboles en cada escala.")
Variabilidad entre árboles en cada escala.
Escala DE mínima DE máxima DE máx / DE mín p (Fligner-Killeen)
Original (cm²) 2,776 15,813 5,70 0,072
Logaritmo 0,154 0,240 1,56 0,850

En la escala original, la desviación estándar del árbol más variable es 5,7 veces la del menos variable (Fligner-Killeen, p = 0,072). Con el logaritmo, esa razón es 1,6 (p = 0,850). Cuanto más cerca de 1 está la razón y mayor es el p, más homogéneas son las varianzas. Además, en escala log los efectos se leen como razones (“tantas veces más grande”).

6.2 ANOVA de un factor: diferencias entre árboles

m1 <- lm(log(area_cm2) ~ id_arbol, data = dg)
a1 <- anova_df(m1)
a1$Fuente <- c("Árbol", "Residual (hojas)")
r2_arb <- summary(m1)$r.squared
tabla(anova_txt(a1), align = "lccccc", caption = "ANOVA de un factor sobre log(área foliar).")
ANOVA de un factor sobre log(área foliar).
Fuente gl SC CM F p
Árbol 5 13,686 2,737 61,18 < 0,001
Residual (hojas) 54 2,416 0,045

Hay diferencias muy marcadas entre los árboles: F(5; 54) = 61,2, p < 0,001. El árbol explica el 85 % de la variación del logaritmo del área. En este análisis las hojas sí funcionan como réplicas, porque la pregunta es por estos árboles concretos y no por la exposición.

Verificación de supuestos.

diag_df <- data.frame(ajustado = fitted(m1), residuo = resid(m1), arbol = dg$id_arbol)
ggplot(diag_df, aes(ajustado, residuo, color = arbol)) +
  geom_hline(yintercept = 0, linetype = 2) +
  geom_point(size = 2.4, alpha = 0.85, position = position_jitter(width = 0.015, height = 0, seed = 1)) +
  labs(title = "Residuos vs. ajustados", x = "Valor ajustado (log área)", y = "Residuo", color = "Árbol")
ggplot(diag_df, aes(sample = residuo)) +
  stat_qq(color = verde_osc, size = 2) + stat_qq_line(color = "#C0392B") +
  labs(title = "Normalidad de los residuos", x = "Cuantiles teóricos", y = "Cuantiles de los residuos")

shap1 <- shapiro.test(resid(m1))
flig1 <- fligner.test(log(area_cm2) ~ id_arbol, data = dg)
kw1   <- kruskal.test(area_cm2 ~ id_arbol, data = dg)
tabla(data.frame(Supuesto = c("Normalidad de residuos", "Homogeneidad de varianzas", "Alternativa sin normalidad"),
                 Prueba = c("Shapiro-Wilk", "Fligner-Killeen", "Kruskal-Wallis"),
                 p = c(fmt_p(shap1$p.value), fmt_p(flig1$p.value), fmt_p(kw1$p.value))),
      align = "llc", caption = "Pruebas de los supuestos del ANOVA.")
Pruebas de los supuestos del ANOVA.
Supuesto Prueba p
Normalidad de residuos Shapiro-Wilk 0,362
Homogeneidad de varianzas Fligner-Killeen 0,850
Alternativa sin normalidad Kruskal-Wallis < 0,001

Los supuestos se cumplen. Normalidad de los residuos (Shapiro-Wilk, p = 0,362), comparada con la recta del gráfico cuantil-cuantil, y dispersión similar entre los 6 árboles (Fligner-Killeen, p = 0,850). La prueba de Kruskal-Wallis, que no exige normalidad, da p < 0,001.

6.3 Comparaciones post-ANOVA (Tukey HSD)

El ANOVA indica que al menos un árbol es distinto. Para saber cuáles se comparan todos los pares (15 comparaciones) con la prueba de Tukey, que controla la probabilidad de cometer algún falso positivo en el conjunto.

tk <- TukeyHSD(aov(log(area_cm2) ~ id_arbol, data = dg))$id_arbol
tk_tab <- data.frame(comparacion = gsub("-", " / ", rownames(tk)),
                     razon = exp(tk[, "diff"]), li = exp(tk[, "lwr"]), ls = exp(tk[, "upr"]),
                     p_aj = tk[, "p adj"], stringsAsFactors = FALSE)
tk_tab$misma <- sapply(strsplit(tk_tab$comparacion, " / "), function(x)
  length(unique(resumen$exposicion[match(x, resumen$arbol)])) == 1)
tk_tab$difiere <- ifelse(tk_tab$p_aj < 0.05, "sí", "no")
tk_tab <- tk_tab[order(tk_tab$p_aj), ]

tabla(data.frame(tk_tab$comparacion, fmt(tk_tab$razon, 2),
                 paste0("[", fmt(tk_tab$li, 2), "; ", fmt(tk_tab$ls, 2), "]"),
                 fmt_p(tk_tab$p_aj), tk_tab$difiere, ifelse(tk_tab$misma, "sí", "no")),
      align = "lccccc",
      col.names = c("Comparación (A / B)", "Razón A / B", "IC 95 %", "p ajustado", "¿Difieren?", "¿Misma exposición?"),
      caption = "Comparaciones de Tukey entre árboles (en escala log, expresadas como razones de área).") |>
  row_spec(which(tk_tab$p_aj < 0.05), bold = TRUE)
Comparaciones de Tukey entre árboles (en escala log, expresadas como razones de área).
Comparación (A / B) Razón A / B IC 95 % p ajustado ¿Difieren? ¿Misma exposición?
G1_T5 / G1_T2 4,18 [3,16; 5,52] < 0,001 sí no
G1_T4 / G1_T2 3,08 [2,33; 4,07] < 0,001 sí sí
G1_T6 / G1_T5 0,34 [0,26; 0,45] < 0,001 sí no
G1_T3 / G1_T2 2,48 [1,87; 3,28] < 0,001 sí no
G1_T5 / G1_T1 2,26 [1,71; 2,99] < 0,001 sí sí
G1_T6 / G1_T4 0,46 [0,35; 0,61] < 0,001 sí sí
G1_T2 / G1_T1 0,54 [0,41; 0,72] < 0,001 sí no
G1_T6 / G1_T3 0,57 [0,43; 0,76] < 0,001 sí no
G1_T5 / G1_T3 1,68 [1,27; 2,23] < 0,001 sí sí
G1_T4 / G1_T1 1,67 [1,26; 2,21] < 0,001 sí no
G1_T6 / G1_T2 1,42 [1,07; 1,87] 0,007 sí sí
G1_T5 / G1_T4 1,36 [1,03; 1,79] 0,025 sí no
G1_T3 / G1_T1 1,34 [1,02; 1,78] 0,032 sí sí
G1_T6 / G1_T1 0,77 [0,58; 1,02] 0,073 no no
G1_T4 / G1_T3 1,24 [0,94; 1,64] 0,216 no no
ggplot(tk_tab, aes(razon, reorder(comparacion, razon), color = difiere)) +
  geom_vline(xintercept = 1, linetype = 2) +
  geom_linerange(aes(xmin = li, xmax = ls), linewidth = 1.1) +
  geom_point(size = 3.2) +
  scale_x_log10(labels = coma) +
  scale_color_manual(values = c(`sí` = "#C0392B", no = "grey55"), name = "¿Difieren al 5 %?") +
  labs(title = "Comparaciones por pares", x = "Razón de áreas (escala logarítmica)", y = NULL)
Figura 5. Razón de áreas entre árboles con su intervalo de Tukey. Un intervalo que cruza 1 indica que no hay diferencia detectable.

Figura 5. Razón de áreas entre árboles con su intervalo de Tukey. Un intervalo que cruza 1 indica que no hay diferencia detectable.

13 de las 15 comparaciones son significativas. Las parejas que no se distinguen son: G1_T6 / G1_T1, G1_T4 / G1_T3. La mayor diferencia se da entre G1_T5 / G1_T2 (hojas unas 4,2 veces más grandes en uno que en otro; tienen exposiciones distintas). Entre los árboles con la misma exposición, 6 de 6 parejas difieren significativamente. Compartir la misma exposición no hace que dos árboles tengan hojas parecidas.

6.4 ANOVA anidado: el efecto de la exposición

m_anid <- lm(log(area_cm2) ~ expo + id_arbol, data = dg)   # id_arbol queda anidado en expo
a_an   <- anova_df(m_anid)
F_expo  <- a_an$CM[1] / a_an$CM[2]; p_expo  <- pf(F_expo,  a_an$gl[1], a_an$gl[2], lower.tail = FALSE)
F_arbol <- a_an$CM[2] / a_an$CM[3]; p_arbol <- pf(F_arbol, a_an$gl[2], a_an$gl[3], lower.tail = FALSE)
p_ingenuo <- a_an$p[1]                                       # exposición contra el error de hojas

tabla(data.frame(Fuente = c("Exposición", "Árbol dentro de exposición", "Hojas (residual)"),
                 gl = a_an$gl, SC = fmt(a_an$SC, 3), CM = fmt(a_an$CM, 3),
                 Denominador = c("CM árbol", "CM hojas", ""),
                 F = c(fmt(F_expo, 2), fmt(F_arbol, 2), ""),
                 p = c(fmt_p(p_expo), fmt_p(p_arbol), "")),
      align = "lcccccc", caption = "ANOVA anidado sobre log(área foliar).") |>
  row_spec(1, bold = TRUE, background = "#FDF4E6")
ANOVA anidado sobre log(área foliar).
Fuente gl SC CM Denominador F p
Exposición 1 3,636 3,636 CM árbol 1,45 0,295
Árbol dentro de exposición 4 10,050 2,512 CM hojas 56,16 < 0,001
Hojas (residual) 54 2,416 0,045
n0 <- nrow(dg) / nlevels(dg$id_arbol)
s2_hoja  <- a_an$CM[3]
s2_arbol <- max((a_an$CM[2] - a_an$CM[3]) / n0, 0)
pct_arb  <- 100 * s2_arbol / (s2_arbol + s2_hoja)
tabla(data.frame(Componente = c("Entre árboles", "Entre hojas del mismo árbol"),
                 Varianza = c(s2_arbol, s2_hoja),
                 Porcentaje = c(pct_arb, 100 - pct_arb)),
      digits = c(0, 4, 1), col.names = c("Componente", "Varianza (log)", "% del total"),
      caption = "Componentes de varianza.")
Componentes de varianza.
Componente Varianza (log) % del total
Entre árboles 0,2468 84,7
Entre hojas del mismo árbol 0,0447 15,3
  • Exposición: F(1; 4) = 1,45, p = 0,295. Probada contra la variación entre árboles, la exposición no tiene un efecto detectable. Con solo 6 árboles, el error tiene apenas 4 grados de libertad.
  • Árbol dentro de exposición: p < 0,001. Árboles con la misma exposición difieren mucho.
  • El error de la pseudorreplicación: si la exposición se probara contra la variación entre hojas, como si las 60 hojas fueran 60 árboles, el resultado sería p < 0,001. Sería falsamente significativo: la diferencia entre individuos se confundiría con un efecto de la luz.
  • Componentes de varianza: alrededor del 85 % de la variación (en log) está entre árboles. Medir más hojas por árbol apenas mejoraría el estudio; lo que se necesita es medir más árboles.

Como la exposición tiene solo dos niveles, no corresponde hacer comparaciones post-ANOVA para ella. Con dos niveles, la prueba F ya es la comparación entre ambos.

6.5 Síntesis de los análisis

tabla(data.frame(
  Pregunta = c("¿Difieren los árboles?", "¿Qué árboles difieren?", "¿Difieren sol y sombra? (permutación)",
               "¿Difieren sol y sombra? (ANOVA anidado)", "Mismo análisis ignorando el árbol"),
  Resultado = c(paste0(ifelse(a1$p[1] < 0.05, "Sí", "No"), ": F = ", fmt(a1$F[1], 1), ", ", p_eq(a1$p[1])),
                paste(sum(tk_tab$p_aj < 0.05), "de", nrow(tk_tab), "pares (Tukey)"),
                paste0(ifelse(p_perm < 0.05, "Sí", "No"), ": ", p_eq(p_perm)),
                paste0(ifelse(p_expo < 0.05, "Sí", "No"), ": F = ", fmt(F_expo, 2), ", ", p_eq(p_expo)),
                paste0(p_eq(p_ingenuo), ifelse(ingenuo_enganoso, " (engañoso)", "")))),
  align = "ll", caption = "Resumen de resultados.")
Resumen de resultados.
Pregunta Resultado
¿Difieren los árboles? Sí: F = 61,2, p < 0,001
¿Qué árboles difieren? 13 de 15 pares (Tukey)
¿Difieren sol y sombra? (permutación) No: p = 0,400
¿Difieren sol y sombra? (ANOVA anidado) No: F = 1,45, p = 0,295
Mismo análisis ignorando el árbol p < 0,001 (engañoso)

7 Discusión

7.1 Respuesta a la pregunta

No encontramos evidencia de que los árboles de sombra tengan hojas más grandes que los de sol. Los promedios fueron 48,0 cm² (sombra) frente a 31,0 cm² (sol; p = 0,295). La hipótesis no se confirma. Sin embargo, con 3 árboles por condición el estudio tiene poca capacidad para detectar un efecto, y por tanto tampoco permite descartar que exista.

El resultado más sólido del estudio es otro: las diferencias entre árboles son grandes. Cerca del 85 % de la variación (en log) está entre árboles. El árbol de hojas más grandes (G1_T5, de sombra) tiene hojas unas 4,2 veces más grandes que el de hojas más pequeñas (G1_T2, de sol), y entre ambos hay unos 12 m. Cuando árboles cercanos difieren tanto, la explicación puramente ambiental es menos probable: las fotografías muestran árboles de porte, edad y follaje distintos, lo que sugiere especies o morfotipos distintos (la especie no se registró).

7.2 Factores ambientales que podrían influir

Si un estudio más amplio encontrara hojas mayores a la sombra, estas serían las explicaciones más probables:

  • Luz. En el sotobosque la luz es el recurso limitante y una lámina más grande capta más de ella. Al sol, la luz sobra y la planta puede invertir en hojas más pequeñas y gruesas (Valladares y Niinemets, 2008).
  • Temperatura. Las hojas pequeñas se enfrían mejor por convección. Esa ventaja es importante a pleno sol en una ciudad cálida como Cali (Wright et al., 2017).
  • Agua. Con más radiación aumenta la transpiración. Hojas más pequeñas reducen la pérdida de agua cuando el suelo está seco o compactado.

En nuestro caso no se midió la humedad del suelo. Los árboles de sombra crecen bajo el dosel de otros árboles y los de sol en zonas abiertas, una de ellas junto a un parqueadero (G1_T6). Luz, temperatura y humedad del suelo cambian juntas entre ambos ambientes, así que, aun si hubiéramos encontrado una diferencia, no podríamos atribuirla a uno solo de esos factores.

7.3 Otros factores además del ambiente

  • Especie y morfotipo. No se registró la especie de cada árbol. Si los árboles son de especies distintas, las diferencias en área foliar reflejan sobre todo la identidad de la especie.
  • Edad y estado del árbol. En las fotografías, G1_T4 parece joven y estaba produciendo hojas nuevas (brotes rojizos), y sus hojas promedian 52,1 cm². Los árboles jóvenes y las ramas vigorosas suelen producir hojas más grandes, así que la edad puede explicar parte de las diferencias entre árboles.
  • Selección de las hojas. Las hojas se tomaron de las ramas que se podían alcanzar. En un árbol de sol, las ramas bajas pueden estar parcialmente sombreadas por la propia copa.
  • Clasificación de la exposición. Se hizo a simple vista y no se midió la luz.
  • Fecha de muestreo. Los árboles se muestrearon en 2 fechas (22/10/2026: G1_T3; 26/10/2026: G1_T1, G1_T2, G1_T4, G1_T5, G1_T6).

7.4 Lo que sabemos y lo que todavía es hipótesis

tabla(data.frame(
  `Resultado` = c(
    paste0("La diferencia sombra − sol fue de ", fmt(dif_obs), " cm² en estos ", nrow(medias),
           " árboles, ", ifelse(hay_efecto, "y es estadísticamente detectable.",
                                "pero no se distingue del azar.")),
    paste0("Quitando un árbol a la vez, la diferencia va de ", fmt(loo_min), " a ", fmt(loo_max), " cm²."),
    paste0("Los árboles difieren mucho entre sí, incluso con la misma exposición (", n_misma_dif,
           " de ", n_misma, " pares); cerca del ", fmt(pct_arb, 0), " % de la variación está entre árboles."),
    ifelse(ingenuo_enganoso,
           "Tratar las hojas como réplicas produce un p falsamente significativo.",
           paste0("Tratar las hojas como réplicas cambia el p (de ", fmt_p(p_perm), " a ", fmt_p(p_hojas), ")."))),
  `Hipótesis` = c(
    ifelse(dif_obs > 0,
           "Que las hojas de sombra son realmente más grandes: la diferencia va en ese sentido, pero con tan pocos árboles no se puede confirmar.",
           "Que existe una diferencia entre sol y sombra: con tan pocos árboles no se puede confirmar."),
    "Que ese efecto se mantiene con más árboles de una misma especie.",
    "Que las diferencias entre árboles se deben a la especie o a la edad: no se registraron.",
    "Que la luz, y no la temperatura o la humedad, es la causa de cualquier diferencia."),
  check.names = FALSE), align = "ll",
  caption = "Distinción entre resultados (observados en estos árboles) e hipótesis (por probar).")
Distinción entre resultados (observados en estos árboles) e hipótesis (por probar).
Resultado Hipótesis
La diferencia sombra − sol fue de 17,0 cm² en estos 6 árboles, pero no se distingue del azar. Que las hojas de sombra son realmente más grandes: la diferencia va en ese sentido, pero con tan pocos árboles no se puede confirmar.
Quitando un árbol a la vez, la diferencia va de 5,6 a 27,6 cm². Que ese efecto se mantiene con más árboles de una misma especie.
Los árboles difieren mucho entre sí, incluso con la misma exposición (6 de 6 pares); cerca del 85 % de la variación está entre árboles. Que las diferencias entre árboles se deben a la especie o a la edad: no se registraron.
Tratar las hojas como réplicas produce un p falsamente significativo. Que la luz, y no la temperatura o la humedad, es la causa de cualquier diferencia.

8 Limitaciones y recomendaciones

Limitación Consecuencia Recomendación
Solo 6 árboles (3 de sol y 3 de sombra) El menor p posible era 0,100; ninguna prueba podía ser significativa. Con la variación entre árboles observada harían falta unos 22 árboles por condición para detectar una diferencia de este tamaño Medir muchos más árboles por exposición, de una misma especie
El resultado depende de pocos árboles Al quitar un árbol, la diferencia sombra − sol va de 5,6 a 27,6 cm² (17,0 con todos) Reportar siempre el análisis quitando un árbol a la vez
No se registró la especie El efecto de la especie se mezcla con el de la exposición Comparar sol y sombra dentro de una misma especie
Luz, temperatura y humedad cambian juntas entre ambientes No se puede atribuir una diferencia a un solo factor (p. ej. bosquete con hojarasca frente a zona pavimentada) Medir cada factor por separado
Exposición clasificada a ojo Algunos árboles podrían estar mal clasificados Medir la luz con luxómetro o una aplicación del celular
Estudio observacional Solo se pueden describir asociaciones, no causas Complementar con plántulas criadas a distintos niveles de sombra
Hojas elegidas por alcance Posible sesgo hacia ramas bajas Tomar hojas de alturas y orientaciones definidas de antemano
Imperfecciones en algunos trazos (hoja rota, papel arrugado) y un error de digitación Cuatro hojas tienen observaciones de campo y un área quedó guardada como fecha; se conservaron porque sus valores son plausibles Repetir el trazo dudoso y configurar la plantilla con formato numérico
Árboles muestreados en 2 fechas distintas (22/10/2026: G1_T3; 26/10/2026: G1_T1, G1_T2, G1_T4, G1_T5, G1_T6) El área puede variar con la época y el estado hídrico Muestrear todo el mismo día y repetir en temporada seca y de lluvias

9 Conclusiones

  1. Con el árbol como unidad de análisis, las hojas de sombra promediaron 48,0 cm² y las de sol 31,0 cm²: una diferencia de 17,0 cm² (55 % respecto al sol). Va en el sentido de la hipótesis, pero no se distingue del azar con tan pocos árboles (permutación, p = 0,400; ANOVA anidado, p = 0,295).
  2. Al quitar cualquier árbol la diferencia conserva el mismo signo, pero su tamaño varía entre 5,6 y 27,6 cm². Quitar G1_T5 la reduce a 5,6 cm²: el resultado depende en buena parte de pocos árboles.
  3. Cerca del 85 % de la variación del área foliar se debe a diferencias entre árboles. Incluso árboles con la misma exposición tuvieron hojas de tamaño distinto: 6 de las 6 comparaciones entre árboles de la misma exposición resultaron significativas. Los extremos fueron G1_T2 (sol) y G1_T5 (sombra), cuyas hojas difieren unas 4,2 veces.
  4. Tratar las hojas como réplicas independientes habría llevado a una conclusión falsa (p < 0,001). La unidad correcta es el árbol.
  5. Son resultados la diferencia observada en estos 6 árboles, su fragilidad y la gran variación entre árboles. Son hipótesis por probar que la luz explique el tamaño de hoja y que la especie, la edad o el microhábitat expliquen las diferencias entre árboles: no se registraron ni se midieron.
  6. Como recomendación, se podrían medir unos 22 árboles por condición o más (estimación aproximada), de una misma especie, con una medición objetiva de la luz y muestreando todos el mismo día.

10 Referencias

Givnish, T. J. (1988). Adaptation to sun and shade: a whole-plant perspective. Australian Journal of Plant Physiology, 15, 63–92.

Hurlbert, S. H. (1984). Pseudoreplication and the design of ecological field experiments. Ecological Monographs, 54(2), 187–211.

R Core Team. (2025). R: A language and environment for statistical computing. R Foundation for Statistical Computing.

Valladares, F., y Niinemets, Ü. (2008). Shade tolerance, a key plant feature of complex nature and consequences. Annual Review of Ecology, Evolution, and Systematics, 39, 237–257.

Wright, I. J., Dong, N., Maire, V., et al. (2017). Global climatic drivers of leaf size. Science, 357(6354), 917–921.

Anexo: entorno de cálculo

sessionInfo()
## R version 4.3.3 (2024-02-29 ucrt)
## Platform: x86_64-w64-mingw32/x64 (64-bit)
## Running under: Windows 11 x64 (build 26300)
## 
## Matrix products: default
## 
## 
## locale:
## [1] LC_COLLATE=Spanish_Colombia.utf8  LC_CTYPE=Spanish_Colombia.utf8   
## [3] LC_MONETARY=Spanish_Colombia.utf8 LC_NUMERIC=C                     
## [5] LC_TIME=Spanish_Colombia.utf8    
## 
## time zone: America/Bogota
## tzcode source: internal
## 
## attached base packages:
## [1] stats     graphics  grDevices utils     datasets  methods   base     
## 
## other attached packages:
## [1] kableExtra_1.4.1 ggplot2_4.0.2    readxl_1.4.5    
## 
## loaded via a namespace (and not attached):
##  [1] gtable_0.3.6            jsonlite_2.0.0          dplyr_1.1.4            
##  [4] compiler_4.3.3          tidyselect_1.2.1        xml2_1.3.8             
##  [7] stringr_1.6.0           leaflet_2.2.3           jquerylib_0.1.4        
## [10] systemfonts_1.2.2       scales_1.4.0            yaml_2.3.10            
## [13] fastmap_1.2.0           mime_0.13               R6_2.6.1               
## [16] labeling_0.4.3          generics_0.1.4          knitr_1.51             
## [19] htmlwidgets_1.6.4       tibble_3.2.1            leaflet.providers_2.0.0
## [22] svglite_2.1.3           bslib_0.10.0            pillar_1.11.1          
## [25] RColorBrewer_1.1-3      rlang_1.1.5             cachem_1.1.0           
## [28] stringi_1.8.7           xfun_0.52               sass_0.4.9             
## [31] S7_0.2.0                otel_0.2.0              viridisLite_0.4.3      
## [34] cli_3.6.4               withr_3.0.2             magrittr_2.0.3         
## [37] crosstalk_1.2.2         digest_0.6.37           grid_4.3.3             
## [40] rstudioapi_0.18.0       lifecycle_1.0.5         vctrs_0.6.5            
## [43] evaluate_1.0.5          glue_1.8.0              farver_2.1.2           
## [46] cellranger_1.1.0        rmarkdown_2.30          tools_4.3.3            
## [49] pkgconfig_2.0.3         htmltools_0.5.8.1