Desarrollar herramientas estadísticas para analizar asociaciones, diferencias y relaciones entre variables cualitativas y cuantitativas en contextos microbiológicos y de control de calidad, manteniendo el punto de muestreo como eje central del análisis.
A partir de la misma base real empleada en el módulo anterior se estudiará:
En los análisis descriptivos estudiamos cada variable por separado.
Ahora avanzaremos hacia preguntas que involucran dos variables simultáneamente.
Por ejemplo:
¿La frecuencia de resultados microbiológicos elevados cambia según el punto de muestreo?
¿Los resultados microbiológicos presentan el mismo comportamiento en todos los puntos?
¿Los resultados de un determinado punto aumentan o disminuyen a través del tiempo?
La técnica estadística apropiada dependerá principalmente de la naturaleza de las variables involucradas.
En este módulo trabajaremos en el siguiente orden:
\[ \boxed{ \text{Cualitativa–Cualitativa} } \]
\[ \Downarrow \]
\[ \boxed{ \text{Cuantitativa–Cualitativa} } \]
\[ \Downarrow \]
\[ \boxed{ \text{Cuantitativa–Cuantitativa} } \]
Esta secuencia permitirá avanzar desde asociaciones entre categorías hasta modelos de correlación, regresión e inferencia.
| Tipo de relación | Ejemplo en la base | Representación | Pruebas principales | Magnitud |
|---|---|---|---|---|
| Cuali–Cuali | Punto × Alerta | Barras / contingencia | Chi-cuadrado / Fisher | OR, V de Cramér |
| Cuanti–Cuali | NMP × Punto | Boxplot | Welch / ANOVA / Kruskal-Wallis | \(d\), \(\eta^2\) |
| Cuanti–Cuanti | Tiempo × NMP dentro del punto | Dispersión | Pearson / Spearman / regresión | \(r\), \(\rho_s\), \(b_1\), \(R^2\) |
\[ \boxed{\text{Pregunta microbiológica}} \]
\[ \Downarrow \]
\[ \boxed{\text{Identificar las variables}} \]
\[ \Downarrow \]
\[ \boxed{\text{Clasificar su naturaleza}} \]
\[ \Downarrow \]
\[ \boxed{\text{Representar gráficamente}} \]
\[ \Downarrow \]
\[ \boxed{\text{Evaluar supuestos}} \]
\[ \Downarrow \]
\[ \boxed{\text{Seleccionar la prueba}} \]
\[ \Downarrow \]
\[ \boxed{ p+\text{IC}+\text{magnitud del efecto} } \]
\[ \Downarrow \]
\[ \boxed{\text{Interpretación estadística}} \]
\[ \Downarrow \]
\[ \boxed{\text{Interpretación microbiológica}} \]
Trabajaremos nuevamente con:
data/DATOS I- SEM 2026.xlsx
La base contiene resultados de coliformes termotolerantes expresados en NMP/100 mL, correspondientes a diferentes puntos y fechas de muestreo.
library(readxl)
datos <- read_excel(
"DATOS I- SEM 2026.xlsx"
)
names(datos) <- c(
"analisis",
"punto",
"resultado_nmp",
"matriz",
"fecha"
)
# Eliminar registros sin identificación completa del punto
datos <- datos[
!is.na(datos$punto) &
trimws(datos$punto) != "PUNTO_",
]
# Convertir resultados microbiológicos a valores numéricos
datos$resultado_nmp <- as.numeric(
gsub(
",",
".",
as.character(datos$resultado_nmp),
fixed = TRUE
)
)
# Asegurar formato de fecha
datos$fecha <- as.Date(
datos$fecha
)
# Crear transformación log10 únicamente para valores positivos
datos$log10_nmp <- ifelse(
!is.na(datos$resultado_nmp) &
datos$resultado_nmp > 0,
log10(datos$resultado_nmp),
NA_real_
)
# Variables temporales
datos$mes_num <- as.integer(
format(
datos$fecha,
"%m"
)
)
datos$mes <- factor(
datos$mes_num,
levels = 1:12,
labels = c(
"Enero", "Febrero", "Marzo", "Abril",
"Mayo", "Junio", "Julio", "Agosto",
"Septiembre", "Octubre", "Noviembre", "Diciembre"
)
)
# Días transcurridos desde la primera fecha
fecha_inicio <- min(
datos$fecha,
na.rm = TRUE
)
datos$dia_estudio <- as.numeric(
datos$fecha - fecha_inicio
)
# Percentil 90 global
P90_global <- as.numeric(
quantile(
datos$resultado_nmp,
probs = 0.90,
na.rm = TRUE
)
)
# Clasificación global
datos$evento_alerta <- ifelse(
datos$resultado_nmp > P90_global,
"Alerta",
"Sin alerta"
)
datos$evento_alerta <- factor(
datos$evento_alerta,
levels = c(
"Alerta",
"Sin alerta"
)
)
# Percentil 90 específico por punto
datos$P90_punto <- ave(
datos$resultado_nmp,
datos$punto,
FUN = function(x) {
rep(
as.numeric(
quantile(
x,
probs = 0.90,
na.rm = TRUE
)
),
length(x)
)
}
)
# Clasificación local
datos$evento_alerta_punto <- ifelse(
datos$resultado_nmp > datos$P90_punto,
"Alerta",
"Sin alerta"
)
datos$evento_alerta_punto <- factor(
datos$evento_alerta_punto,
levels = c(
"Alerta",
"Sin alerta"
)
)
# Lista de puntos disponible para todo el módulo
puntos_mod3 <- sort(
unique(
datos$punto
)
)
El objetivo del módulo no será producir únicamente medidas globales.
Queremos identificar:
\[ \boxed{ \text{cómo se comporta cada punto} } \]
y posteriormente comparar esos comportamientos.
Los puntos pueden diferir en:
tabla_n_punto <- as.data.frame(
table(
datos$punto
)
)
names(
tabla_n_punto
) <- c(
"Punto",
"N"
)
tabla_n_punto <- tabla_n_punto[
order(
tabla_n_punto$N,
decreasing = TRUE
),
]
knitr::kable(
tabla_n_punto,
caption = "Número de observaciones por punto de muestreo"
)
| Punto | N | |
|---|---|---|
| 5 | PUNTO_19 | 22 |
| 8 | PUNTO_23 | 22 |
| 17 | PUNTO_9 | 22 |
| 1 | PUNTO_11 | 21 |
| 2 | PUNTO_16 | 21 |
| 3 | PUNTO_17 | 21 |
| 4 | PUNTO_18 | 21 |
| 6 | PUNTO_20 | 21 |
| 7 | PUNTO_21 | 21 |
| 15 | PUNTO_7 | 21 |
| 14 | PUNTO_6 | 20 |
| 16 | PUNTO_8 | 20 |
| 13 | PUNTO_5 | 19 |
| 10 | PUNTO_25 | 11 |
| 11 | PUNTO_26 | 11 |
| 9 | PUNTO_24 | 7 |
| 12 | PUNTO_4 | 6 |
El tamaño muestral:
\[ n_j \]
no es necesariamente igual en todos los puntos.
Un mayor \(n_j\) proporciona generalmente mayor información para estimar:
Un punto con más observaciones no tiene necesariamente mayor concentración microbiológica.
Significa únicamente que existe una mayor cantidad de información disponible para ese punto.
Una relación cualitativa–cualitativa ocurre cuando las dos variables representan categorías.
En nuestra base estudiaremos:
\[ X= \text{Punto de muestreo} \]
y:
\[ Y= \text{Alerta / Sin alerta} \]
La pregunta será:
¿La frecuencia de resultados clasificados como alerta está asociada con el punto de muestreo?
El punto de muestreo es una variable:
\[ \boxed{\text{cualitativa nominal}} \]
porque sus categorías no poseen un orden jerárquico.
La clasificación:
\[ \text{Alerta / Sin alerta} \]
es una variable:
\[ \boxed{\text{cualitativa nominal dicotómica}} \]
Utilizamos el percentil 90:
\[ P_{90} = Q_{0.90} \]
Se clasifica:
\[ \text{Alerta} \quad \text{si} \quad X_i>P_{90} \]
y:
\[ \text{Sin alerta} \quad \text{si} \quad X_i\leq P_{90} \]
P90_global
## [1] 1897200
El P90 permite identificar resultados situados en la zona superior de la distribución observada.
Debe entenderse como:
\[ \boxed{\text{criterio estadístico de priorización}} \]
y no como:
\[ \boxed{\text{límite normativo}} \]
Superar el P90 no representa automáticamente una no conformidad microbiológica.
Para explicar una tabla \(2\times2\), Chi-cuadrado, Fisher y odds ratio, debemos seleccionar dos puntos que permitan observar las dos categorías.
resumen_seleccion_cc <- do.call(
rbind,
lapply(
split(
datos,
datos$punto
),
function(df) {
data.frame(
N = nrow(df),
Alertas = sum(
df$evento_alerta == "Alerta",
na.rm = TRUE
),
Sin_alerta = sum(
df$evento_alerta == "Sin alerta",
na.rm = TRUE
)
)
}
)
)
resumen_seleccion_cc$Punto <- rownames(
resumen_seleccion_cc
)
rownames(
resumen_seleccion_cc
) <- NULL
# Caso ideal: dos puntos que tengan ambas categorías
puntos_completos_cc <- resumen_seleccion_cc$Punto[
resumen_seleccion_cc$N >= 5 &
resumen_seleccion_cc$Alertas >= 1 &
resumen_seleccion_cc$Sin_alerta >= 1
]
if (
length(
puntos_completos_cc
) >= 2
) {
puntos_cc <- puntos_completos_cc[
1:2
]
} else {
# Si no hay dos puntos completos, buscar una combinación
# que genere al menos una tabla 2x2 válida globalmente
puntos_con_alerta <- resumen_seleccion_cc$Punto[
resumen_seleccion_cc$N >= 5 &
resumen_seleccion_cc$Alertas >= 1
]
puntos_con_sin_alerta <- resumen_seleccion_cc$Punto[
resumen_seleccion_cc$N >= 5 &
resumen_seleccion_cc$Sin_alerta >= 1
]
encontrado_cc <- FALSE
puntos_cc <- NULL
for (
p1 in puntos_con_alerta
) {
candidatos <- setdiff(
puntos_con_sin_alerta,
p1
)
if (
length(
candidatos
) > 0
) {
for (
p2 in candidatos
) {
temp_cc <- datos[
datos$punto %in% c(
p1,
p2
),
]
temp_tab <- table(
temp_cc$punto,
temp_cc$evento_alerta
)
temp_tab <- temp_tab[
rowSums(
temp_tab
) > 0,
colSums(
temp_tab
) > 0,
drop = FALSE
]
if (
nrow(
temp_tab
) == 2 &&
ncol(
temp_tab
) == 2
) {
puntos_cc <- c(
p1,
p2
)
encontrado_cc <- TRUE
break
}
}
}
if (
encontrado_cc
) {
break
}
}
}
if (
!is.null(
puntos_cc
)
) {
ejemplo_cc <- datos[
datos$punto %in% puntos_cc,
c(
"punto",
"fecha",
"resultado_nmp",
"evento_alerta"
)
]
ejemplo_cc$punto <- factor(
ejemplo_cc$punto,
levels = puntos_cc
)
ejemplo_cc$evento_alerta <- factor(
ejemplo_cc$evento_alerta,
levels = c(
"Alerta",
"Sin alerta"
)
)
knitr::kable(
head(
ejemplo_cc,
12
),
caption = paste(
"Ejemplo con los puntos",
paste(
puntos_cc,
collapse = " y "
)
)
)
} else {
ejemplo_cc <- NULL
cat(
"La distribución actual de alertas no permite construir un ejemplo 2x2 con dos puntos. El análisis completo con todos los puntos continúa siendo válido."
)
}
| punto | fecha | resultado_nmp | evento_alerta |
|---|---|---|---|
| PUNTO_5 | 2026-05-06 | 1.100e+07 | Alerta |
| PUNTO_19 | 2026-05-06 | 2.400e+06 | Alerta |
| PUNTO_5 | 2026-01-29 | 4.352e+07 | Alerta |
| PUNTO_19 | 2026-01-29 | 1.439e+05 | Sin alerta |
| PUNTO_5 | 2026-02-10 | 1.320e+06 | Sin alerta |
| PUNTO_19 | 2026-02-10 | 8.400e+03 | Sin alerta |
| PUNTO_19 | 2026-02-24 | 1.350e+04 | Sin alerta |
| PUNTO_19 | 2026-03-03 | 1.019e+02 | Sin alerta |
| PUNTO_19 | 2026-03-03 | 1.019e+05 | Sin alerta |
| PUNTO_5 | 2026-03-24 | 2.187e+02 | Sin alerta |
| PUNTO_5 | 2026-03-24 | 2.187e+07 | Alerta |
| PUNTO_19 | 2026-03-24 | 3.873e+05 | Sin alerta |
Una tabla de contingencia presenta las frecuencias conjuntas de dos variables cualitativas.
Su estructura general es:
| Punto | Alerta | Sin alerta | Total |
|---|---|---|---|
| Punto 1 | \(n_{11}\) | \(n_{12}\) | \(n_{1+}\) |
| Punto 2 | \(n_{21}\) | \(n_{22}\) | \(n_{2+}\) |
| Total | \(n_{+1}\) | \(n_{+2}\) | \(N\) |
donde:
if (
!is.null(
ejemplo_cc
)
) {
tabla_2x2 <- table(
Punto = ejemplo_cc$punto,
Clasificacion = ejemplo_cc$evento_alerta
)
tabla_2x2 <- tabla_2x2[
rowSums(
tabla_2x2
) > 0,
colSums(
tabla_2x2
) > 0,
drop = FALSE
]
tabla_2x2
} else {
tabla_2x2 <- NULL
}
## Clasificacion
## Punto Alerta Sin alerta
## PUNTO_19 1 21
## PUNTO_5 9 10
Para cada punto:
\[ \hat p_{ij} = \frac{ n_{ij} }{ n_{i+} } \]
donde:
if (
!is.null(
tabla_2x2
)
) {
prop_2x2 <- prop.table(
tabla_2x2,
margin = 1
)
round(
prop_2x2,
4
)
} else {
prop_2x2 <- NULL
}
## Clasificacion
## Punto Alerta Sin alerta
## PUNTO_19 0.0455 0.9545
## PUNTO_5 0.4737 0.5263
if (
!is.null(
prop_2x2
)
) {
round(
prop_2x2 * 100,
1
)
}
## Clasificacion
## Punto Alerta Sin alerta
## PUNTO_19 4.5 95.5
## PUNTO_5 47.4 52.6
Las frecuencias absolutas pueden resultar engañosas cuando los tamaños de muestra son diferentes.
Por ello es preferible comparar:
\[ \frac{ \text{Número de alertas} }{ \text{Número total del punto} } \]
Dos puntos podrían tener cinco alertas cada uno, pero si uno tiene 10 observaciones y el otro 100, sus porcentajes serían:
\[ 50\% \]
y:
\[ 5\% \]
respectivamente.
Una mayor proporción de alertas significa que el punto presenta con mayor frecuencia resultados situados por encima del P90 global.
Esto señala una mayor frecuencia relativa de resultados elevados dentro de la distribución analizada.
No constituye, por sí mismo, una conclusión normativa.
if (
!is.null(
tabla_2x2
) &&
nrow(
tabla_2x2
) >= 2 &&
ncol(
tabla_2x2
) >= 2
) {
porcentaje_2x2 <- prop.table(
tabla_2x2,
margin = 1
) * 100
barplot(
t(
porcentaje_2x2
),
beside = FALSE,
col = c(
"#1596a5",
"#dcecef"
),
border = NA,
ylab = "Porcentaje dentro del punto",
xlab = "Punto de muestreo",
main = "Clasificación de alerta entre dos puntos"
)
legend(
"topright",
legend = colnames(
porcentaje_2x2
),
fill = c(
"#1596a5",
"#dcecef"
),
border = NA,
bty = "n"
)
}
Las hipótesis son:
\[ H_0: \text{Punto y alerta son independientes} \]
frente a:
\[ H_1: \text{Punto y alerta están asociados} \]
En términos probabilísticos:
\[ P(A\cap B) = P(A)P(B) \]
donde:
Si existe independencia, conocer el punto no modifica la distribución
esperada de Alerta y Sin alerta.
Bajo independencia:
\[ E_{ij} = \frac{ n_{i+}n_{+j} }{ N } \]
donde:
if (
!is.null(
tabla_2x2
) &&
nrow(
tabla_2x2
) >= 2 &&
ncol(
tabla_2x2
) >= 2
) {
chi_preliminar_cc <- suppressWarnings(
chisq.test(
tabla_2x2,
correct = FALSE
)
)
chi_preliminar_cc$expected
} else {
chi_preliminar_cc <- NULL
}
## Clasificacion
## Punto Alerta Sin alerta
## PUNTO_19 5.365854 16.63415
## PUNTO_5 4.634146 14.36585
La prueba compara frecuencias observadas:
\[ O_{ij} \]
con las frecuencias esperadas:
\[ E_{ij} \]
mediante:
\[ \chi^2 = \sum_{i=1}^{r} \sum_{j=1}^{c} \frac{ (O_{ij}-E_{ij})^2 }{ E_{ij} } \]
donde:
Cada celda aporta:
\[ \frac{ (O_{ij}-E_{ij})^2 }{ E_{ij} } \]
if (
!is.null(
chi_preliminar_cc
)
) {
contribuciones_chi <- (
tabla_2x2 -
chi_preliminar_cc$expected
)^2 /
chi_preliminar_cc$expected
contribuciones_chi
chi_manual <- sum(
contribuciones_chi
)
chi_manual
}
## [1] 10.13799
Los grados de libertad son:
\[ gl= (r-1)(c-1) \]
Para una tabla \(2\times2\):
\[ gl= (2-1)(2-1) = 1 \]
if (
!is.null(
tabla_2x2
) &&
nrow(
tabla_2x2
) >= 2 &&
ncol(
tabla_2x2
) >= 2
) {
prueba_chi_cc <- suppressWarnings(
chisq.test(
tabla_2x2,
correct = FALSE
)
)
prueba_chi_cc
} else {
prueba_chi_cc <- NULL
cat(
"No existe una tabla 2x2 válida para realizar Chi-cuadrado en el ejemplo."
)
}
##
## Pearson's Chi-squared test
##
## data: tabla_2x2
## X-squared = 10.138, df = 1, p-value = 0.001452
Si:
\[ p<0.05 \]
existe evidencia contra la independencia.
Por tanto, existe evidencia de asociación entre punto y alerta.
Si:
\[ p\geq0.05 \]
no existe evidencia suficiente para afirmar que ambas variables estén asociadas.
Una asociación estadística indicaría que la frecuencia relativa de resultados elevados no es homogénea entre las ubicaciones estudiadas.
Algunos puntos podrían concentrar una proporción mayor de resultados situados en la zona superior de la distribución microbiológica.
La aproximación Chi-cuadrado requiere prestar atención a las frecuencias esperadas.
if (
!is.null(
prueba_chi_cc
)
) {
data.frame(
Celdas_esperadas_menores_5 =
sum(
prueba_chi_cc$expected < 5
),
Porcentaje_menores_5 =
mean(
prueba_chi_cc$expected < 5
) * 100
)
}
## Celdas_esperadas_menores_5 Porcentaje_menores_5
## 1 1 25
Una proporción elevada de celdas con valores esperados pequeños reduce la confiabilidad de la aproximación Chi-cuadrado.
Cuando las frecuencias son pequeñas, especialmente en tablas \(2\times2\), puede utilizarse Fisher.
if (
!is.null(
tabla_2x2
) &&
nrow(
tabla_2x2
) >= 2 &&
ncol(
tabla_2x2
) >= 2 &&
all(
rowSums(
tabla_2x2
) > 0
) &&
all(
colSums(
tabla_2x2
) > 0
)
) {
prueba_fisher_cc <- fisher.test(
tabla_2x2
)
prueba_fisher_cc
} else {
prueba_fisher_cc <- NULL
cat(
"Fisher no puede calcularse porque el ejemplo no genera una tabla 2x2 válida."
)
}
##
## Fisher's Exact Test for Count Data
##
## data: tabla_2x2
## p-value = 0.002472
## alternative hypothesis: true odds ratio is not equal to 1
## 95 percent confidence interval:
## 0.001159945 0.504227246
## sample estimates:
## odds ratio
## 0.05698689
Fisher evalúa:
\[ H_0: \text{independencia} \]
frente a:
\[ H_1: \text{asociación} \]
Si:
\[ p<0.05 \]
existe evidencia de asociación entre las dos variables cualitativas.
Si:
\[ p= P(\text{Alerta}) \]
las odds se definen como:
\[ \text{odds} = \frac{ p }{ 1-p } \]
Por ejemplo, para:
\[ p=0.20 \]
se obtiene:
\[ \frac{0.20}{0.80} = 0.25 \]
En una tabla:
| Alerta | Sin alerta | |
|---|---|---|
| Punto 1 | \(a\) | \(b\) |
| Punto 2 | \(c\) | \(d\) |
el odds ratio es:
\[ OR = \frac{ a/b }{ c/d } \]
equivalentemente:
\[ OR = \frac{ ad }{ bc } \]
Si:
\[ OR=1 \]
las odds de alerta son iguales.
Si:
\[ OR>1 \]
el primer punto presenta mayores odds de alerta.
Si:
\[ OR<1 \]
presenta menores odds.
if (
!is.null(
prueba_fisher_cc
)
) {
prueba_fisher_cc$estimate
prueba_fisher_cc$conf.int
} else {
cat(
"No fue posible estimar el odds ratio para el ejemplo."
)
}
## [1] 0.001159945 0.504227246
## attr(,"conf.level")
## [1] 0.95
El OR permite cuantificar cuánto cambia la ocurrencia relativa de alertas entre dos puntos.
Un OR elevado no implica que la ubicación sea la causa directa de los resultados.
El punto puede representar condiciones ambientales, operacionales o hidráulicas diferentes.
Para medir la magnitud de asociación:
\[ V = \sqrt{ \frac{ \chi^2 }{ N\min(r-1,c-1) } } \]
donde:
if (
!is.null(
prueba_chi_cc
)
) {
V_cc <- sqrt(
unname(
prueba_chi_cc$statistic
) /
(
sum(
tabla_2x2
) *
min(
nrow(
tabla_2x2
) - 1,
ncol(
tabla_2x2
) - 1
)
)
)
V_cc
}
## [1] 0.4972606
Valores cercanos a cero representan asociación débil.
Valores mayores representan asociaciones más importantes.
La magnitud debe interpretarse conjuntamente con las dimensiones de la tabla y el contexto microbiológico.
if (
!is.null(
prueba_chi_cc
)
) {
round(
prueba_chi_cc$stdres,
2
)
}
## Clasificacion
## Punto Alerta Sin alerta
## PUNTO_19 -3.18 3.18
## PUNTO_5 3.18 -3.18
Como orientación:
\[ |\text{residuo}|>2 \]
sugiere una discrepancia importante entre observado y esperado.
Un residuo positivo grande en:
\[ \text{Punto X + Alerta} \]
indica:
Se observaron más alertas de las esperadas si las variables fueran independientes.
Un residuo negativo indica menos alertas de las esperadas.
tabla_alerta_total <- table(
Punto = datos$punto,
Clasificacion = datos$evento_alerta
)
tabla_alerta_total
## Clasificacion
## Punto Alerta Sin alerta
## PUNTO_11 0 21
## PUNTO_16 0 21
## PUNTO_17 0 21
## PUNTO_18 0 21
## PUNTO_19 1 21
## PUNTO_20 0 21
## PUNTO_21 0 21
## PUNTO_23 0 22
## PUNTO_24 0 7
## PUNTO_25 0 11
## PUNTO_26 0 11
## PUNTO_4 0 6
## PUNTO_5 9 10
## PUNTO_6 9 11
## PUNTO_7 2 19
## PUNTO_8 8 12
## PUNTO_9 2 20
porcentaje_alerta_total <- prop.table(
tabla_alerta_total,
margin = 1
) * 100
round(
porcentaje_alerta_total,
1
)
## Clasificacion
## Punto Alerta Sin alerta
## PUNTO_11 0.0 100.0
## PUNTO_16 0.0 100.0
## PUNTO_17 0.0 100.0
## PUNTO_18 0.0 100.0
## PUNTO_19 4.5 95.5
## PUNTO_20 0.0 100.0
## PUNTO_21 0.0 100.0
## PUNTO_23 0.0 100.0
## PUNTO_24 0.0 100.0
## PUNTO_25 0.0 100.0
## PUNTO_26 0.0 100.0
## PUNTO_4 0.0 100.0
## PUNTO_5 47.4 52.6
## PUNTO_6 45.0 55.0
## PUNTO_7 9.5 90.5
## PUNTO_8 40.0 60.0
## PUNTO_9 9.1 90.9
if (
"Alerta" %in%
colnames(
porcentaje_alerta_total
)
) {
porcentaje_alerta_simple <- porcentaje_alerta_total[
,
"Alerta"
]
porcentaje_alerta_simple <- sort(
porcentaje_alerta_simple,
decreasing = TRUE
)
par(
mar = c(
5,
7,
4,
2
)
)
barplot(
porcentaje_alerta_simple,
horiz = TRUE,
las = 1,
col = "#1596a5",
border = NA,
xlab = "Porcentaje de alertas",
main = "Frecuencia relativa de alertas por punto"
)
}
tabla_alerta_total_valida <- tabla_alerta_total[
rowSums(
tabla_alerta_total
) > 0,
colSums(
tabla_alerta_total
) > 0,
drop = FALSE
]
if (
nrow(
tabla_alerta_total_valida
) >= 2 &&
ncol(
tabla_alerta_total_valida
) >= 2
) {
chi_total_cc <- suppressWarnings(
chisq.test(
tabla_alerta_total_valida
)
)
chi_total_cc
} else {
chi_total_cc <- NULL
cat(
"No existe variación suficiente en las categorías para calcular Chi-cuadrado."
)
}
##
## Pearson's Chi-squared test
##
## data: tabla_alerta_total_valida
## X-squared = 96.944, df = 16, p-value = 1.291e-13
if (
!is.null(
chi_total_cc
)
) {
round(
chi_total_cc$expected,
2
)
}
## Clasificacion
## Punto Alerta Sin alerta
## PUNTO_11 2.12 18.88
## PUNTO_16 2.12 18.88
## PUNTO_17 2.12 18.88
## PUNTO_18 2.12 18.88
## PUNTO_19 2.22 19.78
## PUNTO_20 2.12 18.88
## PUNTO_21 2.12 18.88
## PUNTO_23 2.22 19.78
## PUNTO_24 0.71 6.29
## PUNTO_25 1.11 9.89
## PUNTO_26 1.11 9.89
## PUNTO_4 0.61 5.39
## PUNTO_5 1.92 17.08
## PUNTO_6 2.02 17.98
## PUNTO_7 2.12 18.88
## PUNTO_8 2.02 17.98
## PUNTO_9 2.22 19.78
if (
!is.null(
chi_total_cc
)
) {
round(
chi_total_cc$stdres,
2
)
}
## Clasificacion
## Punto Alerta Sin alerta
## PUNTO_11 -1.59 1.59
## PUNTO_16 -1.59 1.59
## PUNTO_17 -1.59 1.59
## PUNTO_18 -1.59 1.59
## PUNTO_19 -0.90 0.90
## PUNTO_20 -1.59 1.59
## PUNTO_21 -1.59 1.59
## PUNTO_23 -1.63 1.63
## PUNTO_24 -0.90 0.90
## PUNTO_25 -1.13 1.13
## PUNTO_26 -1.13 1.13
## PUNTO_4 -0.83 0.83
## PUNTO_5 5.57 -5.57
## PUNTO_6 5.36 -5.36
## PUNTO_7 -0.09 0.09
## PUNTO_8 4.59 -4.59
## PUNTO_9 -0.16 0.16
if (
!is.null(
chi_total_cc
) &&
"Alerta" %in%
colnames(
chi_total_cc$stdres
)
) {
tabla_residuos_alerta <- data.frame(
Punto = rownames(
chi_total_cc$stdres
),
Residuo_Alerta =
chi_total_cc$stdres[
,
"Alerta"
]
)
tabla_residuos_alerta$Interpretacion <- ifelse(
tabla_residuos_alerta$Residuo_Alerta > 2,
"Más alertas de las esperadas",
ifelse(
tabla_residuos_alerta$Residuo_Alerta < -2,
"Menos alertas de las esperadas",
"Comportamiento cercano a lo esperado"
)
)
tabla_residuos_alerta <- tabla_residuos_alerta[
order(
tabla_residuos_alerta$Residuo_Alerta,
decreasing = TRUE
),
]
knitr::kable(
tabla_residuos_alerta,
digits = 3,
caption = "Contribución de cada punto a la categoría Alerta"
)
}
| Punto | Residuo_Alerta | Interpretacion | |
|---|---|---|---|
| PUNTO_5 | PUNTO_5 | 5.567 | Más alertas de las esperadas |
| PUNTO_6 | PUNTO_6 | 5.358 | Más alertas de las esperadas |
| PUNTO_8 | PUNTO_8 | 4.590 | Más alertas de las esperadas |
| PUNTO_7 | PUNTO_7 | -0.090 | Comportamiento cercano a lo esperado |
| PUNTO_9 | PUNTO_9 | -0.163 | Comportamiento cercano a lo esperado |
| PUNTO_4 | PUNTO_4 | -0.829 | Comportamiento cercano a lo esperado |
| PUNTO_24 | PUNTO_24 | -0.897 | Comportamiento cercano a lo esperado |
| PUNTO_19 | PUNTO_19 | -0.897 | Comportamiento cercano a lo esperado |
| PUNTO_25 | PUNTO_25 | -1.132 | Comportamiento cercano a lo esperado |
| PUNTO_26 | PUNTO_26 | -1.132 | Comportamiento cercano a lo esperado |
| PUNTO_11 | PUNTO_11 | -1.591 | Comportamiento cercano a lo esperado |
| PUNTO_16 | PUNTO_16 | -1.591 | Comportamiento cercano a lo esperado |
| PUNTO_17 | PUNTO_17 | -1.591 | Comportamiento cercano a lo esperado |
| PUNTO_18 | PUNTO_18 | -1.591 | Comportamiento cercano a lo esperado |
| PUNTO_20 | PUNTO_20 | -1.591 | Comportamiento cercano a lo esperado |
| PUNTO_21 | PUNTO_21 | -1.591 | Comportamiento cercano a lo esperado |
| PUNTO_23 | PUNTO_23 | -1.631 | Comportamiento cercano a lo esperado |
if (
!is.null(
chi_total_cc
)
) {
V_cramer_total <- sqrt(
unname(
chi_total_cc$statistic
) /
(
sum(
tabla_alerta_total_valida
) *
min(
nrow(
tabla_alerta_total_valida
) - 1,
ncol(
tabla_alerta_total_valida
) - 1
)
)
)
V_cramer_total
}
## [1] 0.5619404
También contamos con un criterio específico para cada punto:
\[ P90_j \]
Este identifica resultados elevados respecto del propio comportamiento del punto \(j\).
El criterio global responde:
¿El resultado es elevado frente a toda la base?
El criterio local responde:
¿El resultado es inusual frente al comportamiento propio del punto?
tabla_global_local <- table(
Global = datos$evento_alerta,
Local = datos$evento_alerta_punto
)
tabla_global_local
## Local
## Global Alerta Sin alerta
## Alerta 11 20
## Sin alerta 20 256
Las cuatro posibilidades son:
Alerta global y local El resultado es elevado frente a toda la base y también inusual para ese punto.
Solo alerta global El resultado es elevado frente al conjunto completo, pero puede encontrarse dentro del comportamiento habitual de un punto que generalmente presenta valores altos.
Solo alerta local El resultado no se encuentra entre los más elevados de toda la base, pero es inusual para el comportamiento histórico de ese punto.
Sin alerta No supera ninguno de los criterios estadísticos.
Un punto con comportamiento habitualmente elevado no debe considerarse microbiológicamente favorable únicamente porque un resultado no supere su P90 local.
\[ \boxed{\text{Punto + Alerta}} \]
\[ \Downarrow \]
\[ \boxed{\text{Frecuencias}} \]
\[ \Downarrow \]
\[ \boxed{\text{Proporciones y porcentajes}} \]
\[ \Downarrow \]
\[ \boxed{\text{Tabla de contingencia}} \]
\[ \Downarrow \]
\[ \boxed{\text{Frecuencias esperadas}} \]
\[ \Downarrow \]
\[ \boxed{\chi^2\text{ / Fisher}} \]
\[ \Downarrow \]
\[ \boxed{p} \]
\[ \Downarrow \]
\[ \boxed{\text{OR + V de Cramér}} \]
\[ \Downarrow \]
\[ \boxed{\text{Residuos}} \]
\[ \Downarrow \]
\[ \boxed{\text{Interpretación microbiológica}} \]
Una relación cuantitativa–cualitativa combina una variable numérica con una variable que define grupos.
En nuestra base:
\[ Y= \log_{10}(\text{NMP/100 mL}) \]
y:
\[ X= \text{Punto de muestreo} \]
La pregunta será:
¿Los resultados microbiológicos presentan el mismo comportamiento en todos los puntos?
La variable respuesta será:
\[ Y= \log_{10}(\text{NMP/100 mL}) \]
La variable que define los grupos es:
\[ X= \text{Punto} \]
Por tanto:
conteo_qc <- table(
datos$punto
)
puntos_qc <- names(
conteo_qc[
conteo_qc >= 5
]
)
if (
length(
puntos_qc
) >= 2
) {
puntos_qc <- puntos_qc[
1:2
]
ejemplo_qc <- datos[
datos$punto %in% puntos_qc &
is.finite(
datos$log10_nmp
),
]
ejemplo_qc$punto <- factor(
ejemplo_qc$punto,
levels = puntos_qc
)
knitr::kable(
head(
ejemplo_qc[
,
c(
"punto",
"fecha",
"resultado_nmp",
"log10_nmp"
)
],
12
),
digits = 3,
caption = paste(
"Ejemplo cuantitativa–cualitativa:",
paste(
puntos_qc,
collapse = " y "
)
)
)
} else {
ejemplo_qc <- NULL
}
| punto | fecha | resultado_nmp | log10_nmp |
|---|---|---|---|
| PUNTO_11 | 2026-05-06 | 3500.0 | 3.544 |
| PUNTO_16 | 2026-05-06 | 230.0 | 2.362 |
| PUNTO_11 | 2026-01-29 | 8164.0 | 3.912 |
| PUNTO_16 | 2026-01-29 | 1935.0 | 3.287 |
| PUNTO_11 | 2026-02-10 | 3448.0 | 3.538 |
| PUNTO_16 | 2026-02-10 | 5172.0 | 3.714 |
| PUNTO_11 | 2026-03-03 | 8664.0 | 3.938 |
| PUNTO_11 | 2026-03-03 | 866.4 | 2.938 |
| PUNTO_16 | 2026-03-03 | 360.9 | 2.557 |
| PUNTO_16 | 2026-03-03 | 3609.0 | 3.557 |
| PUNTO_11 | 2026-03-24 | 4352.0 | 3.639 |
| PUNTO_11 | 2026-03-24 | 435.2 | 2.639 |
if (
!is.null(
ejemplo_qc
)
) {
resumen_qc <- do.call(
rbind,
lapply(
split(
ejemplo_qc$log10_nmp,
ejemplo_qc$punto
),
function(x) {
x <- x[
is.finite(x)
]
data.frame(
N = length(x),
Media = mean(x),
Mediana = median(x),
SD = sd(x),
Q1 = as.numeric(
quantile(
x,
0.25
)
),
Q3 = as.numeric(
quantile(
x,
0.75
)
),
IQR = IQR(x)
)
}
)
)
resumen_qc$Punto <- rownames(
resumen_qc
)
rownames(
resumen_qc
) <- NULL
knitr::kable(
resumen_qc,
digits = 3,
caption = "Resumen descriptivo de los dos puntos"
)
}
| N | Media | Mediana | SD | Q1 | Q3 | IQR | Punto |
|---|---|---|---|---|---|---|---|
| 21 | 2.872 | 3.030 | 0.657 | 2.184 | 3.203 | 1.019 | PUNTO_11 |
| 21 | 2.697 | 2.613 | 0.816 | 2.354 | 3.287 | 0.933 | PUNTO_16 |
Debemos comparar:
Un punto puede presentar una media mayor pero una mediana similar debido a observaciones extremadamente elevadas.
La media puede verse fuertemente afectada por episodios microbiológicos de gran magnitud.
La mediana describe mejor el comportamiento central cuando existe fuerte asimetría.
El IQR muestra qué tan variable es el comportamiento habitual del punto.
if (
!is.null(
ejemplo_qc
)
) {
boxplot(
log10_nmp ~ punto,
data = ejemplo_qc,
col = "#edf8f7",
border = "#083d5c",
xlab = "Punto de muestreo",
ylab = expression(
log[10]*"(NMP/100 mL)"
),
main = "Comparación microbiológica entre dos puntos"
)
stripchart(
log10_nmp ~ punto,
data = ejemplo_qc,
vertical = TRUE,
method = "jitter",
add = TRUE,
pch = 19,
cex = 0.65,
col = "#1596a5"
)
}
Debemos revisar:
Una comparación paramétrica requiere considerar:
Independencia Las observaciones deberían ser razonablemente independientes.
Normalidad La distribución de la respuesta dentro de cada grupo debería presentar comportamiento aproximadamente normal para la inferencia clásica.
Varianzas La prueba t clásica supone igualdad de varianzas.
La prueba t de Welch permite varianzas diferentes.
Las hipótesis de Shapiro-Wilk son:
\[ H_0: \text{los datos son compatibles con normalidad} \]
\[ H_1: \text{los datos no son compatibles con normalidad} \]
if (
!is.null(
ejemplo_qc
)
) {
normalidad_qc <- do.call(
rbind,
lapply(
split(
ejemplo_qc$log10_nmp,
ejemplo_qc$punto
),
function(x) {
x <- x[
is.finite(x)
]
if (
length(x) >= 3 &&
length(
unique(x)
) > 1
) {
sh <- shapiro.test(
x
)
data.frame(
N = length(x),
W = unname(
sh$statistic
),
p_value = sh$p.value
)
} else {
data.frame(
N = length(x),
W = NA_real_,
p_value = NA_real_
)
}
}
)
)
normalidad_qc$Punto <- rownames(
normalidad_qc
)
rownames(
normalidad_qc
) <- NULL
normalidad_qc$Decision <- ifelse(
is.na(
normalidad_qc$p_value
),
"No evaluable",
ifelse(
normalidad_qc$p_value < 0.05,
"Evidencia contra normalidad",
"No se rechaza normalidad"
)
)
knitr::kable(
normalidad_qc,
digits = 4,
caption = "Evaluación de normalidad de los dos puntos"
)
}
| N | W | p_value | Punto | Decision |
|---|---|---|---|---|
| 21 | 0.9308 | 0.1432 | PUNTO_11 | No se rechaza normalidad |
| 21 | 0.9688 | 0.7073 | PUNTO_16 | No se rechaza normalidad |
Una desviación de normalidad no implica necesariamente:
Puede representar heterogeneidad microbiológica real o episodios de magnitud elevada.
Las hipótesis son:
\[ H_0: \mu_1=\mu_2 \]
frente a:
\[ H_1: \mu_1\neq\mu_2 \]
equivalentemente:
\[ H_0: \mu_1-\mu_2=0 \]
El estimador es:
\[ \bar X_1-\bar X_2 \]
if (
!is.null(
ejemplo_qc
)
) {
medias_qc <- tapply(
ejemplo_qc$log10_nmp,
ejemplo_qc$punto,
mean,
na.rm = TRUE
)
medias_qc
diferencia_medias_qc <- medias_qc[1] -
medias_qc[2]
diferencia_medias_qc
}
## PUNTO_11
## 0.1748459
El estadístico es:
\[ t = \frac{ \bar X_1-\bar X_2 }{ \sqrt{ \frac{s_1^2}{n_1} + \frac{s_2^2}{n_2} } } \]
donde:
if (
!is.null(
ejemplo_qc
)
) {
prueba_welch_qc <- t.test(
log10_nmp ~ punto,
data = ejemplo_qc,
var.equal = FALSE,
conf.level = 0.95
)
prueba_welch_qc
}
##
## Welch Two Sample t-test
##
## data: log10_nmp by punto
## t = 0.7651, df = 38.255, p-value = 0.4489
## alternative hypothesis: true difference in means between group PUNTO_11 and group PUNTO_16 is not equal to 0
## 95 percent confidence interval:
## -0.2876804 0.6373722
## sample estimates:
## mean in group PUNTO_11 mean in group PUNTO_16
## 2.871775 2.696929
Welch utiliza la aproximación:
\[ \nu= \frac{ \left( \frac{s_1^2}{n_1} + \frac{s_2^2}{n_2} \right)^2 }{ \frac{ (s_1^2/n_1)^2 }{ n_1-1 } + \frac{ (s_2^2/n_2)^2 }{ n_2-1 } } \]
Esto permite comparar grupos con varianzas y tamaños muestrales diferentes.
if (
exists(
"prueba_welch_qc"
)
) {
prueba_welch_qc$conf.int
}
## [1] -0.2876804 0.6373722
## attr(,"conf.level")
## [1] 0.95
Si el intervalo incluye:
\[ 0 \]
los datos son compatibles con ausencia de diferencia de medias.
Si:
\[ p<0.05 \]
existe evidencia de diferencia en las medias.
Pero la conclusión debe considerar también:
Una diferencia estadísticamente detectable significa que los dos puntos presentan diferentes niveles promedio en escala logarítmica.
Esto puede reflejar perfiles microbiológicos distintos entre las ubicaciones.
No demuestra que el punto sea la causa.
Cuando existe fuerte asimetría o valores extremos puede utilizarse una comparación basada en rangos.
if (
!is.null(
ejemplo_qc
)
) {
wilcox.test(
log10_nmp ~ punto,
data = ejemplo_qc,
exact = FALSE,
conf.int = TRUE
)
}
##
## Wilcoxon rank sum test with continuity correction
##
## data: log10_nmp by punto
## W = 249, p-value = 0.4812
## alternative hypothesis: true location shift is not equal to 0
## 95 percent confidence interval:
## -0.3540329 0.6940347
## sample estimates:
## difference in location
## 0.2573363
Wilcoxon evalúa diferencias en rangos/distribuciones.
No debe interpretarse automáticamente como una prueba estrictamente de medianas.
La interpretación como diferencia de localización resulta más apropiada cuando las distribuciones presentan formas comparables.
\[ d = \frac{ \bar X_1-\bar X_2 }{ s_p } \]
donde:
\[ s_p = \sqrt{ \frac{ (n_1-1)s_1^2+ (n_2-1)s_2^2 }{ n_1+n_2-2 } } \]
if (
!is.null(
ejemplo_qc
)
) {
grupos_qc <- split(
ejemplo_qc$log10_nmp,
ejemplo_qc$punto
)
x1_qc <- grupos_qc[[1]]
x2_qc <- grupos_qc[[2]]
n1_qc <- length(
x1_qc
)
n2_qc <- length(
x2_qc
)
s1_qc <- sd(
x1_qc
)
s2_qc <- sd(
x2_qc
)
sp_qc <- sqrt(
(
(n1_qc - 1) * s1_qc^2 +
(n2_qc - 1) * s2_qc^2
) /
(
n1_qc + n2_qc - 2
)
)
d_cohen_qc <- (
mean(
x1_qc
) -
mean(
x2_qc
)
) /
sp_qc
d_cohen_qc
}
## [1] 0.2361155
Una guía descriptiva frecuente es:
| \(|d|\) | Magnitud |
|---|---|
| 0.20 | Pequeña |
| 0.50 | Moderada |
| 0.80 | Grande |
El valor \(p\) informa sobre evidencia estadística.
Cohen \(d\) informa sobre cuánto se separan los dos puntos en relación con su variabilidad interna.
Una diferencia estadísticamente significativa puede tener una magnitud microbiológica pequeña.
Ahora consideramos:
\[ k>2 \]
puntos.
La pregunta será:
¿Todos los puntos presentan el mismo nivel microbiológico?
resumen_total_qc <- do.call(
rbind,
lapply(
split(
datos$log10_nmp,
datos$punto
),
function(x) {
x <- x[
is.finite(x)
]
data.frame(
N = length(x),
Media = mean(x),
Mediana = median(x),
SD = sd(x),
Q1 = as.numeric(
quantile(
x,
0.25
)
),
Q3 = as.numeric(
quantile(
x,
0.75
)
),
IQR = IQR(x)
)
}
)
)
resumen_total_qc$Punto <- rownames(
resumen_total_qc
)
rownames(
resumen_total_qc
) <- NULL
resumen_total_qc <- resumen_total_qc[
,
c(
"Punto",
"N",
"Media",
"Mediana",
"SD",
"Q1",
"Q3",
"IQR"
)
]
knitr::kable(
resumen_total_qc,
digits = 3,
caption = "Resumen cuantitativo por punto"
)
| Punto | N | Media | Mediana | SD | Q1 | Q3 | IQR |
|---|---|---|---|---|---|---|---|
| PUNTO_11 | 21 | 2.872 | 3.030 | 0.657 | 2.184 | 3.203 | 1.019 |
| PUNTO_16 | 21 | 2.697 | 2.613 | 0.816 | 2.354 | 3.287 | 0.933 |
| PUNTO_17 | 21 | 2.662 | 2.681 | 0.699 | 1.993 | 3.322 | 1.328 |
| PUNTO_18 | 21 | 2.649 | 2.738 | 1.226 | 2.074 | 3.304 | 1.229 |
| PUNTO_19 | 22 | 4.115 | 4.569 | 1.518 | 2.538 | 5.476 | 2.938 |
| PUNTO_20 | 21 | 3.504 | 3.362 | 1.672 | 2.221 | 4.843 | 2.622 |
| PUNTO_21 | 21 | 2.947 | 3.080 | 1.207 | 1.831 | 3.831 | 2.000 |
| PUNTO_23 | 22 | 4.095 | 4.688 | 1.495 | 2.689 | 5.494 | 2.806 |
| PUNTO_24 | 7 | 3.859 | 5.382 | 2.186 | 1.601 | 5.601 | 4.000 |
| PUNTO_25 | 11 | 4.054 | 4.380 | 1.513 | 2.488 | 5.464 | 2.976 |
| PUNTO_26 | 11 | 4.219 | 4.380 | 1.506 | 2.713 | 5.626 | 2.913 |
| PUNTO_4 | 6 | 3.434 | 3.713 | 2.329 | 1.574 | 5.302 | 3.729 |
| PUNTO_5 | 19 | 5.001 | 6.233 | 2.566 | 2.360 | 7.289 | 4.929 |
| PUNTO_6 | 20 | 4.764 | 5.494 | 1.981 | 2.594 | 6.557 | 3.962 |
| PUNTO_7 | 21 | 4.199 | 5.041 | 1.875 | 2.252 | 5.957 | 3.705 |
| PUNTO_8 | 20 | 4.766 | 5.777 | 2.017 | 2.657 | 6.458 | 3.801 |
| PUNTO_9 | 22 | 4.265 | 5.490 | 2.040 | 2.085 | 5.983 | 3.898 |
Los puntos pueden mostrar diferentes combinaciones de:
Por ello no debe seleccionarse un punto prioritario únicamente a partir de la media.
par(
mar = c(
8,
5,
4,
2
)
)
boxplot(
log10_nmp ~ punto,
data = datos,
col = "#edf8f7",
border = "#083d5c",
las = 2,
cex.axis = 0.67,
xlab = "",
ylab = expression(
log[10]*"(NMP/100 mL)"
),
main = "Distribución microbiológica por punto"
)
stripchart(
log10_nmp ~ punto,
data = datos,
vertical = TRUE,
method = "jitter",
add = TRUE,
pch = 19,
cex = 0.45,
col = "#1596a5"
)
mtext(
"Punto de muestreo",
side = 1,
line = 6
)
rango_log_mod3 <- range(
datos$log10_nmp,
na.rm = TRUE
)
breaks_mod3 <- seq(
floor(
rango_log_mod3[1]
),
ceiling(
rango_log_mod3[2]
),
length.out = 13
)
par_original_hist <- par(
no.readonly = TRUE
)
par(
mfrow = c(5,4),
mar = c(3.2,3.2,2.7,1),
oma = c(4.5,4,4.5,1)
)
for (
p in puntos_mod3
) {
x <- datos$log10_nmp[
datos$punto == p &
is.finite(
datos$log10_nmp
)
]
if (
length(x) > 0
) {
hist(
x,
breaks = breaks_mod3,
xlim = rango_log_mod3,
col = "#1596a5",
border = "white",
main = p,
xlab = "",
ylab = "",
cex.main = 0.85,
cex.axis = 0.68
)
abline(
v = median(
x,
na.rm = TRUE
),
col = "#083d5c",
lty = 2,
lwd = 2
)
}
}
mtext(
expression(
log[10]*"(NMP/100 mL)"
),
side = 1,
outer = TRUE,
line = 2.3
)
mtext(
"Frecuencia",
side = 2,
outer = TRUE,
line = 2.2
)
mtext(
"Distribución microbiológica por punto",
side = 3,
outer = TRUE,
line = 1.6,
font = 2
)
par(
par_original_hist
)
par_original_qq <- par(
no.readonly = TRUE
)
par(
mfrow = c(5,4),
mar = c(3.2,3.2,2.7,1),
oma = c(4.5,4.5,4,1)
)
for (
p in puntos_mod3
) {
x <- datos$log10_nmp[
datos$punto == p &
is.finite(
datos$log10_nmp
)
]
if (
length(x) >= 3
) {
qqnorm(
x,
pch = 19,
cex = 0.6,
col = "#1596a5",
main = p,
xlab = "",
ylab = ""
)
qqline(
x,
col = "#083d5c",
lwd = 2
)
}
}
mtext(
"Cuantiles teóricos",
side = 1,
outer = TRUE,
line = 2
)
mtext(
"Cuantiles observados",
side = 2,
outer = TRUE,
line = 2
)
mtext(
"Gráficos Q-Q por punto",
side = 3,
outer = TRUE,
line = 1.5,
font = 2
)
par(
par_original_qq
)
normalidad_total_qc <- do.call(
rbind,
lapply(
split(
datos$log10_nmp,
datos$punto
),
function(x) {
x <- x[
is.finite(x)
]
if (
length(x) >= 3 &&
length(x) <= 5000 &&
length(
unique(x)
) > 1
) {
sh <- shapiro.test(
x
)
data.frame(
N = length(x),
W = unname(
sh$statistic
),
p_value = sh$p.value
)
} else {
data.frame(
N = length(x),
W = NA_real_,
p_value = NA_real_
)
}
}
)
)
normalidad_total_qc$Punto <- rownames(
normalidad_total_qc
)
rownames(
normalidad_total_qc
) <- NULL
normalidad_total_qc$Decision <- ifelse(
is.na(
normalidad_total_qc$p_value
),
"No evaluable",
ifelse(
normalidad_total_qc$p_value < 0.05,
"Evidencia contra normalidad",
"No se rechaza normalidad"
)
)
normalidad_total_qc <- normalidad_total_qc[
,
c(
"Punto",
"N",
"W",
"p_value",
"Decision"
)
]
knitr::kable(
normalidad_total_qc,
digits = 4,
caption = "Normalidad en escala log10 por punto"
)
| Punto | N | W | p_value | Decision |
|---|---|---|---|---|
| PUNTO_11 | 21 | 0.9308 | 0.1432 | No se rechaza normalidad |
| PUNTO_16 | 21 | 0.9688 | 0.7073 | No se rechaza normalidad |
| PUNTO_17 | 21 | 0.9486 | 0.3201 | No se rechaza normalidad |
| PUNTO_18 | 21 | 0.9650 | 0.6220 | No se rechaza normalidad |
| PUNTO_19 | 22 | 0.8510 | 0.0035 | Evidencia contra normalidad |
| PUNTO_20 | 21 | 0.9655 | 0.6323 | No se rechaza normalidad |
| PUNTO_21 | 21 | 0.9350 | 0.1732 | No se rechaza normalidad |
| PUNTO_23 | 22 | 0.8687 | 0.0074 | Evidencia contra normalidad |
| PUNTO_24 | 7 | 0.7275 | 0.0073 | Evidencia contra normalidad |
| PUNTO_25 | 11 | 0.7848 | 0.0059 | Evidencia contra normalidad |
| PUNTO_26 | 11 | 0.7757 | 0.0045 | Evidencia contra normalidad |
| PUNTO_4 | 6 | 0.8968 | 0.3555 | No se rechaza normalidad |
| PUNTO_5 | 19 | 0.7877 | 0.0008 | Evidencia contra normalidad |
| PUNTO_6 | 20 | 0.7841 | 0.0005 | Evidencia contra normalidad |
| PUNTO_7 | 21 | 0.8562 | 0.0054 | Evidencia contra normalidad |
| PUNTO_8 | 20 | 0.8126 | 0.0013 | Evidencia contra normalidad |
| PUNTO_9 | 22 | 0.7695 | 0.0002 | Evidencia contra normalidad |
El ANOVA clásico supone:
\[ \sigma_1^2 = \sigma_2^2 = \cdots = \sigma_k^2 \]
Una prueba disponible en R es Bartlett:
bartlett.test(
log10_nmp ~ punto,
data = datos
)
##
## Bartlett test of homogeneity of variances
##
## data: log10_nmp by punto
## Bartlett's K-squared = 74.572, df = 16, p-value = 1.558e-09
Bartlett es sensible a desviaciones de normalidad.
Por ello no debe utilizarse de forma aislada para seleccionar el método.
El modelo es:
\[ Y_{ij} = \mu + \tau_j + \varepsilon_{ij} \]
donde:
\[ H_0: \mu_1= \mu_2= \cdots= \mu_k \]
frente a:
\[ H_1: \text{al menos una media difiere} \]
ANOVA separa:
\[ SS_T = SS_B + SS_W \]
donde:
\[ F = \frac{ MS_B }{ MS_W } \]
donde:
\[ MS_B = \frac{ SS_B }{ k-1 } \]
y:
\[ MS_W = \frac{ SS_W }{ N-k } \]
Hasta este punto hemos presentado el modelo general:
\[ Y_{ij} = \mu+\tau_j+\varepsilon_{ij} \]
pero para comprender realmente el ANOVA necesitamos estudiar de dónde sale cada componente de la tabla.
En este análisis:
El ANOVA responde a la pregunta:
¿La variabilidad existente entre los puntos es suficientemente grande en comparación con la variabilidad que existe dentro de los propios puntos?
Para cualquier observación puede escribirse:
\[ Y_{ij}-\bar Y_{..} = \left( \bar Y_{.j}-\bar Y_{..} \right) + \left( Y_{ij}-\bar Y_{.j} \right) \]
donde:
Esta identidad conduce a:
\[ SS_T = SS_B + SS_W \]
donde:
\[ SS_T = \sum_{j=1}^{k} \sum_{i=1}^{n_j} \left( Y_{ij}-\bar Y_{..} \right)^2 \]
donde:
Los grados de libertad son:
\[ gl_T=N-1 \]
\[ SS_B = \sum_{j=1}^{k} n_j \left( \bar Y_{.j}-\bar Y_{..} \right)^2 \]
Cuanto más separadas estén las medias de los puntos respecto de la media general, mayor será \(SS_B\).
\[ gl_B=k-1 \]
\[ MS_B = \frac{SS_B}{k-1} \]
\[ SS_W = \sum_{j=1}^{k} \sum_{i=1}^{n_j} \left( Y_{ij}-\bar Y_{.j} \right)^2 \]
Esta cantidad mide la variabilidad microbiológica que permanece dentro de los puntos.
\[ gl_W=N-k \]
\[ MS_W = \frac{SS_W}{N-k} \]
\[ F = \frac{MS_B}{MS_W} \]
Bajo:
\[ H_0: \mu_1=\mu_2=\cdots=\mu_k \]
el estadístico sigue una distribución:
\[ F \sim F_{k-1,N-k} \]
y el valor \(p\) es:
\[ p = P \left( F_{k-1,N-k} \geq F_{\text{obs}} \right) \]
Si \(F\) es grande, la variabilidad entre puntos domina sobre la variabilidad interna.
Para comprender el procedimiento utilizaremos un ejemplo pequeño tomado directamente de la misma base del laboratorio.
Se seleccionarán automáticamente tres puntos con al menos tres observaciones válidas y se tomarán tres resultados de cada punto.
El ejemplo es pedagógico: sirve para construir el ANOVA paso a paso. La conclusión general debe realizarse después con todos los datos.
conteo_anova_manual <- table(
datos$punto[
is.finite(datos$log10_nmp)
]
)
puntos_anova_manual <- names(
conteo_anova_manual[
conteo_anova_manual >= 3
]
)
if (
length(puntos_anova_manual) >= 3
) {
puntos_anova_manual <- puntos_anova_manual[1:3]
ejemplo_anova_manual <- do.call(
rbind,
lapply(
puntos_anova_manual,
function(p) {
temp <- datos[
datos$punto == p &
is.finite(datos$log10_nmp),
c(
"punto",
"fecha",
"resultado_nmp",
"log10_nmp"
)
]
temp <- temp[
order(temp$fecha),
]
head(
temp,
3
)
}
)
)
ejemplo_anova_manual$punto <- factor(
ejemplo_anova_manual$punto,
levels = puntos_anova_manual
)
rownames(ejemplo_anova_manual) <- NULL
knitr::kable(
ejemplo_anova_manual,
digits = 4,
caption = "Ejemplo manual con tres puntos y tres observaciones reales por punto"
)
} else {
ejemplo_anova_manual <- NULL
cat(
"No existen tres puntos con al menos tres observaciones válidas."
)
}
| punto | fecha | resultado_nmp | log10_nmp |
|---|---|---|---|
| PUNTO_11 | 2026-01-29 | 8164.0 | 3.9119 |
| PUNTO_11 | 2026-02-10 | 3448.0 | 3.5376 |
| PUNTO_11 | 2026-03-03 | 8664.0 | 3.9377 |
| PUNTO_16 | 2026-01-29 | 1935.0 | 3.2867 |
| PUNTO_16 | 2026-02-10 | 5172.0 | 3.7137 |
| PUNTO_16 | 2026-03-03 | 360.9 | 2.5574 |
| PUNTO_17 | 2026-01-29 | 2333.0 | 3.3679 |
| PUNTO_17 | 2026-02-10 | 3654.0 | 3.5628 |
| PUNTO_17 | 2026-03-03 | 2603.0 | 3.4155 |
\[ \bar Y_{.j} = \frac{1}{n_j} \sum_{i=1}^{n_j} Y_{ij} \]
if (!is.null(ejemplo_anova_manual)) {
medias_manual_anova <- tapply(
ejemplo_anova_manual$log10_nmp,
ejemplo_anova_manual$punto,
mean
)
n_manual_anova <- table(
ejemplo_anova_manual$punto
)
tabla_medias_manual <- data.frame(
Punto = names(medias_manual_anova),
n = as.numeric(n_manual_anova),
Media_log10 = as.numeric(medias_manual_anova)
)
knitr::kable(
tabla_medias_manual,
digits = 4,
caption = "Media de cada punto"
)
}
| Punto | n | Media_log10 |
|---|---|---|
| PUNTO_11 | 3 | 3.7957 |
| PUNTO_16 | 3 | 3.1859 |
| PUNTO_17 | 3 | 3.4487 |
\[ \bar Y_{..} = \frac{1}{N} \sum_{j=1}^{k} \sum_{i=1}^{n_j} Y_{ij} \]
if (!is.null(ejemplo_anova_manual)) {
media_general_manual <- mean(
ejemplo_anova_manual$log10_nmp
)
media_general_manual
}
## [1] 3.476786
En el ejemplo:
\[ \bar Y_{..} = \text{3.4768} \]
Para cada punto:
\[ n_j \left( \bar Y_{.j}-\bar Y_{..} \right)^2 \]
if (!is.null(ejemplo_anova_manual)) {
contribuciones_entre <- data.frame(
Punto = names(medias_manual_anova),
n = as.numeric(n_manual_anova),
Media_punto = as.numeric(medias_manual_anova)
)
contribuciones_entre$Media_general <- media_general_manual
contribuciones_entre$Diferencia <- (
contribuciones_entre$Media_punto -
media_general_manual
)
contribuciones_entre$Diferencia_cuadrado <- (
contribuciones_entre$Diferencia^2
)
contribuciones_entre$Contribucion_SS_entre <- (
contribuciones_entre$n *
contribuciones_entre$Diferencia_cuadrado
)
SS_entre_manual <- sum(
contribuciones_entre$Contribucion_SS_entre
)
knitr::kable(
contribuciones_entre,
digits = 4,
caption = "Cálculo de la suma de cuadrados entre puntos"
)
SS_entre_manual
}
## [1] 0.5613669
\[ SS_B = \text{0.5614} \]
Para cada observación:
\[ Y_{ij}-\bar Y_{.j} \]
y:
\[ \left( Y_{ij}-\bar Y_{.j} \right)^2 \]
if (!is.null(ejemplo_anova_manual)) {
tabla_dentro_manual <- ejemplo_anova_manual[
,
c(
"punto",
"resultado_nmp",
"log10_nmp"
)
]
tabla_dentro_manual$Media_punto <- ave(
tabla_dentro_manual$log10_nmp,
tabla_dentro_manual$punto,
FUN = mean
)
tabla_dentro_manual$Desviacion_dentro <- (
tabla_dentro_manual$log10_nmp -
tabla_dentro_manual$Media_punto
)
tabla_dentro_manual$Desviacion_dentro_cuadrado <- (
tabla_dentro_manual$Desviacion_dentro^2
)
SS_dentro_manual <- sum(
tabla_dentro_manual$Desviacion_dentro_cuadrado
)
knitr::kable(
tabla_dentro_manual,
digits = 4,
caption = "Cálculo de la suma de cuadrados dentro de los puntos"
)
SS_dentro_manual
}
## [1] 0.8046613
\[ SS_W = \text{0.8047} \]
\[ SS_T = \sum \left( Y_{ij}-\bar Y_{..} \right)^2 \]
y debe cumplirse:
\[ SS_T = SS_B+SS_W \]
if (!is.null(ejemplo_anova_manual)) {
tabla_total_manual <- data.frame(
Punto = ejemplo_anova_manual$punto,
Y = ejemplo_anova_manual$log10_nmp,
Media_general = media_general_manual
)
tabla_total_manual$Desviacion_total <- (
tabla_total_manual$Y -
tabla_total_manual$Media_general
)
tabla_total_manual$Desviacion_total_cuadrado <- (
tabla_total_manual$Desviacion_total^2
)
SS_total_manual <- sum(
tabla_total_manual$Desviacion_total_cuadrado
)
comprobacion_SS <- (
SS_entre_manual +
SS_dentro_manual
)
knitr::kable(
tabla_total_manual,
digits = 4,
caption = "Cálculo de la suma de cuadrados total"
)
knitr::kable(
data.frame(
Metodo = c(
"Cálculo directo",
"SS entre + SS dentro"
),
Resultado = c(
SS_total_manual,
comprobacion_SS
)
),
digits = 4,
caption = "Comprobación de la identidad fundamental del ANOVA"
)
}
| Metodo | Resultado |
|---|---|
| Cálculo directo | 1.366 |
| SS entre + SS dentro | 1.366 |
En general:
\[ gl_B=k-1 \]
\[ gl_W=N-k \]
\[ gl_T=N-1 \]
if (!is.null(ejemplo_anova_manual)) {
k_manual <- nlevels(
ejemplo_anova_manual$punto
)
N_manual <- nrow(
ejemplo_anova_manual
)
gl_entre_manual <- k_manual - 1
gl_dentro_manual <- N_manual - k_manual
gl_total_manual <- N_manual - 1
knitr::kable(
data.frame(
Fuente = c(
"Entre puntos",
"Dentro de puntos",
"Total"
),
Grados_libertad = c(
gl_entre_manual,
gl_dentro_manual,
gl_total_manual
)
),
caption = "Grados de libertad del ejemplo"
)
}
| Fuente | Grados_libertad |
|---|---|
| Entre puntos | 2 |
| Dentro de puntos | 6 |
| Total | 8 |
\[ MS_B = \frac{SS_B}{gl_B} \]
\[ MS_W = \frac{SS_W}{gl_W} \]
if (!is.null(ejemplo_anova_manual)) {
MS_entre_manual <- (
SS_entre_manual /
gl_entre_manual
)
MS_dentro_manual <- (
SS_dentro_manual /
gl_dentro_manual
)
knitr::kable(
data.frame(
Fuente = c(
"Entre puntos",
"Dentro de puntos"
),
SS = c(
SS_entre_manual,
SS_dentro_manual
),
gl = c(
gl_entre_manual,
gl_dentro_manual
),
MS = c(
MS_entre_manual,
MS_dentro_manual
)
),
digits = 4,
caption = "Cálculo de los cuadrados medios"
)
}
| Fuente | SS | gl | MS |
|---|---|---|---|
| Entre puntos | 0.5614 | 2 | 0.2807 |
| Dentro de puntos | 0.8047 | 6 | 0.1341 |
\[ F = \frac{MS_B}{MS_W} \]
if (!is.null(ejemplo_anova_manual)) {
F_manual <- (
MS_entre_manual /
MS_dentro_manual
)
p_manual <- pf(
F_manual,
df1 = gl_entre_manual,
df2 = gl_dentro_manual,
lower.tail = FALSE
)
knitr::kable(
data.frame(
F_observado = F_manual,
gl_numerador = gl_entre_manual,
gl_denominador = gl_dentro_manual,
p_value = p_manual
),
digits = 4,
caption = "Cálculo manual del estadístico F"
)
}
| F_observado | gl_numerador | gl_denominador | p_value |
|---|---|---|---|
| 2.0929 | 2 | 6 | 0.2044 |
En el ejemplo:
\[ F = \text{2.0929} \]
y:
\[ p = \text{0.2044} \]
if (!is.null(ejemplo_anova_manual)) {
tabla_anova_manual_mod3 <- data.frame(
Fuente = c(
"Entre puntos",
"Dentro de puntos",
"Total"
),
SS = c(
SS_entre_manual,
SS_dentro_manual,
SS_total_manual
),
gl = c(
gl_entre_manual,
gl_dentro_manual,
gl_total_manual
),
MS = c(
MS_entre_manual,
MS_dentro_manual,
NA_real_
),
F = c(
F_manual,
NA_real_,
NA_real_
),
p_value = c(
p_manual,
NA_real_,
NA_real_
)
)
knitr::kable(
tabla_anova_manual_mod3,
digits = 4,
caption = "Tabla ANOVA construida manualmente"
)
}
| Fuente | SS | gl | MS | F | p_value |
|---|---|---|---|---|---|
| Entre puntos | 0.5614 | 2 | 0.2807 | 2.0929 | 0.2044 |
| Dentro de puntos | 0.8047 | 6 | 0.1341 | NA | NA |
| Total | 1.3660 | 8 | NA | NA | NA |
aov()if (!is.null(ejemplo_anova_manual)) {
modelo_comprobacion_manual <- aov(
log10_nmp ~ punto,
data = ejemplo_anova_manual
)
summary(
modelo_comprobacion_manual
)
}
## Df Sum Sq Mean Sq F value Pr(>F)
## punto 2 0.5614 0.2807 2.093 0.204
## Residuals 6 0.8047 0.1341
Los valores obtenidos mediante el procedimiento manual deben
coincidir, salvo redondeo, con la salida de aov().
Si:
\[ p<0.05 \]
existe evidencia contra:
\[ H_0: \mu_1=\mu_2=\mu_3 \]
y podemos concluir que al menos una media difiere.
Si:
\[ p\geq0.05 \]
no existe evidencia suficiente para afirmar que las medias sean distintas.
Este ejemplo utiliza solo nueve observaciones con fines didácticos; no sustituye el ANOVA completo de la base.
El estadístico \(F\) compara:
\[ \boxed{\text{variabilidad entre puntos}} \]
contra:
\[ \boxed{\text{variabilidad dentro de los puntos}} \]
Un valor grande de \(F\) indica que los niveles promedio de los puntos están separados en relación con la variabilidad que existe dentro de cada punto.
Microbiológicamente esto sugiere que las ubicaciones pueden presentar perfiles diferentes.
Sin embargo:
\[ \boxed{ \text{diferencia estadística} \neq \text{causalidad} } \]
La base es observacional. Por ello las diferencias pueden estar asociadas con características ambientales, hidráulicas, temporales u operacionales que no aparecen explícitamente en la base.
| Fuente | Suma de cuadrados | Grados de libertad | Cuadrado medio | Estadístico |
|---|---|---|---|---|
| Entre puntos | \(SS_B\) | \(k-1\) | \(MS_B=SS_B/(k-1)\) | \(F=MS_B/MS_W\) |
| Dentro de puntos | \(SS_W\) | \(N-k\) | \(MS_W=SS_W/(N-k)\) | |
| Total | \(SS_T\) | \(N-1\) |
La identidad fundamental es:
\[ \boxed{ SS_T=SS_B+SS_W } \]
modelo_anova_qc <- aov(
log10_nmp ~ punto,
data = datos
)
summary(
modelo_anova_qc
)
## Df Sum Sq Mean Sq F value Pr(>F)
## punto 16 195.3 12.207 4.588 3.35e-08 ***
## Residuals 290 771.6 2.661
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Si:
\[ p<0.05 \]
existe evidencia de que no todas las medias son iguales.
Sin embargo, la prueba global no identifica cuáles puntos difieren.
Un resultado significativo indica heterogeneidad microbiológica entre las ubicaciones.
Algunos puntos presentan niveles medios diferentes de otros, pero es necesario continuar con comparaciones posteriores.
Cuando las varianzas pueden ser diferentes:
resultado_welch_total <- oneway.test(
log10_nmp ~ punto,
data = datos,
var.equal = FALSE
)
resultado_welch_total
##
## One-way analysis of means (not assuming equal variances)
##
## data: log10_nmp and punto
## F = 5.7343, num df = 16.000, denom df = 85.756, p-value = 3.209e-08
Cuando las condiciones paramétricas no son adecuadas puede utilizarse:
\[ H = \frac{ 12 }{ N(N+1) } \sum_{j=1}^{k} \frac{ R_j^2 }{ n_j } - 3(N+1) \]
donde:
resultado_kruskal_qc <- kruskal.test(
resultado_nmp ~ punto,
data = datos
)
resultado_kruskal_qc
##
## Kruskal-Wallis rank sum test
##
## data: resultado_nmp by punto
## Kruskal-Wallis chi-squared = 45.19, df = 16, p-value = 0.0001298
Si:
\[ p<0.05 \]
existe evidencia de heterogeneidad en rangos/distribuciones entre puntos.
No significa automáticamente que todas las medianas sean diferentes.
Los puntos no presentan un comportamiento microbiológico homogéneo.
La diferencia puede involucrar:
| Situación | Método |
|---|---|
| Comportamiento aproximadamente normal y varianzas similares | ANOVA |
| Medias de interés pero varianzas diferentes | Welch |
| Fuerte no normalidad, extremos o rangos | Kruskal-Wallis |
La selección debe considerar conjuntamente:
TukeyHSD(
modelo_anova_qc
)
## Tukey multiple comparisons of means
## 95% family-wise confidence level
##
## Fit: aov(formula = log10_nmp ~ punto, data = datos)
##
## $punto
## diff lwr upr p adj
## PUNTO_16-PUNTO_11 -0.174845893 -1.93147335 1.5817816 1.0000000
## PUNTO_17-PUNTO_11 -0.210130108 -1.96675756 1.5464973 1.0000000
## PUNTO_18-PUNTO_11 -0.223180466 -1.97980792 1.5334470 1.0000000
## PUNTO_19-PUNTO_11 1.243225111 -0.49332594 2.9797762 0.5053624
## PUNTO_20-PUNTO_11 0.632523218 -1.12410424 2.3891507 0.9980389
## PUNTO_21-PUNTO_11 0.075702432 -1.68092502 1.8323299 1.0000000
## PUNTO_23-PUNTO_11 1.223304954 -0.51324610 2.9598560 0.5355531
## PUNTO_24-PUNTO_11 0.986992460 -1.49725391 3.4712388 0.9940951
## PUNTO_25-PUNTO_11 1.181783645 -0.93678878 3.3003561 0.8728556
## PUNTO_26-PUNTO_11 1.347210245 -0.77136218 3.4657827 0.7123867
## PUNTO_4-PUNTO_11 0.562330771 -2.07261041 3.1972720 0.9999980
## PUNTO_5-PUNTO_11 2.129701714 0.32743997 3.9319635 0.0054744
## PUNTO_6-PUNTO_11 1.892613884 0.11416413 3.6710636 0.0241980
## PUNTO_7-PUNTO_11 1.327234606 -0.42939285 3.0838621 0.4042740
## PUNTO_8-PUNTO_11 1.893763925 0.11531418 3.6722137 0.0240139
## PUNTO_9-PUNTO_11 1.392819693 -0.34373136 3.1293707 0.2977587
## PUNTO_17-PUNTO_16 -0.035284215 -1.79191167 1.7213432 1.0000000
## PUNTO_18-PUNTO_16 -0.048334574 -1.80496203 1.7082929 1.0000000
## PUNTO_19-PUNTO_16 1.418071003 -0.31848005 3.1546221 0.2680489
## PUNTO_20-PUNTO_16 0.807369110 -0.94925834 2.5639966 0.9742952
## PUNTO_21-PUNTO_16 0.250548324 -1.50607913 2.0071758 1.0000000
## PUNTO_23-PUNTO_16 1.398150846 -0.33840021 3.1347019 0.2913315
## PUNTO_24-PUNTO_16 1.161838352 -1.32240802 3.6460847 0.9697960
## PUNTO_25-PUNTO_16 1.356629537 -0.76194288 3.4752020 0.7015305
## PUNTO_26-PUNTO_16 1.522056138 -0.59651628 3.6406286 0.4987677
## PUNTO_4-PUNTO_16 0.737176664 -1.89776452 3.3721178 0.9999156
## PUNTO_5-PUNTO_16 2.304547606 0.50228587 4.1068093 0.0013979
## PUNTO_6-PUNTO_16 2.067459777 0.28901003 3.8459095 0.0070688
## PUNTO_7-PUNTO_16 1.502080499 -0.25454695 3.2587080 0.1987096
## PUNTO_8-PUNTO_16 2.068609818 0.29016007 3.8470596 0.0070087
## PUNTO_9-PUNTO_16 1.567665585 -0.16888547 3.3042166 0.1317030
## PUNTO_18-PUNTO_17 -0.013050359 -1.76967781 1.7435771 1.0000000
## PUNTO_19-PUNTO_17 1.453355218 -0.28319583 3.1899063 0.2297351
## PUNTO_20-PUNTO_17 0.842653325 -0.91397413 2.5992808 0.9620158
## PUNTO_21-PUNTO_17 0.285832539 -1.47079491 2.0424600 1.0000000
## PUNTO_23-PUNTO_17 1.433435061 -0.30311599 3.1699861 0.2508996
## PUNTO_24-PUNTO_17 1.197122567 -1.28712380 3.6813689 0.9604530
## PUNTO_25-PUNTO_17 1.391913752 -0.72665867 3.5104862 0.6597761
## PUNTO_26-PUNTO_17 1.557340353 -0.56123207 3.6759128 0.4556403
## PUNTO_4-PUNTO_17 0.772460879 -1.86248030 3.4074021 0.9998443
## PUNTO_5-PUNTO_17 2.339831821 0.53757008 4.1420936 0.0010466
## PUNTO_6-PUNTO_17 2.102743991 0.32429424 3.8811937 0.0054257
## PUNTO_7-PUNTO_17 1.537364714 -0.21926274 3.2939922 0.1679789
## PUNTO_8-PUNTO_17 2.103894033 0.32544428 3.8823438 0.0053786
## PUNTO_9-PUNTO_17 1.602949800 -0.13360125 3.3395009 0.1090809
## PUNTO_19-PUNTO_18 1.466405577 -0.27014548 3.2029566 0.2165318
## PUNTO_20-PUNTO_18 0.855703684 -0.90092377 2.6123311 0.9565105
## PUNTO_21-PUNTO_18 0.298882898 -1.45774456 2.0555104 0.9999999
## PUNTO_23-PUNTO_18 1.446485420 -0.29006563 3.1830365 0.2368964
## PUNTO_24-PUNTO_18 1.210172926 -1.27407344 3.6944193 0.9565027
## PUNTO_25-PUNTO_18 1.404964111 -0.71360831 3.5235365 0.6439717
## PUNTO_26-PUNTO_18 1.570390711 -0.54818171 3.6889631 0.4399607
## PUNTO_4-PUNTO_18 0.785511237 -1.84942994 3.4204524 0.9998067
## PUNTO_5-PUNTO_18 2.352882180 0.55062044 4.1551439 0.0009393
## PUNTO_6-PUNTO_18 2.115794350 0.33734460 3.8942441 0.0049135
## PUNTO_7-PUNTO_18 1.550415073 -0.20621238 3.3070425 0.1575438
## PUNTO_8-PUNTO_18 2.116944391 0.33849464 3.8953941 0.0048706
## PUNTO_9-PUNTO_18 1.616000159 -0.12055089 3.3525512 0.1015416
## PUNTO_20-PUNTO_19 -0.610701893 -2.34725295 1.1258492 0.9985121
## PUNTO_21-PUNTO_19 -1.167522679 -2.90407373 0.5690284 0.6202066
## PUNTO_23-PUNTO_19 -0.019920157 -1.73615997 1.6963197 1.0000000
## PUNTO_24-PUNTO_19 -0.256232651 -2.72632366 2.2138584 1.0000000
## PUNTO_25-PUNTO_19 -0.061441466 -2.16339738 2.0405144 1.0000000
## PUNTO_26-PUNTO_19 0.103985134 -1.99797078 2.2059410 1.0000000
## PUNTO_4-PUNTO_19 -0.680894340 -3.30249396 1.9407053 0.9999688
## PUNTO_5-PUNTO_19 0.886476603 -0.89622273 2.6691759 0.9480801
## PUNTO_6-PUNTO_19 0.649388773 -1.10923372 2.4080113 0.9973799
## PUNTO_7-PUNTO_19 0.084009496 -1.65254156 1.8205605 1.0000000
## PUNTO_8-PUNTO_19 0.650538814 -1.10808367 2.4091613 0.9973265
## PUNTO_9-PUNTO_19 0.149594582 -1.56664523 1.8658344 1.0000000
## PUNTO_21-PUNTO_20 -0.556820786 -2.31344824 1.1998067 0.9995788
## PUNTO_23-PUNTO_20 0.590781736 -1.14576932 2.3273328 0.9989974
## PUNTO_24-PUNTO_20 0.354469242 -2.12977713 2.8387156 1.0000000
## PUNTO_25-PUNTO_20 0.549260427 -1.56931200 2.6678328 0.9999696
## PUNTO_26-PUNTO_20 0.714687027 -1.40388539 2.8332594 0.9990945
## PUNTO_4-PUNTO_20 -0.070192447 -2.70513363 2.5647487 1.0000000
## PUNTO_5-PUNTO_20 1.497178496 -0.30508324 3.2994402 0.2410204
## PUNTO_6-PUNTO_20 1.260090666 -0.51835908 3.0385404 0.5247703
## PUNTO_7-PUNTO_20 0.694711389 -1.06191607 2.4513388 0.9943810
## PUNTO_8-PUNTO_20 1.261240707 -0.51720904 3.0396905 0.5230666
## PUNTO_9-PUNTO_20 0.760296475 -0.97625458 2.4968475 0.9838879
## PUNTO_23-PUNTO_21 1.147602522 -0.58894853 2.8841536 0.6499276
## PUNTO_24-PUNTO_21 0.911290028 -1.57295634 3.3955364 0.9975706
## PUNTO_25-PUNTO_21 1.106081213 -1.01249121 3.2246536 0.9226015
## PUNTO_26-PUNTO_21 1.271507813 -0.84706461 3.3900802 0.7937470
## PUNTO_4-PUNTO_21 0.486628340 -2.14831284 3.1215695 0.9999998
## PUNTO_5-PUNTO_21 2.053999282 0.25173754 3.8562610 0.0095214
## PUNTO_6-PUNTO_21 1.816911452 0.03846170 3.5953612 0.0394406
## PUNTO_7-PUNTO_21 1.251532175 -0.50509528 3.0081596 0.5144272
## PUNTO_8-PUNTO_21 1.818061493 0.03961174 3.5965112 0.0391573
## PUNTO_9-PUNTO_21 1.317117261 -0.41943379 3.0536683 0.3971612
## PUNTO_24-PUNTO_23 -0.236312494 -2.70640350 2.2337785 1.0000000
## PUNTO_25-PUNTO_23 -0.041521309 -2.14347722 2.0604346 1.0000000
## PUNTO_26-PUNTO_23 0.123905291 -1.97805062 2.2258612 1.0000000
## PUNTO_4-PUNTO_23 -0.660974182 -3.28257380 1.9606254 0.9999792
## PUNTO_5-PUNTO_23 0.906396760 -0.87630258 2.6890961 0.9374890
## PUNTO_6-PUNTO_23 0.669308930 -1.08931356 2.4279314 0.9963155
## PUNTO_7-PUNTO_23 0.103929653 -1.63262140 1.8404807 1.0000000
## PUNTO_8-PUNTO_23 0.670458971 -1.08816352 2.4290815 0.9962443
## PUNTO_9-PUNTO_23 0.169514739 -1.54672508 1.8857546 1.0000000
## PUNTO_25-PUNTO_24 0.194791185 -2.55731512 2.9468975 1.0000000
## PUNTO_26-PUNTO_24 0.360217785 -2.39188852 3.1123241 1.0000000
## PUNTO_4-PUNTO_24 -0.424661689 -3.59146687 2.7421435 1.0000000
## PUNTO_5-PUNTO_24 1.142709254 -1.37401230 3.6594308 0.9770843
## PUNTO_6-PUNTO_24 0.905621424 -1.59410327 3.4053461 0.9978949
## PUNTO_7-PUNTO_24 0.340242147 -2.14400422 2.8244885 1.0000000
## PUNTO_8-PUNTO_24 0.906771465 -1.59295322 3.4064962 0.9978638
## PUNTO_9-PUNTO_24 0.405827233 -2.06426377 2.8759182 1.0000000
## PUNTO_26-PUNTO_25 0.165426600 -2.26170302 2.5925562 1.0000000
## PUNTO_4-PUNTO_25 -0.619452873 -3.50831495 2.2694092 0.9999979
## PUNTO_5-PUNTO_25 0.947918069 -1.20864319 3.1044793 0.9832497
## PUNTO_6-PUNTO_25 0.710830239 -1.42587110 2.8475316 0.9992353
## PUNTO_7-PUNTO_25 0.145450962 -1.97312146 2.2640234 1.0000000
## PUNTO_8-PUNTO_25 0.711980280 -1.42472106 2.8486816 0.9992201
## PUNTO_9-PUNTO_25 0.211036048 -1.89091986 2.3129920 1.0000000
## PUNTO_4-PUNTO_26 -0.784879474 -3.67374155 2.1039826 0.9999428
## PUNTO_5-PUNTO_26 0.782491469 -1.37406979 2.9390527 0.9978574
## PUNTO_6-PUNTO_26 0.545403639 -1.59129770 2.6821050 0.9999754
## PUNTO_7-PUNTO_26 -0.019975639 -2.13854806 2.0985968 1.0000000
## PUNTO_8-PUNTO_26 0.546553680 -1.59014766 2.6832550 0.9999747
## PUNTO_9-PUNTO_26 0.045609448 -2.05634646 2.1475654 1.0000000
## PUNTO_5-PUNTO_4 1.567370942 -1.09821012 4.2329520 0.8185984
## PUNTO_6-PUNTO_4 1.330283113 -1.31925619 3.9798224 0.9436882
## PUNTO_7-PUNTO_4 0.764903835 -1.87003735 3.3998450 0.9998630
## PUNTO_8-PUNTO_4 1.331433154 -1.31810615 3.9809725 0.9432796
## PUNTO_9-PUNTO_4 0.830488921 -1.79111070 3.4520885 0.9995820
## PUNTO_6-PUNTO_5 -0.237087830 -2.06062584 1.5864502 1.0000000
## PUNTO_7-PUNTO_5 -0.802467107 -2.60472885 0.9997946 0.9810031
## PUNTO_8-PUNTO_5 -0.235937789 -2.05947580 1.5876002 1.0000000
## PUNTO_9-PUNTO_5 -0.736882021 -2.51958136 1.0458173 0.9910117
## PUNTO_7-PUNTO_6 -0.565379278 -2.34382903 1.2130705 0.9995632
## PUNTO_8-PUNTO_6 0.001150041 -1.79885746 1.8011575 1.0000000
## PUNTO_9-PUNTO_6 -0.499794191 -2.25841668 1.2588283 0.9998962
## PUNTO_8-PUNTO_7 0.566529319 -1.21192043 2.3449791 0.9995520
## PUNTO_9-PUNTO_7 0.065585086 -1.67096597 1.8021361 1.0000000
## PUNTO_9-PUNTO_8 -0.500944232 -2.25956672 1.2576783 0.9998930
Tukey identifica cuáles pares de medias presentan diferencias.
if (
resultado_kruskal_qc$p.value < 0.05
) {
comparaciones_kruskal <- pairwise.wilcox.test(
datos$resultado_nmp,
datos$punto,
p.adjust.method = "BH",
exact = FALSE
)
comparaciones_kruskal
}
##
## Pairwise comparisons using Wilcoxon rank sum test with continuity correction
##
## data: datos$resultado_nmp and datos$punto
##
## PUNTO_11 PUNTO_16 PUNTO_17 PUNTO_18 PUNTO_19 PUNTO_20 PUNTO_21
## PUNTO_16 0.648 - - - - - -
## PUNTO_17 0.471 0.959 - - - - -
## PUNTO_18 0.812 0.907 0.907 - - - -
## PUNTO_19 0.097 0.079 0.079 0.088 - - -
## PUNTO_20 0.334 0.177 0.174 0.214 0.463 - -
## PUNTO_21 0.846 0.564 0.471 0.564 0.089 0.501 -
## PUNTO_23 0.122 0.079 0.079 0.079 0.998 0.531 0.097
## PUNTO_24 0.757 0.564 0.757 0.508 0.947 0.846 0.531
## PUNTO_25 0.174 0.132 0.136 0.161 0.959 0.613 0.168
## PUNTO_26 0.168 0.079 0.094 0.123 0.471 0.564 0.168
## PUNTO_4 0.564 0.564 0.531 0.471 0.889 0.939 0.818
## PUNTO_5 0.151 0.136 0.136 0.120 0.364 0.168 0.120
## PUNTO_6 0.079 0.079 0.079 0.079 0.245 0.151 0.079
## PUNTO_7 0.161 0.136 0.122 0.114 0.983 0.360 0.097
## PUNTO_8 0.097 0.079 0.079 0.079 0.226 0.136 0.079
## PUNTO_9 0.261 0.190 0.168 0.136 0.804 0.310 0.115
## PUNTO_23 PUNTO_24 PUNTO_25 PUNTO_26 PUNTO_4 PUNTO_5 PUNTO_6 PUNTO_7
## PUNTO_16 - - - - - - - -
## PUNTO_17 - - - - - - - -
## PUNTO_18 - - - - - - - -
## PUNTO_19 - - - - - - - -
## PUNTO_20 - - - - - - - -
## PUNTO_21 - - - - - - - -
## PUNTO_23 - - - - - - - -
## PUNTO_24 0.845 - - - - - - -
## PUNTO_25 0.904 1.000 - - - - - -
## PUNTO_26 0.826 0.695 0.471 - - - - -
## PUNTO_4 0.904 0.907 0.939 0.904 - - - -
## PUNTO_5 0.319 0.223 0.515 0.559 0.257 - - -
## PUNTO_6 0.310 0.236 0.382 0.564 0.310 0.714 - -
## PUNTO_7 0.907 0.725 0.983 0.904 0.564 0.214 0.177 -
## PUNTO_8 0.236 0.252 0.323 0.564 0.310 0.564 0.907 0.183
## PUNTO_9 0.691 0.419 0.845 0.983 0.508 0.161 0.136 0.904
## PUNTO_8
## PUNTO_16 -
## PUNTO_17 -
## PUNTO_18 -
## PUNTO_19 -
## PUNTO_20 -
## PUNTO_21 -
## PUNTO_23 -
## PUNTO_24 -
## PUNTO_25 -
## PUNTO_26 -
## PUNTO_4 -
## PUNTO_5 -
## PUNTO_6 -
## PUNTO_7 -
## PUNTO_8 -
## PUNTO_9 0.202
##
## P value adjustment method: BH
El ajuste BH controla el problema de realizar numerosas
comparaciones.
\[ IC_{95\%} = \bar x \pm t_{\alpha/2,n-1} \frac{ s }{ \sqrt n } \]
donde:
IC_medias_qc <- do.call(
rbind,
lapply(
split(
datos$log10_nmp,
datos$punto
),
function(x) {
x <- x[
is.finite(x)
]
n <- length(x)
if (
n >= 2
) {
media <- mean(x)
s <- sd(x)
error <- qt(
0.975,
df = n - 1
) *
s /
sqrt(n)
data.frame(
N = n,
Media = media,
IC_inf = media - error,
IC_sup = media + error
)
} else {
data.frame(
N = n,
Media = NA_real_,
IC_inf = NA_real_,
IC_sup = NA_real_
)
}
}
)
)
IC_medias_qc$Punto <- rownames(
IC_medias_qc
)
rownames(
IC_medias_qc
) <- NULL
IC_medias_qc <- IC_medias_qc[
,
c(
"Punto",
"N",
"Media",
"IC_inf",
"IC_sup"
)
]
knitr::kable(
IC_medias_qc,
digits = 3,
caption = "Intervalo de confianza del 95% para la media por punto"
)
| Punto | N | Media | IC_inf | IC_sup |
|---|---|---|---|---|
| PUNTO_11 | 21 | 2.872 | 2.573 | 3.171 |
| PUNTO_16 | 21 | 2.697 | 2.326 | 3.068 |
| PUNTO_17 | 21 | 2.662 | 2.344 | 2.980 |
| PUNTO_18 | 21 | 2.649 | 2.091 | 3.207 |
| PUNTO_19 | 22 | 4.115 | 3.442 | 4.788 |
| PUNTO_20 | 21 | 3.504 | 2.743 | 4.265 |
| PUNTO_21 | 21 | 2.947 | 2.398 | 3.497 |
| PUNTO_23 | 22 | 4.095 | 3.432 | 4.758 |
| PUNTO_24 | 7 | 3.859 | 1.837 | 5.880 |
| PUNTO_25 | 11 | 4.054 | 3.037 | 5.070 |
| PUNTO_26 | 11 | 4.219 | 3.207 | 5.231 |
| PUNTO_4 | 6 | 3.434 | 0.990 | 5.878 |
| PUNTO_5 | 19 | 5.001 | 3.765 | 6.238 |
| PUNTO_6 | 20 | 4.764 | 3.837 | 5.692 |
| PUNTO_7 | 21 | 4.199 | 3.345 | 5.053 |
| PUNTO_8 | 20 | 4.766 | 3.821 | 5.710 |
| PUNTO_9 | 22 | 4.265 | 3.360 | 5.169 |
datos_ic_qc <- IC_medias_qc[
complete.cases(
IC_medias_qc[
,
c(
"Media",
"IC_inf",
"IC_sup"
)
]
),
]
datos_ic_qc <- datos_ic_qc[
order(
datos_ic_qc$Media
),
]
x_ic_qc <- seq_len(
nrow(
datos_ic_qc
)
)
plot(
x_ic_qc,
datos_ic_qc$Media,
pch = 19,
cex = 1.1,
col = "#1596a5",
xaxt = "n",
xlab = "Punto de muestreo",
ylab = expression(
"Media de " *
log[10] *
"(NMP/100 mL)"
),
main = "Media e IC 95% por punto",
ylim = range(
c(
datos_ic_qc$IC_inf,
datos_ic_qc$IC_sup
),
na.rm = TRUE
)
)
arrows(
x0 = x_ic_qc,
y0 = datos_ic_qc$IC_inf,
x1 = x_ic_qc,
y1 = datos_ic_qc$IC_sup,
angle = 90,
code = 3,
length = 0.05,
col = "#083d5c"
)
axis(
1,
at = x_ic_qc,
labels = datos_ic_qc$Punto,
las = 2,
cex.axis = 0.70
)
\[ \eta^2 = \frac{ SS_B }{ SS_T } \]
donde:
tabla_anova_qc <- summary(
modelo_anova_qc
)[[1]]
SS_entre_qc <- tabla_anova_qc[
"punto",
"Sum Sq"
]
SS_total_qc <- sum(
tabla_anova_qc[
,
"Sum Sq"
]
)
eta2_qc <- SS_entre_qc /
SS_total_qc
eta2_qc
## [1] 0.2020074
Una orientación descriptiva:
| \(\eta^2\) | Magnitud aproximada |
|---|---|
| 0.01 | Pequeña |
| 0.06 | Moderada |
| 0.14 o superior | Grande |
Eta cuadrado responde:
¿Qué proporción de la variabilidad microbiológica observada está asociada con diferencias entre puntos?
Un valor elevado significa que la ubicación está asociada con una parte importante de la variabilidad observada.
No implica causalidad.
Una observación atípica estadísticamente no debe eliminarse automáticamente.
Debe revisarse:
Atípico estadístico no significa dato incorrecto.
En un sistema microbiológico puede representar precisamente un evento importante.
No debemos concluir únicamente a partir de:
\[ p<0.05 \]
La interpretación debe integrar:
\[ \boxed{ \text{Boxplot} + N + \bar X + \text{Mediana} + IQR } \]
\[ + \]
\[ \boxed{ \text{Normalidad} + \text{Varianzas} } \]
\[ + \]
\[ \boxed{ \text{ANOVA / Welch / Kruskal} } \]
\[ + \]
\[ \boxed{ \text{Post hoc} + IC + \text{Tamaño del efecto} } \]
Una conclusión puede redactarse:
Los puntos de muestreo presentaron diferentes perfiles microbiológicos. Algunos mostraron resultados centrales superiores, mientras otros presentaron una mayor dispersión o episodios extremos. La prueba estadística permitió establecer si las diferencias observadas fueron compatibles o no con la variabilidad muestral. La interpretación debe complementarse con las condiciones propias de cada ubicación y con su comportamiento temporal.
\[ \boxed{\text{Resultado microbiológico + Punto}} \]
\[ \Downarrow \]
\[ \boxed{\text{Resumen por punto}} \]
\[ \Downarrow \]
\[ \boxed{\text{Boxplot + observaciones}} \]
\[ \Downarrow \]
\[ \boxed{\text{Normalidad + varianzas}} \]
\[ \Downarrow \]
\[ \begin{cases} \text{Welch / ANOVA}\\ \text{Wilcoxon / Kruskal-Wallis} \end{cases} \]
\[ \Downarrow \]
\[ \boxed{\text{Post hoc}} \]
\[ \Downarrow \]
\[ \boxed{\text{IC + tamaño del efecto}} \]
\[ \Downarrow \]
\[ \boxed{\text{Interpretación microbiológica}} \]
Finalmente estudiaremos una relación entre dos variables numéricas.
En nuestra base:
\[ X= \text{Tiempo transcurrido} \]
y:
\[ Y= \log_{10}(\text{NMP/100 mL}) \]
El análisis se realizará independientemente para cada punto.
La pregunta será:
¿Los resultados microbiológicos de cada punto presentan una tendencia a través del tiempo?
Definimos:
\[ X_i = \text{Fecha}_i - \text{Fecha inicial} \]
expresada en días.
Por tanto:
\[ X_i=20 \]
significa que la observación ocurrió 20 días después de la primera fecha de referencia.
La respuesta es:
\[ Y_i = \log_{10} (\text{NMP}_i) \]
La transformación facilita el análisis de datos que abarcan varios órdenes de magnitud.
Por ejemplo:
\[ 10^2=100 \]
\[ 10^4=10\,000 \]
\[ 10^6=1\,000\,000 \]
Cada observación se representa como:
\[ (x_i,y_i) \]
donde:
conteo_qq <- table(
datos$punto
)
puntos_qq <- names(
conteo_qq[
conteo_qq >= 5
]
)
punto_ejemplo_qq <- puntos_qq[
1
]
ejemplo_5_qq <- datos[
datos$punto == punto_ejemplo_qq &
is.finite(
datos$dia_estudio
) &
is.finite(
datos$log10_nmp
),
c(
"punto",
"fecha",
"dia_estudio",
"resultado_nmp",
"log10_nmp"
)
]
ejemplo_5_qq <- ejemplo_5_qq[
order(
ejemplo_5_qq$fecha
),
]
ejemplo_5_qq <- head(
ejemplo_5_qq,
5
)
knitr::kable(
ejemplo_5_qq,
digits = 3,
caption = paste(
"Cinco observaciones reales del punto",
punto_ejemplo_qq
)
)
| punto | fecha | dia_estudio | resultado_nmp | log10_nmp |
|---|---|---|---|---|
| PUNTO_11 | 2026-01-29 | 0 | 8164.0 | 3.912 |
| PUNTO_11 | 2026-02-10 | 12 | 3448.0 | 3.538 |
| PUNTO_11 | 2026-03-03 | 33 | 8664.0 | 3.938 |
| PUNTO_11 | 2026-03-03 | 33 | 866.4 | 2.938 |
| PUNTO_11 | 2026-03-24 | 54 | 4352.0 | 3.639 |
El diagrama de dispersión debe ser el primer recurso para estudiar dos variables cuantitativas.
plot(
ejemplo_5_qq$dia_estudio,
ejemplo_5_qq$log10_nmp,
pch = 19,
cex = 1.2,
col = "#1596a5",
xlab = "Días transcurridos",
ylab = expression(
log[10]*"(NMP/100 mL)"
),
main = paste(
"Diagrama de dispersión -",
punto_ejemplo_qq
)
)
text(
ejemplo_5_qq$dia_estudio,
ejemplo_5_qq$log10_nmp,
labels = seq_len(
nrow(
ejemplo_5_qq
)
),
pos = 3,
cex = 0.8,
col = "#083d5c"
)
Debemos revisar:
Una tendencia ascendente puede indicar que los resultados microbiológicos aumentan conforme transcurre el periodo.
Una tendencia descendente puede indicar disminución.
Una nube sin una dirección clara indica ausencia de una tendencia lineal evidente.
\[ \bar{x} = \frac{1}{n} \sum_{i=1}^{n} x_i \]
\[ \bar{y} = \frac{1}{n} \sum_{i=1}^{n} y_i \]
x5_qq <- ejemplo_5_qq$dia_estudio
y5_qq <- ejemplo_5_qq$log10_nmp
media_x_qq <- mean(
x5_qq
)
media_y_qq <- mean(
y5_qq
)
media_x_qq
## [1] 26.4
media_y_qq
## [1] 3.592719
La covarianza es:
\[ s_{XY} = \frac{ \sum_{i=1}^{n} (x_i-\bar{x}) (y_i-\bar{y}) }{ n-1 } \]
donde:
tabla_covarianza_qq <- data.frame(
X = x5_qq,
Y = y5_qq,
X_menos_media =
x5_qq - media_x_qq,
Y_menos_media =
y5_qq - media_y_qq,
Producto =
(
x5_qq - media_x_qq
) *
(
y5_qq - media_y_qq
)
)
knitr::kable(
tabla_covarianza_qq,
digits = 4,
caption = "Cálculo de la covarianza"
)
| X | Y | X_menos_media | Y_menos_media | Producto |
|---|---|---|---|---|
| 0 | 3.9119 | -26.4 | 0.3192 | -8.4265 |
| 12 | 3.5376 | -14.4 | -0.0552 | 0.7942 |
| 33 | 3.9377 | 6.6 | 0.3450 | 2.2770 |
| 33 | 2.9377 | 6.6 | -0.6550 | -4.3230 |
| 54 | 3.6387 | 27.6 | 0.0460 | 1.2688 |
suma_productos_qq <- sum(
(
x5_qq - media_x_qq
) *
(
y5_qq - media_y_qq
)
)
cov_manual_qq <- suma_productos_qq /
(
length(
x5_qq
) - 1
)
cov_manual_qq
## [1] -2.102378
cov(
x5_qq,
y5_qq
)
## [1] -2.102378
Si:
\[ s_{XY}>0 \]
las variables tienden a moverse en el mismo sentido.
Si:
\[ s_{XY}<0 \]
tienden a moverse en sentidos contrarios.
Su magnitud depende de las unidades, por lo que se utiliza principalmente como paso conceptual hacia la correlación.
Pearson mide la dirección y fuerza de una relación lineal.
\[ r = \frac{ s_{XY} }{ s_Xs_Y } \]
o:
\[ r = \frac{ \sum (x_i-\bar{x}) (y_i-\bar{y}) }{ \sqrt{ \sum (x_i-\bar{x})^2 } \sqrt{ \sum (y_i-\bar{y})^2 } } \]
donde:
\[ -1 \leq r \leq 1 \]
Si:
\[ r>0 \]
la relación es positiva.
Si:
\[ r<0 \]
es negativa.
Si:
\[ r\approx0 \]
la asociación lineal es débil.
| \(|r|\) | Interpretación |
|---|---|
| 0.00–0.19 | Muy débil |
| 0.20–0.39 | Débil |
| 0.40–0.59 | Moderada |
| 0.60–0.79 | Fuerte |
| 0.80–1.00 | Muy fuerte |
sx_qq <- sd(
x5_qq
)
sy_qq <- sd(
y5_qq
)
r_manual_qq <- cov_manual_qq /
(
sx_qq *
sy_qq
)
r_manual_qq
## [1] -0.2481456
cor(
x5_qq,
y5_qq,
method = "pearson"
)
## [1] -0.2481456
Queremos evaluar:
\[ H_0: \rho=0 \]
frente a:
\[ H_1: \rho\neq0 \]
El estadístico es:
\[ t = r \sqrt{ \frac{ n-2 }{ 1-r^2 } } \]
con:
\[ gl=n-2 \]
prueba_pearson_qq <- cor.test(
x5_qq,
y5_qq,
method = "pearson"
)
prueba_pearson_qq
##
## Pearson's product-moment correlation
##
## data: x5_qq and y5_qq
## t = -0.44368, df = 3, p-value = 0.6873
## alternative hypothesis: true correlation is not equal to 0
## 95 percent confidence interval:
## -0.9273802 0.8118623
## sample estimates:
## cor
## -0.2481456
prueba_pearson_qq$conf.int
## [1] -0.9273802 0.8118623
## attr(,"conf.level")
## [1] 0.95
Si el intervalo incluye:
\[ 0 \]
la muestra es compatible con ausencia de correlación lineal poblacional.
Una correlación positiva significa que dentro del punto los resultados tienden a aumentar conforme avanza el tiempo.
Una correlación negativa indica tendencia contraria.
Una correlación pequeña indica poca relación lineal temporal.
Cuando existe:
puede utilizarse Spearman.
Cuando no existen empates:
\[ \rho_s = 1- \frac{ 6\sum d_i^2 }{ n(n^2-1) } \]
donde:
cor.test(
x5_qq,
y5_qq,
method = "spearman",
exact = FALSE
)
##
## Spearman's rank correlation rho
##
## data: x5_qq and y5_qq
## S = 22.052, p-value = 0.8696
## alternative hypothesis: true rho is not equal to 0
## sample estimates:
## rho
## -0.1025978
| Situación | Método |
|---|---|
| Relación aproximadamente lineal | Pearson |
| Ausencia de observaciones extremadamente influyentes | Pearson |
| Fuerte asimetría | Spearman |
| Valores extremos | Spearman |
| Relación monotónica no lineal | Spearman |
El modelo es:
\[ Y_i = \beta_0 + \beta_1X_i + \varepsilon_i \]
donde:
\[ \beta_0 \]
representa la respuesta promedio esperada cuando:
\[ X=0 \]
En este análisis corresponde al inicio del periodo utilizado como referencia.
\[ \beta_1 \]
representa el cambio promedio esperado en:
\[ \log_{10}(\text{NMP/100 mL}) \]
por cada día adicional.
Si:
\[ \beta_1>0 \]
la tendencia es creciente.
Si:
\[ \beta_1<0 \]
la tendencia es decreciente.
La recta estimada es:
\[ \hat Y = b_0+ b_1X \]
Los coeficientes minimizan:
\[ SSE = \sum_{i=1}^{n} (y_i-\hat y_i)^2 \]
donde:
\[ b_1 = \frac{ \sum (x_i-\bar{x}) (y_i-\bar{y}) }{ \sum (x_i-\bar{x})^2 } \]
\[ b_0 = \bar y- b_1\bar x \]
numerador_b1_qq <- sum(
(
x5_qq -
media_x_qq
) *
(
y5_qq -
media_y_qq
)
)
denominador_b1_qq <- sum(
(
x5_qq -
media_x_qq
)^2
)
b1_qq <- numerador_b1_qq /
denominador_b1_qq
b0_qq <- media_y_qq -
b1_qq *
media_x_qq
b0_qq
## [1] 3.719351
b1_qq
## [1] -0.004796664
modelo_qq <- lm(
log10_nmp ~ dia_estudio,
data = ejemplo_5_qq
)
summary(
modelo_qq
)
##
## Call:
## lm(formula = log10_nmp ~ dia_estudio, data = ejemplo_5_qq)
##
## Residuals:
## 1 2 3 4 5
## 0.1926 -0.1242 0.3767 -0.6233 0.1784
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 3.719351 0.349920 10.629 0.00178 **
## dia_estudio -0.004797 0.010811 -0.444 0.68732
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.4527 on 3 degrees of freedom
## Multiple R-squared: 0.06158, Adjusted R-squared: -0.2512
## F-statistic: 0.1969 on 1 and 3 DF, p-value: 0.6873
plot(
ejemplo_5_qq$dia_estudio,
ejemplo_5_qq$log10_nmp,
pch = 19,
cex = 1.2,
col = "#1596a5",
xlab = "Días transcurridos",
ylab = expression(
log[10]*"(NMP/100 mL)"
),
main = paste(
"Modelo lineal -",
punto_ejemplo_qq
)
)
abline(
modelo_qq,
col = "#083d5c",
lwd = 2
)
Para cada observación:
\[ \hat y_i = b_0+ b_1x_i \]
El residuo es:
\[ e_i = y_i- \hat y_i \]
tabla_residuos_qq <- data.frame(
Dia =
ejemplo_5_qq$dia_estudio,
Observado =
ejemplo_5_qq$log10_nmp,
Ajustado =
fitted(
modelo_qq
),
Residuo =
residuals(
modelo_qq
)
)
knitr::kable(
tabla_residuos_qq,
digits = 4,
caption = "Valores observados, ajustados y residuos"
)
| Dia | Observado | Ajustado | Residuo |
|---|---|---|---|
| 0 | 3.9119 | 3.7194 | 0.1926 |
| 12 | 3.5376 | 3.6618 | -0.1242 |
| 33 | 3.9377 | 3.5611 | 0.3767 |
| 33 | 2.9377 | 3.5611 | -0.6233 |
| 54 | 3.6387 | 3.4603 | 0.1784 |
El modelo clásico considera:
Linealidad \[ E(Y|X) = \beta_0+ \beta_1X \]
Media del error \[ E(\varepsilon_i)=0 \]
Homocedasticidad \[ Var( \varepsilon_i|X_i ) = \sigma^2 \]
Normalidad \[ \varepsilon_i \sim N( 0, \sigma^2 ) \]
Independencia \[ Cov( \varepsilon_i, \varepsilon_j ) = 0 \]
para:
\[ i\neq j \]
En regresión, el supuesto de normalidad se refiere fundamentalmente a:
\[ \boxed{\text{los residuos}} \]
y no exige necesariamente que la variable microbiológica original presente distribución normal.
par(
mfrow = c(
1,
2
)
)
plot(
fitted(
modelo_qq
),
residuals(
modelo_qq
),
pch = 19,
col = "#1596a5",
xlab = "Valores ajustados",
ylab = "Residuos",
main = "Residuos vs. ajustados"
)
abline(
h = 0,
col = "#083d5c",
lty = 2,
lwd = 2
)
qqnorm(
residuals(
modelo_qq
),
pch = 19,
col = "#1596a5",
main = "Q-Q de residuos"
)
qqline(
residuals(
modelo_qq
),
col = "#083d5c",
lwd = 2
)
par(
mfrow = c(
1,
1
)
)
En residuos versus ajustados buscamos:
En Q-Q buscamos una aproximación razonable a la línea.
Las hipótesis son:
\[ H_0: \beta_1=0 \]
\[ H_1: \beta_1\neq0 \]
El estadístico es:
\[ t = \frac{ b_1 }{ SE(b_1) } \]
con:
\[ gl=n-2 \]
\[ s = \sqrt{ \frac{ SSE }{ n-2 } } \]
summary(
modelo_qq
)$sigma
## [1] 0.4526758
\[ SE(b_1) = \frac{ s }{ \sqrt{ \sum (x_i-\bar{x})^2 } } \]
coef(
summary(
modelo_qq
)
)
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 3.719351128 0.34992042 10.6291343 0.001779544
## dia_estudio -0.004796664 0.01081114 -0.4436778 0.687324309
\[ IC_{95\%} (\beta_1) = b_1 \pm t_{\alpha/2,n-2} SE(b_1) \]
confint(
modelo_qq,
level = 0.95
)
## 2.5 % 97.5 %
## (Intercept) 2.60574819 4.83295407
## dia_estudio -0.03920254 0.02960921
Si el intervalo incluye:
\[ 0 \]
los datos son compatibles con ausencia de tendencia lineal.
\[ R^2 = 1- \frac{ SSE }{ SST } \]
donde:
\[ SSE = \sum (y_i-\hat y_i)^2 \]
y:
\[ SST = \sum (y_i-\bar y)^2 \]
summary(
modelo_qq
)$r.squared
## [1] 0.06157625
En regresión lineal simple con intercepto:
\[ R^2 = r^2 \]
Pearson conserva la dirección mediante el signo.
\(R^2\) expresa proporción de variabilidad explicada y no tiene signo.
Un \(R^2\) elevado indica que la tendencia temporal lineal representa una parte importante de la variabilidad observada en el punto.
Un \(R^2\) pequeño indica que el tiempo explica poco y que otros factores pueden estar relacionados con el comportamiento microbiológico.
Para:
\[ X=x_0 \]
la respuesta estimada es:
\[ \hat Y_0 = b_0+ b_1x_0 \]
El error estándar de la media estimada es:
\[ SE( \hat Y_0 ) = s \sqrt{ \frac{1}{n} + \frac{ (x_0-\bar{x})^2 }{ \sum (x_i-\bar{x})^2 } } \]
nuevo_dia_qq <- data.frame(
dia_estudio = median(
ejemplo_5_qq$dia_estudio
)
)
IC_media_qq <- predict(
modelo_qq,
newdata = nuevo_dia_qq,
interval = "confidence",
level = 0.95
)
IC_media_qq
## fit lwr upr
## 1 3.561061 2.877951 4.244172
Este intervalo responde:
¿Dónde se encuentra el promedio esperado?
Para una nueva observación individual:
IP_qq <- predict(
modelo_qq,
newdata = nuevo_dia_qq,
interval = "prediction",
level = 0.95
)
IP_qq
## fit lwr upr
## 1 3.561061 1.966691 5.155431
El intervalo de predicción responde:
¿Dónde podría encontrarse una nueva muestra individual?
\[ \boxed{ \text{IC de la media} } \]
estima incertidumbre sobre el promedio.
\[ \boxed{ \text{Intervalo de predicción} } \]
incluye además la variabilidad de una nueva observación.
Por eso:
\[ IP_{95\%} \]
normalmente es más amplio.
secuencia_qq <- data.frame(
dia_estudio = seq(
min(
ejemplo_5_qq$dia_estudio
),
max(
ejemplo_5_qq$dia_estudio
),
length.out = 100
)
)
banda_ic_qq <- predict(
modelo_qq,
newdata = secuencia_qq,
interval = "confidence",
level = 0.95
)
banda_ip_qq <- predict(
modelo_qq,
newdata = secuencia_qq,
interval = "prediction",
level = 0.95
)
plot(
ejemplo_5_qq$dia_estudio,
ejemplo_5_qq$log10_nmp,
pch = 19,
col = "#1596a5",
xlab = "Días transcurridos",
ylab = expression(
log[10]*"(NMP/100 mL)"
),
main = paste(
"Regresión e intervalos -",
punto_ejemplo_qq
)
)
lines(
secuencia_qq$dia_estudio,
banda_ic_qq[
,
"fit"
],
col = "#083d5c",
lwd = 2
)
lines(
secuencia_qq$dia_estudio,
banda_ic_qq[
,
"lwr"
],
col = "#2c9f80",
lty = 2,
lwd = 1.5
)
lines(
secuencia_qq$dia_estudio,
banda_ic_qq[
,
"upr"
],
col = "#2c9f80",
lty = 2,
lwd = 1.5
)
lines(
secuencia_qq$dia_estudio,
banda_ip_qq[
,
"lwr"
],
col = "#315f66",
lty = 3,
lwd = 1.2
)
lines(
secuencia_qq$dia_estudio,
banda_ip_qq[
,
"upr"
],
col = "#315f66",
lty = 3,
lwd = 1.2
)
legend(
"topright",
legend = c(
"Recta estimada",
"IC 95% de la media",
"IP 95% individual"
),
col = c(
"#083d5c",
"#2c9f80",
"#315f66"
),
lty = c(
1,
2,
3
),
lwd = c(
2,
1.5,
1.2
),
bty = "n",
cex = 0.75
)
Como:
\[ Y = \log_{10}(\text{NMP}) \]
entonces:
\[ \text{NMP} = 10^Y \]
Por ejemplo:
10^(
IC_media_qq
)
## fit lwr upr
## 1 3639.663 755.0066 17545.74
La retrotransformación de la media en escala logarítmica no coincide necesariamente con la media aritmética en la escala original.
La predicción es más confiable dentro del rango observado:
\[ X_{\min} \leq x_0 \leq X_{\max} \]
Predecir fuera de este intervalo se denomina:
\[ \boxed{\text{extrapolación}} \]
y debe interpretarse con mucha cautela.
Ahora repetiremos:
\[ \text{Tiempo} \quad \text{vs.} \quad \log_{10}(\text{NMP}) \]
independientemente para cada punto.
par_original_disp <- par(
no.readonly = TRUE
)
par(
mfrow = c(5,4),
mar = c(3.5,3.5,2.8,1),
oma = c(5,4.5,4.5,1)
)
for (
p in puntos_mod3
) {
dp <- datos[
datos$punto == p &
is.finite(
datos$dia_estudio
) &
is.finite(
datos$log10_nmp
),
]
if (
nrow(dp) > 0
) {
plot(
dp$dia_estudio,
dp$log10_nmp,
pch = 19,
cex = 0.7,
col = "#1596a5",
main = p,
xlab = "",
ylab = "",
cex.main = 0.86,
cex.axis = 0.67
)
if (
nrow(dp) >= 3 &&
length(
unique(
dp$dia_estudio
)
) >= 2
) {
mod_p <- lm(
log10_nmp ~ dia_estudio,
data = dp
)
abline(
mod_p,
col = "#083d5c",
lwd = 2
)
}
}
}
mtext(
"Días transcurridos",
side = 1,
outer = TRUE,
line = 2.8
)
mtext(
expression(
log[10]*"(NMP/100 mL)"
),
side = 2,
outer = TRUE,
line = 2.8
)
mtext(
"Relación temporal por punto",
side = 3,
outer = TRUE,
line = 1.7,
font = 2
)
mtext(
"Línea continua: tendencia lineal estimada",
side = 3,
outer = TRUE,
line = 0.3,
cex = 0.75
)
par(
par_original_disp
)
correlaciones_puntos_qq <- do.call(
rbind,
lapply(
split(
datos,
datos$punto
),
function(df) {
df <- df[
is.finite(
df$dia_estudio
) &
is.finite(
df$log10_nmp
),
]
if (
nrow(df) >= 4 &&
length(
unique(
df$dia_estudio
)
) >= 2 &&
length(
unique(
df$log10_nmp
)
) >= 2
) {
pearson <- cor.test(
df$dia_estudio,
df$log10_nmp,
method = "pearson"
)
spearman <- suppressWarnings(
cor.test(
df$dia_estudio,
df$log10_nmp,
method = "spearman",
exact = FALSE
)
)
data.frame(
N = nrow(df),
Pearson_r =
unname(
pearson$estimate
),
Pearson_IC_inf =
pearson$conf.int[1],
Pearson_IC_sup =
pearson$conf.int[2],
Pearson_p =
pearson$p.value,
Spearman_rho =
unname(
spearman$estimate
),
Spearman_p =
spearman$p.value
)
} else {
data.frame(
N = nrow(df),
Pearson_r = NA_real_,
Pearson_IC_inf = NA_real_,
Pearson_IC_sup = NA_real_,
Pearson_p = NA_real_,
Spearman_rho = NA_real_,
Spearman_p = NA_real_
)
}
}
)
)
correlaciones_puntos_qq$Punto <- rownames(
correlaciones_puntos_qq
)
rownames(
correlaciones_puntos_qq
) <- NULL
correlaciones_puntos_qq <- correlaciones_puntos_qq[
,
c(
"Punto",
"N",
"Pearson_r",
"Pearson_IC_inf",
"Pearson_IC_sup",
"Pearson_p",
"Spearman_rho",
"Spearman_p"
)
]
knitr::kable(
correlaciones_puntos_qq,
digits = 4,
caption = "Correlaciones temporales por punto"
)
| Punto | N | Pearson_r | Pearson_IC_inf | Pearson_IC_sup | Pearson_p | Spearman_rho | Spearman_p |
|---|---|---|---|---|---|---|---|
| PUNTO_11 | 21 | -0.5432 | -0.7897 | -0.1457 | 0.0109 | -0.5327 | 0.0129 |
| PUNTO_16 | 21 | -0.1616 | -0.5546 | 0.2903 | 0.4839 | -0.1612 | 0.4850 |
| PUNTO_17 | 21 | -0.4929 | -0.7623 | -0.0777 | 0.0232 | -0.4670 | 0.0328 |
| PUNTO_18 | 21 | -0.3851 | -0.7004 | 0.0558 | 0.0847 | -0.4420 | 0.0448 |
| PUNTO_19 | 22 | 0.0184 | -0.4063 | 0.4367 | 0.9351 | 0.1552 | 0.4904 |
| PUNTO_20 | 21 | 0.2959 | -0.1557 | 0.6452 | 0.1928 | 0.2768 | 0.2245 |
| PUNTO_21 | 21 | 0.0058 | -0.4270 | 0.4364 | 0.9801 | 0.0551 | 0.8125 |
| PUNTO_23 | 22 | -0.0092 | -0.4291 | 0.4140 | 0.9677 | 0.1399 | 0.5346 |
| PUNTO_24 | 7 | -0.2782 | -0.8526 | 0.6007 | 0.5458 | -0.2386 | 0.6063 |
| PUNTO_25 | 11 | 0.0316 | -0.5792 | 0.6198 | 0.9264 | 0.2402 | 0.4768 |
| PUNTO_26 | 11 | 0.0384 | -0.5747 | 0.6239 | 0.9107 | 0.2575 | 0.4446 |
| PUNTO_4 | 6 | -0.8146 | -0.9790 | -0.0089 | 0.0484 | -0.8533 | 0.0307 |
| PUNTO_5 | 19 | -0.1420 | -0.5601 | 0.3337 | 0.5620 | -0.0053 | 0.9829 |
| PUNTO_6 | 20 | -0.0811 | -0.5055 | 0.3749 | 0.7340 | 0.0075 | 0.9748 |
| PUNTO_7 | 21 | -0.1032 | -0.5121 | 0.3438 | 0.6561 | 0.0961 | 0.6787 |
| PUNTO_8 | 20 | -0.2752 | -0.6398 | 0.1905 | 0.2402 | -0.2413 | 0.3054 |
| PUNTO_9 | 22 | -0.1713 | -0.5530 | 0.2698 | 0.4458 | -0.0696 | 0.7582 |
regresiones_puntos_qq <- do.call(
rbind,
lapply(
split(
datos,
datos$punto
),
function(df) {
df <- df[
is.finite(
df$dia_estudio
) &
is.finite(
df$log10_nmp
),
]
if (
nrow(df) >= 4 &&
length(
unique(
df$dia_estudio
)
) >= 2
) {
mod <- lm(
log10_nmp ~ dia_estudio,
data = df
)
sm <- summary(
mod
)
ic <- confint(
mod,
"dia_estudio",
level = 0.95
)
data.frame(
N = nrow(df),
Intercepto =
coef(mod)[1],
Pendiente =
coef(mod)[2],
IC95_inf =
ic[1],
IC95_sup =
ic[2],
p_pendiente =
coef(sm)[
"dia_estudio",
"Pr(>|t|)"
],
R2 =
sm$r.squared,
R2_ajustado =
sm$adj.r.squared,
Error_residual =
sm$sigma
)
} else {
data.frame(
N = nrow(df),
Intercepto = NA_real_,
Pendiente = NA_real_,
IC95_inf = NA_real_,
IC95_sup = NA_real_,
p_pendiente = NA_real_,
R2 = NA_real_,
R2_ajustado = NA_real_,
Error_residual = NA_real_
)
}
}
)
)
regresiones_puntos_qq$Punto <- rownames(
regresiones_puntos_qq
)
rownames(
regresiones_puntos_qq
) <- NULL
regresiones_puntos_qq <- regresiones_puntos_qq[
,
c(
"Punto",
"N",
"Intercepto",
"Pendiente",
"IC95_inf",
"IC95_sup",
"p_pendiente",
"R2",
"R2_ajustado",
"Error_residual"
)
]
knitr::kable(
regresiones_puntos_qq,
digits = 4,
caption = "Modelos temporales por punto"
)
| Punto | N | Intercepto | Pendiente | IC95_inf | IC95_sup | p_pendiente | R2 | R2_ajustado | Error_residual |
|---|---|---|---|---|---|---|---|---|---|
| PUNTO_11 | 21 | 3.5094 | -0.0065 | -0.0113 | -0.0017 | 0.0109 | 0.2951 | 0.2580 | 0.5657 |
| PUNTO_16 | 21 | 2.9326 | -0.0024 | -0.0094 | 0.0046 | 0.4839 | 0.0261 | -0.0251 | 0.8260 |
| PUNTO_17 | 21 | 3.2772 | -0.0063 | -0.0116 | -0.0010 | 0.0232 | 0.2429 | 0.2031 | 0.6238 |
| PUNTO_18 | 21 | 3.4924 | -0.0086 | -0.0185 | 0.0013 | 0.0847 | 0.1483 | 0.1035 | 1.1606 |
| PUNTO_19 | 22 | 4.0674 | 0.0005 | -0.0122 | 0.0132 | 0.9351 | 0.0003 | -0.0496 | 1.5551 |
| PUNTO_20 | 21 | 2.6201 | 0.0090 | -0.0049 | 0.0229 | 0.1928 | 0.0876 | 0.0395 | 1.6383 |
| PUNTO_21 | 21 | 2.9350 | 0.0001 | -0.0104 | 0.0107 | 0.9801 | 0.0000 | -0.0526 | 1.2388 |
| PUNTO_23 | 22 | 4.1184 | -0.0002 | -0.0127 | 0.0122 | 0.9677 | 0.0001 | -0.0499 | 1.5317 |
| PUNTO_24 | 7 | 6.8535 | -0.0201 | -0.0999 | 0.0597 | 0.5458 | 0.0774 | -0.1071 | 2.3001 |
| PUNTO_25 | 11 | 3.7931 | 0.0018 | -0.0415 | 0.0452 | 0.9264 | 0.0010 | -0.1100 | 1.5940 |
| PUNTO_26 | 11 | 3.9041 | 0.0022 | -0.0410 | 0.0454 | 0.9107 | 0.0015 | -0.1095 | 1.5868 |
| PUNTO_4 | 6 | 6.1470 | -0.0512 | -0.1018 | -0.0006 | 0.0484 | 0.6635 | 0.5794 | 1.5104 |
| PUNTO_5 | 19 | 5.7209 | -0.0068 | -0.0312 | 0.0176 | 0.5620 | 0.0202 | -0.0375 | 2.6132 |
| PUNTO_6 | 20 | 5.0648 | -0.0030 | -0.0210 | 0.0150 | 0.7340 | 0.0066 | -0.0486 | 2.0286 |
| PUNTO_7 | 21 | 4.5412 | -0.0035 | -0.0196 | 0.0127 | 0.6561 | 0.0107 | -0.0414 | 1.9137 |
| PUNTO_8 | 20 | 5.7914 | -0.0101 | -0.0277 | 0.0074 | 0.2402 | 0.0758 | 0.0244 | 1.9924 |
| PUNTO_9 | 22 | 4.8593 | -0.0063 | -0.0230 | 0.0105 | 0.4458 | 0.0294 | -0.0192 | 2.0595 |
normalidad_residuos_qq <- do.call(
rbind,
lapply(
split(
datos,
datos$punto
),
function(df) {
df <- df[
is.finite(
df$dia_estudio
) &
is.finite(
df$log10_nmp
),
]
if (
nrow(df) >= 4 &&
length(
unique(
df$dia_estudio
)
) >= 2
) {
mod <- lm(
log10_nmp ~ dia_estudio,
data = df
)
res <- residuals(
mod
)
if (
length(res) >= 3 &&
length(
unique(
res
)
) > 1
) {
sh <- shapiro.test(
res
)
data.frame(
N = length(res),
W =
unname(
sh$statistic
),
p_value =
sh$p.value
)
} else {
data.frame(
N = length(res),
W = NA_real_,
p_value = NA_real_
)
}
} else {
data.frame(
N = nrow(df),
W = NA_real_,
p_value = NA_real_
)
}
}
)
)
normalidad_residuos_qq$Punto <- rownames(
normalidad_residuos_qq
)
rownames(
normalidad_residuos_qq
) <- NULL
normalidad_residuos_qq$Decision <- ifelse(
is.na(
normalidad_residuos_qq$p_value
),
"No evaluable",
ifelse(
normalidad_residuos_qq$p_value < 0.05,
"Evidencia contra normalidad",
"No se rechaza normalidad"
)
)
normalidad_residuos_qq <- normalidad_residuos_qq[
,
c(
"Punto",
"N",
"W",
"p_value",
"Decision"
)
]
knitr::kable(
normalidad_residuos_qq,
digits = 4,
caption = "Normalidad de residuos de los modelos temporales por punto"
)
| Punto | N | W | p_value | Decision |
|---|---|---|---|---|
| PUNTO_11 | 21 | 0.9208 | 0.0901 | No se rechaza normalidad |
| PUNTO_16 | 21 | 0.9698 | 0.7279 | No se rechaza normalidad |
| PUNTO_17 | 21 | 0.9635 | 0.5884 | No se rechaza normalidad |
| PUNTO_18 | 21 | 0.9542 | 0.4070 | No se rechaza normalidad |
| PUNTO_19 | 22 | 0.8448 | 0.0028 | Evidencia contra normalidad |
| PUNTO_20 | 21 | 0.8928 | 0.0255 | Evidencia contra normalidad |
| PUNTO_21 | 21 | 0.9345 | 0.1693 | No se rechaza normalidad |
| PUNTO_23 | 22 | 0.8727 | 0.0088 | Evidencia contra normalidad |
| PUNTO_24 | 7 | 0.8803 | 0.2277 | No se rechaza normalidad |
| PUNTO_25 | 11 | 0.7692 | 0.0037 | Evidencia contra normalidad |
| PUNTO_26 | 11 | 0.7572 | 0.0026 | Evidencia contra normalidad |
| PUNTO_4 | 6 | 0.9121 | 0.4504 | No se rechaza normalidad |
| PUNTO_5 | 19 | 0.8485 | 0.0063 | Evidencia contra normalidad |
| PUNTO_6 | 20 | 0.8099 | 0.0012 | Evidencia contra normalidad |
| PUNTO_7 | 21 | 0.9009 | 0.0365 | Evidencia contra normalidad |
| PUNTO_8 | 20 | 0.8881 | 0.0248 | Evidencia contra normalidad |
| PUNTO_9 | 22 | 0.8392 | 0.0022 | Evidencia contra normalidad |
Para cada punto debemos considerar simultáneamente:
\[ \boxed{ \text{Dispersión} + r + IC(r) + \rho_s } \]
\[ + \]
\[ \boxed{ b_1 + IC(\beta_1) + p + R^2 } \]
\[ + \]
\[ \boxed{ \text{Diagnóstico de residuos} } \]
Una conclusión no debe construirse únicamente a partir del valor \(p\).
Para cada punto se debe responder:
¿Los resultados microbiológicos aumentan o disminuyen a través del tiempo?
¿Qué tan fuerte es esa tendencia?
¿Qué tan grande es el cambio estimado?
¿Cuánta incertidumbre existe?
¿Qué proporción de la variabilidad puede ser representada mediante una tendencia lineal?
Una pendiente positiva puede indicar una tendencia microbiológica creciente.
Una pendiente negativa puede indicar una tendencia decreciente.
Una pendiente cercana a cero indica poca tendencia lineal.
Sin embargo:
\[ \boxed{ \text{asociación temporal} \neq \text{causalidad} } \]
Pueden intervenir:
Un mismo punto aparece en diferentes fechas.
Por tanto, las observaciones pueden presentar dependencia temporal.
Los métodos presentados constituyen una primera aproximación estadística.
Para análisis avanzados podrían utilizarse:
\[ \boxed{\text{Seleccionar punto}} \]
\[ \Downarrow \]
\[ \boxed{\text{Tiempo + Resultado}} \]
\[ \Downarrow \]
\[ \boxed{\text{Diagrama de dispersión}} \]
\[ \Downarrow \]
\[ \boxed{\text{Covarianza}} \]
\[ \Downarrow \]
\[ \boxed{\text{Pearson / Spearman}} \]
\[ \Downarrow \]
\[ \boxed{ Y= \beta_0+ \beta_1X+ \varepsilon } \]
\[ \Downarrow \]
\[ \boxed{\text{Pendiente + IC + }p} \]
\[ \Downarrow \]
\[ \boxed{\text{Diagnóstico de residuos}} \]
\[ \Downarrow \]
\[ \boxed{R^2} \]
\[ \Downarrow \]
\[ \boxed{\text{IC de media + Predicción}} \]
\[ \Downarrow \]
\[ \boxed{\text{Interpretación microbiológica}} \]
| Tipo | Pregunta | Método principal | Medida de magnitud |
|---|---|---|---|
| Cuali–Cuali | ¿La frecuencia de alerta depende del punto? | Chi/Fisher | OR, V de Cramér |
| Cuanti–Cuali | ¿Los resultados difieren entre puntos? | Welch/ANOVA/Kruskal | \(d,\eta^2\) |
| Cuanti–Cuanti | ¿El resultado cambia con el tiempo dentro del punto? | Pearson/Spearman/regresión | \(r,\rho_s,b_1,R^2\) |
Un valor:
\[ p<0.001 \]
indica evidencia estadística.
No indica automáticamente una relación fuerte o una diferencia grande.
Por ello:
\[ \boxed{ p \neq \text{magnitud del efecto} } \]
Siempre debemos reportar también:
según corresponda.
Puede observarse:
\[ \text{Punto} \leftrightarrow \text{Alerta} \]
o:
\[ \text{Tiempo} \leftrightarrow \text{Resultado} \]
sin demostrar:
\[ X \longrightarrow Y \]
Pueden existir factores adicionales no observados.
La pregunta estadística es:
¿Existe evidencia de una diferencia o asociación?
La pregunta microbiológica es:
¿La magnitud y naturaleza de esa diferencia tiene importancia para comprender el comportamiento del sistema?
Ambas deben responderse.
El análisis final debe permitir responder:
Cada estudiante trabajará con el periodo temporal asignado.
Todos utilizarán la misma base, pero cada estudiante o grupo analizará un periodo diferente.
Deberán desarrollar los tres tipos de análisis.
Cualitativa–Cualitativa \[ \text{Punto} \times \text{Alerta} \]
Deberán incluir:
Cuantitativa–Cualitativa \[ \text{Resultado} \times \text{Punto} \]
Deberán incluir:
Cuantitativa–Cuantitativa \[ \text{Tiempo} \times \text{Resultado} \]
analizado dentro de cada punto.
Deberán incluir:
1. La naturaleza de las variables determina el procedimiento estadístico.
2. El punto de muestreo constituye el eje principal del análisis.
3. Cuali–Cuali estudia asociaciones entre categorías.
4. Cuanti–Cuali compara una variable numérica entre puntos.
5. Cuanti–Cuanti estudia relaciones numéricas y tendencias temporales.
6. El gráfico debe preceder a la inferencia.
7. Los supuestos orientan la selección de la prueba.
8. El valor p no mide la magnitud del efecto.
9. Los intervalos de confianza expresan incertidumbre.
10. Una asociación estadística no implica causalidad.
11. Una diferencia estadística debe evaluarse también por su importancia microbiológica.
12. Toda conclusión debe integrar estadística, punto de muestreo y contexto microbiológico.
Blair, R. C., & Taylor, R. A. (2008). Bioestadística. Pearson Educación.
Bermúdez, J. D. (2013). Diez lecciones de estadística básica. Universitat de València.
Díaz Portillo, J. (2011). Guía práctica del curso de Bioestadística aplicada a las Ciencias de la Salud. Instituto Nacional de Gestión Sanitaria.
Yousefi, M., Najafi Saleh, H., Yaseri, M., Mahvi, A. H., Soleimani, H., Saeedi, Z., Zohdi, S., & Mohammadi, A. A. (2018). Data on microbiological quality assessment of rural drinking water supplies in Poldasht county. Data in Brief, 17, 763–769. https://doi.org/10.1016/j.dib.2018.02.003