How to Count Buildings Inside a Wildfire Perimeter in Python

On this page

Problem statement

"How many buildings were inside the fire?" is usually the first question after a wildfire reaches a town, and a perimeter plus a building footprint layer answers it in a few lines. The answer is easy to over-read. A perimeter is the outer edge of the burn, so buildings inside it were exposed, not necessarily lost. Building footprints and damage records count different things. And the edge of a mapped perimeter is generalised, so some damage lies just outside it.

The 2025 Eaton Fire burned into Altadena, and CAL FIRE's damage inspection teams (DINS) recorded every structure they assessed. This page counts Overture building footprints inside the Eaton perimeter, then checks the count against those inspections.

Quick answer

Fetch footprints for the perimeter's bounding box, project both layers, and select with a spatial predicate:

inside = BUILDINGS[BUILDINGS.intersects(PERIM)]
print(f"{len(BUILDINGS):,} footprints in the box; {len(inside):,} intersect the Eaton perimeter, "
      f"{BUILDINGS.within(PERIM).sum():,} lie wholly within it")
print(f"footprint area inside: median {inside.area.median():.0f} m², total {inside.area.sum() / 1e6:.2f} km²")
66,102 footprints in the box; 11,344 intersect the Eaton perimeter, 10,678 lie wholly within it
footprint area inside: median 160 m², total 2.05 km²
Two maps of the 2025 Eaton Fire: on the left, the whole perimeter above Altadena and Pasadena with building footprints, those with a destroyed structure in red, concentrated in the western half; on the right, a close-up of part of the southern edge where several destroyed structures, marked with crosses, lie just outside the mapped line. Two maps of the 2025 Eaton Fire: on the left, the whole perimeter above Altadena and Pasadena with building footprints, those with a destroyed structure in red, concentrated in the western half; on the right, a close-up of part of the southern edge where several destroyed structures, marked with crosses, lie just outside the mapped line.
Overture building footprints inside the Eaton perimeter, matched to CAL FIRE damage inspections: 7,272 footprints with a destroyed structure, 4,072 others inside.

Intersects or within, exposed against damaged

1. Choose intersects or within deliberately

Buildings on the edge are partly inside. intersects counts them, within does not; the difference is the buildings straddling the line, and on a fire edge running through a neighbourhood that is hundreds. State which one you used.

2. Compare exposed with damaged

DINS records one point per structure inspected, with a damage class. Count them inside the perimeter:

DINS["inside"] = DINS.within(PERIM)
t = pd.crosstab(DINS.DAMAGE, DINS.inside.map({True: "inside", False: "outside"}))
print(t.loc[["Destroyed (>50%)", "Major (26-50%)", "Minor (10-25%)", "Affected (1-9%)", "No Damage", "Inaccessible"]].to_string())
inside_d = DINS[DINS.inside]
print(f"inside the perimeter: {inside_d.DAMAGE.str.startswith('Destroyed').mean():.0%} destroyed, {(inside_d.DAMAGE == 'No Damage').mean():.0%} undamaged")
inside            inside  outside
DAMAGE                           
Destroyed (>50%)    9266      153
Major (26-50%)        60       10
Minor (10-25%)       115       33
Affected (1-9%)      690      168
No Damage           2942     4951
Inaccessible           6       34
inside the perimeter: 71% destroyed, 22% undamaged

3. Look at damage outside the line

Some structures outside the mapped perimeter were destroyed. Measure how far:

out = DINS[~DINS.inside & DINS.DAMAGE.str.startswith("Destroyed")]
d = out.distance(PERIM.boundary)
print(f"{len(out)} destroyed structures outside the perimeter: median {d.median():.0f} m from it, {(d <= 100).mean():.0%} within 100 m, max {d.max():.0f} m")
153 destroyed structures outside the perimeter: median 10 m from it, 80% within 100 m, max 516 m

A mapped perimeter is drawn around the burned area at a working scale, and spot fires from embers land beyond the main front. A building just outside the line is not safe by definition.

