Imagen ampliada
# Formatos con coma decimal
fmt   <- function(x, d = 1) formatC(x, format = "f", digits = d, decimal.mark = ",")
fmt_p <- function(p) ifelse(p < 0.001, "< 0,001", formatC(p, format = "f", digits = 3, decimal.mark = ","))
tabla <- function(x, ...) knitr::kable(x, ...)

# Distancia entre dos puntos (fórmula de Haversine), en metros
haversine <- function(lat1, lon1, lat2, lon2) {
  r <- 6371000; p <- pi / 180
  a <- sin((lat2 - lat1) * p / 2)^2 + cos(lat1 * p) * cos(lat2 * p) * sin((lon2 - lon1) * p / 2)^2
  2 * r * asin(sqrt(a))
}

# Prueba de permutación exacta para la diferencia de medias entre dos grupos
perm_exacta <- function(y, etiqueta, nivel) {
  n <- length(y); k <- sum(etiqueta == nivel)
  comb <- combn(n, k)
  dist <- apply(comb, 2, function(i) mean(y[i]) - mean(y[-i]))
  obs  <- mean(y[etiqueta == nivel]) - mean(y[etiqueta != nivel])
  extremos <- sum(abs(dist) >= abs(obs) - 1e-9)
  list(obs = obs, dist = dist, n = length(dist), extremos = extremos, p = extremos / length(dist))
}

col_expo <- c(sol = "#D89A00", sombra = "#1B6CA8")

# Estilo común de las gráficas (R base)
tema <- function(mar = c(4.5, 4.8, 3, 1)) {
  invisible(par(mar = mar, bty = "l", las = 1, col.axis = "grey25", col.lab = "grey15",
                col.main = "grey10", font.main = 2, cex.main = 1.05, tcl = -0.3, mgp = c(2.9, 0.7, 0)))
}

# Escribe HTML "tal cual" en el informe (tarjetas, galerías, pasos del cálculo del área)
html_crudo <- function(x) {
  cuerpo <- paste(x, collapse = "\n")
  cat("\n", strrep("`", 3), "{=html}\n", cuerpo, "\n", strrep("`", 3), "\n\n", sep = "")
}

# Cursiva de markdown (*texto*) a HTML (<em>texto</em>)
cursiva <- function(x) gsub("\\*([^*]+)\\*", "<em>\\1</em>", x)

1 Contexto del estudio

1.1 Ficha del estudio

# --- Lectura de la base (la original no se modifica) ---
crudo <- as.data.frame(readxl::read_excel(params$archivo), stringsAsFactors = FALSE)
names(crudo) <- tolower(trimws(names(crudo)))

esperadas <- c("grupo", "id_arbol", "lat", "lon", "n_hoja", "area_cm2", "exposicion", "fecha", "observaciones")
faltan <- setdiff(esperadas, names(crudo))
if (length(faltan) > 0) stop("A la base le faltan estas columnas: ", paste(faltan, collapse = ", "))

num <- function(x) suppressWarnings(as.numeric(gsub(",", ".", as.character(x))))
parse_fecha <- function(x) {
  if (inherits(x, "POSIXt") || inherits(x, "Date")) return(as.Date(x))
  as.Date(as.character(x), tryFormats = c("%d/%m/%Y", "%Y-%m-%d"))
}

datos <- crudo                                   # copia de trabajo
datos$grupo      <- num(datos$grupo)
datos$id_arbol   <- trimws(as.character(datos$id_arbol))
datos$lat        <- num(datos$lat)
datos$lon        <- num(datos$lon)
datos$n_hoja     <- num(datos$n_hoja)
datos$area_cm2   <- num(datos$area_cm2)
datos$exposicion <- tolower(trimws(as.character(datos$exposicion)))
datos$fecha      <- parse_fecha(datos$fecha)
datos$observaciones <- as.character(datos$observaciones)

n_arboles <- length(unique(datos$id_arbol))
n_hojas   <- nrow(datos)
5
árboles
50
hojas medidas
3 + 2
árboles de sol + sombra
22/10 – 29/10
fechas de medición
fechas <- range(datos$fecha, na.rm = TRUE)
ficha <- data.frame(
  Elemento = c("Pregunta", "Tipo de estudio", "Lugar", "Fechas de recolección", "Factor de interés",
               "Variable respuesta", "Unidad experimental", "Unidad de muestreo", "Tamaño de la muestra", "Instrumentos"),
  Descripción = c(
    "¿Cómo cambia el área de las hojas según el lugar donde crece el árbol?",
    "Observacional y comparativo: no se asignó sol o sombra, se eligieron árboles que ya estaban en cada condición",
    "Campus de la Pontificia Universidad Javeriana Cali, barrio Pance y Conjunto residencial La siembra en el barrio el ingenio 2, Cali (Valle del Cauca, Colombia  )",
    paste(format(fechas[1], "%d/%m/%Y"), "a", format(fechas[2], "%d/%m/%Y")),
    "Exposición del árbol: sol (luz directa la mayor parte del día) o sombra",
    "Área de la hoja en cm²",
    "El árbol (la exposición es una propiedad suya)",
    "La hoja: 10 hojas adultas y sanas por árbol",
    paste0(n_arboles, " árboles; ", n_hojas, " hojas registradas (", length(unique(datos$grupo)), " grupos)"),
    "Celular (cámara y GPS), papel blanco, lápiz,Marcador y marco de lados conocidos, y un script que calcula el área a partir de la foto"))
tabla(ficha, align = "ll")
Elemento Descripción
Pregunta ¿Cómo cambia el área de las hojas según el lugar donde crece el árbol?
Tipo de estudio Observacional y comparativo: no se asignó sol o sombra, se eligieron árboles que ya estaban en cada condición
Lugar Campus de la Pontificia Universidad Javeriana Cali, barrio Pance y Conjunto residencial La siembra en el barrio el ingenio 2, Cali (Valle del Cauca, Colombia )
Fechas de recolección 22/10/2026 a 29/10/2026
Factor de interés Exposición del árbol: sol (luz directa la mayor parte del día) o sombra
Variable respuesta Área de la hoja en cm²
Unidad experimental El árbol (la exposición es una propiedad suya)
Unidad de muestreo La hoja: 10 hojas adultas y sanas por árbol
Tamaño de la muestra 5 árboles; 50 hojas registradas (5 grupos)
Instrumentos Celular (cámara y GPS), papel blanco, lápiz,Marcador y marco de lados conocidos, y un script que calcula el área a partir de la foto

1.2 Sitio de estudio y protocolo

El estudio se hizo en el campus de la universidad y en el Conjunto residencial La siembra , en Cali, en el valle geográfico del río Cauca (zona tropical). Dentro del campus hay árboles en zonas abiertas y bajo otras condiciones(tapado por un edificio u arbol), lo que da un gradiente natural de luz. La marca de agua de las fotos de campo (altitud del GPS del celular, que es aproximada) indica unos 1.040 a 1.060 m s. n. m.

El protocolo, en resumen:

  1. Se eligieron árboles sanos, unos a pleno sol y otros a la sombra, y se registraron sus coordenadas con el GPS del celular.
  2. De cada árbol se tomaron 10 hojas adultas al alcance de nosotros, expandidas y sin daño.
  3. Cada hoja se trazó sobre papel, se fotografió dentro de un marco rectangular de lados conocidos y se midió su área con un script (corrección de perspectiva usando el marco como escala).
  4. Los datos se capturaron en una plantilla (una fila por hoja).

especie de cada árbol:

info_arboles <- data.frame(
  id = c("sl1_la", "sm1_gn", "sl2_bu", "sm2_na", "sl3_pa"),
  especie = c("Annona muricata nombre común (Guanabana)", "citrus sp. nombre comun (limón)",
              "*Bunchosia* sp. (entre *B. armeniaca* y *B. glandulifera*)",
              "posiblemente *Casearia* sp.", "Ficus sp."),
  marco = c("completar", "completar", "8 \u00d7 16", "8 \u00d7 10", "completar"),
  stringsAsFactors = FALSE)

