Cómo usar este archivo. Guarda este .Rmd, datos_area_foliar_Mai.csv y (cuando las tengas) la carpeta fotos/ en la misma carpeta (en RStudio: Session > Set Working Directory > To Source File Location) y pulsa Knit. Las tablas, las gráficas y los números dentro del texto se recalculan solos. Los textos marcados como [COMPLETAR: …] son datos de campo que no están en el Excel (especie, cómo elegiste los árboles, tamaño del marco, etc.): reemplázalos con lo que sí hiciste. Paquetes: knitr y rmarkdown (siempre) y leaflet para el mapa interactivo (install.packages("leaflet"); si no está, el informe dibuja un mapa simple).

1 Contexto del experimento

1.1 Ficha del experimento

if (!file.exists(params$archivo)) {
  stop("No encuentro el archivo '", params$archivo, "' en la carpeta de este .Rmd (", getwd(), "). ",
       "Guarda ahí el CSV con ese nombre exacto, o cambia el nombre en 'params: archivo:' del encabezado.")
}
crudo <- read.csv(params$archivo, stringsAsFactors = FALSE, check.names = FALSE,
                  na.strings = c("", "NA"), encoding = "UTF-8")

# Nombres de columna tal como vienen del Excel (algunos terminan en un espacio "no separable")
nombres_originales <- names(crudo)
n_nombres_nbsp <- sum(grepl("\u00a0", nombres_originales, fixed = TRUE))

nm <- gsub("\u00a0", " ", nombres_originales, fixed = TRUE)
nm <- tolower(trimws(nm))
nm[nm == "latitud - longitud"] <- "lat_lon"
names(crudo) <- nm

esperadas <- c("grupo", "id_arbol", "lat_lon", "n_hoja", "area_cm2", "exposicion", "fecha")
faltan <- setdiff(esperadas, names(crudo))
if (length(faltan) > 0) stop("Faltan columnas en la base: ", paste(faltan, collapse = ", "))

# El Excel no trae la columna "observaciones" que pide la plantilla: se crea vacía
sin_observaciones <- !"observaciones" %in% names(crudo)
if (sin_observaciones) crudo$observaciones <- NA_character_
fechas   <- as.Date(crudo$fecha)
ids      <- unique(trimws(crudo$id_arbol))     # en el orden en que aparecen en el archivo
n_arbol  <- length(ids)

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 universitario, Cali (Valle del Cauca, Colombia)",
    ifelse(min(fechas) == max(fechas), format(min(fechas), "%d/%m/%Y"),
           paste(format(min(fechas), "%d/%m/%Y"), "y", format(max(fechas), "%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_arbol, " árboles (", sum(grepl("_S", ids)), " de sombra y ", sum(grepl("_L", ids)),
           " de sol); ", nrow(crudo), " filas registradas"),
    "Celular (cámara y GPS), papel blanco, lápiz y regla, y un script en R 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 universitario, Cali (Valle del Cauca, Colombia)
Fechas de recolección 20/10/2026 y 21/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 (2 de sombra y 3 de sol); 50 filas registradas
Instrumentos Celular (cámara y GPS), papel blanco, lápiz y regla, y un script en R 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, en Cali, en el valle geográfico del río Cauca, una zona tropical con dos temporadas de lluvia al año. El campus cuenta con extensas zonas verdes cargadas de una gran diversidad de vegetación, es por esto que podemos encontrar árboles en diferentes zonas bajo distintas condiciones de exposición solar. En este orden de ideas, podemos encontrar árboles más recubiertos por edificaciones y otros árboles, y otros en zonas más abiertas y despejadas.

El protocolo, en resumen:

  1. Se eligieron 5 árboles adultos y sanos de hojas simples, 2 a la sombra y 3 a pleno sol, y se registraron sus coordenadas con el GPS del celular. Para la selección de los árboles expuestos al sol se aseguró de que ningún otro árbol o edificación bloqueara la recepción de este recurso, mientras que para los árboles de sombra se aseguró que estos estuvieran lo más tapados y bloqueados del sol posible.
  2. De cada árbol se tomaron 10 hojas adultas, expandidas y sin daño, repartidas por toda la copa. Independientemente de si fueron tomadas de los árboles expuestos o no a sol, se consideró seleccionar hojas de tamaño moderado (no tan grandes).
  3. Cada hoja se trazó sobre papel, se fotografió dentro de un marco de lados conocidos y se midió su área con el script.
  4. Los datos se capturaron en la plantilla (una fila por hoja). Los árboles Mai_S1, Mai_S2 y Mai_L1 se muestrearon el 20/10/2026, y Mai_L2 y Mai_L3 el 21/10/2026.

1.3 Mapa de los sitios

Cada punto es un árbol. Haz clic en un punto para ver su foto (si existe) y sus datos. Con la vista de satélite se puede comprobar si el árbol está realmente a cielo abierto o bajo otras copas, que es justo lo que define su exposición.

# Un registro por árbol. Se usa la coordenada de la primera fila de cada árbol (ver la sección 4,
# donde se explica por qué).
primera <- crudo[!duplicated(trimws(crudo$id_arbol)), ]
sitios <- data.frame(id_arbol = trimws(primera$id_arbol),
                     exposicion = tolower(trimws(primera$exposicion)),
                     separar_coord(primera$lat_lon), stringsAsFactors = FALSE)
sitios$hojas <- as.integer(table(trimws(crudo$id_arbol))[sitios$id_arbol])

# Dos árboles con la misma coordenada se taparían en el mapa: solo para dibujar, el segundo
# se corre unos 4 m al este (los datos no se modifican)
sitios$lon_mapa <- sitios$lon + ifelse(duplicated(paste(sitios$lat, sitios$lon)), 0.00004, 0)

# Foto de cada árbol: busca fotos/ID.jpg (o .jpeg, .png) y avisa si falta alguna
buscar_foto <- function(id) {
  candidatas <- file.path(params$carpeta_fotos, paste0(id, c(".jpg", ".jpeg", ".png")))
  existentes <- candidatas[file.exists(candidatas)]
  if (length(existentes) > 0) existentes[1] else NA_character_
}
sitios$foto <- vapply(sitios$id_arbol, buscar_foto, character(1))
cat("Árboles:", nrow(sitios), "| con foto:", sum(!is.na(sitios$foto)), "\n")
## Árboles: 5 | con foto: 0
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>Hojas registradas: ", s$hojas, "<br>", fmt(s$lat, 6), "; ", fmt(s$lon, 6))
}, character(1))
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_mapa, 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_mapa, 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_mapa, sitios$lat, pch = 21, cex = 2.5, 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_mapa, sitios$lat, sub("^Mai_", "", sitios$id_arbol), pos = 3, cex = 0.8)
  legend("topright", legend = names(col_expo), pt.bg = col_expo, pch = 21, bty = "n")
}

1.4 Fotos de los árboles

Una buena base de datos incluye una foto de cada árbol: sirve para comprobar después la clasificación en sol y sombra, y para que el lector se imagine el sitio. Guarda una foto por árbol en la carpeta fotos/ con el ID como nombre (Mai_S1.jpg, Mai_S2.jpg, Mai_L1.jpg, Mai_L2.jpg, Mai_L3.jpg): el informe las encuentra solo, las muestra aquí y las pone en el mapa. Toma la foto de la copa completa y desde el mismo lugar para todos los árboles. Mientras no estén, aparece “foto pendiente”.

tarjetas <- vapply(seq_len(nrow(sitios)), function(i) {
  s <- sitios[i, ]
  img <- if (is.na(s$foto)) "<div class='sinfoto'>foto pendiente</div>"
         else paste0("<img src='", knitr::image_uri(s$foto), "'>")
  paste0("<figure>", img, "<figcaption><b>", s$id_arbol, "</b> - ", s$exposicion, "</figcaption></figure>")
}, character(1))

cat("\n\n<div class='galeria'>\n", paste(tarjetas, collapse = "\n"), "\n</div>\n\n")
foto pendiente
Mai_S1 - sombra
foto pendiente
Mai_S2 - sombra
foto pendiente
Mai_L1 - sol
foto pendiente
Mai_L2 - sol
foto pendiente
Mai_L3 - sol

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: 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. Esto para poder aprovechar más área de recepción de luz solar en ambientes donde esta escaza y que el árbol pueda llevar a cabo procesos de fotosíntesis y crecimiento. Sin embargo, se tiene en cuenta que,la diferencia debería verse en los promedios por árbol y no depender de un solo árbol.

3 Métodos

Diseño. Se midieron 5 árboles (2 a la sombra y 3 a pleno sol) y 10 hojas por árbol. Un árbol es de sol si recibe luz directa la mayor parte del día y no lo cubren otros árboles ni edificios; en caso contrario, de sombra. Las hojas fueron adultas, completamente expandidas, sin daño evidente y repartidas por toda la copa.