Bar chart of CAL FIRE damage inspections for the Eaton Fire: inside the perimeter 9,266 destroyed, 60 major, 115 minor, 690 affected, 2,942 undamaged and 6 inaccessible; outside it 153 destroyed, 10 major, 33 minor, 168 affected, 4,951 undamaged and 34 inaccessible. Bar chart of CAL FIRE damage inspections for the Eaton Fire: inside the perimeter 9,266 destroyed, 60 major, 115 minor, 690 affected, 2,942 undamaged and 6 inaccessible; outside it 153 destroyed, 10 major, 33 minor, 168 affected, 4,951 undamaged and 34 inaccessible.
Most structures inside the perimeter were destroyed, but not all: 2,942 had no damage. A few were destroyed outside the line.

4. Match records to footprints

DINS points and footprints are different units: an inspection point is one structure, a footprint is one outline, and sheds, garages and units in a building can each have their own point. Match each point to the nearest footprint within 15 m:

m = gpd.sjoin_nearest(DINS, BUILDINGS[["id", "geometry"]], how="left", max_distance=15, distance_col="m")
m = m[~m.index.duplicated()]
print(f"DINS points matched to a footprint: {m.id.notna().mean():.1%}")
destroyed_ids = set(m.loc[m.DAMAGE.str.startswith("Destroyed"), "id"].dropna())
print(f"footprints inside the perimeter: {len(inside):,}; with a destroyed structure: {inside.id.isin(destroyed_ids).sum():,} ({inside.id.isin(destroyed_ids).mean():.0%})")
print(f"destroyed DINS points inside: {(DINS.inside & DINS.DAMAGE.str.startswith('Destroyed')).sum():,}")
DINS points matched to a footprint: 98.7%
footprints inside the perimeter: 11,344; with a destroyed structure: 7,272 (64%)
destroyed DINS points inside: 9,266

The 9,266 destroyed structures fall on 7,272 footprints. Example 1 below shows why: a third of them are utility and miscellaneous structures, the category for sheds and detached garages, which share a footprint with the house or have none of their own.

What kind of structures the Eaton Fire destroyed

The data used on this page

The Eaton Fire perimeter from CAL FIRE's historic fire perimeter layer; CAL FIRE's Damage Inspection (DINS) public view for the Eaton Fire; and Overture Maps building footprints for the perimeter's bounding box, read from Overture's public bucket with DuckDB. All are projected to California Albers (EPSG:3310).

import re
from pathlib import Path

import duckdb
import geopandas as gpd
import pandas as pd
import requests

Path("data").mkdir(exist_ok=True)
SERVICES = "https://services1.arcgis.com/jUJYIo9tSA7EHvfZ/arcgis/rest/services/"
per = gpd.read_file(requests.get(SERVICES + "California_Historic_Fire_Perimeters/FeatureServer/0/query",
                                 params={"where": "FIRE_NAME = 'EATON' AND YEAR_ = 2025", "outFields": "FIRE_NAME,GIS_ACRES",
                                         "outSR": 4326, "f": "geojson"}, timeout=120).text)
rows, offset = [], 0
while True:
    page = requests.get(SERVICES + "DINS_2025_Eaton_Public_View/FeatureServer/0/query",
                        params={"where": "1=1", "outFields": "DAMAGE,STRUCTURETYPE", "outSR": 4326,
                                "resultOffset": offset, "resultRecordCount": 2000, "f": "json"}, timeout=120).json()
    rows += [dict(f["attributes"], x=f["geometry"]["x"], y=f["geometry"]["y"]) for f in page["features"]]
    offset += len(page["features"])
    if not page.get("exceededTransferLimit"):
        break
dins = pd.DataFrame(rows)
DINS = gpd.GeoDataFrame(dins, geometry=gpd.points_from_xy(dins.x, dins.y), crs=4326).to_crs(3310)
FOOT = Path("data/eaton_buildings.parquet")
if not FOOT.exists():
    listing = requests.get("https://overturemaps-us-west-2.s3.amazonaws.com/", params={"list-type": "2", "prefix": "release/", "delimiter": "/"}, timeout=60).text
    release = sorted(re.findall(r"<Prefix>release/([^<]+)/</Prefix>", listing))[-1]
    x0, y0, x1, y1 = per.total_bounds + [-0.01, -0.01, 0.01, 0.01]
    con = duckdb.connect()
    con.execute("install httpfs; load httpfs; install spatial; load spatial; set s3_region = 'us-west-2'; set enable_progress_bar = false")
    con.execute(f"""copy (select id, height, subtype, class, geometry
                          from read_parquet('s3://overturemaps-us-west-2/release/{release}/theme=buildings/type=building/*.parquet', hive_partitioning = 0)
                          where bbox.xmin < {x1} and bbox.xmax > {x0} and bbox.ymin < {y1} and bbox.ymax > {y0})
                    to '{FOOT}' (format parquet)""")