# Área (cm²) del marco de un árbol, a partir de "ancho × largo"
marco_cm2 <- function(id) {
  m <- info_arboles$marco[info_arboles$id == id]
  if (length(m) != 1) return(NA_real_)
  p <- suppressWarnings(as.numeric(strsplit(m, "\\s*[x\u00d7]\\s*")[[1]]))
  if (length(p) == 2 && !anyNA(p)) prod(p) else NA_real_
}

1.3 Los árboles de estudio

Una foto de cada árbol permite ver dónde crece y juzgar si la clasificación en sol o sombra tiene sentido. Haz clic en una foto para ampliarla.

# Coordenadas por árbol (la corrección de coordenadas se documenta en la sección 4)
corregir_coord <- function(x, limite) { while (!is.na(x) && abs(x) > limite) x <- x / 10; x }
sitios <- unique(datos[, c("grupo", "id_arbol", "lat", "lon", "exposicion")])
sitios$lat <- sapply(sitios$lat, corregir_coord, limite = 90)
sitios$lon <- sapply(sitios$lon, corregir_coord, limite = 180)
sitios$hojas <- as.vector(table(datos$id_arbol)[sitios$id_arbol])

buscar_foto <- function(id) {
  f <- Sys.glob(file.path("fotos", paste0(id, ".*")))
  if (length(f) > 0) f[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='200'><br>")
  paste0(img, "<b>", s$id_arbol, "</b><br>Exposición: ", s$exposicion, "<br>Grupo ", s$grupo,
         "<br>Hojas registradas: ", s$hojas, "<br>", fmt(s$lat, 5), "; ", fmt(s$lon, 5))
}, character(1))

cat("Árboles:", nrow(sitios), "| con foto:", sum(!is.na(sitios$foto)), "\n")
Árboles: 5 | con foto: 5 
Foto del árbol sl1_la
sl1_lasol
Annona muricata nombre común (Guanabana)
Grupo 1 · 10 hojas
Foto del árbol sm1_gn
sm1_gnsombra
citrus sp. nombre comun (limón)
Grupo 2 · 10 hojas
Foto del árbol sl2_bu
sl2_busol
Bunchosia sp. (entre B. armeniaca y B. glandulifera)
Grupo 3 · 10 hojas
Foto del árbol sm2_na
sm2_nasombra
posiblemente Casearia sp.
Grupo 4 · 10 hojas
Foto del árbol sl3_pa
sl3_pasol
Ficus sp.
Grupo 5 · 10 hojas

1.4 Mapa de los sitios

Cada punto es un árbol. Haz clic en un punto para ver sus datos

if (requireNamespace("leaflet", quietly = TRUE)) {
  library(leaflet)
  mapa <- leaflet(sitios) %>%
    addProviderTiles(providers$Esri.WorldImagery, group = "Satélite") %>%
    addTiles(group = "Calles") %>%
    addCircleMarkers(data = sitios[sitios$exposicion == "sol", ], lng = ~lon, lat = ~lat,
                     radius = 9, weight = 2, color = "white", fillColor = col_expo[["sol"]],
                     fillOpacity = 0.95, label = ~id_arbol, popup = ~popup,
                     popupOptions = popupOptions(maxWidth = 240), group = "Sol") %>%
    addCircleMarkers(data = sitios[sitios$exposicion == "sombra", ], lng = ~lon, lat = ~lat,
                     radius = 9, weight = 2, color = "white", fillColor = col_expo[["sombra"]],
                     fillOpacity = 0.95, label = ~id_arbol, popup = ~popup,
                     popupOptions = popupOptions(maxWidth = 240), group = "Sombra") %>%
    addLayersControl(baseGroups = c("Satélite", "Calles"), overlayGroups = c("Sol", "Sombra"),
                     options = layersControlOptions(collapsed = FALSE)) %>%
    addLegend("bottomright", colors = unname(col_expo), labels = c("Sol", "Sombra"),
              title = "Exposición", opacity = 1) %>%
    addScaleBar("bottomleft")
  mapa
} else {
  cat("El paquete 'leaflet' no está instalado: se muestra un mapa simple.",
      "Para el mapa interactivo ejecuta install.packages('leaflet').\n")
  plot(sitios$lon, sitios$lat, pch = 21, cex = 2, bg = col_expo[sitios$exposicion],
       asp = 1 / cos(mean(sitios$lat) * pi / 180), xlab = "Longitud", ylab = "Latitud",
       main = "Ubicación de los árboles")
  text(sitios$lon, sitios$lat, sitios$id_arbol, pos = 3, cex = 0.7)
  legend("topright", legend = names(col_expo), pt.bg = col_expo, pch = 21, bty = "n")
}

2 Introducción

Pregunta. ¿Cómo cambia el área de las hojas según el lugar donde crece el árbol?

Por qué esperamos una diferencia. Una hoja equilibra dos cosas: captar luz y no perder demasiada agua ni calentarse de más. A pleno sol sobra luz y el riesgo es el exceso de calor y de transpiración, así que se esperan hojas más pequeñas. A la sombra la luz es el recurso escaso, y una lámina más grande intercepta más de ella.

Hipótesis. Hipótesis. Las hojas de los árboles que crecen a la sombra tienen, en promedio, mayor área que las de los árboles que crecen a pleno sol. Dicho como hipótesis estadísticas, con el promedio de los árboles como unidad:

H₀: el área foliar media es igual en los árboles de sol y de sombra (μ_sol = μ_sombra).

H₁: el área foliar media es distinta (μ_sombra ≠ μ_sol); la dirección esperada es μ_sombra > μ_sol.

3 Métodos

3.1 Diseño del muestreo

Se midieron 10 hojas por árbol en árboles ya clasificados como de sol (luz directa la mayor parte del día, sin cobertura de otros árboles ni edificios) o de sombra. Las hojas fueron adultas, expandidas y sin daño evidente, y se tomaron de distintas partes.

3.2 Medición del área de cada hoja

Cada hoja se trazó sobre papel, se fotografió dentro de un marco rectangular de lados conocidos y un script calculó el área en cm² (la foto se corrige en perspectiva con el marco y el área se obtiene por conteo de píxeles dentro del contorno de la hoja). Los datos se anotaron en una plantilla: una fila por hoja, con grupo, árbol, coordenadas, número de hoja, área, exposición, fecha y observaciones.

El área no se midió con regla ni con papel milimetrado: se calculó a partir de una foto del trazo. Las imágenes de abajo son las salidas reales del script para dos hojas de ejemplo, de modo que se vea cómo se pasa de un dibujo a un número en cm².

# ¿A qué árbol y a qué número de hoja corresponde cada trazo? (ajústalo si hace falta)
hojas_trazadas <- data.frame(
  clave  = c("A", "B", "C", "D", "E"),
  arbol  = "sm2_na",
  n_hoja = 1:5,
  stringsAsFactors = FALSE)

# Área registrada en la base para esa hoja, y área del marco usado en ese árbol
hojas_trazadas$area <- mapply(function(a, n) {
  v <- datos$area_cm2[datos$id_arbol == a & datos$n_hoja == n]
  if (length(v) == 1) v else NA_real_
}, hojas_trazadas$arbol, hojas_trazadas$n_hoja, USE.NAMES = FALSE)
hojas_trazadas$marco_cm2 <- vapply(hojas_trazadas$arbol, marco_cm2, numeric(1), USE.NAMES = FALSE)

# Imágenes: hojas/hoja<clave>_<paso>_<nombre>.png
# paso 1 = trazo y marco, 2 = trazos detectados, 3 = selección final, 4 = vista corregida
archivo_hoja <- function(clave, paso) {
  f <- Sys.glob(file.path("hojas", paste0("hoja", clave, "_", paso, "_*")))
  if (length(f) > 0) f[1] else NA_character_
}

3.2.1 Del trazo al número, paso a paso

