Code
STATE = "WY" # must match what wumi_perimeters.ipynb was run for
EQUAL_AREA = "EPSG:5070" # CONUS Albers -- also WUMI's native projection, convenientlyThis notebook was written by generative AI. It’s meant to show querying across sources/formats, not provide informative analysis. Take the results with a major grain of salt.
Every mapped fire perimeter since 1984 for one state, drawn as a translucent polygon on the same map. A place that has never burned stays white. A place that has burned once is a single faint wash of color. A place that has burned twice, or five times, gets darker each time a perimeter stacks on top of it – the overlap is the signal, and it comes for free from drawing every polygon at the same low opacity rather than computing anything extra.
| Layer | Source | What it tells you |
|---|---|---|
| Fire perimeters | WUMI, merged from MTBS/CalFire/USGS/WFIGS/IAFPH | Where a fire actually burned, 1984 to the present, one polygon per fire |
views.sqlThe risk notebook uses MTBS alone, which has a size floor (~1,000 acres in the west) and a reporting lag of a year or more. WUMI merges MTBS with CalFire, USGS, WFIGS, and IAFPH, so it catches smaller and more recent fires MTBS hasn’t gotten to yet – more perimeters, which is exactly what matters for a map about how often the same ground reburns.
The tradeoff: getting at it needs a Dryad API account, since Dryad’s website (unlike every other source in views.sql) is behind bot detection that only its REST API sidesteps. wumi_perimeters.ipynb handles that and caches the result; run it first, for the same STATE configured below.
A fire perimeter, not a zone. There is no downscaling or areal interpolation here – either two mapped shapes overlap in space, or they don’t.
Only fires with a real, agency-mapped perimeter are included. WUMI also ships a circular fallback – a fire’s perimeter assuming it was a disc centered on the ignition point – for fires nobody ever mapped; wumi_perimeters.ipynb drops those rather than draw a guess as if it were an observation. So this map understates total fire coverage, and understates it more for older and smaller fires, which were less likely to get mapped at all.
Run time (UTC): 2026-09-24T06:21:20+00:00
State : WY
Configure the import path:
| Config | value |
|---|---|
| autopandas | True |
| displaycon | False |
setup.sql installs the extensions; views.sql defines the reproject macro this notebook uses below. Neither one touches the WUMI data – that lives in the parquet cache wumi_perimeters.ipynb wrote, not in a remote view, because Dryad’s downloads can’t be read directly (see that notebook for why).
The state outline, for orienting the map – not for filtering, since the cached perimeters are already scoped to one state.
Read straight from the cache. poly_area_h/burn_area_h are hectares within the perimeter and hectares actually classified as burned inside it, respectively – distinct because a perimeter is not uniformly burned inside.
%%sql
CREATE OR REPLACE TABLE fires AS
SELECT fireid,
source,
name,
date::DATE AS ignition_date,
year(date::DATE) AS ignition_year,
poly_area_h AS poly_area_ha,
burn_area_h AS burn_area_ha,
cause_human,
geometry AS geom
FROM read_parquet('data/wumi_perimeters/{{STATE}}.parquet');
SELECT count(*) AS fires,
min(ignition_year) AS first_year,
max(ignition_year) AS last_year,
round(sum(burn_area_ha) / 1e6, 2) AS million_ha_burned
FROM fires;| fires | first_year | last_year | million_ha_burned | |
|---|---|---|---|---|
| 0 | 1328 | 1984 | 2026 | 2.47 |
If fires is empty or missing, wumi_perimeters.ipynb has not been run for this STATE yet – do that first.
For each fire, how many other fires’ perimeters it intersects. OVERLAPS is a reserved word in SQL (a temporal operator), which is why this table is not called that.
%%sql
CREATE OR REPLACE TABLE overlap_counts AS
SELECT a.fireid AS fireid,
count(*) AS n_overlapping_fires
FROM fires AS a
JOIN fires AS b ON a.fireid != b.fireid AND ST_Intersects(a.geom, b.geom)
GROUP BY a.fireid;
SELECT count(*) AS fires_with_overlap,
(SELECT count(*) FROM fires) AS total_fires
FROM overlap_counts;| fires_with_overlap | total_fires | |
|---|---|---|
| 0 | 855 | 1328 |
The most reburned ground, by how many other perimeters overlap each fire:
| fireid | name | ignition_year | burn_area_ha | n_overlapping_fires | |
|---|---|---|---|---|---|
| 0 | 20240822_449420_1060710 | REMINGTON | 2024 | 73170.0 | 22 |
| 1 | 19880710_439951_1102962 | MINK | 1988 | NaN | 19 |
| 2 | 19880722_444307_1110990 | NORTH FORK | 1988 | NaN | 18 |
| 3 | 20110822_451700_1061830 | DIAMOND COMPLEX | 2011 | 12182.0 | 16 |
| 4 | 20110821_451715_1061812 | LITTLE FORK | 2011 | NaN | 16 |
| 5 | 19880709_447370_1099540 | CLOVERMIST | 1988 | 79132.0 | 15 |
| 6 | 20170831_449560_1064710 | DEER CREEK | 2017 | 33518.0 | 13 |
| 7 | 19880722_447080_1108210 | NORTH FORK | 1988 | 159392.0 | 12 |
| 8 | 20060713_448550_1063220 | BUFFALO CREEK | 2006 | 14377.0 | 11 |
| 9 | 20120627_422010_1054900 | ARAPAHO | 2012 | 34759.0 | 10 |
Every perimeter, at the same low fill opacity. No per-fire styling and no pre-computed overlap surface – Leaflet stacking semi-transparent fills is the overlap computation, visually.
Some of these perimeters (the 1988 Yellowstone complexes especially) carry thousands of vertices, which is more precision than a state-wide map can show or a browser wants to render. ST_SimplifyPreserveTopology trims that before drawing; the notebook’s numbers above are all computed from the unsimplified geometry.
import folium
from folium.plugins import Fullscreen
from helpers import add_map_caption, read_geo
SIMPLIFY_TOLERANCE_M = 100 # in EQUAL_AREA's units; visually lossless at state scale
fire_shapes = read_geo(
conn,
"SELECT fireid, name, ignition_year, round(burn_area_ha) AS burn_area_ha, "
f"ST_AsText(reproject(ST_SimplifyPreserveTopology(geom, {SIMPLIFY_TOLERANCE_M}), "
f"'{EQUAL_AREA}', 'EPSG:4269')) AS geom FROM fires",
crs="EPSG:4269",
)
state_outline = read_geo(
conn, "SELECT ST_AsText(geom) AS geom FROM study_area", crs="EPSG:4269"
)
FIRE_FILL = "#b2182b"
FIRE_FILL_OPACITY = 0.18
bounds = state_outline.total_bounds
m = folium.Map()
m.fit_bounds([[bounds[1], bounds[0]], [bounds[3], bounds[2]]])
folium.GeoJson(
state_outline,
style_function=lambda _: {"color": "#999999", "weight": 1, "fillOpacity": 0},
).add_to(m)
folium.GeoJson(
fire_shapes,
style_function=lambda _: {
"fillColor": FIRE_FILL,
"color": "#67000d",
"weight": 0.4,
"fillOpacity": FIRE_FILL_OPACITY,
},
tooltip=folium.GeoJsonTooltip(
fields=["name", "ignition_year", "burn_area_ha"],
aliases=["fire", "year", "burned (ha)"],
),
).add_to(m)
# what a spot burned n times looks like, with n fills stacked on top of each other
add_map_caption(
m,
f"Wildfire perimeters, {STATE}",
f"{fire_shapes['ignition_year'].min()}-{fire_shapes['ignition_year'].max()}",
{
f"burned {n}x": f"background: {FIRE_FILL}; opacity: {1 - (1 - FIRE_FILL_OPACITY) ** n:.2f}"
for n in (1, 2, 4)
},
)
Fullscreen().add_to(m)
mwumi_perimeters.ipynb drops WUMI’s circular fallback – a guessed disc for fires nobody ever mapped – so this understates total fire coverage, more so for older and smaller fires.EPSG:5070 is CONUS-specific, same caveat as risk.ipynb.n_overlapping_fires treats a reburn 40 years later the same as one 4 years later. Joining on ignition_year gap would separate “resilient landscape” from “still recovering.”risk.ipynb – burn probability, Red Flag Warnings, and active detections all slot in as more DuckDB views in views.sql over the same fires table.