Medición. Cada hoja se trazó sobre papel, se fotografió dentro de un marco rectangular de lados conocidos y un script en R calculó el área en cm². Los datos se anotaron en la plantilla: una fila por hoja, con árbol, coordenadas, número de hoja, área, exposición y fecha.

Análisis (todo en R base, R version 4.5.2 (2025-10-31 ucrt)):

  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: un criterio fijo (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 y se comparan esos promedios (prueba de permutación exacta, t de Welch y Wilcoxon).
  6. Diseño experimental y ANOVA: de un factor entre árboles (con Tukey) y anidado (hojas dentro de árboles dentro de exposición), con el error que corresponde a cada comparación.

4 Datos y revisión de calidad

Antes de calcular nada hay que mirar la base. Cada revisión busca un tipo de error distinto y se hace sobre una copia (datos): la base original (crudo) no se toca.

4.1 Lo que trae la base

head(crudo, 6)       # las primeras filas
##   grupo id_arbol              lat_lon n_hoja area_cm2 exposicion      fecha
## 1   Mai   Mai_S1 3.347778 - 76.532778      1    85,65     Sombra 2026-10-20
## 2   Mai   Mai_S1 3.347778 - 76.532779      2    81,78     Sombra 2026-10-20
## 3   Mai   Mai_S1 3.347778 - 76.532780      3   101,18     Sombra 2026-10-20
## 4   Mai   Mai_S1 3.347778 - 76.532781      4    91,99     Sombra 2026-10-20
## 5   Mai   Mai_S1 3.347778 - 76.532782      5    95,33     Sombra 2026-10-20
## 6   Mai   Mai_S1 3.347778 - 76.532783      6    71,68     Sombra 2026-10-20
##   observaciones
## 1          <NA>
## 2          <NA>
## 3          <NA>
## 4          <NA>
## 5          <NA>
## 6          <NA>
sapply(crudo, class) # el tipo de dato que R asignó a cada columna
##         grupo      id_arbol       lat_lon        n_hoja      area_cm2 
##   "character"   "character"   "character"     "integer"     "numeric" 
##    exposicion         fecha observaciones 
##   "character"   "character"   "character"

La base tiene 50 filas y 5 árboles. Si todo hubiera salido perfecto, serían 5 árboles × 10 hojas = 50, que es justo lo que hay. Eso no significa que esté libre de problemas: veamos cuáles encontré.

Nota sobre el archivo Excel. El Excel original tiene una sola hoja (Hoja1) con 7 columnas con datos. Las celdas de area_cm2 estaban guardadas como texto (por eso a_numero() acepta texto con coma o con punto), y a este informe se le pasa la misma tabla exportada a CSV, sin cambiar ningún valor.

4.2 Revisiones

datos <- crudo
datos$fila <- seq_len(nrow(datos))   # número de fila en el archivo (sin contar el encabezado)

# 1. Nombres de columna: ¿alguno trae el espacio "no separable" que deja el Excel?
data.frame(original = gsub("\u00a0", "<nbsp>", nombres_originales, fixed = TRUE), usado = nm[seq_along(nombres_originales)])
##             original      usado
## 1        grupo<nbsp>      grupo
## 2     id_arbol<nbsp>   id_arbol
## 3 Latitud - Longitud    lat_lon
## 4       n_hoja<nbsp>     n_hoja
## 5     area_cm2<nbsp>   area_cm2
## 6   Exposicion<nbsp> exposicion
## 7              Fecha      fecha

5 de las 7 columnas terminan en un espacio invisible (no separable), y una se llama Latitud - Longitud. R no las reconoce con el nombre que uno escribiría (datos$grupo no funcionaría). Se limpian los espacios y se pasan a minúsculas.

# 2. ¿El ID tiene el formato Mai_S# o Mai_L# ?
id_mal <- unique(datos$id_arbol[!grepl("^Mai_[SL][0-9]+$", datos$id_arbol)])
id_mal
## character(0)
# 3. ¿De cuántas maneras escribieron "sol" y "sombra"?
table(datos$exposicion, useNA = "ifany")
## 
##    Sol Sombra 
##     30     20

Los IDs tienen el formato correcto (Mai_S# para sombra y Mai_L# para sol). La exposición está escrita con mayúscula inicial (Sol, Sombra). Para R, "Sol" y "sol" son categorías distintas, así que se normaliza a minúsculas y se comprueba que la letra del ID (S o L) coincide con la exposición escrita:

n_id_corregido   <- sum(datos$id_arbol != trimws(datos$id_arbol))
n_expo_corregida <- sum(datos$exposicion != tolower(trimws(datos$exposicion)))

datos$id_arbol   <- trimws(datos$id_arbol)
datos$exposicion <- tolower(trimws(datos$exposicion))

table(datos$exposicion)
## 
##    sol sombra 
##     30     20
stopifnot(all(datos$exposicion %in% c("sol", "sombra")))

# La letra del ID (S = sombra, L = luz/sol) debe coincidir con la exposición registrada
expo_por_id <- ifelse(grepl("_S", datos$id_arbol), "sombra", "sol")
table(id = expo_por_id, columna_exposicion = datos$exposicion)
##         columna_exposicion
## id       sol sombra
##   sol     30      0
##   sombra   0     20
stopifnot(all(expo_por_id == datos$exposicion))

# Cada árbol debe tener una sola exposición
n_expo <- tapply(datos$exposicion, datos$id_arbol, function(x) length(unique(x)))
names(n_expo)[n_expo > 1]
## character(0)
# 4. Coordenadas: latitud y longitud vienen juntas en una sola celda de texto
head(unique(datos[, c("id_arbol", "lat_lon")]), 12)
##    id_arbol              lat_lon
## 1    Mai_S1 3.347778 - 76.532778
## 2    Mai_S1 3.347778 - 76.532779
## 3    Mai_S1 3.347778 - 76.532780
## 4    Mai_S1 3.347778 - 76.532781
## 5    Mai_S1 3.347778 - 76.532782
## 6    Mai_S1 3.347778 - 76.532783
## 7    Mai_S1 3.347778 - 76.532784
## 8    Mai_S1 3.347778 - 76.532785
## 9    Mai_S1 3.347778 - 76.532786
## 10   Mai_S1 3.347778 - 76.532787
## 11   Mai_S2     3.3475 - 76.5325
## 12   Mai_S2     3.3475 - 76.5326
co <- separar_coord(datos$lat_lon)
datos$lat_original <- co$lat
datos$lon_original <- co$lon
summary(datos[, c("lat_original", "lon_original")], digits = 7)   # deben caer en la zona del campus
##   lat_original       lon_original      
##  Min.   :3,347222   Min.   :-76,53340  
##  1st Qu.:3,347222   1st Qu.:-76,53306  
##  Median :3,347500   Median :-76,53278  
##  Mean   :3,347444   Mean   :-76,53284  
##  3rd Qu.:3,347500   3rd Qu.:-76,53270  
##  Max.   :3,347778   Max.   :-76,53250
# ¿Cuánto se mueve un mismo árbol de una hoja a otra? (distancia entre su punto más al sur-oeste y más al nor-este, en m)
deriva_m <- sapply(split(datos, datos$id_arbol), function(d)
  distancia_m(min(d$lat_original), min(d$lon_original), max(d$lat_original), max(d$lon_original)))
round(deriva_m, 1)
## Mai_L1 Mai_L2 Mai_L3 Mai_S1 Mai_S2 
##    1,0    0,0    0,0    1,0   99,9
# ¿Las coordenadas cambian con un paso constante de una fila a la siguiente?
paso_constante <- sapply(split(datos, datos$id_arbol), function(d)
  length(unique(round(diff(d$lon_original), 7))) == 1)
paso_constante
## Mai_L1 Mai_L2 Mai_L3 Mai_S1 Mai_S2 
##   TRUE   TRUE   TRUE   TRUE   TRUE

Hay tres problemas con las coordenadas:

  1. Formato. Latitud y longitud están en una sola celda, separadas por un guion (3.347778 - 76.532778), y en un caso con dos espacios y el signo (3.3472222 -76.5327). El guion se parece al signo menos, pero la longitud de Cali es oeste, es decir, negativa. La función separar_coord() saca los dos números y le pone el signo negativo a la longitud.
  2. Una coordenada distinta por hoja, con un paso fijo. En Mai_S1, Mai_S2 y Mai_L1 los últimos dígitos de la longitud suben en la misma cantidad en cada fila, como pasa cuando se arrastra una celda en Excel. Un árbol no se mueve entre una hoja y otra. En Mai_S2 ese paso suma unos 100 m entre la primera y la última hoja, lo que no puede ser real. En Mai_S1 y Mai_L1 suma solo 1,0 m: es igual de artificial, pero sin importancia práctica. (Mai_L2 y Mai_L3 repiten la misma coordenada en todas sus hojas, que es lo esperable.) Se usa la coordenada de la primera fila de cada árbol, que es la que se escribió a mano.
  3. Mai_L2 y Mai_L3 tienen exactamente la misma coordenada. Ver la revisión de árboles repetidos más abajo.
ref <- datos[!duplicated(datos$id_arbol), c("id_arbol", "lat_original", "lon_original")]
datos$lat <- ref$lat_original[match(datos$id_arbol, ref$id_arbol)]
datos$lon <- ref$lon_original[match(datos$id_arbol, ref$id_arbol)]
n_coord_cambiada <- sum(datos$lat != datos$lat_original | datos$lon != datos$lon_original)

if (!is.numeric(datos$area_cm2)) datos$area_cm2 <- a_numero(datos$area_cm2)
n_coord_cambiada
## [1] 27
# 5. Áreas faltantes o no válidas (la plantilla pide área > 0)
datos[is.na(datos$area_cm2) | datos$area_cm2 <= 0,
      c("fila", "id_arbol", "n_hoja", "area_cm2", "observaciones")]
## [1] fila          id_arbol      n_hoja        area_cm2      observaciones
## <0 rows> (o 0- extensión row.names)
# 6. Filas repetidas (mismo árbol y mismo número de hoja) y n_hoja fuera de 1-10
repetida <- duplicated(datos[, c("id_arbol", "n_hoja")])
datos[repetida | duplicated(datos[, c("id_arbol", "n_hoja")], fromLast = TRUE),
      c("fila", "id_arbol", "n_hoja", "area_cm2")]
## [1] fila     id_arbol n_hoja   area_cm2
## <0 rows> (o 0- extensión row.names)
range(datos$n_hoja)
## [1]  1 10
# 7. ¿Cuántas hojas tiene cada árbol (sin contar las repetidas)?
n_por_arbol <- table(datos$id_arbol[!repetida])
n_por_arbol
## 
## Mai_L1 Mai_L2 Mai_L3 Mai_S1 Mai_S2 
##     10     10     10     10     10
# 8. ¿Cuántos árboles hay en cada exposición?
table(unique(datos[, c("id_arbol", "exposicion")])$exposicion)
## 
##    sol sombra 
##      3      2
# 9. ¿Hay valores de área idénticos dentro de un mismo árbol? (pueden ser una coincidencia o una celda copiada)
igual <- datos[duplicated(datos[, c("id_arbol", "area_cm2")]) |
               duplicated(datos[, c("id_arbol", "area_cm2")], fromLast = TRUE),
               c("fila", "id_arbol", "n_hoja", "area_cm2")]
igual
##    fila id_arbol n_hoja area_cm2
## 22   22   Mai_L1      2    70,96
## 24   24   Mai_L1      4    70,96
## 46   46   Mai_L3      6    60,62
## 49   49   Mai_L3      9    60,62

No hay filas repetidas, el número de hoja va de 1 a 10 en todos los árboles y cada árbol tiene 10 hojas. Hay 4 hojas que comparten el valor de otra hoja del mismo árbol (por ejemplo, 70,96 cm² en Mai_L1 y 60,62 cm² en Mai_L3). Con áreas dadas con dos decimales y 10 hojas por árbol puede ser una coincidencia, pero también una celda copiada. Como no hay una razón para descartarlas (son hojas distintas con números de hoja distintos), se conservan y se comprueba la sensibilidad del resultado en la sección de valores atípicos.

# 10. Valores extremos respecto a su propio árbol: área / mediana del árbol
mediana_arbol <- ave(datos$area_cm2, datos$id_arbol,
                     FUN = function(x) median(x[!is.na(x) & x > 0]))
datos$razon_mediana <- datos$area_cm2 / mediana_arbol
range(datos$razon_mediana, na.rm = TRUE)
## [1] 0,7420626 1,6126785
extremos <- datos[!is.na(datos$razon_mediana) & datos$area_cm2 > 0 &
                    (datos$razon_mediana > 10 | datos$razon_mediana < 0.1),
                  c("fila", "id_arbol", "n_hoja", "area_cm2", "razon_mediana")]
extremos
## [1] fila          id_arbol      n_hoja        area_cm2      razon_mediana
## <0 rows> (o 0- extensión row.names)

Ninguna hoja vale 10 veces la mediana de su árbol ni una décima parte: no hay errores de unidad (mm² en lugar de cm²), que son los más graves porque multiplican el área por 100. La hoja más extrema respecto a su árbol vale 1,61 veces la mediana de su árbol.

# 11. Observaciones: lo que los propios estudiantes anotaron
tabla(datos[!is.na(datos$observaciones),
            c("fila", "id_arbol", "n_hoja", "area_cm2", "observaciones")], row.names = FALSE)
fila id_arbol n_hoja area_cm2 observaciones

El Excel no tiene la columna observaciones que pide la plantilla, así que no hay anotaciones sobre hojas dañadas, fotos dudosas o trazos corregidos. Se creó vacía. Esto es una limitación: no hay forma de saber por la base si alguna hoja tuvo un problema de medición. [COMPLETAR: si recuerdas alguna hoja con trazo dudoso, daño o foto borrosa, anótala aquí.]

# 12. ¿Cuándo se midió cada árbol?
table(arbol = datos$id_arbol, fecha = datos$fecha)
##         fecha
## arbol    2026-10-20 2026-10-21
##   Mai_L1         10          0
##   Mai_L2          0         10
##   Mai_L3          0         10
##   Mai_S1         10          0
##   Mai_S2         10          0
# 13. ¿Dos árboles con ID distinto son en realidad el mismo? Se compara la distancia entre árboles
arb <- datos[!duplicated(datos$id_arbol), c("id_arbol", "exposicion", "lat", "lon")]
pares <- t(combn(nrow(arb), 2))
d <- distancia_m(arb$lat[pares[, 1]], arb$lon[pares[, 1]], arb$lat[pares[, 2]], arb$lon[pares[, 2]])
dist_pares <- data.frame(arbol_a = arb$id_arbol[pares[, 1]], arbol_b = arb$id_arbol[pares[, 2]],
                         distancia_m = round(d, 1))
dist_pares[order(dist_pares$distancia_m), ]
##    arbol_a arbol_b distancia_m
## 10  Mai_L2  Mai_L3         0,0
## 6   Mai_S2  Mai_L2        38,0
## 7   Mai_S2  Mai_L3        38,0
## 1   Mai_S1  Mai_S2        43,7
## 2   Mai_S1  Mai_L1        43,7
## 8   Mai_L1  Mai_L2        50,2
## 9   Mai_L1  Mai_L3        50,2
## 5   Mai_S2  Mai_L1        61,7
## 3   Mai_S1  Mai_L2        62,4
## 4   Mai_S1  Mai_L3        62,4
# ¿Las hojas de los dos árboles "gemelos" se parecen?
gemelos <- subset(datos, id_arbol %in% c("Mai_L2", "Mai_L3"))
aggregate(area_cm2 ~ id_arbol, gemelos, function(x) round(c(media = mean(x), mediana = median(x), de = sd(x)), 1))
##   id_arbol area_cm2.media area_cm2.mediana area_cm2.de
## 1   Mai_L2           78,7             71,8        21,3
## 2   Mai_L3           60,6             60,6         3,2

Mai_L2 y Mai_L3 tienen la misma coordenada (distancia de 0 m), la misma exposición y la misma fecha. Podría ser el mismo árbol medido dos veces, pero sus hojas son muy distintas: la media de Mai_L2 es de 78,7 cm² con una DE de 21,3, y la de Mai_L3 es de 60,6 cm² con una DE de 3,2. Y una DE tan baja (CV de 5 %) no se parece a la de ningún otro árbol. Lo más probable es que sean dos árboles vecinos y que el segundo reciba las coordenadas del primero (el GPS del celular no distingue 3 o 4 m). Por eso se mantienen como dos árboles, con la advertencia de que la coordenada de uno de los dos es aproximada.

4.3 Decisiones

Cada problema se resuelve con una regla explícita, y la fila original se conserva en la base (usar y motivo) para poder auditar lo que se hizo.

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

marcar <- function(filas, motivo) {          # excluye filas que no tengan ya un motivo
  filas <- filas & datos$usar
  datos$usar[filas]   <<- FALSE
  datos$motivo[filas] <<- motivo
  sum(filas)
}

danadas <- grepl("mordid|insect|hongo|dañ|roto|rota|herbiv", datos$observaciones, ignore.case = TRUE)

n_repetidas <- marcar(repetida, "fila repetida")
n_sin_area  <- marcar(is.na(datos$area_cm2), "sin área")
n_cero      <- marcar(!is.na(datos$area_cm2) & datos$area_cm2 <= 0, "área no válida (<= 0)")
n_danadas   <- marcar(!is.na(danadas) & danadas, "hoja dañada (el protocolo pide hojas sin daño)")

limpios <- datos[datos$usar, ]
limpios <- limpios[order(match(limpios$id_arbol, ids), limpios$n_hoja), ]
limpios$id_arbol <- factor(limpios$id_arbol, levels = ids)
decisiones <- data.frame(
  Problema = c("Nombres de columna con espacio invisible", "Exposición escrita con mayúscula inicial",
               "Latitud y longitud juntas en una sola celda de texto",
               "Coordenada distinta en cada hoja del mismo árbol (paso fijo, tipo \"arrastrar\" en Excel)",
               "Mismas coordenadas en Mai_L2 y Mai_L3", "Área guardada como texto en el Excel",
               "Fila repetida", "Área faltante o igual a 0", "Hoja dañada",
               "Área en mm² en lugar de cm²", "Valores de área idénticos dentro de un árbol",
               "No hay columna de observaciones", "Árboles con menos de 10 hojas"),
  Filas = c(paste(n_nombres_nbsp, "columnas"), n_expo_corregida, nrow(crudo), n_coord_cambiada,
            sum(datos$id_arbol %in% c("Mai_L2", "Mai_L3")), nrow(crudo),
            n_repetidas, n_sin_area + n_cero, n_danadas, 0, nrow(igual),
            ifelse(sin_observaciones, "todas", 0), sum(n_por_arbol[n_por_arbol != 10])),
  Decisión = c("Quitar los espacios y pasar a minúsculas",
               "Pasar a minúsculas (y comprobar que coincide con la letra S o L del ID)",
               "Separar en lat y lon; la longitud se toma negativa (oeste)",
               "Usar la coordenada de la primera fila del árbol",
               "Conservar como dos árboles; coordenada aproximada (se anota en limitaciones)",
               "Convertir a número",
               "Ninguna fila para eliminar", "Ninguna fila para excluir", "Ninguna fila para excluir (no hay observaciones)",
               "Ningún caso: nada que corregir",
               "Conservar: son hojas distintas; se revisa la sensibilidad",
               "Crear la columna vacía y anotarlo como limitación", "Ninguno: los 5 árboles tienen 10 hojas"))
tabla(decisiones, align = "lrl")
Problema Filas Decisión
Nombres de columna con espacio invisible 5 columnas Quitar los espacios y pasar a minúsculas
Exposición escrita con mayúscula inicial 50 Pasar a minúsculas (y comprobar que coincide con la letra S o L del ID)
Latitud y longitud juntas en una sola celda de texto 50 Separar en lat y lon; la longitud se toma negativa (oeste)
Coordenada distinta en cada hoja del mismo árbol (paso fijo, tipo “arrastrar” en Excel) 27 Usar la coordenada de la primera fila del árbol
Mismas coordenadas en Mai_L2 y Mai_L3 20 Conservar como dos árboles; coordenada aproximada (se anota en limitaciones)
Área guardada como texto en el Excel 50 Convertir a número
Fila repetida 0 Ninguna fila para eliminar
Área faltante o igual a 0 0 Ninguna fila para excluir
Hoja dañada 0 Ninguna fila para excluir (no hay observaciones)
Área en mm² en lugar de cm² 0 Ningún caso: nada que corregir
Valores de área idénticos dentro de un árbol 4 Conservar: son hojas distintas; se revisa la sensibilidad
No hay columna de observaciones todas Crear la columna vacía y anotarlo como limitación
Árboles con menos de 10 hojas 0 Ninguno: los 5 árboles tienen 10 hojas
cat("Filas originales:", nrow(crudo), "\n",
    "Filas que se usan:", nrow(limpios), "\n",
    "Árboles distintos:", length(unique(limpios$id_arbol)), "\n")
## Filas originales: 50 
##  Filas que se usan: 50 
##  Árboles distintos: 5
if (params$guardar_limpios) {
  write.csv(limpios[, c("id_arbol", "lat", "lon", "n_hoja", "area_cm2", "exposicion", "fecha", "observaciones")],
            "datos_limpios_area_foliar.csv", row.names = FALSE, na = "")
}

Idea clave. Ninguna de estas decisiones es “correcta” por sí sola; lo que importa es que sean explícitas y reproducibles. Quien lea este informe debe poder ver qué se hizo con cada fila dudosa. La base revisada (datos_limpios_area_foliar.csv) es parte de lo que se entrega.

5 Resultados

Desde aquí se usa la base revisada: 5 árboles y 50 hojas.

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

5.1 Estadística descriptiva por á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, row.names = FALSE, 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 = 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.
Mai_S1 sombra 10 86,4 88,8 11,9 13,8 65,9 101,2
Mai_S2 sombra 10 62,7 61,8 12,3 19,5 47,0 78,7
Mai_L1 sol 10 61,0 59,1 13,0 21,3 45,2 85,3
Mai_L2 sol 10 78,7 71,8 21,3 27,1 55,1 115,8
Mai_L3 sol 10 60,6 60,6 3,2 5,2 55,5 66,2

Dos cosas para mirar en esta tabla: (1) la diferencia entre árboles (las medias van de 60,6 a 86,4 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 5 % en Mai_L3 y 27 % en Mai_L2). Las medias y las medianas de cada árbol son muy parecidas, así que ningún árbol tiene una hoja que lo arrastre.

5.2 Un diagrama de cajas por árbol

par(mar = c(4.5, 4.8, 3, 1))
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 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")
Área foliar de cada hoja, por árbol. Color = exposición.

Área foliar de cada hoja, por árbol. Color = exposició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. Se ve que Mai_S1 (sombra) y Mai_L2 (sol) tienen hojas más grandes, que Mai_L3 (sol) es muy uniforme y que Mai_S2 (sombra) se parece más a los árboles de sol Mai_L1 y Mai_L3 que a Mai_S1.

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, como en la revisión de calidad.
  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 si no estuviera (análisis de sensibilidad).
fuera <- do.call(rbind, lapply(split(dg, dg$id_arbol), function(d) {
  lim <- boxplot.stats(d$area_cm2)$stats[c(1, 5)]          # bigotes: límites de Tukey
  q   <- quantile(d$area_cm2, c(0.25, 0.75)); ric <- diff(q)
  o   <- d[d$area_cm2 < q[1] - 1.5 * ric | d$area_cm2 > q[2] + 1.5 * ric, ]
  if (nrow(o) == 0) return(NULL)
  data.frame(arbol = as.character(o$id_arbol), n_hoja = o$n_hoja, area_cm2 = o$area_cm2,
             mediana_arbol = median(d$area_cm2), RIC = ric,
             limite_inferior = q[1] - 1.5 * ric, limite_superior = q[2] + 1.5 * ric)
}))

if (is.null(fuera) || nrow(fuera) == 0) {
  cat("Ninguna hoja queda fuera de los límites de Tukey de su árbol.\n")
} else {
  fuera$decision <- "Conservada: sin causa de medición registrada"
  tabla(fuera, row.names = FALSE, digits = c(0, 0, 2, 1, 2, 1, 1, 0),
        col.names = c("Árbol", "Hoja", "Área (cm²)", "Mediana del árbol", "RIC", "Límite inferior", "Límite superior", "Decisión"),
        caption = "Hojas fuera de los límites de Tukey de su árbol, y qué se hizo con cada una.")
}
Hojas fuera de los límites de Tukey de su árbol, y qué se hizo con cada una.
Árbol Hoja Área (cm²) Mediana del árbol RIC Límite inferior Límite superior Decisión
Mai_L2 3 115,75 71,8 18,35 39,5 112,9 Conservada: sin causa de medición registrada
Mai_L2 10 114,33 71,8 18,35 39,5 112,9 Conservada: sin causa de medición registrada
Mai_L3 2 56,83 60,6 0,98 58,3 62,3 Conservada: sin causa de medición registrada
Mai_L3 3 64,73 60,6 0,98 58,3 62,3 Conservada: sin causa de medición registrada
Mai_L3 5 55,51 60,6 0,98 58,3 62,3 Conservada: sin causa de medición registrada
Mai_L3 10 66,24 60,6 0,98 58,3 62,3 Conservada: sin causa de medición registrada
  • Mai_L2: 2 hojas fuera de los límites (115,75; 114,33 cm²). El RIC del árbol es de 18,4 cm² y su CV es de 27 %. Son las hojas más grandes del árbol (hasta 1,61 veces su mediana) y quedan apenas por encima del límite superior.
  • Mai_L3: 4 hojas fuera de los límites (56,83; 64,73; 55,51; 66,24 cm²). El RIC del árbol es de 1,0 cm² y su CV es de 5 %. Como el árbol es muy uniforme, una hoja que se aleja unos pocos cm² del resto ya queda “fuera”, aunque su área es normal frente a los otros árboles: la regla se aplica dentro de cada árbol, y un árbol muy uniforme marca como atípicas hojas que no lo son en términos absolutos.

Ninguna de estas hojas tiene una causa de medición documentada (no hay observaciones ni errores de unidad), de modo que, según el criterio, se conservan todas.

sens <- do.call(rbind, lapply(split(dg, dg$id_arbol), function(d) {
  q <- quantile(d$area_cm2, c(0.25, 0.75)); ric <- diff(q)
  out <- d$area_cm2 < q[1] - 1.5 * ric | d$area_cm2 > q[2] + 1.5 * ric
  data.frame(arbol = as.character(d$id_arbol[1]), exposicion = d$exposicion[1],
             media = mean(d$area_cm2), mediana = median(d$area_cm2),
             media_sin_atipicas = mean(d$area_cm2[!out]))
}))

dif_sens <- sapply(c(media = "media", mediana = "mediana", media_sin_atipicas = "media_sin_atipicas"),
                   function(v) mean(sens[[v]][sens$exposicion == "sombra"]) -
                               mean(sens[[v]][sens$exposicion == "sol"]))

tabla(data.frame(Resumen_por_arbol = c("Media (todas las hojas)", "Mediana",
                                       "Media sin las hojas atípicas"),
                 Diferencia_sombra_menos_sol = unname(dif_sens)),
      digits = 1, col.names = c("Cómo se resume cada árbol", "Sombra − sol (cm²)"),
      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) 7,8
Mediana 11,5
Media sin las hojas atípicas 10,8

La diferencia entre sombra y sol (promedio de los árboles de sombra menos promedio de los árboles de sol) va de 7,8 a 11,5 cm² según cómo se resuma cada árbol. Las hojas atípicas no cambian la conclusión: la diferencia sigue siendo positiva y de tamaño parecido, así que el resultado no depende de ellas.

5.4 Un diagrama de cajas por exposición

simbolo <- c(16, 17, 15, 18, 8)[as.integer(dg$id_arbol)]
par(mar = c(4.5, 4.8, 3, 1))
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")
set.seed(2)   # para que el "jitter" (separación horizontal de los puntos) sea siempre el mismo
points(as.integer(dg$expo) + runif(nrow(dg), -0.15, 0.15), dg$area_cm2,
       pch = simbolo, col = adjustcolor("black", 0.6))
legend("topleft", legend = levels(dg$id_arbol), pch = c(16, 17, 15, 18, 8), bty = "n",
       title = "Árbol", cex = 0.85)
Hojas de sol y de sombra. Cada símbolo es un árbol distinto.

Hojas de sol y de sombra. Cada símbolo es un árbol distinto.

Este gráfico es descriptivo: muestra la forma de las dos nubes de hojas, y se ven muy parecidas y con mucho solape. Fíjate en que los símbolos iguales (hojas del mismo árbol) tienden a quedar juntos. Eso es justo lo que hace que las hojas no sean independientes, y lo que impide usarlas como si fueran 50 datos independientes en una prueba.

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 = dg, FUN = mean)
medias$exposicion <- factor(medias$exposicion, levels = c("sol", "sombra"))
medias <- medias[order(medias$exposicion, medias$id_arbol), ]

n_expo_arb <- table(medias$exposicion)
n_expo_arb
## 
##    sol sombra 
##      3      2
tabla(medias, row.names = FALSE, digits = 1,
      col.names = c("Árbol", "Exposición", "Media del árbol (cm²)"))
Árbol Exposición Media del árbol (cm²)
Mai_L1 sol 61,0
Mai_L2 sol 78,7
Mai_L3 sol 60,6
Mai_S1 sombra 86,4
Mai_S2 sombra 62,7
dif_obs <- mean(medias$area_cm2[medias$exposicion == "sombra"]) -
           mean(medias$area_cm2[medias$exposicion == "sol"])
dif_obs
## [1] 7,764333

Hay 2 árboles de sombra y 3 de sol: 5 datos independientes en total, no 50. La diferencia observada entre los promedios de árbol es de 7,8 cm² (sombra − sol): la media de los árboles de sombra (74,5 cm²) es mayor que la de los de sol (66,8 cm²), en la dirección de la hipótesis.

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

Con tan pocos árboles se puede hacer algo mejor que suponer una distribución: una prueba de permutación exacta. Si la exposición no importara, cualquiera de las formas de repartir las etiquetas “sol” y “sombra” entre los árboles sería igual de probable. Se calcula la diferencia para todas ellas y se mira dónde queda la que observamos.

y         <- medias$area_cm2
es_sombra <- medias$exposicion == "sombra"
n_som     <- sum(es_sombra)

repartos <- combn(length(y), n_som)                         # todas las formas de elegir los árboles de 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)
n_igual_o_mayor <- sum(abs(dif_perm) >= abs(dif_obs) - 1e-9)

par(mar = c(4.5, 4.8, 3, 1))
plot(dif_perm, rep(0, length(dif_perm)), pch = 21, bg = "grey85", cex = 2.2, yaxt = "n",
     ylab = "", xlab = "Diferencia sombra - sol (cm²)",
     main = paste(ncol(repartos), "repartos posibles"))
points(dif_obs, 0, pch = 21, bg = col_expo["sombra"], cex = 2.6)
abline(v = 0, lty = 3)
legend("topright", legend = "reparto observado", pch = 21, pt.bg = col_expo["sombra"],
       bty = "n", pt.cex = 1.6)
Diferencia sombra − sol para cada reparto posible de las etiquetas entre los 5 árboles.

Diferencia sombra − sol para cada reparto posible de las etiquetas entre los 5 árboles.

Hay 10 repartos posibles y 5 de ellos dan una diferencia tan grande o más (en valor absoluto) que la observada, así que p = 0,500. El valor más pequeño que cualquier resultado podría dar con 2 y 3 árboles sería 0,100. Con 5 árboles (2 y 3), la prueba de permutación 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 ~ expo, data = dg)                 # trata las hojas como independientes
p_hojas <- t_hojas$p.value
comparacion <- data.frame(
  Análisis = c("Hojas como datos independientes (incorrecto)", "Un promedio por árbol (permutación exacta)"),
  n_sol = c(sum(dg$expo == "sol"), sum(!es_sombra)),
  n_sombra = c(sum(dg$expo == "sombra"), n_som),
  p = c(fmt_p(p_hojas), fmt_p(p_perm)))