BUILDINGS = gpd.read_parquet(FOOT).to_crs(3310)
PERIM = per.to_crs(3310).geometry.make_valid().union_all()
print(f"Eaton perimeter {per.GIS_ACRES.sum():,.0f} acres; {len(DINS):,} inspected structures; {len(BUILDINGS):,} Overture footprints")
Eaton perimeter 14,056 acres; 18,428 inspected structures; 66,102 Overture footprints

Example 1: what kind of structures were lost

lost = DINS[DINS.inside & DINS.DAMAGE.str.startswith("Destroyed")]
print(lost.STRUCTURETYPE.value_counts().head(5).to_string())
STRUCTURETYPE
Single Family Residence Single Story    5084
Utility Misc Structure                  3079
Single Family Residence Multi Story      831
Commercial Building Single Story          76
Multi Family Residence Multi Story        64

Trivial code, careful interpretation

Counting buildings in a perimeter is a point-in-polygon or polygon-in-polygon query, and the code is trivial. The interpretation is not. A perimeter marks where fire reached, at the scale it was mapped, and within it some buildings burn and some survive. Here more than a fifth of the inspected structures inside the line were undamaged. Building footprints and damage inspections also count differently: an inspection covers every structure, including outbuildings and separate units, while a footprint layer has one outline per mapped building. A shed or garage within 15 m of a house matches the house's outline, so several inspection points can land on one footprint.

Four answers to how many buildings were in the 2025 Eaton Fire. how many buildings were "in the fire"?counted asnumberwhat it measuresfootprints intersecting11,344exposurefootprints wholly within10,678exposure, edge excludedDINS structures destroyed9,266losses, incl. shedsfootprints, destroyed7,272buildings lostOverture footprints and CAL FIRE damage inspections, inside the final perimeter spatialworkflow.io
Four answers to one question, each measuring something different.

So a perimeter count gives exposure: the number of buildings the fire reached. For losses, use damage inspection data where it exists, or before-and-after imagery where it does not. For risk analysis, exposure is the right quantity, with the definition stated.

Footprint sources, inspection coverage and perimeter versions

Counting buildings in a fire perimeter: problems and fixes. building count problemsSymptomFix“11,344 buildings lost”inside is not lost: 22% undamagedcounts differ by hundredsstate intersects or withinlosses just outside the linebuffer the perimeter; 80% within100 mdamage points outnumber footprintssheds share a footprint; match bydistanceEaton Fire, 2025 spatialworkflow.io
Building count problems and fixes.
  • Footprint sources differ: OpenStreetMap, Microsoft and Google footprints, and Overture's merge of them, give different counts in the same place; name the source and release.
  • DINS coverage: inspections cover structures within and near the perimeter that teams could reach; 40 here were inaccessible.
  • Perimeter version: interim perimeters change during a fire; use the final one for counts after the event.
  • Units: multi-family buildings have one footprint but many homes; for households, use census or parcel data.
  • Projection: measure footprint areas and distances in an equal-area or local projected CRS, not in degrees.

FAQ

How do I count buildings inside a wildfire perimeter?

Load building footprints for the area (Overture, OSM or Microsoft), project them and the perimeter to the same CRS, and select with intersects or within.

Were all buildings inside a fire perimeter destroyed?

No. In the 2025 Eaton Fire, 71% of the structures inspected inside the perimeter were destroyed and 22% were undamaged.

Where can I get wildfire building damage data?

For California, CAL FIRE publishes Damage Inspection (DINS) points for major fires as public ArcGIS services and on the state open data portal.

Why is there damage outside the fire perimeter?

Perimeters are generalised outlines, and embers start spot fires beyond the main front. Most such damage here lay within 100 m of the mapped edge.