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¶
#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
#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
#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
# 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:¶
#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]
# 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]
#Verificamos que los datos no tengan anormalidades
df_nut.count()
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
#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¶
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
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
#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
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
#revision de datos faltantes en columna Fecha
df_nut["Fecha"].value_counts(dropna=False)
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
#revision de datos faltantes en columna Profundidad
df_nut["Profundidad (m)"].value_counts(dropna=False)
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
#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)
#Se verifica que ya no haya datos faltantes
df_nut.isna().sum()
#Una vez borrados los datos nos quedamos con 2509 registros
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¶
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
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
#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
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¶
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"
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
#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
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 |
#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
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
#Se importa esto para agregar datos de tipo Point (coordenadas)
from shapely.geometry import Point
#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)
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
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¶
# Creación de GeoDataFrame
gdf_nut = gpd.GeoDataFrame(df_nut, geometry='Coordenada', crs = 'EPSG:4326')
# Datasets disponibles en GeoPandas
gpd.datasets.available
['naturalearth_cities', 'naturalearth_lowres', 'nybb']
# 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'))
Gráficos¶
Gráfico de dispersión¶
#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')
#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()
Gráfico de barras¶
#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
#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()
Mapa de calor¶
#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()
Analisis de datos¶
Informe con pandas-profiling¶
# 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
#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")
#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()
#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]
#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
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 |
#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)
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¶
EPA. (2021, Julio). Factsheet on water quality parameters. EPA. https://www.epa.gov/system/files/documents/2021-07/parameter-factsheet_nutrients.pdf
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-
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/
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
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