# UNIVERSIDAD CENTRAL DEL ECUADOR
# Facultad de Ingeniería en Geología, Minas, Petroleos y Ambiental
# Ingeniería Ambiental
# Autor: Grupo 1
# Fecha: 07/02/2026
library(readr)
library(dplyr)
##
## Adjuntando el paquete: 'dplyr'
## The following objects are masked from 'package:stats':
##
## filter, lag
## The following objects are masked from 'package:base':
##
## intersect, setdiff, setequal, union
library(ggplot2)
library(tidyr)
datos <- read.csv(
"china_water_pollution_data.csv",
header = TRUE,
sep = ",",
dec = ".",
fileEncoding = "UTF-8"
)
str(datos)
## 'data.frame': 3000 obs. of 25 variables:
## $ Province : chr "Zhejiang" "Sichuan" "Zhejiang" "Beijing" ...
## $ City : chr "Ningbo" "Mianyang" "Ningbo" "Beijing" ...
## $ Monitoring_Station : chr "Ningbo_Station_2" "Mianyang_Station_1" "Ningbo_Station_8" "Beijing_Station_10" ...
## $ Latitude : num 25.5 32.2 30 30 43.5 ...
## $ Longitude : num 123 113 125 118 122 ...
## $ Date : chr "2023-06-01" "2023-03-05" "2023-07-13" "2023-02-17" ...
## $ Water_Temperature_C : num 22.5 27.3 21 16.6 21.8 ...
## $ pH : num 6.93 6.89 6.02 7.31 7.77 7.16 7.2 7.38 7.77 7.17 ...
## $ Dissolved_Oxygen_mg_L : num 9.3 8.14 5.34 10.06 7.93 ...
## $ Conductivity_uS_cm : num 652 358 520 593 656 ...
## $ Turbidity_NTU : num 0.85 4.49 17.46 7.38 3.7 ...
## $ Nitrate_mg_L : num 2.14 2.06 2.11 1.9 1.8 2.69 1.99 2.7 1.82 2.11 ...
## $ Nitrite_mg_L : num 0.03 0.015 0.029 0.014 0.019 0.022 0.003 0.034 0.026 0.028 ...
## $ Ammonia_N_mg_L : num 0.38 0.38 0.3 0.2 0.22 0.44 0.29 0.32 0.71 0.53 ...
## $ Total_Phosphorus_mg_L : num 0.074 0.147 0.021 0.155 0.152 0.134 0.05 0.104 0.126 0.039 ...
## $ Total_Nitrogen_mg_L : num 2.71 3.15 3.39 2.91 3.45 2.87 3.05 2.75 3.68 3.52 ...
## $ COD_mg_L : num 15.4 16.8 17.3 17.9 20.1 ...
## $ BOD_mg_L : num 1.39 2.98 2.65 5.18 3.47 5.27 3.34 5.71 3.68 4.73 ...
## $ Heavy_Metals_Pb_ug_L : num 6.9 4.68 3.24 3.2 2.01 4.42 5.01 6.75 4.59 3.09 ...
## $ Heavy_Metals_Cd_ug_L : num 0.66 0.39 0.27 0.67 0.34 -0.03 0.6 0.5 0.21 0.35 ...
## $ Heavy_Metals_Hg_ug_L : num 0.02 0.1 0.11 0.11 0.14 0.12 0.09 0.08 0.16 0.04 ...
## $ Coliform_Count_CFU_100mL: int 87 116 110 99 82 91 92 91 95 108 ...
## $ Water_Quality_Index : num 36.6 66.2 98.7 71.3 16.1 ...
## $ Pollution_Level : chr "Very Poor" "Excellent" "Poor" "Poor" ...
## $ Remarks : chr "High pollution spike detected" "High pollution spike detected" "High pollution spike detected" "Monitoring recommended" ...
datos$COD_mg_L <- as.numeric(datos$COD_mg_L)
datos$BOD_mg_L <- as.numeric(datos$BOD_mg_L)
datos_depu <- datos %>%
select(BOD_mg_L, COD_mg_L) %>%
na.omit() %>%
filter(BOD_mg_L > 0, COD_mg_L > 0)
cat("Filtrado NA/positivos:", nrow(datos), "→", nrow(datos_depu), "filas\n")
## Filtrado NA/positivos: 3000 → 3000 filas
remove_iqr_outliers <- function(x) {
Q1 <- quantile(x, 0.25, na.rm = TRUE)
Q3 <- quantile(x, 0.75, na.rm = TRUE)
IQR <- Q3 - Q1
x >= Q1 - 1.5 * IQR & x <= Q3 + 1.5 * IQR
}
mask_bod <- remove_iqr_outliers(datos_depu$BOD_mg_L)
mask_cod <- remove_iqr_outliers(datos_depu$COD_mg_L)
datos_depu <- datos_depu[mask_bod & mask_cod, ]
cat("Filtrado IQR:", nrow(datos_depu), "filas limpias tras IQR\n")
## Filtrado IQR: 2966 filas limpias tras IQR
by_bin <- 0.591
datos_depu <- datos_depu %>%
mutate(BOD_BIN = cut(
BOD_mg_L,
breaks = seq(floor(min(BOD_mg_L)),
ceiling(max(BOD_mg_L)),
by = by_bin),
include.lowest = TRUE
))
# Medianas por bin
med_bin <- datos_depu %>%
group_by(BOD_BIN) %>%
summarise(
BOD_median = median(BOD_mg_L, na.rm = TRUE),
COD_median = median(COD_mg_L, na.rm = TRUE),
n_bin = n()
) %>%
drop_na()
med_bin
## # A tibble: 10 × 4
## BOD_BIN BOD_median COD_median n_bin
## <fct> <dbl> <dbl> <int>
## 1 [1,1.59] 1.46 18.7 16
## 2 (1.59,2.18] 2.00 18.9 70
## 3 (2.18,2.77] 2.54 19.8 246
## 4 (2.77,3.36] 3.13 20.0 447
## 5 (3.36,3.96] 3.69 20.4 621
## 6 (3.96,4.55] 4.22 20.0 662
## 7 (4.55,5.14] 4.82 19.8 495
## 8 (5.14,5.73] 5.34 19.5 276
## 9 (5.73,6.32] 5.94 20.3 107
## 10 (6.32,6.91] 6.42 21.3 26
# Mantener positivos
med_bin_pot <- med_bin %>%
filter(BOD_median > 0, COD_median > 0)
cat("Bins usados:", nrow(med_bin_pot), "\n")
## Bins usados: 10
by_bin <- 0.591
datos_depu <- datos_depu %>%
mutate(BOD_BIN = cut(
BOD_mg_L,
breaks = seq(floor(min(BOD_mg_L)),
ceiling(max(BOD_mg_L)),
by = by_bin),
include.lowest = TRUE
))
med_bin <- datos_depu %>%
group_by(BOD_BIN) %>%
summarise(
BOD_median = median(BOD_mg_L, na.rm = TRUE),
COD_median = median(COD_mg_L, na.rm = TRUE),
n_bin = n()
) %>%
drop_na()
med_bin_pot <- med_bin %>% filter(BOD_median > 0, COD_median > 0)
cat("Bins usados:", nrow(med_bin_pot), "\n")
## Bins usados: 10
med_bin_pot
## # A tibble: 10 × 4
## BOD_BIN BOD_median COD_median n_bin
## <fct> <dbl> <dbl> <int>
## 1 [1,1.59] 1.46 18.7 16
## 2 (1.59,2.18] 2.00 18.9 70
## 3 (2.18,2.77] 2.54 19.8 246
## 4 (2.77,3.36] 3.13 20.0 447
## 5 (3.36,3.96] 3.69 20.4 621
## 6 (3.96,4.55] 4.22 20.0 662
## 7 (4.55,5.14] 4.82 19.8 495
## 8 (5.14,5.73] 5.34 19.5 276
## 9 (5.73,6.32] 5.94 20.3 107
## 10 (6.32,6.91] 6.42 21.3 26
x <- med_bin_pot$BOD_median
y <- med_bin_pot$COD_median
# A partir del diagrama de dispersión se observa una
# relación no lineal entre la Demanda Bioquímica de Oxígeno (BOD)
# y la Demanda Química de Oxígeno (COD).
# La tendencia muestra un crecimiento suave de la COD conforme
# aumenta la BOD, lo que sugiere el uso de un modelo potencial
# del tipo y = a * x^b.
# Variables para la conjetura (medianas por bin)
x <- med_bin_pot$BOD_median
y <- med_bin_pot$COD_median
plot(
x, y,
pch = 19,
col = "steelblue",
xlab = "BOD (mediana por bin) [mg/L]",
ylab = "COD (mediana por bin) [mg/L]",
main = "Gráfica N°1: Relación entre la BOD y la COD",
font.main = 2
)
# Ajuste potencial en escala log-log:
# log(COD) = log(a) + b*log(BOD)
modelo_pot <- lm(log(COD_median) ~ log(BOD_median), data = med_bin_pot)
summary(modelo_pot)
##
## Call:
## lm(formula = log(COD_median) ~ log(BOD_median), data = med_bin_pot)
##
## Residuals:
## Min 1Q Median 3Q Max
## -0.043940 -0.013812 -0.003557 0.016481 0.036037
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 2.91019 0.02354 123.633 2.05e-14 ***
## log(BOD_median) 0.06160 0.01731 3.558 0.00742 **
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.02535 on 8 degrees of freedom
## Multiple R-squared: 0.6128, Adjusted R-squared: 0.5644
## F-statistic: 12.66 on 1 and 8 DF, p-value: 0.007418
# Parámetros del modelo potencial: COD = a * BOD^b
coef_pot <- coef(modelo_pot)
a_pot <- exp(coef_pot[1]) # a = e^(intercepto)
b_pot <- coef_pot[2] # b = pendiente
a_pot
## (Intercept)
## 18.36036
b_pot
## log(BOD_median)
## 0.06159927
# Curva ajustada
x_vals <- seq(min(med_bin_pot$BOD_median),
max(med_bin_pot$BOD_median),
length.out = 200)
y_pred <- a_pot * x_vals^b_pot
# Gráfica (puntos + curva)
plot(med_bin_pot$BOD_median, med_bin_pot$COD_median,
pch = 19, col = "steelblue",
xlab = "BOD (mediana por bin) [mg/L]",
ylab = "COD (mediana por bin) [mg/L]",
main = "Gráfica N°2: Modelo potencial ajustado entre BOD y COD\nChina, 2023",
font.main = 2)
lines(x_vals, y_pred, col = "darkgreen", lwd = 2)
legend("topleft",
legend = c("Medianas por bin",
paste0("COD = ", round(a_pot,4), " * BOD^", round(b_pot,4))),
col = c("steelblue", "darkgreen"),
pch = c(19, NA),
lwd = c(NA, 2),
bty = "n")
# Correlación de Pearson sobre variables transformadas (log-log)
r <- cor(
log(med_bin_pot$BOD_median),
log(med_bin_pot$COD_median),
method = "pearson"
)
r
## [1] 0.7828295
cat("Correlación de Pearson (%):", round(r * 100, 2), "%\n")
## Correlación de Pearson (%): 78.28 %
r2 <- summary(modelo_pot)$r.squared * 100
cat("R² potencial =", round(r2, 2), "%\n")
## R² potencial = 61.28 %
## 7. Restricciones del modelo
# Restricción 1: el modelo potencial requiere valores positivos (por el log)
# (en tu caso ya filtraste BOD_median > 0 y COD_median > 0, pero lo verificamos)
sum(med_bin_pot$BOD_median <= 0)
## [1] 0
sum(med_bin_pot$COD_median <= 0)
## [1] 0
# Restricción 2: el modelo es válido solo dentro del rango observado de BOD
rango_BOD <- range(med_bin_pot$BOD_median)
rango_COD <- range(med_bin_pot$COD_median)
cat("Rango válido de BOD (mg/L): [", round(rango_BOD[1], 2), ",", round(rango_BOD[2], 2), "]\n")
## Rango válido de BOD (mg/L): [ 1.46 , 6.42 ]
cat("Rango observado de COD (mg/L): [", round(rango_COD[1], 2), ",", round(rango_COD[2], 2), "]\n")
## Rango observado de COD (mg/L): [ 18.7 , 21.34 ]
## numeric(0)
# El modelo potencial COD = a * BOD^b solo está definido para valores positivos
# de la Demanda Bioquímica de Oxígeno (BOD > 0), debido a la transformación
# logarítmica utilizada en el ajuste del modelo.
# Asimismo, el modelo es válido únicamente dentro del rango observado de la BOD
# en los datos analizados y no debe extrapolarse fuera de dicho intervalo.
# Valor objetivo (puedes cambiarlo)
BOD_objetivo <- 4 # mg/L
# Estimación usando el modelo potencial: COD = a * BOD^b
COD_est <- a_pot * BOD_objetivo^b_pot
# Mostrar como en el ejemplo (texto centrado grande)
plot(1, type = "n", axes = FALSE, xlab = "", ylab = "")
text(
1, 1,
labels = paste0(
"¿Cuál es la concentración esperada de DQO (COD)\n",
"cuando la DBO (BOD) del agua es ", BOD_objetivo, " mg/L?\n\n",
"Resultado estimado: ", round(COD_est, 3), " mg/L"
),
cex = 1.3,
col = "blue",
font = 2
)
Utilizando las medianas agrupadas por intervalos de la Demanda Bioquímica de Oxígeno (BOD), y transformando ambas variables a escala logarítmica (log-log), se modeló la relación no lineal entre la BOD y la Demanda Química de Oxígeno (COD) mediante un modelo de tipo potencial, común en el análisis de procesos de contaminación orgánica en cuerpos de agua.
El ajuste obtuvo un coeficiente de determinación R² ≈ 61 % y una correlación de Pearson log-log de 78 %, lo que indica una relación positiva y moderada entre la BOD y la COD. En el contexto del estudio de la contaminación del agua en China en 2023, el modelo potencial evidencia una relación escalable entre ambas variables; sin embargo, la COD no depende exclusivamente de la BOD, ya que intervienen otros factores fisicoquímicos y aportes externos.