tabla(comparacion, col.names = c("Análisis", "n sol", "n sombra", "p"), align = "lrrr")
Análisis n sol n sombra p
Hojas como datos independientes (incorrecto) 30 20 0,115
Un promedio por árbol (permutación exacta) 3 2 0,500

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

5.5.3 Pruebas sobre los promedios por árbol

tt   <- t.test(area_cm2 ~ exposicion, data = medias)              # Welch: sol - sombra
dif  <- unname(tt$estimate[2] - tt$estimate[1])                    # sombra - sol
ic   <- -rev(tt$conf.int)

tl     <- t.test(log(area_cm2) ~ exposicion, data = medias)        # en escala log: razón de medias geométricas
razon  <- unname(exp(tl$estimate[2] - tl$estimate[1]))
ic_raz <- exp(-rev(tl$conf.int))

wil    <- wilcox.test(area_cm2 ~ exposicion, data = medias, exact = TRUE)
p_min_wil <- 2 / choose(nrow(medias), n_som)

pruebas <- data.frame(
  Análisis = c("Welch sobre promedios de árbol (cm²)",
               "Welch sobre log(promedio de árbol): razón sombra / sol",
               "Wilcoxon exacto sobre promedios de árbol"),
  Estimación = c(paste(fmt(dif), "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(tt$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_arb["sombra"],
                       " árboles de sombra, ", n_expo_arb["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²) 7,8 cm² [-69,9; 85,5] 0,633
Welch sobre log(promedio de árbol): razón sombra / sol 1,11 veces [0,41; 3,03] 0,635
Wilcoxon exacto sobre promedios de árbol - - 0,400

Se muestran tres formas de comparar el mismo conjunto de promedios por árbol. Como el área es positiva y asimétrica, la comparación en razón (segunda fila) es la más natural: ¿cuántas veces más grande es una hoja de sombra?. Los intervalos de confianza son enormes (con 5 árboles la prueba t tiene solo 1,5 grados de libertad, aprox.): el efecto podría ir desde una razón de 0,41 (hojas de sombra mucho más pequeñas) hasta 3,03 (mucho más grandes). La prueba de Wilcoxon compara rangos y no necesita suponer normalidad, pero con 2 y 3 árboles su p más pequeño posible es de 0,200: no puede dar un resultado significativo.

par(mar = c(4.5, 4.8, 3, 1))
plot(NA, xlim = c(0.5, 2.5), ylim = range(c(resumen$media - resumen$de, resumen$media + resumen$de)),
     xaxt = "n", las = 1, xlab = "Exposición", ylab = expression("Área foliar media del árbol (cm"^2*")"),
     main = "Un punto por árbol")
axis(1, at = 1:2, labels = c("sol", "sombra"))
x_base <- ifelse(resumen$exposicion == "sol", 1, 2)
x_arb  <- x_base + ave(seq_len(nrow(resumen)), resumen$exposicion,
                       FUN = function(i) seq(-0.15, 0.15, length.out = length(i)))
segments(x_arb, resumen$media - resumen$de, x_arb, resumen$media + resumen$de, col = "grey40", lwd = 2)
points(x_arb, resumen$media, pch = 21, bg = col_expo[resumen$exposicion], cex = 2.2)
text(x_arb, resumen$media, sub("^Mai_", "", resumen$arbol), pos = 4, offset = 1, cex = 0.8)
Cada punto es un árbol (promedio de sus 10 hojas). Las barritas son ±1 DE entre las hojas del árbol.

Cada punto es un árbol (promedio de sus 10 hojas). Las barritas son ±1 DE entre las hojas del árbol.

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

Hasta aquí comparamos sol y sombra con promedios por árbol y pruebas t, Wilcoxon y de permutación. El análisis de varianza (ANOVA) generaliza esa comparación y permite usar la estructura completa del diseño. En esta sección se responden dos preguntas, cada una con su herramienta:

Pregunta Herramienta Después del ANOVA
¿Difieren entre sí los 5 árboles? 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 experimental

La actividad es un estudio observacional con estructura jerárquica. Cada elemento del diseño tiene un papel:

  • Factor de interés (fijo): exposición, con 2 niveles (sol, sombra).
  • Bloque: en el diseño de la guía, el grupo de estudiantes. Aquí hay un solo grupo (Mai) que midió árboles de las dos exposiciones, así que hay un único bloque: no hay otros grupos con los que comparar y, por eso, no se puede hacer un ANOVA de bloques, ni estimar la interacción grupo × exposición. Todos los efectos se miden dentro del mismo grupo. La fecha de muestreo tampoco funciona como bloque: el 20/10 se midieron árboles de sombra y de sol, pero el 21/10 solo de sol (Mai_L2 y Mai_L3).
  • 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.

Con esos elementos, el modelo es

log(área) ~ exposición + árbol(exposición) + error de hoja

El término árbol(exposición) (árboles dentro de cada exposición) es el error experimental contra el que se prueba la exposición. Si ese término se ignora y se usa el error de las hojas, se cae en la pseudorreplicación que vimos antes. El diseño no está balanceado: hay 2 árboles de sombra y 3 de sol.

tab_diseno <- unique(dg[, c("id_arbol", "exposicion", "fecha")])
tab_diseno$hojas <- as.integer(table(dg$id_arbol)[as.character(tab_diseno$id_arbol)])
tabla(tab_diseno, row.names = FALSE, col.names = c("Árbol (unidad experimental)", "Exposición (factor)", "Fecha", "Hojas (submuestras)"),
      caption = "Diseño: 5 unidades experimentales y 10 submuestras en cada una.")
Diseño: 5 unidades experimentales y 10 submuestras en cada una.
Árbol (unidad experimental) Exposición (factor) Fecha Hojas (submuestras)
Mai_S1 sombra 2026-10-20 10
Mai_S2 sombra 2026-10-20 10
Mai_L1 sol 2026-10-20 10
Mai_L2 sol 2026-10-21 10
Mai_L3 sol 2026-10-21 10

¿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 (por ejemplo, “1,3 veces”). Se puede mirar la relación entre la media de cada árbol y su desviación estándar:

por_arbol <- aggregate(area_cm2 ~ id_arbol, data = dg,
                       FUN = function(x) c(media = mean(x), de = sd(x)))
por_arbol_log <- aggregate(log(area_cm2) ~ id_arbol, data = dg,
                           FUN = function(x) c(media = mean(x), de = sd(x)))
r_orig <- cor(por_arbol$area_cm2[, "media"], por_arbol$area_cm2[, "de"])
r_log  <- cor(por_arbol_log[[2]][, "media"], por_arbol_log[[2]][, "de"])
c(escala_original = r_orig, escala_log = r_log)
## escala_original      escala_log 
##       0,4979462       0,1882791

Entre árboles, la correlación entre la media y la DE es de 0,50 en la escala original y de 0,19 con el logaritmo. Con solo 5 árboles estas correlaciones son muy inestables, así que no son una prueba: la razón principal para usar el logaritmo es que el área es una cantidad positiva y que se quiere comparar en razones.

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

Se compara el log del área de las hojas entre los 5 árboles. Aquí las hojas sí son réplicas (de cada árbol), y la pregunta es por esos árboles concretos, no por la exposición.

m1 <- lm(log(area_cm2) ~ id_arbol, data = dg)
a1 <- tabla_anova(m1)
a1$Fuente <- c("Entre árboles", "Dentro de árboles (hojas)")
tabla(formato_anova(a1), row.names = FALSE, align = "lrrrrr",
      caption = "ANOVA de un factor sobre log(área), hojas de los 5 árboles.")
ANOVA de un factor sobre log(área), hojas de los 5 árboles.
Fuente gl SC CM F p
Entre árboles 4 1,064 0,266 7,75 < 0,001
Dentro de árboles (hojas) 45 1,543 0,034

Hay diferencias entre árboles: F(4; 45) = 7,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.

par(mfrow = c(1, 2), mar = c(4.5, 4.8, 3, 1))
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))
Diagnóstico del ANOVA de un factor: residuos frente a valores ajustados (izquierda) y gráfico cuantil-cuantil (derecha).

Diagnóstico del ANOVA de un factor: residuos frente a valores ajustados (izquierda) y gráfico cuantil-cuantil (derecha).

shap1 <- shapiro.test(resid(m1))
flig1 <- fligner.test(log(area_cm2) ~ id_arbol, data = dg)
kw1   <- kruskal.test(log(area_cm2) ~ id_arbol, data = dg)     # alternativa sin supuesto de normalidad
c(shapiro_p = shap1$p.value, fligner_p = flig1$p.value, kruskal_p = kw1$p.value)
##    shapiro_p    fligner_p    kruskal_p 
## 0,4421656632 0,0095294432 0,0004792903

La prueba de Shapiro-Wilk (normalidad) da p = 0,442 y la de Fligner-Killeen (igualdad de varianzas) p = 0,010. Un p grande significa que no hay evidencia contra el supuesto; un p pequeño, que sí la hay. Los residuos son compatibles con una distribución normal. En cambio, las varianzas no son iguales entre árboles: la dispersión del log del área es mucho menor en Mai_L3 que en los demás (en el gráfico de residuos, los puntos de ese árbol quedan pegados a cero). Esto afecta sobre todo a las comparaciones en las que participa ese árbol, así que abajo se repiten con una alternativa que no supone varianzas iguales (ANOVA de Welch y pares de Welch con ajuste de Holm). Con pocos datos estas pruebas pueden no detectar un desvío, y con muchos detectan desvíos sin importancia; por eso conviene mirar siempre el gráfico: los puntos deberían seguir la recta y la nube de residuos no debería abrirse como un embudo. La prueba no paramétrica de Kruskal-Wallis (p < 0,001) da la misma conclusión que el ANOVA.

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

Con 5 árboles hay 10 comparaciones por pares. Si se hicieran 10 pruebas t sueltas al 5 %, la probabilidad de al menos un falso positivo sería de alrededor de 40 %. La prueba de Tukey HSD compara todos los pares y controla ese error para el conjunto completo.

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)
# Tukey devuelve "B-A" (diff = media de B menos media de A): se muestra como razón B / A
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, " / ")
misma_expo <- sapply(pares_ids, function(x) length(unique(resumen$exposicion[match(x, resumen$arbol)])) == 1)
n_misma_sig <- sum(misma_expo & tk_tab$p_aj < 0.05)

