Documento complementario del informe final
2026-05-25
Este documento complementario reúne cinco anexos del proyecto Pipeline multilenguaje GeoAI para el monitoreo del manglar en la Ciénaga Grande de Santa Marta (2013–2025). Cada anexo desarrolla material de soporte que sustenta las decisiones metodológicas, la reproducibilidad y la trazabilidad de los resultados reportados en el informe principal (informe_final_revisado.docx):
sig_unal v1.11 con los paquetes adicionales de Python, R y Julia.(Nota: el Anexo D se conserva sin contenido para mantener la correspondencia con versiones previas y evitar modificar las referencias internas.)
El proyecto se ejecuta sobre el contenedor Docker sig_unal v1.11 proporcionado para la asignatura, con las siguientes modificaciones:
# Librerías Python adicionales instaladas
pip install earthengine-api geemap segment-geospatial leafmap
pip install rasterio geopandas xarray shapely folium rasterstats
pip install matplotlib pandas numpy scikit-learn contextily
pip install cdsapi netCDF4 # descarga ERA5-Land desde CDS
# Autenticación Google Earth Engine
earthengine authenticate --auth_mode=notebook
# Autenticación Climate Data Store (ECMWF) — para ERA5-Land
# 1) Crear cuenta en https://cds.climate.copernicus.eu/
# 2) Aceptar términos del conjunto de datos reanalysis-era5-land-monthly-means
# 3) Crear archivo ~/.cdsapirc con la URL y la API key personales
# Paquetes R adicionales
install.packages(c("bfast", "terra", "sf", "ggplot2", "tseries",
"stars", "dplyr", "tidyr", "readr", "stringr"))
# Paquetes Julia adicionales
using Pkg
Pkg.add(["GeoJSON", "DataFrames", "CSV", "Statistics"])
La autenticación con GEE se realiza mediante el flujo gcloud auth application-default login con quota project configurado, que genera credenciales almacenadas en ~/.config/gcloud/application_default_credentials.json. El contenedor requiere un mínimo de 8 GB de RAM asignados en Docker Desktop para ejecutar SamGeo con el backbone vit_b; con vit_h se requieren al menos 12 GB. Las composiciones RGB se remuestrean de 10 a 30 metros antes de la segmentación para reducir el consumo de memoria.
El repositorio público está disponible en https://github.com/linaq11/proyecto-cgsm-curso bajo licencia MIT. La versión anterior del proyecto (línea base marzo 2026 sobre AOI preliminar de 5.073 km²) permanece archivada en https://github.com/LinaQuinteroF/proyecto-cgsm para fines de comparación histórica.
proyecto-cgsm-curso/
├── notebooks/ 30 notebooks: 23 serie principal + 7 legacy (trazabilidad)
│ orden de ejecución: 01 → 02 → 02b → 03_acotado →
│ 04_acotado → 04b_acotado → 05_acotado → 06b → 07 → 08 →
│ 09b → 10 → 11 → 11b → 12 → 12b → 12c → 13 → 14
├── src/
│ ├── python/ utils.py (9377 + DE-9IM), aoi_acotado.py, build_cubos.py,
│ │ make_dashboard_html.py, alertas_manglar.py, merge_cubo.py
│ ├── R/ 03_bfast_ndvi.R, 05_stars_cubo.R, 06_bfast_manglar_unificado.R,
│ │ 08_correlacion_trilingual.R
│ └── julia/ 04_fragmentacion.jl (4326 legacy + 9377 vigente),
│ 05_correlacion_trilingual.jl
├── data/
│ ├── raw/ AOI preliminar y acotado, RUNAP SFF + VPI, INVEMAR GBIF,
│ │ IDEAM El Banco / Ganadería Caribe
│ └── processed/
│ ├── cubo/ 3 datacubes NetCDF CF-1.8 (40 / 275 / 119 MB)
│ └── samgeo/ máscaras y GeoJSON por periodo
├── outputs/
│ ├── tables/ 47 CSV con resultados numéricos
│ ├── figures/ 35 PNG (mapas, series, correlaciones, alertas)
│ └── maps/ dashboard_CGSM_final.html
└── docs/ informe_final.{qmd,html,pdf}, informe_anexos.{qmd,html,pdf}
La serie vigente (notebooks 01–12 + 02b, 04b/c_acotado, 09b, 11b, 12b/c, 14) reproduce las cifras del cuerpo del informe sobre el AOI acotado de 835 km². La serie legacy (03, 04, 04b, 05, 06, 09 sin sufijo _acotado) opera sobre el AOI preliminar de 5.073 km² y se conserva sólo por trazabilidad histórica; no sostiene ninguna conclusión del informe. La guía de ejecución por etapa con dependencias entre notebooks se documenta en notebooks/README.md. Las composiciones trimestrales y anuales en GeoTIFF y los NetCDF de mayor tamaño no se versionan por restricciones de tamaño de GitHub: se regeneran ejecutando los notebooks en orden secuencial con acceso autenticado a GEE.
La diferencia entre los conteos de parches en Julia y Python se debe al tratamiento geométrico de polígonos con anillos interiores y geometrías multipartes. Mientras geopandas calcula el área neta descontando huecos internos, la implementación en Julia se utilizó para caracterizar unidades geométricas de paisaje y métricas de conectividad. Por esta razón, el informe distingue explícitamente entre conteos derivados de Julia (Tabla 5 del cuerpo del informe) para métricas de forma y aislamiento, y salidas geopandas (en outputs/tables/parches_geopandas.csv del repositorio) para indicadores de superficie efectiva.
Para mantener el cuerpo del informe centrado en la interpretación de resultados, los fragmentos de código que materializan cada fase del pipeline se documentan en este anexo, organizados temáticamente en cuatro apartados que reproducen el orden lógico de la cadena de procesamiento: datacube y series temporales (E.1), reproyección y topología sobre parches segmentados (E.2), forzamiento climático y detección SAR (E.3), y métricas de fragmentación del paisaje en Julia (E.4). Los conceptos técnicos invocados aquí ,datacube, z-score, bfast, SamGeo, predicado DE-9IM, Random Forest, backscatter SAR, dispersión de doble rebote, F1-score, se introducen y explican funcionalmente en el cuerpo del informe_final_revisado.docx; este anexo se concentra en la implementación operativa y supone que el lector conoce ya su significado conceptual.
Nota: los fragmentos incluidos son extractos abreviados para documentación metodológica. La versión ejecutable completa se encuentra en los notebooks y scripts del repositorio.
El datacube trimestral materializado como NetCDF CF-1.8 se abre con evaluación perezosa mediante xarray.open_dataset con argumento chunks, de modo que las operaciones de selección y reducción se planifican como un grafo de cómputo dask y solo se materializan los píxeles necesarios al invocar .compute(). Este patrón resulta indispensable para los tres archivos del proyecto (40 MB, 275 MB y 119 MB) cuando se trabaja sobre la totalidad de las 31 láminas trimestrales sin saturar la memoria del contenedor.
import xarray as xr
ds = xr.open_dataset('data/processed/cubo/cgsm_datacube_trimestral.nc',
chunks={'time': 1, 'x': 512, 'y': 512})
# Atributos CF-1.8: Conventions, institution, creator_name, crs_origen, ...
# Acceso perezoso por chunks; .compute() materializa cuando es necesario
ndvi_serie = ds['reflectance'].sel(band_idx='NDVI').median(dim=['x','y']).compute()
Sobre la serie combinada Landsat 8 + Sentinel-2 se calcula el z-score temporal por estación mediante groupby().transform(), lo que permite identificar anomalías pronunciadas como aquellas con z_score < −2. El resultado alimenta tanto la tabla de eventos de mortandad como la lógica del módulo de alertas tempranas.
# Z-scores y detección de anomalias por estación
df['z_score'] = df.groupby('estación')['ndvi'].transform(
lambda x: (x - x.mean()) / x.std())
anomalias = df[df['z_score'] < -2].sort_values('z_score')
stars en R y extracción sobre estaciones INVEMARComo contraparte local del flujo Python+GEE de la Fase 2, los TIFs trimestrales descargados desde GEE se ensamblan en R como un cubo stars proxy —con evaluación perezosa hasta que st_extract materializa los valores sobre las ocho estaciones INVEMAR—. La salida series_temporales_stars.csv se contrasta contra serie_temporal_ndvi_definitiva.csv para sostener la validación cruzada multilingüe Python ↔︎ R reportada en la sección homónima.
library(stars); library(sf); library(dplyr)
tifs <- list.files("data/processed/s2", pattern = "NDVI.*\\.tif$", full.names = TRUE)
fechas <- as.Date(stringr::str_match(basename(tifs), "(\\d{4})_Q(\\d)")[, c(2, 3)])
cubo <- read_stars(tifs, along = list(time = fechas), proxy = TRUE)
serie <- st_extract(cubo, st_transform(estaciones_sf, st_crs(cubo)))
La colección óptica del proyecto se arma en Google Earth Engine filtrando COPERNICUS/S2_SR_HARMONIZED sobre el AOI, aplicando una máscara de nubes basada en la banda QA60 y añadiendo en una sola pasada los tres índices espectrales que sostienen el resto del pipeline —NDVI, NDWI y CMRI—.
# Construir coleccion S2 SR Harmonized y agregar tres indices espectrales
def add_indices(image):
ndvi = image.normalizedDifference(['B8', 'B4']).rename('NDVI')
ndwi = image.normalizedDifference(['B3', 'B8']).rename('NDWI')
cmri = ndvi.subtract(ndwi).rename('CMRI')
return image.addBands([ndvi, ndwi, cmri])
s2 = (ee.ImageCollection('COPERNICUS/S2_SR_HARMONIZED')
.filterBounds(aoi)
.filterDate('2018-01-01', '2025-12-31')
.filter(ee.Filter.lt('CLOUDY_PIXEL_PERCENTAGE', 20))
.map(mask_s2_clouds)
.map(add_indices))
Sobre las series mensuales combinadas Landsat 8 + Sentinel-2 (929 observaciones) se aplica el algoritmo bfast::bfast con h = 0,10 y descomposición armónica en dos iteraciones, lo que arroja los quiebres asociados a El Niño 2015–2016, La Niña 2020–2021 y El Niño 2023–2024.
library(bfast)
serie <- read.csv("outputs/tables/serie_temporal_ndvi_combinada.csv")
ts_est <- ts(subset(serie, estación == "Cano_Palos")$ndvi,
start = c(2013, 1), frequency = 12)
fit <- bfast(ts_est, h = 0.10, season = "harmonic", max.iter = 2)
plot(fit) # quiebres 2016 (El Nino), 2020 (La Nina), 2023-24 (El Nino)
Sobre las mismas series mensuales combinadas Landsat 8 + Sentinel-2 se aplica bfast::bfastmonitor con una ventana histórica 2013–2019 y un periodo de monitoreo 2020–2025. El modelo ajusta una descomposición armónica con harmon de orden 3 sobre la ventana histórica, considerada estable tras la rehabilitación hidráulica de canales (1996–1998), y evalúa el periodo de monitoreo en busca de breakpoints al nivel de significancia p < 0,05. La salida outputs/tables/bfastmonitor_estaciones.csv contiene, por estación, la fecha del breakpoint detectado, su magnitud y el estado del monitor a la fecha de corte, y alimenta directamente la lógica del semáforo descrita en la sección homónima del cuerpo del informe.
library(bfast)
serie <- read.csv("outputs/tables/serie_temporal_ndvi_combinada.csv")
# Por estación, ajustar bfastmonitor con history 2013-2019 / monitor 2020-2025
resultados <- lapply(unique(serie$estación), function(est) {
ts_est <- ts(subset(serie, estación == est)$ndvi,
start = c(2013, 1), frequency = 12)
mon <- bfastmonitor(ts_est,
start = c(2020, 1),
formula = response ~ harmon,
order = 3,
level = 0.05)
data.frame(estación = est,
breakpoint = as.character(time(mon$breakpoint)),
magnitud = mon$magnitude)
})
write.csv(do.call(rbind, resultados),
"outputs/tables/bfastmonitor_estaciones.csv", row.names = FALSE)
Los rasters de máscara producidos por SamGeo se inspeccionan abriendo el archivo con rasterio.open() para extraer dimensiones, CRS, resolución y valor de NoData sin materializar la grilla en memoria; los GeoJSON resultantes se reproyectan a MAGNA-SIRGAS Origen Nacional (EPSG:9377) —estándar oficial del IGAC desde la Resolución 471 de 2020— y sobre los parches reproyectados se aplican los predicados topológicos intersects con la frontera del AOI y contains con los puntos de muestreo INVEMAR, en ambos casos delegando en GEOS los mismos cálculos que sustentan PostGIS y JTS.
from src.python.utils import (
raster_metadata, vector_to_9377, area_ha_9377,
parches_borde, parches_con_punto, EPSG_NACIONAL,
)
# Inspeccion lazy del raster: metadatos sin cargar la grilla
meta = raster_metadata('data/processed/samgeo/mask_actual.tif')
# Reproyeccion al sistema oficial colombiano MAGNA-SIRGAS
vector_to_9377('data/processed/samgeo/manglar_actual.geojson',
'data/processed/samgeo/manglar_actual_9377.geojson')
# Predicados topológicos DE-9IM sobre los parches reproyectados
gdf = area_ha_9377(gpd.read_file('data/processed/samgeo/manglar_actual_9377.geojson'))
gdf = parches_borde(gdf, aoi) # intersects con la frontera
gdf = parches_con_punto(gdf, estaciones) # contains con los puntos INVEMAR
La segmentación automática de las composiciones RGB de los tres periodos de referencia se ejecuta con el backbone vit_b de SamGeo —versión adaptada al límite de memoria del contenedor sig_unal v1.11— produciendo una máscara raster por periodo que se vectoriza a GeoJSON para el posterior filtrado por área y cálculo de métricas de fragmentación.
from samgeo import SamGeo
# Modelo base vit_b para ajustarse a la RAM del contenedor
sam = SamGeo(model_type='vit_b', automatic=True)
for periodo in ['degradación', 'recuperación', 'actual']:
rgb = f'{res_dir}/CGSM_RGB_{periodo}_30m.tif'
mask = f'{out_dir}/mask_{periodo}.tif'
poly = f'{out_dir}/manglar_{periodo}.geojson'
sam.generate(rgb, output=mask) # raster binario
sam.tiff_to_vector(mask, poly) # parches vectoriales
La descarga del reanálisis ERA5-Land del ECMWF se realiza con cdsapi directamente desde el Climate Data Store, solicitando precipitación total y temperatura a dos metros sobre un bounding box que envuelve la CGSM con buffer; los datos se reciben en NetCDF con convenciones CF y se cargan con xarray.open_dataset(chunks=...) aplicando un patrón perezoso, calculando la climatología mensual y la anomalía respecto a ella para los rezagos cero a tres meses.
import cdsapi, xarray as xr
c = cdsapi.Client()
c.retrieve('reanalysis-era5-land-monthly-means', {
'product_type': 'monthly_averaged_reanalysis',
'variable': ['total_precipitation', '2m_temperature'],
'year': [str(y) for y in range(2018, 2026)],
'month': [f'{m:02d}' for m in range(1, 13)],
'time': '00:00',
'area': [11.20, -75.05, 10.30, -74.05],
'format': 'netcdf'
}, 'data/raw/era5_land_cgsm_monthly.nc')
ds = xr.open_dataset('data/raw/era5_land_cgsm_monthly.nc', chunks={'time': 12})
anom = ds.groupby('time.month') - ds.groupby('time.month').mean('time')
Para el evento de septiembre de 2020 se realizó una detección de inundación mediante Sentinel-1 SAR, banda VH y modo IW, comparando el backscatter medio del periodo seco de referencia, enero-marzo de 2020, con el periodo de inundación, septiembre-octubre de 2020. En el código, la diferencia se definió como SAR diff = seco − inundación. Bajo esta convención, se identificó agua abierta cuando SAR diff fue superior a +3 dB, asociada con disminución del backscatter durante la inundación por reflexión especular, y manglar inundado bajo dosel cuando SAR diff fue inferior a −2 dB, asociado con aumento del backscatter durante la inundación por dispersión de doble rebote agua-tronco.
s1_dry = ee.ImageCollection('COPERNICUS/S1_GRD') \
.filterDate('2020-01-01','2020-03-31').select('VH').median()
s1_flood = ee.ImageCollection('COPERNICUS/S1_GRD') \
.filterDate('2020-09-01','2020-10-31').select('VH').median()
sar_diff = s1_dry.subtract(s1_flood)
flood_open = sar_diff.gt(3).selfMask() # agua abierta
flood_canopy = sar_diff.lt(-2).selfMask() # bajo dosel
La función compute_metrics agrega los parches segmentados de cada periodo, calcula el área de cada polígono mediante la fórmula del cordón sobre coordenadas en metros (EPSG:9377), deriva el índice de forma medio MSI = perímetro / √(π·área) y la distancia al vecino más cercano NND, y devuelve un DataFrame con la fila resumen del periodo. Esta función se invoca tres veces —una por periodo de referencia— y los resultados alimentan la tabla de fragmentación reportada en la sección homónima del cuerpo del informe.
using GeoJSON, DataFrames, Statistics
function compute_metrics(periodo, base_dir)
path = joinpath(base_dir, "manglar_$(periodo)_9377.geojson")
parches = GeoJSON.read(read(path, String))
# Area por formula del cordon (shoelace) y perímetro en metros
áreas = [shoelace_area(p) for p in parches]
perim = [polygon_perimeter(p) for p in parches]
msi = perim ./ sqrt.(pi .* áreas) # indice de forma medio
nnd = nearest_neighbor_distances(parches) # km al vecino más cercano
return DataFrame(
periodo = periodo,
n_parches = length(parches),
area_total = sum(áreas) / 1e4, # hectáreas
area_media = mean(áreas) / 1e4,
msi_mean = mean(msi),
nnd_mean = mean(nnd) / 1e3,
)
end
Para mantener el cuerpo del informe centrado en la narrativa de los hallazgos, dos tablas de detalle metodológico se reubican en este anexo: el cruce estación por estación entre la señal SAR de septiembre 2020, la anomalía NDVI y la frecuencia de inundación histórica de la Global Flood Database (referida en la sección de Validación con datos NASA), y las nueve transiciones de superficie del JRC Global Surface Water 1984–2021 (referida en la sección de Dinámica hídrica).
Nota de homologación de estaciones complementarias. En la serie legacy desarrollada sobre el AOI preliminar de 5.073 km², dos estaciones complementarias del Complejo de Pajarales fueron registradas con la nomenclatura CP Pajarales y VIPIS. Durante la depuración metodológica hacia el AOI acotado de 835,3 km², estas estaciones fueron homologadas y reubicadas sobre cobertura de manglar denso verificada mediante NDVI > 0,4 en composiciones Sentinel-2 de 2024. En la nomenclatura vigente del informe principal, CP Pajarales corresponde a CP Aguas Negras y VIPIS corresponde a CP Luna. Esta homologación no representa únicamente un cambio de etiqueta, sino también un ajuste espacial de coordenadas para garantizar consistencia con el AOI acotado y con la cobertura efectiva de manglar analizada. Las tablas de este anexo reportan la nomenclatura vigente y conservan la equivalencia legacy únicamente como nota de trazabilidad.
| Nombre legacy | Nombre vigente | Observación |
| CP Pajarales | CP Aguas Negras | Reubicada hacia manglar denso del sector Aguas Negras, dentro del Complejo de Pajarales |
| VIPIS | CP Luna | Reubicada hacia manglar denso del sector CP Luna, dentro del Complejo de Pajarales |
Tabla 1: Cruce de señal SAR, NDVI y frecuencia de inundación histórica por estación, restringido al AOI acotado SFF + VPI Salamanca.
|
Tabla 2: Transiciones JRC Global Surface Water (1984–2021) dentro del AOI acotado SFF + VPI Salamanca. El AOI contiene 385,3 km² de agua permanente (>75 % del tiempo) y 44,1 km² estacional (25–75 %), de modo que casi la mitad del polígono protegido es lámina de agua.
|
Tabla 3: Entorno de ejecución completo del proyecto.
|
Tabla 4: Quiebres estructurales detectados por BFAST sobre las ocho estaciones del proyecto en la serie combinada Landsat 8 + Sentinel-2 (2013–2025). El análisis refinado sobre las cuatro estaciones de manglar denso se reporta en la Tabla 5 del cuerpo del informe principal.
|
Tabla 5: Parches de manglar truncados por la frontera del AOI acotado (predicado DE-9IM
|
Tabla 6: Anomalías significativas de NDVI (z<−2) en la serie combinada 2013–2025, ordenadas por z-score decreciente.
|
Tabla 7: Flujo de datos del pipeline multilenguaje.
|
Tabla 8: Eventos de inundación con mayor área afectada dentro del AOI acotado (Global Flood Database, 2001-2017).
|
Tabla 9: Correlación de Pearson entre la serie mensual Sentinel-1 SAR-VH y el NDVI z-score derivado de Sentinel-2 para rezagos de 0 a 3 meses, calculada sobre las ocho estaciones entre 2018 y 2025. Valores en negrita: correlaciones significativas (p < 0,001) sobre las cuatro estaciones de manglar denso del Complejo de Pajarales.
|
Tabla 10: Datacubes NetCDF CF-1.8 materializados sobre el AOI acotado en EPSG:9377 (MAGNA-SIRGAS Origen Nacional), comprimidos con zlib nivel 4.
|
Tabla 11: Correlación de Pearson entre índices ENSO globales (ONI y SOI de NOAA) y NDVI z-score desagregada por naturaleza espectral.
|
Tabla 12: Correlación de Pearson entre anomalía de caudal IDEAM y NDVI z-score, desagregada por estación hidrológica, naturaleza espectral y rezago temporal.
|
Tabla 13: Correlación de Pearson entre anomalía mensual CHIRPS sobre cuencas aportantes y NDVI z-score de las estaciones de la CGSM.
|
Tabla 14: Correlación entre anomalías climáticas ERA5-Land (precipitación y temperatura a 2 m) y NDVI z-score, desagregada por naturaleza espectral.
|