# Formatos con coma decimal
fmt <- function(x, d = 1) formatC(x, format = "f", digits = d, decimal.mark = ",")
fmt_p <- function(p) ifelse(p < 0.001, "< 0,001", formatC(p, format = "f", digits = 3, decimal.mark = ","))
tabla <- function(x, ...) knitr::kable(x, ...)
# Distancia entre dos puntos (fórmula de Haversine), en metros
haversine <- function(lat1, lon1, lat2, lon2) {
r <- 6371000; p <- pi / 180
a <- sin((lat2 - lat1) * p / 2)^2 + cos(lat1 * p) * cos(lat2 * p) * sin((lon2 - lon1) * p / 2)^2
2 * r * asin(sqrt(a))
}
# Prueba de permutación exacta para la diferencia de medias entre dos grupos
perm_exacta <- function(y, etiqueta, nivel) {
n <- length(y); k <- sum(etiqueta == nivel)
comb <- combn(n, k)
dist <- apply(comb, 2, function(i) mean(y[i]) - mean(y[-i]))
obs <- mean(y[etiqueta == nivel]) - mean(y[etiqueta != nivel])
extremos <- sum(abs(dist) >= abs(obs) - 1e-9)
list(obs = obs, dist = dist, n = length(dist), extremos = extremos, p = extremos / length(dist))
}
col_expo <- c(sol = "#D89A00", sombra = "#1B6CA8")
# Estilo común de las gráficas (R base)
tema <- function(mar = c(4.5, 4.8, 3, 1)) {
invisible(par(mar = mar, bty = "l", las = 1, col.axis = "grey25", col.lab = "grey15",
col.main = "grey10", font.main = 2, cex.main = 1.05, tcl = -0.3, mgp = c(2.9, 0.7, 0)))
}
# Escribe HTML "tal cual" en el informe (tarjetas, galerías, pasos del cálculo del área)
html_crudo <- function(x) {
cuerpo <- paste(x, collapse = "\n")
cat("\n", strrep("`", 3), "{=html}\n", cuerpo, "\n", strrep("`", 3), "\n\n", sep = "")
}
# Cursiva de markdown (*texto*) a HTML (<em>texto</em>)
cursiva <- function(x) gsub("\\*([^*]+)\\*", "<em>\\1</em>", x)# --- Lectura de la base (la original no se modifica) ---
crudo <- as.data.frame(readxl::read_excel(params$archivo), stringsAsFactors = FALSE)
names(crudo) <- tolower(trimws(names(crudo)))
esperadas <- c("grupo", "id_arbol", "lat", "lon", "n_hoja", "area_cm2", "exposicion", "fecha", "observaciones")
faltan <- setdiff(esperadas, names(crudo))
if (length(faltan) > 0) stop("A la base le faltan estas columnas: ", paste(faltan, collapse = ", "))
num <- function(x) suppressWarnings(as.numeric(gsub(",", ".", as.character(x))))
parse_fecha <- function(x) {
if (inherits(x, "POSIXt") || inherits(x, "Date")) return(as.Date(x))
as.Date(as.character(x), tryFormats = c("%d/%m/%Y", "%Y-%m-%d"))
}
datos <- crudo # copia de trabajo
datos$grupo <- num(datos$grupo)
datos$id_arbol <- trimws(as.character(datos$id_arbol))
datos$lat <- num(datos$lat)
datos$lon <- num(datos$lon)
datos$n_hoja <- num(datos$n_hoja)
datos$area_cm2 <- num(datos$area_cm2)
datos$exposicion <- tolower(trimws(as.character(datos$exposicion)))
datos$fecha <- parse_fecha(datos$fecha)
datos$observaciones <- as.character(datos$observaciones)
n_arboles <- length(unique(datos$id_arbol))
n_hojas <- nrow(datos)fechas <- range(datos$fecha, na.rm = TRUE)
ficha <- data.frame(
Elemento = c("Pregunta", "Tipo de estudio", "Lugar", "Fechas de recolección", "Factor de interés",
"Variable respuesta", "Unidad experimental", "Unidad de muestreo", "Tamaño de la muestra", "Instrumentos"),
Descripción = c(
"¿Cómo cambia el área de las hojas según el lugar donde crece el árbol?",
"Observacional y comparativo: no se asignó sol o sombra, se eligieron árboles que ya estaban en cada condición",
"Campus de la Pontificia Universidad Javeriana Cali, barrio Pance y Conjunto residencial La siembra en el barrio el ingenio 2, Cali (Valle del Cauca, Colombia )",
paste(format(fechas[1], "%d/%m/%Y"), "a", format(fechas[2], "%d/%m/%Y")),
"Exposición del árbol: sol (luz directa la mayor parte del día) o sombra",
"Área de la hoja en cm²",
"El árbol (la exposición es una propiedad suya)",
"La hoja: 10 hojas adultas y sanas por árbol",
paste0(n_arboles, " árboles; ", n_hojas, " hojas registradas (", length(unique(datos$grupo)), " grupos)"),
"Celular (cámara y GPS), papel blanco, lápiz,Marcador y marco de lados conocidos, y un script que calcula el área a partir de la foto"))
tabla(ficha, align = "ll")| Elemento | Descripción |
|---|---|
| Pregunta | ¿Cómo cambia el área de las hojas según el lugar donde crece el árbol? |
| Tipo de estudio | Observacional y comparativo: no se asignó sol o sombra, se eligieron árboles que ya estaban en cada condición |
| Lugar | Campus de la Pontificia Universidad Javeriana Cali, barrio Pance y Conjunto residencial La siembra en el barrio el ingenio 2, Cali (Valle del Cauca, Colombia ) |
| Fechas de recolección | 22/10/2026 a 29/10/2026 |
| Factor de interés | Exposición del árbol: sol (luz directa la mayor parte del día) o sombra |
| Variable respuesta | Área de la hoja en cm² |
| Unidad experimental | El árbol (la exposición es una propiedad suya) |
| Unidad de muestreo | La hoja: 10 hojas adultas y sanas por árbol |
| Tamaño de la muestra | 5 árboles; 50 hojas registradas (5 grupos) |
| Instrumentos | Celular (cámara y GPS), papel blanco, lápiz,Marcador y marco de lados conocidos, y un script que calcula el área a partir de la foto |
El estudio se hizo en el campus de la universidad y en el Conjunto residencial La siembra , en Cali, en el valle geográfico del río Cauca (zona tropical). Dentro del campus hay árboles en zonas abiertas y bajo otras condiciones(tapado por un edificio u arbol), lo que da un gradiente natural de luz. La marca de agua de las fotos de campo (altitud del GPS del celular, que es aproximada) indica unos 1.040 a 1.060 m s. n. m.
El protocolo, en resumen:
especie de cada árbol:
info_arboles <- data.frame(
id = c("sl1_la", "sm1_gn", "sl2_bu", "sm2_na", "sl3_pa"),
especie = c("Annona muricata nombre común (Guanabana)", "citrus sp. nombre comun (limón)",
"*Bunchosia* sp. (entre *B. armeniaca* y *B. glandulifera*)",
"posiblemente *Casearia* sp.", "Ficus sp."),
marco = c("completar", "completar", "8 \u00d7 16", "8 \u00d7 10", "completar"),
stringsAsFactors = FALSE)
# Área (cm²) del marco de un árbol, a partir de "ancho × largo"
marco_cm2 <- function(id) {
m <- info_arboles$marco[info_arboles$id == id]
if (length(m) != 1) return(NA_real_)
p <- suppressWarnings(as.numeric(strsplit(m, "\\s*[x\u00d7]\\s*")[[1]]))
if (length(p) == 2 && !anyNA(p)) prod(p) else NA_real_
}Una foto de cada árbol permite ver dónde crece y juzgar si la clasificación en sol o sombra tiene sentido. Haz clic en una foto para ampliarla.
# Coordenadas por árbol (la corrección de coordenadas se documenta en la sección 4)
corregir_coord <- function(x, limite) { while (!is.na(x) && abs(x) > limite) x <- x / 10; x }
sitios <- unique(datos[, c("grupo", "id_arbol", "lat", "lon", "exposicion")])
sitios$lat <- sapply(sitios$lat, corregir_coord, limite = 90)
sitios$lon <- sapply(sitios$lon, corregir_coord, limite = 180)
sitios$hojas <- as.vector(table(datos$id_arbol)[sitios$id_arbol])
buscar_foto <- function(id) {
f <- Sys.glob(file.path("fotos", paste0(id, ".*")))
if (length(f) > 0) f[1] else NA_character_
}
sitios$foto <- vapply(sitios$id_arbol, buscar_foto, character(1))
sitios$popup <- vapply(seq_len(nrow(sitios)), function(i) {
s <- sitios[i, ]
img <- if (is.na(s$foto)) "" else paste0("<img src='", knitr::image_uri(s$foto), "' width='200'><br>")
paste0(img, "<b>", s$id_arbol, "</b><br>Exposición: ", s$exposicion, "<br>Grupo ", s$grupo,
"<br>Hojas registradas: ", s$hojas, "<br>", fmt(s$lat, 5), "; ", fmt(s$lon, 5))
}, character(1))
cat("Árboles:", nrow(sitios), "| con foto:", sum(!is.na(sitios$foto)), "\n")Árboles: 5 | con foto: 5
Cada punto es un árbol. Haz clic en un punto para ver sus datos
if (requireNamespace("leaflet", quietly = TRUE)) {
library(leaflet)
mapa <- leaflet(sitios) %>%
addProviderTiles(providers$Esri.WorldImagery, group = "Satélite") %>%
addTiles(group = "Calles") %>%
addCircleMarkers(data = sitios[sitios$exposicion == "sol", ], lng = ~lon, lat = ~lat,
radius = 9, weight = 2, color = "white", fillColor = col_expo[["sol"]],
fillOpacity = 0.95, label = ~id_arbol, popup = ~popup,
popupOptions = popupOptions(maxWidth = 240), group = "Sol") %>%
addCircleMarkers(data = sitios[sitios$exposicion == "sombra", ], lng = ~lon, lat = ~lat,
radius = 9, weight = 2, color = "white", fillColor = col_expo[["sombra"]],
fillOpacity = 0.95, label = ~id_arbol, popup = ~popup,
popupOptions = popupOptions(maxWidth = 240), group = "Sombra") %>%
addLayersControl(baseGroups = c("Satélite", "Calles"), overlayGroups = c("Sol", "Sombra"),
options = layersControlOptions(collapsed = FALSE)) %>%
addLegend("bottomright", colors = unname(col_expo), labels = c("Sol", "Sombra"),
title = "Exposición", opacity = 1) %>%
addScaleBar("bottomleft")
mapa
} else {
cat("El paquete 'leaflet' no está instalado: se muestra un mapa simple.",
"Para el mapa interactivo ejecuta install.packages('leaflet').\n")
plot(sitios$lon, sitios$lat, pch = 21, cex = 2, bg = col_expo[sitios$exposicion],
asp = 1 / cos(mean(sitios$lat) * pi / 180), xlab = "Longitud", ylab = "Latitud",
main = "Ubicación de los árboles")
text(sitios$lon, sitios$lat, sitios$id_arbol, pos = 3, cex = 0.7)
legend("topright", legend = names(col_expo), pt.bg = col_expo, pch = 21, bty = "n")
}Pregunta. ¿Cómo cambia el área de las hojas según el lugar donde crece el árbol?
Por qué esperamos una diferencia. Una hoja equilibra dos cosas: captar luz y no perder demasiada agua ni calentarse de más. A pleno sol sobra luz y el riesgo es el exceso de calor y de transpiración, así que se esperan hojas más pequeñas. A la sombra la luz es el recurso escaso, y una lámina más grande intercepta más de ella.
Hipótesis. Hipótesis. Las hojas de los árboles que crecen a la sombra tienen, en promedio, mayor área que las de los árboles que crecen a pleno sol. Dicho como hipótesis estadísticas, con el promedio de los árboles como unidad:
H₀: el área foliar media es igual en los árboles de sol y de sombra (μ_sol = μ_sombra).
H₁: el área foliar media es distinta (μ_sombra ≠ μ_sol); la dirección esperada es μ_sombra > μ_sol.
Se midieron 10 hojas por árbol en árboles ya clasificados como de sol (luz directa la mayor parte del día, sin cobertura de otros árboles ni edificios) o de sombra. Las hojas fueron adultas, expandidas y sin daño evidente, y se tomaron de distintas partes.
Cada hoja se trazó sobre papel, se fotografió dentro de un marco rectangular de lados conocidos y un script calculó el área en cm² (la foto se corrige en perspectiva con el marco y el área se obtiene por conteo de píxeles dentro del contorno de la hoja). Los datos se anotaron en una plantilla: una fila por hoja, con grupo, árbol, coordenadas, número de hoja, área, exposición, fecha y observaciones.
El área no se midió con regla ni con papel milimetrado: se calculó a partir de una foto del trazo. Las imágenes de abajo son las salidas reales del script para dos hojas de ejemplo, de modo que se vea cómo se pasa de un dibujo a un número en cm².
# ¿A qué árbol y a qué número de hoja corresponde cada trazo? (ajústalo si hace falta)
hojas_trazadas <- data.frame(
clave = c("A", "B", "C", "D", "E"),
arbol = "sm2_na",
n_hoja = 1:5,
stringsAsFactors = FALSE)
# Área registrada en la base para esa hoja, y área del marco usado en ese árbol
hojas_trazadas$area <- mapply(function(a, n) {
v <- datos$area_cm2[datos$id_arbol == a & datos$n_hoja == n]
if (length(v) == 1) v else NA_real_
}, hojas_trazadas$arbol, hojas_trazadas$n_hoja, USE.NAMES = FALSE)
hojas_trazadas$marco_cm2 <- vapply(hojas_trazadas$arbol, marco_cm2, numeric(1), USE.NAMES = FALSE)
# Imágenes: hojas/hoja<clave>_<paso>_<nombre>.png
# paso 1 = trazo y marco, 2 = trazos detectados, 3 = selección final, 4 = vista corregida
archivo_hoja <- function(clave, paso) {
f <- Sys.glob(file.path("hojas", paste0("hoja", clave, "_", paso, "_*")))
if (length(f) > 0) f[1] else NA_character_
}Como el marco mide lo que mide (por ejemplo 8 × 10 cm), cada píxel de la foto corregida equivale a una superficie conocida. En esencia: área de la hoja = (píxeles dentro del contorno ÷ píxeles dentro del marco) × área real del marco.
Qué se revisa en cada hoja antes de aceptar la medición:
Estas son las hojas de las que se dispone del trazo en su marco. Su área viene de la base (la misma que se analiza más adelante); la clave A–E solo sirve para nombrarlas en el informe.
Todo en R base (R version 4.6.1 (2026-06-24 ucrt)), salvo la lectura del .xlsx y el mapa:
Antes de calcular nada hay que mirar la base. Se trabaja sobre una
copia (datos); la base original (crudo) no se
toca.
La base tiene 50 filas y 5 valores distintos en la
columna grupo. Si todo estuviera completo, serían (número
de árboles × 10 hojas) = 50.
id_limpio <- gsub("\\s+", "", tolower(datos$id_arbol))
expo_validas <- datos$exposicion %in% c("sol", "sombra")
dup <- duplicated(datos[, c("id_arbol", "n_hoja")])
expo_por_arbol <- tapply(datos$exposicion, datos$id_arbol, function(x) length(unique(x)))
hojas_por_arbol <- table(datos$id_arbol)
med_arbol <- ave(datos$area_cm2, datos$id_arbol, FUN = function(x) median(x, na.rm = TRUE))
extremas <- !is.na(datos$area_cm2) & datos$area_cm2 > 0 &
(datos$area_cm2 > 10 * med_arbol | datos$area_cm2 < 0.1 * med_arbol)
lat_mala <- !is.na(datos$lat) & abs(datos$lat) > 90
lon_mala <- !is.na(datos$lon) & abs(datos$lon) > 180
revision <- data.frame(
Revision = c(
"IDs con mayúsculas o espacios de más",
"Exposiciones escritas de varias formas o distintas de sol/sombra",
"Áreas faltantes o <= 0",
"Filas repetidas (árbol y n.º de hoja)",
"Árboles con exposición distinta en sus filas",
"Árboles que no tienen 10 hojas",
"Hojas con un área > 10 veces o < 0,1 veces la mediana de su árbol",
"Coordenadas fuera de rango (latitud > 90° o longitud > 180°)"
),
Resultado = c(
sum(datos$id_arbol != id_limpio),
sum(!expo_validas),
sum(is.na(datos$area_cm2) | datos$area_cm2 <= 0),
sum(dup),
sum(expo_por_arbol > 1),
sum(hojas_por_arbol != 10),
sum(extremas),
sum(lat_mala | lon_mala)
)
)
tabla(revision, align = "lr")| Revision | Resultado |
|---|---|
| IDs con mayúsculas o espacios de más | 0 |
| Exposiciones escritas de varias formas o distintas de sol/sombra | 0 |
| Áreas faltantes o <= 0 | 0 |
| Filas repetidas (árbol y n.º de hoja) | 0 |
| Árboles con exposición distinta en sus filas | 0 |
| Árboles que no tienen 10 hojas | 0 |
| Hojas con un área > 10 veces o < 0,1 veces la mediana de su árbol | 0 |
| Coordenadas fuera de rango (latitud > 90° o longitud > 180°) | 0 |
Hojas por árbol:
sl1_la sl2_bu sl3_pa sm1_gn sm2_na
10 10 10 10 10
cat("\nÁrboles por grupo:\n"); print(table(tapply(datos$id_arbol, datos$grupo, function(x) length(unique(x)))),
zero.print = ".")
Árboles por grupo:
1
5
Fecha(s) de medición por árbol:
print(tapply(format(datos$fecha, "%d/%m/%Y"), datos$id_arbol, function(x) paste(unique(x), collapse = ", "))) sl1_la sl2_bu sl3_pa sm1_gn sm2_na
"22/10/2026" "23/10/2026" "29/10/2026" "22/10/2026" "29/10/2026"
Rango de n_hoja: 1 10
cat("\nÁrboles por exposición:\n"); print(table(tapply(datos$exposicion, datos$id_arbol, function(x) x[1])))
Árboles por exposición:
sol sombra
3 2
Formato de los IDs. Los IDs aparecen como un código:
s + l o m (sol o sombra) + número
de árbol + _ + dos letras que hacen refrencia a la especie.
Se comprueba que todos sigan el mismo formato: un ID que no lo cumple
puede ser un error de captura.
ids <- sort(unique(datos$id_arbol))
formato_ok <- grepl("^s[lm][0-9]+_[a-z]{2}$", ids)
# la letra l/m del ID debe coincidir con la exposición registrada
expo_id <- ifelse(substr(ids, 2, 2) == "l", "sol", "sombra")
expo_reg <- tapply(datos$exposicion, datos$id_arbol, function(x) x[1])[ids]
tabla(data.frame(ID = ids,
Formato = ifelse(formato_ok, "correcto", "DISTINTO: revisar"),
`Exposición coincide con el ID` = ifelse(expo_id == expo_reg, "sí", "NO: revisar"),
check.names = FALSE), row.names = FALSE, align = "lll")| ID | Formato | Exposición coincide con el ID |
|---|---|---|
| sl1_la | correcto | sí |
| sl2_bu | correcto | sí |
| sl3_pa | correcto | sí |
| sm1_gn | correcto | sí |
| sm2_na | correcto | sí |
Ubicación:
coord_raw <- unique(datos[, c("id_arbol", "lat", "lon")])
coord_raw$problema <- ifelse(abs(coord_raw$lat) > 90 | abs(coord_raw$lon) > 180, "fuera de rango", "")
coord_raw$lat <- fmt(coord_raw$lat, 4); coord_raw$lon <- fmt(coord_raw$lon, 4)
tabla(coord_raw, row.names = FALSE, align = "lrrl")| id_arbol | lat | lon | problema |
|---|---|---|---|
| sl1_la | 3,3458 | -76,5337 | |
| sm1_gn | 3,3469 | -76,5333 | |
| sl2_bu | 3,3467 | -76,5333 | |
| sm2_na | 3,3478 | -76,5328 | |
| sl3_pa | 3,3805 | -76,5611 |
Cada problema se resuelve con una regla explícita, y las filas
originales se conservan en la base (usar y
motivo) para poder auditar lo que se hizo.
datos$usar <- TRUE
datos$motivo <- ""
# 1. Coordenadas fuera de rango: se asume un separador decimal mal puesto (se divide por 10 hasta quedar en rango)
n_coord <- sum(lat_mala | lon_mala)
datos$lat <- sapply(datos$lat, corregir_coord, limite = 90)
datos$lon <- sapply(datos$lon, corregir_coord, limite = 180)
datos$motivo[lat_mala | lon_mala] <- "coordenada corregida (separador decimal)"
# 2. Filas repetidas: se elimina la copia
datos$usar[dup] <- FALSE
datos$motivo[dup] <- "fila repetida"
# 3. Área faltante o <= 0: se excluye la hoja
sin_area <- is.na(datos$area_cm2) | datos$area_cm2 <= 0
datos$usar[sin_area] <- FALSE
datos$motivo[sin_area] <- "área faltante o <= 0"
# 4. Área en mm² en lugar de cm² (más de 10 veces la mediana del árbol): se divide por 100
en_mm <- !sin_area & datos$area_cm2 > 10 * med_arbol & datos$area_cm2 / 100 > 0.1 * med_arbol
datos$area_cm2[en_mm] <- datos$area_cm2[en_mm] / 100
datos$motivo[en_mm] <- "área en mm² convertida a cm²"
decisiones <- data.frame(
Problema = c("Coordenada fuera de rango", "Fila repetida", "Área faltante o <= 0", "Área en mm² en lugar de cm²"),
Filas = c(n_coord, sum(dup), sum(sin_area), sum(en_mm)),
Decisión = c("Dividir por 10 hasta quedar en rango (p. ej. -765,327 pasa a -76,5327)",
"Eliminar la copia", "Excluir la hoja (no hay dato válido)", "Dividir por 100"))
tabla(decisiones, align = "lrl")| Problema | Filas | Decisión |
|---|---|---|
| Coordenada fuera de rango | 0 | Dividir por 10 hasta quedar en rango (p. ej. -765,327 pasa a -76,5327) |
| Fila repetida | 0 | Eliminar la copia |
| Área faltante o <= 0 | 0 | Excluir la hoja (no hay dato válido) |
| Área en mm² en lugar de cm² | 0 | Dividir por 100 |
limpios <- datos[datos$usar, ]
cat("Filas originales:", nrow(crudo), "\nFilas que se usan:", nrow(limpios), "\nÁrboles:", length(unique(limpios$id_arbol)), "\n")Filas originales: 50
Filas que se usan: 50
Árboles: 5
coord <- unique(limpios[, c("id_arbol", "lat", "lon")])
pares <- t(combn(nrow(coord), 2))
dist_tab <- data.frame(
`Árbol A` = coord$id_arbol[pares[, 1]], `Árbol B` = coord$id_arbol[pares[, 2]],
d = haversine(coord$lat[pares[, 1]], coord$lon[pares[, 1]], coord$lat[pares[, 2]], coord$lon[pares[, 2]]),
check.names = FALSE)
dist_tab <- dist_tab[order(dist_tab$d), ]
dist_tab$`Distancia (m)` <- round(dist_tab$d); dist_tab$d <- NULL
tabla(dist_tab, row.names = FALSE, align = "llr")| Árbol A | Árbol B | Distancia (m) |
|---|---|---|
| sm1_gn | sl2_bu | 22 |
| sl1_la | sl2_bu | 109 |
| sm1_gn | sm2_na | 114 |
| sl1_la | sm1_gn | 130 |
| sl2_bu | sm2_na | 134 |
| sl1_la | sm2_na | 244 |
| sm2_na | sl3_pa | 4805 |
| sm1_gn | sl3_pa | 4846 |
| sl2_bu | sl3_pa | 4863 |
| sl1_la | sl3_pa | 4913 |
Después de corregir, la distancia entre árboles va de 22 a 4913 m: ningún par está a menos de 5 m, así que no hay indicios de árboles medidos dos veces.
limpios$id_arbol <- as.character(limpios$id_arbol)
orden_arboles <- unique(limpios[order(limpios$grupo), "id_arbol"])
resumen <- do.call(rbind, lapply(orden_arboles, function(a) {
x <- limpios$area_cm2[limpios$id_arbol == a]
data.frame(arbol = a, exposicion = limpios$exposicion[limpios$id_arbol == a][1], n = length(x),
media = mean(x), mediana = median(x), de = sd(x), cv = 100 * sd(x) / mean(x),
min = min(x), max = max(x))
}))
tabla(data.frame(Árbol = resumen$arbol, Exposición = resumen$exposicion, n = resumen$n,
Media = fmt(resumen$media), Mediana = fmt(resumen$mediana), DE = fmt(resumen$de),
`CV (%)` = fmt(resumen$cv), `Mín.` = fmt(resumen$min), `Máx.` = fmt(resumen$max),
check.names = FALSE),
align = "llrrrrrrr", caption = "Área foliar (cm²) por árbol. DE = desviación estándar; CV = DE / media.")| Árbol | Exposición | n | Media | Mediana | DE | CV (%) | Mín. | Máx. |
|---|---|---|---|---|---|---|---|---|
| sl1_la | sol | 10 | 32,7 | 33,1 | 4,5 | 13,8 | 23,6 | 37,7 |
| sm1_gn | sombra | 10 | 67,1 | 66,7 | 12,5 | 18,7 | 49,0 | 85,0 |
| sl2_bu | sol | 10 | 41,9 | 40,9 | 4,0 | 9,5 | 36,7 | 50,1 |
| sm2_na | sombra | 10 | 15,9 | 16,4 | 1,9 | 11,7 | 13,4 | 19,0 |
| sl3_pa | sol | 10 | 14,7 | 14,6 | 1,4 | 9,8 | 12,7 | 16,3 |
Dos cosas para mirar en esta tabla: (1) la diferencia entre árboles (las medias van de 14,7 a 67,1 cm²), que conviene comparar con la variación dentro de cada árbol (la DE); (2) el CV, que mide cuánto varían las hojas de un árbol respecto a su propia media (aquí entre 10 % y 19 %).
dg <- limpios
dg$id_arbol <- factor(dg$id_arbol, levels = resumen$arbol)
tema()
boxplot(area_cm2 ~ id_arbol, data = dg, outline = FALSE, las = 1,
col = adjustcolor(col_expo[resumen$exposicion], 0.55), border = "grey25",
xlab = "Árbol", ylab = expression("Área foliar (cm"^2*")"),
main = "Área foliar de cada hoja, por árbol")
stripchart(area_cm2 ~ id_arbol, data = dg, vertical = TRUE, method = "jitter", jitter = 0.12,
pch = 16, col = adjustcolor("black", 0.45), add = TRUE)
legend("topright", legend = c("sol", "sombra"), fill = adjustcolor(col_expo, 0.55), bty = "n")Cada caja resume las 10 hojas de un árbol (la línea central es la mediana y la caja contiene la mitad central de las hojas); los puntos son las hojas.
Criterio. Se revisa cada hoja con dos preguntas, en este orden:
es_atipica <- function(x) {
q <- quantile(x, c(0.25, 0.75)); r <- q[2] - q[1]
x < q[1] - 1.5 * r | x > q[2] + 1.5 * r
}
limpios$atipica <- ave(limpios$area_cm2, limpios$id_arbol, FUN = es_atipica) == 1
atip <- limpios[limpios$atipica, c("id_arbol", "n_hoja", "area_cm2", "observaciones")]
atip$area_cm2 <- fmt(atip$area_cm2, 2)
atip$observaciones[is.na(atip$observaciones)] <- "La hoja es una fiel representación de la diversidad del árbol"
if (nrow(atip) > 0) tabla(atip, row.names = FALSE, caption = "Hojas fuera de los límites de Tukey de su árbol.") else cat("Ninguna hoja queda fuera de los límites de Tukey de su árbol.\n")| id_arbol | n_hoja | area_cm2 | observaciones |
|---|---|---|---|
| sl2_bu | 2 | 50,14 | La hoja es una fiel representación de la diversidad del árbol |
# Sensibilidad: diferencia sombra - sol según cómo se resuma cada árbol
dif_sombra_sol <- function(v, expo) mean(v[expo == "sombra"]) - mean(v[expo == "sol"])
exp_arbol <- resumen$exposicion
med_todas <- tapply(limpios$area_cm2, limpios$id_arbol, mean)[resumen$arbol]
med_mediana <- tapply(limpios$area_cm2, limpios$id_arbol, median)[resumen$arbol]
med_sin <- tapply(limpios$area_cm2[!limpios$atipica], limpios$id_arbol[!limpios$atipica], mean)[resumen$arbol]
sens <- data.frame(
`Cómo se resume cada árbol` = c("Media (todas las hojas)", "Mediana", "Media sin las hojas atípicas"),
`Sombra − sol (cm²)` = fmt(c(dif_sombra_sol(med_todas, exp_arbol),
dif_sombra_sol(med_mediana, exp_arbol),
dif_sombra_sol(med_sin, exp_arbol))),
check.names = FALSE)
tabla(sens, align = "lr", caption = "Sensibilidad de la diferencia entre exposiciones a las hojas atípicas.")| Cómo se resume cada árbol | Sombra − sol (cm²) |
|---|---|
| Media (todas las hojas) | 11,7 |
| Mediana | 12,0 |
| Media sin las hojas atípicas | 12,0 |
rango_sens <- range(c(dif_sombra_sol(med_todas, exp_arbol), dif_sombra_sol(med_mediana, exp_arbol), dif_sombra_sol(med_sin, exp_arbol)))La diferencia entre sombra y sol va de 11,7 a 12,0 cm² según cómo se trate la hoja atípica.
dg$expo <- factor(dg$exposicion, levels = c("sol", "sombra"))
simbolos <- c(16, 17, 15, 18, 8, 3, 4, 1)[seq_along(resumen$arbol)]
tema()
boxplot(area_cm2 ~ expo, data = dg, outline = FALSE, las = 1, col = adjustcolor(col_expo, 0.5),
border = "grey25", xlab = "Exposición", ylab = expression("Área foliar (cm"^2*")"),
main = "Hojas por exposición")
points(as.integer(dg$expo) + runif(nrow(dg), -0.15, 0.15), dg$area_cm2,
pch = simbolos[as.integer(dg$id_arbol)], col = adjustcolor("black", 0.6))
legend("topleft", legend = levels(dg$id_arbol), pch = simbolos, bty = "n", title = "Árbol", cex = 0.85)Las hojas no son independientes porque se agrupan por individuo
Lo que se ha asignado a sol o a sombra es el árbol, no la hoja. Las 10 hojas de un mismo árbol comparten suelo, edad, genética y luz. Por eso se promedian y se compara un valor por árbol.
medias <- aggregate(area_cm2 ~ id_arbol + exposicion, data = limpios, FUN = mean)
medias$exposicion <- factor(medias$exposicion, levels = c("sol", "sombra")) # sol = nivel de referencia en las pruebas
medias <- medias[order(medias$exposicion == "sol", medias$id_arbol), ] # en la tabla, primero sombra
n_expo <- table(medias$exposicion)
tabla(data.frame(Árbol = medias$id_arbol, Exposición = medias$exposicion,
`Media del árbol (cm²)` = fmt(medias$area_cm2), check.names = FALSE), align = "llr")| Árbol | Exposición | Media del árbol (cm²) |
|---|---|---|
| sm1_gn | sombra | 67,1 |
| sm2_na | sombra | 15,9 |
| sl1_la | sol | 32,7 |
| sl2_bu | sol | 41,9 |
| sl3_pa | sol | 14,7 |
m_sombra <- mean(medias$area_cm2[medias$exposicion == "sombra"])
m_sol <- mean(medias$area_cm2[medias$exposicion == "sol"])
dif_obs <- m_sombra - m_solHay 2 árbol(es) de sombra y 3 de sol: 5 datos independientes, no 50. La media de los árboles de sombra es 41,5 cm² y la de sol 29,8 cm²; la diferencia observada (sombra − sol) es de 11,7 cm².
Con tan pocos árboles se puede hacer una prueba de permutación exacta: si la exposición no importara, cualquier forma de repartir las etiquetas “sol” y “sombra” entre los árboles sería igual de probable. Se calcula la diferencia para todos los repartos posibles y se mira dónde queda el observado.
pe <- perm_exacta(medias$area_cm2, as.character(medias$exposicion), "sombra")
p_perm <- pe$p
tema(c(4.5, 1, 3, 1))
stripchart(pe$dist, method = "stack", pch = 21, bg = "grey85", cex = 2, xlab = "Diferencia sombra − sol (cm²)",
main = paste(pe$n, "repartos posibles"), yaxt = "n", xlim = range(c(pe$dist, pe$obs)) * 1.05)
abline(v = 0, lty = 3, col = "grey50")
points(pe$obs, 1, pch = 21, bg = "#1B6CA8", cex = 2.6)
legend("topright", legend = "reparto observado", pch = 21, pt.bg = "#1B6CA8", bty = "n")Hay 10 repartos posibles; 6 de ellos dan una diferencia al menos tan extrema como la observada, así que p = 0,600. El valor más pequeño que cualquier resultado podría dar con 3 y 2 árboles sería 0,100. Con 5 árboles, el diseño no puede dar resultados “significativos” al 5 %, sean cuales sean los datos.
t_hojas <- t.test(area_cm2 ~ exposicion, data = limpios) # trata las hojas como independientes
t_arb <- t.test(area_cm2 ~ exposicion, data = medias) # un promedio por árbol (Welch)
p_hojas <- t_hojas$p.value
p_welch <- t_arb$p.value
comp <- data.frame(
Análisis = c("Hojas como datos independientes (incorrecto)", "Un promedio por árbol (Welch)",
"Un promedio por árbol (permutación exacta)"),
`n sol` = c(sum(limpios$exposicion == "sol"), n_expo[["sol"]], n_expo[["sol"]]),
`n sombra` = c(sum(limpios$exposicion == "sombra"), n_expo[["sombra"]], n_expo[["sombra"]]),
p = c(fmt_p(p_hojas), fmt_p(p_welch), fmt_p(p_perm)), check.names = FALSE)
tabla(comp, align = "lrrr")| Análisis | n sol | n sombra | p |
|---|---|---|---|
| Hojas como datos independientes (incorrecto) | 30 | 20 | 0,086 |
| Un promedio por árbol (Welch) | 3 | 2 | 0,727 |
| Un promedio por árbol (permutación exacta) | 3 | 2 | 0,600 |
Tratar las hojas como independientes multiplica por 10 el tamaño de muestra y cambia el p de 0,600 (permutación) a 0,086. Esa precisión es falsa: se llama pseudorreplicación. Las hojas de un árbol son 10 mediciones de lo mismo, no 10 árboles.
dif_w <- unname(t_arb$estimate[2] - t_arb$estimate[1]); ic_w <- -rev(t_arb$conf.int)
tl <- t.test(log(area_cm2) ~ exposicion, data = medias)
razon <- unname(exp(tl$estimate[2] - tl$estimate[1])); ic_raz <- exp(-rev(tl$conf.int))
wil <- wilcox.test(area_cm2 ~ exposicion, data = medias)
pruebas <- data.frame(
Análisis = c("Welch sobre promedios de árbol (cm²)", "Welch sobre log(promedio): razón sombra / sol",
"Wilcoxon sobre promedios de árbol"),
Estimación = c(paste(fmt(dif_w), "cm²"), paste0(fmt(razon, 2), " veces"), "-"),
IC95 = c(paste0("[", fmt(ic_w[1]), "; ", fmt(ic_w[2]), "]"),
paste0("[", fmt(ic_raz[1], 2), "; ", fmt(ic_raz[2], 2), "]"), "-"),
p = c(fmt_p(t_arb$p.value), fmt_p(tl$p.value), fmt_p(wil$p.value)))
tabla(pruebas, align = "lrrr",
caption = paste0("Sombra frente a sol con el árbol como unidad (", n_expo[["sombra"]], " árboles de sombra, ",
n_expo[["sol"]], " de sol)."))| Análisis | Estimación | IC95 | p |
|---|---|---|---|
| Welch sobre promedios de árbol (cm²) | 11,7 cm² | [-220,2; 243,7] | 0,727 |
| Welch sobre log(promedio): razón sombra / sol | 1,20 veces | [0,01; 225,47] | 0,845 |
| Wilcoxon sobre promedios de árbol | - | - | 0,800 |
Los intervalos son muy anchos porque salen de 2 y 3 árboles por categoría: con tan pocos datos, lo único razonable es describir la dirección y el tamaño aproximado de la diferencia, sin afirmar que existe.
El análisis de varianza (ANOVA) permite usar la estructura del diseño. Aquí se responden dos preguntas:
| Pregunta | Herramienta | Después del ANOVA |
|---|---|---|
| ¿Difieren entre sí los árboles medidos? | ANOVA de un factor (árbol) | Tukey HSD: ¿cuáles árboles? |
| ¿Hay efecto de la exposición respetando que las hojas están dentro de árboles? | ANOVA anidado (árbol dentro de exposición) | Componentes de varianza |
Modelo:
log(área) ~ exposición + árbol(exposición) + error de hoja.
El término árbol(exposición) es el error experimental
contra el que se prueba la exposición.
¿Por qué el logaritmo del área? El área es positiva y las hojas grandes varían más que las pequeñas. Con el logaritmo, la variabilidad se parece más entre árboles y los efectos se interpretan como razones (“1,3 veces”).
sd_a <- tapply(limpios$area_cm2, limpios$id_arbol, sd); mu_a <- tapply(limpios$area_cm2, limpios$id_arbol, mean)
sd_l <- tapply(log(limpios$area_cm2), limpios$id_arbol, sd); mu_l <- tapply(log(limpios$area_cm2), limpios$id_arbol, mean)
c(escala_original = cor(mu_a, sd_a), escala_log = cor(mu_l, sd_l))escala_original escala_log
0,9513261 0,6518606
La correlación entre la media de cada árbol y su desviación estándar es 0,95 en la escala original y 0,65 con el logaritmo. (Con solo 5 árboles esta comparación es muy aproximada.)
Aquí las hojas sí son réplicas (de cada árbol) y la pregunta es por esos árboles concretos, no por la exposición.Es decir,pseudoreplicas.
limpios$arbol <- factor(limpios$id_arbol, levels = resumen$arbol)
m1 <- aov(log(area_cm2) ~ arbol, data = limpios)
a1 <- summary(m1)[[1]]
tabla_a1 <- data.frame(
Fuente = c("Entre árboles", "Dentro de árboles (hojas)"),
gl = a1$Df, SC = fmt(a1$`Sum Sq`, 3), CM = fmt(a1$`Mean Sq`, 3),
F = c(fmt(a1$`F value`[1], 2), ""), p = c(fmt_p(a1$`Pr(>F)`[1]), ""))
tabla(tabla_a1, align = "lrrrrr", caption = "ANOVA de un factor sobre log(área).")| Fuente | gl | SC | CM | F | p |
|---|---|---|---|---|---|
| Entre árboles | 4 | 16,575 | 4,144 | 228,79 | < 0,001 |
| Dentro de árboles (hojas) | 45 | 0,815 | 0,018 |
Hay diferencias entre árboles: F(4; 45) = 228,8, p < 0,001. El ANOVA solo dice que al menos un árbol difiere; para saber cuáles hay que hacer comparaciones múltiples.
Supuestos. Los residuos deberían ser aproximadamente normales y tener varianza parecida entre árboles.
tema()
par(mfrow = c(1, 2))
plot(fitted(m1), resid(m1), pch = 16, col = adjustcolor("black", 0.5), las = 1,
xlab = "Valor ajustado (log área)", ylab = "Residuo", main = "Residuos vs ajustados")
abline(h = 0, lty = 2)
qqnorm(resid(m1), pch = 16, col = adjustcolor("black", 0.5), las = 1, main = "Normalidad de los residuos",
xlab = "Cuantiles teóricos", ylab = "Cuantiles de los residuos")
qqline(resid(m1))par(mfrow = c(1, 1))
sh1 <- shapiro.test(resid(m1))$p.value
fl1 <- fligner.test(log(area_cm2) ~ arbol, data = limpios)$p.value
kw1 <- kruskal.test(log(area_cm2) ~ arbol, data = limpios)$p.value
c(shapiro_p = sh1, fligner_p = fl1, kruskal_p = kw1) shapiro_p fligner_p kruskal_p
4,119529e-01 1,888595e-01 4,743825e-09
Shapiro-Wilk (normalidad): p = 0,412. Fligner-Killeen (igualdad de varianzas): p = 0,189. Kruskal-Wallis (alternativa sin supuesto de normalidad): p < 0,001. Un p grande significa que no hay evidencia contra el supuesto; un p pequeño, que sí la hay. Mira siempre el gráfico: los puntos del cuantil-cuantil deberían seguir la recta y los residuos no deberían abrirse como un embudo.
Con 5 árboles hay 10 comparaciones por pares. Hacer pruebas t sueltas infla la probabilidad de algún falso positivo; la prueba de Tukey HSD compara todos los pares y controla ese error para el conjunto.
tk <- TukeyHSD(m1)$arbol
tk_tab <- data.frame(comparacion = gsub("-", " / ", rownames(tk)),
razon = exp(tk[, "diff"]), li = exp(tk[, "lwr"]), ls = exp(tk[, "upr"]),
p_aj = tk[, "p adj"], stringsAsFactors = FALSE)
tk_tab <- tk_tab[order(tk_tab$p_aj), ]
# ¿Cuántos pares que difieren son de árboles con la MISMA exposición?
pares_ids <- strsplit(tk_tab$comparacion, " / ")
expo_de <- setNames(resumen$exposicion, resumen$arbol)
misma_expo <- sapply(pares_ids, function(x) length(unique(expo_de[x])) == 1)
n_sig <- sum(tk_tab$p_aj < 0.05)
n_misma_sig <- sum(misma_expo & tk_tab$p_aj < 0.05)
tabla(data.frame(`Comparación (A / B)` = tk_tab$comparacion, `Razón A / B` = fmt(tk_tab$razon, 2),
`IC 95 % (Tukey)` = paste0("[", fmt(tk_tab$li, 2), "; ", fmt(tk_tab$ls, 2), "]"),
`p ajustado` = fmt_p(tk_tab$p_aj), `Difieren (5 %)` = ifelse(tk_tab$p_aj < 0.05, "sí", "no"),
`Exposición del par` = ifelse(misma_expo, "misma", "distinta"), check.names = FALSE),
align = "lrrrll", caption = "Comparaciones de Tukey entre árboles (escala logarítmica, mostradas como razones).")| Comparación (A / B) | Razón A / B | IC 95 % (Tukey) | p ajustado | Difieren (5 %) | Exposición del par |
|---|---|---|---|---|---|
| sm2_na / sm1_gn | 0,24 | [0,20; 0,28] | < 0,001 | sí | misma |
| sl3_pa / sm1_gn | 0,22 | [0,19; 0,26] | < 0,001 | sí | distinta |
| sl3_pa / sl2_bu | 0,35 | [0,30; 0,42] | < 0,001 | sí | misma |
| sm2_na / sl2_bu | 0,38 | [0,32; 0,45] | < 0,001 | sí | distinta |
| sl3_pa / sl1_la | 0,45 | [0,38; 0,53] | < 0,001 | sí | misma |
| sm2_na / sl1_la | 0,49 | [0,41; 0,58] | < 0,001 | sí | distinta |
| sm1_gn / sl1_la | 2,04 | [1,72; 2,42] | < 0,001 | sí | distinta |
| sl2_bu / sm1_gn | 0,63 | [0,53; 0,75] | < 0,001 | sí | distinta |
| sl2_bu / sl1_la | 1,29 | [1,08; 1,53] | 0,001 | sí | misma |
| sl3_pa / sm2_na | 0,92 | [0,78; 1,10] | 0,677 | no | distinta |
tema(c(4.5, 8, 3, 1))
y <- seq_len(nrow(tk_tab))
plot(tk_tab$razon, y, log = "x", xlim = range(c(tk_tab$li, tk_tab$ls)), ylim = c(0.5, nrow(tk_tab) + 0.5),
yaxt = "n", ylab = "", xlab = "Razón entre árboles (escala logarítmica)", main = "Comparaciones por pares",
pch = 16, col = ifelse(tk_tab$p_aj < 0.05, "#C75B12", "grey40"), cex = 1.3)
segments(tk_tab$li, y, tk_tab$ls, y, col = ifelse(tk_tab$p_aj < 0.05, "#C75B12", "grey40"), lwd = 2)
axis(2, at = y, labels = tk_tab$comparacion, las = 1, cex.axis = 0.8)
abline(v = 1, lty = 2)En 9 de las 10 comparaciones los árboles difieren de forma detectable (en naranja). Cruza esto con la exposición de cada árbol: 4 de esas diferencias son entre árboles de la misma exposición. Es decir, dos árboles a la sombra (o dos al sol) pueden diferir tanto como un árbol de sol y uno de sombra. Esa variación entre árboles es el “ruido” contra el que tiene que competir el efecto de la exposición, y la razón por la que no puede probarse con hojas sueltas.
Las hojas están dentro de árboles y los árboles dentro de una exposición: un diseño anidado. El efecto de la exposición se prueba contra la variación entre árboles, no contra la variación entre hojas.
lim <- limpios
lim$expo <- factor(lim$exposicion, levels = c("sol", "sombra"))
lim$arbol <- factor(lim$id_arbol)
m_anid <- lm(log(area_cm2) ~ expo + arbol, data = lim) # el árbol está dentro de expo (cada árbol tiene una sola exposición)
a_an <- anova(m_anid)
gl <- a_an$Df; sc <- a_an$`Sum Sq`; cm <- sc / gl
F_expo <- cm[1] / cm[2]; p_expo <- pf(F_expo, gl[1], gl[2], lower.tail = FALSE)
F_arbol <- cm[2] / cm[3]; p_arbol <- pf(F_arbol, gl[2], gl[3], lower.tail = FALSE)
tab_an <- data.frame(
Fuente = c("Exposición", "Árbol (dentro de exposición)", "Hojas (residual)"),
gl = gl, SC = fmt(sc, 3), CM = fmt(cm, 3),
Contra = c("CM de árbol", "CM de hojas", ""),
F = c(fmt(F_expo, 2), fmt(F_arbol, 2), ""), p = c(fmt_p(p_expo), fmt_p(p_arbol), ""))
tabla(tab_an, align = "lrrrlrr", caption = "ANOVA anidado sobre log(área): hojas dentro de árboles dentro de exposición.")| Fuente | gl | SC | CM | Contra | F | p |
|---|---|---|---|---|---|---|
| Exposición | 1 | 0,381 | 0,381 | CM de árbol | 0,07 | 0,808 |
| Árbol (dentro de exposición) | 3 | 16,193 | 5,398 | CM de hojas | 298,03 | < 0,001 |
| Hojas (residual) | 45 | 0,815 | 0,018 |
# Versión ingenua (incorrecta): exposición contra el CM de las hojas
F_ing <- cm[1] / cm[3]; p_ing <- pf(F_ing, gl[1], gl[3], lower.tail = FALSE)Cuánto de la variación se debe al árbol y cuánto a las hojas (componentes de varianza, aproximados):
n_por_arbol <- mean(table(lim$arbol))
var_hoja <- cm[3]
var_arbol <- max((cm[2] - cm[3]) / n_por_arbol, 0)
pct_arbol <- 100 * var_arbol / (var_arbol + var_hoja)
r2_arbol <- 100 * a1$`Sum Sq`[1] / sum(a1$`Sum Sq`)
c(var_arbol = var_arbol, var_hoja = var_hoja, pct_arbol = pct_arbol) var_arbol var_hoja pct_arbol
0,53796435 0,01811122 96,74302924
Alrededor del 97 % de la variación (en log) que queda después de quitar la exposición se debe a diferencias entre árboles, y el árbol por sí solo explica el 95 % de la variación del logaritmo del área. Por eso conviene medir más árboles antes que más hojas por árbol.
En el diseño de referencia, cada grupo mide árboles de sol y de sombra, y el grupo sirve de bloque. En esta base cada grupo tiene un solo árbol (ver “Árboles por grupo” en la revisión), así que el grupo y el árbol son lo mismo y no se pueden separar: el ANOVA de bloques con interacción grupo × exposición no tiene grados de libertad para estimarse. Por eso aquí no se hace. Para poder hacerlo, cada grupo tendría que medir al menos 2 árboles de sol y 2 de sombra.
resumen_a <- data.frame(
Pregunta = c("¿Difieren los árboles?", "¿Qué árboles difieren?", "Sol vs sombra con árbol como unidad (Welch)",
"Sol vs sombra con árbol como unidad (permutación exacta)", "Sol vs sombra, ANOVA anidado"),
Herramienta = c("ANOVA de un factor", "Tukey HSD", "Prueba t sobre promedios", "Permutación", "F anidada (árbol como error)"),
Resultado = c(paste0("F = ", fmt(a1$`F value`[1], 1), ", p ", fmt_p(a1$`Pr(>F)`[1])),
paste(n_sig, "de", nrow(tk_tab), "pares"),
paste0("p = ", fmt_p(p_welch)), paste0("p = ", fmt_p(p_perm)),
paste0("F = ", fmt(F_expo, 2), ", p = ", fmt_p(p_expo))))
tabla(resumen_a, align = "lll")| Pregunta | Herramienta | Resultado |
|---|---|---|
| ¿Difieren los árboles? | ANOVA de un factor | F = 228,8, p < 0,001 |
| ¿Qué árboles difieren? | Tukey HSD | 9 de 10 pares |
| Sol vs sombra con árbol como unidad (Welch) | Prueba t sobre promedios | p = 0,727 |
| Sol vs sombra con árbol como unidad (permutación exacta) | Permutación | p = 0,600 |
| Sol vs sombra, ANOVA anidado | F anidada (árbol como error) | F = 0,07, p = 0,808 |
mapa_res <- merge(medias, sitios[, c("id_arbol", "lat", "lon", "foto")], by = "id_arbol")
mapa_res$popup <- vapply(seq_len(nrow(mapa_res)), function(i) {
s <- mapa_res[i, ]
img <- if (is.na(s$foto)) "" else paste0("<img src='", knitr::image_uri(s$foto), "' width='200'><br>")
paste0(img, "<b>", s$id_arbol, "</b> (", s$exposicion, ")<br>Área media: ", fmt(s$area_cm2), " cm²")
}, character(1))
if (requireNamespace("leaflet", quietly = TRUE)) {
library(leaflet)
leaflet(mapa_res) %>%
addProviderTiles(providers$Esri.WorldImagery, group = "Satélite") %>%
addTiles(group = "Calles") %>%
addCircleMarkers(lng = ~lon, lat = ~lat, radius = ~sqrt(area_cm2) * 1.8, weight = 1.5,
color = "white", fillColor = ~col_expo[as.character(exposicion)], fillOpacity = 0.85,
label = ~paste0(id_arbol, ": ", round(area_cm2), " cm²"),
popup = ~popup, popupOptions = popupOptions(maxWidth = 240)) %>%
addLayersControl(baseGroups = c("Satélite", "Calles"), options = layersControlOptions(collapsed = FALSE)) %>%
addLegend("bottomright", colors = unname(col_expo), labels = c("Sol", "Sombra"),
title = "Exposición (tamaño = área media)", opacity = 1) %>%
addScaleBar("bottomleft")
} else {
plot(mapa_res$lon, mapa_res$lat, pch = 21, bg = col_expo[as.character(mapa_res$exposicion)],
cex = sqrt(mapa_res$area_cm2) / 3, asp = 1 / cos(mean(mapa_res$lat) * pi / 180),
xlab = "Longitud", ylab = "Latitud", main = "Área foliar media por árbol")
text(mapa_res$lon, mapa_res$lat, mapa_res$id_arbol, pos = 3, cex = 0.7)
}El tamaño del círculo es proporcional al área media de cada árbol. Sirve para mirar si hay un patrón espacial (por ejemplo, una zona del campus con hojas grandes) que pudiera confundirse con el efecto de la exposición. Con solo 5 puntos no se puede probar ningún patrón espacial: solo se puede mirar si algo llama la atención.
Qué encontramos. Con el árbol como unidad de análisis, los árboles de sombra tuvieron hojas con un área media mayor que los de sol: 41,5 cm² frente a 29,8 cm² (media de los promedios por árbol; 2 árboles de sombra y 3 de sol), una diferencia de 11,7 cm². El sentido de la diferencia coincide con la hipótesis inicial.
Qué tan fuerte es la evidencia. La prueba de permutación da p = 0,600 y el ANOVA anidado p = 0,808. No hay evidencia suficiente para afirmar que la exposición cambia el área foliar: con tan pocos árboles, esta prueba tiene muy poca potencia. Además, los rangos se solapan: al menos un árbol de sombra tiene un promedio menor que algún árbol de sol, es decir, la separación entre las dos exposiciones no es limpia.
Lo que domina es la variación entre árboles. El árbol explica cerca del 95 % de la variación del logaritmo del área, y Tukey encontró 9 pares de árboles distintos, 4 de ellos con la misma exposición. Es decir, las diferencias entre árboles son grandes aunque tengan la misma exposición.
Qué más pudo influir (explicaciones alternativas que debes discutir):
Resultados frente a hipótesis.
| Es un resultado (lo que los datos muestran) | Es una hipótesis (lo que habría que probar) |
|---|---|
| En estos 5 árboles del campus, los de sombra tuvieron hojas con área media mayor que los de sol. | La causa es una respuesta de la hoja a la luz o a la temperatura. |
| Los árboles difieren mucho entre sí, incluso con la misma exposición. | Ocurre en toda la flora del campus, en cualquier época del año. |
| Con 5 árboles el diseño no permite conclusiones fuertes. | Las especies de sol y de sombra responden igual. |