q42·advanced

Is the river through my town running high, and is a flood building upstream?

hydrologydisasterswater-resources Datasets: 3 30–60 min

Water-surface elevation is the height of the top of a river, measured from orbit. A satellite breaks a river into short stretches (called reaches) and, each time it flies over, reports two numbers per reach: how high the water is, and how wide the channel is. Watch those numbers climb on the reaches above your town and you can spot a flood wave while it is still upstream.

Is the river through my town running high, and is a flood building upstream?

Water-surface elevation is the height of the top of a river, measured from orbit. A satellite breaks a river into short stretches (called reaches) and, each time it flies over, reports two numbers per reach: how high the water is, and how wide the channel is. Watch those numbers climb on the reaches above your town and you can spot a flood wave while it is still upstream.

Which data to use, and how

Use this dataset: SWOT river reaches (short name SWOT_L2_HR_RiverSP_reach_D). SWOT is a NASA + CNES (the French space agency) satellite, flying since 2023, that measures the height and width of any river wider than about 100 m. Each row in the data is one reach, tagged with a reach_id, a wse (water-surface elevation, in metres), and a width (in metres).

Verified locally: for the Lower Mississippi near Baton Rouge, this product read a real water height of 1.88 m (channel width about 999 m) on a reach in the box from the 1 Jan 2024 overpass. That was one of 11 valid reaches the satellite measured there on that pass. The single number matters less than how it changes between overpasses. A steady rise pass after pass is the signature of a flood building.

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

  1. Pick your river stretch. A small latitude/longitude box around your town, plus the months you care about.
  2. Pull each overpass. For every time the satellite flew over, get the reaches inside your box and throw away the fill value (-999999999999, a placeholder meaning “no reading here”).
  3. Read height and width per reach. wse tells you how high the water sits; a growing width means water is spilling toward the banks.
  4. Watch one reach over time. Keep a single reach_id and line up its wse across successive overpasses. Rising means water climbing. Check the reaches upstream first, since they rise hours to days before your town does.

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

  • How high a river reach is sitting on a given overpass. The wse field, in metres, for every reach SWOT measured over your town.
  • How wide the channel is, and whether it is widening. The width field, a direct sign of water spreading toward the banks.
  • Whether the river is rising. Line up wse for the same reach_id across overpasses and read the trend.
  • Whether a flood is building upstream. Check the reaches above your town first; a rise there comes before a rise at home by hours to days.
  • How far above normal today is. Compare this overpass to the spread of past overpasses for the same reach.
  • Where water historically sits. Overlay JRC Global Surface Water to see the floodplain and the permanent-water baseline your reading should be judged against.

What it can’t tell you

  • The actual flow (discharge, in m³/s). SWOT gives height and width, not volume. Turning height into flow needs a calibration curve or a hydraulic model. The height alone won’t do it.
  • How today compares to your town’s official flood line. wse is measured against a satellite reference surface (a geoid datum, a standardized “sea-level” model of the Earth’s shape), not the local benchmark your town’s flood stage is defined against. You have to line up the two reference levels before comparing to a flood threshold.
  • Small streams. SWOT only sees rivers wider than about 100 m; a creek through your neighbourhood is too narrow.
  • Continuous, real-time tracking. Each reach is seen only on overpasses (every few days), so a fast flash flood can rise and fall between visits.
  • Why the river is rising. Pair with GPM IMERG rainfall upstream to tell a storm apart from a dam release or snowmelt.
  • Whether your specific street will flood. Reach-scale height is not a street-level flood map; that needs a detailed terrain model and local hydraulics.

Gotchas to watch for

  • It’s a zipped shapefile, not the usual NetCDF. SWOT river reaches ship as a zipped shapefile (a GIS map-data format), not the array format most NASA products use, so the normal reader won’t touch it. Open it with geopandas using a zip:// path, as shown in the code below.
  • Drop the fill value before you do any maths. Reaches with no valid reading are stored as -999999999999 (a fill value — a stand-in number meaning “missing”). Average that in and it will wreck your numbers, so filter out any row where wse equals that value first.
  • Heights are relative, not absolute. wse is referenced to a geoid datum, so the raw number won’t match a local gauge and comparing it straight to a flood-stage number is meaningless. What you can trust is the change between overpasses, and that change is what flags a rising flood.
  • A few days between looks. You only get one reading per overpass, so a quick flood can peak unseen between visits. Watch upstream reaches for early warning, and don’t expect a smooth hour-by-hour curve.

The real-data code

The Run it cell above runs the method on synthetic data (no login). Below is the whole recipe against the real archive: search every overpass, open the zipped shapefiles, read wse/width per reach, track one reach across time, print whether it’s rising, and plot it. It needs a free Earthdata Login.

import os, re, warnings, numpy as np, pandas as pd, earthaccess, geopandas as gpd
import matplotlib.pyplot as plt
warnings.filterwarnings("ignore")