Hoja A · árbol sm2_na, hoja n.º 1
1
Trazo y marco
Trazo y marco
La hoja se traza con lápiz dentro de un marco rectangular de lados conocidos. El marco es la regla del método: da la escala.
2
Trazos detectados
Trazos detectados
El script deja solo las líneas (blanco sobre negro). Se revisa que el marco y el contorno de la hoja sean continuos: si el trazo tiene un hueco, no se sabe qué queda dentro.
3
Selección final
Selección final
Azul: el marco. Verde y rojo: el borde exterior y el borde interior de la línea de la hoja (el lápiz deja una línea con grosor, así que el contorno tiene dos bordes).
4
Vista corregida
Vista corregida
La foto se endereza con el marco (lado 1 horizontal, lado 2 vertical) para quitar la perspectiva. El interior de la hoja (verde claro) se cuenta en píxeles.
Área registrada en la base: 16,18 cm². El marco mide 80 cm², así que la hoja ocupa cerca del 20 % del marco.
Hoja B · árbol sm2_na, hoja n.º 2
1
Trazo y marco
Trazo y marco
La hoja se traza con lápiz dentro de un marco rectangular de lados conocidos. El marco es la regla del método: da la escala.
2
Trazos detectados
Trazos detectados
El script deja solo las líneas (blanco sobre negro). Se revisa que el marco y el contorno de la hoja sean continuos: si el trazo tiene un hueco, no se sabe qué queda dentro.
3
Selección final
Selección final
Azul: el marco. Verde y rojo: el borde exterior y el borde interior de la línea de la hoja (el lápiz deja una línea con grosor, así que el contorno tiene dos bordes).
4
Vista corregida
Vista corregida
La foto se endereza con el marco (lado 1 horizontal, lado 2 vertical) para quitar la perspectiva. El interior de la hoja (verde claro) se cuenta en píxeles.
Área registrada en la base: 14,05 cm². El marco mide 80 cm², así que la hoja ocupa cerca del 18 % del marco.

Como el marco mide lo que mide (por ejemplo 8 × 10 cm), cada píxel de la foto corregida equivale a una superficie conocida. En esencia: área de la hoja = (píxeles dentro del contorno ÷ píxeles dentro del marco) × área real del marco.

Qué se revisa en cada hoja antes de aceptar la medición:

  • Que el marco y el contorno estén cerrados en la imagen de trazos detectados (paso 2).
  • Que la selección (paso 3) siga el borde de la hoja y no otra cosa, como una sombra o una arruga del papel.
  • Que la vista corregida (paso 4) deje el marco como un rectángulo y la hoja bien rellena.
  • El grosor del trazo de lápiz o marcador es una fuente de error de medición: entre el borde exterior y el interior hay una diferencia del orden de unas décimas de cm² en hojas de unos 15 cm². Es pequeña frente a las diferencias entre árboles que aparecen en los resultados, pero conviene mencionarla.

3.2.2 Las hojas trazadas

Trazo de la hoja A
Hoja A
sm2_na · hoja n.º 1
16,18 cm²
Trazo de la hoja B
Hoja B
sm2_na · hoja n.º 2
14,05 cm²
Trazo de la hoja C
Hoja C
sm2_na · hoja n.º 3
16,58 cm²
Trazo de la hoja D
Hoja D
sm2_na · hoja n.º 4
14,71 cm²
Trazo de la hoja E
Hoja E
sm2_na · hoja n.º 5
19,02 cm²

Estas son las hojas de las que se dispone del trazo en su marco. Su área viene de la base (la misma que se analiza más adelante); la clave A–E solo sirve para nombrarlas en el informe.

3.3 Análisis estadístico

Todo en R base (R version 4.6.1 (2026-06-24 ucrt)), salvo la lectura del .xlsx y el mapa:

  1. Revisión de la base y decisiones documentadas sobre cada problema encontrado.
  2. Por árbol: n, media, mediana, desviación estándar y coeficiente de variación.
  3. Gráficas: un diagrama de cajas por árbol y otro por exposición.
  4. Valores atípicos: regla de Tukey dentro de cada árbol, más la revisión de las observaciones.
  5. Comparación sol–sombra con el árbol como unidad de análisis (se promedian las 10 hojas de cada árbol): permutación exacta y prueba de Welch.
  6. ANOVA de un factor entre árboles (con Tukey) y ANOVA anidado (hojas dentro de árboles dentro de exposición).

4 Datos y revisión de calidad

Antes de calcular nada hay que mirar la base. Se trabaja sobre una copia (datos); la base original (crudo) no se toca.

4.1 Lo que trae la base

head(datos)

La base tiene 50 filas y 5 valores distintos en la columna grupo. Si todo estuviera completo, serían (número de árboles × 10 hojas) = 50.

4.2 Revisiones

id_limpio <- gsub("\\s+", "", tolower(datos$id_arbol))
expo_validas <- datos$exposicion %in% c("sol", "sombra")
dup <- duplicated(datos[, c("id_arbol", "n_hoja")])
expo_por_arbol <- tapply(datos$exposicion, datos$id_arbol, function(x) length(unique(x)))
hojas_por_arbol <- table(datos$id_arbol)
med_arbol <- ave(datos$area_cm2, datos$id_arbol, FUN = function(x) median(x, na.rm = TRUE))
extremas <- !is.na(datos$area_cm2) & datos$area_cm2 > 0 &
  (datos$area_cm2 > 10 * med_arbol | datos$area_cm2 < 0.1 * med_arbol)
lat_mala <- !is.na(datos$lat) & abs(datos$lat) > 90
lon_mala <- !is.na(datos$lon) & abs(datos$lon) > 180

revision <- data.frame(
  Revision = c(
    "IDs con mayúsculas o espacios de más",
    "Exposiciones escritas de varias formas o distintas de sol/sombra",
    "Áreas faltantes o <= 0",
    "Filas repetidas (árbol y n.º de hoja)",
    "Árboles con exposición distinta en sus filas",
    "Árboles que no tienen 10 hojas",
    "Hojas con un área > 10 veces o < 0,1 veces la mediana de su árbol",
    "Coordenadas fuera de rango (latitud > 90° o longitud > 180°)"
  ),
  Resultado = c(
    sum(datos$id_arbol != id_limpio),
    sum(!expo_validas),
    sum(is.na(datos$area_cm2) | datos$area_cm2 <= 0),
    sum(dup),
    sum(expo_por_arbol > 1),
    sum(hojas_por_arbol != 10),
    sum(extremas),
    sum(lat_mala | lon_mala)
  )
)

tabla(revision, align = "lr")
Revision Resultado
IDs con mayúsculas o espacios de más 0
Exposiciones escritas de varias formas o distintas de sol/sombra 0
Áreas faltantes o <= 0 0
Filas repetidas (árbol y n.º de hoja) 0
Árboles con exposición distinta en sus filas 0
Árboles que no tienen 10 hojas 0
Hojas con un área > 10 veces o < 0,1 veces la mediana de su árbol 0
Coordenadas fuera de rango (latitud > 90° o longitud > 180°) 0
cat("Hojas por árbol:\n"); print(hojas_por_arbol)
Hojas por árbol:

sl1_la sl2_bu sl3_pa sm1_gn sm2_na 
    10     10     10     10     10 
cat("\nÁrboles por grupo:\n"); print(table(tapply(datos$id_arbol, datos$grupo, function(x) length(unique(x)))),
                                      zero.print = ".")

Árboles por grupo:

1 
5 
cat("\nFecha(s) de medición por árbol:\n")

Fecha(s) de medición por árbol:
print(tapply(format(datos$fecha, "%d/%m/%Y"), datos$id_arbol, function(x) paste(unique(x), collapse = ", ")))
      sl1_la       sl2_bu       sl3_pa       sm1_gn       sm2_na 
"22/10/2026" "23/10/2026" "29/10/2026" "22/10/2026" "29/10/2026" 
cat("\nRango de n_hoja:", range(datos$n_hoja, na.rm = TRUE), "\n")

