Esta sección presenta el estudio para alguien que no estuvo en el campo: qué se quería saber, dónde y cuándo se midió y con qué diseño. Todo lo que aparece aquí se calcula a partir de la misma base que se analiza más adelante.
# Lectura de la base original (todo como texto, para revisar cada valor tal como se escribió).
stopifnot(file.exists(params$archivo))
bruto <- readxl::read_excel(params$archivo, sheet = 1, skip = 1,
col_types = "text", .name_repair = "minimal")
bruto <- as.data.frame(bruto, stringsAsFactors = FALSE)
if (all(is.na(bruto[[1]]))) bruto <- bruto[, -1] # la plantilla deja la columna A vacía
stopifnot(ncol(bruto) >= 9)
bruto <- bruto[, 1:9]
names(bruto) <- c("grupo", "id_arbol", "lat", "lon", "n_hoja", "area_cm2",
"exposicion", "fecha", "observaciones") # nombres exigidos por la guía
bruto <- bruto[!is.na(bruto$id_arbol), ]
fechas <- as.Date(as.numeric(bruto$fecha), origin = "1899-12-30")
n_arbol <- length(unique(toupper(trimws(bruto$id_arbol))))
ficha <- data.frame(
Elemento = c("Pregunta", "Tipo de estudio", "Lugar", "Fecha de recolección",
"Factor de interés", "Variable respuesta", "Unidad experimental",
"Unidad de muestreo", "Tamaño de la muestra", "Instrumentos"),
Descripción = c(
"¿Cómo cambia el área de las hojas según el lugar donde crece el árbol?",
"Observacional y comparativo: no se asignó sol o sombra; se eligieron árboles que ya estaban en cada condición",
"Campus de la Universidad del Valle, sede Meléndez, Cali (Valle del Cauca, Colombia)",
if (min(fechas) == max(fechas)) format(min(fechas), "%d/%m/%Y")
else paste(format(min(fechas), "%d/%m/%Y"), "a", format(max(fechas), "%d/%m/%Y")),
"Exposición del árbol a la luz: sol (luz directa la mayor parte del día), semisombra 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("Grupo ", params$grupo, ": ", n_arbol, " árboles × 10 hojas; ", nrow(bruto), " filas registradas"),
"Celular (cámara y GPS), papel blanco, lápiz y regla, y un script de Google Colab que calcula el área a partir de la foto"))
tabla(ficha, align = "ll")
| Elemento | Descripción |
|---|---|
| Pregunta | ¿Cómo cambia el área de las hojas según el lugar donde crece el árbol? |
| Tipo de estudio | Observacional y comparativo: no se asignó sol o sombra; se eligieron árboles que ya estaban en cada condición |
| Lugar | Campus de la Universidad del Valle, sede Meléndez, Cali (Valle del Cauca, Colombia) |
| Fecha de recolección | 22/10/2026 |
| Factor de interés | Exposición del árbol a la luz: sol (luz directa la mayor parte del día), semisombra 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 | Grupo 1: 5 árboles × 10 hojas; 50 filas registradas |
| Instrumentos | Celular (cámara y GPS), papel blanco, lápiz y regla, y un script de Google Colab que calcula el área a partir de la foto |
El estudio se hizo en el campus Meléndez de la Universidad del Valle, en Cali, en el valle geográfico del río Cauca: una zona tropical a unos 1 000 m de altitud, con temperaturas medias cercanas a los 24 °C y dos temporadas de lluvia al año (valores aproximados). Dentro del campus hay árboles en zonas abiertas, junto a edificios y vías pavimentadas, y entre otros árboles, lo que da un gradiente natural de luz. Las fotos tomadas el día del muestreo muestran el cielo nublado.
El protocolo, en resumen:
Cada punto es un árbol. Al hacer clic en un punto se ve su foto y sus datos; arriba a la derecha se puede cambiar el fondo (satélite o calles) y ocultar categorías. Con la vista de satélite se comprueba 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 hoja de cada árbol
# (la revisión completa de las coordenadas está en la sección 4).
norm_expo <- function(x) {
x <- tolower(trimws(x))
x[x %in% c("sol", "soleado", "pleno sol")] <- "sol"
x[x %in% c("sombra", "sombreado")] <- "sombra"
x[x %in% c("semi", "semisombra", "semi-sombra", "semi sombra")] <- "semi"
x
}
b <- bruto
b$id_arbol <- toupper(trimws(b$id_arbol))
b$exposicion <- norm_expo(b$exposicion)
b$lat_dec <- dms_a_dec(b$lat)
b$lon_dec <- dms_a_dec(b$lon)
sitios <- do.call(rbind, lapply(split(b, b$id_arbol), function(d)
data.frame(grupo = as.integer(as.numeric(d$grupo[1])), id_arbol = d$id_arbol[1],
lat = d$lat_dec[1], lon = d$lon_dec[1], exposicion = d$exposicion[1],
hojas = nrow(d), stringsAsFactors = FALSE)))
sitios <- sitios[order(as.integer(sub(".*_T", "", sitios$id_arbol))), ]
rownames(sitios) <- NULL
# 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: 5
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, 5), "; ", fmt(s$lon, 5))
}, character(1))
presentes <- intersect(niveles_expo, unique(sitios$exposicion))
if (requireNamespace("leaflet", quietly = TRUE)) {
library(leaflet)
mapa <- leaflet() %>%
addProviderTiles(providers$Esri.WorldImagery, group = "Satélite") %>%
addTiles(group = "Calles")
for (e in presentes) {
d <- sitios[sitios$exposicion == e, ]
mapa <- addCircleMarkers(mapa, data = d, lng = ~lon, lat = ~lat, radius = 9, weight = 2,
color = "white", fillColor = col_expo[[e]], fillOpacity = 0.95,
label = ~id_arbol, popup = ~popup,
popupOptions = popupOptions(maxWidth = 240), group = e)
}
mapa %>%
addLayersControl(baseGroups = c("Satélite", "Calles"), overlayGroups = presentes,
options = layersControlOptions(collapsed = FALSE)) %>%
addLegend("bottomright", colors = unname(col_expo[presentes]), labels = presentes,
title = "Exposición", opacity = 1) %>%
addScaleBar("bottomleft")
} else {
cat("El paquete 'leaflet' no está instalado: se muestra un mapa simple.",
"Para el mapa interactivo ejecuta install.packages('leaflet').\n")
plot(sitios$lon, sitios$lat, pch = 21, cex = 3, bg = col_expo[sitios$exposicion],
asp = 1 / cos(mean(sitios$lat) * pi / 180), xlab = "Longitud", ylab = "Latitud",
main = "Ubicación de los árboles")
text(sitios$lon, sitios$lat, sub(".*_", "", sitios$id_arbol), cex = 0.8)
legend("topright", legend = presentes, pt.bg = col_expo[presentes], pch = 21, bty = "n")
}
Una foto de cada árbol sirve para comprobar después la clasificación en sol, semisombra y sombra, y para que el lector se imagine el sitio.
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")
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, y los árboles de semisombra tendrían valores intermedios. Si es cierta, la diferencia debería verse en los promedios por árbol y no depender de un solo árbol.
Diseño. El grupo 1 midió 5 árboles y 10 hojas por árbol (50 hojas). 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, no. Además de sol y sombra, el grupo usó una categoría intermedia, semi (semisombra), que no estaba prevista en la guía y se trata como categoría propia. 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 calculó el área en cm². Los datos se anotaron en la plantilla de la guía: una fila por hoja, con grupo, árbol, coordenadas, número de hoja, área, exposición, fecha y observaciones.
Análisis:
Antes de calcular nada hay que mirar la base. Las revisiones se hacen
sobre una copia (datos): la base original
(bruto) no se toca.
head(bruto, 6) # las primeras filas, tal como vienen en el Excel
## grupo id_arbol lat lon n_hoja area_cm2 exposicion fecha
## 1 1 G1_T1 N 3°22'35.27 W 76°31'51.26 1 27.50 semi 46317
## 2 1 G1_T1 N 3°22'35.27 W 76°31'51.26 2 22.07 semi 46317
## 3 1 G1_T1 N 3°22'35.27 W 76°31'51.26 3 35.89 semi 46317
## 4 1 G1_T1 N 3°22'35.27 W 76°31'51.26 4 38.70 semi 46317
## 5 1 G1_T1 N 3°22'35.27 W 76°31'51.26 5 39.41 semi 46317
## 6 1 G1_T1 N 3°22'35.27 W 76°31'51.26 6 41.87 semi 46317
## observaciones
## 1 <NA>
## 2 <NA>
## 3 <NA>
## 4 <NA>
## 5 <NA>
## 6 <NA>
La base tiene 50 filas y 5 árboles: 5 árboles × 10 hojas = 50 filas, como pide la guía.
datos <- bruto
# 1. Identificador (formato G#_T#) y grupo
datos$id_arbol <- toupper(trimws(datos$id_arbol))
datos$grupo <- as.integer(as.numeric(datos$grupo))
datos$n_hoja <- as.integer(as.numeric(datos$n_hoja))
id_mal <- unique(datos$id_arbol[!grepl("^G[0-9]+_T[0-9]+$", datos$id_arbol)])
id_mal # debe estar vacío
## character(0)
stopifnot(all(as.integer(sub("^G([0-9]+)_.*", "\\1", datos$id_arbol)) == datos$grupo))
# 2. Área: número mayor que 0 en todas las filas
datos$area_cm2 <- as.numeric(gsub(",", ".", trimws(datos$area_cm2)))
datos[is.na(datos$area_cm2) | datos$area_cm2 <= 0, c("id_arbol", "n_hoja", "area_cm2")]
## [1] id_arbol n_hoja area_cm2
## <0 rows> (o 0- extensión row.names)
# 3. Exposición: una sola por árbol
datos$exposicion <- norm_expo(datos$exposicion)
table(datos$exposicion)
##
## semi sol sombra
## 20 20 10
n_expo <- tapply(datos$exposicion, datos$id_arbol, function(x) length(unique(x)))
names(n_expo)[n_expo > 1] # debe estar vacío
## character(0)
# 4. Filas repetidas, número de hoja y hojas por árbol
repetida <- duplicated(datos[, c("id_arbol", "n_hoja")])
sum(repetida)
## [1] 0
range(datos$n_hoja)
## [1] 1 10
n_por_arbol <- table(datos$id_arbol)
n_por_arbol
##
## G1_T1 G1_T2 G1_T3 G1_T4 G1_T5
## 10 10 10 10 10
# 5. Fecha de recolección
datos$fecha <- as.Date(as.numeric(datos$fecha), origin = "1899-12-30")
table(datos$fecha)
##
## 2026-10-22
## 50
# 6. Observaciones anotadas por los estudiantes
n_obs <- sum(!is.na(datos$observaciones))
n_obs
## [1] 0
No hay filas repetidas, todas las áreas son mayores que 0, el número de hoja va de 1 a 10 en cada árbol y cada árbol tiene una sola exposición. Todas las hojas se midieron el mismo día. La columna de observaciones está vacía (0 anotaciones): no se registró ninguna hoja dañada ni trazo dudoso, así que no hay motivos documentados para excluir hojas.
La guía pide las coordenadas en grados decimales, y la base las trae
en grados-minutos-segundos con la letra del hemisferio (por ejemplo
N 3°22'35.27). Se convierten a grados decimales (latitud
norte positiva, longitud oeste negativa). Como las 10 hojas de un árbol
comparten el mismo punto, se usa un solo punto por árbol.
head(unique(bruto[, c("id_arbol", "lat", "lon")]), 6) # como venían
## id_arbol lat lon
## 1 G1_T1 N 3°22'35.27 W 76°31'51.26
## 11 G1_T2 N 3°22'36.42 W 76°31'52.92
## 21 G1_T3 N 3°22'37.24 W 76°31'53.23
## 31 G1_T4 N 3°22'38.14 W 76°31'54.25
## 41 G1_T5 N 3°22'38.26 W 76°31'52.51
datos$lat <- dms_a_dec(bruto$lat)
datos$lon <- dms_a_dec(bruto$lon)
primera <- !duplicated(datos$id_arbol)
arb <- datos[primera, c("id_arbol", "grupo", "exposicion", "lat", "lon")]
arb <- arb[order(as.integer(sub(".*_T", "", arb$id_arbol))), ]
rownames(arb) <- NULL
tabla(transform(arb, lat = fmt(lat, 5), lon = fmt(lon, 5)), row.names = FALSE,
align = "lllrr", caption = "Coordenadas en grados decimales (un punto por árbol).")
| id_arbol | grupo | exposicion | lat | lon |
|---|---|---|---|---|
| G1_T1 | 1 | semi | 3,37646 | -76,53091 |
| G1_T2 | 1 | sombra | 3,37678 | -76,53137 |
| G1_T3 | 1 | semi | 3,37701 | -76,53145 |
| G1_T4 | 1 | sol | 3,37726 | -76,53174 |
| G1_T5 | 1 | sol | 3,37729 | -76,53125 |
# Distancia en metros entre los árboles (sirve para detectar una coordenada mal digitada)
D <- outer(seq_len(nrow(arb)), seq_len(nrow(arb)),
Vectorize(function(i, j) distancia_m(arb$lat[i], arb$lon[i], arb$lat[j], arb$lon[j])))
dimnames(D) <- list(arb$id_arbol, arb$id_arbol)
tabla(round(D), caption = "Distancia en metros entre los árboles del grupo.")
| G1_T1 | G1_T2 | G1_T3 | G1_T4 | G1_T5 | |
|---|---|---|---|---|---|
| G1_T1 | 0 | 62 | 86 | 128 | 100 |
| G1_T2 | 62 | 0 | 27 | 67 | 58 |
| G1_T3 | 86 | 27 | 0 | 42 | 39 |
| G1_T4 | 128 | 67 | 42 | 0 | 54 |
| G1_T5 | 100 | 58 | 39 | 54 | 0 |
# Coordenada única por árbol en la base de trabajo
datos$lat <- arb$lat[match(datos$id_arbol, arb$id_arbol)]
datos$lon <- arb$lon[match(datos$id_arbol, arb$id_arbol)]
Los cinco árboles quedan a menos de 130 m unos de otros, dentro del campus (se ve en el mapa de la sección 1).
n_sol <- sum(arb$exposicion == "sol")
n_sombra <- sum(arb$exposicion == "sombra")
n_semi <- sum(arb$exposicion == "semi")
cumple <- data.frame(
Requisito = c("5 árboles por grupo", "10 hojas por árbol", "Al menos 2 árboles a pleno sol",
"Al menos 2 árboles a la sombra", "Exposición solo `sol` o `sombra`",
"Coordenadas en grados decimales", "Área > 0 en todas las filas", "Hojas medidas el mismo día"),
Resultado = c(paste(nrow(arb), "árboles"),
ifelse(all(n_por_arbol == 10), "sí, 10 en todos", "no en todos"),
paste(n_sol, "árboles"), paste(n_sombra, "árbol(es)"),
paste0("no: hay ", n_semi, " árboles `semi`"),
"venían en grados-minutos-segundos (convertidas)",
ifelse(all(datos$area_cm2 > 0, na.rm = TRUE), "sí", "no"),
ifelse(length(unique(datos$fecha)) == 1, paste("sí:", format(unique(datos$fecha), "%d/%m/%Y")), "no")),
Cumple = c(nrow(arb) == 5, all(n_por_arbol == 10), n_sol >= 2, n_sombra >= 2, n_semi == 0,
TRUE, all(datos$area_cm2 > 0, na.rm = TRUE), length(unique(datos$fecha)) == 1))
cumple$Cumple <- ifelse(cumple$Cumple, "✔", "✘")
tabla(cumple, row.names = FALSE, align = "llc", caption = "Cumplimiento de la guía por parte de la base.")
| Requisito | Resultado | Cumple |
|---|---|---|
| 5 árboles por grupo | 5 árboles | ✔ |
| 10 hojas por árbol | sí, 10 en todos | ✔ |
| Al menos 2 árboles a pleno sol | 2 árboles | ✔ |
| Al menos 2 árboles a la sombra | 1 árbol(es) | ✘ |
Exposición solo sol o
sombra |
no: hay 2 árboles semi |
✘ |
| Coordenadas en grados decimales | venían en grados-minutos-segundos (convertidas) | ✔ |
| Área > 0 en todas las filas | sí | ✔ |
| Hojas medidas el mismo día | sí: 22/10/2026 | ✔ |
La guía solo admite sol o sombra, se usó
además semi (semisombra). Se conserva como categoría
propia.
limpios <- datos[order(datos$grupo, as.integer(sub(".*_T", "", datos$id_arbol)), datos$n_hoja), ]
cat("Filas:", nrow(limpios), "\n", "Árboles:", length(unique(limpios$id_arbol)), "\n")
## Filas: 50
## Árboles: 5
if (params$guardar_limpios) {
salida <- limpios[, c("grupo", "id_arbol", "lat", "lon", "n_hoja", "area_cm2", "exposicion",
"fecha", "observaciones")]
write.csv(salida, "datos_limpios_area_foliar.csv", row.names = FALSE, na = "", fileEncoding = "UTF-8")
}
La base revisada (datos_limpios_area_foliar.csv, con las
columnas de la plantilla y las coordenadas en grados decimales) es parte
de lo que se entrega.
Desde aquí se analiza el grupo, como en la guía: 5 árboles y 50 hojas.
g <- params$grupo
dg <- limpios[limpios$grupo == g, ]
stopifnot(nrow(dg) > 0)
dg$exposicion <- factor(dg$exposicion, levels = niveles_expo)
resumen <- do.call(rbind, lapply(split(dg, dg$id_arbol), function(d) {
data.frame(arbol = d$id_arbol[1], exposicion = as.character(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))
}))
resumen <- resumen[order(as.integer(sub(".*_T", "", resumen$arbol))), ]
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.")
| Árbol | Exposición | n | Media | Mediana | DE | CV (%) | Mín. | Máx. |
|---|---|---|---|---|---|---|---|---|
| G1_T1 | semi | 10 | 43,5 | 40,6 | 13,9 | 32,0 | 22,1 | 64,2 |
| G1_T2 | sombra | 10 | 57,8 | 52,6 | 23,4 | 40,5 | 30,8 | 102,0 |
| G1_T3 | semi | 10 | 70,9 | 76,6 | 25,4 | 35,9 | 17,7 | 97,6 |
| G1_T4 | sol | 10 | 63,2 | 67,5 | 15,7 | 24,9 | 35,0 | 82,9 |
| G1_T5 | sol | 10 | 158,1 | 120,6 | 93,6 | 59,2 | 59,7 | 319,8 |
Dos cosas para mirar en esta tabla.
La diferencia entre árboles: las medias van de 43,5 a 158,1 cm², y el árbol con la media más alta (G1_T5) tiene hojas entre 2,2 y 3,6 veces más grandes, en promedio, que los otros cuatro.
El CV, que mide cuánto varían las hojas de un árbol respecto a su propia media, va de 25 % a 59 %: G1_T5 es el más variable (hojas de 59,7 a 319,8 cm²) y G1_T4 el más homogéneo. En G1_T5 la media (158,1) supera con claridad a la mediana (120,6), señal de que unas pocas hojas muy grandes jalan el promedio hacia arriba.
dg$id_arbol <- factor(dg$id_arbol, levels = resumen$arbol)
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 = paste("Grupo", g, "- á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("topleft", legend = presentes, fill = adjustcolor(col_expo[presentes], 0.55), bty = "n")
Área foliar de cada hoja, por árbol. Color = exposición.
El gráfico muestra lo mismo que la tabla: las cajas de los árboles distintos de G1_T5 se solapan bastante entre sí (sus medianas van de 40,6 a 76,6 cm²), mientras que la caja de G1_T5 queda casi toda por encima y es la más ancha. Tampoco los árboles de la misma exposición se parecen necesariamente: los dos árboles de sol (G1_T4 y G1_T5) tienen medianas muy distintas (67,5 y 120,6 cm²).
Criterio. Se revisa cada hoja con dos preguntas, en este orden:
fuera <- do.call(rbind, lapply(split(dg, dg$id_arbol), function(d) {
d[d$area_cm2 %in% boxplot.stats(d$area_cm2)$out,
c("id_arbol", "n_hoja", "area_cm2", "observaciones")]
}))
if (is.null(fuera) || nrow(fuera) == 0) {
cat("Ninguna hoja queda fuera de los límites de Tukey de su árbol.\n")
} else {
tabla(fuera, row.names = FALSE, digits = 2,
caption = "Hojas fuera de los límites de Tukey de su árbol.")
}
## Ninguna hoja queda fuera de los límites de Tukey de su árbol.
sens <- do.call(rbind, lapply(split(dg, dg$id_arbol), function(d) {
out <- boxplot.stats(d$area_cm2)$out
data.frame(arbol = d$id_arbol[1], exposicion = as.character(d$exposicion[1]),
media = mean(d$area_cm2), mediana = median(d$area_cm2),
media_sin_atipicas = mean(d$area_cm2[!d$area_cm2 %in% out]))
}))
sens <- sens[order(as.integer(sub(".*_T", "", sens$arbol))), ]
tabla(sens, row.names = FALSE, digits = 1,
col.names = c("Árbol", "Exposición", "Media (todas)", "Mediana", "Media sin atípicas"),
caption = "Sensibilidad del resumen de cada árbol (cm²) a las hojas atípicas.")
| Árbol | Exposición | Media (todas) | Mediana | Media sin atípicas |
|---|---|---|---|---|
| G1_T1 | semi | 43,5 | 40,6 | 43,5 |
| G1_T2 | sombra | 57,8 | 52,6 | 57,8 |
| G1_T3 | semi | 70,9 | 76,6 | 70,9 |
| G1_T4 | sol | 63,2 | 67,5 | 63,2 |
| G1_T5 | sol | 158,1 | 120,6 | 158,1 |
Resultado del criterio. Ninguna hoja queda fuera de los límites de Tukey de su árbol, ni siquiera las más grandes de G1_T5 (hasta 319,8 cm²): como ese árbol es tan variable, sus límites son muy amplios. Respecto a la primera pregunta (causa de medición), no hay errores de medición lo mas probable es que se hayan tomado hojas que no estaban completamente desarrolladas por error; las hojas del T1_G5 corresponden a un arbol de Noni ( Morinda citrifolia L. ), el cual naturalmente tiene hojas visiblemente mucho más grandes que el resto de los arboles del experimento. Decisión: se conservan todas las hojas.
dg$expo <- factor(dg$exposicion, levels = presentes)
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,
ylim = range(dg$area_cm2), # que el eje incluya todas las hojas, también las más grandes
col = adjustcolor(col_expo[presentes], 0.5), border = "grey25",
xlab = "Exposición", ylab = expression("Área foliar (cm"^2*")"),
main = paste("Grupo", g, "- hojas por exposición"))
set.seed(2) # para que la separación horizontal de los puntos sea siempre la misma
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 por exposición. Cada símbolo es un árbol distinto.
Este gráfico es descriptivo: muestra la forma de las nubes de hojas de cada categoría. 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. Además, sombra está representada por un solo árbol, así que su caja describe a ese árbol, no a “la sombra” en general.
Las medianas de las hojas son sol = 76,2 cm²; semi = 54,4 cm²; sombra = 52,6 cm². El orden (sol por encima) es el contrario al que planteaba la hipótesis, pero la caja de sol está dominada por las hojas de G1_T5, y semisombra y sombra son casi iguales entre sí.
Lo que se asignó a sol, semisombra o 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. La pregunta de la guía es cuántos árboles hay realmente por categoría:
medias_g <- aggregate(area_cm2 ~ id_arbol + exposicion, data = dg, FUN = mean)
medias_g$exposicion <- factor(medias_g$exposicion, levels = niveles_expo)
medias_g <- medias_g[order(medias_g$exposicion, medias_g$id_arbol), ]
medias_g <- medias_g[medias_g$area_cm2 > 0 & !is.na(medias_g$area_cm2), ]
tabla(medias_g, row.names = FALSE, digits = 1, col.names = c("Árbol", "Exposición", "Media del árbol (cm²)"))
| Árbol | Exposición | Media del árbol (cm²) |
|---|---|---|
| G1_T4 | sol | 63,2 |
| G1_T5 | sol | 158,1 |
| G1_T1 | semi | 43,5 |
| G1_T3 | semi | 70,9 |
| G1_T2 | sombra | 57,8 |
por_expo <- do.call(rbind, lapply(split(medias_g, droplevels(medias_g$exposicion)), function(d)
data.frame(exposicion = as.character(d$exposicion[1]), arboles = nrow(d), hojas = 10 * nrow(d),
media = mean(d$area_cm2), de = if (nrow(d) > 1) sd(d$area_cm2) else NA_real_,
minimo = min(d$area_cm2), maximo = max(d$area_cm2))))
tabla(por_expo, row.names = FALSE, digits = c(0, 0, 0, 1, 1, 1, 1),
col.names = c("Exposición", "Árboles", "Hojas", "Media de los árboles", "DE entre árboles", "Mín.", "Máx."),
caption = "Árboles realmente independientes por categoría de exposición (unidad = el árbol).")
| Exposición | Árboles | Hojas | Media de los árboles | DE entre árboles | Mín. | Máx. |
|---|---|---|---|---|---|---|
| sol | 2 | 20 | 110,6 | 67,1 | 63,2 | 158,1 |
| semi | 2 | 20 | 57,2 | 19,3 | 43,5 | 70,9 |
| sombra | 1 | 10 | 57,8 | — | 57,8 | 57,8 |
mg <- medias_g
mg$x <- match(as.character(mg$exposicion), presentes)
set.seed(3)
par(mar = c(4.5, 4.8, 3, 1))
plot(mg$x + runif(nrow(mg), -0.08, 0.08), mg$area_cm2, pch = 21, bg = col_expo[as.character(mg$exposicion)],
cex = 3, xaxt = "n", xlim = c(0.5, length(presentes) + 0.5), las = 1,
ylim = c(0, max(mg$area_cm2) * 1.1), xlab = "Exposición",
ylab = expression("Área foliar media del árbol (cm"^2*")"), main = "Un punto por árbol")
axis(1, at = seq_along(presentes), labels = presentes)
text(mg$x + 0.22, mg$area_cm2, sub(".*_", "", mg$id_arbol), cex = 0.9)
Cada punto es un árbol (promedio de sus 10 hojas). Es la comparación con la que se puede razonar: 5 puntos.
En total hay 5 datos independientes, no 50: 2 árboles de sol, 2 de semisombra y 1 de sombra. Con un solo árbol de sombra no se puede estimar la variación dentro de esa categoría (la DE queda sin calcular), y la comparación sol–sombra de la guía no puede hacerse como prueba estadística.
Con tan pocos árboles se puede hacer algo mejor que suponer una distribución: una prueba de permutación exacta. Como solo hay un árbol de sombra, la comparación se hace entre sol (2 árboles) y sin sol pleno (semisombra y sombra juntas, 3 árboles). Si la exposición no importara, cualquiera de las formas de repartir las etiquetas entre los árboles sería igual de probable.
medias_g$sin_sol <- medias_g$exposicion != "sol"
y <- medias_g$area_cm2
es_ss <- medias_g$sin_sol
n_ss <- sum(es_ss)
repartos <- combn(length(y), n_ss) # todas las formas de elegir los árboles "sin sol pleno"
dif_perm <- apply(repartos, 2, function(i) mean(y[i]) - mean(y[-i]))
dif_obs <- mean(y[es_ss]) - mean(y[!es_ss])
p_perm <- mean(abs(dif_perm) >= abs(dif_obs) - 1e-9)
p_min <- 1 / ncol(repartos)
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 (sin sol pleno - sol) en 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)
Hay 10 repartos posibles. La diferencia observada entre los promedios de árbol es de -53,2 cm² (sin sol pleno − sol) y el valor de p exacto es 0,200. El valor más pequeño que cualquier resultado podría dar con 3 y 2 árboles es 0,100: con 5 árboles el diseño no puede dar resultados “significativos” al 5 %, sean cuales sean los datos.
dg$sin_sol <- dg$exposicion != "sol"
t_hojas <- t.test(area_cm2 ~ sin_sol, data = dg) # trata las hojas como independientes
p_hojas <- t_hojas$p.value
dif_hojas <- unname(t_hojas$estimate[2] - t_hojas$estimate[1])
comparacion_g <- data.frame(
Análisis = c("Hojas como datos independientes (incorrecto)", "Un promedio por árbol (permutación exacta)"),
n_sol = c(sum(!dg$sin_sol), sum(!es_ss)), n_sin_sol = c(sum(dg$sin_sol), n_ss),
Diferencia = c(fmt(dif_hojas), fmt(dif_obs)), p = c(fmt_p(p_hojas), fmt_p(p_perm)))
tabla(comparacion_g, col.names = c("Análisis", "n sol", "n sin sol pleno", "Diferencia (cm²)", "p"), align = "lrrrr")
| Análisis | n sol | n sin sol pleno | Diferencia (cm²) | p |
|---|---|---|---|---|
| Hojas como datos independientes (incorrecto) | 20 | 30 | -53,2 | 0,010 |
| Un promedio por árbol (permutación exacta) | 2 | 3 | -53,2 | 0,200 |
Tratar las hojas como independientes multiplica por 10 el tamaño de la muestra (50 hojas en lugar de 5 árboles) y cambia el p de 0,200 a 0,010. Esa precisión es falsa: se llama pseudorreplicación. Las hojas de un árbol son 10 mediciones de lo mismo, no 10 árboles.
Con 5 árboles, quitar uno cambia mucho el panorama. Esta tabla repite la diferencia entre sol y sin sol pleno quitando cada árbol por turno:
loo <- do.call(rbind, lapply(seq_len(nrow(medias_g)), function(i) {
d <- medias_g[-i, ]
data.frame(sin_el_arbol = medias_g$id_arbol[i], exposicion = as.character(medias_g$exposicion[i]),
n_sol = sum(!d$sin_sol), n_sin_sol = sum(d$sin_sol),
dif = if (any(!d$sin_sol) && any(d$sin_sol))
mean(d$area_cm2[d$sin_sol]) - mean(d$area_cm2[!d$sin_sol]) else NA_real_)
}))
tabla(loo, row.names = FALSE, digits = 1,
col.names = c("Árbol que se quita", "Su exposición", "Árboles de sol", "Árboles sin sol pleno", "Diferencia (sin sol pleno − sol), cm²"),
caption = "Sensibilidad de la diferencia al quitar un árbol.")
| Árbol que se quita | Su exposición | Árboles de sol | Árboles sin sol pleno | Diferencia (sin sol pleno − sol), cm² |
|---|---|---|---|---|
| G1_T4 | sol | 1 | 3 | -100,7 |
| G1_T5 | sol | 1 | 3 | -5,7 |
| G1_T1 | semi | 2 | 2 | -46,3 |
| G1_T3 | semi | 2 | 2 | -60,0 |
| G1_T2 | sombra | 2 | 2 | -53,4 |
dif_sin_max <- loo$dif[loo$sin_el_arbol == arbol_max]
Sin G1_T5 la diferencia pasa de -53,2 a -5,7 cm²: casi desaparece. Y si se quita G1_T4 o G1_T5, queda un solo árbol de sol y la comparación depende de un único punto. El resultado entero se apoya en G1_T5.
Hasta aquí se comparó con promedios por árbol y una prueba de permutación. El análisis de varianza (ANOVA) permite usar la estructura completa del diseño. En este informe se usan dos modelos, cada uno para una pregunta distinta:
| Pregunta | Herramienta | Después del ANOVA |
|---|---|---|
| ¿Difieren entre sí los árboles del grupo? | 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 |
La actividad es un estudio observacional con estructura jerárquica. Cada elemento del diseño tiene un papel:
Con esos elementos, el modelo es
log(área) ~ exposición + árbol(exposición) + error de hoja
El término árbol(exposición) es el error experimental contra el que se prueba la exposición. Si se ignorara y se usara el error de las hojas, se caería en la pseudorreplicación de la sección anterior. El diseño está desbalanceado y es muy pequeño: 2 árboles de sol, 2 de semisombra y 1 de sombra.
¿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, “una hoja es 1,5 veces mayor”).
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,9874797 0,7911316
Entre los 5 árboles, la correlación entre la desviación estándar y la media es 0,99 en la escala original y 0,79 con el logaritmo. (Con solo 5 puntos esta correlación es una pista, no una prueba.)
Se compara el log del área de las hojas entre los 5 árboles del grupo. Aquí las hojas sí son réplicas (de cada árbol), y la pregunta es por esos árboles concretos, no por la exposición.
dg$id_arbol <- factor(dg$id_arbol)
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 del grupo.")
| Fuente | gl | SC | CM | F | p |
|---|---|---|---|---|---|
| Entre árboles | 4 | 7,693 | 1,923 | 9,81 | < 0,001 |
| Dentro de árboles (hojas) | 45 | 8,821 | 0,196 |
Hay diferencias entre árboles: F(4; 45) = 9,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).
par(mfrow = c(1, 1))
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
ow1 <- oneway.test(log(area_cm2) ~ id_arbol, data = dg) # ANOVA de Welch: no exige varianzas iguales
c(shapiro_p = shap1$p.value, fligner_p = flig1$p.value, welch_p = ow1$p.value, kruskal_p = kw1$p.value)
## shapiro_p fligner_p welch_p kruskal_p
## 0,5302701978 0,0542976225 0,0009155429 0,0002241962
La prueba de Shapiro-Wilk (normalidad de los residuos) da p = 0,530 y la de Fligner-Killeen (igualdad de varianzas entre árboles) p = 0,054. Un p grande significa que no hay evidencia contra el supuesto; un p pequeño, que sí la hay. Aquí no hay evidencia contra la normalidad de los residuos, pero la igualdad de varianzas queda en el límite: en escala logarítmica los árboles G1_T3 y G1_T5 varían más que G1_T1 y G1_T4. Por eso se comprueba la conclusión con dos alternativas que no exigen esos supuestos: el ANOVA de Welch (no pide varianzas iguales; p < 0,001) y Kruskal-Wallis (no pide normalidad; p < 0,001). Las tres pruebas coinciden en que hay diferencias entre árboles.
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 del 40 %. La prueba de Tukey HSD compara todos los pares y controla ese error.
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 <- 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_sig <- sum(tk_tab$p_aj < 0.05)
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"),
ifelse(misma_expo, "misma", "distinta"))
tabla(tabla_tk, col.names = c("Comparación (A / B)", "Razón A / B", "IC 95 % (Tukey)", "p ajustado", "Difieren (5 %)", "Exposición"),
align = "lrrrrc", caption = "Comparaciones de Tukey entre árboles (escala logarítmica, mostradas como razones).")
| Comparación (A / B) | Razón A / B | IC 95 % (Tukey) | p ajustado | Difieren (5 %) | Exposición |
|---|---|---|---|---|---|
| G1_T5 / G1_T1 | 3,25 | [1,85; 5,70] | < 0,001 | sí | distinta |
| G1_T5 / G1_T2 | 2,49 | [1,42; 4,37] | < 0,001 | sí | distinta |
| G1_T5 / G1_T4 | 2,20 | [1,25; 3,86] | 0,002 | sí | misma |
| G1_T5 / G1_T3 | 2,08 | [1,18; 3,65] | 0,005 | sí | distinta |
| G1_T3 / G1_T1 | 1,56 | [0,89; 2,74] | 0,181 | no | misma |
| G1_T4 / G1_T1 | 1,48 | [0,84; 2,59] | 0,299 | no | distinta |
| G1_T2 / G1_T1 | 1,30 | [0,74; 2,29] | 0,672 | no | distinta |
| G1_T3 / G1_T2 | 1,20 | [0,68; 2,10] | 0,890 | no | distinta |
| G1_T4 / G1_T2 | 1,13 | [0,65; 1,99] | 0,969 | no | distinta |
| G1_T4 / G1_T3 | 0,95 | [0,54; 1,66] | 0,999 | no | distinta |
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.
En 4 de las 10 comparaciones los árboles difieren de forma detectable (en naranja), y todas incluyen a G1_T5, cuyas hojas son entre 2,1 y 3,2 veces mayores que las de cada uno de los otros cuatro. Entre esos otros cuatro no se detectó ninguna diferencia (todos los p ajustados son ≥ 0,18), aunque sean de exposiciones distintas.
Al cruzar esto con la exposición de cada árbol, 1 de las 4 diferencias es entre árboles de la misma exposición (G1_T5 frente a G1_T4, ambos de sol). Es decir, las hojas grandes de G1_T5 no se explican solo por estar al sol: G1_T4, que también es de sol, tiene hojas del mismo tamaño que las de los árboles de semisombra y sombra. Esto ilustra por qué el árbol, y no solo la exposición, es lo que hay que mirar.
Las hojas están dentro de árboles y los árboles dentro de una exposición: un diseño anidado. El efecto de la exposición se prueba contra la variación entre árboles, no contra la variación entre hojas.
dg$arbol <- factor(dg$id_arbol)
dg$expo2 <- factor(dg$exposicion, levels = presentes)
m_anid <- lm(log(area_cm2) ~ expo2 + arbol, data = dg) # el árbol está dentro de la exposición (cada árbol tiene una sola)
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),
F = c(fmt(F_expo, 2), fmt(F_arbol, 2), ""),
p = c(fmt_p(p_expo), fmt_p(p_arbol), ""),
`Se prueba contra` = c("Árbol (dentro de exposición)", "Hojas (residual)", ""),
check.names = FALSE)
tabla(tab_an, row.names = FALSE, align = "lrrrrrl",
caption = "ANOVA anidado sobre log(área): la exposición se prueba con los árboles como error.")
| Fuente | gl | SC | CM | F | p | Se prueba contra |
|---|---|---|---|---|---|---|
| Exposición | 2 | 3,599 | 1,799 | 0,88 | 0,532 | Árbol (dentro de exposición) |
| Árbol (dentro de exposición) | 2 | 4,095 | 2,047 | 10,44 | < 0,001 | Hojas (residual) |
| Hojas (residual) | 45 | 8,821 | 0,196 |
# Componentes de varianza (10 hojas por árbol)
n_h <- 10
s2_hoja <- a_an$CM[3]
s2_arbol <- max((a_an$CM[2] - a_an$CM[3]) / n_h, 0)
pct_arbol <- 100 * s2_arbol / (s2_arbol + s2_hoja)
r2_arbol <- summary(lm(log(area_cm2) ~ arbol, data = dg))$r.squared
La exposición no tiene un efecto detectable sobre el log del área: F(2; 2) = 0,88, p = 0,532. Hay que leerlo con cuidado: el error contra el que se prueba tiene solo 2 grados de libertad (5 árboles menos 3 exposiciones), así que la prueba tiene muy poca potencia y solo detectaría diferencias enormes. No demuestra que la exposición no importe; demuestra que con este diseño no se puede saber.
Lo que sí es claro es que los árboles dentro de una misma exposición difieren entre sí (F = 10,4, p < 0,001). Según los componentes de varianza, cerca del 49 % de la variación del log del área está entre árboles de una misma exposición, y la identidad del árbol por sí sola explica el 47 % de la variación total. Una parte muy grande de la variación (cerca de la mitad) está entre árboles y no entre hojas: medir más hojas por árbol ayudaría poco para comparar exposiciones; haría falta medir más árboles.
Qué encontramos. Con el árbol como unidad de análisis, los árboles de sol tuvieron hojas de mayor área media (110,6 cm², 2 árboles) que los de semisombra (57,2 cm², 2 árboles) y que el de sombra (57,8 cm², 1 árbol). Es el sentido contrario al de la hipótesis, que esperaba hojas más grandes a la sombra. Pero este resultado es frágil, por tres razones:
¿Coincide con la hipótesis inicial? No: los datos no respaldan que las hojas de sombra sean más grandes, pero tampoco la refutan. Con un solo árbol de sombra y dos de sol, el diseño no tenía capacidad para detectar ni para descartar esa diferencia. Lo que sí se ve con claridad es que los árboles difieren entre sí (ANOVA, Welch y Kruskal-Wallis coinciden) y que esa diferencia no sigue la exposición: G1_T5 (sol) tiene hojas 2 a 3 veces mayores que las de los demás, mientras que G1_T4 (también sol) es indistinguible de los árboles de semisombra y sombra.
Posibles explicaciones ambientales (son hipótesis para explicar el patrón, no algo que estos datos demuestren):
Qué más pudo influir (explicaciones alternativas que hay que descartar antes de atribuirlo todo a la luz):
Resultados frente a hipótesis. Con solo 5 árboles, estas son las conclusiones que se pueden sostener y las que no:
| 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 sol tuvieron la mayor área foliar media, pero solo por un árbol (G1_T5). | Que las hojas de sombra son más grandes que las de sol en los árboles del campus (o lo contrario). |
| Los árboles difieren entre sí en el tamaño de sus hojas, y no de forma ordenada por exposición. | Que las diferencias entre árboles se deben a la especie, a la edad o al suelo, y no a la luz. |
| El diseño tiene 2 árboles de sol, 2 de semisombra y 1 de sombra: no permite probar la hipótesis. | Que la respuesta a la luz es igual en todas las especies y en cualquier época del año. |
Qué haría falta para responder la pregunta. Más árboles por categoría (al menos 2 de sombra, como pide la guía, y mejor muchos más: la base conjunta de todos los grupos sería el paso natural); registrar la especie o morfotipo; medir la luz en lugar de estimarla; y verificar las hojas de G1_T5.
sessionInfo()
## R version 4.5.2 (2025-10-31 ucrt)
## Platform: x86_64-w64-mingw32/x64
## Running under: Windows 11 x64 (build 26200)
##
## Matrix products: default
## LAPACK version 3.12.1
##
## 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] leaflet_2.2.3
##
## loaded via a namespace (and not attached):
## [1] vctrs_0.7.1 cli_3.6.5 knitr_1.51
## [4] rlang_1.1.7 xfun_0.56 otel_0.2.0
## [7] jsonlite_2.0.0 glue_1.8.0 htmltools_0.5.9
## [10] sass_0.4.10 readxl_1.4.5 rmarkdown_2.30
## [13] cellranger_1.1.0 crosstalk_1.2.2 evaluate_1.0.5
## [16] jquerylib_0.1.4 tibble_3.3.1 fastmap_1.2.0
## [19] yaml_2.3.12 lifecycle_1.0.5 compiler_4.5.2
## [22] htmlwidgets_1.6.4 pkgconfig_2.0.3 rstudioapi_0.18.0
## [25] leaflet.providers_2.0.0 digest_0.6.39 R6_2.6.1
## [28] pillar_1.11.1 magrittr_2.0.4 bslib_0.10.0
## [31] tools_4.5.2 mime_0.13 cachem_1.1.0