Evaluación de la Calidad del Agua en Estuarios y Lagos de Importancia Nacional: Un Estudio Hidroquímico para la Sostenibilidad Ambiental¶

Integrantes:¶

Mónica Gabriela Pacas Mejía (grupo miércoles)

Milton Antonio Sandoval (grupo jueves)

Patricia Steffany Arias Orellana (grupo jueves)

Descripción¶

El presente proyecto recopila un conjunto de datos tomados por el Laboratorio de Toxinas Marinas en la Universidad de El Salvador con el objetivo de monitorear la calidad del agua en cuerpos de agua que representan una gran importancia para la biodiversidad, economía y calidad de vida a nivel nacional. Este estudio se desarrolló por más de 2 años con el fin de recopilar y análizar datos hidroquímicos para evaluar el estado actual de los cuerpos de agua en estudio; con los resultados obtenidos se busca tener la capacidad de identificar posibles fuentes de contaminación según la posición del cuerpo de agua y proponer medidas para la gestión sostenible de estos recursos acuáticos. Con la ayuda de ciencia de datos y las herramientas con las que contamos usando Python, podremos automatizar el análisis de los datos obtenidos, obteniendo el análisis de datos de forma más eficaz y precisa.

Justificación¶

Las condiciones fisicoquímicas de los estuarios y lagos estudiados representan una gran influencia en la biodiversidad y las actividades economicas que se realizan en estos (agrícolas, industriales y recreativas). Por tal motivo, es necesario monitorear la calidad de agua con el fin de brindar una buena calidad de vida y salud pública al tener la capacidad de identificar posibles fuentes de contaminación según las concentraciones de cada nutriente.

Antecedentes¶

El exceso (o aumento) de nutrientes se encuentra entre los problemas más comunes de contaminación del agua que afectan los cuerpos de agua alrededor del mundo. Altas concentraciones de nutrientes resultantes de las actividades humanas pueden disminuir la salud de los ecosistemas, ya que puede provocar un exceso de crecimiento biológico (eutrofización) y floraciones algales nocivas (FAN). En agua dulce, las FAN a menudo son causadas por cianobacterias (algas verdiazules) (EPA, 2021).

Ciertos tipos de cianobacterias pueden producir toxinas que llegan al medio acuático y redes alimentarias terrestres dañando la salud de animales y humanos. Además, la proliferación de algas puede provocar hipoxia o bajo nivel de oxígeno disuelto (OD) en el agua, ya sea a través de la respiración de las algas o el consumo de oxígeno, por descomponedores cuando las algas mueren (EPA, 2021).

El fenómeno de El Niño (2018-2019) cuasó alteraciones climáticas y probablemente una floración de algas en algunos cuerpos de agua dulce en 2019 en El Salvador. Este proyecto, realizado entre 2020 y 2022, busca evaluar el impacto de estos eventos en la calidad del agua de estuarios y lagos de importancia nacional, por lo que es de mucho interés poder elaborar un proceso automatizado para el análisis de datos de esta naturaleza, ya que, como se mostrará a continuación, estamos ante un estudio de más de 2 años recopilando datos que, su análisis por métodos tradicionales pueda resultar en conclusiones poco precisas o dificultades al momento de obtener una correcta descripción, procesamiento y exploración de datos.

Objetivos¶

  • Describir la proporción/concentración de nutrientes en los cuerpos de agua estudiados.
  • Utilizar técnicas de análisis estadístico y modelado para interpretar los datos recopilados.
  • Identificar patrones espaciales y temporales de variación en la calidad del agua.
  • Automatizar el análisis de datos hidroquímicos con Python para identificar posibles patrones de contaminación en muestras de agua.

Descripción del conjunto de datos¶

A continuación, se trabajará con una tabla de datos de calidad de agua recopilados durante un monitoreo desarrollado entre febrero 2020 y octubre 2022. Dicha tabla presenta las columnas:

  • N°: número correlativo
  • Sitio de muestreo: cuerpo de agua de donde se tomó la muestra. Se contemplaron 6 sitios ()
  • Coordenada: en cada sitio se tiene un solo punto de muestreo con coordenadas específicas.
  • Tipo de muestra: si es de agua dulce o salobre
  • Fecha: mes y año en que se colectó la muestra
  • Profundidad: nivel de profundidad, en metros, que puede ser a 0.5 o 20 m
  • PO4 abs 880, uM PO4/L: Concentración de fosfato
  • NH4 abs 640, uM NH4/L: Concentración de amonio
  • NO3 abs 540, uM NO3/L: Concentración de nitrato

Preprocesamiento de datos¶

