Basin-scale riparian monitoring from satellite data: a STAC ETL over Sentinel and Landsat, geospatial ML, and a map-based decision-support UI.
PythonSTACGeospatial ML.NET 10PostGISReact 18MapLibre GLAspire

A basin-scale monitoring system for riparian ecosystems in the San Juan River watershed (Colorado + New Mexico): satellite imagery in, health-graded map intelligence out. It learns where riparian vegetation actually is from satellite time series — instead of assuming a fixed distance from a stream — then scores its condition (including invasive tamarisk and Russian-olive cover) and tracks change across the Earth-observation record. It’s where my two careers meet: a decade of GIS engineering and an environmental-studies background on one side, modern data engineering and ML on the other.
The live site is an interactive story map of the corridor, a Random Forest vs. Ai2 OlmoEarth foundation-model field test, and a grounded RAG agent that streams cited answers about the watershed.
Most portfolio ML projects stop at a notebook. This one is a system: reproducible ETL over public satellite archives, models validated the way spatial data demands (spatial CV, not random splits), a typed API layer, and a UI a watershed manager could actually use. It’s deployed with Docker and PostgreSQL, guarded in production by rate limits, input shields, and Cloudflare Turnstile.
And it holds itself to scientific standards: the repo carries a public
engineering & methodology walkthrough
inviting scrutiny of the weak points, and a RETRACTIONS.md. When a result’s
interpretation didn’t survive re-verification, the correction was published, not buried.
The headline finding stands either way: on an out-of-distribution reach, the pixel-wise
Random Forest collapses to near-chance while the OlmoEarth foundation model holds, a
measured demonstration of classical-ML brittleness to distribution shift.
That finding is also what the document agent answers from. It runs hybrid RAG over the watershed corpus plus the experiment results, so asking it to compare the two models returns a table with every row cited back to a source you can open. The model selector is honest about what’s actually serving, too: OLMo is listed as disabled while no provider serves it, rather than quietly substituting another model.