Rango de n_hoja: 1 10 
cat("\nÁrboles por exposición:\n"); print(table(tapply(datos$exposicion, datos$id_arbol, function(x) x[1])))

Árboles por exposición:

   sol sombra 
     3      2 

Formato de los IDs. Los IDs aparecen como un código: s + l o m (sol o sombra) + número de árbol + _ + dos letras que hacen refrencia a la especie. Se comprueba que todos sigan el mismo formato: un ID que no lo cumple puede ser un error de captura.

ids <- sort(unique(datos$id_arbol))
formato_ok <- grepl("^s[lm][0-9]+_[a-z]{2}$", ids)
# la letra l/m del ID debe coincidir con la exposición registrada
expo_id <- ifelse(substr(ids, 2, 2) == "l", "sol", "sombra")
expo_reg <- tapply(datos$exposicion, datos$id_arbol, function(x) x[1])[ids]
tabla(data.frame(ID = ids,
                 Formato = ifelse(formato_ok, "correcto", "DISTINTO: revisar"),
                 `Exposición coincide con el ID` = ifelse(expo_id == expo_reg, "sí", "NO: revisar"),
                 check.names = FALSE), row.names = FALSE, align = "lll")
ID Formato Exposición coincide con el ID
sl1_la correcto sí
sl2_bu correcto sí
sl3_pa correcto sí
sm1_gn correcto sí
sm2_na correcto sí

Ubicación:

coord_raw <- unique(datos[, c("id_arbol", "lat", "lon")])
coord_raw$problema <- ifelse(abs(coord_raw$lat) > 90 | abs(coord_raw$lon) > 180, "fuera de rango", "")
coord_raw$lat <- fmt(coord_raw$lat, 4); coord_raw$lon <- fmt(coord_raw$lon, 4)
tabla(coord_raw, row.names = FALSE, align = "lrrl")
id_arbol lat lon problema
sl1_la 3,3458 -76,5337
sm1_gn 3,3469 -76,5333
sl2_bu 3,3467 -76,5333
sm2_na 3,3478 -76,5328
sl3_pa 3,3805 -76,5611

4.3 Decisiones

Cada problema se resuelve con una regla explícita, y las filas originales se conservan en la base (usar y motivo) para poder auditar lo que se hizo.

datos$usar   <- TRUE
datos$motivo <- ""

# 1. Coordenadas fuera de rango: se asume un separador decimal mal puesto (se divide por 10 hasta quedar en rango)
n_coord <- sum(lat_mala | lon_mala)
datos$lat <- sapply(datos$lat, corregir_coord, limite = 90)
datos$lon <- sapply(datos$lon, corregir_coord, limite = 180)
datos$motivo[lat_mala | lon_mala] <- "coordenada corregida (separador decimal)"

# 2. Filas repetidas: se elimina la copia
datos$usar[dup] <- FALSE
datos$motivo[dup] <- "fila repetida"

# 3. Área faltante o <= 0: se excluye la hoja
sin_area <- is.na(datos$area_cm2) | datos$area_cm2 <= 0
datos$usar[sin_area] <- FALSE
datos$motivo[sin_area] <- "área faltante o <= 0"

# 4. Área en mm² en lugar de cm² (más de 10 veces la mediana del árbol): se divide por 100
en_mm <- !sin_area & datos$area_cm2 > 10 * med_arbol & datos$area_cm2 / 100 > 0.1 * med_arbol
datos$area_cm2[en_mm] <- datos$area_cm2[en_mm] / 100
datos$motivo[en_mm] <- "área en mm² convertida a cm²"

decisiones <- data.frame(
  Problema = c("Coordenada fuera de rango", "Fila repetida", "Área faltante o <= 0", "Área en mm² en lugar de cm²"),
  Filas = c(n_coord, sum(dup), sum(sin_area), sum(en_mm)),
  Decisión = c("Dividir por 10 hasta quedar en rango (p. ej. -765,327 pasa a -76,5327)",
               "Eliminar la copia", "Excluir la hoja (no hay dato válido)", "Dividir por 100"))
tabla(decisiones, align = "lrl")
Problema Filas Decisión
Coordenada fuera de rango 0 Dividir por 10 hasta quedar en rango (p. ej. -765,327 pasa a -76,5327)
Fila repetida 0 Eliminar la copia
Área faltante o <= 0 0 Excluir la hoja (no hay dato válido)
Área en mm² en lugar de cm² 0 Dividir por 100
limpios <- datos[datos$usar, ]
cat("Filas originales:", nrow(crudo), "\nFilas que se usan:", nrow(limpios), "\nÁrboles:", length(unique(limpios$id_arbol)), "\n")
Filas originales: 50 
Filas que se usan: 50 
Árboles: 5 
coord <- unique(limpios[, c("id_arbol", "lat", "lon")])
pares <- t(combn(nrow(coord), 2))
dist_tab <- data.frame(
  `Árbol A` = coord$id_arbol[pares[, 1]], `Árbol B` = coord$id_arbol[pares[, 2]],
  d = haversine(coord$lat[pares[, 1]], coord$lon[pares[, 1]], coord$lat[pares[, 2]], coord$lon[pares[, 2]]),
  check.names = FALSE)
dist_tab <- dist_tab[order(dist_tab$d), ]
dist_tab$`Distancia (m)` <- round(dist_tab$d); dist_tab$d <- NULL
tabla(dist_tab, row.names = FALSE, align = "llr")
Árbol A Árbol B Distancia (m)
sm1_gn sl2_bu 22
sl1_la sl2_bu 109
sm1_gn sm2_na 114
sl1_la sm1_gn 130
sl2_bu sm2_na 134
sl1_la sm2_na 244
sm2_na sl3_pa 4805
sm1_gn sl3_pa 4846
sl2_bu sl3_pa 4863
sl1_la sl3_pa 4913

Después de corregir, la distancia entre árboles va de 22 a 4913 m: ningún par está a menos de 5 m, así que no hay indicios de árboles medidos dos veces.

5 Resultados

5.1 Estadística descriptiva por árbol

limpios$id_arbol <- as.character(limpios$id_arbol)
orden_arboles <- unique(limpios[order(limpios$grupo), "id_arbol"])

resumen <- do.call(rbind, lapply(orden_arboles, function(a) {
  x <- limpios$area_cm2[limpios$id_arbol == a]
  data.frame(arbol = a, exposicion = limpios$exposicion[limpios$id_arbol == a][1], n = length(x),
             media = mean(x), mediana = median(x), de = sd(x), cv = 100 * sd(x) / mean(x),
             min = min(x), max = max(x))
}))

tabla(data.frame(Árbol = resumen$arbol, Exposición = resumen$exposicion, n = resumen$n,
                 Media = fmt(resumen$media), Mediana = fmt(resumen$mediana), DE = fmt(resumen$de),
                 `CV (%)` = fmt(resumen$cv), `Mín.` = fmt(resumen$min), `Máx.` = fmt(resumen$max),
                 check.names = FALSE),
      align = "llrrrrrrr", caption = "Área foliar (cm²) por árbol. DE = desviación estándar; CV = DE / media.")
Área foliar (cm²) por árbol. DE = desviación estándar; CV = DE / media.
Árbol Exposición n Media Mediana DE CV (%) Mín. Máx.
sl1_la sol 10 32,7 33,1 4,5 13,8 23,6 37,7
sm1_gn sombra 10 67,1 66,7 12,5 18,7 49,0 85,0
sl2_bu sol 10 41,9 40,9 4,0 9,5 36,7 50,1
sm2_na sombra 10 15,9 16,4 1,9 11,7 13,4 19,0
sl3_pa sol 10 14,7 14,6 1,4 9,8 12,7 16,3

Dos cosas para mirar en esta tabla: (1) la diferencia entre árboles (las medias van de 14,7 a 67,1 cm²), que conviene comparar con la variación dentro de cada árbol (la DE); (2) el CV, que mide cuánto varían las hojas de un árbol respecto a su propia media (aquí entre 10 % y 19 %).

