After the storm, which neighborhoods lost power — and who lives there?
Draw a rectangle to pick your area of interest, then see what NASA data covers it (live, here in your browser) or download a ready-to-run notebook with your AOI pre-filled. The notebook runs in any Python environment — it needs a free Earthdata Login to fetch the data.
-82.8, 27.6 → -82.2, 28.1 (Tampa Bay, Florida (Hurricane Milton, Oct 2024))When a hurricane knocks out the grid, the city goes dark — and from orbit you can actually see it. NASA's **Black Marble** product (`VNP46A2`) turns the VIIRS day/night band into a daily, gap-filled, moonlight-corrected map of how much light each square kilometre is throwing off at night. Compare a clear night *before* landfall to a night *after*, and the blackout shows up as a plunge in [radiance](/glossary/radiance/). Lay a free population grid on top, and you can start to ask the human question: not just *where* the lights went out, but roughly *how many people* were sitting in the dark. **Verified locally.** Over Tampa Bay (27.6–28.1 °N), the `VNP46A2` field `Gap_Filled_DNB_BRDF-Corrected_NTL` averaged **19.18 nW/cm²/sr** on the clear night of 8 Oct 2024 — the eve of Hurricane Milton's landfall — and fell to **9.35 nW/cm²/sr** on the night of 11 Oct, a **51% drop** in mean radiance across the AOI. The lights then crept back (13.8 on the 12th, 15.3 on the 13th) as crews restored power. That recovery curve is the grid coming back online, written in light.
After the storm, which neighborhoods lost power — and who lives there?
When a hurricane knocks out the grid, the city goes dark — and from orbit you can actually see it.
NASA’s Black Marble product (VNP46A2) turns the VIIRS day/night band into a daily,
gap-filled, moonlight-corrected map of how much light each square kilometre is throwing off at
night. Compare a clear night before landfall to a night after, and the blackout shows up as a
plunge in radiance. Lay a free population grid on top, and
you can start to ask the human question: not just where the lights went out, but roughly how
many people were sitting in the dark.
Verified locally. Over Tampa Bay (27.6–28.1 °N), the VNP46A2 field
Gap_Filled_DNB_BRDF-Corrected_NTL averaged 19.18 nW/cm²/sr on the clear night of 8 Oct 2024
— the eve of Hurricane Milton’s landfall — and fell to 9.35 nW/cm²/sr on the night of 11 Oct,
a 51% drop in mean radiance across the AOI. The lights then crept back (13.8 on the 12th, 15.3
on the 13th) as crews restored power. That recovery curve is the grid coming back online, written
in light.
What you can answer
- How much darker a city got after the storm — compare mean
Gap_Filled_DNB_BRDF-Corrected_NTLover your bounding box, a clear night before vs. a night after (units: nW/cm²/sr) - Which neighborhoods went dark — the grid is ~500 m, so you can map the per-pixel drop and see which districts lost the most light
- How fast the grid recovered — track the AOI mean night by night and watch the radiance climb back toward its pre-storm baseline
- Roughly how many people were in the dark zone — overlay WorldPop 1 km population on the pixels that lost light, and sum the population underneath
- Which counties to name — clip your AOI against geoBoundaries ADM2 so you can say “Pinellas and Hillsborough,” not just “a bright blob”
- A before/after story for any storm since 2012 — VIIRS has flown nightly since then, so the same recipe works for past hurricanes, ice storms, or wildfire evacuations
What you can NOT answer with these datasets alone
- Confirmed power outages — a darker pixel is consistent with a blackout, but clouds, smoke, storm surge, and evacuations also dim a city. Cross-check utility outage maps before claiming a grid failure.
- Who, exactly, lost power — WorldPop is a modeled ~1 km population estimate, not a customer list; it tells you the order of magnitude of people affected, not addresses or households
- The cause of any one dark patch — the gap-filled product blends nights to fill clouds, so a single dark pixel can reflect missing data, not a real outage. Check the quality flags.
- Economic damage or injuries — radiance is light, not dollars or lives; impact assessment needs ground reporting, insurance data, and damage surveys
- Indoor vs. outdoor light — VIIRS sees streetlights, signage, and spill from windows together; it can’t separate “houses dark” from “streetlights off”
- Demographic equity on its own — WorldPop gives counts, not income, age, or vulnerability; pair it with census data to ask who was least able to recover
Code template (Python, cloud-direct)
Verified locally.
VNP46A2is a daily HDF5 tile on a 2400×2400 grid. The brightness lives atHDFEOS/GRIDS/VIIRS_Grid_DNB_2d/Data Fields/Gap_Filled_DNB_BRDF-Corrected_NTL, withlat/lonarrays in the same group so you can subset to your bounding box before averaging. Read the real_FillValueandscale_factorfrom the dataset attributes — on the granules I checked the fill was-999.9and the scale was1.0, with units nW/cm²/sr.
import os, re, warnings, collections
warnings.filterwarnings("ignore")
import earthaccess, h5py, numpy as np
# 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 = -82.8, 27.6, -82.2, 28.1 # Tampa Bay (Hurricane Milton)
GRP = 'HDFEOS/GRIDS/VIIRS_Grid_DNB_2d/Data Fields'
NTL = f'{GRP}/Gap_Filled_DNB_BRDF-Corrected_NTL'
def aoi_mean(granule):
f = earthaccess.open([granule])[0]
h = h5py.File(f, 'r')
ds = h[NTL]
lat, lon = h[f'{GRP}/lat'][:], h[f'{GRP}/lon'][:]
fill = float(np.array(ds.attrs.get('_FillValue', -999.9)).ravel()[0])
scale = float(np.array(ds.attrs.get('scale_factor', 1.0)).ravel()[0])
iy = np.where((lat >= S) & (lat <= N))[0]
ix = np.where((lon >= W) & (lon <= E))[0]
sub = ds[iy.min():iy.max()+1, ix.min():ix.max()+1].astype('float64')
sub[sub == fill] = np.nan
return np.nanmean(sub * scale)
res = earthaccess.search_data(short_name="VNP46A2",
temporal=("2024-10-05", "2024-10-13"),
bounding_box=(W, S, E, N))
by_date = collections.OrderedDict()
for g in res:
d = g['umm']['TemporalExtent']['RangeDateTime']['BeginningDateTime'][:10]
by_date.setdefault(d, []).append(g)
for d, gs in by_date.items():
print(d, round(aoi_mean(gs[0]), 3), "nW/cm2/sr")
# 2024-10-08 -> 19.18 (clear night before landfall)
# 2024-10-11 -> 9.35 (after Milton) == ~51% drop in mean radiance
# next: keep the dark pixels (big per-pixel drop), reproject WorldPop wpic1km
# onto them, and sum population to estimate how many people went dark.Make it yours → Set the bounding box, the before/after night dates, and the radiance-drop threshold in the notebook to match your storm.
The Before-After-Control-Impact vs a noise floor at the heart of this question — runnable on synthetic data, right here. The full earthaccess code template further down does it on real NASA data (needs an Earthdata login).