Cómo usar este informe. Guarda este
.Rmdy tu base (area_foliar.xlsx) en la misma carpeta y pulsa Knit. Si tu archivo se llama distinto, cambiaarchivoen el encabezado (params). Las tablas, gráficas y los números dentro del texto se recalculan solos; la interpretación hay que revisarla y reescribirla con tus propias palabras. Paquetes:knitr,rmarkdown,readxl(para leer el .xlsx) y, opcionalmente,leaflet(mapa interactivo).
crudo <- leer_base(params$archivo)
esperadas <- c("grupo", "id_arbol", "lat", "lon", "n_hoja", "area_cm2", "exposicion", "fecha", "observaciones")
faltan <- setdiff(esperadas, names(crudo))
if (length(faltan) > 0) stop("Faltan columnas en la base: ", paste(faltan, collapse = ", "), call. = FALSE)
crudo$fecha <- as.Date(crudo$fecha)
n_arbol_crudo <- length(unique(tolower(trimws(crudo$id_arbol))))
n_grupos <- length(unique(crudo$grupo))
ficha <- data.frame(
Elemento = c("Pregunta", "Tipo de estudio", "Lugar", "Fechas de recolección", "Factor de interés",
"Variable respuesta", "Unidad experimental", "Unidad de muestreo", "Tamaño de la muestra",
"Instrumentos"),
Descripción = c(
"¿Cómo cambia el área de las hojas según el lugar donde crece el árbol?",
"Observacional y comparativo: no se asignó sol o sombra, se eligieron árboles que ya estaban en cada condición",
"Campus universitario, Cali (Valle del Cauca, Colombia)",
paste(format(min(crudo$fecha), "%d/%m/%Y"), "a", format(max(crudo$fecha), "%d/%m/%Y")),
"Exposición del árbol: sol (luz directa la mayor parte del día) o sombra",
"Área de la hoja en cm²",
"El árbol (la exposición es una propiedad suya)",
"La hoja: 10 hojas adultas y sanas por árbol",
paste0(n_arbol_crudo, " árboles; ", nrow(crudo), " hojas registradas (", n_grupos, " grupos)"),
"Celular (cámara y GPS), papel blanco, lápiz y regla, y un script en R que calcula el área a partir de la foto"))
tabla(ficha, align = "ll")
| Elemento | Descripción |
|---|---|
| Pregunta | ¿Cómo cambia el área de las hojas según el lugar donde crece el árbol? |
| Tipo de estudio | Observacional y comparativo: no se asignó sol o sombra, se eligieron árboles que ya estaban en cada condición |
| Lugar | Campus universitario, Cali (Valle del Cauca, Colombia) |
| Fechas de recolección | 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 y regla, y un script en R que calcula el área a partir de la foto |
El estudio se hizo en el campus de la universidad, en Cali, en el valle geográfico del río Cauca (zona tropical). (Si quieres citar altitud, temperatura o régimen de lluvias, busca el dato en una fuente, por ejemplo la estación meteorológica más cercana.) Dentro del campus hay árboles en zonas abiertas y bajo otras copas, lo que da un gradiente natural de luz.
El protocolo, en resumen:
Cada punto es un árbol. Haz clic en un punto para ver sus datos.
# Un registro por árbol (con coordenadas normalizadas)
sitios <- unique(data.frame(grupo = crudo$grupo,
id_arbol = tolower(trimws(crudo$id_arbol)),
lat = a_numero(crudo$lat), lon = a_numero(crudo$lon),
exposicion = tolower(trimws(crudo$exposicion)),
stringsAsFactors = FALSE))
sitios$hojas <- as.integer(table(tolower(trimws(crudo$id_arbol)))[sitios$id_arbol])
buscar_foto <- function(id) {
cand <- file.path(params$carpeta_fotos, paste0(id, c(".jpg", ".jpeg", ".png")))
ex <- cand[file.exists(cand)]
if (length(ex) > 0) ex[1] else NA_character_
}
sitios$foto <- vapply(sitios$id_arbol, buscar_foto, character(1))
cat("Árboles:", nrow(sitios), "| con foto:", sum(!is.na(sitios$foto)), "\n")
## Árboles: 5 | con foto: 0
sitios$popup <- vapply(seq_len(nrow(sitios)), function(i) {
s <- sitios[i, ]
img <- if (is.na(s$foto)) "" else paste0("<img src='", knitr::image_uri(s$foto), "' width='200'><br>")
paste0(img, "<b>", s$id_arbol, "</b><br>Exposición: ", s$exposicion, "<br>Grupo ", s$grupo,
"<br>Hojas registradas: ", s$hojas, "<br>", fmt(s$lat, 5), "; ", fmt(s$lon, 5))
}, character(1))
if (requireNamespace("leaflet", quietly = TRUE)) {
library(leaflet)
leaflet(sitios) %>%
addProviderTiles(providers$Esri.WorldImagery, group = "Satélite") %>%
addTiles(group = "Calles") %>%
addCircleMarkers(lng = ~lon, lat = ~lat, radius = 9, weight = 2, color = "white",
fillColor = ~col_expo[exposicion], fillOpacity = 0.95,
label = ~id_arbol, 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", opacity = 1) %>%
addScaleBar("bottomleft")
} else {
cat("El paquete 'leaflet' no está instalado: se muestra un mapa simple.\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, cex = 0.7, pos = 3)
legend("topright", legend = names(col_expo), pt.bg = col_expo, pch = 21, bty = "n")
}
if (any(!is.na(sitios$foto))) {
cat("## Fotos de los árboles\n\n")
for (i in which(!is.na(sitios$foto))) {
cat(sprintf("**%s** (%s)\n\n{width=30%%}\n\n", sitios$id_arbol[i], sitios$exposicion[i], sitios$foto[i]))
}
} else {
cat("*(Opcional: guarda una foto de la copa de cada árbol en la carpeta `", params$carpeta_fotos,
"/` con el ID como nombre, por ejemplo `sl1_la.jpg`, y aparecerá aquí.)*\n", sep = "")
}
(Opcional: guarda una foto de la copa de cada árbol en la carpeta
fotos/ con el ID como nombre, por ejemplo
sl1_la.jpg, y aparecerá aquí.)
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. (Escribe tu hipótesis con tus palabras y justifícala antes de mirar los resultados.)
Diseño. 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.
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 una plantilla: una fila por hoja, con grupo, árbol, coordenadas, número de hoja, área, exposición, fecha y observaciones.
Análisis (todo en R base, R version 4.6.1 (2026-06-24 ucrt)):
Antes de calcular nada hay que mirar la base. Se trabaja sobre una
copia (datos); la base original (crudo) no se
toca.
head(crudo, 6)
## grupo id_arbol lat lon n_hoja area_cm2 exposicion fecha
## 1 1 sl1_la 3.3458 -76.5337 1 37.42 sol 2026-10-22
## 2 1 sl1_la 3.3458 -76.5337 2 37.70 sol 2026-10-22
## 3 1 sl1_la 3.3458 -76.5337 3 37.34 sol 2026-10-22
## 4 1 sl1_la 3.3458 -76.5337 4 34.92 sol 2026-10-22
## 5 1 sl1_la 3.3458 -76.5337 5 32.49 sol 2026-10-22
## 6 1 sl1_la 3.3458 -76.5337 6 30.32 sol 2026-10-22
## observaciones
## 1 NA
## 2 NA
## 3 NA
## 4 NA
## 5 NA
## 6 NA
sapply(crudo, function(x) class(x)[1])
## grupo id_arbol lat lon n_hoja
## "numeric" "character" "numeric" "numeric" "numeric"
## area_cm2 exposicion fecha observaciones
## "numeric" "character" "Date" "logical"
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).
datos <- crudo
datos$fila <- seq_len(nrow(datos)) + 1L # fila en la hoja de cálculo (la 1 es el encabezado)
# Normalizar texto
n_id_corregido <- sum(datos$id_arbol != tolower(trimws(datos$id_arbol)))
n_expo_corregida <- sum(datos$exposicion != tolower(trimws(datos$exposicion)))
datos$id_arbol <- tolower(trimws(datos$id_arbol))
datos$exposicion <- tolower(trimws(datos$exposicion))
stopifnot(all(datos$exposicion %in% c("sol", "sombra")))
# Números (por si alguien escribió coma decimal)
datos$lat <- a_numero(datos$lat); datos$lon <- a_numero(datos$lon)
datos$area_cm2 <- a_numero(datos$area_cm2)
# 1. Áreas faltantes o no válidas
malas <- is.na(datos$area_cm2) | datos$area_cm2 <= 0
# 2. Filas repetidas (mismo árbol y mismo número de hoja)
repetida <- duplicated(datos[, c("id_arbol", "n_hoja")])
# 3. Hojas por árbol
n_por_arbol <- table(datos$id_arbol[!repetida])
# 4. ¿Cada árbol tiene una sola exposición?
n_expo <- tapply(datos$exposicion, datos$id_arbol, function(x) length(unique(x)))
# 5. ¿Cuántos árboles por grupo?
arb_por_grupo <- tapply(datos$id_arbol, datos$grupo, function(x) length(unique(x)))
# 6. Valores extremos respecto a su propio árbol
mediana_arbol <- ave(datos$area_cm2, datos$id_arbol, FUN = function(x) median(x[!is.na(x) & x > 0]))
datos$razon_mediana <- datos$area_cm2 / mediana_arbol
extremos <- !is.na(datos$razon_mediana) & datos$area_cm2 > 0 &
(datos$razon_mediana > 10 | datos$razon_mediana < 0.1)
# 7. Observaciones escritas
con_obs <- !is.na(datos$observaciones) & nzchar(trimws(as.character(datos$observaciones)))
# 8. Fechas por árbol
fechas_arbol <- tapply(format(datos$fecha, "%d/%m/%Y"), datos$id_arbol, function(x) paste(unique(x), collapse = ", "))
revision <- data.frame(
Revisión = c("IDs con mayúsculas o espacios de más", "Exposiciones escritas de varias formas",
"Á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",
"Filas con observaciones escritas"),
Resultado = c(n_id_corregido, n_expo_corregida, sum(malas), sum(repetida),
sum(n_expo > 1), sum(n_por_arbol != 10), sum(extremos), sum(con_obs)))
tabla(revision, align = "lr")
| Revisión | Resultado |
|---|---|
| IDs con mayúsculas o espacios de más | 0 |
| Exposiciones escritas de varias formas | 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 |
| Filas con observaciones escritas | 0 |
cat("Hojas por árbol:\n"); print(n_por_arbol)
## Hojas por árbol:
##
## sl1_ sl1_la sl2_bu sm1_gn sm2_na
## 10 10 10 10 10
cat("\nÁrboles por grupo:\n"); print(arb_por_grupo)
##
## Árboles por grupo:
## 1 2 3 4 5
## 1 1 1 1 1
cat("\nFecha(s) de medición por árbol:\n"); print(fechas_arbol)
##
## Fecha(s) de medición por árbol:
## sl1_ sl1_la sl2_bu sm1_gn sm2_na
## "29/10/2026" "22/10/2026" "23/10/2026" "22/10/2026" "29/10/2026"
cat("\nRango de n_hoja:", range(datos$n_hoja), "\n")
##
## Rango de n_hoja: 1 10
cat("\nÁrboles por exposición:\n"); print(table(unique(datos[, c("id_arbol", "exposicion")])$exposicion))
##
## Árboles por exposición:
##
## sol sombra
## 3 2
Formato de los IDs. Los IDs de esta base parecen un
código (por ejemplo sl1_la: sol, árbol 1, y las letras
finales podrían ser la especie). Comprueba que todos sigan el mismo
formato: un ID que no lo cumple puede ser un error de captura o un dato
incompleto.
ids <- sort(unique(datos$id_arbol))
formato <- ifelse(nchar(ids) == max(nchar(ids)), "igual que la mayoría", "DISTINTO: revisar")
tabla(data.frame(ID = ids, Formato = formato), align = "ll")
| ID | Formato |
|---|---|
| sl1_ | DISTINTO: revisar |
| sl1_la | igual que la mayoría |
| sl2_bu | igual que la mayoría |
| sm1_gn | igual que la mayoría |
| sm2_na | igual que la mayoría |
Ubicación. Las coordenadas deberían caer en la zona del campus. Se calcula la distancia entre cada par de árboles (fórmula de Haversine):
arb <- aggregate(cbind(lat, lon) ~ id_arbol + grupo + exposicion, data = datos, FUN = mean)
pares <- t(combn(nrow(arb), 2))
d <- distancia_m(arb$lat[pares[, 1]], arb$lon[pares[, 1]], arb$lat[pares[, 2]], arb$lon[pares[, 2]])
dist_tab <- data.frame(arbol_a = arb$id_arbol[pares[, 1]], arbol_b = arb$id_arbol[pares[, 2]],
distancia_m = round(d))
dist_tab <- dist_tab[order(dist_tab$distancia_m), ]
tabla(dist_tab, row.names = FALSE, col.names = c("Árbol A", "Árbol B", "Distancia (m)"))
| Árbol A | Árbol B | Distancia (m) |
|---|---|---|
| sl2_bu | sm1_gn | 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 |
| sl1_ | sm2_na | 3641 |
| sl1_ | sm1_gn | 3744 |
| sl2_bu | sl1_ | 3766 |
| sl1_la | sl1_ | 3869 |
# Árboles de grupos distintos a menos de dist_duplicado_m: posible mismo árbol medido dos veces
otro_grupo <- arb$grupo[pares[, 1]] != arb$grupo[pares[, 2]]
repetidos <- dist_tab[d < params$dist_duplicado_m & otro_grupo, ]
# Árbol alejado del resto: distancia a la mediana de las coordenadas
dist_centro <- distancia_m(median(arb$lat), median(arb$lon), arb$lat, arb$lon)
lejanos <- arb$id_arbol[dist_centro > 1000]
Ningún par de árboles de grupos distintos está a menos de 5 m: no hay indicios de árboles medidos dos veces. Atención: sl1_ queda a más de 1 km del resto de árboles (3744 m). Si todos los árboles deben estar en el campus, revisa que sus coordenadas estén bien escritas (por ejemplo, un dígito mal copiado en la latitud o la longitud).
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$area_cm2_original <- datos$area_cm2
datos$usar <- TRUE
datos$motivo <- ""
datos$corregida <- FALSE
marcar <- function(filas, motivo) {
filas <- filas & datos$usar
datos$usar[filas] <<- FALSE
datos$motivo[filas] <<- motivo
sum(filas)
}
n_repetidas <- marcar(repetida, "fila repetida")
n_malas <- marcar(malas, "sin área o área <= 0")
# Error de unidad (mm² en lugar de cm²): se corrige dividiendo por 100
mm2 <- datos$usar & !is.na(datos$razon_mediana) & datos$razon_mediana > 10
datos$area_cm2[mm2] <- datos$area_cm2[mm2] / 100
datos$corregida[mm2] <- TRUE
n_mm2 <- sum(mm2)
limpios <- datos[datos$usar, ]
limpios$expo <- factor(limpios$exposicion, levels = c("sol", "sombra"))
orden_arbol <- unique(limpios[order(limpios$grupo, limpios$id_arbol), "id_arbol"])
limpios$id_arbol <- factor(limpios$id_arbol, levels = orden_arbol)
limpios <- limpios[order(limpios$id_arbol, limpios$n_hoja), ]
decisiones <- data.frame(
Problema = c("Fila repetida", "Área faltante o <= 0", "Área en mm² en lugar de cm²"),
Filas = c(n_repetidas, n_malas, n_mm2),
Decisión = c("Eliminar la copia", "Excluir la hoja (no hay dato válido)", "Dividir por 100"))
tabla(decisiones, align = "lrl")
| Problema | Filas | Decisión |
|---|---|---|
| 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 |
cat("Filas originales:", nrow(crudo), "\n",
"Filas que se usan:", nrow(limpios), "\n",
"Árboles:", length(unique(limpios$id_arbol)), "\n")
## Filas originales: 50
## Filas que se usan: 50
## Árboles: 5
if (params$guardar_limpios) {
write.csv(limpios[, c("grupo", "id_arbol", "lat", "lon", "n_hoja", "area_cm2", "exposicion",
"fecha", "observaciones", "corregida")],
"datos_limpios_area_foliar.csv", row.names = FALSE, na = "")
}
Idea clave. Ninguna decisión es “correcta” por sí sola; lo que importa es que sean explícitas y reproducibles.
resumen <- do.call(rbind, lapply(split(limpios, limpios$id_arbol), function(d) {
data.frame(arbol = as.character(d$id_arbol[1]), exposicion = d$exposicion[1], n = nrow(d),
media = mean(d$area_cm2), mediana = median(d$area_cm2), de = sd(d$area_cm2),
cv_pct = cv(d$area_cm2), minimo = min(d$area_cm2), maximo = max(d$area_cm2))
}))
tabla(resumen, row.names = FALSE, digits = c(0, 0, 0, 1, 1, 1, 1, 1, 1),
col.names = c("Árbol", "Exposición", "n", "Media", "Mediana", "DE", "CV (%)", "Mín.", "Máx."),
caption = "Área foliar (cm²) por árbol. DE = desviación estándar; CV = DE / media.")
| Á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 |
| sl1_ | sol | 10 | 14.7 | 14.6 | 1.4 | 9.8 | 12.7 | 16.3 |
Las medias de los árboles van de 14,7 a 67,1 cm², y el CV (variación de las hojas de un árbol respecto a su propia media) va de 10 % a 19 %. Compara la diferencia entre árboles con la variación dentro de cada árbol.
par(mar = c(4.5, 4.8, 3, 1))
col_arb <- col_expo[resumen$exposicion]
boxplot(area_cm2 ~ id_arbol, data = limpios, outline = FALSE, las = 1,
col = adjustcolor(col_arb, 0.55), border = "grey25",
xlab = "Árbol", ylab = expression("Área foliar (cm"^2*")"),
main = "Área foliar por árbol")
stripchart(area_cm2 ~ id_arbol, data = limpios, vertical = TRUE, method = "jitter", jitter = 0.12,
pch = 16, col = adjustcolor("black", 0.45), add = TRUE)
legend("topright", legend = c("sol", "sombra"), fill = adjustcolor(col_expo, 0.55), bty = "n")
Área foliar de cada hoja, por árbol. Color = exposición.
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:
fuera <- do.call(rbind, lapply(split(limpios, limpios$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, caption = "Hojas fuera de los límites de Tukey de su árbol.")
}
| id_arbol | n_hoja | area_cm2 | observaciones |
|---|---|---|---|
| sl2_bu | 2 | 50.14 | NA |
sens <- do.call(rbind, lapply(split(limpios, limpios$id_arbol), function(d) {
out <- boxplot.stats(d$area_cm2)$out
data.frame(arbol = as.character(d$id_arbol[1]), exposicion = d$exposicion[1],
media = mean(d$area_cm2), mediana = median(d$area_cm2),
media_sin_atipicas = mean(d$area_cm2[!d$area_cm2 %in% out]))
}))
dif_sens <- sapply(c("media", "mediana", "media_sin_atipicas"), function(v)
mean(sens[[v]][sens$exposicion == "sombra"]) - mean(sens[[v]][sens$exposicion == "sol"]))
tabla(data.frame(Resumen = c("Media (todas las hojas)", "Mediana", "Media sin las hojas atípicas"),
Dif = unname(dif_sens)), digits = 1,
col.names = c("Cómo se resume cada árbol", "Sombra − sol (cm²)"),
caption = "Sensibilidad de la diferencia entre exposiciones a las hojas atípicas.")
| 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 |
La diferencia entre sombra y sol va de 11,7 a 12,0 cm² según cómo se trate la hoja atípica (si la hay). Cuando una conclusión depende de una sola hoja, hay que decirlo.
simbolos <- c(16, 17, 15, 18, 8, 3, 4, 1)
simbolo <- simbolos[as.integer(limpios$id_arbol)]
par(mar = c(4.5, 4.8, 3, 1))
boxplot(area_cm2 ~ expo, data = limpios, outline = FALSE, las = 1,
col = adjustcolor(col_expo, 0.5), border = "grey25",
xlab = "Exposición", ylab = expression("Área foliar (cm"^2*")"), main = "Hojas por exposición")
set.seed(2)
points(as.integer(limpios$expo) + runif(nrow(limpios), -0.15, 0.15), limpios$area_cm2,
pch = simbolo, col = adjustcolor("black", 0.6))
legend("topleft", legend = levels(limpios$id_arbol), pch = simbolos[seq_along(levels(limpios$id_arbol))],
bty = "n", title = "Árbol", cex = 0.85)
Hojas de sol y de sombra. Cada símbolo es un árbol distinto.
Este gráfico es descriptivo. Fíjate en que los símbolos iguales (hojas del mismo árbol) tienden a quedar juntos: por eso las hojas no son independientes y no se pueden usar como si fueran 50 datos independientes en una prueba.
Lo que se asignó 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("sombra", "sol"))
medias <- medias[order(medias$exposicion, medias$id_arbol), ]
tabla(medias, row.names = FALSE, digits = 1, col.names = c("Árbol", "Exposición", "Media del árbol (cm²)"))
| Á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 |
| sl1_ | sol | 14.7 |
n_som <- sum(medias$exposicion == "sombra"); n_sol <- sum(medias$exposicion == "sol")
media_som <- mean(medias$area_cm2[medias$exposicion == "sombra"])
media_sol <- mean(medias$area_cm2[medias$exposicion == "sol"])
dif_g <- media_som - media_sol
Hay 2 árbol(es) de sombra y 3 de sol: 5 datos independientes, no 50. La media de los árboles de sombra es 41,5 cm² y la de sol 29,8 cm²; la diferencia observada (sombra − sol) es de 11,7 cm².
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.
y <- medias$area_cm2
es_sombra <- medias$exposicion == "sombra"
repartos <- combn(length(y), n_som)
dif_perm <- apply(repartos, 2, function(i) mean(y[i]) - mean(y[-i]))
dif_obs <- mean(y[es_sombra]) - mean(y[!es_sombra])
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 sombra − sol (cm²)", main = paste(ncol(repartos), "repartos posibles"))
points(dif_obs, 0, pch = 21, bg = col_expo["sombra"], cex = 2.6)
abline(v = 0, lty = 3)
legend("topright", legend = "reparto observado", pch = 21, pt.bg = col_expo["sombra"], bty = "n", pt.cex = 1.6)
Diferencia sombra − sol para cada reparto posible de las etiquetas entre los árboles.
Hay 10 repartos posibles y p = 0,600. Con 2 y 3 árboles, el valor de p más pequeño que cualquier resultado podría dar es 0,100: el diseño no puede dar resultados “significativos” al 5 %, sean cuales sean los datos.
t_hojas <- t.test(area_cm2 ~ expo, data = limpios) # incorrecto: hojas como independientes
t_arbol <- t.test(area_cm2 ~ exposicion, data = medias) # promedios por árbol (Welch)
comparacion <- 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$expo == "sol"), n_sol, n_sol),
n_sombra = c(sum(limpios$expo == "sombra"), n_som, n_som),
p = c(fmt_p(t_hojas$p.value), fmt_p(t_arbol$p.value), fmt_p(p_perm)))
tabla(comparacion, col.names = c("Análisis", "n sol", "n sombra", "p"), align = "lrrr")
| Análisis | n sol | n sombra | p |
|---|---|---|---|
| Hojas como datos independientes (incorrecto) | 30 | 20 | 0,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.
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 entre árboles y los efectos se interpretan como razones (“1,3 veces”).
por_arbol <- aggregate(area_cm2 ~ id_arbol, data = limpios, FUN = function(x) c(media = mean(x), de = sd(x)))
por_arbol_log <- aggregate(log(area_cm2) ~ id_arbol, data = limpios, 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.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.
m1 <- lm(log(area_cm2) ~ id_arbol, data = limpios)
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).")
| 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.
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 = limpios)
kw1 <- kruskal.test(log(area_cm2) ~ id_arbol, data = limpios)
c(shapiro_p = shap1$p.value, fligner_p = flig1$p.value, kruskal_p = kw1$p.value)
## 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; Tukey HSD compara todos los pares y controla ese error para el conjunto.
tk <- TukeyHSD(aov(log(area_cm2) ~ id_arbol, data = limpios))$id_arbol
tk_tab <- data.frame(comparacion = 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), ]
# ¿Los árboles del par tienen la misma exposición?
expo_de <- setNames(resumen$exposicion, resumen$arbol)
# los IDs tienen guion bajo pero no guion, así que se separa por "-"
partes <- strsplit(tk_tab$comparacion, "-")
misma_expo <- sapply(partes, function(x) expo_de[x[1]] == expo_de[x[2]])
n_misma_sig <- sum(misma_expo & tk_tab$p_aj < 0.05)
tabla(data.frame(Comparación = gsub("-", " / ", tk_tab$comparacion),
Razón = fmt(tk_tab$razon, 2),
IC = paste0("[", fmt(tk_tab$li, 2), "; ", fmt(tk_tab$ls, 2), "]"),
p = fmt_p(tk_tab$p_aj),
Dif = ifelse(tk_tab$p_aj < 0.05, "sí", "no"),
Expo = ifelse(misma_expo, "misma", "distinta")),
col.names = c("Comparación (A / B)", "Razón A / B", "IC 95 % (Tukey)", "p ajustado",
"Difieren (5 %)", "Exposición del par"),
align = "lrrrrr",
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 |
| sl1_ / sm1_gn | 0,22 | [0,19; 0,26] | < 0,001 | sí | distinta |
| sl1_ / 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 |
| sl1_ / 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 |
| sl1_ / sm2_na | 0,92 | [0,78; 1,10] | 0,677 | no | distinta |
k <- nrow(tk_tab)
par(mar = c(4.5, 10, 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 = gsub("-", " / ", 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 9 de las 10 comparaciones los árboles difieren de forma detectable. De esas, 4 son entre árboles de la misma exposición: dos árboles de sol (o dos de sombra) pueden diferir tanto como uno 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.
Las hojas están dentro de árboles y los árboles dentro de una exposición. El efecto de la exposición se prueba contra la variación entre árboles, no contra la variación entre hojas.
m_anid <- lm(log(area_cm2) ~ expo + id_arbol, data = limpios) # cada árbol tiene una sola exposición
a_an <- tabla_anova(m_anid)
# Fila 1 = exposición, fila 2 = árbol (dentro de exposición), fila 3 = hojas
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]
tab_an <- data.frame(
Fuente = c("Exposición", "Árbol (dentro de exposición)", "Hojas (residual)"),
gl = a_an$gl, SC = fmt(a_an$SC, 3), CM = fmt(a_an$CM, 3),
Contra = c("CM de árbol", "CM de hojas", ""),
F = c(fmt(F_expo, 2), fmt(F_arbol, 2), ""),
p = c(fmt_p(p_expo), fmt_p(p_arbol), ""))
tabla(tab_an, row.names = FALSE, align = "lrrrlrr",
caption = "ANOVA anidado sobre log(área): hojas dentro de árboles dentro de exposición.")
| 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 |
Cuánto de la variación se debe al árbol y cuánto a las hojas (componentes de varianza, aproximados):
n0 <- nrow(limpios) / nlevels(limpios$id_arbol)
s2_res <- a_an$CM[3]
s2_arb <- max((a_an$CM[2] - a_an$CM[3]) / n0, 0)
pct_arb <- 100 * s2_arb / (s2_arb + s2_res)
c(var_arbol = s2_arb, var_hoja = s2_res, pct_arbol = pct_arb)
## var_arbol var_hoja pct_arbol
## 0.53796435 0.01811122 96.74302924
r2_arb <- summary(m1)$r.squared
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"),
Resultado = c(paste0("F = ", fmt(a1$F[1], 1), ", p ", ifelse(a1$p[1] < 0.001, "< 0,001", paste("=", fmt_p(a1$p[1])))),
paste(sum(tk_tab$p_aj < 0.05), "de", nrow(tk_tab), "pares"),
paste0("p = ", fmt_p(t_arbol$p.value)),
paste0("p = ", fmt_p(p_perm)),
paste0("F = ", fmt(F_expo, 2), ", p = ", fmt_p(p_expo))))
tabla(resumen_a, align = "lll", caption = "Resumen de los análisis.")
| 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 | 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")
}
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.
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 intervalos y 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.
Posibles explicaciones ambientales (hipótesis, no algo que estos datos demuestren):
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. |
R.version.string
## [1] "R version 4.6.1 (2026-06-24 ucrt)"
Sys.Date()
## [1] "2026-11-02"