5.2 Un diagrama de cajas por árbol

dg <- limpios
dg$id_arbol <- factor(dg$id_arbol, levels = resumen$arbol)
tema()
boxplot(area_cm2 ~ id_arbol, data = dg, outline = FALSE, las = 1,
        col = adjustcolor(col_expo[resumen$exposicion], 0.55), border = "grey25",
        xlab = "Árbol", ylab = expression("Área foliar (cm"^2*")"),
        main = "Área foliar de cada hoja, por árbol")
stripchart(area_cm2 ~ id_arbol, data = dg, vertical = TRUE, method = "jitter", jitter = 0.12,
           pch = 16, col = adjustcolor("black", 0.45), add = TRUE)
legend("topright", legend = c("sol", "sombra"), fill = adjustcolor(col_expo, 0.55), bty = "n")

Cada caja resume las 10 hojas de un árbol (la línea central es la mediana y la caja contiene la mitad central de las hojas); los puntos son las hojas.

5.3 Valores atípicos: ¿error o hoja realmente distinta?

Criterio. Se revisa cada hoja con dos preguntas, en este orden:

  1. ¿Hay una causa de medición (unidad equivocada, hoja dañada, trazo dudoso, foto borrosa)? Si sí, se corrige o se excluye.
  2. Si no hay causa de medición pero la hoja queda fuera de la regla de Tukey de su árbol (por debajo de Q1 − 1,5·RIC o por encima de Q3 + 1,5·RIC), es una hoja realmente distinta: se conserva y se comprueba cuánto cambia el resultado sin ella (análisis de sensibilidad).
es_atipica <- function(x) {
  q <- quantile(x, c(0.25, 0.75)); r <- q[2] - q[1]
  x < q[1] - 1.5 * r | x > q[2] + 1.5 * r
}
limpios$atipica <- ave(limpios$area_cm2, limpios$id_arbol, FUN = es_atipica) == 1
atip <- limpios[limpios$atipica, c("id_arbol", "n_hoja", "area_cm2", "observaciones")]
atip$area_cm2 <- fmt(atip$area_cm2, 2)
atip$observaciones[is.na(atip$observaciones)] <- "La hoja es una fiel representación de la diversidad del árbol"
if (nrow(atip) > 0) tabla(atip, row.names = FALSE, caption = "Hojas fuera de los límites de Tukey de su árbol.") else cat("Ninguna hoja queda fuera de los límites de Tukey de su árbol.\n")
Hojas fuera de los límites de Tukey de su árbol.
id_arbol n_hoja area_cm2 observaciones
sl2_bu 2 50,14 La hoja es una fiel representación de la diversidad del árbol
# Sensibilidad: diferencia sombra - sol según cómo se resuma cada árbol
dif_sombra_sol <- function(v, expo) mean(v[expo == "sombra"]) - mean(v[expo == "sol"])
exp_arbol <- resumen$exposicion
med_todas  <- tapply(limpios$area_cm2, limpios$id_arbol, mean)[resumen$arbol]
med_mediana <- tapply(limpios$area_cm2, limpios$id_arbol, median)[resumen$arbol]
med_sin    <- tapply(limpios$area_cm2[!limpios$atipica], limpios$id_arbol[!limpios$atipica], mean)[resumen$arbol]
sens <- data.frame(
  `Cómo se resume cada árbol` = c("Media (todas las hojas)", "Mediana", "Media sin las hojas atípicas"),
  `Sombra − sol (cm²)` = fmt(c(dif_sombra_sol(med_todas, exp_arbol),
                                dif_sombra_sol(med_mediana, exp_arbol),
                                dif_sombra_sol(med_sin, exp_arbol))),
  check.names = FALSE)
tabla(sens, align = "lr", caption = "Sensibilidad de la diferencia entre exposiciones a las hojas atípicas.")
Sensibilidad de la diferencia entre exposiciones a las hojas atípicas.
Cómo se resume cada árbol Sombra − sol (cm²)
Media (todas las hojas) 11,7
Mediana 12,0
Media sin las hojas atípicas 12,0
rango_sens <- range(c(dif_sombra_sol(med_todas, exp_arbol), dif_sombra_sol(med_mediana, exp_arbol), dif_sombra_sol(med_sin, exp_arbol)))

La diferencia entre sombra y sol va de 11,7 a 12,0 cm² según cómo se trate la hoja atípica.

5.4 Un diagrama de cajas por exposición

dg$expo <- factor(dg$exposicion, levels = c("sol", "sombra"))
simbolos <- c(16, 17, 15, 18, 8, 3, 4, 1)[seq_along(resumen$arbol)]
tema()
boxplot(area_cm2 ~ expo, data = dg, outline = FALSE, las = 1, col = adjustcolor(col_expo, 0.5),
        border = "grey25", xlab = "Exposición", ylab = expression("Área foliar (cm"^2*")"),
        main = "Hojas por exposición")
points(as.integer(dg$expo) + runif(nrow(dg), -0.15, 0.15), dg$area_cm2,
       pch = simbolos[as.integer(dg$id_arbol)], col = adjustcolor("black", 0.6))
legend("topleft", legend = levels(dg$id_arbol), pch = simbolos, bty = "n", title = "Árbol", cex = 0.85)

Las hojas no son independientes porque se agrupan por individuo

5.5 Unidad de análisis: el árbol

Lo que se ha asignado a sol o a sombra es el árbol, no la hoja. Las 10 hojas de un mismo árbol comparten suelo, edad, genética y luz. Por eso se promedian y se compara un valor por árbol.

medias <- aggregate(area_cm2 ~ id_arbol + exposicion, data = limpios, FUN = mean)
medias$exposicion <- factor(medias$exposicion, levels = c("sol", "sombra"))   # sol = nivel de referencia en las pruebas
medias <- medias[order(medias$exposicion == "sol", medias$id_arbol), ]          # en la tabla, primero sombra
n_expo <- table(medias$exposicion)
tabla(data.frame(Árbol = medias$id_arbol, Exposición = medias$exposicion,
                 `Media del árbol (cm²)` = fmt(medias$area_cm2), check.names = FALSE), align = "llr")
Árbol Exposición Media del árbol (cm²)
sm1_gn sombra 67,1
sm2_na sombra 15,9
sl1_la sol 32,7
sl2_bu sol 41,9
sl3_pa sol 14,7
m_sombra <- mean(medias$area_cm2[medias$exposicion == "sombra"])
m_sol    <- mean(medias$area_cm2[medias$exposicion == "sol"])
dif_obs  <- m_sombra - m_sol

Hay 2 árbol(es) de sombra y 3 de sol: 5 datos independientes, no 50. La media de los árboles de sombra es 41,5 cm² y la de sol 29,8 cm²; la diferencia observada (sombra − sol) es de 11,7 cm².

5.5.1 ¿Qué tan raro sería ver esa diferencia por azar?

Con tan pocos árboles se puede hacer una prueba de permutación exacta: si la exposición no importara, cualquier forma de repartir las etiquetas “sol” y “sombra” entre los árboles sería igual de probable. Se calcula la diferencia para todos los repartos posibles y se mira dónde queda el observado.

pe <- perm_exacta(medias$area_cm2, as.character(medias$exposicion), "sombra")
p_perm <- pe$p
tema(c(4.5, 1, 3, 1))
stripchart(pe$dist, method = "stack", pch = 21, bg = "grey85", cex = 2, xlab = "Diferencia sombra − sol (cm²)",
           main = paste(pe$n, "repartos posibles"), yaxt = "n", xlim = range(c(pe$dist, pe$obs)) * 1.05)
abline(v = 0, lty = 3, col = "grey50")
points(pe$obs, 1, pch = 21, bg = "#1B6CA8", cex = 2.6)
legend("topright", legend = "reparto observado", pch = 21, pt.bg = "#1B6CA8", bty = "n")

