q14·intermediate

When and where do harmful algal blooms occur in my coastal waters?

oceanbiologywater-qualitypublic-health Datasets: 5 20–60 min

A harmful algal bloom is a sudden population explosion of microscopic ocean plants (phytoplankton), sometimes so dense the water turns green, red, or brown. Some of these blooms release toxins or choke off oxygen, harming fish, shellfish, and people. What satellites actually measure is chlorophyll-a, the green pigment all these plants use to catch sunlight. More chlorophyll in the water means more plant life floating near the surface.

When and where do harmful algal blooms occur in my coastal waters?

A harmful algal bloom is a sudden population explosion of microscopic ocean plants (phytoplankton), sometimes so dense the water turns green, red, or brown. Some of these blooms release toxins or choke off oxygen, harming fish, shellfish, and people. What satellites actually measure is chlorophyll-a, the green pigment all these plants use to catch sunlight. More chlorophyll in the water means more plant life floating near the surface.

Which data to use, and how

Use this dataset: MODIS Aqua chlorophyll (short name MODISA_L3m_CHL). It goes back to 2002, is already gridded, and is the easiest place to start. It tells you how much green plant pigment is in the surface water. Want to tell which kind of plant is blooming? The newer PACE ocean-color (PACE_OCI_L2_BGC) instrument, flying since 2024, reads many more colors of light and can hint at the species. Start with MODIS though.

Then do four steps (the runnable code further down does all of this; this is just the idea):

  1. Pick your coast. A small latitude/longitude box over your bay or shoreline, and the years you care about.
  2. Build a normal year. For each calendar month, average all the years of MODIS chlorophyll to learn what a typical January, February, and so on looks like for that spot. This average is the climatology.
  3. Compare today to normal. Subtract that typical value from the current month. What’s left is the anomaly: how much higher (or lower) than usual the water is right now. A big positive anomaly means a possible bloom.
  4. Map it and track it. Draw where the anomaly is high to see the bloom’s extent, and watch it month to month to see whether blooms are getting bigger or more frequent over the years.

That’s the whole method. Below, “How a scientist answers this” names the exact tools, and the Run it cell does it live on synthetic data so you can see each step work.

What you can find out

  • Where the water is unusually green right now. Anomaly maps flag spots with far more plant life than normal, a likely bloom.
  • When blooms usually happen. A 22-year MODIS climatology shows your bay’s typical bloom season.
  • How big a bloom is and how long it lasts. Its spatial extent and persistence over time.
  • Whether blooms are changing. Across the satellite era, are they getting more frequent or more intense?
  • What kind of plant it is. With PACE’s many-color (hyperspectral) view, you can start to tell cyanobacteria (the toxic “blue-green algae”) apart from harmless diatoms.

What it can’t tell you

  • How toxic the water is. Satellites count green pigment, not poison. Toxin levels (microcystin, brevetoxin) need someone to scoop up a water sample and test it in a lab. Worse, some species pump out intense toxins even when there’s little visible green.
  • Whether it’s safe to swim or eat the shellfish. That’s a health call. It needs the bloom map plus shellfish-bed closures and beach-use data layered on top.
  • What’s happening below the surface. Satellites see only the top sunlit layer of water (roughly the top 5–30 m, depending on how clear the water is). A bloom hiding deeper is invisible.
  • The exact species. PACE can suggest the type of plant. Pinning down the actual species still needs a microscope in a lab.

Gotchas to watch for

  • Murky coastal water fools the math. Near shore, the water is full of mud, dissolved gunk from rivers (CDOM, colored dissolved organic matter), and shallow bottoms, all of which the ocean-color algorithms can mistake for plant life (these are called Case-II waters). PACE handles them better than MODIS, but the recipes are still being refined. Fix: trust the anomaly more than the raw number, and treat near-shore values with caution.
  • Clouds erase the data. The satellite can’t see water through clouds, so single days are full of holes. Fix: use 8-day or monthly composites (many passes stacked together) so clear moments fill the gaps.
  • High chlorophyll isn’t always a bloom. Some bays are naturally rich and green year-round. Fix: always use the anomaly (now minus the typical value), never the absolute chlorophyll number, so you flag what’s genuinely unusual.
  • Use the right MODIS version. NASA periodically reprocesses the whole archive; use the R2022.0 reprocessing by default, and re-pull any older data so your whole time series is consistent. Mixing versions can create fake jumps.

The real-data code

The Run it cell above runs this method on synthetic data (no login). Below is the whole recipe against the real archive: search, open the gridded chlorophyll, build the monthly climatology, compute the anomaly, map it, and track bloom area over time. It needs a free Earthdata Login.

import earthaccess, numpy as np, xarray as xr
import matplotlib.pyplot as plt

earthaccess.login(strategy="netrc")

aoi = (-71, 41, -67, 45)         # your coast as (W, S, E, N) — here, Gulf of Maine
years = range(2003, 2025)