tabla_tk <- 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), ifelse(tk_tab$p_aj < 0.05, "sí", "no"))
tabla(tabla_tk, col.names = c("Comparación (B / A)", "Razón B / A", "IC 95 % (Tukey)", "p ajustado", "Difieren (5 %)"),
      align = "lrrrr", caption = "Comparaciones de Tukey entre árboles (escala logarítmica, mostradas como razones del área de B respecto a la de A).")
Comparaciones de Tukey entre árboles (escala logarítmica, mostradas como razones del área de B respecto a la de A).
Comparación (B / A) Razón B / A IC 95 % (Tukey) p ajustado Difieren (5 %)
Mai_L1 / Mai_S1 0,70 [0,55; 0,88] < 0,001 sí
Mai_L3 / Mai_S1 0,71 [0,56; 0,89] 0,001 sí
Mai_S2 / Mai_S1 0,72 [0,57; 0,91] 0,002 sí
Mai_L2 / Mai_L1 1,28 [1,01; 1,62] 0,037 sí
Mai_L3 / Mai_L2 0,79 [0,63; 1,00] 0,053 no
Mai_L2 / Mai_S2 1,24 [0,98; 1,57] 0,088 no
Mai_L2 / Mai_S1 0,89 [0,71; 1,13] 0,648 no
Mai_L1 / Mai_S2 0,97 [0,77; 1,23] 0,996 no
Mai_L3 / Mai_S2 0,98 [0,78; 1,24] 0,999 no
Mai_L3 / Mai_L1 1,01 [0,80; 1,28] 1,000 no
k <- nrow(tk_tab)
par(mar = c(4.5, 9, 3, 1))
plot(tk_tab$razon, seq_len(k), log = "x", pch = 16, yaxt = "n", ylab = "",
     xlim = range(c(tk_tab$li, tk_tab$ls)), cex = 1.2,
     col = ifelse(tk_tab$p_aj < 0.05, "#D55E00", "grey40"),
     xlab = "Razón entre árboles (escala logarítmica)", main = "Comparaciones por pares")