Hay 10 repartos posibles; 6 de ellos dan una diferencia al menos tan extrema como la observada, así que p = 0,600. El valor más pequeño que cualquier resultado podría dar con 3 y 2 árboles sería 0,100. Con 5 árboles, el diseño no puede dar resultados “significativos” al 5 %, sean cuales sean los datos.

5.5.2 Lo que habría pasado ignorando el árbol

t_hojas <- t.test(area_cm2 ~ exposicion, data = limpios)         # trata las hojas como independientes
t_arb   <- t.test(area_cm2 ~ exposicion, data = medias)          # un promedio por árbol (Welch)
p_hojas <- t_hojas$p.value
p_welch <- t_arb$p.value
comp <- data.frame(
  Análisis = c("Hojas como datos independientes (incorrecto)", "Un promedio por árbol (Welch)",
               "Un promedio por árbol (permutación exacta)"),
  `n sol` = c(sum(limpios$exposicion == "sol"), n_expo[["sol"]], n_expo[["sol"]]),
  `n sombra` = c(sum(limpios$exposicion == "sombra"), n_expo[["sombra"]], n_expo[["sombra"]]),
  p = c(fmt_p(p_hojas), fmt_p(p_welch), fmt_p(p_perm)), check.names = FALSE)
tabla(comp, align = "lrrr")
Análisis n sol n sombra p
Hojas como datos independientes (incorrecto) 30 20 0,086
Un promedio por árbol (Welch) 3 2 0,727
Un promedio por árbol (permutación exacta) 3 2 0,600

Tratar las hojas como independientes multiplica por 10 el tamaño de muestra y cambia el p de 0,600 (permutación) a 0,086. Esa precisión es falsa: se llama pseudorreplicación. Las hojas de un árbol son 10 mediciones de lo mismo, no 10 árboles.

dif_w <- unname(t_arb$estimate[2] - t_arb$estimate[1]); ic_w <- -rev(t_arb$conf.int)
tl <- t.test(log(area_cm2) ~ exposicion, data = medias)
razon <- unname(exp(tl$estimate[2] - tl$estimate[1])); ic_raz <- exp(-rev(tl$conf.int))
wil <- wilcox.test(area_cm2 ~ exposicion, data = medias)
pruebas <- data.frame(
  Análisis = c("Welch sobre promedios de árbol (cm²)", "Welch sobre log(promedio): razón sombra / sol",
               "Wilcoxon sobre promedios de árbol"),
  Estimación = c(paste(fmt(dif_w), "cm²"), paste0(fmt(razon, 2), " veces"), "-"),
  IC95 = c(paste0("[", fmt(ic_w[1]), "; ", fmt(ic_w[2]), "]"),
           paste0("[", fmt(ic_raz[1], 2), "; ", fmt(ic_raz[2], 2), "]"), "-"),
  p = c(fmt_p(t_arb$p.value), fmt_p(tl$p.value), fmt_p(wil$p.value)))
tabla(pruebas, align = "lrrr",
      caption = paste0("Sombra frente a sol con el árbol como unidad (", n_expo[["sombra"]], " árboles de sombra, ",
                       n_expo[["sol"]], " de sol)."))
Sombra frente a sol con el árbol como unidad (2 árboles de sombra, 3 de sol).
Análisis Estimación IC95 p
Welch sobre promedios de árbol (cm²) 11,7 cm² [-220,2; 243,7] 0,727
Welch sobre log(promedio): razón sombra / sol 1,20 veces [0,01; 225,47] 0,845
Wilcoxon sobre promedios de árbol - - 0,800

Los intervalos son muy anchos porque salen de 2 y 3 árboles por categoría: con tan pocos datos, lo único razonable es describir la dirección y el tamaño aproximado de la diferencia, sin afirmar que existe.

6 Diseño experimental, ANOVA y comparaciones post-ANOVA

El análisis de varianza (ANOVA) permite usar la estructura del diseño. Aquí se responden dos preguntas:

Pregunta Herramienta Después del ANOVA
¿Difieren entre sí los árboles medidos? ANOVA de un factor (árbol) Tukey HSD: ¿cuáles árboles?
¿Hay efecto de la exposición respetando que las hojas están dentro de árboles? ANOVA anidado (árbol dentro de exposición) Componentes de varianza

6.1 El diseño

  • Factor de interés (fijo): exposición, con 2 niveles (sol, sombra).
  • Unidad experimental: el árbol, porque la exposición es una propiedad del árbol.
  • Unidad de muestreo (submuestra): la hoja. Las 10 hojas de un árbol miden al árbol; no son réplicas de la exposición.

Modelo: log(área) ~ exposición + árbol(exposición) + error de hoja. El término árbol(exposición) es el error experimental contra el que se prueba la exposición.

¿Por qué el logaritmo del área? El área es positiva y las hojas grandes varían más que las pequeñas. Con el logaritmo, la variabilidad se parece más entre árboles y los efectos se interpretan como razones (“1,3 veces”).

sd_a <- tapply(limpios$area_cm2, limpios$id_arbol, sd); mu_a <- tapply(limpios$area_cm2, limpios$id_arbol, mean)
sd_l <- tapply(log(limpios$area_cm2), limpios$id_arbol, sd); mu_l <- tapply(log(limpios$area_cm2), limpios$id_arbol, mean)
c(escala_original = cor(mu_a, sd_a), escala_log = cor(mu_l, sd_l))
escala_original      escala_log 
      0,9513261       0,6518606 

La correlación entre la media de cada árbol y su desviación estándar es 0,95 en la escala original y 0,65 con el logaritmo. (Con solo 5 árboles esta comparación es muy aproximada.)

6.2 ANOVA de un factor: ¿difieren los árboles?

Aquí las hojas sí son réplicas (de cada árbol) y la pregunta es por esos árboles concretos, no por la exposición.Es decir,pseudoreplicas.

limpios$arbol <- factor(limpios$id_arbol, levels = resumen$arbol)
m1 <- aov(log(area_cm2) ~ arbol, data = limpios)
a1 <- summary(m1)[[1]]
tabla_a1 <- data.frame(
  Fuente = c("Entre árboles", "Dentro de árboles (hojas)"),
  gl = a1$Df, SC = fmt(a1$`Sum Sq`, 3), CM = fmt(a1$`Mean Sq`, 3),
  F = c(fmt(a1$`F value`[1], 2), ""), p = c(fmt_p(a1$`Pr(>F)`[1]), ""))
tabla(tabla_a1, align = "lrrrrr", caption = "ANOVA de un factor sobre log(área).")
ANOVA de un factor sobre log(área).
Fuente gl SC CM F p
Entre árboles 4 16,575 4,144 228,79 < 0,001
Dentro de árboles (hojas) 45 0,815 0,018

Hay diferencias entre árboles: F(4; 45) = 228,8, p < 0,001. El ANOVA solo dice que al menos un árbol difiere; para saber cuáles hay que hacer comparaciones múltiples.

Supuestos. Los residuos deberían ser aproximadamente normales y tener varianza parecida entre árboles.

tema()
par(mfrow = c(1, 2))
plot(fitted(m1), resid(m1), pch = 16, col = adjustcolor("black", 0.5), las = 1,
     xlab = "Valor ajustado (log área)", ylab = "Residuo", main = "Residuos vs ajustados")
abline(h = 0, lty = 2)
qqnorm(resid(m1), pch = 16, col = adjustcolor("black", 0.5), las = 1, main = "Normalidad de los residuos",
       xlab = "Cuantiles teóricos", ylab = "Cuantiles de los residuos")
qqline(resid(m1))

par(mfrow = c(1, 1))
sh1 <- shapiro.test(resid(m1))$p.value
fl1 <- fligner.test(log(area_cm2) ~ arbol, data = limpios)$p.value
kw1 <- kruskal.test(log(area_cm2) ~ arbol, data = limpios)$p.value
c(shapiro_p = sh1, fligner_p = fl1, kruskal_p = kw1)
   shapiro_p    fligner_p    kruskal_p 