In [2]:
#Instalaciones
!pip install pandas scikit-learn
!pip install ydata-profiling #instalación de pandas profiling
Requirement already satisfied: pandas in /usr/local/lib/python3.10/dist-packages (2.0.3)
Requirement already satisfied: scikit-learn in /usr/local/lib/python3.10/dist-packages (1.2.2)
Requirement already satisfied: python-dateutil>=2.8.2 in /usr/local/lib/python3.10/dist-packages (from pandas) (2.8.2)
Requirement already satisfied: pytz>=2020.1 in /usr/local/lib/python3.10/dist-packages (from pandas) (2023.4)
Requirement already satisfied: tzdata>=2022.1 in /usr/local/lib/python3.10/dist-packages (from pandas) (2024.1)
Requirement already satisfied: numpy>=1.21.0 in /usr/local/lib/python3.10/dist-packages (from pandas) (1.25.2)
Requirement already satisfied: scipy>=1.3.2 in /usr/local/lib/python3.10/dist-packages (from scikit-learn) (1.11.4)
Requirement already satisfied: joblib>=1.1.1 in /usr/local/lib/python3.10/dist-packages (from scikit-learn) (1.4.2)
Requirement already satisfied: threadpoolctl>=2.0.0 in /usr/local/lib/python3.10/dist-packages (from scikit-learn) (3.5.0)
Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.10/dist-packages (from python-dateutil>=2.8.2->pandas) (1.16.0)
Collecting ydata-profiling
  Downloading ydata_profiling-4.8.3-py2.py3-none-any.whl (359 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 359.5/359.5 kB 4.2 MB/s eta 0:00:00
Requirement already satisfied: scipy<1.14,>=1.4.1 in /usr/local/lib/python3.10/dist-packages (from ydata-profiling) (1.11.4)
Requirement already satisfied: pandas!=1.4.0,<3,>1.1 in /usr/local/lib/python3.10/dist-packages (from ydata-profiling) (2.0.3)
Requirement already satisfied: matplotlib<3.9,>=3.2 in /usr/local/lib/python3.10/dist-packages (from ydata-profiling) (3.7.1)
Requirement already satisfied: pydantic>=2 in /usr/local/lib/python3.10/dist-packages (from ydata-profiling) (2.7.3)
Requirement already satisfied: PyYAML<6.1,>=5.0.0 in /usr/local/lib/python3.10/dist-packages (from ydata-profiling) (6.0.1)
Requirement already satisfied: jinja2<3.2,>=2.11.1 in /usr/local/lib/python3.10/dist-packages (from ydata-profiling) (3.1.4)
Collecting visions[type_image_path]<0.7.7,>=0.7.5 (from ydata-profiling)
  Downloading visions-0.7.6-py3-none-any.whl (104 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 104.8/104.8 kB 8.5 MB/s eta 0:00:00
Requirement already satisfied: numpy<2,>=1.16.0 in /usr/local/lib/python3.10/dist-packages (from ydata-profiling) (1.25.2)
Collecting htmlmin==0.1.12 (from ydata-profiling)
  Downloading htmlmin-0.1.12.tar.gz (19 kB)
  Preparing metadata (setup.py) ... done
Collecting phik<0.13,>=0.11.1 (from ydata-profiling)
  Downloading phik-0.12.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (686 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 686.1/686.1 kB 7.9 MB/s eta 0:00:00
Requirement already satisfied: requests<3,>=2.24.0 in /usr/local/lib/python3.10/dist-packages (from ydata-profiling) (2.31.0)
Requirement already satisfied: tqdm<5,>=4.48.2 in /usr/local/lib/python3.10/dist-packages (from ydata-profiling) (4.66.4)
Requirement already satisfied: seaborn<0.14,>=0.10.1 in /usr/local/lib/python3.10/dist-packages (from ydata-profiling) (0.13.1)
Collecting multimethod<2,>=1.4 (from ydata-profiling)
  Downloading multimethod-1.11.2-py3-none-any.whl (10 kB)
Requirement already satisfied: statsmodels<1,>=0.13.2 in /usr/local/lib/python3.10/dist-packages (from ydata-profiling) (0.14.2)
Collecting typeguard<5,>=3 (from ydata-profiling)
  Downloading typeguard-4.3.0-py3-none-any.whl (35 kB)
Collecting imagehash==4.3.1 (from ydata-profiling)
  Downloading ImageHash-4.3.1-py2.py3-none-any.whl (296 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 296.5/296.5 kB 8.4 MB/s eta 0:00:00
Requirement already satisfied: wordcloud>=1.9.1 in /usr/local/lib/python3.10/dist-packages (from ydata-profiling) (1.9.3)
Collecting dacite>=1.8 (from ydata-profiling)
  Downloading dacite-1.8.1-py3-none-any.whl (14 kB)
Requirement already satisfied: numba<1,>=0.56.0 in /usr/local/lib/python3.10/dist-packages (from ydata-profiling) (0.58.1)
Requirement already satisfied: PyWavelets in /usr/local/lib/python3.10/dist-packages (from imagehash==4.3.1->ydata-profiling) (1.6.0)
Requirement already satisfied: pillow in /usr/local/lib/python3.10/dist-packages (from imagehash==4.3.1->ydata-profiling) (9.4.0)
Requirement already satisfied: MarkupSafe>=2.0 in /usr/local/lib/python3.10/dist-packages (from jinja2<3.2,>=2.11.1->ydata-profiling) (2.1.5)
Requirement already satisfied: contourpy>=1.0.1 in /usr/local/lib/python3.10/dist-packages (from matplotlib<3.9,>=3.2->ydata-profiling) (1.2.1)
Requirement already satisfied: cycler>=0.10 in /usr/local/lib/python3.10/dist-packages (from matplotlib<3.9,>=3.2->ydata-profiling) (0.12.1)
Requirement already satisfied: fonttools>=4.22.0 in /usr/local/lib/python3.10/dist-packages (from matplotlib<3.9,>=3.2->ydata-profiling) (4.53.0)
Requirement already satisfied: kiwisolver>=1.0.1 in /usr/local/lib/python3.10/dist-packages (from matplotlib<3.9,>=3.2->ydata-profiling) (1.4.5)
Requirement already satisfied: packaging>=20.0 in /usr/local/lib/python3.10/dist-packages (from matplotlib<3.9,>=3.2->ydata-profiling) (24.0)
Requirement already satisfied: pyparsing>=2.3.1 in /usr/local/lib/python3.10/dist-packages (from matplotlib<3.9,>=3.2->ydata-profiling) (3.1.2)
Requirement already satisfied: python-dateutil>=2.7 in /usr/local/lib/python3.10/dist-packages (from matplotlib<3.9,>=3.2->ydata-profiling) (2.8.2)
Requirement already satisfied: llvmlite<0.42,>=0.41.0dev0 in /usr/local/lib/python3.10/dist-packages (from numba<1,>=0.56.0->ydata-profiling) (0.41.1)
Requirement already satisfied: pytz>=2020.1 in /usr/local/lib/python3.10/dist-packages (from pandas!=1.4.0,<3,>1.1->ydata-profiling) (2023.4)
Requirement already satisfied: tzdata>=2022.1 in /usr/local/lib/python3.10/dist-packages (from pandas!=1.4.0,<3,>1.1->ydata-profiling) (2024.1)
Requirement already satisfied: joblib>=0.14.1 in /usr/local/lib/python3.10/dist-packages (from phik<0.13,>=0.11.1->ydata-profiling) (1.4.2)
Requirement already satisfied: annotated-types>=0.4.0 in /usr/local/lib/python3.10/dist-packages (from pydantic>=2->ydata-profiling) (0.7.0)
Requirement already satisfied: pydantic-core==2.18.4 in /usr/local/lib/python3.10/dist-packages (from pydantic>=2->ydata-profiling) (2.18.4)
Requirement already satisfied: typing-extensions>=4.6.1 in /usr/local/lib/python3.10/dist-packages (from pydantic>=2->ydata-profiling) (4.12.1)
Requirement already satisfied: charset-normalizer<4,>=2 in /usr/local/lib/python3.10/dist-packages (from requests<3,>=2.24.0->ydata-profiling) (3.3.2)
Requirement already satisfied: idna<4,>=2.5 in /usr/local/lib/python3.10/dist-packages (from requests<3,>=2.24.0->ydata-profiling) (3.7)
Requirement already satisfied: urllib3<3,>=1.21.1 in /usr/local/lib/python3.10/dist-packages (from requests<3,>=2.24.0->ydata-profiling) (2.0.7)
Requirement already satisfied: certifi>=2017.4.17 in /usr/local/lib/python3.10/dist-packages (from requests<3,>=2.24.0->ydata-profiling) (2024.6.2)
Requirement already satisfied: patsy>=0.5.6 in /usr/local/lib/python3.10/dist-packages (from statsmodels<1,>=0.13.2->ydata-profiling) (0.5.6)
Requirement already satisfied: attrs>=19.3.0 in /usr/local/lib/python3.10/dist-packages (from visions[type_image_path]<0.7.7,>=0.7.5->ydata-profiling) (23.2.0)
Requirement already satisfied: networkx>=2.4 in /usr/local/lib/python3.10/dist-packages (from visions[type_image_path]<0.7.7,>=0.7.5->ydata-profiling) (3.3)
Requirement already satisfied: six in /usr/local/lib/python3.10/dist-packages (from patsy>=0.5.6->statsmodels<1,>=0.13.2->ydata-profiling) (1.16.0)
Building wheels for collected packages: htmlmin
  Building wheel for htmlmin (setup.py) ... done
  Created wheel for htmlmin: filename=htmlmin-0.1.12-py3-none-any.whl size=27080 sha256=b9739001356adb5231380f10c2db8ef5eaa4b378078f70a76430a041f0d3fe86
  Stored in directory: /root/.cache/pip/wheels/dd/91/29/a79cecb328d01739e64017b6fb9a1ab9d8cb1853098ec5966d
Successfully built htmlmin
Installing collected packages: htmlmin, typeguard, multimethod, dacite, imagehash, visions, phik, ydata-profiling
Successfully installed dacite-1.8.1 htmlmin-0.1.12 imagehash-4.3.1 multimethod-1.11.2 phik-0.12.4 typeguard-4.3.0 visions-0.7.6 ydata-profiling-4.8.3
In [3]:
#Importación de todas las librerias que utilizaremos
import pandas as pd #dataframes

from datetime import datetime #formato de fecha

from ydata_profiling import ProfileReport #informe para analisis de datos

from sklearn.linear_model import LinearRegression #modelo linear
from sklearn.metrics import r2_score

import seaborn as sns #gráficas
import matplotlib.pyplot as plt #grafica

# Importar geopandas y geodatasets
!pip install geodatasets
import geopandas as gpd
import geodatasets
import numpy as np
import pandas as pd
from matplotlib import pyplot as plt
Collecting geodatasets
  Downloading geodatasets-2023.12.0-py3-none-any.whl (19 kB)
Requirement already satisfied: pooch in /usr/local/lib/python3.10/dist-packages (from geodatasets) (1.8.1)
Requirement already satisfied: platformdirs>=2.5.0 in /usr/local/lib/python3.10/dist-packages (from pooch->geodatasets) (4.2.2)
Requirement already satisfied: packaging>=20.0 in /usr/local/lib/python3.10/dist-packages (from pooch->geodatasets) (24.0)
Requirement already satisfied: requests>=2.19.0 in /usr/local/lib/python3.10/dist-packages (from pooch->geodatasets) (2.31.0)
Requirement already satisfied: charset-normalizer<4,>=2 in /usr/local/lib/python3.10/dist-packages (from requests>=2.19.0->pooch->geodatasets) (3.3.2)
Requirement already satisfied: idna<4,>=2.5 in /usr/local/lib/python3.10/dist-packages (from requests>=2.19.0->pooch->geodatasets) (3.7)
Requirement already satisfied: urllib3<3,>=1.21.1 in /usr/local/lib/python3.10/dist-packages (from requests>=2.19.0->pooch->geodatasets) (2.0.7)
Requirement already satisfied: certifi>=2017.4.17 in /usr/local/lib/python3.10/dist-packages (from requests>=2.19.0->pooch->geodatasets) (2024.6.2)
Installing collected packages: geodatasets
Successfully installed geodatasets-2023.12.0

Importación de datos

In [4]:
#importar biblioteca para acceder a carpeta drive
from google.colab import drive #importamos la biblioteca
drive.mount('/content/drive') #montamos nuestro drive
Mounted at /content/drive
In [5]:
# Llamamos el archivo .csv
# Cada integrante crea un acceso directo de la carpeta Proyecto_Python y lo ubica en "Mi unidad" o "My drive" para usar la misma ruta
ruta_archivo = "/content/drive/MyDrive/Proyecto Python/Labtox_nutrientes.csv"

# Leemos el archivo CSV y lo convertimos en un DataFrame
df_nut = pd.read_csv(ruta_archivo)


print(df_nut)
        Nº   Sitio de muestreo  Coordenada Tipo de muestra       Fecha  \
0        1  Lago de Coatepeque         NaN          marine   24/2/2020   
1        2  Lago de Coatepeque         NaN          marine   24/2/2020   
2        3  Lago de Coatepeque         NaN          marine   24/2/2020   
3        4  Lago de Coatepeque         NaN          marine   24/2/2020   
4        5    Lago de Ilopango         NaN          marine   24/2/2020   
...    ...                 ...         ...             ...         ...   
2514  2899  Laguna de Chanmico         NaN        seawater  12/10/2022   
2515  2900   Laguna de Olomega         NaN        seawater  12/10/2022   
2516  2901   Laguna de Olomega         NaN        seawater  12/10/2022   
2517  2902   Laguna de Olomega         NaN        seawater  12/10/2022   
2518  2903   Laguna de Olomega         NaN        seawater  12/10/2022   

      Profundidad (m) PO4 abs 880  uM PO4/L NH4 abs 640  uM NH4/L NO3 abs 540  \
0                 0.5       0.002       NaN       0.000       NaN       0.017   
1                 0.5       0.002       NaN       0.000       NaN       0.021   
2                20.0       0.003       NaN       0.006       NaN       0.045   
3                20.0       0.011       NaN       0.010       NaN       0.136   
4                 0.5       0.000       NaN       0.000       NaN       0.019   
...               ...         ...       ...         ...       ...         ...   
2514             20.0       0.012       NaN       0.008       NaN       0.071   
2515              0.5       0.001       NaN       0.003       NaN       0.029   
2516              0.5       0.001       NaN       0.002       NaN       0.007   
2517             20.0       0.015       NaN       0.008       NaN       0.141   
2518             20.0       0.006       NaN       0.009       NaN       0.156   

       uM NO3/L  Unnamed: 12  Unnamed: 13  
0           NaN          NaN          NaN  
1           NaN          NaN          NaN  
2           NaN          NaN          NaN  
3           NaN          NaN          NaN  
4           NaN          NaN          NaN  
...         ...          ...          ...  
2514        NaN          NaN          NaN  
2515        NaN          NaN          NaN  
2516        NaN          NaN          NaN  
2517        NaN          NaN          NaN  
2518        NaN          NaN          NaN  

[2519 rows x 14 columns]

Exploración y modificación de datos:¶

In [6]:
#Borramos columnas que no necesitaremos para el ordenamiento de datos
del df_nut['Unnamed: 12']
del df_nut['Unnamed: 13']
del df_nut['uM PO4/L']
del df_nut['uM NH4/L']
del df_nut['\xa0uM NO3/L'] #Esta columna tiene un espacio que posteriormente complicara el uso de dicha columna por lo que se eliminarán estas tres columnas de concentración

print (df_nut)
        Nº   Sitio de muestreo  Coordenada Tipo de muestra       Fecha  \
0        1  Lago de Coatepeque         NaN          marine   24/2/2020   
1        2  Lago de Coatepeque         NaN          marine   24/2/2020   
2        3  Lago de Coatepeque         NaN          marine   24/2/2020   
3        4  Lago de Coatepeque         NaN          marine   24/2/2020   
4        5    Lago de Ilopango         NaN          marine   24/2/2020   
...    ...                 ...         ...             ...         ...   
2514  2899  Laguna de Chanmico         NaN        seawater  12/10/2022   
2515  2900   Laguna de Olomega         NaN        seawater  12/10/2022   
2516  2901   Laguna de Olomega         NaN        seawater  12/10/2022   
2517  2902   Laguna de Olomega         NaN        seawater  12/10/2022   
2518  2903   Laguna de Olomega         NaN        seawater  12/10/2022   

      Profundidad (m) PO4 abs 880 NH4 abs 640 NO3 abs 540  
0                 0.5       0.002       0.000       0.017  
1                 0.5       0.002       0.000       0.021  
2                20.0       0.003       0.006       0.045  
3                20.0       0.011       0.010       0.136  
4                 0.5       0.000       0.000       0.019  
...               ...         ...         ...         ...  
2514             20.0       0.012       0.008       0.071  
2515              0.5       0.001       0.003       0.029  
2516              0.5       0.001       0.002       0.007  
2517             20.0       0.015       0.008       0.141  
2518             20.0       0.006       0.009       0.156  

[2519 rows x 9 columns]
In [7]:
# Colocaremos correctamente la fecha ya que a partir de esto vamos a ordenar los datos
df_nut['Fecha'] = pd.to_datetime(df_nut['Fecha'], dayfirst=True, errors='coerce')
df_nut['Fecha'] = df_nut['Fecha'].dt.strftime('%d-%m-%Y')

# Muestra el DataFrame resultante
print(df_nut)
        Nº   Sitio de muestreo  Coordenada Tipo de muestra       Fecha  \
0        1  Lago de Coatepeque         NaN          marine  24-02-2020   
1        2  Lago de Coatepeque         NaN          marine  24-02-2020   
2        3  Lago de Coatepeque         NaN          marine  24-02-2020   
3        4  Lago de Coatepeque         NaN          marine  24-02-2020   
4        5    Lago de Ilopango         NaN          marine  24-02-2020   
...    ...                 ...         ...             ...         ...   
2514  2899  Laguna de Chanmico         NaN        seawater  12-10-2022   
2515  2900   Laguna de Olomega         NaN        seawater  12-10-2022   
2516  2901   Laguna de Olomega         NaN        seawater  12-10-2022   
2517  2902   Laguna de Olomega         NaN        seawater  12-10-2022   
2518  2903   Laguna de Olomega         NaN        seawater  12-10-2022   

      Profundidad (m) PO4 abs 880 NH4 abs 640 NO3 abs 540  
0                 0.5       0.002       0.000       0.017  
1                 0.5       0.002       0.000       0.021  
2                20.0       0.003       0.006       0.045  
3                20.0       0.011       0.010       0.136  
4                 0.5       0.000       0.000       0.019  
...               ...         ...         ...         ...  
2514             20.0       0.012       0.008       0.071  
2515              0.5       0.001       0.003       0.029  
2516              0.5       0.001       0.002       0.007  
2517             20.0       0.015       0.008       0.141  
2518             20.0       0.006       0.009       0.156  

[2519 rows x 9 columns]
In [8]:
#Verificamos que los datos no tengan anormalidades
df_nut.count()
Out[8]:
Nº                   2515
Sitio de muestreo    2514
Coordenada              0
Tipo de muestra      2519
Fecha                2515
Profundidad (m)      2514
PO4 abs 880          2519
NH4 abs 640          2519
NO3 abs 540          2519
dtype: int64
In [ ]:
#Cuando llamamos a df_count al inicio, nos aseguramos que son 2519 datos por lo que tendremos que analizar las columnas:
# Sitio de muestreo, Fecha y Profundidad
# Las columnas N°, Coordenada y Tipo de muestra se modificarán cuando las demás se encuentren llenadas correctamente

Búsqueda de datos faltantes NaN¶

In [9]:
df_nut.isna().sum()
#Se observa que las columnas N°, Sitio de muestreo, fecha y profundidad tienen datos faltantes
#La columnas de coordenada está completamente vacía debido a que se agregarán sus valores después de la limpieza de datos
Out[9]:
Nº                      4
Sitio de muestreo       5
Coordenada           2519
Tipo de muestra         0
Fecha                   4
Profundidad (m)         5
PO4 abs 880             0
NH4 abs 640             0
NO3 abs 540             0
dtype: int64
In [ ]:
#revision de datos faltantes en columna sitio de muestreo
df_nut["Sitio de muestreo"].value_counts(dropna=False) #Utilizamos el parámetro dropna para incluir los espacios vacíos
Out[ ]:
Sitio de muestreo
Lago de Ilopango        447
Estero de Jaltepeque    445
Laguna de Olomega       434
Barra de Santiago       433
Laguna de Chanmico      427
Lago de Coatepeque      322
NaN                       5
Lago de Coatepeque        4
Lago de Ilopango          2
Name: count, dtype: int64
In [10]:
#revision de datos faltantes en columna Fecha
df_nut["Fecha"].value_counts(dropna=False)
Out[10]:
Fecha
07-12-2021    72
22-03-2022    42
22-09-2020    36
29-09-2020    36
18-01-2021    25
              ..
15-02-2022    19
17-03-2020    15
NaN            4
04-03-2021     1
22-02-2021     1
Name: count, Length: 109, dtype: int64
In [ ]:
#revision de datos faltantes en columna Profundidad
df_nut["Profundidad (m)"].value_counts(dropna=False)
Out[ ]:
Profundidad (m)
0.5     1262
20.0    1252
NaN        5
Name: count, dtype: int64

Eliminación de datos NaN¶

Debido a que los datos faltantes no se pueden recuperar (exceptuando N° y coordenada), se eliminarán del data frame para evitar interferencias en el análisis

In [12]:
#Eliminamos las filas que contengan NaN en las columnas pertinentes:
#Sitio de muestreo
df_nut.dropna(subset=['Sitio de muestreo'], inplace=True)
#Fecha
df_nut.dropna(subset=['Fecha'], inplace=True)
#Profundidad (m)
df_nut.dropna(subset=['Profundidad (m)'], inplace=True)
In [13]:
#Se verifica que ya no haya datos faltantes
df_nut.isna().sum()
#Una vez borrados los datos nos quedamos con 2509 registros
Out[13]:
Nº                      3
Sitio de muestreo       0
Coordenada           2509
Tipo de muestra         0
Fecha                   0
Profundidad (m)         0
PO4 abs 880             0
NH4 abs 640             0
NO3 abs 540             0
dtype: int64

Verificacion y correccion de datos/variables¶


Debemos verificar que los datos concuerden con cada variable presentada y que tengan sentido, es decir, que cada columna debe presentar las siguientes categorías correctamente escritas.

Sitio de Muestreo: Laguna de Chanmico, Laguna de Olomega, Lago de Coatepeque, Lago de Ilopango, Estero de Jaltepeque, Barra de Santiago.

Tipo de muestra: dulce (Lago, Laguna), salobre (Estero, Barra)

Sitio de muestreo¶

In [14]:
df_nut['Sitio de muestreo'].value_counts()
#Las categorias Lago de Coatepeque y Lago de Ilopango se repiten dos veces, probablemente debido a algun espacio adicional
Out[14]:
Sitio de muestreo
Lago de Ilopango        447
Estero de Jaltepeque    440
Laguna de Olomega       434
Barra de Santiago       433
Laguna de Chanmico      427
Lago de Coatepeque      322
Lago de Coatepeque        4
Lago de Ilopango          2
Name: count, dtype: int64
In [15]:
#Reemplazaremos los nombres para unificar los datos

for index, dato in df_nut['Sitio de muestreo'].items(): # Se usa items() para iterar en la serie
  if dato.count("Ilopango") > 0: #condicion 1, que aparezca la palabra "Ilopango"
    df_nut.loc[index, 'Sitio de muestreo'] = "Lago de Ilopango" # Reemplaza el dato
  elif dato.count("Coatepeque") > 0: #condicion 2, que aparezca la palabra "Coatepeque"
    df_nut.loc[index, 'Sitio de muestreo'] = "Lago de Coatepeque" # Reemplaza el dato
  else:
    pass #Si nada se cumple solo sigue adelante con el ciclo, sin modificar nada

df_nut['Sitio de muestreo'].value_counts() #Verificamos si los valores se sustituyeron adecuadamente
Out[15]:
Sitio de muestreo
Lago de Ilopango        449
Estero de Jaltepeque    440
Laguna de Olomega       434
Barra de Santiago       433
Laguna de Chanmico      427
Lago de Coatepeque      326
Name: count, dtype: int64

Tipo de muestra¶

In [16]:
df_nut['Tipo de muestra'].value_counts()
#Se observa una gran discordancia en los datos, ya que solo queremos que se muestren los valores "dulce" o "salobre"
#Sabemos que si dice "marine", "seawater", "Barra" o "Estero", hace referencia a "salobre"
#Y si dice "Lago" o "Laguna" hace referencia a "dulce"
Out[16]:
Tipo de muestra
seawater                 2010
marine                    415
standard                   28
marine                     24
Laguna de Chanmico          8
Laguna de Olomega           8
Lago de Coatepeque          4
Lago de Ilopango            4
Estero de Jaltepeque        4
Barra de Santiago           4
Name: count, dtype: int64
In [17]:
#Nos queda averiguar a que tipo de datos hace referencia "standard"
# Para ello filtramos y observamos solo los datos con esta categoria
df_nut[df_nut['Tipo de muestra'].str.contains('standard')]

#Los datos de sitio de muestreo son solo de "Estero" y "Barra", por lo que "standard" se debera sustituir a "salobre" tambien
Out[17]:
Nº Sitio de muestreo Coordenada Tipo de muestra Fecha Profundidad (m) PO4 abs 880 NH4 abs 640 NO3 abs 540
474 531 Estero de Jaltepeque NaN standard 18-01-2021 20.0 0.004 0.011 0.068
498 559 Estero de Jaltepeque NaN standard 02-02-2021 20.0 0.009 0.007 0.142
594 3.1 Estero de Jaltepeque NaN standard 25-01-2021 20.0 0.016 0.008 0.119
633 688 Barra de Santiago NaN standard 09-03-2021 20.0 0.008 0.011 0.16
693 754 Estero de Jaltepeque NaN standard 30-03-2021 20.0 0.008 0.010 0.095
712 775 Estero de Jaltepeque NaN standard 07-04-2021 0.5 0.000 0.002 0.037
716 779 Barra de Santiago NaN standard 07-04-2021 0.5 0.001 0.002 0.023
741 801 Estero de Jaltepeque NaN standard 12-04-2021 0.5 0.001 0.003 0.006
745 805 Barra de Santiago NaN standard 12-04-2021 0.5 0.001 0.002 0.029
765 828 Estero de Jaltepeque NaN standard 20-04-2021 0.5 0.002 0.001 0.004
769 832 Barra de Santiago NaN standard 20-04-2021 0.5 0.002 0.001 0.002
789 853 Barra de Santiago NaN standard 27-04-2021 0.5 0.001 0.000 0.031
815 878 Estero de Jaltepeque NaN standard 04-05-2021 20.0 0.016 0.007 0.077
819 882 Barra de Santiago NaN standard 04-05-2021 20.0 0.010 0.007 0.105
841 905 Barra de Santiago NaN standard 11-05-2021 0.5 0.001 0.004 0.019
867 938 Barra de Santiago NaN standard 18-05-2021 20.0 0.008 0.010 0.06
885 957 Estero de Jaltepeque NaN standard 25-05-2021 0.5 0.001 0.001 0.026
889 961 Barra de Santiago NaN standard 25-05-2021 0.5 0.002 0.004 0.042
909 982 Estero de Jaltepeque NaN standard 02-06-2021 0.5 0.002 0.002 0.030
913 986 Barra de Santiago NaN standard 02-06-2021 0.5 0.001 0.003 0.036
933 1007 Estero de Jaltepeque NaN standard 08-06-2021 0.5 0.001 0.002 0.004
937 1011 Barra de Santiago NaN standard 08-06-2021 0.5 0.000 0.005 0.018
957 1038 Estero de Jaltepeque NaN standard 15-06-2021 0.5 0.001 0.000 0.010
961 1042 Barra de Santiago NaN standard 15-06-2021 0.5 0.001 0.003 0.008
981 1063 Estero de Jaltepeque NaN standard 21-06-2021 0.5 0.002 0.003 0.036
985 1067 Barra de Santiago NaN standard 21-06-2021 0.5 0.000 0.001 0.032
997 1086 Estero de Jaltepeque NaN standard 29-06-2021 0.5 0.002 0.003 0.024
1001 1090 Barra de Santiago NaN standard 29-06-2021 0.5 0.001 0.004 0.027
In [18]:
#Reemplazaremos los nombres para unificar los datos

for index, dato in df_nut['Sitio de muestreo'].items():
  if dato.count("Barra") or dato.count("Estero") > 0:
    df_nut.loc[index, 'Tipo de muestra'] = "salobre"
  elif dato.count("Lago") or dato.count("Laguna") > 0:
    df_nut.loc[index, 'Tipo de muestra'] = "dulce"
  else:
    pass #Si nada se cumple solo sigue adelante con el ciclo, sin modificar nada

df_nut['Tipo de muestra'].value_counts() #Verificamos si los valores se sustituyeron adecuadamente
Out[18]:
Tipo de muestra
dulce      1636
salobre     873
Name: count, dtype: int64

Agregar datos faltantes (Coordenadas)¶

Se investigaron las coordenadas de los puntos de muestreo para cada sitio:

Lago de Ilopango 13.662765, -89.022671

Estero de Jaltepeque 13.300637, -88.879297

Laguna de Olomega 13.302372, -88.045159

Barra de Santiago 13.695576, -90.006284

Laguna de Chanmico 13.779743, -89.356072

Lago de Coatepeque 13.871775, -89.542288

In [19]:
#Se importa esto para agregar datos de tipo Point (coordenadas)
from shapely.geometry import Point
In [20]:
#Se crean variables para almacenar cada punto con el formato adecuado
ilopango = Point(-89.022671, 13.662765)
coatepeque = Point(-89.542288, 13.871775)
chanmico = Point(-89.356072, 13.779743)
santiago = Point(-89.542288, 13.871775)
jaltepeque = Point(-88.879297, 13.300637)
olomega = Point(-88.045159, 13.302372)
In [21]:
for index, dato in df_nut['Sitio de muestreo'].items(): # Se usa items() para iterar en la serie

  if dato.count("Ilopango") > 0: #condicion 1, que aparezca la palabra "Ilopango"
    df_nut.loc[index, 'Coordenada'] = ilopango # Agrega el dato

  elif dato.count("Coatepeque") > 0:
   df_nut.loc[index, 'Coordenada'] = coatepeque

  elif dato.count("Jaltepeque") > 0:
    df_nut.loc[index, 'Coordenada'] = jaltepeque

  elif dato.count("Chanmico") > 0:
    df_nut.loc[index, 'Coordenada'] = chanmico

  elif dato.count("Santiago") > 0:
    df_nut.loc[index, 'Coordenada'] = santiago

  elif dato.count("Olomega") > 0:
    df_nut.loc[index, 'Coordenada'] = olomega
  else:
    pass #Si nada se cumple solo sigue adelante con el ciclo, sin modificar nada

df_nut['Coordenada'] #Verificamos si los valores se agregaron adecuadamente
Out[21]:
0       POINT (-89.542288 13.871775)
1       POINT (-89.542288 13.871775)
2       POINT (-89.542288 13.871775)
3       POINT (-89.542288 13.871775)
4       POINT (-89.022671 13.662765)
                    ...             
2514    POINT (-89.356072 13.779743)
2515    POINT (-88.045159 13.302372)
2516    POINT (-88.045159 13.302372)
2517    POINT (-88.045159 13.302372)
2518    POINT (-88.045159 13.302372)
Name: Coordenada, Length: 2509, dtype: object

Visualización en mapa¶

In [22]:
# Creación de GeoDataFrame
gdf_nut = gpd.GeoDataFrame(df_nut, geometry='Coordenada', crs = 'EPSG:4326')
In [23]:
# Datasets disponibles en GeoPandas
gpd.datasets.available
Out[23]:
['naturalearth_cities', 'naturalearth_lowres', 'nybb']
In [24]:
# Acceder a la data de Natural Earth via GeoPandas
world = gpd.read_file(gpd.datasets.get_path('naturalearth_lowres'))
country = "El Salvador"
el_salvador = world[world.name == country]

# Mapa El Salvador
fig, ax = plt.subplots(figsize=(20,20))
el_salvador.plot(ax=ax, color='white', edgecolor='black')

# Colocar puntos sobre el mapa base
gdf_nut.plot(ax=ax, marker='o', color='red', markersize=50);

# Colocar los puntos de 'Sitio de muestreo'
for x, y, label in zip(gdf_nut['Coordenada'].x, gdf_nut['Coordenada'].y, gdf_nut['Sitio de muestreo']):
    ax.text(x, y, label, fontsize=15, ha='right')

plt.title('Puntos de muestreo')
plt.xlabel('Longitud')
plt.ylabel('Latitud')
plt.show()
<ipython-input-24-4aa393045261>:2: FutureWarning: The geopandas.dataset module is deprecated and will be removed in GeoPandas 1.0. You can get the original 'naturalearth_lowres' data from https://www.naturalearthdata.com/downloads/110m-cultural-vectors/.
  world = gpd.read_file(gpd.datasets.get_path('naturalearth_lowres'))
No description has been provided for this image

Gráficos¶

Gráfico de dispersión¶

In [25]:
#Asegurarnos que todos los datos en fecha sean en el formato de fecha
df_nut['Fecha'] = pd.to_datetime(df_nut['Fecha'], errors='coerce')

# Asegurarnos que las columnas es los valores de nutrientes sean numericos
df_nut['PO4 abs 880'] = pd.to_numeric(df_nut['PO4 abs 880'], errors='coerce')
df_nut['NH4 abs 640'] = pd.to_numeric(df_nut['NH4 abs 640'], errors='coerce')
df_nut['NO3 abs 540'] = pd.to_numeric(df_nut['NO3 abs 540'], errors='coerce')

#Con el formato de fecha correcto, utilizar solo fecha y año
df_nut['Año y Mes'] = df_nut['Fecha'].dt.to_period('M')

#Calcular la media mensual de cada nutriente en cada profundidad
media_mensual = df_nut.groupby(['Año y Mes', 'Profundidad (m)']).agg({
    'PO4 abs 880': 'mean',
    'NH4 abs 640': 'mean',
    'NO3 abs 540': 'mean'
}).reset_index()

#Imprimir la media mensual
print(media_mensual)
   Año y Mes  Profundidad (m)  PO4 abs 880  NH4 abs 640  NO3 abs 540
0    2020-02              0.5     0.000917     0.002000     0.022167
1    2020-02             20.0     0.009917     0.007833     0.101500
2    2020-03              0.5     0.000969     0.002875     0.019531
3    2020-03             20.0     0.010367     0.008700     0.087500
4    2020-09              0.5     0.001029     0.002400     0.019743
5    2020-09             20.0     0.009853     0.008412     0.102794
6    2020-10              0.5     0.000854     0.002500     0.022000
7    2020-10             20.0     0.010229     0.008917     0.113667
8    2020-11              0.5     0.000979     0.002750     0.021063
9    2020-11             20.0     0.009723     0.008426     0.105638
10   2020-12              0.5     0.000941     0.002441     0.021382
11   2020-12             20.0     0.008812     0.008469     0.100969
12   2021-01              0.5     0.001022     0.002400     0.018333
13   2021-01             20.0     0.009911     0.008511     0.107800
14   2021-02              0.5     0.000979     0.002458     0.019854
15   2021-02             20.0     0.010023     0.008227     0.101909
16   2021-03              0.5     0.000885     0.002481     0.021038
17   2021-03             20.0     0.010680     0.008800     0.100300
18   2021-04              0.5     0.001022     0.002565     0.021043
19   2021-04             20.0     0.010717     0.008500     0.109239
20   2021-05              0.5     0.001125     0.002583     0.023000
21   2021-05             20.0     0.009229     0.008125     0.103167
22   2021-06              0.5     0.001036     0.002571     0.021054
23   2021-06             20.0     0.010107     0.008482     0.105696
24   2021-07              0.5     0.001022     0.002457     0.021283
25   2021-07             20.0     0.011043     0.008630     0.111217
26   2021-08              0.5     0.001022     0.002478     0.021478
27   2021-08             20.0     0.011159     0.008364     0.106295
28   2021-09              0.5     0.000831     0.002373     0.018559
29   2021-09             20.0     0.009571     0.008536     0.105911
30   2021-10              0.5     0.001000     0.002396     0.017437
31   2021-10             20.0     0.010688     0.008625     0.109938
32   2021-11              0.5     0.000929     0.002393     0.021250
33   2021-11             20.0     0.009607     0.008446     0.102321
34   2021-12              0.5     0.001021     0.002271     0.020667
35   2021-12             20.0     0.009917     0.008625     0.105979
36   2022-01              0.5     0.001179     0.002661     0.019946
37   2022-01             20.0     0.011339     0.008696     0.107571
38   2022-02              0.5     0.001143     0.002214     0.020167
39   2022-02             20.0     0.010318     0.008636     0.112341
40   2022-03              0.5     0.000848     0.002478     0.021870
41   2022-03             20.0     0.010047     0.008581     0.112070
42   2022-04              0.5     0.000864     0.002659     0.021182
43   2022-04             20.0     0.010636     0.008841     0.104000
44   2022-05              0.5     0.001103     0.002828     0.020293
45   2022-05             20.0     0.010088     0.008351     0.106544
46   2022-06              0.5     0.000971     0.002412     0.020971
47   2022-06             20.0     0.009676     0.008912     0.098912
48   2022-07              0.5     0.001048     0.002286     0.020548
49   2022-07             20.0     0.008525     0.008600     0.105750
50   2022-08              0.5     0.001000     0.002500     0.021776
51   2022-08             20.0     0.009724     0.008483     0.104931
52   2022-09              0.5     0.000891     0.002261     0.019565
53   2022-09             20.0     0.011217     0.008587     0.104522
54   2022-10              0.5     0.000958     0.002583     0.020333
55   2022-10             20.0     0.011250     0.008375     0.109542
<ipython-input-25-55830d63c520>:2: UserWarning: Parsing dates in %d-%m-%Y format when dayfirst=False (the default) was specified. Pass `dayfirst=True` or specify a format to silence this warning.
  df_nut['Fecha'] = pd.to_datetime(df_nut['Fecha'], errors='coerce')
In [26]:
#Gráfico de dispersión: promedio por mes del valor de cada nutriente dependiendo la profundidad.

# Tamaño del grafico
grafico_dispersion = plt.figure(figsize=(15, 8))

# PO4 abs 880 grafico
plt.subplot(3, 1, 1)
ax = sns.scatterplot(data=media_mensual, x='PO4 abs 880', y='Profundidad (m)', hue='Año y Mes', palette='viridis')
plt.title('Media mensual PO4 Abs 880 vs Profundidad')
#Agregar las etiquetas de los ejes
ax.set(xlabel='PO4 abs 880', ylabel='Profundidad (m)')
ax.legend(title='Año y Mes', bbox_to_anchor=(1.05, 1), loc='upper left')


# NH4 abs 640 gráfico
plt.subplot(3, 1, 2)
ax = sns.scatterplot(data=media_mensual, x='NH4 abs 640', y='Profundidad (m)', hue='Año y Mes', palette='viridis')
plt.title('Media mensual NH4 Abs 640 vs Profundidad')
# Agregar las etiquetas de los ejes
ax.set(xlabel='NH4 abs 640', ylabel='Profundidad (m)')
ax.legend().remove() #sin leyenda porque es la misma para todos


# NO3 abs 540 gráfico
plt.subplot(3, 1, 3)
ax = sns.scatterplot(data=media_mensual, x='NO3 abs 540', y='Profundidad (m)', hue='Año y Mes', palette='viridis')
plt.title('Media mensual NO3 Abs 540 vs Profundidad')
# Agregar las etiquetas de los ejes
ax.set(xlabel='NO3 Abs 540' , ylabel='Profundidad (m)')
ax.legend().remove()

plt.tight_layout()
plt.subplots_adjust(right=0.75, hspace=0.8)  # Ajuste de margen y espacio entre cada subplot

plt.show()
<ipython-input-26-ba0110701a97>:32: UserWarning: Tight layout not applied. tight_layout cannot make axes height small enough to accommodate all axes decorations.
  plt.tight_layout()
No description has been provided for this image

Gráfico de barras¶

In [27]:
#Gráfico de barras
#promedio de concentraciones x año, pero tendrían que ser los mismos meses, es decir de febrero a octubre del 2020, 2021 y 2022 porque el 2020 no tiene enero creo y el 2022 no tiene nov ni dic

#Con el formato de fecha correcto, utilizar solo fecha y año
df_nut['Año'] = df_nut['Fecha'].dt.to_period('Y')


#Calcular la media mensual de cada nutriente en cada profundidad
media_anual = df_nut.groupby(['Año', 'Profundidad (m)']).agg({
    'PO4 abs 880': 'mean',
    'NH4 abs 640': 'mean',
    'NO3 abs 540': 'mean'
}).reset_index()

print(media_anual)
    Año  Profundidad (m)  PO4 abs 880  NH4 abs 640  NO3 abs 540
0  2020              0.5     0.000947     0.002560     0.020938
1  2020             20.0     0.009828     0.008552     0.103399
2  2021              0.5     0.000987     0.002452     0.020408
3  2021             20.0     0.010196     0.008492     0.105731
4  2022              0.5     0.001009     0.002502     0.020680
5  2022             20.0     0.010278     0.008601     0.106617
In [28]:
#Filtro de profundidad
profundidad_05 = media_anual[media_anual['Profundidad (m)'] == 0.5]
profundidad_20 = media_anual[media_anual['Profundidad (m)'] == 20]

#Gráfico
fig, axes = plt.subplots(2, 1, figsize=(10, 8), sharex=True)

#Gráfico para el de 0.5m de profundidad
profundidad_05.plot(kind='bar', x='Año', y=['PO4 abs 880', 'NH4 abs 640', 'NO3 abs 540'], ax=axes[0], colormap='flare')
axes[0].set_title('Media de niveles anuales de los nutrientes a 0.5 m de profundidad')
axes[0].set_ylabel('Concentración media')
axes[0].legend(title='Nutriente')

#Gráfico para el de 20m de profundidad
profundidad_20.plot(kind='bar', x='Año', y=['PO4 abs 880', 'NH4 abs 640', 'NO3 abs 540'], ax=axes[1], colormap='flare')
axes[1].set_title('Media de niveles anuales de los nutrientes a 20 m de profundidad')
axes[1].set_xlabel('Año')
axes[1].set_ylabel('Concentración media')
axes[1].legend(title='Nutriente')

plt.tight_layout()
plt.show()
No description has been provided for this image

Mapa de calor¶

In [29]:
#Mapa de calor comparando correlación de los nutrientes

# Columnas a evaluar
columnas_nutrientes = ['NO3 abs 540', 'NH4 abs 640', 'PO4 abs 880']
df_corr = df_nut[columnas_nutrientes].apply(pd.to_numeric, errors='coerce')

#Correlación
correlacion_mapa = df_corr.corr()

# Plotting the heatmap
plt.figure(figsize=(8, 6))
sns.heatmap(correlacion_mapa, annot=True, cmap='coolwarm', vmin=-1, vmax=1)
plt.title('Mapa de calor de la correlación de NO3, NH4, y PO4 abs')
plt.show()
No description has been provided for this image

Analisis de datos¶

Informe con pandas-profiling¶

In [30]:
# Creamos un informe con pandas-profiling
nombre = "Muestreo y evaluación de nutrientes en lagos de El Salvador"
profile = ProfileReport(df_nut, title=nombre, explorative=True)

# Mostrar el informe en un notebook (si estás usando Jupyter o similares)
profile.to_notebook_iframe()
Summarize dataset:   0%|          | 0/5 [00:00<?, ?it/s]
Generate report structure:   0%|          | 0/1 [00:00<?, ?it/s]
Render HTML:   0%|          | 0/1 [00:00<?, ?it/s]

Cálculo de concentraciones de las muestras

In [31]:
#Utilizaremos la lectura de archivo xlsx porque el documento de los estándares posee dos hojas

file = '/content/drive/MyDrive/Proyecto Python/Standards.xlsx'

# Leer el archivo Excel y convertirlo a DataFrame
dfcurv = pd.read_excel(file, sheet_name = "Curva cal")
In [32]:
#Se hará uso de scikit-learn para poder obtener los datos de regresión lineal que nos permitirá obtener las concentraciones de las muestras

nut = ["PO4 abs 880", "NH4 abs 640", "NO3 abs 540"]
std = ["PO4 Conc uM", "NH4 Conc uM", "NO3 Conc uM"]

estad = [] #Será lista para guardar los estadísticos

fig, axs = plt.subplots(1, 3, figsize=(18, 6)) #Creamos subplots para la gráfica

# Regresión lineal para cada conjunto de datos y los gráficos
for i, (X_col, Y_col) in enumerate(zip(std, nut)):
    X = dfcurv[[X_col]]
    Y = dfcurv[Y_col]

    # Modelo de regresión lineal
    model = LinearRegression()

    # Ajustar el modelo con los datos
    model.fit(X, Y)

    # Obtener la pendiente y la intersección
    pend = model.coef_[0]
    inter = model.intercept_

    # Predecir los valores de Y usando el modelo ajustado
    Y_pred = model.predict(X)

    # Calcular el coeficiente de determinación (R^2)
    # para observar si este es lo suficientemente alto para calcular las concentraciones
    r2 = r2_score(Y, Y_pred)

    # Almacenar los resultados
    estad.append({
        'X_column': X_col,
        'Y_column': Y_col,
        'Pendiente (Coeficiente)': pend,
        'Intersección': inter,
        'R^2': r2
    })

    # Graficar los datos y la línea de regresión
    axs[i].scatter(X, Y, color='blue', label='Datos')
    axs[i].plot(X, Y_pred, color='red', label='Regresión Lineal')
    axs[i].set_title(f'{X_col} vs {Y_col}\n$R^2$={r2:.2f}')
    axs[i].set_xlabel(X_col)
    axs[i].set_ylabel(Y_col)
    axs[i].legend()

plt.tight_layout() #Ajuste de la gráfica
plt.show()
No description has been provided for this image
In [33]:
#Ahora vamos a utilizar el intercepto y la pendiente para calcular las concentraciones

calnut = ["PO4 abs 880", "NH4 abs 640", "NO3 abs 540"]
concnut = ["uM PO4/L", "uM NH4/L", "uM NO3/L"]

#Vamos a convertir las columnas en formato numérico para que pueda ejecutarse la fórmula

df_nut['PO4 abs 880'] = pd.to_numeric(df_nut['PO4 abs 880'], errors='coerce')
df_nut['NH4 abs 640'] = pd.to_numeric(df_nut['NH4 abs 640'], errors='coerce')
df_nut['NO3 abs 540'] = pd.to_numeric(df_nut['NO3 abs 540'], errors='coerce')

for i, (new_col, res_col) in enumerate(zip(calnut, concnut)):
    slope = estad[i]['Pendiente (Coeficiente)']
    intercept = estad[i]['Intersección']
    df_nut[res_col] = (df_nut[new_col] - intercept) / slope

# Mostrar el nuevo DataFrame actualizado
print(df_nut)
        Nº   Sitio de muestreo                    Coordenada Tipo de muestra  \
0        1  Lago de Coatepeque  POINT (-89.542288 13.871775)           dulce   
1        2  Lago de Coatepeque  POINT (-89.542288 13.871775)           dulce   
2        3  Lago de Coatepeque  POINT (-89.542288 13.871775)           dulce   
3        4  Lago de Coatepeque  POINT (-89.542288 13.871775)           dulce   
4        5    Lago de Ilopango  POINT (-89.022671 13.662765)           dulce   
...    ...                 ...                           ...             ...   
2514  2899  Laguna de Chanmico  POINT (-89.356072 13.779743)           dulce   
2515  2900   Laguna de Olomega  POINT (-88.045159 13.302372)           dulce   
2516  2901   Laguna de Olomega  POINT (-88.045159 13.302372)           dulce   
2517  2902   Laguna de Olomega  POINT (-88.045159 13.302372)           dulce   
2518  2903   Laguna de Olomega  POINT (-88.045159 13.302372)           dulce   

          Fecha  Profundidad (m)  PO4 abs 880  NH4 abs 640  NO3 abs 540  \
0    2020-02-24              0.5        0.002        0.000        0.017   
1    2020-02-24              0.5        0.002        0.000        0.021   
2    2020-02-24             20.0        0.003        0.006        0.045   
3    2020-02-24             20.0        0.011        0.010        0.136   
4    2020-02-24              0.5        0.000        0.000        0.019   
...         ...              ...          ...          ...          ...   
2514 2022-10-12             20.0        0.012        0.008        0.071   
2515 2022-10-12              0.5        0.001        0.003        0.029   
2516 2022-10-12              0.5        0.001        0.002        0.007   
2517 2022-10-12             20.0        0.015        0.008        0.141   
2518 2022-10-12             20.0        0.006        0.009        0.156   

     Año y Mes   Año  uM PO4/L  uM NH4/L  uM NO3/L  
0      2020-02  2020  0.071881 -0.137405  0.883725  
1      2020-02  2020  0.071881 -0.137405  0.962113  
2      2020-02  2020  0.122567  0.183206  1.432437  
3      2020-02  2020  0.528052  0.396947  3.215752  
4      2020-02  2020 -0.029490 -0.137405  0.922919  
...        ...   ...       ...       ...       ...  
2514   2022-10  2022  0.578737  0.290076  1.941956  
2515   2022-10  2022  0.021195  0.022901  1.118888  
2516   2022-10  2022  0.021195 -0.030534  0.687757  
2517   2022-10  2022  0.730794  0.290076  3.313736  
2518   2022-10  2022  0.274624  0.343511  3.607689  

[2509 rows x 14 columns]
In [34]:
#Al haber calculado las concentraciones, realizaremos la validación de datos al obtener los límites de detección y cuantificación
#Esto hará que nuestros datos sean más certeros para nuestras conclusiones

file = '/content/drive/MyDrive/Proyecto Python/Standards.xlsx'

# Leer el archivo Excel y convertirlo a DataFrame
dflim = pd.read_excel(file, sheet_name = "Limites")

dflim
Out[34]:
NH4 std NH4 Conc uM NH4 abs 640 NO3 std NO3 Conc uM NO3 abs 540 PO4 std PO4 Conc uM PO4 abs 880
0 1 0.0 0.003 1 0.0 0.007 1 0.0 0.001
1 2 1.0 0.021 2 2.5 0.083 2 0.1 0.002
2 3 2.5 0.049 3 5.0 0.145 3 0.5 0.009
3 4 5.0 0.096 4 7.5 0.312 4 1.0 0.019
4 5 7.5 0.144 5 10.0 0.426 5 5.0 0.098
5 6 10.0 0.196 6 12.5 0.514 6 10.0 0.198
6 7 12.5 0.242 7 15.0 0.598 7 20.0 0.382
7 8 15.0 0.289 8 17.5 0.667 8 30.0 0.571
8 9 17.5 0.346 9 20.0 0.742 9 40.0 0.704
9 10 20.0 0.397 10 25.0 0.873 10 50.0 0.853
In [35]:
#Listas con los nombres de las columnas a utilizar
conc = ["NH4 Conc uM", "NO3 Conc uM", "PO4 Conc uM"]
abs = ["NH4 abs 640", "NO3 abs 540", "PO4 abs 880"]

#Variable para almacenar el límite

lim =[]

for conc_col, abs_col in zip(conc, abs):
    X = dflim[[conc_col]]
    y = dflim[abs_col]

    # Crear y ajustar el modelo de regresión lineal
    model = LinearRegression()
    model.fit(X, y)

    # Obtener la pendiente (slope) y la intersección (intercept) de la regresión lineal
    slope = model.coef_[0]
    intercept = model.intercept_

    # Señal del blanco y su desviación estándar
    signal_blank = y[X[conc_col] == 0].values[0]
    std_blank = y[X[conc_col] == 0].std()

    # Factores para LOD y LOQ
    k_lod = 3
    k_loq = 10

    # Cálculo del LOD y LOQ
    lod = (k_lod * std_blank) / slope
    loq = (k_loq * std_blank) / slope

    # Guardar resultados
    lim.append({
        'Concentration': conc_col,
        'Signal': abs_col,
        'LOD': lod,
        'LOQ': loq
    })

    # Creación del gráfico
    plt.figure(figsize=(10, 6))
    plt.scatter(X, y, color='blue', label='Datos')
    plt.plot(X, intercept + slope * X, 'r', label='Línea de regresión')
    plt.axhline(y=signal_blank, color='gray', linestyle='--', label='Señal en blanco')
    plt.xlabel('Concentración')
    plt.ylabel('Señal')
    plt.legend()
    plt.title(f'Relación entre {conc_col} y {abs_col}')
    plt.grid(True)
    plt.show()

# Convertir resultados a DataFrame para visualización
results_df = pd.DataFrame(lim)

print(results_df)
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
  Concentration       Signal  LOD  LOQ
0   NH4 Conc uM  NH4 abs 640  NaN  NaN
1   NO3 Conc uM  NO3 abs 540  NaN  NaN
2   PO4 Conc uM  PO4 abs 880  NaN  NaN

Descripción de los datos obtenidos¶

La base de datos originalmente presentaba varios errores y datos faltantes, que dificultarían el analisis inmediato de los mismos, luego del preprocesamiento se obtuvo dataframe limpio y corregido con el cual se pudo explorar la relación entre los nutrientes estudiados, sus sitios de muestreo y la profundidad. Además de analizar su variación a lo largo de los años de muestreo y calcular su concentración.

Conclusiones¶

A lo largo de los años en que se realizó el monitoreo, no se evidencia un cambio significativo en la concentración de los nutrientes medidos en los sitios de muestreo. Los registros a mayor profundidad (20m) presentaron mayores concentraciones para los tres tipos de nutrientes analizados (PO4, NH3, NH4) los cuales presentaron una correlación positiva entre ellos (mayor a 0.70), lo que quiere decir que si se da un aumento en la concentración de uno de los nutrientes, los otros también aumentarán.

Referencias¶

  1. EPA. (2021, Julio). Factsheet on water quality parameters. EPA. https://www.epa.gov/system/files/documents/2021-07/parameter-factsheet_nutrients.pdf

  2. Sense Kraken. (2023, noviembre 28). The Power of Data: Using Analytics for Water Quality Monitoring and Surveillance. Linkedin. https://www.linkedin.com/pulse/power-data-using-analytics-water-quality-monitoring-surveillance-

  3. Discover Data Science. (s.f). How Is Data Science Being Used to Tackle the Global Problem of Clean Water?. Discover Data Science. https://www.discoverdatascience.org/social-good/clean-water/

  4. Castrillo, Maria & Lopez Garcia, Alvaro. (2020). Estimation of high frequency nutrient concentrations from water quality surrogates using machine learning methods. https://www.researchgate.net/publication/338853046_Estimation_of_high_frequency_nutrient_concentrations_from_water_quality_surrogates_using_machine_learning_methods

  5. Yan, Xiaohui, Tianqi Zhang, Wenying Du, Qingjia Meng, Xinghan Xu, and Xiang Zhao. 2024. "A Comprehensive Review of Machine Learning for Water Quality Prediction over the Past Five Years" Journal of Marine Science and Engineering 12, no. 1: 159. https://doi.org/10.3390/jmse12010159