segments(tk_tab$li, seq_len(k), tk_tab$ls, seq_len(k), lwd = 2,
         col = ifelse(tk_tab$p_aj < 0.05, "#D55E00", "grey40"))
axis(2, at = seq_len(k), labels = tk_tab$comparacion, las = 1, cex.axis = 0.8)
abline(v = 1, lty = 2)
Razón entre árboles con su intervalo de Tukey. Si el intervalo cruza 1, no hay diferencia detectable.

Razón entre árboles con su intervalo de Tukey. Si el intervalo cruza 1, no hay diferencia detectable.

En 4 de las 10 comparaciones los árboles difieren de forma detectable (en naranja). Cruza esto con la exposición de cada árbol (tabla de la sección 5): 2 de esas diferencias son entre árboles de la misma exposición. Es decir, dos árboles de sol, o dos de sombra, 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.

# Comprobación sin suponer varianzas iguales
ow <- oneway.test(log(area_cm2) ~ id_arbol, data = dg, var.equal = FALSE)
pw <- pairwise.t.test(log(dg$area_cm2), dg$id_arbol, pool.sd = FALSE, p.adjust.method = "holm")$p.value
pw_largo <- data.frame(par = outer(rownames(pw), colnames(pw), paste, sep = " - ")[!is.na(pw)], p_holm = pw[!is.na(pw)])
c(welch_anova_p = ow$p.value)
## welch_anova_p 
##  0,0000200676
pw_largo[order(pw_largo$p_holm), ]
##                par       p_holm
## 4  Mai_L3 - Mai_S1 0,0001499147
## 2  Mai_L1 - Mai_S1 0,0035294705
## 1  Mai_S2 - Mai_S1 0,0047137476
## 10 Mai_L3 - Mai_L2 0,1271846638
## 8  Mai_L2 - Mai_L1 0,1849313115
## 6  Mai_L2 - Mai_S2 0,2529676435
## 3  Mai_L2 - Mai_S1 0,9546299717
## 5  Mai_L1 - Mai_S2 1,0000000000
## 7  Mai_L3 - Mai_S2 1,0000000000
## 9  Mai_L3 - Mai_L1 1,0000000000

