How to Find Where a Wildfire Reburned Earlier Fires in Python
On this page
Problem statement
Fires in fire-prone landscapes burn into the scars of earlier fires. Knowing how much of a new fire reburned old ones, and how long ago, matters for ecology, fuel management and recovery planning. Ground that burns twice in a few years can convert from forest to shrub. Ground that has not burned for a century carries heavy fuel. Answering it is an overlay of one perimeter with a century of others. That overlay goes wrong easily: older perimeters overlap one another, so their areas cannot simply be added, many records are invalid polygons, and the historical record gets thinner the further back it goes.
This page overlays the 2024 Park Fire with every earlier perimeter in CAL FIRE's historical archive. It measures how much of the fire had burned before, how many times, and how recently.
Quick answer
Take the earlier perimeters that intersect the fire, clip them to it, and count how many overlap each piece of ground with a union overlay:
earlier = hist[(hist.YEAR_ < 2024) & hist.intersects(PARK)].copy()
earlier["geometry"] = earlier.geometry.intersection(PARK)
pieces = gpd.GeoDataFrame(geometry=list(polygonize(unary_union([g.boundary for g in [PARK, *earlier.geometry]]))), crs=3310)
pieces = pieces[pieces.representative_point().within(PARK)]
pt = pieces.representative_point()
pieces["times"] = [int(earlier.geometry.contains(p).sum()) for p in pt]
pieces["last"] = [earlier.loc[earlier.geometry.contains(p), "YEAR_"].max() for p in pt]
reburned = pieces[pieces.times > 0].area.sum() / PARK.area
print(f"{len(earlier)} earlier perimeters overlap the Park Fire; {reburned:.0%} of its area had burned at least once before 2024")
133 earlier perimeters overlap the Park Fire; 76% of its area had burned at least once before 2024
History around the fire, no added overlaps, times burned
1. Get the history around the fire
Query the historical archive with the fire's bounding box rather than downloading all 23,000 perimeters. ArcGIS services take an envelope as geometry with spatialRel=esriSpatialRelIntersects:
print(f"{len(hist)} perimeters in the bounding box, {hist.YEAR_.min()}–{hist.YEAR_.max()}; "
f"invalid as downloaded: {(~RAW.geometry.is_valid).sum()}")
print(hist[hist.YEAR_ < 2024].groupby((hist.YEAR_ // 10) * 10).size().tail(8).to_string())
242 perimeters in the bounding box, 1911–2025; invalid as downloaded: 7
YEAR_
1950 13
1960 18
1970 12
1980 16
1990 22
2000 19
2010 27
2020 14
2. Do not add overlapping areas
Earlier fires overlap each other, so the sum of their areas inside the Park Fire overstates the ground they cover. Compare the naive sum with the union:
naive = earlier.area.sum() / PARK.area
union = unary_union(earlier.geometry.tolist()).area / PARK.area
print(f"sum of earlier fire areas inside Park: {naive:.0%} of the fire; their union: {union:.0%}")
sum of earlier fire areas inside Park: 152% of the fire; their union: 76%
The union counts each piece of ground once. The sum counts ground that burned three times three times.
3. Count times burned and years since
The union overlay above split the fire into pieces, each covered by the same set of earlier fires. Summarise them:
area = pieces.area / PARK.area
print(pieces.groupby(pieces.times.clip(upper=3)).apply(lambda g: area[g.index].sum(), include_groups=False)
.rename(index={3: "3+"}).map("{:.0%}".format).to_string())
since = 2024 - pieces.loc[pieces.times > 0, "last"]
w = area[pieces.times > 0]
print(f"previously burned ground: last burned a median of {np.median(np.repeat(since.values, (w * 1000).round().astype(int))):.0f} years before 2024")
print(f"burned within the 30 years before 2024 (since 1994): {area[(pieces.times > 0) & (pieces['last'] >= 1994)].sum():.0%} of the Park Fire")
times
0 24%
1 29%
2 25%
3+ 22%
previously burned ground: last burned a median of 34 years before 2024
burned within the 30 years before 2024 (since 1994): 35% of the Park Fire
The earlier fires it burned into
The data used on this page
CAL FIRE California Historical Fire Perimeters (public ArcGIS feature service California_Historic_Fire_Perimeters, layer "California Fire Perimeters (all)", 1878–2025), queried for the Park Fire's bounding box and projected to California Albers (EPSG:3310).
from pathlib import Path
import geopandas as gpd
import numpy as np
import pandas as pd
import requests
from shapely.ops import polygonize, unary_union
Path("data").mkdir(exist_ok=True)
LAYER = "https://services1.arcgis.com/jUJYIo9tSA7EHvfZ/arcgis/rest/services/California_Historic_Fire_Perimeters/FeatureServer/0/query"
F = Path("data/park_area_history.geojson")
if not F.exists():
F.write_bytes(requests.get(LAYER, params={"geometry": "-122.1,39.7,-121.5,40.4", "geometryType": "esriGeometryEnvelope", "inSR": 4326,
"spatialRel": "esriSpatialRelIntersects", "outFields": "YEAR_,FIRE_NAME,GIS_ACRES,CAUSE",
"outSR": 4326, "f": "geojson"}, timeout=300).content)
RAW = gpd.read_file(F)
hist = RAW.to_crs(3310)
hist["geometry"] = hist.geometry.make_valid().buffer(0)
PARK = hist[(hist.YEAR_ == 2024) & (hist.FIRE_NAME == "PARK")].geometry.union_all()
print(f"{len(hist)} perimeters; Park Fire {PARK.area / 4046.8564224:,.0f} acres")
242 perimeters; Park Fire 429,603 acres
Example 1: the fires it burned into
largest = earlier.assign(acres_inside=earlier.area / 4046.8564224).nlargest(6, "acres_inside")
print(largest[["YEAR_", "FIRE_NAME", "acres_inside"]].to_string(index=False, formatters={"acres_inside": "{:,.0f}".format}))
YEAR_ FIRE_NAME acres_inside
1990 CAMPBELL 126,520
1999 GUN II 60,391
1994 BARKLEY 43,905
1931 NaN 41,972
1990 FINLEY 30,976
1918 NaN 21,907
From overlapping perimeters to one surface
A reburn analysis turns a stack of overlapping perimeters into a single surface: for every piece of ground, how many earlier fires covered it and when the last one was. Polygonising the boundaries of all perimeters produces exactly those pieces. Each one lies wholly inside or wholly outside each fire, so a point test is enough to count the fires covering it. The alternative, adding up areas, double counts every overlap. In a landscape that burns repeatedly, that error is large.
The history has limits that belong in any conclusion. Perimeters before the 1950s are sparse and only larger fires were mapped. Small fires are missing in every decade, and older perimeters are coarse. "Never burned before" therefore means "no fire in the record", which for the early twentieth century says little. Recent decades are well recorded, and the share of a new fire that burned in the last twenty or thirty years is the most reliable figure this analysis gives: 35% of the Park Fire had burned since 1994.
Invalid geometry, prescribed fire and polygonising cost
- Invalid geometry: repair with
make_valid()(andbuffer(0)for leftovers) before any overlay. - Same fire twice: the archive can hold a fire as several parts or revisions; check for duplicate names and years.
- Prescribed fire is in a separate CAL FIRE layer; include it if the question is about fuels.
- Times Burned layer: CAL FIRE also publishes a precomputed "times burned" surface; this page computes it from the perimeters so the method is visible.
- Polygonize cost grows quickly with the number of perimeters; clip to the area of interest first.
Internal links
- How to download CAL FIRE perimeters — the perimeters and their codes
- Why are my perimeter overlays failing? — repairing geometry
FAQ
How do I find where a wildfire burned over earlier fires?
Overlay its perimeter with earlier perimeters: clip them to the fire, polygonise all boundaries into non-overlapping pieces, and count the earlier fires containing each piece.
Why can't I just add up the areas of earlier fires?
Earlier fires overlap each other; adding their areas counts the overlapping ground more than once.
How far back do California fire perimeters go?
CAL FIRE's archive has records from 1878, but coverage before the 1950s is sparse and limited to larger fires.
What is a reburn?
A fire that burns ground already burned by an earlier fire, often within a few decades, which can change how the vegetation recovers.