Resumen. Medimos el área de 60 hojas de 6 árboles del campus de la Universidad del Valle, 3 creciendo a pleno sol y 3 bajo el dosel de otros árboles o la sombra de edificios, para evaluar si las hojas de sombra son más grandes. Con el árbol como unidad de análisis, los árboles de sombra promediaron 48,0 cm² y los de sol 31,0 cm² (permutación exacta, p = 0,400; ANOVA anidado, p = 0,295). No encontramos una diferencia detectable entre sol y sombra. Los árboles difirieron entre sí (ANOVA, p < 0,001): cerca del 85 % de la variación del área se debe a diferencias entre árboles, con G1_T2 y G1_T5 en los extremos. Con solo 3 árboles en la condición menos representada, el diseño no podía detectar un efecto al 5 %, sin importar su tamaño. Para responder la pregunta haría falta comparar más árboles de una misma especie.
El tamaño de una hoja resulta de un compromiso. Una lámina grande capta más luz, pero también pierde más agua por transpiración y, bajo sol directo, se calienta más, porque la capa de aire quieto que la rodea es más gruesa y disipa peor el calor (Givnish, 1988; Wright et al., 2017). Por eso, dentro de una misma especie, las hojas que se desarrollan con mucha luz suelen ser más pequeñas y gruesas, y las que crecen con poca luz más grandes y delgadas, lo que maximiza la luz que interceptan (Valladares y Niinemets, 2008).
El campus universitario ofrece un contraste natural para poner a prueba esta idea: hay árboles aislados en zonas de pasto, expuestos al sol todo el día, y árboles que crecen dentro de bosquetes, bajo la copa de otros.
Pregunta. ¿Cambia el área de las hojas según la exposición a la luz del lugar donde crece el árbol?
Hipótesis. Los árboles que crecen a la sombra tienen hojas de mayor área promedio que los que crecen a pleno sol.
Predicción. Si la hipótesis es cierta, el promedio de área foliar de los árboles de sombra será mayor que el de los árboles de sol, y la diferencia se mantendrá al usar el árbol, y no la hoja, como unidad de comparación.
# Todo se lee como texto: así Excel no "adivina" tipos (una de las áreas quedó guardada como fecha)
crudo <- as.data.frame(read_excel(params$archivo, col_types = "text"))
esperadas <- c("grupo", "id_arbol", "lat", "lon", "n_hoja",
"area_cm2", "exposicion", "fecha", "observaciones")
names(crudo) # nombres tal como vienen en el Excel## [1] "grupo 1" "id_arbol" "lat" "lon"
## [5] "n_hoja" "area_cm2" "exposicion" "fecha"
## [9] "observaciones"
names(crudo)[1:9] <- esperadas # la 1.ª columna dice "grupo 1": se renombra por posición
crudo <- crudo[!is.na(crudo$id_arbol), esperadas]fechas <- desde_excel(crudo$fecha)
n_arbol <- length(unique(toupper(trimws(crudo$id_arbol))))
ficha <- data.frame(
Aspecto = c("Lugar", "Fechas de muestreo", "Tipo de estudio", "Factor", "Variable respuesta",
"Unidad experimental", "Unidad de muestreo", "Tamaño de muestra", "Medición"),
Detalle = c("Campus Meléndez, Universidad del Valle, Cali (≈ 1 000 m s.n.m.)",
paste(format(sort(unique(fechas)), "%d/%m/%Y"), collapse = " y "),
"Observacional: los árboles ya estaban al sol o a la sombra",
"Exposición del árbol, con dos niveles: sol y sombra",
"Área de la lámina foliar (cm²)",
"Árbol",
"Hoja (10 por árbol)",
paste0(n_arbol, " árboles, ", nrow(crudo), " hojas"),
"Contorno trazado en papel, foto dentro de un marco de lados conocidos y cálculo del área en Google Colab"))
tabla(ficha, caption = "Ficha del estudio.", align = "ll")| Aspecto | Detalle |
|---|---|
| Lugar | Campus Meléndez, Universidad del Valle, Cali (≈ 1 000 m s.n.m.) |
| Fechas de muestreo | 22/10/2026 y 26/10/2026 |
| Tipo de estudio | Observacional: los árboles ya estaban al sol o a la sombra |
| Factor | Exposición del árbol, con dos niveles: sol y sombra |
| Variable respuesta | Área de la lámina foliar (cm²) |
| Unidad experimental | Árbol |
| Unidad de muestreo | Hoja (10 por árbol) |
| Tamaño de muestra | 6 árboles, 60 hojas |
| Medición | Contorno trazado en papel, foto dentro de un marco de lados conocidos y cálculo del área en Google Colab |
El campus está en el valle geográfico del río Cauca, en una zona de clima cálido (temperatura media cercana a 24 °C) con dos temporadas de lluvias al año. Los árboles muestreados se encuentran en el sector arborizado del campus, donde alternan potreros abiertos, bosquetes con dosel continuo y zonas pavimentadas. De cada árbol se anotaron las coordenadas y se tomó una foto de él y de su ambiente. Se utilizaron hojas de papel, regla y lápiz para trazar el contorno de las hojas recolectadas, que se guardaron en sobres; el área de cada hoja se calculó después con un script de Google Colab.
sitios <- unique(data.frame(id_arbol = toupper(trimws(crudo$id_arbol)),
lat = a_numero(crudo$lat),
lon = -abs(a_numero(crudo$lon)), # Cali está al oeste: longitud negativa (sección 3)
exposicion = tolower(trimws(crudo$exposicion)),
stringsAsFactors = FALSE))
buscar_foto <- function(id) {
candidatas <- file.path(params$carpeta_fotos, paste0(id, c(".jpeg", ".jpg", ".png", ".JPG", ".JPEG")))
existentes <- candidatas[file.exists(candidatas)]
if (length(existentes) > 0) existentes[1] else NA_character_
}
sitios$foto <- vapply(sitios$id_arbol, buscar_foto, character(1))
sitios$popup <- vapply(seq_len(nrow(sitios)), function(i) {
s <- sitios[i, ]
img <- if (is.na(s$foto)) "" else paste0("<img src='", knitr::image_uri(s$foto), "' width='180'><br>")
paste0(img, "<b>", s$id_arbol, "</b> (", s$exposicion, ")<br>", fmt(s$lat, 5), "; ", fmt(s$lon, 5))
}, character(1))
tabla(sitios[, c("id_arbol", "exposicion", "lat", "lon")], digits = 6,
col.names = c("Árbol", "Exposición", "Latitud", "Longitud"),
caption = "Coordenadas de los árboles (grados decimales, WGS84).")| Árbol | Exposición | Latitud | Longitud |
|---|---|---|---|
| G1_T1 | sombra | 3,375537 | -76,53550 |
| G1_T2 | sol | 3,375512 | -76,53571 |
| G1_T3 | sombra | 3,377788 | -76,53324 |
| G1_T4 | sol | 3,375408 | -76,53562 |
| G1_T5 | sombra | 3,375517 | -76,53560 |
| G1_T6 | sol | 3,375541 | -76,53542 |
if (requireNamespace("leaflet", quietly = TRUE)) {
leaflet::leaflet(sitios) |>
leaflet::addProviderTiles(leaflet::providers$Esri.WorldImagery, group = "Satélite") |>
leaflet::addTiles(group = "Calles") |>
leaflet::addCircleMarkers(lng = ~lon, lat = ~lat, radius = 10, weight = 2, color = "white",
fillColor = ~unname(col_expo[exposicion]), fillOpacity = 0.95,
label = ~id_arbol, popup = ~popup) |>
leaflet::addLayersControl(baseGroups = c("Satélite", "Calles"),
options = leaflet::layersControlOptions(collapsed = FALSE)) |>
leaflet::addLegend("bottomright", colors = unname(col_expo), labels = c("Sol", "Sombra"),
title = "Exposición", opacity = 1) |>
leaflet::addScaleBar("bottomleft")
} else {
ggplot(sitios, aes(lon, lat, fill = exposicion)) +
geom_point(shape = 21, size = 6, color = "white") +
geom_text(aes(label = id_arbol), vjust = -1.4, size = 3.5) +
scale_fill_manual(values = col_expo, name = "Exposición") +
coord_quickmap() +
labs(title = "Ubicación de los árboles", x = "Longitud", y = "Latitud",
caption = "Para el mapa interactivo: install.packages(\"leaflet\")")
}Al hacer clic en cada punto se ve la foto del árbol. La distancia entre árboles va de 9 m (el par más cercano) a 374 m (el par más alejado).
# Galería de fotos: busca G1_T#.jpeg / .jpg / .png en la carpeta indicada en params$carpeta_fotos
galeria <- function(rutas, pies, clase = "") {
tarjetas <- vapply(seq_along(rutas), function(i) {
img <- if (is.na(rutas[i]) || !file.exists(rutas[i])) "<div class='sinfoto'>sin imagen</div>"
else paste0("<img class='", clase, "' src='", knitr::image_uri(rutas[i]), "'>")
paste0("<figure>", img, "<figcaption>", pies[i], "</figcaption></figure>")
}, character(1))
cat("\n\n<div class='galeria'>\n", paste(tarjetas, collapse = "\n"), "\n</div>\n\n")
}
pies <- paste0("<b>", sitios$id_arbol, "</b> · ", sitios$exposicion)Fotografías de campo de los 6 árboles, tomadas el día del muestreo:
Los árboles clasificados como de sol son G1_T2, G1_T4, G1_T6 y los de sombra son G1_T1, G1_T3, G1_T5. Las fotografías permiten contrastar esa clasificación y ver que los árboles no tienen la misma forma ni, probablemente, la misma edad:
G1_T1 y G1_T3 crecen dentro de un
bosquete, bajo un dosel continuo que filtra la luz y deja el suelo
cubierto de hojarasca.G1_T2 es bajo y arbustivo, ramificado desde la base, y
crece entre pasto alto.G1_T4 parece joven, de tronco delgado, con brotes
rojizos y el cielo despejado sobre la copa.G1_T5 tiene una copa densa de hojas grandes y oscuras,
en una zona de pasto bajo la sombra de otros árboles.G1_T6 es de tronco delgado y crece en el borde de un
parqueadero, con cielo despejado y suelo pavimentado cerca.G1_T# y se registraron
sus coordenadas con el GPS del celular.Todo el análisis se hizo en R version 4.3.3 (2024-02-29 ucrt), con
los paquetes readxl, ggplot2,
kableExtra y leaflet. Se siguieron estos
pasos:
Antes de analizar, se revisó la base en busca de errores de
digitación y de formato. Todas las correcciones se hacen sobre una copia
(datos); el archivo original no se modifica.
datos <- crudo
datos$fila <- seq_len(nrow(datos)) + 1 # fila en el Excel (la 1 es el encabezado)
datos$id_arbol <- toupper(trimws(datos$id_arbol))
datos$exposicion <- tolower(trimws(datos$exposicion))
# Formato del ID, valores de exposición y una sola exposición por árbol
c(id_correcto = all(grepl("^G[0-9]+_T[0-9]+$", datos$id_arbol)),
expo_valida = all(datos$exposicion %in% c("sol", "sombra")),
una_expo_x_arbol = all(tapply(datos$exposicion, datos$id_arbol, function(x) length(unique(x))) == 1))## id_correcto expo_valida una_expo_x_arbol
## TRUE TRUE TRUE
##
## 1 2 3 4 5 6
## 10 10 10 10 10 10
La columna grupo debería decir 1 en todas
las filas, pero contiene 1, 2, 3, 4, 5, 6: en esos casos se digitó el
número del árbol. Se reemplaza por el número de grupo
que aparece en el ID.
datos$grupo <- as.integer(sub("^G([0-9]+)_.*", "\\1", datos$id_arbol))
datos$lat <- a_numero(datos$lat)
datos$lon <- a_numero(datos$lon)
range(datos$lon)## [1] 76,53324 76,53571
datos$lon <- -abs(datos$lon)
datos$fecha <- desde_excel(datos$fecha)
datos$n_hoja <- as.integer(datos$n_hoja)Las longitudes se anotaron sin signo (76,53). Como Cali está al oeste del meridiano de Greenwich, la longitud es negativa (−76,53), y así se corrigió.
datos$area_texto <- trimws(datos$area_cm2)
datos$area_cm2 <- a_numero(datos$area_texto)
mediana_arbol <- ave(datos$area_cm2, datos$id_arbol, FUN = function(x) median(x, na.rm = TRUE))
datos$razon_mediana <- datos$area_cm2 / mediana_arbol
# Hojas sin área o con un valor absurdo frente a su propio árbol
sospechosas <- datos[which(is.na(datos$area_cm2) | datos$razon_mediana > 10 | datos$razon_mediana < 0.1),
c("fila", "id_arbol", "n_hoja", "area_texto")]
tabla(sospechosas, caption = "Áreas sospechosas.", col.names = c("Fila", "Árbol", "Hoja", "Valor leído"))| Fila | Árbol | Hoja | Valor leído |
|---|---|---|---|
| 11 | G1_T1 | 10 | 46295 |
El área de la hoja 10 de G1_T1 llega como un número de
varias decenas de miles: es una fecha. En el Excel esa
celda tiene formato dd.mm y muestra 30.09. Al
escribir 30.9, Excel lo interpretó como el 30 de
septiembre. El valor medido era 30,9 cm², coherente con las demás hojas
de ese árbol (entre 22 y 40 cm²). Se recupera a partir de la fecha:
es_fecha <- (!is.na(datos$area_cm2) & datos$area_cm2 > 10000) |
grepl("^[0-9]{4}-[0-9]{2}-[0-9]{2}", datos$area_texto)
f_area <- desde_excel(datos$area_texto[es_fecha])
datos$area_cm2[es_fecha] <- a_numero(paste0(as.integer(format(f_area, "%d")), ".",
as.integer(format(f_area, "%m"))))
datos$corregida <- es_fecha
datos[es_fecha, c("id_arbol", "n_hoja", "area_cm2")]## id_arbol n_hoja area_cm2
## 10 G1_T1 10 30,9
# Controles finales
c(areas_faltantes = sum(is.na(datos$area_cm2) | datos$area_cm2 <= 0),
filas_repetidas = sum(duplicated(datos[, c("id_arbol", "n_hoja")])),
con_observacion = sum(!is.na(datos$observaciones)))## areas_faltantes filas_repetidas con_observacion
## 0 0 4
##
## G1_T1 G1_T2 G1_T3 G1_T4 G1_T5 G1_T6
## 10 10 10 10 10 10
decisiones <- data.frame(
Problema = c("1.ª columna llamada «grupo 1»",
"Columna grupo con el número del árbol",
"Longitud sin signo negativo",
"Área de G1_T1 hoja 10 convertida en fecha (30/09)"),
Filas = c(nrow(datos), nrow(datos), nrow(datos), sum(datos$corregida)),
Solución = c("Renombrar según la plantilla",
"Reemplazar por 1, tomado del ID",
"Cambiar a negativo",
"Recuperar el valor digitado: 30,9 cm²"))
tabla(decisiones, caption = "Problemas encontrados y decisión tomada.", align = "lcl")| Problema | Filas | Solución |
|---|---|---|
| 1.ª columna llamada «grupo 1» | 60 | Renombrar según la plantilla |
| Columna grupo con el número del árbol | 60 | Reemplazar por 1, tomado del ID |
| Longitud sin signo negativo | 60 | Cambiar a negativo |
| Área de G1_T1 hoja 10 convertida en fecha (30/09) | 1 | Recuperar el valor digitado: 30,9 cm² |
limpios <- datos[order(datos$id_arbol, datos$n_hoja),
c("grupo", "id_arbol", "lat", "lon", "n_hoja", "area_cm2",
"exposicion", "fecha", "observaciones", "corregida")]
if (params$guardar_limpios) write.csv(limpios, "datos_limpios_area_foliar_G1.csv", row.names = FALSE, na = "")No hubo áreas faltantes ni filas repetidas. Cuatro hojas tienen
observaciones de campo (forma menos ovoide, hoja levemente rota, un
pequeño roto en el marco y papel arrugado); sus áreas están dentro del
rango normal de su árbol, por lo que se conservaron. La base revisada
tiene 60 hojas de 6 árboles y se guarda como
datos_limpios_area_foliar_G1.csv.
dg <- limpios
dg$id_arbol <- factor(dg$id_arbol)
dg$expo <- factor(dg$exposicion, levels = c("sol", "sombra"))resumen <- do.call(rbind, lapply(split(dg, dg$id_arbol), function(d) {
data.frame(arbol = as.character(d$id_arbol[1]), exposicion = d$exposicion[1], n = nrow(d),
media = mean(d$area_cm2), mediana = median(d$area_cm2), de = sd(d$area_cm2),
cv_pct = cv(d$area_cm2), minimo = min(d$area_cm2), maximo = max(d$area_cm2))
}))
tabla(resumen, digits = c(0, 0, 0, 1, 1, 1, 1, 1, 1),
col.names = c("Árbol", "Exposición", "n", "Media", "Mediana", "DE", "CV (%)", "Mín.", "Máx."),
caption = "Área foliar (cm²) por árbol. DE: desviación estándar; CV: coeficiente de variación.") |>
column_spec(2, background = ifelse(resumen$exposicion == "sol", "#FBE8CC", "#D5E8DF")) |>
column_spec(4, bold = TRUE)| Árbol | Exposición | n | Media | Mediana | DE | CV (%) | Mín. | Máx. |
|---|---|---|---|---|---|---|---|---|
| G1_T1 | sombra | 10 | 31,2 | 32,1 | 6,2 | 19,8 | 22,2 | 39,8 |
| G1_T2 | sol | 10 | 16,8 | 16,0 | 2,8 | 16,6 | 13,9 | 22,9 |
| G1_T3 | sombra | 10 | 42,0 | 41,8 | 9,3 | 22,1 | 30,2 | 61,8 |
| G1_T4 | sol | 10 | 52,1 | 54,8 | 10,4 | 19,9 | 31,4 | 67,4 |
| G1_T5 | sombra | 10 | 70,8 | 68,7 | 15,8 | 22,3 | 42,7 | 106,2 |
| G1_T6 | sol | 10 | 24,1 | 23,1 | 5,6 | 23,3 | 14,5 | 34,4 |
El tamaño de hoja varía mucho de un árbol a otro. La media va de 16,8 cm² en G1_T2 (sol) a 70,8 cm² en G1_T5 (sombra): el árbol de hojas más grandes tiene hojas unas 4,2 veces más grandes que el de hojas más pequeñas. Dentro de cada árbol, la media y la mediana difieren como máximo en 5 %.
Los coeficientes de variación de los 6 árboles van de 17 a 23 %. La correlación entre la media y la desviación estándar de los árboles es de 0,99. Las hojas de los árboles con hojas grandes varían más en cm², pero en proporción parecida (CV similar), y eso justifica trabajar con el logaritmo del área en el ANOVA.
set.seed(4)
ggplot(dg, aes(id_arbol, area_cm2, fill = expo)) +
geom_boxplot(alpha = 0.55, width = 0.6, outlier.shape = NA) +
geom_jitter(width = 0.12, height = 0, shape = 21, size = 2.3, fill = "white", alpha = 0.9) +
stat_summary(fun = mean, geom = "point", shape = 23, size = 3.5, fill = "black") +
scale_fill_manual(values = col_expo, name = "Exposición") +
scale_y_continuous(labels = coma) +
labs(title = "Área foliar por árbol", subtitle = "Cada punto es una hoja (10 por árbol)",
x = NULL, y = "Área foliar (cm²)")Figura 1. Área de cada hoja según el árbol. El rombo negro marca la media.
Las cajas de G1_T2 y G1_T5 (los árboles de hojas más pequeñas y más grandes) no se solapan en absoluto. Entre árboles se concentra cerca del 85 % de la variación del área (ver ANOVA más adelante).
Para decidir qué hacer con una hoja extrema seguimos dos preguntas, en este orden:
G1_T1.fuera <- do.call(rbind, lapply(split(dg, dg$id_arbol), function(d) {
out <- boxplot.stats(d$area_cm2)$out
if (length(out) == 0) return(NULL)
data.frame(arbol = as.character(d$id_arbol[1]), n_hoja = d$n_hoja[d$area_cm2 %in% out],
area = d$area_cm2[d$area_cm2 %in% out], mediana_arbol = median(d$area_cm2))
}))
tabla(fuera, digits = c(0, 0, 2, 1), col.names = c("Árbol", "Hoja", "Área (cm²)", "Mediana del árbol (cm²)"),
caption = "Hojas fuera de los límites de Tukey de su árbol.")| Árbol | Hoja | Área (cm²) | Mediana del árbol (cm²) |
|---|---|---|---|
| G1_T2 | 2 | 22,85 | 16,0 |
| G1_T5 | 1 | 106,20 | 68,7 |
| G1_T5 | 4 | 42,66 | 68,7 |
sens <- do.call(rbind, lapply(split(dg, dg$id_arbol), function(d) {
out <- boxplot.stats(d$area_cm2)$out
data.frame(exposicion = d$exposicion[1], media = mean(d$area_cm2), mediana = median(d$area_cm2),
media_sin = mean(d$area_cm2[!d$area_cm2 %in% out]))
}))
dif_sens <- sapply(c("media", "mediana", "media_sin"), function(v)
mean(sens[[v]][sens$exposicion == "sombra"]) - mean(sens[[v]][sens$exposicion == "sol"]))
tabla(data.frame(c("Media de todas las hojas", "Mediana", "Media sin hojas atípicas"), unname(dif_sens)),
digits = 2, col.names = c("Resumen de cada árbol", "Diferencia sombra − sol (cm²)"),
caption = "Sensibilidad del resultado a la hoja atípica.")| Resumen de cada árbol | Diferencia sombra − sol (cm²) |
|---|---|
| Media de todas las hojas | 17,00 |
| Mediana | 16,25 |
| Media sin hojas atípicas | 16,93 |
3 hojas quedan fuera de los límites de Tukey de su árbol (tabla anterior), en G1_T2 y G1_T5. Ninguna tiene una anotación de campo que sugiera un problema de medición y sus tamaños son plausibles para esos árboles, así que se conservan. Además, quitarlas no cambia la conclusión: la diferencia sombra − sol pasa de 17,00 a 16,93 cm².
set.seed(7)
ggplot(dg, aes(expo, area_cm2)) +
geom_boxplot(aes(fill = expo), alpha = 0.45, width = 0.5, outlier.shape = NA, show.legend = FALSE) +
geom_jitter(aes(shape = id_arbol), width = 0.15, height = 0, size = 2.6, alpha = 0.8) +
scale_fill_manual(values = col_expo) +
scale_shape_manual(values = c(16, 17, 15, 18, 3, 8, 4, 7, 10, 12)[seq_len(nlevels(dg$id_arbol))], name = "Árbol") +
scale_y_continuous(labels = coma) +
labs(title = "Área foliar por exposición", subtitle = "Descriptivo: las hojas de un árbol no son independientes",
x = "Exposición", y = "Área foliar (cm²)")Figura 2. Hojas agrupadas por exposición. La forma del punto indica el árbol.
por_expo <- split(dg, dg$expo)
tabla(data.frame(Exposición = names(por_expo),
Árboles = sapply(por_expo, function(d) length(unique(d$id_arbol))),
Hojas = sapply(por_expo, nrow),
Media = fmt(sapply(por_expo, function(d) mean(d$area_cm2)), 1),
Mediana = fmt(sapply(por_expo, function(d) median(d$area_cm2)), 1),
DE = fmt(sapply(por_expo, function(d) sd(d$area_cm2)), 1)),
digits = 0, caption = "Resumen de las hojas por exposición.")| Exposición | Árboles | Hojas | Media | Mediana | DE |
|---|---|---|---|---|---|
| sol | 3 | 30 | 31,0 | 23,5 | 16,9 |
| sombra | 3 | 30 | 48,0 | 41,8 | 20,2 |
Cada punto es una hoja y la forma indica el árbol. La mediana de las hojas de sol es 23,5 cm² y la de las hojas de sombra, 41,8 cm². Los puntos del mismo árbol se agrupan entre sí y cada caja junta varios árboles, por lo que las 60 hojas no pueden tratarse como 60 observaciones independientes de la exposición.
Lo que se clasificó como de sol o de sombra fue el árbol. Sus 10 hojas comparten genética, edad, suelo y luz, así que representan a un solo individuo. Para comparar las exposiciones se resume cada árbol con el promedio de sus hojas. Así quedan 6 datos, no 60.
medias <- aggregate(area_cm2 ~ id_arbol + exposicion, data = dg, FUN = mean)
medias$id_arbol <- as.character(medias$id_arbol)
medias$exposicion <- factor(medias$exposicion, levels = c("sol", "sombra"))
medias <- medias[order(medias$exposicion, medias$id_arbol), ]
n_expo <- table(medias$exposicion)
prom_expo <- tapply(medias$area_cm2, medias$exposicion, mean)
dif_obs <- unname(prom_expo["sombra"] - prom_expo["sol"])
tabla(medias, digits = c(0, 0, 1), col.names = c("Árbol", "Exposición", "Área media (cm²)"),
caption = "Promedio de área foliar de cada árbol.") |>
column_spec(2, background = ifelse(medias$exposicion == "sol", "#FBE8CC", "#D5E8DF"))| Árbol | Exposición | Área media (cm²) |
|---|---|---|
| G1_T2 | sol | 16,8 |
| G1_T4 | sol | 52,1 |
| G1_T6 | sol | 24,1 |
| G1_T1 | sombra | 31,2 |
| G1_T3 | sombra | 42,0 |
| G1_T5 | sombra | 70,8 |
ggplot(medias, aes(exposicion, area_cm2, fill = exposicion)) +
geom_boxplot(alpha = 0.3, width = 0.45, outlier.shape = NA, show.legend = FALSE) +
stat_summary(fun = mean, fun.min = mean, fun.max = mean, geom = "crossbar",
width = 0.45, color = "black", linewidth = 0.4, show.legend = FALSE) +
geom_point(shape = 21, size = 7, color = "white", stroke = 1.2, show.legend = FALSE) +
geom_text(aes(label = id_arbol), nudge_x = 0.32, size = 3.6) +
scale_fill_manual(values = col_expo) +
scale_y_continuous(labels = coma, limits = c(0, NA)) +
labs(title = "Área foliar media por árbol",
subtitle = paste0(n_expo["sol"], " árboles de sol y ", n_expo["sombra"], " de sombra"),
x = "Exposición", y = "Área foliar media del árbol (cm²)")Figura 3. Un punto por árbol (promedio de sus 10 hojas). La línea horizontal es el promedio de cada exposición.
En promedio, los árboles de sombra tienen hojas de 48,0 cm² y los de sol de 31,0 cm². La diferencia sombra − sol es de 17,0 cm² (positiva si las hojas de sombra son más grandes). Para ponerla en contexto: las medias de los árboles de sol van de 16,8 a 52,1 cm² y las de sombra de 31,2 a 70,8 cm². Los dos rangos se solapan: la exposición, por sí sola, no ordena a los árboles.
En este mapa el tamaño de cada círculo es proporcional al área foliar media del árbol. Sirve para ver si hay una zona del campus con hojas grandes que pudiera confundirse con el efecto de la exposición.
mapa_res <- merge(medias, sitios[, c("id_arbol", "lat", "lon", "foto")], by = "id_arbol")
mapa_res$popup <- vapply(seq_len(nrow(mapa_res)), function(i) {
s <- mapa_res[i, ]
img <- if (is.na(s$foto)) "" else paste0("<img src='", knitr::image_uri(s$foto), "' width='180'><br>")
paste0(img, "<b>", s$id_arbol, "</b> (", s$exposicion, ")<br>Área media: ", fmt(s$area_cm2), " cm²")
}, character(1))
mapa_res$expo_txt <- as.character(mapa_res$exposicion)
if (requireNamespace("leaflet", quietly = TRUE)) {
leaflet::leaflet(mapa_res) |>
leaflet::addProviderTiles(leaflet::providers$Esri.WorldImagery, group = "Satélite") |>
leaflet::addTiles(group = "Calles") |>
leaflet::addCircleMarkers(lng = ~lon, lat = ~lat, radius = ~sqrt(area_cm2) * 2.2, weight = 2,
color = "white", fillColor = ~unname(col_expo[expo_txt]), fillOpacity = 0.85,
label = ~paste0(id_arbol, ": ", fmt(area_cm2), " cm²"), popup = ~popup) |>
leaflet::addLayersControl(baseGroups = c("Satélite", "Calles"),
options = leaflet::layersControlOptions(collapsed = FALSE)) |>
leaflet::addLegend("bottomright", colors = unname(col_expo), labels = c("Sol", "Sombra"),
title = "Exposición (tamaño = área media)", opacity = 1) |>
leaflet::addScaleBar("bottomleft")
} else {
ggplot(mapa_res, aes(lon, lat, fill = expo_txt, size = area_cm2)) +
geom_point(shape = 21, color = "white") +
geom_text(aes(label = id_arbol), size = 3.5, vjust = -2) +
scale_fill_manual(values = col_expo, name = "Exposición") +
scale_size_area(max_size = 16, name = "Área media (cm²)") +
coord_quickmap() +
labs(title = "Área foliar media por árbol", x = "Longitud", y = "Latitud")
}Los árboles con las hojas más pequeñas (G1_T2) y más grandes (G1_T5) están separados por unos 12 m, así que el tamaño de hoja no parece organizarse por zonas del campus. El árbol G1_T3 queda aparte, a más de 340 m de cualquiera de los demás, mientras que los otros 5 árboles están a menos de 40 m entre sí.
Si la exposición no tuviera ningún efecto, las etiquetas “sol” y “sombra” podrían haberse repartido de cualquier forma entre los 6 árboles. La prueba calcula la diferencia sombra − sol para todos los repartos posibles y compara con ellos la diferencia observada.
y <- medias$area_cm2
es_sombra <- medias$exposicion == "sombra"
repartos <- combn(length(y), sum(es_sombra))
dif_perm <- apply(repartos, 2, function(i) mean(y[i]) - mean(y[-i]))
p_perm <- mean(abs(dif_perm) >= abs(dif_obs) - 1e-9)
p_min <- mean(abs(dif_perm) >= max(abs(dif_perm)) - 1e-9) # el menor p que este diseño puede dar
perm_df <- data.frame(dif = dif_perm, tipo = ifelse(abs(dif_perm - dif_obs) < 1e-9, "Observado", "Otro reparto"))
ggplot(perm_df, aes(dif, 0, fill = tipo)) +
geom_vline(xintercept = 0, linetype = 3) +
geom_point(shape = 21, size = 8, color = "grey30") +
scale_fill_manual(values = c(Observado = col_expo[["sombra"]], `Otro reparto` = "grey85"), name = NULL) +
scale_y_continuous(NULL, breaks = NULL) +
scale_x_continuous(labels = coma) +
labs(x = "Diferencia sombra − sol (cm²)", title = paste(ncol(repartos), "repartos posibles"))Figura 4. Diferencia sombra − sol en cada uno de los repartos posibles de las etiquetas.
Solo hay 20 repartos posibles de las etiquetas, y la diferencia observada da p = 0,400. Además, con 3 árboles de sol y 3 de sombra, el menor p que podría obtenerse, fueran cuales fueran los datos, es 0,100. El diseño no tenía capacidad para detectar una diferencia al 5 %, por grande que fuera.
tt <- t.test(area_cm2 ~ exposicion, data = medias)
tl <- t.test(log(area_cm2) ~ exposicion, data = medias)
wil <- wilcox.test(area_cm2 ~ exposicion, data = medias)
ic <- -rev(tt$conf.int) # sombra − sol
razon <- unname(exp(tl$estimate[2] - tl$estimate[1]))
ic_raz <- exp(-rev(tl$conf.int))
tabla(data.frame(
Prueba = c("Permutación exacta", "t de Welch (cm²)", "t de Welch sobre log: razón sombra/sol", "Wilcoxon"),
Efecto = c(paste(fmt(dif_obs), "cm²"), paste(fmt(dif_obs), "cm²"), paste0(fmt(razon, 2), " veces"), "—"),
IC95 = c("—", paste0("[", fmt(ic[1]), "; ", fmt(ic[2]), "]"),
paste0("[", fmt(ic_raz[1], 2), "; ", fmt(ic_raz[2], 2), "]"), "—"),
p = c(fmt_p(p_perm), fmt_p(tt$p.value), fmt_p(tl$p.value), fmt_p(wil$p.value))),
align = "lccc", col.names = c("Prueba", "Efecto estimado", "IC 95 %", "p"),
caption = paste0("Comparación sombra − sol con un dato por árbol (", n_expo["sol"], " de sol y ", n_expo["sombra"], " de sombra)."))| Prueba | Efecto estimado | IC 95 % | p |
|---|---|---|---|
| Permutación exacta | 17,0 cm² | — | 0,400 |
| t de Welch (cm²) | 17,0 cm² | [-27,6; 61,6] | 0,348 |
| t de Welch sobre log: razón sombra/sol | 1,64 veces | [0,50; 5,39] | 0,302 |
| Wilcoxon | — | — | 0,400 |
Ninguna prueba detecta diferencia (p entre 0,302 y 0,400). Los intervalos de confianza muestran la incertidumbre de la estimación con tan pocos árboles. Que no encontremos diferencia no significa que no exista: significa que, con estos datos, no se puede saber.
Con solo 3 árboles por condición, un árbol puede mover mucho el promedio. Para comprobarlo se repite la diferencia sombra − sol quitando, uno a uno, cada árbol.
loo <- do.call(rbind, lapply(medias$id_arbol, function(a) {
m <- medias[medias$id_arbol != a, ]
data.frame(quitado = a,
exposicion = as.character(medias$exposicion[medias$id_arbol == a]),
dif = mean(m$area_cm2[m$exposicion == "sombra"]) - mean(m$area_cm2[m$exposicion == "sol"]))
}))
tabla(loo, digits = c(0, 0, 1),
col.names = c("Árbol quitado", "Su exposición", "Diferencia sombra − sol (cm²)"),
caption = "Diferencia sombra − sol al quitar un árbol a la vez.")| Árbol quitado | Su exposición | Diferencia sombra − sol (cm²) |
|---|---|---|
| G1_T2 | sol | 9,9 |
| G1_T4 | sol | 27,6 |
| G1_T6 | sol | 13,6 |
| G1_T1 | sombra | 25,4 |
| G1_T3 | sombra | 20,0 |
| G1_T5 | sombra | 5,6 |
loo_min <- min(loo$dif); loo_max <- max(loo$dif)
loo_mismo_signo <- all(sign(loo$dif) == sign(dif_obs))
arb_clave <- loo$quitado[which.min(abs(loo$dif))]La diferencia conserva el mismo signo en todos los casos, pero su tamaño va de 5,6 a 27,6 cm² (con todos los árboles es 17,0 cm²). El árbol que más la reduce al quitarlo es G1_T5.
sd_p <- sqrt(mean(tapply(medias$area_cm2, medias$exposicion, var))) # DE entre árboles dentro de cada exposición
n_nec <- ceiling(power.t.test(delta = abs(dif_obs), sd = sd_p, power = 0.8, sig.level = 0.05)$n)Con la desviación estándar entre árboles observada (19,6 cm²), detectar una diferencia como la observada (17,0 cm²) con 80 % de potencia requeriría unos 22 árboles por condición. Es una estimación aproximada: con tan pocos árboles, tanto la diferencia como la variación pueden estar mal estimadas.
t_hojas <- t.test(area_cm2 ~ expo, data = dg)
tabla(data.frame(Análisis = c("Hojas como datos independientes (incorrecto)", "Un promedio por árbol (correcto)"),
Datos = c(nrow(dg), nrow(medias)),
p = c(fmt_p(t_hojas$p.value), fmt_p(p_perm))),
align = "lcc", col.names = c("Análisis", "Datos usados", "p"),
caption = "Efecto de la unidad de análisis sobre la prueba.") |>
row_spec(1, color = "#A33B2B")| Análisis | Datos usados | p |
|---|---|---|
| Hojas como datos independientes (incorrecto) | 60 | < 0,001 |
| Un promedio por árbol (correcto) | 6 | 0,400 |
Usar las hojas como réplicas de la exposición se conoce como pseudorreplicación (Hurlbert, 1984): se multiplica por diez el tamaño de muestra sin agregar información sobre la exposición. Aquí el valor p pasa de < 0,001 (hojas como réplicas) a 0,400 (árbol como unidad). Ignorar el árbol produce un resultado falsamente significativo.
tabla(data.frame(
Elemento = c("Factor (fijo)", "Unidad experimental", "Unidad de muestreo", "Réplicas", "Asignación"),
`En este estudio` = c("Exposición: sol y sombra", "Árbol", "Hoja (submuestra)",
paste0(n_expo["sol"], " árboles de sol y ", n_expo["sombra"], " de sombra; 10 hojas por árbol"),
"No aleatoria (estudio observacional)"),
check.names = FALSE),
align = "ll", caption = "Elementos del diseño.")| Elemento | En este estudio |
|---|---|
| Factor (fijo) | Exposición: sol y sombra |
| Unidad experimental | Árbol |
| Unidad de muestreo | Hoja (submuestra) |
| Réplicas | 3 árboles de sol y 3 de sombra; 10 hojas por árbol |
| Asignación | No aleatoria (estudio observacional) |
Las hojas están anidadas en los árboles, y cada árbol pertenece a una sola exposición. El modelo es
\[\log(\text{área}_{ijk}) = \mu + E_i + A_{j(i)} + \varepsilon_{k(ij)},\]
donde \(E_i\) es el efecto de la exposición, \(A_{j(i)}\) el del árbol \(j\) dentro de la exposición \(i\) y \(\varepsilon_{k(ij)}\) la variación entre hojas de un mismo árbol. El término del árbol es el error experimental: la exposición debe compararse contra él, y no contra la variación entre hojas.
¿Por qué usar el logaritmo?
de_orig <- tapply(dg$area_cm2, dg$id_arbol, sd)
de_log <- tapply(log(dg$area_cm2), dg$id_arbol, sd)
p_flig_orig <- fligner.test(area_cm2 ~ id_arbol, data = dg)$p.value
p_flig_log <- fligner.test(log(area_cm2) ~ id_arbol, data = dg)$p.value
tabla(data.frame(Escala = c("Original (cm²)", "Logaritmo"),
DE_min = c(min(de_orig), min(de_log)), DE_max = c(max(de_orig), max(de_log)),
Cociente = c(max(de_orig) / min(de_orig), max(de_log) / min(de_log)),
p = c(fmt_p(p_flig_orig), fmt_p(p_flig_log))),
digits = c(0, 3, 3, 2, 0),
col.names = c("Escala", "DE mínima", "DE máxima", "DE máx / DE mín", "p (Fligner-Killeen)"),
caption = "Variabilidad entre árboles en cada escala.")| Escala | DE mínima | DE máxima | DE máx / DE mín | p (Fligner-Killeen) |
|---|---|---|---|---|
| Original (cm²) | 2,776 | 15,813 | 5,70 | 0,072 |
| Logaritmo | 0,154 | 0,240 | 1,56 | 0,850 |
En la escala original, la desviación estándar del árbol más variable es 5,7 veces la del menos variable (Fligner-Killeen, p = 0,072). Con el logaritmo, esa razón es 1,6 (p = 0,850). Cuanto más cerca de 1 está la razón y mayor es el p, más homogéneas son las varianzas. Además, en escala log los efectos se leen como razones (“tantas veces más grande”).
m1 <- lm(log(area_cm2) ~ id_arbol, data = dg)
a1 <- anova_df(m1)
a1$Fuente <- c("Árbol", "Residual (hojas)")
r2_arb <- summary(m1)$r.squared
tabla(anova_txt(a1), align = "lccccc", caption = "ANOVA de un factor sobre log(área foliar).")| Fuente | gl | SC | CM | F | p |
|---|---|---|---|---|---|
| Árbol | 5 | 13,686 | 2,737 | 61,18 | < 0,001 |
| Residual (hojas) | 54 | 2,416 | 0,045 |
Hay diferencias muy marcadas entre los árboles: F(5; 54) = 61,2, p < 0,001. El árbol explica el 85 % de la variación del logaritmo del área. En este análisis las hojas sí funcionan como réplicas, porque la pregunta es por estos árboles concretos y no por la exposición.
Verificación de supuestos.
diag_df <- data.frame(ajustado = fitted(m1), residuo = resid(m1), arbol = dg$id_arbol)
ggplot(diag_df, aes(ajustado, residuo, color = arbol)) +
geom_hline(yintercept = 0, linetype = 2) +
geom_point(size = 2.4, alpha = 0.85, position = position_jitter(width = 0.015, height = 0, seed = 1)) +
labs(title = "Residuos vs. ajustados", x = "Valor ajustado (log área)", y = "Residuo", color = "Árbol")
ggplot(diag_df, aes(sample = residuo)) +
stat_qq(color = verde_osc, size = 2) + stat_qq_line(color = "#C0392B") +
labs(title = "Normalidad de los residuos", x = "Cuantiles teóricos", y = "Cuantiles de los residuos")
shap1 <- shapiro.test(resid(m1))
flig1 <- fligner.test(log(area_cm2) ~ id_arbol, data = dg)
kw1 <- kruskal.test(area_cm2 ~ id_arbol, data = dg)
tabla(data.frame(Supuesto = c("Normalidad de residuos", "Homogeneidad de varianzas", "Alternativa sin normalidad"),
Prueba = c("Shapiro-Wilk", "Fligner-Killeen", "Kruskal-Wallis"),
p = c(fmt_p(shap1$p.value), fmt_p(flig1$p.value), fmt_p(kw1$p.value))),
align = "llc", caption = "Pruebas de los supuestos del ANOVA.")| Supuesto | Prueba | p |
|---|---|---|
| Normalidad de residuos | Shapiro-Wilk | 0,362 |
| Homogeneidad de varianzas | Fligner-Killeen | 0,850 |
| Alternativa sin normalidad | Kruskal-Wallis | < 0,001 |
Los supuestos se cumplen. Normalidad de los residuos (Shapiro-Wilk, p = 0,362), comparada con la recta del gráfico cuantil-cuantil, y dispersión similar entre los 6 árboles (Fligner-Killeen, p = 0,850). La prueba de Kruskal-Wallis, que no exige normalidad, da p < 0,001.
El ANOVA indica que al menos un árbol es distinto. Para saber cuáles se comparan todos los pares (15 comparaciones) con la prueba de Tukey, que controla la probabilidad de cometer algún falso positivo en el conjunto.
tk <- TukeyHSD(aov(log(area_cm2) ~ id_arbol, data = dg))$id_arbol
tk_tab <- data.frame(comparacion = gsub("-", " / ", rownames(tk)),
razon = exp(tk[, "diff"]), li = exp(tk[, "lwr"]), ls = exp(tk[, "upr"]),
p_aj = tk[, "p adj"], stringsAsFactors = FALSE)
tk_tab$misma <- sapply(strsplit(tk_tab$comparacion, " / "), function(x)
length(unique(resumen$exposicion[match(x, resumen$arbol)])) == 1)
tk_tab$difiere <- ifelse(tk_tab$p_aj < 0.05, "sí", "no")
tk_tab <- tk_tab[order(tk_tab$p_aj), ]
tabla(data.frame(tk_tab$comparacion, fmt(tk_tab$razon, 2),
paste0("[", fmt(tk_tab$li, 2), "; ", fmt(tk_tab$ls, 2), "]"),
fmt_p(tk_tab$p_aj), tk_tab$difiere, ifelse(tk_tab$misma, "sí", "no")),
align = "lccccc",
col.names = c("Comparación (A / B)", "Razón A / B", "IC 95 %", "p ajustado", "¿Difieren?", "¿Misma exposición?"),
caption = "Comparaciones de Tukey entre árboles (en escala log, expresadas como razones de área).") |>
row_spec(which(tk_tab$p_aj < 0.05), bold = TRUE)| Comparación (A / B) | Razón A / B | IC 95 % | p ajustado | ¿Difieren? | ¿Misma exposición? |
|---|---|---|---|---|---|
| G1_T5 / G1_T2 | 4,18 | [3,16; 5,52] | < 0,001 | sí | no |
| G1_T4 / G1_T2 | 3,08 | [2,33; 4,07] | < 0,001 | sí | sí |
| G1_T6 / G1_T5 | 0,34 | [0,26; 0,45] | < 0,001 | sí | no |
| G1_T3 / G1_T2 | 2,48 | [1,87; 3,28] | < 0,001 | sí | no |
| G1_T5 / G1_T1 | 2,26 | [1,71; 2,99] | < 0,001 | sí | sí |
| G1_T6 / G1_T4 | 0,46 | [0,35; 0,61] | < 0,001 | sí | sí |
| G1_T2 / G1_T1 | 0,54 | [0,41; 0,72] | < 0,001 | sí | no |
| G1_T6 / G1_T3 | 0,57 | [0,43; 0,76] | < 0,001 | sí | no |
| G1_T5 / G1_T3 | 1,68 | [1,27; 2,23] | < 0,001 | sí | sí |
| G1_T4 / G1_T1 | 1,67 | [1,26; 2,21] | < 0,001 | sí | no |
| G1_T6 / G1_T2 | 1,42 | [1,07; 1,87] | 0,007 | sí | sí |
| G1_T5 / G1_T4 | 1,36 | [1,03; 1,79] | 0,025 | sí | no |
| G1_T3 / G1_T1 | 1,34 | [1,02; 1,78] | 0,032 | sí | sí |
| G1_T6 / G1_T1 | 0,77 | [0,58; 1,02] | 0,073 | no | no |
| G1_T4 / G1_T3 | 1,24 | [0,94; 1,64] | 0,216 | no | no |
ggplot(tk_tab, aes(razon, reorder(comparacion, razon), color = difiere)) +
geom_vline(xintercept = 1, linetype = 2) +
geom_linerange(aes(xmin = li, xmax = ls), linewidth = 1.1) +
geom_point(size = 3.2) +
scale_x_log10(labels = coma) +
scale_color_manual(values = c(`sí` = "#C0392B", no = "grey55"), name = "¿Difieren al 5 %?") +
labs(title = "Comparaciones por pares", x = "Razón de áreas (escala logarítmica)", y = NULL)Figura 5. Razón de áreas entre árboles con su intervalo de Tukey. Un intervalo que cruza 1 indica que no hay diferencia detectable.
13 de las 15 comparaciones son significativas. Las parejas que no se distinguen son: G1_T6 / G1_T1, G1_T4 / G1_T3. La mayor diferencia se da entre G1_T5 / G1_T2 (hojas unas 4,2 veces más grandes en uno que en otro; tienen exposiciones distintas). Entre los árboles con la misma exposición, 6 de 6 parejas difieren significativamente. Compartir la misma exposición no hace que dos árboles tengan hojas parecidas.
m_anid <- lm(log(area_cm2) ~ expo + id_arbol, data = dg) # id_arbol queda anidado en expo
a_an <- anova_df(m_anid)
F_expo <- a_an$CM[1] / a_an$CM[2]; p_expo <- pf(F_expo, a_an$gl[1], a_an$gl[2], lower.tail = FALSE)
F_arbol <- a_an$CM[2] / a_an$CM[3]; p_arbol <- pf(F_arbol, a_an$gl[2], a_an$gl[3], lower.tail = FALSE)
p_ingenuo <- a_an$p[1] # exposición contra el error de hojas
tabla(data.frame(Fuente = c("Exposición", "Árbol dentro de exposición", "Hojas (residual)"),
gl = a_an$gl, SC = fmt(a_an$SC, 3), CM = fmt(a_an$CM, 3),
Denominador = c("CM árbol", "CM hojas", ""),
F = c(fmt(F_expo, 2), fmt(F_arbol, 2), ""),
p = c(fmt_p(p_expo), fmt_p(p_arbol), "")),
align = "lcccccc", caption = "ANOVA anidado sobre log(área foliar).") |>
row_spec(1, bold = TRUE, background = "#FDF4E6")| Fuente | gl | SC | CM | Denominador | F | p |
|---|---|---|---|---|---|---|
| Exposición | 1 | 3,636 | 3,636 | CM árbol | 1,45 | 0,295 |
| Árbol dentro de exposición | 4 | 10,050 | 2,512 | CM hojas | 56,16 | < 0,001 |
| Hojas (residual) | 54 | 2,416 | 0,045 |
n0 <- nrow(dg) / nlevels(dg$id_arbol)
s2_hoja <- a_an$CM[3]
s2_arbol <- max((a_an$CM[2] - a_an$CM[3]) / n0, 0)
pct_arb <- 100 * s2_arbol / (s2_arbol + s2_hoja)
tabla(data.frame(Componente = c("Entre árboles", "Entre hojas del mismo árbol"),
Varianza = c(s2_arbol, s2_hoja),
Porcentaje = c(pct_arb, 100 - pct_arb)),
digits = c(0, 4, 1), col.names = c("Componente", "Varianza (log)", "% del total"),
caption = "Componentes de varianza.")| Componente | Varianza (log) | % del total |
|---|---|---|
| Entre árboles | 0,2468 | 84,7 |
| Entre hojas del mismo árbol | 0,0447 | 15,3 |
Como la exposición tiene solo dos niveles, no corresponde hacer comparaciones post-ANOVA para ella. Con dos niveles, la prueba F ya es la comparación entre ambos.
tabla(data.frame(
Pregunta = c("¿Difieren los árboles?", "¿Qué árboles difieren?", "¿Difieren sol y sombra? (permutación)",
"¿Difieren sol y sombra? (ANOVA anidado)", "Mismo análisis ignorando el árbol"),
Resultado = c(paste0(ifelse(a1$p[1] < 0.05, "Sí", "No"), ": F = ", fmt(a1$F[1], 1), ", ", p_eq(a1$p[1])),
paste(sum(tk_tab$p_aj < 0.05), "de", nrow(tk_tab), "pares (Tukey)"),
paste0(ifelse(p_perm < 0.05, "Sí", "No"), ": ", p_eq(p_perm)),
paste0(ifelse(p_expo < 0.05, "Sí", "No"), ": F = ", fmt(F_expo, 2), ", ", p_eq(p_expo)),
paste0(p_eq(p_ingenuo), ifelse(ingenuo_enganoso, " (engañoso)", "")))),
align = "ll", caption = "Resumen de resultados.")| Pregunta | Resultado |
|---|---|
| ¿Difieren los árboles? | Sí: F = 61,2, p < 0,001 |
| ¿Qué árboles difieren? | 13 de 15 pares (Tukey) |
| ¿Difieren sol y sombra? (permutación) | No: p = 0,400 |
| ¿Difieren sol y sombra? (ANOVA anidado) | No: F = 1,45, p = 0,295 |
| Mismo análisis ignorando el árbol | p < 0,001 (engañoso) |
No encontramos evidencia de que los árboles de sombra tengan hojas más grandes que los de sol. Los promedios fueron 48,0 cm² (sombra) frente a 31,0 cm² (sol; p = 0,295). La hipótesis no se confirma. Sin embargo, con 3 árboles por condición el estudio tiene poca capacidad para detectar un efecto, y por tanto tampoco permite descartar que exista.
El resultado más sólido del estudio es otro: las diferencias entre árboles son grandes. Cerca del 85 % de la variación (en log) está entre árboles. El árbol de hojas más grandes (G1_T5, de sombra) tiene hojas unas 4,2 veces más grandes que el de hojas más pequeñas (G1_T2, de sol), y entre ambos hay unos 12 m. Cuando árboles cercanos difieren tanto, la explicación puramente ambiental es menos probable: las fotografías muestran árboles de porte, edad y follaje distintos, lo que sugiere especies o morfotipos distintos (la especie no se registró).
Si un estudio más amplio encontrara hojas mayores a la sombra, estas serían las explicaciones más probables:
En nuestro caso no se midió la humedad del suelo. Los árboles de
sombra crecen bajo el dosel de otros árboles y los de sol en zonas
abiertas, una de ellas junto a un parqueadero (G1_T6). Luz,
temperatura y humedad del suelo cambian juntas entre ambos ambientes,
así que, aun si hubiéramos encontrado una diferencia, no podríamos
atribuirla a uno solo de esos factores.
G1_T4 parece joven y estaba produciendo hojas nuevas
(brotes rojizos), y sus hojas promedian 52,1 cm². Los árboles jóvenes y
las ramas vigorosas suelen producir hojas más grandes, así que la edad
puede explicar parte de las diferencias entre árboles.tabla(data.frame(
`Resultado` = c(
paste0("La diferencia sombra − sol fue de ", fmt(dif_obs), " cm² en estos ", nrow(medias),
" árboles, ", ifelse(hay_efecto, "y es estadísticamente detectable.",
"pero no se distingue del azar.")),
paste0("Quitando un árbol a la vez, la diferencia va de ", fmt(loo_min), " a ", fmt(loo_max), " cm²."),
paste0("Los árboles difieren mucho entre sí, incluso con la misma exposición (", n_misma_dif,
" de ", n_misma, " pares); cerca del ", fmt(pct_arb, 0), " % de la variación está entre árboles."),
ifelse(ingenuo_enganoso,
"Tratar las hojas como réplicas produce un p falsamente significativo.",
paste0("Tratar las hojas como réplicas cambia el p (de ", fmt_p(p_perm), " a ", fmt_p(p_hojas), ")."))),
`Hipótesis` = c(
ifelse(dif_obs > 0,
"Que las hojas de sombra son realmente más grandes: la diferencia va en ese sentido, pero con tan pocos árboles no se puede confirmar.",
"Que existe una diferencia entre sol y sombra: con tan pocos árboles no se puede confirmar."),
"Que ese efecto se mantiene con más árboles de una misma especie.",
"Que las diferencias entre árboles se deben a la especie o a la edad: no se registraron.",
"Que la luz, y no la temperatura o la humedad, es la causa de cualquier diferencia."),
check.names = FALSE), align = "ll",
caption = "Distinción entre resultados (observados en estos árboles) e hipótesis (por probar).")| Resultado | Hipótesis |
|---|---|
| La diferencia sombra − sol fue de 17,0 cm² en estos 6 árboles, pero no se distingue del azar. | Que las hojas de sombra son realmente más grandes: la diferencia va en ese sentido, pero con tan pocos árboles no se puede confirmar. |
| Quitando un árbol a la vez, la diferencia va de 5,6 a 27,6 cm². | Que ese efecto se mantiene con más árboles de una misma especie. |
| Los árboles difieren mucho entre sí, incluso con la misma exposición (6 de 6 pares); cerca del 85 % de la variación está entre árboles. | Que las diferencias entre árboles se deben a la especie o a la edad: no se registraron. |
| Tratar las hojas como réplicas produce un p falsamente significativo. | Que la luz, y no la temperatura o la humedad, es la causa de cualquier diferencia. |
| Limitación | Consecuencia | Recomendación |
|---|---|---|
| Solo 6 árboles (3 de sol y 3 de sombra) | El menor p posible era 0,100; ninguna prueba podía ser significativa. Con la variación entre árboles observada harían falta unos 22 árboles por condición para detectar una diferencia de este tamaño | Medir muchos más árboles por exposición, de una misma especie |
| El resultado depende de pocos árboles | Al quitar un árbol, la diferencia sombra − sol va de 5,6 a 27,6 cm² (17,0 con todos) | Reportar siempre el análisis quitando un árbol a la vez |
| No se registró la especie | El efecto de la especie se mezcla con el de la exposición | Comparar sol y sombra dentro de una misma especie |
| Luz, temperatura y humedad cambian juntas entre ambientes | No se puede atribuir una diferencia a un solo factor (p. ej. bosquete con hojarasca frente a zona pavimentada) | Medir cada factor por separado |
| Exposición clasificada a ojo | Algunos árboles podrían estar mal clasificados | Medir la luz con luxómetro o una aplicación del celular |
| Estudio observacional | Solo se pueden describir asociaciones, no causas | Complementar con plántulas criadas a distintos niveles de sombra |
| Hojas elegidas por alcance | Posible sesgo hacia ramas bajas | Tomar hojas de alturas y orientaciones definidas de antemano |
| Imperfecciones en algunos trazos (hoja rota, papel arrugado) y un error de digitación | Cuatro hojas tienen observaciones de campo y un área quedó guardada como fecha; se conservaron porque sus valores son plausibles | Repetir el trazo dudoso y configurar la plantilla con formato numérico |
| Árboles muestreados en 2 fechas distintas (22/10/2026: G1_T3; 26/10/2026: G1_T1, G1_T2, G1_T4, G1_T5, G1_T6) | El área puede variar con la época y el estado hídrico | Muestrear todo el mismo día y repetir en temporada seca y de lluvias |
Givnish, T. J. (1988). Adaptation to sun and shade: a whole-plant perspective. Australian Journal of Plant Physiology, 15, 63–92.
Hurlbert, S. H. (1984). Pseudoreplication and the design of ecological field experiments. Ecological Monographs, 54(2), 187–211.
R Core Team. (2025). R: A language and environment for statistical computing. R Foundation for Statistical Computing.
Valladares, F., y Niinemets, Ü. (2008). Shade tolerance, a key plant feature of complex nature and consequences. Annual Review of Ecology, Evolution, and Systematics, 39, 237–257.
Wright, I. J., Dong, N., Maire, V., et al. (2017). Global climatic drivers of leaf size. Science, 357(6354), 917–921.
## R version 4.3.3 (2024-02-29 ucrt)
## Platform: x86_64-w64-mingw32/x64 (64-bit)
## Running under: Windows 11 x64 (build 26300)
##
## Matrix products: default
##
##
## locale:
## [1] LC_COLLATE=Spanish_Colombia.utf8 LC_CTYPE=Spanish_Colombia.utf8
## [3] LC_MONETARY=Spanish_Colombia.utf8 LC_NUMERIC=C
## [5] LC_TIME=Spanish_Colombia.utf8
##
## time zone: America/Bogota
## tzcode source: internal
##
## attached base packages:
## [1] stats graphics grDevices utils datasets methods base
##
## other attached packages:
## [1] kableExtra_1.4.1 ggplot2_4.0.2 readxl_1.4.5
##
## loaded via a namespace (and not attached):
## [1] gtable_0.3.6 jsonlite_2.0.0 dplyr_1.1.4
## [4] compiler_4.3.3 tidyselect_1.2.1 xml2_1.3.8
## [7] stringr_1.6.0 leaflet_2.2.3 jquerylib_0.1.4
## [10] systemfonts_1.2.2 scales_1.4.0 yaml_2.3.10
## [13] fastmap_1.2.0 mime_0.13 R6_2.6.1
## [16] labeling_0.4.3 generics_0.1.4 knitr_1.51
## [19] htmlwidgets_1.6.4 tibble_3.2.1 leaflet.providers_2.0.0
## [22] svglite_2.1.3 bslib_0.10.0 pillar_1.11.1
## [25] RColorBrewer_1.1-3 rlang_1.1.5 cachem_1.1.0
## [28] stringi_1.8.7 xfun_0.52 sass_0.4.9
## [31] S7_0.2.0 otel_0.2.0 viridisLite_0.4.3
## [34] cli_3.6.4 withr_3.0.2 magrittr_2.0.3
## [37] crosstalk_1.2.2 digest_0.6.37 grid_4.3.3
## [40] rstudioapi_0.18.0 lifecycle_1.0.5 vctrs_0.6.5
## [43] evaluate_1.0.5 glue_1.8.0 farver_2.1.2
## [46] cellranger_1.1.0 rmarkdown_2.30 tools_4.3.3
## [49] pkgconfig_2.0.3 htmltools_0.5.8.1