El ANOVA de Welch (que no supone varianzas iguales) da p < 0,001; con pares de Welch y ajuste de Holm, 3 pares quedan por debajo del 5 % (con Tukey eran 4). La conclusión general (los árboles no son iguales entre sí) se mantiene, pero los pares concretos que difieren dependen del método cuando un árbol tiene mucha menos variación que los demás; conviene presentar los resultados de Tukey como orientativos.

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

Ahora con las hojas 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 <- dg
lim$arbol <- factor(lim$id_arbol)

m_anid <- lm(log(area_cm2) ~ expo + arbol, data = lim)       # arbol esta dentro de expo (cada arbol tiene una sola exposicion)
a_an   <- tabla_anova(m_anid)

# Cada fuente se prueba contra el cuadrado medio que corresponde a su unidad
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)

tab_an <- 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),
  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, row.names = FALSE, 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,142 0,142 CM de árbol 0,46 0,545
Árbol (dentro de exposición) 3 0,922 0,307 CM de hojas 8,96 < 0,001
Hojas (residual) 45 1,543 0,034
p_ingenuo <- a_an$p[1]     # prueba "ingenua": exposición contra el CM de las hojas (la que da lm() por defecto)
  • Exposición: F(1; 3) = 0,46, p = 0,545: con el árbol como unidad, no hay evidencia de diferencia entre sol y sombra. Con solo 3 grados de libertad para el error, esta prueba tiene muy poca potencia.
  • Árbol dentro de exposición: F(3; 45) = 8,96, p < 0,001. Árboles de la misma exposición difieren mucho entre sí; ese es el ruido contra el que debe competir el efecto de la exposición.
  • La versión ingenua (probar la exposición contra el CM de las hojas, que es lo que anova() hace por defecto) daría p = 0,048, un resultado que parece significativo y no lo es: es pseudorreplicación. Si no se hubiera usado el error correcto, la conclusión habría sido la contraria.

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