# 1. One chlorophyll map per month: open each monthly MODIS Aqua L3 granule and
#    clip the gridded log-chlorophyll to the box. (Chl is log-normal, so we work
#    in log space and exp back at the end.)
def monthly_chl(y, m):
    grans = earthaccess.search_data(
        short_name="MODISA_L3m_CHL", bounding_box=aoi,
        temporal=(f"{y}-{m:02d}-01", f"{y}-{m:02d}-28"))
    if not grans:
        return None
    ds = xr.open_dataset(earthaccess.open(grans)[0])
    chl = ds["chlor_a"].sel(lat=slice(aoi[3], aoi[1]), lon=slice(aoi[0], aoi[2]))
    return np.log10(chl.where(chl > 0))

# Stack every month into one cube indexed by (year, month, lat, lon).
grid = monthly_chl(years[-1], 7)          # a present-month map to fix the grid shape
cube = np.full((len(years), 12) + grid.shape, np.nan)
for i, y in enumerate(years):
    for m in range(12):
        mp = monthly_chl(y, m + 1)
        if mp is not None:
            cube[i, m] = mp.values

# 2. Build the climatology = the typical log-chl for each calendar month.
climatology = np.nanmean(cube, axis=0)                  # shape (12, lat, lon)

# 3. Anomaly = how far each month sits above its own normal. Pick the most recent
#    fully-observed month and map where the water is unusually green.
latest = cube[-1]
m_idx = max(i for i in range(12) if np.isfinite(latest[i]).any())
anomaly = latest[m_idx] - climatology[m_idx]           # log10 units

# 4. Bloom area per year: count pixels whose July anomaly exceeds +0.3 log10
#    (~2x normal chlorophyll) — a robust, mud-proof bloom flag.
bloom_area = np.array([np.nansum((cube[i, 6] - climatology[6]) > 0.3) for i in range(len(years))])
trend = np.polyfit(np.arange(len(years))[bloom_area > 0], bloom_area[bloom_area > 0], 1)[0]
print(f"Latest anomaly hotspot: {np.nanmax(anomaly):+.2f} log10(mg/m^3) over normal; "
      f"July bloom area trend {trend:+.1f} pixels/yr "
      f"({'expanding' if trend > 0 else 'shrinking'})")

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(11, 4))
im = ax1.imshow(anomaly, origin="upper", cmap="RdBu_r", vmin=-0.5, vmax=0.5)
ax1.set_title("Chlorophyll anomaly (this month vs normal)"); fig.colorbar(im, ax=ax1)
ax2.plot(list(years), bloom_area, "-o"); ax2.set_title("July bloom area by year")
ax2.set_xlabel("year"); ax2.set_ylabel("bloom pixels"); plt.tight_layout(); plt.show()

# Before you trust it: cross-check a flagged bloom against the NOAA HAB Operational
# Forecast, and treat near-shore Case-II pixels with caution (mud/CDOM inflate Chl).

Where the data comes from

Everything here comes from NASA through one free login (Earthdata Login). All of it lives in a single archive, the Ocean Biology DAAC (OB.DAAC), so both the long-running MODIS Aqua chlorophyll record and the new PACE hyperspectral measurements load from the same place, no extra accounts needed.

Sources

How a scientist answers this
Parameters
Near-surface chlorophyll-a concentration (mg/m3) from PACE OCI L2/L3 (hyperspectral, 2024+) and MODIS Aqua (2002+, for the long climatology), plus PACE phytoplankton-functional-type and CDOM absorption products to separate cyanobacteria from diatoms and flag runoff. A bloom is flagged where chlorophyll exceeds a high percentile of the local climatology (or a regional threshold such as >~10 mg/m3 in coastal waters).
Method
Build a per-pixel climatology of chlorophyll for each calendar period from the MODIS-Aqua record, then express the current scene as a standardized anomaly (z-score) or percent-of-normal; map contiguous high-anomaly pixels as bloom extent, track day-to-day persistence, and use PACE functional-type/CDOM bands to discriminate the dominant phytoplankton group.
Validation
Cross-check PACE against the overlapping MODIS/VIIRS record and confirm blooms against in-situ samples or shellfish-monitoring programs, since satellite chlorophyll is uncertain in turbid, CDOM-rich, or shallow coastal water and cannot measure toxin levels or sub-surface layers.
In plain EnglishMeasure how green the surface water is from space, compare it to the normal seasonal greenness for that spot, and flag unusually high values as a possible bloom — confirming the actual species and toxicity needs water samples.

Make it yours → Change the coastal bounding box, the season/months, and the chlorophyll anomaly threshold, and swap MODIS for PACE to gain phytoplankton-type detail.

🧪 Virtual Mock Lab stand-in data · runs in your browser · no login

A safe place to practise the method on PACE_OCI_L2_BGC. The code is the real earthaccess workflow, but we feed it stand-in data shaped like the real product — so you can run it, change the numbers, and break it without an account. When you want the real data, use the recipe under “The real-data code” above, or download the notebook below.

editable · runs in your browser
Recipe ready · not yet run The recipe and the Virtual Mock Lab above are ready to use. We haven't yet run this question end-to-end on real data, so it has no verified answer yet — only the questions we've actually computed carry one. That's the honest line between a method and a result.