# load Earthdata creds from .env without `source` (passwords can break the shell)
for line in open(".env"):
    m = re.match(r'\s*(?:export\s+)?([A-Z0-9_]+)\s*=\s*(.*)\s*$', line)
    if m: os.environ.setdefault(m.group(1), m.group(2).strip().strip('"').strip("'"))
earthaccess.login(strategy="environment")   # free Earthdata Login

W, S, E, N = -91.3, 30.2, -90.9, 30.6        # Lower Mississippi near Baton Rouge
FILL = -999999999999                         # SWOT "no reading" placeholder

# 1. Every overpass that touched the box over the window we care about.
grans = earthaccess.search_data(short_name="SWOT_L2_HR_RiverSP_reach_D",
                                temporal=("2024-01-01", "2024-06-30"),
                                bounding_box=(W, S, E, N))

# 2. The product is a ZIPPED SHAPEFILE — download all overpasses, then read each
#    with a zip:// path, clip to the box, and drop the fill value.
files = earthaccess.download(grans, local_path="swot")
frames = []
for f in files:
    gdf = gpd.read_file(f"zip://{f}").cx[W:E, S:N]
    gdf = gdf[(gdf["wse"] != FILL) & (gdf["width"] != FILL)]
    if len(gdf):
        gdf["t"] = pd.to_datetime(gdf["time_str"], errors="coerce")  # overpass time
        frames.append(gdf[["reach_id", "wse", "width", "t"]])
obs = pd.concat(frames, ignore_index=True).dropna(subset=["t"]).sort_values("t")
print(f"{len(obs)} reach-readings across {obs['t'].dt.date.nunique()} overpasses")

# 3. Track ONE reach over time — pick the reach seen on the most overpasses so the
#    trend is well-sampled. wse rising pass after pass is the flood signature.
target = obs["reach_id"].value_counts().idxmax()
ts = obs[obs["reach_id"] == target].sort_values("t")
print(f"reach {target}: {len(ts)} overpasses, "
      f"wse {ts['wse'].iloc[0]:.2f} m -> {ts['wse'].iloc[-1]:.2f} m, "
      f"width {ts['width'].iloc[0]:.0f} m -> {ts['width'].iloc[-1]:.0f} m")

# 4. Slope of wse vs time (m per day) decides rising / falling / flat.
days = (ts["t"] - ts["t"].iloc[0]).dt.total_seconds().to_numpy() / 86400
slope = np.polyfit(days, ts["wse"].to_numpy(), 1)[0] if len(ts) > 1 else 0.0
verdict = "RISING — possible flood building" if slope > 0.01 else \
          "falling" if slope < -0.01 else "roughly steady"
print(f"VERDICT: reach {target} is {verdict} ({slope:+.3f} m/day)")

# 5. Plot this reach's water-surface elevation across overpasses.
plt.plot(ts["t"], ts["wse"], "o-")
plt.ylabel("water-surface elevation (m)"); plt.xlabel("overpass date")
plt.title(f"SWOT reach {target}{verdict}")
plt.tight_layout(); plt.show()

# Trust check: wse is on a geoid datum, not a local gauge, so judge the *change*,
# not the absolute number — and read the reaches UPSTREAM first, they rise before you do.

Where the data comes from

SWOT river reaches come from NASA’s PO.DAAC archive (the ocean/hydrology data center) through one free login (Earthdata Login). The optional add-ons are separate: GPM IMERG rainfall (also NASA, same login) tells you whether a storm caused the rise, and JRC Global Surface Water (from the European Commission’s Joint Research Centre) gives you the floodplain baseline to judge a reading against.

Sources

  • SWOT river reach product (SWOT_L2_HR_RiverSP_reach_D) is delivered as a zipped shapefile; each row is a river reach with reach_id, wse (metres), and width (metres), and the fill value is -999999999999.
How a scientist answers this
Parameters
Per-reach river water-surface elevation (SWOT_L2_HR_RiverSP `wse`, metres relative to a geoid datum) and channel `width` (metres) for reaches wider than ~100 m, tracked across overpasses; GPM IMERG gives upstream rainfall and JRC Global Surface Water the floodplain baseline.
Method
For each town and upstream reach, build a time series of `wse` and `width` across SWOT overpasses (the change between passes, not the absolute height, is the flood signal), flag a rising trend, and check whether upstream reaches rise before the town's reach; corroborate with accumulated upstream IMERG rainfall as a leading indicator.
Validation
Keep only reaches passing SWOT data-quality flags, report valid-reach counts per overpass, account for the coarse temporal sampling (multi-day repeat) and node/reach aggregation, and cross-check rises against IMERG rainfall and any in-situ gauges.
In plain EnglishWatch the satellite-measured water height and width of river reaches over successive passes, and if reaches upstream of your town start climbing, a flood wave may be on its way.

Make it yours → Pick the town's reach and the upstream reaches, the overpass date range, and the rise threshold in the notebook.

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

A safe place to practise the method on SWOT_L2_HR_RiverSP_reach_D. 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

Datasets used

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.