4,119529e-01 1,888595e-01 4,743825e-09 

Shapiro-Wilk (normalidad): p = 0,412. Fligner-Killeen (igualdad de varianzas): p = 0,189. Kruskal-Wallis (alternativa sin supuesto de normalidad): p < 0,001. Un p grande significa que no hay evidencia contra el supuesto; un p pequeño, que sí la hay. Mira siempre el gráfico: los puntos del cuantil-cuantil deberían seguir la recta y los residuos no deberían abrirse como un embudo.

6.3 Post-ANOVA: ¿qué árboles difieren? (Tukey HSD)

Con 5 árboles hay 10 comparaciones por pares. Hacer pruebas t sueltas infla la probabilidad de algún falso positivo; la prueba de Tukey HSD compara todos los pares y controla ese error para el conjunto.

tk <- TukeyHSD(m1)$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 <- tk_tab[order(tk_tab$p_aj), ]

# ¿Cuántos pares que difieren son de árboles con la MISMA exposición?
pares_ids <- strsplit(tk_tab$comparacion, " / ")
expo_de <- setNames(resumen$exposicion, resumen$arbol)
misma_expo <- sapply(pares_ids, function(x) length(unique(expo_de[x])) == 1)
n_sig <- sum(tk_tab$p_aj < 0.05)
n_misma_sig <- sum(misma_expo & tk_tab$p_aj < 0.05)

tabla(data.frame(`Comparación (A / B)` = tk_tab$comparacion, `Razón A / B` = fmt(tk_tab$razon, 2),
                 `IC 95 % (Tukey)` = paste0("[", fmt(tk_tab$li, 2), "; ", fmt(tk_tab$ls, 2), "]"),
                 `p ajustado` = fmt_p(tk_tab$p_aj), `Difieren (5 %)` = ifelse(tk_tab$p_aj < 0.05, "sí", "no"),
                 `Exposición del par` = ifelse(misma_expo, "misma", "distinta"), check.names = FALSE),
      align = "lrrrll", caption = "Comparaciones de Tukey entre árboles (escala logarítmica, mostradas como razones).")
Comparaciones de Tukey entre árboles (escala logarítmica, mostradas como razones).
Comparación (A / B) Razón A / B IC 95 % (Tukey) p ajustado Difieren (5 %) Exposición del par
sm2_na / sm1_gn 0,24 [0,20; 0,28] < 0,001 sí misma
sl3_pa / sm1_gn 0,22 [0,19; 0,26] < 0,001 sí distinta
sl3_pa / sl2_bu 0,35 [0,30; 0,42] < 0,001 sí misma
sm2_na / sl2_bu 0,38 [0,32; 0,45] < 0,001 sí distinta
sl3_pa / sl1_la 0,45 [0,38; 0,53] < 0,001 sí misma
sm2_na / sl1_la 0,49 [0,41; 0,58] < 0,001 sí distinta
sm1_gn / sl1_la 2,04 [1,72; 2,42] < 0,001 sí distinta
sl2_bu / sm1_gn 0,63 [0,53; 0,75] < 0,001 sí distinta
sl2_bu / sl1_la 1,29 [1,08; 1,53] 0,001 sí misma
sl3_pa / sm2_na 0,92 [0,78; 1,10] 0,677 no distinta
tema(c(4.5, 8, 3, 1))
y <- seq_len(nrow(tk_tab))
plot(tk_tab$razon, y, log = "x", xlim = range(c(tk_tab$li, tk_tab$ls)), ylim = c(0.5, nrow(tk_tab) + 0.5),
     yaxt = "n", ylab = "", xlab = "Razón entre árboles (escala logarítmica)", main = "Comparaciones por pares",
     pch = 16, col = ifelse(tk_tab$p_aj < 0.05, "#C75B12", "grey40"), cex = 1.3)
segments(tk_tab$li, y, tk_tab$ls, y, col = ifelse(tk_tab$p_aj < 0.05, "#C75B12", "grey40"), lwd = 2)
axis(2, at = y, labels = tk_tab$comparacion, las = 1, cex.axis = 0.8)
abline(v = 1, lty = 2)

En 9 de las 10 comparaciones los árboles difieren de forma detectable (en naranja). Cruza esto con la exposición de cada árbol: 4 de esas diferencias son entre árboles de la misma exposición. Es decir, dos árboles a la sombra (o dos al sol) pueden diferir tanto como un árbol de sol y uno de sombra. Esa variación entre árboles es el “ruido” contra el que tiene que competir el efecto de la exposición, y la razón por la que no puede probarse con hojas sueltas.

6.4 ANOVA anidado: el efecto de la exposición con el error correcto

Las hojas están dentro de árboles y los árboles dentro de una exposición: un diseño anidado. El efecto de la exposición se prueba contra la variación entre árboles, no contra la variación entre hojas.

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

m_anid <- lm(log(area_cm2) ~ expo + arbol, data = lim)   # el árbol está dentro de expo (cada árbol tiene una sola exposición)
a_an <- anova(m_anid)
gl <- a_an$Df; sc <- a_an$`Sum Sq`; cm <- sc / gl
F_expo  <- cm[1] / cm[2]; p_expo  <- pf(F_expo,  gl[1], gl[2], lower.tail = FALSE)
F_arbol <- cm[2] / cm[3]; p_arbol <- pf(F_arbol, gl[2], gl[3], lower.tail = FALSE)

tab_an <- data.frame(
  Fuente = c("Exposición", "Árbol (dentro de exposición)", "Hojas (residual)"),
  gl = gl, SC = fmt(sc, 3), CM = fmt(cm, 3),
  Contra = c("CM de árbol", "CM de hojas", ""),
  F = c(fmt(F_expo, 2), fmt(F_arbol, 2), ""), p = c(fmt_p(p_expo), fmt_p(p_arbol), ""))
tabla(tab_an, align = "lrrrlrr", caption = "ANOVA anidado sobre log(área): hojas dentro de árboles dentro de exposición.")
ANOVA anidado sobre log(área): hojas dentro de árboles dentro de exposición.
Fuente gl SC CM Contra F p
Exposición 1 0,381 0,381 CM de árbol 0,07 0,808
Árbol (dentro de exposición) 3 16,193 5,398 CM de hojas 298,03 < 0,001
Hojas (residual) 45 0,815 0,018
# Versión ingenua (incorrecta): exposición contra el CM de las hojas
F_ing <- cm[1] / cm[3]; p_ing <- pf(F_ing, gl[1], gl[3], lower.tail = FALSE)
  • Exposición: F(1; 3) = 0,07, p = 0,808. Con el árbol como unidad no hay evidencia suficiente de diferencia entre sol y sombra.
  • Árbol dentro de exposición: p < 0,001. Árboles de la misma exposición difieren entre sí.
  • La versión ingenua (probar la exposición contra el CM de las hojas) daría p < 0,001, una certeza que los datos no respaldan porque solo hay 5 árboles.

Cuánto de la variación se debe al árbol y cuánto a las hojas (componentes de varianza, aproximados):

n_por_arbol <- mean(table(lim$arbol))
var_hoja  <- cm[3]
var_arbol <- max((cm[2] - cm[3]) / n_por_arbol, 0)
pct_arbol <- 100 * var_arbol / (var_arbol + var_hoja)
r2_arbol  <- 100 * a1$`Sum Sq`[1] / sum(a1$`Sum Sq`)
c(var_arbol = var_arbol, var_hoja = var_hoja, pct_arbol = pct_arbol)
  var_arbol    var_hoja   pct_arbol 
 0,53796435  0,01811122 96,74302924 

Alrededor del 97 % de la variación (en log) que queda después de quitar la exposición se debe a diferencias entre árboles, y el árbol por sí solo explica el 95 % de la variación del logaritmo del área. Por eso conviene medir más árboles antes que más hojas por árbol.

6.5 ¿Se puede hacer un ANOVA de bloques por grupo?