n0     <- nrow(lim) / nlevels(lim$arbol)                    # hojas por árbol (10)
s2_res <- a_an$CM[3]
s2_arb <- max((a_an$CM[2] - a_an$CM[3]) / n0, 0)
pct_arb <- 100 * s2_arb / (s2_arb + s2_res)
r2_arb <- summary(m1)$r.squared
c(var_arbol = s2_arb, var_hoja = s2_res, pct_arbol = pct_arb, r2_arbol = r2_arb)
##   var_arbol    var_hoja   pct_arbol    r2_arbol 
##  0,02728961  0,03429018 44,31585964  0,40804157

Alrededor del 44 % de la variación (en log) que queda después de quitar la exposición se debe a diferencias entre árboles, y el resto a diferencias entre hojas de un mismo árbol. En total, el árbol explica el 41 % de la variación del log del área. Por eso conviene medir más árboles antes que más hojas por árbol.

6.5 Post-ANOVA para la exposición; supuestos con la unidad correcta

La exposición tiene solo 2 niveles. Cuando un factor tiene dos niveles no hay nada que “comparar por pares” después del ANOVA: la prueba F ya es la comparación. Se comprueba con un ANOVA de un factor sobre el log del promedio de cada árbol, donde Tukey da el mismo valor que la F:

medias$log_area <- log(medias$area_cm2)
m_b  <- lm(log_area ~ exposicion, data = medias)
a_b  <- tabla_anova(m_b)
a_b$Fuente <- c("Exposición", "Error (árboles)")
tabla(formato_anova(a_b), row.names = FALSE, align = "lrrrrr",
      caption = "ANOVA de un factor sobre log(promedio por árbol): un dato por árbol.")
ANOVA de un factor sobre log(promedio por árbol): un dato por árbol.
Fuente gl SC CM F p
Exposición 1 0,013 0,013 0,41 0,566
Error (árboles) 3 0,096 0,032
tk_e <- TukeyHSD(aov(log_area ~ exposicion, data = medias))$exposicion
c(p_Tukey = tk_e[, "p adj"], p_F = a_b$p[1])
##   p_Tukey       p_F 
## 0,5661309 0,5661309
ef   <- coef(m_b)[["exposicionsombra"]]
ic_e <- confint(m_b)["exposicionsombra", ]

Los dos p son idénticos (p = 0,566). Los pares de Tukey sirven cuando el factor tiene 3 o más niveles, como en la comparación entre árboles. Razón sombra / sol: 1,11 (IC 95 %: 0,66 a 1,87). El intervalo incluye el 1 (sin diferencia). Como solo hay un grupo, no hay ANOVA de bloques ni efectos simples por grupo (ver el diseño).

Supuestos con un dato por árbol.

par(mfrow = c(1, 2), mar = c(4.5, 4.8, 3, 1))
plot(fitted(m_b), resid(m_b), pch = 21, bg = col_expo[as.character(medias$exposicion)], las = 1, cex = 1.6,
     xlab = "Valor ajustado (log del área media)", ylab = "Residuo", main = "Residuos vs ajustados")
abline(h = 0, lty = 2)
qqnorm(resid(m_b), pch = 21, bg = "grey75", las = 1, cex = 1.6, main = "Normalidad de los residuos",
       xlab = "Cuantiles teóricos", ylab = "Cuantiles de los residuos")
qqline(resid(m_b))
Diagnóstico del modelo con un dato por árbol: residuos frente a ajustados (izquierda) y gráfico cuantil-cuantil (derecha). Cada punto es un árbol.

Diagnóstico del modelo con un dato por árbol: residuos frente a ajustados (izquierda) y gráfico cuantil-cuantil (derecha). Cada punto es un árbol.

shap_b <- shapiro.test(resid(m_b))
shap_b$p.value
## [1] 0,1275698

Con 5 puntos estos gráficos y la prueba de Shapiro-Wilk (p = 0,128) casi no pueden detectar nada: un p grande aquí no demuestra que los supuestos se cumplan, solo que no hay evidencia en contra con tan pocos datos. Por eso la conclusión se apoya también en la prueba de permutación exacta, que no supone normalidad.

