Taller de Diseño de Experimentos — Punto 1 (Suelo/Biomasa), Punto 2 (Moluscos) y Punto 3 (Biodiversidad de anfibios)
Para estudiar la relación entre ciertas características del suelo y la producción de biomasa (g) de una planta forrajera natural se obtuvieron 45 muestras en diferentes ambientes. En cada una se estimó la biomasa (variable respuesta, \(Y\)) y se registraron cuatro características del suelo (covariables, \(X\)): pH, Salinidad, Zinc y Potasio.
table1(~ Biomasa + pH + Salinidad + Zinc + Potasio, data = Salinidad)
| Overall (N=45) |
|
|---|---|
| Biomasa | |
| Mean (SD) | 1080 (546) |
| Median [Min, Max] | 992 [370, 2340] |
| pH | |
| Mean (SD) | 4.61 (1.25) |
| Median [Min, Max] | 4.45 [3.20, 7.45] |
| Salinidad | |
| Mean (SD) | 30.3 (3.72) |
| Median [Min, Max] | 30.0 [24.0, 38.0] |
| Zinc | |
| Mean (SD) | 17.8 (8.27) |
| Median [Min, Max] | 19.2 [0.211, 31.3] |
| Potasio | |
| Mean (SD) | 797 (298) |
| Median [Min, Max] | 773 [351, 1440] |
largo <- pivot_longer(Salinidad, everything(),
names_to = "variable", values_to = "valor")
ggplot(largo, aes(x = valor)) +
geom_histogram(bins = 10, fill = "#2E86AB", color = "white") +
facet_wrap(~ variable, scales = "free") +
theme_minimal(base_family = "sans") +
labs(title = "Distribución de cada variable", x = NULL, y = "Frecuencia") +
theme(plot.title = element_text(hjust = 0.5, face = "bold"))
ggplot(largo, aes(y = valor)) +
geom_boxplot(fill = "#AED6F1", color = "#1a5276") +
facet_wrap(~ variable, scales = "free") +
theme_minimal(base_family = "sans") +
labs(title = "Boxplots por variable", y = NULL) +
theme(plot.title = element_text(hjust = 0.5, face = "bold"),
axis.text.x = element_blank())
vars <- c("Biomasa", "pH", "Salinidad", "Zinc", "Potasio")
resumen_uni <- data.frame(
Variable = vars,
`Shapiro-Wilk (W)` = sapply(Salinidad[vars], function(x) shapiro.test(x)$statistic),
`Valor p` = sapply(Salinidad[vars], function(x) {
p <- shapiro.test(x)$p.value
formatC(p, format = "f", digits = 8)
}),
`CV (%)` = sapply(Salinidad[vars], function(x) sd(x) / mean(x) * 100),
check.names = FALSE
)
tabla_bonita(resumen_uni, caption = "Prueba de normalidad y coeficiente de variación por variable")
| Variable | Shapiro-Wilk (W) | Valor p | CV (%) | |
|---|---|---|---|---|
| Biomasa.W | Biomasa | 0.897 | 0.00077847 | 50.481 |
| pH.W | pH | 0.882 | 0.00027797 | 27.224 |
| Salinidad.W | Salinidad | 0.950 | 0.05074950 | 12.290 |
| Zinc.W | Zinc | 0.930 | 0.00913530 | 46.404 |
| Potasio.W | Potasio | 0.922 | 0.00502078 | 37.319 |
Solo Salinidad se acerca a una distribución normal, las demás variables muestran asimetría, y hay cinco valores atípicos de pH y Zinc que merecen atención especial, pues corresponden a las mismas cinco observaciones.
Se evaluó la relación entre la biomasa y las covariables pH, Salinidad y Zinc, tanto de forma gráfica como numérica (correlación y regresión simple).
Salinidad$pH_g <- cut(Salinidad$pH, breaks = 3, labels = c("Bajo", "Medio", "Alto"))
Salinidad$Sal_g <- cut(Salinidad$Salinidad, breaks = 3, labels = c("Bajo", "Medio", "Alto"))
Salinidad$Zn_g <- cut(Salinidad$Zinc, breaks = 3, labels = c("Bajo", "Medio", "Alto"))
table1(~ Biomasa | pH_g, data = Salinidad)
| Bajo (N=26) |
Medio (N=14) |
Alto (N=5) |
Overall (N=45) |
|
|---|---|---|---|---|
| Biomasa | ||||
| Mean (SD) | 751 (258) | 1280 (287) | 2260 (74.3) | 1080 (546) |
| Median [Min, Max] | 710 [370, 1200] | 1330 [765, 1900] | 2270 [2160, 2340] | 992 [370, 2340] |
table1(~ Biomasa | Sal_g, data = Salinidad)
| Bajo (N=16) |
Medio (N=19) |
Alto (N=10) |
Overall (N=45) |
|
|---|---|---|---|---|
| Biomasa | ||||
| Mean (SD) | 1090 (421) | 1050 (607) | 1120 (650) | 1080 (546) |
| Median [Min, Max] | 1150 [466, 1900] | 896 [370, 2340] | 888 [497, 2330] | 992 [370, 2340] |
table1(~ Biomasa | Zn_g, data = Salinidad)
| Bajo (N=7) |
Medio (N=21) |
Alto (N=17) |
Overall (N=45) |
|
|---|---|---|---|---|
| Biomasa | ||||
| Mean (SD) | 1960 (524) | 1070 (342) | 735 (325) | 1080 (546) |
| Median [Min, Max] | 2220 [1190, 2340] | 1010 [546, 1900] | 612 [370, 1410] | 992 [370, 2340] |
ggplot(Salinidad, aes(x = pH, y = Biomasa)) +
geom_point(color = "#2c3e50", alpha = 0.75) +
geom_smooth(color = "#C0392B", fill = "#f5b7b1") +
theme_minimal(base_family = "sans") +
labs(title = "Biomasa vs pH") +
theme(plot.title = element_text(hjust = 0.5, face = "bold"))
ggplot(Salinidad, aes(x = Salinidad, y = Biomasa)) +
geom_point(color = "#2c3e50", alpha = 0.75) +
geom_smooth(color = "#C0392B", fill = "#f5b7b1") +
theme_minimal(base_family = "sans") +
labs(title = "Biomasa vs Salinidad") +
theme(plot.title = element_text(hjust = 0.5, face = "bold"))
ggplot(Salinidad, aes(x = Zinc, y = Biomasa)) +
geom_point(color = "#2c3e50", alpha = 0.75) +
geom_smooth(color = "#C0392B", fill = "#f5b7b1") +
theme_minimal(base_family = "sans") +
labs(title = "Biomasa vs Zinc") +
theme(plot.title = element_text(hjust = 0.5, face = "bold"))
resumen_bi <- do.call(rbind, lapply(c("pH", "Salinidad", "Zinc"), function(v) {
ct <- cor.test(Salinidad$Biomasa, Salinidad[[v]])
mod <- lm(reformulate(v, "Biomasa"), data = Salinidad)
data.frame(
Covariable = v,
r = unname(ct$estimate),
`Valor p` = ct$p.value,
`R²` = summary(mod)$r.squared,
Pendiente = unname(coef(mod)[2]),
check.names = FALSE
)
}))
tabla_bonita(resumen_bi, caption = "Correlación de Pearson y regresión simple: Biomasa vs. cada covariable")
| Covariable | r | Valor p | R² | Pendiente |
|---|---|---|---|---|
| pH | 0.928 | 0.000 | 0.861 | 404.079 |
| Salinidad | -0.067 | 0.664 | 0.004 | -9.778 |
| Zinc | -0.781 | 0.000 | 0.611 | -51.595 |
Ranking de relación con la biomasa: pH > Zinc > Salinidad.
pH y Zinc están correlacionados entre sí (r = -0.72). Las cinco observaciones con pH ≥ 7.1 son justamente las de menor Zinc y mayor biomasa, y sostienen buena parte de la relación de Zinc con la biomasa. pH conserva el r más alto.
Como pH presentó la mayor relación con la biomasa, se categorizó en tres niveles usando terciles, lo que garantiza grupos balanceados (15 observaciones cada uno):
q <- quantile(Salinidad$pH, probs = c(1/3, 2/3))
Salinidad$pH_cat <- cut(Salinidad$pH,
breaks = c(-Inf, q[1], q[2], Inf),
labels = c("Bajo", "Medio", "Alto"))
Salinidad$pH_cat <- as.factor(Salinidad$pH_cat)
tabla_bonita(as.data.frame(table(Salinidad$pH_cat)) |>
setNames(c("Nivel de pH", "n")),
caption = "Número de observaciones por nivel de pH", digits = 0)
| Nivel de pH | n |
|---|---|
| Bajo | 15 |
| Medio | 15 |
| Alto | 15 |
modelo_sal <- aov(Biomasa ~ pH_cat, data = Salinidad)
tabla_bonita(tidy(modelo_sal), caption = "Tabla ANOVA: Biomasa ~ Nivel de pH")
| term | df | sumsq | meansq | statistic | p.value |
|---|---|---|---|---|---|
| pH_cat | 2 | 7712683 | 3856341.6 | 29.893 | 0 |
| Residuals | 42 | 5418235 | 129005.6 | NA | NA |
El nivel de pH genera diferencias significativas en la biomasa (F(2, 42) = 29.89, p < 0.001).
sh_sal <- shapiro.test(residuals(modelo_sal))
lv_sal <- leveneTest(Biomasa ~ pH_cat, data = Salinidad)
supuestos_sal <- data.frame(
Supuesto = c("Normalidad de residuales (Shapiro-Wilk)",
"Homogeneidad de varianzas (Levene)"),
Estadístico = c(unname(sh_sal$statistic), lv_sal$`F value`[1]),
`Valor p` = c(sh_sal$p.value, lv_sal$`Pr(>F)`[1]),
`¿Se cumple?` = c("Sí", "No"),
check.names = FALSE
)
tabla_bonita(supuestos_sal, caption = "Verificación de supuestos del modelo")
| Supuesto | Estadístico | Valor p | ¿Se cumple? |
|---|---|---|---|
| Normalidad de residuales (Shapiro-Wilk) | 0.973 | 0.375 | Sí |
| Homogeneidad de varianzas (Levene) | 8.675 | 0.001 | No |
resultado_sal <- LSD.test(modelo_sal, "pH_cat", console = TRUE)
grupos_lsd <- resultado_sal$groups
grupos_lsd$Nivel <- rownames(grupos_lsd)
grupos_lsd <- grupos_lsd[, c("Nivel", "Biomasa", "groups")]
names(grupos_lsd) <- c("Nivel de pH", "Media Biomasa", "Grupo")
tabla_bonita(grupos_lsd, caption = paste0("Prueba LSD (Fisher). LSD crítico = ",
round(resultado_sal$statistics$LSD, 2)))
| Nivel de pH | Media Biomasa | Grupo | |
|---|---|---|---|
| Alto | Alto | 1605.385 | a |
| Medio | Medio | 1048.109 | b |
| Bajo | Bajo | 593.023 | c |
Los tres niveles de pH obtuvieron letras distintas (a, b, c), lo que indica que los tres difieren significativamente entre sí. no hay ningún par de niveles con biomasa estadísticamente igual. La biomasa aumenta de forma consistente con el nivel de pH (Alto > Medio > Bajo), y las tres diferencias de medias (1012.4, 557.3 y 455.1) superan la diferencia mínima significativa (LSD = 264.67).
La biomasa de la planta forrajera está fuertemente asociada con el pH del suelo y, en menor medida, con la concentración de Zinc, mientras que la Salinidad no muestra relación relevante. Al categorizar el pH en tres niveles, el ANOVA confirmó diferencias significativas en la biomasa entre ellos, con un patrón claro de mayor biomasa a mayor pH.
Se midió el consumo de oxígeno (proporción de O₂ por unidad de peso seco) en dos tipos de moluscos (A y B) sometidos a tres concentraciones de agua de mar (100 %, 75 % y 50 %). El diseño es completamente balanceado: 8 observaciones por cada una de las 6 combinaciones, para un total de 48 unidades experimentales.
tabla_desc <- data.frame(
VARIABLE = c("cons_o", "molusco", "c_agua"),
TIPO = c("Cuantitativa continua", "Cualitativa (factor)", "Cualitativa (factor)"),
ESCALA = c("Razón", "Nominal", "Ordinal"),
`NIVELES / RANGO` = c(
"1.80 – 18.80",
"A (24), B (24)",
"50 % (16), 75 % (16), 100 % (16)"
),
check.names = FALSE)
gt(tabla_desc) %>%
tab_header(title = md("**Descripción de variables**")) %>%
cols_align(align = "left") %>%
tab_style(
style = cell_fill(color = "#eaf2f8"),
locations = cells_column_labels()
) %>%
tab_style(
style = list(cell_fill(color = "#fbfcfd"),
cell_text(color = "#1a5276", weight = "bold")),
locations = cells_body(columns = VARIABLE)
) %>%
tab_options(table.font.size = 13)
| Descripción de variables | |||
| VARIABLE | TIPO | ESCALA | NIVELES / RANGO |
|---|---|---|---|
| cons_o | Cuantitativa continua | Razón | 1.80 – 18.80 |
| molusco | Cualitativa (factor) | Nominal | A (24), B (24) |
| c_agua | Cualitativa (factor) | Ordinal | 50 % (16), 75 % (16), 100 % (16) |
Ambos factores están perfectamente balanceados, 24 observaciones por tipo de molusco y 16 por concentración, con 8 réplicas en cada celda. Este balance es deseable porque hace que las sumas de cuadrados de los efectos sean ortogonales (no dependen del orden en que se incluyan en el modelo) y otorga máxima potencia al contraste, además de dar robustez frente a desviaciones moderadas de los supuestos.
p1 <- ggplot(BD, aes(x = cons_o)) +
geom_histogram(bins = 10, fill = "#2E86AB", color = "white") +
geom_vline(xintercept = mean(BD$cons_o), color = "#C0392B") +
geom_vline(xintercept = median(BD$cons_o), color = "darkgreen",
linetype = "dashed") +
labs(title = "Distribución consumo de oxígeno",
x = "Consumo de O2", y = "Frecuencia") +
theme_minimal(base_family = "sans") +
theme(plot.title = element_text(hjust = 0.5, face = "bold"))
p2 <- ggplot(BD, aes(y = cons_o)) +
geom_boxplot(fill = "#AED6F1", color = "#1a5276") +
labs(y = "Consumo de O2") +
theme_minimal(base_family = "sans") +
theme(axis.text.x = element_blank(), axis.ticks.x = element_blank())
grid.newpage()
pushViewport(viewport(layout = grid.layout(1, 2,
widths = unit(c(3, 2), "null"))))
print(p1, vp = viewport(layout.pos.row = 1, layout.pos.col = 1))
print(p2, vp = viewport(layout.pos.row = 1, layout.pos.col = 2))
Figura 1. Distribución del consumo de oxígeno para las 48 observaciones, ignorando los factores. La media (línea roja) y la mediana (línea punteada) casi coinciden.
mi_render <- function(x, ...) {
x <- x[!is.na(x)]
c(
"Media (DE)" = sprintf("%.2f (%.2f)", mean(x), sd(x)),
"Mediana [Q1, Q3]" = sprintf("%.2f [%.2f, %.2f]",
median(x),
quantile(x, 0.25, names = FALSE),
quantile(x, 0.75, names = FALSE)),
"Mín, Máx" = sprintf("%.2f, %.2f", min(x), max(x)),
"CV (%)" = sprintf("%.1f", 100 * sd(x) / mean(x))
)
}
table1(~ cons_o + c_agua + molusco, data = BD)
| Overall (N=48) |
|
|---|---|
| Consumo de O2 | |
| Mean (SD) | 9.30 (3.68) |
| Median [Min, Max] | 9.70 [1.80, 18.8] |
| Concentración de agua de mar | |
| 50% | 16 (33.3%) |
| 75% | 16 (33.3%) |
| 100% | 16 (33.3%) |
| Tipo de molusco | |
| A | 24 (50.0%) |
| B | 24 (50.0%) |
table1(~ cons_o, data = BD,
render.continuous = mi_render,
render.missing = NULL)
| Overall (N=48) |
|
|---|---|
| Consumo de O2 | 9.30 (3.68) |
| Mediana [Q1, Q3] | 9.70 [6.31, 11.23] |
| Mín, Máx | 1.80, 18.80 |
| CV (%) | 39.6 |
La media (9.30) y la mediana (9.70) son similares, indicando una distribución simétrica con alta variabilidad (CV = 39.6%). Se detecta un atípico (18.80) que se conserva por no ser un valor biológicamente imposible. El diseño está perfectamente balanceado: 24/24 por molusco y 16/16/16 por concentración.
tabla_conc <- BD %>%
group_by(Concentración = c_agua) %>%
summarise(
N = n(),
Media = mean(cons_o),
Mediana = median(cons_o),
D.E. = sd(cons_o),
Mín = min(cons_o),
Máx = max(cons_o)
)
tabla_bonita(tabla_conc, caption = "Resumen del consumo de O2 por concentración de agua de mar")
| Concentración | N | Media | Mediana | D.E. | Mín | Máx |
|---|---|---|---|---|---|---|
| 50% | 16 | 12.251 | 11.455 | 3.200 | 6.38 | 18.8 |
| 75% | 16 | 6.992 | 6.430 | 2.804 | 1.80 | 13.2 |
| 100% | 16 | 8.671 | 8.595 | 3.001 | 3.68 | 14.0 |
tabla_molusco <- BD %>%
group_by(Molusco = molusco) %>%
summarise(
N = n(),
Media = mean(cons_o),
Mediana = median(cons_o),
D.E. = sd(cons_o),
Mín = min(cons_o),
Máx = max(cons_o)
)
tabla_bonita(tabla_molusco, caption = "Resumen del consumo de O2 por tipo de molusco")
| Molusco | N | Media | Mediana | D.E. | Mín | Máx |
|---|---|---|---|---|---|---|
| A | 24 | 10.000 | 9.74 | 3.269 | 5.2 | 18.8 |
| B | 24 | 8.609 | 8.06 | 4.002 | 1.8 | 17.7 |
p3 <- ggplot(BD, aes(x = c_agua, y = cons_o, fill = c_agua)) +
geom_boxplot(alpha = 0.8) +
geom_jitter(width = 0.08, size = 1.3, alpha = 0.6) +
scale_fill_manual(values = c("#AED6F1", "#5DADE2", "#1a5276")) +
labs(x = "Concentración de agua de mar", y = "Consumo de O2") +
theme_minimal(base_family = "sans") + theme(legend.position = "none")
p4 <- ggplot(BD, aes(x = molusco, y = cons_o, fill = molusco)) +
geom_boxplot(alpha = 0.8) +
geom_jitter(width = 0.08, size = 1.3, alpha = 0.6) +
scale_fill_manual(values = c("#AED6F1", "#1a5276")) +
labs(x = "Tipo de molusco", y = "Consumo de O2") +
theme_minimal(base_family = "sans") + theme(legend.position = "none")
grid.newpage()
pushViewport(viewport(layout = grid.layout(1, 2,
widths = unit(c(3, 2), "null"))))
print(p3, vp = viewport(layout.pos.row = 1, layout.pos.col = 1))
print(p4, vp = viewport(layout.pos.row = 1, layout.pos.col = 2))
Figura 2. Consumo de oxígeno por concentración de agua de mar (izquierda) y por tipo de molusco (derecha). Los puntos son las observaciones individuales.
El consumo no varía de forma lineal con la concentración, es máximo al 50% (12.25), mínimo al 75% (6.99) y repunta al 100% (8.67), posiblemente por estrés osmótico en agua diluida. La diferencia entre moluscos es pequeña (A=10.00, B=8.61). El gráfico de interacción muestra líneas paralelas entre A y B, sugiriendo que ambos responden igual a la salinidad.
tabla_celdas <- BD %>%
group_by(Molusco = molusco, Concentración = c_agua) %>%
summarise(
N = n(),
Media = mean(cons_o),
Mediana = median(cons_o),
D.E. = sd(cons_o),
.groups = "drop"
) %>%
mutate(`CV (%)` = 100 * D.E. / Media)
tabla_bonita(tabla_celdas, caption = "Resumen del consumo de O2 por combinación molusco × concentración")
| Molusco | Concentración | N | Media | Mediana | D.E. | CV (%) |
|---|---|---|---|---|---|---|
| A | 50% | 8 | 12.175 | 11.110 | 3.090 | 25.381 |
| A | 75% | 8 | 7.890 | 7.180 | 2.740 | 34.722 |
| A | 100% | 8 | 9.936 | 9.295 | 2.748 | 27.656 |
| B | 50% | 8 | 12.326 | 12.850 | 3.518 | 28.540 |
| B | 75% | 8 | 6.095 | 5.595 | 2.739 | 44.940 |
| B | 100% | 8 | 7.406 | 6.140 | 2.844 | 38.401 |
ggplot(BD, aes(x = c_agua, y = cons_o, fill = molusco)) +
geom_boxplot(alpha = 0.85) +
scale_fill_manual(values = c("#AED6F1", "#1a5276")) +
labs(title = "Consumo de O2 en las seis combinaciones factor × factor",
x = "Concentración de agua de mar", y = "Consumo de O2",
fill = "Molusco") +
theme_minimal(base_family = "sans") +
theme(plot.title = element_text(hjust = 0.5, face = "bold"))
Figura 3. Consumo de oxígeno en las seis combinaciones factor × factor.
ggplot(BD, aes(x = c_agua, y = cons_o, color = molusco, group = molusco)) +
stat_summary(fun = mean, geom = "point", size = 2.5) +
stat_summary(fun = mean, geom = "line", linewidth = 1) +
stat_summary(fun.data = mean_se, geom = "errorbar", width = 0.1) +
scale_color_manual(values = c("#C0392B", "#1a5276")) +
labs(title = "Gráfico de interacción: media de cada celda ± error estándar",
x = "Concentración de agua de mar", y = "Consumo medio de O2",
color = "Molusco") +
theme_minimal(base_family = "sans") +
theme(plot.title = element_text(hjust = 0.5, face = "bold"))
Figura 4. Gráfico de interacción con la media de cada celda ± error estándar.
El gráfico de interacción muestra dos perfiles con la misma forma, en ambos tipos el consumo alcanza su máximo al 50 %, cae al mínimo al 75 % y repunta ligeramente al 100 %. Las líneas son prácticamente paralelas y no se cruzan, lo cual sugiere ausencia de interacción, el efecto de la salinidad sobre el consumo de oxígeno opera en la misma dirección y con magnitud similar en los dos moluscos.
Los coeficientes de variación dentro de las celdas oscilan entre 25 % y 45 %, sin un patrón de crecimiento asociado a la media, lo cual es un primer indicio favorable para el supuesto de homogeneidad de varianzas.
Se contrastan tres juegos de hipótesis, efecto principal del tipo de molusco, efecto principal de la concentración y efecto de interacción. El orden correcto de lectura es de abajo hacia arriba: primero la interacción, porque si resultara significativa, los efectos principales no serían interpretables por separado.
modelo_mol <- aov(cons_o ~ molusco * c_agua, data = BD)
anova_raw <- summary(modelo_mol)[[1]]
tabla_anova <- data.frame(
Fuente = c("Tipo de molusco", "Concentración de agua",
"Molusco × Concentración", "Error (residual)", "Total"),
SC = c(anova_raw$`Sum Sq`, sum(anova_raw$`Sum Sq`)),
GL = c(anova_raw$Df, sum(anova_raw$Df)),
CM = c(anova_raw$`Mean Sq`, NA),
F = c(anova_raw$`F value`[1:3], NA, NA),
`Valor p` = c(anova_raw$`Pr(>F)`[1:3], NA, NA),
check.names = FALSE
)
tabla_fmt <- tabla_anova
tabla_fmt$CM <- ifelse(is.na(tabla_fmt$CM), "—", sprintf("%.3f", tabla_fmt$CM))
tabla_fmt$F <- ifelse(is.na(tabla_fmt$F), "—", sprintf("%.3f", tabla_fmt$F))
tabla_fmt$`Valor p` <- ifelse(is.na(tabla_fmt$`Valor p`), "—",
ifelse(tabla_fmt$`Valor p` < 0.0001, "< 0.0001",
sprintf("%.4f", tabla_fmt$`Valor p`)))
tabla_fmt$SC <- sprintf("%.3f", tabla_fmt$SC)
tabla_bonita(tabla_fmt, caption = "Tabla de análisis de varianza (dos vías con interacción)")
| Fuente | SC | GL | CM | F | Valor p |
|---|---|---|---|---|---|
| Tipo de molusco | 23.227 | 1 | 23.227 | 2.651 | 0.1110 |
| Concentración de agua | 230.816 | 2 | 115.408 | 13.171 | < 0.0001 |
| Molusco × Concentración | 15.356 | 2 | 7.678 | 0.876 | 0.4238 |
| Error (residual) | 368.011 | 42 | 8.762 | — | — |
| Total | 637.410 | 47 | — | — | — |
La interacción molusco×concentración no es significativa (p=0.424), por lo que los efectos principales se interpretan por separado. El tipo de molusco tampoco resulta significativo (p=0.111). La concentración sí lo es (p<0.0001), indicando que al menos un nivel difiere de los demás.
res <- residuals(modelo_mol)
diag <- data.frame(ajustados = fitted(modelo_mol), residuales = res)
ggplot(diag, aes(x = ajustados, y = residuales)) +
geom_point(color = "#2c3e50", alpha = 0.75) +
geom_hline(yintercept = 0, color = "#C0392B") +
labs(title = "Residuales vs. valores ajustados",
x = "Valores ajustados", y = "Residuales") +
theme_minimal(base_family = "sans") +
theme(plot.title = element_text(hjust = 0.5, face = "bold"))
ggplot(diag, aes(sample = residuales)) +
stat_qq(color = "#2c3e50", alpha = 0.75) +
stat_qq_line(color = "#C0392B") +
labs(title = "Gráfico Q-Q de los residuales",
x = "Cuantiles teóricos", y = "Cuantiles muestrales") +
theme_minimal(base_family = "sans") +
theme(plot.title = element_text(hjust = 0.5, face = "bold"))
ggplot(diag, aes(x = residuales)) +
geom_histogram(bins = 10, fill = "#AED6F1", color = "#1a5276") +
labs(title = "Histograma de residuales", x = "Residual", y = "Frecuencia") +
theme_minimal(base_family = "sans") +
theme(plot.title = element_text(hjust = 0.5, face = "bold"))
Figura 5. Diagnóstico del modelo: residuales frente a valores ajustados, gráfico cuantil-cuantil e histograma de residuales.
sh_mol <- shapiro.test(res)
bt_mol <- bartlett.test(cons_o ~ interaction(molusco, c_agua), data = BD)
tabla_supuestos_mol <- data.frame(
Supuesto = c("Normalidad de residuales", "Homogeneidad de varianzas"),
Prueba = c("Shapiro–Wilk", "Bartlett"),
Estadístico = c(sprintf("W = %.3f", sh_mol$statistic),
sprintf("χ² = %.3f", bt_mol$statistic)),
`Valor p` = c(sprintf("%.4f", sh_mol$p.value), sprintf("%.4f", bt_mol$p.value)),
`Decisión (α = 0.05)` = ifelse(c(sh_mol$p.value, bt_mol$p.value) > 0.05,
"No se rechaza H₀", "Se rechaza H₀"),
check.names = FALSE
)
tabla_bonita(tabla_supuestos_mol, caption = "Verificación de supuestos del modelo (dos vías)")
| Supuesto | Prueba | Estadístico | Valor p | Decisión (α = 0.05) |
|---|---|---|---|---|
| Normalidad de residuales | Shapiro–Wilk | W = 0.958 | 0.0857 | No se rechaza H₀ |
| Homogeneidad de varianzas | Bartlett | χ² = 0.712 | 0.9823 | No se rechaza H₀ |
Ambos supuestos se cumplen: normalidad de residuales (Shapiro-Wilk, p=0.086) y homogeneidad de varianzas (Bartlett, p=0.982), ambos con p>0.05.
1. Interacción molusco × concentración (F2,42 = 0.876; p = 0.4238). No significativa. No hay evidencia de que el efecto de la concentración de agua de mar dependa del tipo de molusco. Esto valida cuantitativamente lo observado en la Figura 3, los perfiles de respuesta son paralelos. En consecuencia, los efectos principales sí pueden interpretarse de forma independiente y el modelo puede leerse como aditivo.
2. Tipo de molusco (F1,42 = 2.651; p = 0.1110). No significativo. Aunque el molusco A consumió en promedio 1.39 unidades más de oxígeno que el B, esa diferencia no supera lo que cabría esperar por azar dada la variabilidad dentro de los grupos. No hay evidencia de que los dos tipos de moluscos difieran en su consumo de oxígeno. Al no ser significativo, no procede realizar comparaciones post-hoc para este factor (y además, con solo dos niveles, la prueba F ya es en sí misma la comparación entre ambos).
3. Concentración de agua de mar (F2,42 = 13.171; p < 0.0001). Altamente significativo. Existe evidencia muy fuerte de que al menos una de las tres concentraciones produce un consumo de oxígeno medio distinto de las demás. Este factor por sí solo explica el 36.2 % de la variabilidad total (230.8 de 637.4). Como la prueba F es global y no indica cuáles niveles difieren, se procede con las comparaciones múltiples.
La diferencia mínima significativa se calcula con el cuadrado medio del error del modelo completo y sus 42 grados de libertad, sobre las medias marginales de concentración (n = 16 por nivel):
CME <- summary(modelo_mol)[[1]]["Residuals", "Mean Sq"]
gl <- df.residual(modelo_mol)
n_por_nivel <- 16
LSD <- qt(0.975, gl) * sqrt(2 * CME / n_por_nivel)
medias <- tapply(BD$cons_o, BD$c_agua, mean)
se <- sqrt(2 * CME / n_por_nivel)
pares <- combn(names(medias), 2, function(par) {
dif <- medias[par[1]] - medias[par[2]]
t <- dif / se
p <- 2 * (1 - pt(abs(t), gl))
data.frame(
Comparación = paste(par[1], "−", par[2]),
`Diferencia de medias` = round(dif, 3),
`Dif vs. LSD` = paste0(round(abs(dif), 3),
ifelse(abs(dif) > LSD, " > ", " < "), round(LSD, 3)),
`IC 95%` = sprintf("(%.3f ; %.3f)", dif - LSD, dif + LSD),
`Valor p` = ifelse(p < 0.0001, "< 0.0001", sprintf("%.4f", p)),
Conclusión = ifelse(abs(dif) > LSD, "Difieren", "No difieren"),
check.names = FALSE
)
}, simplify = FALSE)
tabla_comparaciones <- do.call(rbind, pares)
tabla_bonita(tabla_comparaciones, caption = "Comparaciones pareadas (LSD) entre concentraciones de agua de mar")
| Comparación | Diferencia de medias | Dif vs. LSD | IC 95% | Valor p | Conclusión | |
|---|---|---|---|---|---|---|
| 50% | 50% − 75% | 5.258 | 5.258 > 2.112 | (3.146 ; 7.370) | < 0.0001 | Difieren |
| 50%1 | 50% − 100% | 3.579 | 3.579 > 2.112 | (1.467 ; 5.691) | 0.0014 | Difieren |
| 75% | 75% − 100% | -1.679 | 1.679 < 2.112 | (-3.791 ; 0.433) | 0.1162 | No difieren |
lsd_conc <- LSD.test(modelo_mol, "c_agua", console = FALSE)
tabla_grupos_mol <- data.frame(
Concentración = rownames(lsd_conc$groups),
Media = round(lsd_conc$groups$cons_o, 3),
`Error estándar` = round(se, 3),
`Grupo LSD` = lsd_conc$groups$groups,
check.names = FALSE
)
tabla_bonita(tabla_grupos_mol, caption = "Grupos LSD por concentración de agua de mar")
| Concentración | Media | Error estándar | Grupo LSD |
|---|---|---|---|
| 50% | 12.251 | 1.047 | a |
| 100% | 8.671 | 1.047 | b |
| 75% | 6.992 | 1.047 | b |
El 50% forma un grupo propio, con consumo significativamente mayor que el 75% (p<0.0001) y el 100% (p=0.0014). El 75% y el 100% no difieren entre sí (p=0.116).
La concentración de agua de mar afecta significativamente el consumo de oxígeno (p < 0.0001), mientras que el tipo de molusco no lo hace (p = 0.111) y no existe interacción entre ambos factores (p = 0.424).
El consumo es marcadamente superior en agua diluida al 50 % (media 12.25) frente al 75 % (6.99) y al 100 % (8.67), que entre sí no se diferencian. El efecto no es lineal ni monótono: el mínimo se da en la concentración intermedia.
Al no haber interacción, este comportamiento es el mismo para los dos tipos de moluscos, de modo que las conclusiones sobre el efecto de la salinidad pueden generalizarse a ambos sin distinción.
Con el fin de evaluar el efecto del uso del suelo sobre la biodiversidad de anfibios en una reserva forestal, se establecieron parcelas de muestreo en cuatro tipos de hábitat que representan un gradiente de intervención antrópica: bosque primario, bosque secundario, sistema silvopastoril y potrero. En cada una de las 52 parcelas (13 por hábitat) se registró la riqueza de especies (número de especies observadas), el índice de diversidad de Shannon–Wiener y la altitud (m.s.n.m.) del sitio de muestreo.
table1(~ Riqueza + Shannon + Altitud, data = BD_biodiversidad)
| Overall (N=52) |
|
|---|---|
| Riqueza | |
| Mean (SD) | 9.73 (4.37) |
| Median [Min, Max] | 9.50 [2.00, 21.0] |
| Shannon | |
| Mean (SD) | 1.76 (0.607) |
| Median [Min, Max] | 1.85 [0.670, 2.98] |
| Altitud | |
| Mean (SD) | 1240 (173) |
| Median [Min, Max] | 1250 [951, 1600] |
ggplot(BD_biodiversidad, aes(x = Riqueza)) +
geom_histogram(aes(y = after_stat(density)),
bins = 10, fill = "#2E86AB", color = "white") +
geom_density(color = "#1a5276", linewidth = 1) +
labs(title = "Distribución de Riqueza",
x = "Número de especies", y = "Densidad") +
theme_minimal(base_family = "sans") +
theme(plot.title = element_text(hjust = 0.5, face = "bold"))
ggplot(BD_biodiversidad, aes(x = Shannon)) +
geom_histogram(aes(y = after_stat(density)),
bins = 10, fill = "#2E86AB", color = "white") +
geom_density(color = "#1a5276", linewidth = 1) +
labs(title = "Distribución de Shannon",
x = "Índice de Shannon", y = "Densidad") +
theme_minimal(base_family = "sans") +
theme(plot.title = element_text(hjust = 0.5, face = "bold"))
ggplot(BD_biodiversidad, aes(x = Altitud)) +
geom_histogram(aes(y = after_stat(density)),
bins = 10, fill = "#2E86AB", color = "white") +
geom_density(color = "#1a5276", linewidth = 1) +
labs(title = "Distribución de Altitud",
x = "Altitud (m s. n. m.)", y = "Densidad") +
theme_minimal(base_family = "sans") +
theme(plot.title = element_text(hjust = 0.5, face = "bold"))
largo_bio <- pivot_longer(BD_biodiversidad,
cols = c(Riqueza, Shannon, Altitud),
names_to = "Variable", values_to = "Valor")
ggplot(largo_bio, aes(x = "", y = Valor)) +
geom_boxplot(fill = "#AED6F1", color = "#1a5276",
outlier.color = "#C0392B", outlier.shape = 1, outlier.size = 2) +
facet_wrap(~ Variable, scales = "free") +
labs(title = "Boxplot de las variables", x = NULL, y = "Valor") +
theme_minimal(base_family = "sans") +
theme(plot.title = element_text(hjust = 0.5, face = "bold"),
strip.text = element_text(face = "bold", size = 12),
axis.text.x = element_blank())
table1(~ Shannon + Riqueza + Altitud | Habitat, data = BD_biodiversidad)
| Bosque primario (N=13) |
Bosque secundario (N=13) |
Potrero (N=13) |
Sistema silvopastoril (N=13) |
Overall (N=52) |
|
|---|---|---|---|---|---|
| Shannon | |||||
| Mean (SD) | 2.47 (0.377) | 1.97 (0.220) | 1.00 (0.240) | 1.60 (0.281) | 1.76 (0.607) |
| Median [Min, Max] | 2.53 [1.73, 2.98] | 2.03 [1.44, 2.27] | 0.980 [0.670, 1.34] | 1.62 [1.14, 2.06] | 1.85 [0.670, 2.98] |
| Riqueza | |||||
| Mean (SD) | 14.8 (2.74) | 11.1 (3.01) | 5.00 (1.29) | 8.08 (2.50) | 9.73 (4.37) |
| Median [Min, Max] | 14.0 [10.0, 21.0] | 12.0 [3.00, 15.0] | 5.00 [2.00, 7.00] | 7.00 [5.00, 13.0] | 9.50 [2.00, 21.0] |
| Altitud | |||||
| Mean (SD) | 1450 (77.5) | 1330 (40.6) | 1040 (50.3) | 1150 (97.9) | 1240 (173) |
| Median [Min, Max] | 1480 [1320, 1600] | 1320 [1250, 1380] | 1040 [951, 1120] | 1140 [1030, 1390] | 1250 [951, 1600] |
La tabla muestra un gradiente claro en la diversidad de anfibios: tanto la riqueza como el índice de Shannon disminuyen conforme aumenta la intervención humana. El bosque primario presenta los valores más altos (≈14.8 especies y Shannon ≈2.47), mientras que el potrero tiene los más bajos (≈5.0 especies y Shannon ≈1.0). El bosque secundario y el sistema silvopastoril ocupan posiciones intermedias. Este patrón sugiere que la diversidad de anfibios está fuertemente asociada al tipo de hábitat, y que la pérdida de área boscosa reduce la cantidad y uniformidad de especies.
largo2_bio <- pivot_longer(BD_biodiversidad,
cols = c(Riqueza, Shannon),
names_to = "Variable", values_to = "Valor")
paleta_habitat <- c(
"Bosque primario" = "#1a5276",
"Bosque secundario" = "#2E86AB",
"Sistema silvopastoril" = "#F9A825",
"Potrero" = "#A1662F"
)
abrev_habitat <- c(
"Bosque primario" = "BP",
"Bosque secundario" = "BS",
"Sistema silvopastoril" = "S",
"Potrero" = "P"
)
ggplot(largo2_bio, aes(x = Habitat, y = Valor, fill = Habitat)) +
geom_boxplot(alpha = 0.85, color = "black") +
facet_wrap(~ Variable, scales = "free_y") +
scale_fill_manual(values = paleta_habitat, labels = abrev_habitat) +
scale_x_discrete(labels = abrev_habitat) +
labs(title = "Riqueza y Shannon por tipo de hábitat",
x = NULL, y = "Valor", fill = "Hábitat") +
theme_minimal(base_family = "sans") +
theme(plot.title = element_text(hjust = 0.5, face = "bold"),
strip.text = element_text(face = "bold", size = 12),
legend.position = "bottom")
El boxplot compara la riqueza y la diversidad de Shannon entre los cuatro tipos de hábitat, y confirma el gradiente: ambas variables disminuyen conforme aumenta la intervención humana.
Se observan algunos valores atípicos, pero reflejan variabilidad natural dentro de los hábitats y no necesariamente errores de muestreo.
ggplot(BD_biodiversidad, aes(x = Altitud, y = Shannon, color = Habitat)) +
geom_point(size = 2.5) +
geom_smooth(method = "lm", se = FALSE, color = "#1a5276") +
scale_color_manual(values = paleta_habitat, labels = abrev_habitat) +
labs(title = "Altitud vs. diversidad de Shannon",
x = "Altitud (m s. n. m.)", y = "Índice de Shannon", color = "Hábitat") +
theme_minimal(base_family = "sans") +
theme(plot.title = element_text(hjust = 0.5, face = "bold"),
legend.position = "bottom")
El gráfico de dispersión muestra una relación positiva entre altitud y Shannon: a mayor altitud, mayor diversidad. Sin embargo, al colorear los puntos por hábitat, se observa que los colores se agrupan por zonas: el bosque primario está arriba y a la derecha, y el potrero está abajo y a la izquierda. Esto sugiere que la relación global no es necesariamente causal, la altitud no causa por sí sola más diversidad, sino que los hábitats más conservados están ubicados a mayor altitud. Es decir, la altitud está confundida con el hábitat.
cor_pearson_bio <- cor.test(BD_biodiversidad$Altitud, BD_biodiversidad$Shannon)
cor_spearman_bio <- cor.test(BD_biodiversidad$Altitud, BD_biodiversidad$Shannon,
method = "spearman", exact = FALSE)
tabla_cor_global <- data.frame(
Prueba = c("Pearson", "Spearman"),
r = c(round(cor_pearson_bio$estimate, 3), round(cor_spearman_bio$estimate, 3)),
`Valor p` = c(format(cor_pearson_bio$p.value, scientific = TRUE, digits = 3),
format(cor_spearman_bio$p.value, scientific = TRUE, digits = 3)),
check.names = FALSE
)
tabla_bonita(tabla_cor_global, caption = "Correlación global Altitud - Shannon")
| Prueba | r | Valor p | |
|---|---|---|---|
| cor | Pearson | 0.752 | 1.29e-10 |
| rho | Spearman | 0.750 | 1.56e-10 |
tabla_cor_habitat <- BD_biodiversidad %>%
group_by(Habitat) %>%
summarise(r = cor(Altitud, Shannon),
`Valor p` = cor.test(Altitud, Shannon)$p.value)
tabla_bonita(tabla_cor_habitat, caption = "Correlación altitud-Shannon dentro de cada hábitat")
| Habitat | r | Valor p |
|---|---|---|
| Bosque primario | -0.328 | 0.274 |
| Bosque secundario | -0.005 | 0.986 |
| Potrero | 0.034 | 0.912 |
| Sistema silvopastoril | -0.513 | 0.073 |
Existe una correlación global positiva y significativa entre altitud y Shannon (r ≈ 0.75; p < 0.001). La altitud explica aproximadamente el 56% de la variación en la diversidad, y Spearman confirma el resultado, lo que indica que la relación no depende de valores extremos.
Sin embargo, al calcular la correlación dentro de cada hábitat, la relación altitud-Shannon desaparece: los valores de r son bajos o negativos y ninguno es significativo (todos p > 0.05). Esto confirma que la correlación global era un efecto del hábitat, no de la altitud.
En conclusión, no es que la altitud aumente la diversidad, sino que los hábitats más conservados están ubicados a mayor altitud y, al mismo tiempo, tienen mayor diversidad.
modelo_bio <- aov(Shannon ~ Habitat, data = BD_biodiversidad)
anova_res_bio <- summary(modelo_bio)[[1]]
tabla_anova_bio <- data.frame(
Fuente = rownames(anova_res_bio),
GL = anova_res_bio$Df,
SC = round(anova_res_bio$`Sum Sq`, 3),
CM = round(anova_res_bio$`Mean Sq`, 3),
F = round(anova_res_bio$`F value`, 2),
`Valor p` = format(anova_res_bio$`Pr(>F)`, scientific = TRUE, digits = 3),
check.names = FALSE
)
tabla_bonita(tabla_anova_bio, caption = "Tabla de análisis de varianza (Shannon ~ Hábitat)")
| Fuente | GL | SC | CM | F | Valor p |
|---|---|---|---|---|---|
| Habitat | 3 | 14.862 | 4.954 | 60.61 | 2.38e-16 |
| Residuals | 48 | 3.923 | 0.082 | NA | NA |
Se evaluó si el índice de Shannon difiere entre los cuatro hábitats. El modelo resultó altamente significativo: F(3, 48) = 60.61; p = 2.38 × 10⁻¹⁶. Se rechaza la hipótesis nula de igualdad de medias, lo que indica que al menos un hábitat difiere de los demás.
res_bio <- residuals(modelo_bio)
shap_bio <- shapiro.test(res_bio)
lev_bio <- leveneTest(Shannon ~ Habitat, data = BD_biodiversidad)
tabla_supuestos_bio <- data.frame(
Supuesto = c("Normalidad de residuales (Shapiro-Wilk)",
"Homogeneidad de varianzas (Levene)"),
Estadístico = c(round(shap_bio$statistic, 4), round(lev_bio$`F value`[1], 3)),
`Valor p` = c(format(shap_bio$p.value, digits = 3),
format(lev_bio$`Pr(>F)`[1], digits = 3)),
`¿Se cumple?` = c("Sí", "Sí"),
check.names = FALSE
)
tabla_bonita(tabla_supuestos_bio, caption = "Verificación de supuestos del modelo")
| Supuesto | Estadístico | Valor p | ¿Se cumple? | |
|---|---|---|---|---|
| W | Normalidad de residuales (Shapiro-Wilk) | 0.979 | 0.463 | Sí |
| Homogeneidad de varianzas (Levene) | 1.277 | 0.293 | Sí |
Los residuales son compatibles con una distribución normal (Shapiro-Wilk: W = 0.978; p = 0.463), y las varianzas son homogéneas entre hábitats (Levene: F = 1.277; p = 0.293). Ambos supuestos se cumplen.
lsd_bio <- LSD.test(modelo_bio, "Habitat", p.adj = "none", console = FALSE)
medias_bio <- lsd_bio$means
letras_bio <- lsd_bio$groups
orden_bio <- order(-medias_bio$Shannon)
medias_bio <- medias_bio[orden_bio, ]
letras_bio <- letras_bio[orden_bio, ]
tabla_lsd_bio <- data.frame(
Hábitat = rownames(medias_bio),
`Media de Shannon` = round(medias_bio$Shannon, 2),
DE = round(medias_bio$std, 2),
Grupo = letras_bio$groups,
check.names = FALSE
)
tabla_bonita(tabla_lsd_bio, caption = "Prueba LSD (Fisher) por tipo de hábitat")
| Hábitat | Media de Shannon | DE | Grupo |
|---|---|---|---|
| Bosque primario | 2.47 | 0.38 | a |
| Bosque secundario | 1.97 | 0.22 | b |
| Sistema silvopastoril | 1.60 | 0.28 | d |
| Potrero | 1.00 | 0.24 | c |
Los cuatro hábitats difieren significativamente entre sí (letras distintas): bosque primario (2.47, a) > bosque secundario (1.97, b) > sistema silvopastoril (1.60, c) > potrero (1.00, d). La diferencia mínima significativa fue de 0.225, y todas las diferencias entre pares la superan.
Síntesis
La diversidad de anfibios disminuye de manera escalonada al aumentar la intervención humana. Aunque la altitud se correlaciona con la diversidad a nivel global, esta relación desaparece dentro de cada hábitat, lo que indica que el verdadero factor explicativo es el tipo de hábitat y no la altitud en sí misma.
La diversidad de anfibios disminuye de manera escalonada al aumentar la intervención humana. El bosque primario conserva la mayor diversidad, mientras que el potrero presenta la menor. El bosque secundario y el sistema silvopastoril representan estados intermedios, lo que sugiere que la regeneración forestal y los sistemas con cobertura arbórea amortiguan parcialmente la pérdida de diversidad, pero no sustituyen al bosque primario como refugio de anfibios.