En el diseño de referencia, cada grupo mide árboles de sol y de sombra, y el grupo sirve de bloque. En esta base cada grupo tiene un solo árbol (ver “Árboles por grupo” en la revisión), así que el grupo y el árbol son lo mismo y no se pueden separar: el ANOVA de bloques con interacción grupo × exposición no tiene grados de libertad para estimarse. Por eso aquí no se hace. Para poder hacerlo, cada grupo tendría que medir al menos 2 árboles de sol y 2 de sombra.

6.6 Resumen de los análisis

resumen_a <- data.frame(
  Pregunta = c("¿Difieren los árboles?", "¿Qué árboles difieren?", "Sol vs sombra con árbol como unidad (Welch)",
               "Sol vs sombra con árbol como unidad (permutación exacta)", "Sol vs sombra, ANOVA anidado"),
  Herramienta = c("ANOVA de un factor", "Tukey HSD", "Prueba t sobre promedios", "Permutación", "F anidada (árbol como error)"),
  Resultado = c(paste0("F = ", fmt(a1$`F value`[1], 1), ", p ", fmt_p(a1$`Pr(>F)`[1])),
                paste(n_sig, "de", nrow(tk_tab), "pares"),
                paste0("p = ", fmt_p(p_welch)), paste0("p = ", fmt_p(p_perm)),
                paste0("F = ", fmt(F_expo, 2), ", p = ", fmt_p(p_expo))))
tabla(resumen_a, align = "lll")
Pregunta Herramienta Resultado
¿Difieren los árboles? ANOVA de un factor F = 228,8, p < 0,001
¿Qué árboles difieren? Tukey HSD 9 de 10 pares
Sol vs sombra con árbol como unidad (Welch) Prueba t sobre promedios p = 0,727
Sol vs sombra con árbol como unidad (permutación exacta) Permutación p = 0,600
Sol vs sombra, ANOVA anidado F anidada (árbol como error) F = 0,07, p = 0,808

7 Qué muestra el mapa con el área media

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='200'><br>")
  paste0(img, "<b>", s$id_arbol, "</b> (", s$exposicion, ")<br>Área media: ", fmt(s$area_cm2), " cm²")
}, character(1))

if (requireNamespace("leaflet", quietly = TRUE)) {
  library(leaflet)
  leaflet(mapa_res) %>%
    addProviderTiles(providers$Esri.WorldImagery, group = "Satélite") %>%
    addTiles(group = "Calles") %>%
    addCircleMarkers(lng = ~lon, lat = ~lat, radius = ~sqrt(area_cm2) * 1.8, weight = 1.5,
                     color = "white", fillColor = ~col_expo[as.character(exposicion)], fillOpacity = 0.85,
                     label = ~paste0(id_arbol, ": ", round(area_cm2), " cm²"),
                     popup = ~popup, popupOptions = popupOptions(maxWidth = 240)) %>%
    addLayersControl(baseGroups = c("Satélite", "Calles"), options = layersControlOptions(collapsed = FALSE)) %>%
    addLegend("bottomright", colors = unname(col_expo), labels = c("Sol", "Sombra"),
              title = "Exposición (tamaño = área media)", opacity = 1) %>%
    addScaleBar("bottomleft")
} else {
  plot(mapa_res$lon, mapa_res$lat, pch = 21, bg = col_expo[as.character(mapa_res$exposicion)],
       cex = sqrt(mapa_res$area_cm2) / 3, asp = 1 / cos(mean(mapa_res$lat) * pi / 180),
       xlab = "Longitud", ylab = "Latitud", main = "Área foliar media por árbol")
  text(mapa_res$lon, mapa_res$lat, mapa_res$id_arbol, pos = 3, cex = 0.7)
}

El tamaño del círculo es proporcional al área media de cada árbol. Sirve para mirar si hay un patrón espacial (por ejemplo, una zona del campus con hojas grandes) que pudiera confundirse con el efecto de la exposición. Con solo 5 puntos no se puede probar ningún patrón espacial: solo se puede mirar si algo llama la atención.

8 Discusión

Qué encontramos. Con el árbol como unidad de análisis, los árboles de sombra tuvieron hojas con un área media mayor que los de sol: 41,5 cm² frente a 29,8 cm² (media de los promedios por árbol; 2 árboles de sombra y 3 de sol), una diferencia de 11,7 cm². El sentido de la diferencia coincide con la hipótesis inicial.

Qué tan fuerte es la evidencia. La prueba de permutación da p = 0,600 y el ANOVA anidado p = 0,808. No hay evidencia suficiente para afirmar que la exposición cambia el área foliar: con tan pocos árboles, esta prueba tiene muy poca potencia. Además, los rangos se solapan: al menos un árbol de sombra tiene un promedio menor que algún árbol de sol, es decir, la separación entre las dos exposiciones no es limpia.

Lo que domina es la variación entre árboles. El árbol explica cerca del 95 % de la variación del logaritmo del área, y Tukey encontró 9 pares de árboles distintos, 4 de ellos con la misma exposición. Es decir, las diferencias entre árboles son grandes aunque tengan la misma exposición.

Qué más pudo influir (explicaciones alternativas que debes discutir):

  • La especie. Los IDs sugieren que los árboles son de especies distintas (por ejemplo, Bunchosia en sl2_bu y posiblemente Casearia en sm2_na). Si los árboles de sombra son por casualidad de especies de hoja grande (o pequeña), la diferencia sería de composición de especies y no de respuesta a la luz. Con una especie distinta por árbol, exposición y especie están mezcladas y estos datos no pueden separarlas.
  • Fechas distintas. Los árboles se midieron en fechas distintas: sl1_la = 22/10/2026; sl2_bu = 23/10/2026; sl3_pa = 29/10/2026; sm1_gn = 22/10/2026; sm2_na = 29/10/2026. El estado hídrico del árbol (por ejemplo, antes o después de lluvia) puede cambiar el tamaño de la hoja.
  • Edad y tamaño del árbol y la calidad del suelo donde crece.
  • Cómo se eligieron las hojas. Escoger “las más bonitas” o hojas de una sola parte de la copa sesga el área.
  • La clasificación en sol y sombra se hizo a ojo, sin medir la luz. Las fotos de la sección 1.3 permiten revisarla: ¿cada árbol tiene realmente la luz que su ID indica?

Resultados frente a hipótesis.

Es un resultado (lo que los datos muestran) Es una hipótesis (lo que habría que probar)
En estos 5 árboles del campus, los de sombra tuvieron hojas con área media mayor que los de sol. La causa es una respuesta de la hoja a la luz o a la temperatura.
Los árboles difieren mucho entre sí, incluso con la misma exposición. Ocurre en toda la flora del campus, en cualquier época del año.
Con 5 árboles el diseño no permite conclusiones fuertes. Las especies de sol y de sombra responden igual.

9 Limitaciones

  • Muy pocos árboles. Son 5 árboles en total (3 de sol y 2 de sombra). El valor de p más pequeño que puede dar la permutación es 0,100, y las estimaciones son muy inestables.
  • Un árbol por grupo. No se puede separar el efecto del grupo del efecto del árbol, y no se puede hacer el ANOVA de bloques.
  • No hubo asignación al azar. Los árboles ya estaban en el sol o en la sombra: es un estudio observacional. Se puede hablar de asociación, no de causa.
  • La exposición es una categoría gruesa, decidida a ojo. No se midió la luz y la especie no se controló.
  • Hojas elegidas por personas, no al azar.
  • Error de medición. El área depende de la foto, del trazo y del script. Las decisiones de limpieza se documentaron, pero otras personas podrían haber decidido distinto.
  • Fechas distintas de medición entre árboles, y un solo periodo del año.
  • Coordenadas. Una longitud venía con el separador decimal mal puesto y se corrigió con una regla explícita (sección 4.3); conviene confirmarla con el GPS original.
  • Supuestos de las pruebas. Con tan pocos datos, las pruebas de normalidad y de igualdad de varianzas tienen poca potencia.