Introducción
En este trabajo buscamos evaluar si es posible clasificar el agua como potable o no potable utilizando únicamente variables químicas medidas en laboratorio, como pH, sólidos, sulfatos o turbidez. La idea es responder una pregunta clave:
¿Estas variables realmente permiten distinguir entre agua apta para consumo y agua contaminada?
Para estudiarlo, aplicamos un modelo de regresión logística, hicimos un análisis exploratorio completo y evaluamos su capacidad predictiva con métricas formales como matriz de confusión y curva ROC. Además, construimos una aplicación Shiny como demostración práctica de cómo estos modelos podrían integrarse en herramientas operativas.
library(tidyverse)
## Warning: package 'tidyverse' was built under R version 4.5.2
## Warning: package 'ggplot2' was built under R version 4.5.2
## Warning: package 'tibble' was built under R version 4.5.1
## Warning: package 'tidyr' was built under R version 4.5.1
## Warning: package 'readr' was built under R version 4.5.1
## Warning: package 'purrr' was built under R version 4.5.1
## Warning: package 'dplyr' was built under R version 4.5.1
## Warning: package 'forcats' was built under R version 4.5.1
## Warning: package 'lubridate' was built under R version 4.5.1
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ dplyr 1.1.4 ✔ readr 2.1.5
## ✔ forcats 1.0.0 ✔ stringr 1.5.2
## ✔ ggplot2 4.0.0 ✔ tibble 3.3.0
## ✔ lubridate 1.9.4 ✔ tidyr 1.3.1
## ✔ purrr 1.1.0
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag() masks stats::lag()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(janitor)
## Warning: package 'janitor' was built under R version 4.5.2
##
## Adjuntando el paquete: 'janitor'
##
## The following objects are masked from 'package:stats':
##
## chisq.test, fisher.test
library(caret)
## Warning: package 'caret' was built under R version 4.5.2
## Cargando paquete requerido: lattice
##
## Adjuntando el paquete: 'caret'
##
## The following object is masked from 'package:purrr':
##
## lift
library(pROC)
## Warning: package 'pROC' was built under R version 4.5.2
## Type 'citation("pROC")' for a citation.
##
## Adjuntando el paquete: 'pROC'
##
## The following objects are masked from 'package:stats':
##
## cov, smooth, var
# Cargar datos
water <- read.csv("C:/Users/Paula Andrea Suarez/Desktop/1 Bioestadistica/water_potability.csv")
# Limpiar nombres de columnas
water <- clean_names(water)
# Convertir potability en factor
water$potability <- as.factor(water$potability)
# Eliminar filas con NA
water <- water %>% drop_na()
# Verificar balance de clases
table(water$potability)
##
## 0 1
## 1200 811
La variable potability muestra un desbalance claro en las clases: de 2011 muestras, 1200 (59.6%) son de agua no potable y 811 (40.4%) son de agua potable. Esto significa que la mayoría de los datos corresponde a agua no apta para consumo. Este desbalance puede afectar el modelo, porque tiende a aprender mejor la clase mayoritaria (no potable) y a tener más dificultad para reconocer la clase minoritaria (potable).
summary(water)
## ph hardness solids chloramines
## Min. : 0.2275 Min. : 73.49 Min. : 320.9 Min. : 1.391
## 1st Qu.: 6.0897 1st Qu.:176.74 1st Qu.:15615.7 1st Qu.: 6.139
## Median : 7.0273 Median :197.19 Median :20933.5 Median : 7.144
## Mean : 7.0860 Mean :195.97 Mean :21917.4 Mean : 7.134
## 3rd Qu.: 8.0530 3rd Qu.:216.44 3rd Qu.:27182.6 3rd Qu.: 8.110
## Max. :14.0000 Max. :317.34 Max. :56488.7 Max. :13.127
## sulfate conductivity organic_carbon trihalomethanes
## Min. :129.0 Min. :201.6 Min. : 2.20 Min. : 8.577
## 1st Qu.:307.6 1st Qu.:366.7 1st Qu.:12.12 1st Qu.: 55.953
## Median :332.2 Median :423.5 Median :14.32 Median : 66.542
## Mean :333.2 Mean :426.5 Mean :14.36 Mean : 66.401
## 3rd Qu.:359.3 3rd Qu.:482.4 3rd Qu.:16.68 3rd Qu.: 77.292
## Max. :481.0 Max. :753.3 Max. :27.01 Max. :124.000
## turbidity potability
## Min. :1.450 0:1200
## 1st Qu.:3.443 1: 811
## Median :3.968
## Mean :3.970
## 3rd Qu.:4.514
## Max. :6.495
# Comparación de medias por potabilidad
water %>%
group_by(potability) %>%
summarise(across(everything(), ~ mean(.x, na.rm = TRUE)))
## # A tibble: 2 × 10
## potability ph hardness solids chloramines sulfate conductivity
## <fct> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 0 7.07 196. 21629. 7.11 334. 428.
## 2 1 7.11 196. 22345. 7.17 332. 425.
## # ℹ 3 more variables: organic_carbon <dbl>, trihalomethanes <dbl>,
## # turbidity <dbl>
Al comparar las medias de cada variable entre agua potable y no potable, se ve que las diferencias son muy pequeñas. Esto significa que las características químicas del agua son prácticamente iguales en ambos grupos. Debido a esto, las variables no logran separar claramente las dos clases, y por eso el modelo de regresión logística tiene un rendimiento limitado.El problema no es el modelo, sino que los datos no muestran señales fuertes que permitan distinguir fácilmente si el agua es potable o no.
ggplot(water, aes(x = ph, fill = potability)) +
geom_histogram(bins = 30, position = "dodge") +
labs(
title = "Distribución del pH según la potabilidad",
x = "pH",
y = "Frecuencia",
fill = "Potabilidad"
)
Esta superposición visual indica que los valores de pH en agua potable y no potable son muy similares y cambian dentro del mismo rango.
Por lo tanto, el pH no funciona como una variable que diferencie los dos tipos de agua, ya que no se observa un patrón claro por color. Esto confirma que el pH no es un buen predictor para el modelo de regresión logística: como los valores se mezclan entre las dos categorías, el modelo no puede usar esta variable para clasificar correctamente.
Para entender si existen relaciones internas importantes, construimos una matriz de correlación.
library(reshape2)
## Warning: package 'reshape2' was built under R version 4.5.2
##
## Adjuntando el paquete: 'reshape2'
## The following object is masked from 'package:tidyr':
##
## smiths
num_cols <- water %>% select(-potability)
corr <- cor(num_cols)
melted <- melt(corr)
ggplot(melted, aes(Var1, Var2, fill = value)) +
geom_tile() +
scale_fill_gradient2() +
theme(axis.text.x = element_text(angle = 90)) +
labs(title = "Matriz de correlación entre variables químicas")
casi todas las variables químicas no están relacionadas entre sí, porque los valores son muy bajos y los cuadros se ven casi blancos. Solo se observa una relación moderada entre sólidos y dureza, y entre sólidos y sulfatos, lo cual es lógico porque más minerales aumentan los sólidos totales. El resto de variables, como pH, turbidez o trihalometanos, no presentan correlaciones importantes. En general, las variables químicas del agua varían de manera independiente en este conjunto de datos.
set.seed(123)
split <- createDataPartition(water$potability, p = 0.7, list = FALSE)
train <- water[split, ]
test <- water[-split, ]
Se separaron 70% de los datos para entrenar y 30% para probar, manteniendo la proporción de clases en ambos conjuntos. Esto evita que el modelo aprenda más de una clase que de otra y que las métricas queden sesgadas.
modelo2 <- glm(
potability ~ .,
data = train,
family = "binomial"
)
summary(modelo2)
##
## Call:
## glm(formula = potability ~ ., family = "binomial", data = train)
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) -3.993e-01 9.098e-01 -0.439 0.6607
## ph 4.694e-02 3.447e-02 1.362 0.1733
## hardness -3.172e-05 1.719e-03 -0.018 0.9853
## solids 1.434e-05 6.448e-06 2.224 0.0262 *
## chloramines 4.438e-02 3.508e-02 1.265 0.2058
## sulfate -5.791e-04 1.387e-03 -0.418 0.6762
## conductivity -8.417e-04 6.925e-04 -1.215 0.2242
## organic_carbon -3.164e-02 1.663e-02 -1.902 0.0571 .
## trihalomethanes -9.586e-04 3.478e-03 -0.276 0.7829
## turbidity 3.011e-02 7.115e-02 0.423 0.6721
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for binomial family taken to be 1)
##
## Null deviance: 1899.0 on 1407 degrees of freedom
## Residual deviance: 1885.1 on 1398 degrees of freedom
## AIC: 1905.1
##
## Number of Fisher Scoring iterations: 4
Cada variable tiene dos valores importantes: el Estimate y el p-value. El Estimate indica si al aumentar esa variable la probabilidad de potabilidad aumenta, disminuye o prácticamente no cambia.
Si el Estimate es positivo, significa que valores más altos de esa variable están asociados con una probabilidad un poco mayor de que el agua sea potable. Si es negativo, ocurre lo contrario: aumentar esa variable tiende a reducir la probabilidad de potabilidad. Y si el número es muy cercano a cero, quiere decir que la variable casi no tiene efecto.
El segundo valor, el p-value, nos dice si ese efecto es estadísticamente confiable o si probablemente ocurrió por azar. La mayoría de las variables químicas tienen p-values altos, lo que indica que no logran diferenciar de forma confiable entre agua potable y no potable. Solo la variable solids aparece como significativa, pero incluso su efecto es pequeño
test$prob <- predict(modelo2, test, type = "response")
test$pred <- ifelse(test$prob > 0.5, 1, 0)
test$pred <- as.factor(test$pred)
confusionMatrix(test$pred, test$potability)
## Confusion Matrix and Statistics
##
## Reference
## Prediction 0 1
## 0 353 229
## 1 7 14
##
## Accuracy : 0.6086
## 95% CI : (0.5684, 0.6478)
## No Information Rate : 0.597
## P-Value [Acc > NIR] : 0.2954
##
## Kappa : 0.0448
##
## Mcnemar's Test P-Value : <2e-16
##
## Sensitivity : 0.98056
## Specificity : 0.05761
## Pos Pred Value : 0.60653
## Neg Pred Value : 0.66667
## Prevalence : 0.59701
## Detection Rate : 0.58541
## Detection Prevalence : 0.96517
## Balanced Accuracy : 0.51908
##
## 'Positive' Class : 0
##
La matriz de confusión muestra que el modelo tiene un desempeño limitado al clasificar la potabilidad del agua. Aunque logra identificar correctamente la mayoría de las muestras no potables (sensibilidad del 98%), falla casi por completo al reconocer el agua potable (especificidad de apenas 5.7%). La exactitud total del 60% no es realmente buena, ya que es muy similar al No Information Rate (59%), lo que significa que el modelo predice casi igual a simplemente decir siempre “no potable”. El coeficiente Kappa (0.04) confirma que el modelo apenas mejora respecto al azar. En conjunto, estos resultados indican que las variables químicas disponibles en el dataset no contienen suficiente información para separar adecuadamente el agua potable de la no potable, lo que explica el bajo rendimiento del modelo logístico.
roc_obj <- roc(as.numeric(test$potability), test$prob)
## Setting levels: control = 1, case = 2
## Setting direction: controls > cases
plot(roc_obj, main = "Curva ROC – Modelo Logístico Mejorado")
auc(roc_obj)
## Area under the curve: 0.538
Luego analizamos la curva ROC para evaluar la capacidad del modelo de distinguir entre agua potable y no potable. El resultado fue un AUC de aproximadamente 0.53, lo cual es prácticamente igual al azar. Esto refuerza la idea de que las variables químicas del dataset no diferencian de forma clara los dos grupos, y por eso el modelo no logra clasificarlos correctamente.
Los resultados del análisis muestran que las variables químicas disponibles en el dataset no permiten diferenciar de manera clara entre agua potable y no potable. En el análisis descriptivo se observaron valores muy similares entre ambas clases, y esto se reflejó directamente en el modelo logístico: la mayoría de las variables no fueron significativas y los coeficientes fueron muy pequeños. Aunque la variable solids resultó significativa, su efecto fue mínimo y no aporta capacidad real de clasificación.
El rendimiento del modelo también fue bajo. La matriz de confusión evidenció que el modelo identifica bien el agua no potable, pero falla al reconocer el agua potable, lo que indica una fuerte desbalance en la capacidad de clasificación. El AUC de 0.53 confirma que el modelo no distingue adecuadamente las dos categorías y su desempeño es prácticamente equivalente al azar.
En conjunto, estos resultados indican que el conjunto de datos no contiene información suficiente para construir un modelo predictivo confiable. Por esta razón, tanto el modelo estadístico como la aplicación Shiny deben entenderse como herramientas demostrativas del proceso analítico, más que como sistemas reales de alerta o clasificación de potabilidad.
Como complemento del análisis estadístico, se desarrolló una aplicación sencilla en Shiny que permite simular cómo funcionaría un sistema básico de alerta para evaluar la potabilidad del agua. En esta herramienta, el usuario puede ingresar valores de las variables químicas (como pH, sólidos, dureza o cloraminas) y el sistema utiliza el modelo de regresión logística entrenado para calcular la probabilidad de que esa muestra sea potable.
La aplicación no busca diagnosticar con precisión, sino mostrar de manera práctica cómo un modelo estadístico puede integrarse en una interfaz interactiva. Aunque el rendimiento del modelo es limitado porque los datos originales no muestran diferencias claras entre agua potable y no potable la aplicación permite visualizar el proceso completo: ingresar datos, procesarlos con el modelo y obtener una predicción inmediata.
En resumen, la aplicación sirve como una demostración funcional de cómo los modelos estadísticos pueden apoyar herramientas de monitoreo de calidad del agua.