6.6 Resumen de los análisis

resumen_a <- data.frame(
  Pregunta = c("¿Difieren los 5 árboles?", "¿Qué árboles difieren?",
               "Sol vs sombra, permutación exacta (árbol como unidad)",
               "Sol vs sombra, t de Welch (árbol como unidad)",
               "Sol vs sombra, ANOVA anidado (árbol como error)",
               "Sol vs sombra, ANOVA sobre promedios de árbol",
               "Sol vs sombra ignorando el árbol (incorrecto)"),
  Herramienta = c("ANOVA de un factor", "Tukey HSD", "Permutación", "Prueba t sobre promedios",
                  "F anidada", "ANOVA de un factor, un dato por árbol", "ANOVA por defecto / t sobre hojas"),
  Resultado = c(paste0("F = ", fmt(a1$F[1], 1), ", p ", fmt_p(a1$p[1])),
                paste(sum(tk_tab$p_aj < 0.05), "de", nrow(tk_tab), "pares"),
                paste0("p = ", fmt_p(p_perm)),
                paste0("p = ", fmt_p(tt$p.value)),
                paste0("F = ", fmt(F_expo, 2), ", p = ", fmt_p(p_expo)),
                paste0("razón ", fmt(exp(ef), 2), ", p = ", fmt_p(a_b$p[1])),
                paste0("p = ", fmt_p(p_ingenuo), " (anova) y ", fmt_p(p_hojas), " (t)")))
tabla(resumen_a, align = "lll")
Pregunta Herramienta Resultado
¿Difieren los 5 árboles? ANOVA de un factor F = 7,8, p < 0,001
¿Qué árboles difieren? Tukey HSD 4 de 10 pares
Sol vs sombra, permutación exacta (árbol como unidad) Permutación p = 0,500
Sol vs sombra, t de Welch (árbol como unidad) Prueba t sobre promedios p = 0,633
Sol vs sombra, ANOVA anidado (árbol como error) F anidada F = 0,46, p = 0,545
Sol vs sombra, ANOVA sobre promedios de árbol ANOVA de un factor, un dato por árbol razón 1,11, p = 0,566
Sol vs sombra ignorando el árbol (incorrecto) ANOVA por defecto / t sobre hojas p = 0,048 (anova) y 0,115 (t)

Los caminos que usan el árbol como unidad dan resultados parecidos (p entre 0,500 y 0,633): ninguno encuentra una diferencia entre sol y sombra, sea cual sea la prueba elegida. Lo que cambia la conclusión es ignorar el árbol, que da p más pequeños sin que haya más información.

7 Discusión

Qué encontramos. Con el árbol como unidad de análisis, los árboles de sombra tuvieron hojas algo más grandes que los de sol en promedio: 74,5 cm² frente a 66,8 cm² (media de los promedios por árbol; 2 árboles de sombra y 3 de sol). La diferencia es de 7,8 cm² (IC 95 %: -69,9 a 85,5 cm²) o, dicho en razón, una hoja de sombra equivale a 1,11 veces una de sol (IC 95 %: 0,41 a 3,03). Las pruebas dan p entre 0,400 y 0,635. La dirección coincide con la hipótesis, pero la diferencia no se puede distinguir del azar: los datos no permiten ni confirmarla ni descartarla.

Qué tan fuerte es la evidencia. Es débil. Los intervalos son muy anchos (la razón podría estar entre 0,41 y 3,03), y con 2 y 3 árboles el diseño tiene tan poca potencia que ni siquiera el p mínimo posible (0,100 en la permutación) llegaría al 5 %. Que el resultado no sea significativo no significa que no haya efecto: significa que con 5 árboles no se podría haber detectado uno de tamaño moderado.

Lo que añade el ANOVA. El ANOVA entre árboles muestra que los árboles sí son distintos entre sí (p < 0,001) y que esa variación es grande: el árbol explica el 41 % de la variación del log del área. En el ANOVA anidado la exposición, probada contra el árbol, da p = 0,545; la prueba ingenua con las hojas como réplicas habría dado p = 0,048. Esa diferencia entre las dos pruebas es la lección central del análisis: con el error equivocado se habría “confirmado” la hipótesis; con el correcto, no. Además, 2 de las diferencias entre árboles que detecta Tukey son entre árboles de la misma exposición: por ejemplo, el árbol de sombra Mai_S2 se parece más a los árboles de sol Mai_L1 y Mai_L3 que al otro árbol de sombra, Mai_S1.

Posibles explicaciones ambientales (son hipótesis para explicar el patrón, no algo que estos datos demuestren):

  • Luz. A la sombra, una lámina mayor intercepta más luz; las hojas de sol, más pequeñas y gruesas, ya tienen luz de sobra.
  • Temperatura. Una hoja pequeña tiene una capa de aire más delgada en su borde y se enfría con más facilidad: es una ventaja a pleno sol, donde la hoja recibe mucha radiación.
  • Agua. A pleno sol la demanda de evaporación es mayor; hojas más pequeñas pierden menos agua. En el campus, el suelo pavimentado o compactado alrededor de un árbol también puede limitar el agua disponible.

Qué más pudo influir (explicaciones alternativas que debes discutir antes de atribuirlo todo a la luz):

  • El árbol, y no solo su exposición. Los árboles difieren mucho entre sí. Si Mai_S1 y Mai_L2 (los de hojas más grandes) pertenecen a especies de hoja grande, la diferencia entre árboles sería de composición de especies y no de respuesta a la luz. En la plantilla no se registró la especie.
  • Un árbol muy uniforme. Mai_L3 tiene un CV de solo 5 %, muy por debajo de los demás. Puede ser un árbol de hoja de tamaño muy regular o un efecto de cómo se escogieron sus hojas (por ejemplo, todas de la misma rama). Comprobarlo en campo ayudaría.
  • 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 la parte baja de la copa, sesga el área.
  • La clasificación en sol y sombra se hizo a ojo, y no se midió la luz.
  • La fecha de muestreo. Mai_L2 y Mai_L3 se midieron un día después que los otros tres, y la turgencia de la hoja cambia con las condiciones del día.

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 un área media algo mayor que los de sol, pero la diferencia no se distingue del azar. La causa es una respuesta de la hoja a la luz o a la temperatura.
Los 5 árboles difieren entre sí, también entre árboles de la misma exposición. Los árboles de sombra tienen hojas más grandes que los de sol en el campus en general.
Probar la exposición con las hojas como réplicas da un p mucho menor que con el árbol como unidad. Las especies de sol y de sombra responden igual, en cualquier época del año.

8 Limitaciones

  • Solo 5 árboles (2 de sombra y 3 de sol). La prueba de la exposición tiene 3 grados de libertad para el error y los intervalos son muy anchos. Con este diseño, la prueba de permutación exacta no puede dar un p menor que 0,100, y la de Wilcoxon exacta, uno menor que 0,200.
  • Un solo grupo, un solo bloque. No se puede separar el efecto de la exposición del efecto del sitio o del grupo ni estudiar si el efecto cambia entre grupos (interacción).
  • 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 que la sombra cause hojas más grandes.
  • La exposición es una categoría gruesa, decidida a ojo. No se midió la luz (por ejemplo, con una aplicación o un fotómetro) ni se registró la especie.
  • Hojas elegidas por personas. Aunque el protocolo pide repartirlas por toda la copa, no se eligieron al azar.
  • Error de medición y datos sin anotaciones. El área depende de la foto, del trazo y del script; el Excel no trae la columna de observaciones, así que no hay registro de problemas de medición. Las decisiones de limpieza se documentaron, pero otras personas podrían haber decidido distinto.
  • Coordenadas dudosas. Las coordenadas de varias hojas del mismo árbol cambiaban con un paso fijo (se usó la de la primera fila), y Mai_L2 y Mai_L3 comparten la misma coordenada: la ubicación de uno de los dos en el mapa es aproximada.
  • Un solo momento y dos días de muestreo. Las hojas se midieron el 20 y el 21 de octubre de 2026; el área puede cambiar con la época del año y con el estado hídrico del árbol, y los árboles Mai_L2 y Mai_L3 se midieron un día después que los otros.
  • Supuestos de las pruebas. Con 3 y 2 árboles, los supuestos de normalidad y varianzas iguales casi no se pueden comprobar; las varianzas entre árboles tampoco son iguales (Fligner-Killeen, p 0,010), por lo que las comparaciones de Tukey son orientativas. La permutación exacta no supone normalidad.
  • Comparaciones múltiples. Las comparaciones entre los 5 árboles (10 pares) se ajustaron con Tukey y, como comprobación, con Holm.

9 Información de la sesión

R.version.string
## [1] "R version 4.5.2 (2025-10-31 ucrt)"
Sys.Date()
## [1] "2026-11-05"