Introduction

In January 2025, the Palisades Fire burned through Pacific Palisades and Malibu in Los Angeles County during a Santa Ana wind event and destroyed more than 6,800 structures, making it one of the most destructive wildfires in California’s history. Almost two years later, many of those homeowners are now deciding how to rebuild, and the city is deciding what the rebuilt neighborhoods should look like.

Most advice about protecting a home from wildfire is about the home itself (home hardening) or the area around it (defensible space), but once a fire reaches a neighborhood, the biggest fuel source near a house is often the property next door. A burning home throws embers and radiant heat at its neighbors, and if the homes are close enough, the fire can move from house to house without any wildland fuel at all. For the Palisades Fire, this report asks: were homes that sat closer to other buildings destroyed more often?

After the 2018 Camp Fire in Paradise, homes more than 18 meters from a destroyed structure survived at a much higher rate than homes closer to one (Knapp et al., 2021), and in a study of the 2025 Palisades and Eaton fires, the number of nearby structures was the strongest predictor of which homes were lost, stronger than the trees around them (Kenny et al., 2026). The pattern isn’t the same in every landscape, however; across whole regions of Southern California, homes in low-density, scattered development were more likely to be lost than homes in dense neighborhoods because they sit next to more wildland (Syphard et al., 2012). Spacing between homes matters most once the fire is already inside a neighborhood, which is where most of the Palisades homes were lost.

The interactive map draws each home as a circle sized by how much space it had around it, so we can see where homes were packed together and how they fared.

Data

CAL FIRE’s Office of the State Fire Marshal inspects every structure inside a fire’s footprint after the fire and publishes the results as the CAL FIRE Damage Inspection (DINS) data on ArcGIS Online. I used the same copy as Labs 2 and 3, downloaded on October 16th, 2026, which has 132,522 structures from fires between 2013 and 2025. Each row is one inspected structure, and each has a longitude and latitude, so the data is already in the form of points.

The fields used in this report are:

  • Damage: one of five classes, from No Damage through Affected (1-10%), Minor (10-25%), Major (25-50%), and Destroyed (more than 50%). Inspectors can mark a structure “Inaccessible” if they couldn’t reach it.
  • Structure category: single-family home, multi-family home, commercial building, and other types of structures like sheds and detached garages.
  • Parcel data: the address and year built, joined by CAL FIRE from county assessor records.
  • Location: the incident name, and longitude and latitude in WGS84 (EPSG:4326).

To show where the fire burned, I also used the fire’s final perimeter from NIFC’s WFIGS Interagency Fire Perimeters layer, downloaded on October 8th, 2026. Its area of 23,442 acres matches the official acreage and lines up with CAL FIRE’s own perimeter for the fire.

Data preparation

Load packages and data

Besides the packages from earlier labs, sf turns the coordinates into point geometry and measures distances between buildings, leaflet builds the interactive map, and htmltools makes text safe to put inside a popup.

library(tidyverse) # data wrangling and ggplot2, including stringr and purrr
library(janitor) # clean_names() for tidy column names
library(sf) # simple features: spatial vector data
library(leaflet) # interactive maps
library(htmltools) # htmlEscape() for safe popup text
library(here) # builds file paths from project root
dins <- read_csv(here("GEOG588", "lab4", "data", "dins_raw.csv")) |>
  clean_names() # `* Damage` becomes `damage`, `Year Built (parcel)` becomes `year_built_parcel`

perimeter <- st_read(here("GEOG588", "lab4", "data", "palisades_perimeter.geojson"),
                     quiet = TRUE) # quiet hides the file summary sf prints

The perimeter is already in WGS84, the same CRS as DINS, so it’s ready to map.

Settings

The choices that shape the analysis are set here at the top, so they’re easy to find and change:

fire_name    <- "Palisades"
utm_crs      <- 32611 # UTM zone 11N, which measures in meters
min_gap_m    <- 1     # structures closer than this are duplicate points, not neighbors
search_m     <- 500   # look for a home's nearest neighbor within this distance
max_radius_m <- 25    # largest circle drawn on the map, so isolated homes don't dominate

damage_levels <- c(
  "No Damage",
  "Affected (>0-10%)",
  "Minor (10-25%)",
  "Major (25-50%)",
  "Destroyed (>50%)"
)

Keep the Palisades Fire structures and homes

Spacing is measured from each home to the nearest other structure of any kind, because a burning garage, apartment building, or store can ignite a home just as well as another house. I kept two tables: every structure the inspectors recorded in the fire (for the calculations), and the single-family homes (for the map).

structures <- dins |>
  filter(incident_name == fire_name) |>
  st_as_sf(coords = c("longitude", "latitude"), crs = 4326) |>
  st_transform(utm_crs) # meters, so distances are easy to compute

homes <- structures |>
  filter(
    structure_category == "Single Residence", # includes mobile homes
    damage != "Inaccessible" # not inspected, so no outcome to map
  ) |>
  mutate(
    damage     = factor(damage, levels = damage_levels), # order from least to most damage
    address    = str_to_title(replace_na(site_address_parcel, "Address not recorded")),
    year_built = na_if(year_built_parcel, 0), # 0 means the year is unknown

    # the outcome for the map and chart: destroyed, or still standing
    destroyed = damage == "Destroyed (>50%)",
    outcome   = factor(if_else(destroyed, "Destroyed", "Survived"),
                       levels = c("Survived", "Destroyed")),

    # a three-way status for the hover labels only
    status = case_when(
      destroyed             ~ "destroyed",
      damage == "No Damage" ~ "survived",
      .default              = "damaged"
    )
  )

That leaves 9,457 homes, out of 12,137 structures:

homes |>
  st_drop_geometry() |>
  count(damage) |>
  mutate(percent = n / sum(n) * 100) |>
  knitr::kable(digits = 0, col.names = c("Damage", "Homes", "% of homes"))
Damage Homes % of homes
No Damage 3263 35
Affected (>0-10%) 581 6
Minor (10-25%) 134 1
Major (25-50%) 51 1
Destroyed (>50%) 5428 57

The fire destroyed 57% of the homes it reached.

Measure the distance to the nearest structure

For each home, I wanted to calculate the distance to the closest other structure. st_is_within_distance() first finds, for each home, the row numbers of all structures within 500 meters. That list includes the home itself, at a distance of 0. The small function below then measures the distance from the home to each of those structures and keeps the shortest one.

In terms of data cleaning, DINS sometimes has two points at the same spot, such as a home recorded twice, so distances under 1 meter are dropped as duplicates rather than counted as neighbors, and because the data is in UTM, the distance is in meters.

home_xy      <- st_coordinates(homes) # x and y in meters, one row per home
structure_xy <- st_coordinates(structures)

# for each home, the row numbers of the structures within search_m (including itself)
candidates <- st_is_within_distance(homes, structures, dist = search_m)

# distance from home i to its nearest other structure
nearest_gap <- function(i) {
  rows <- candidates[[i]]
  gaps <- sqrt((structure_xy[rows, 1] - home_xy[i, 1])^2 +
               (structure_xy[rows, 2] - home_xy[i, 2])^2)
  gaps <- gaps[gaps >= min_gap_m] # drops the home itself and duplicate points
  if (length(gaps) == 0) NA_real_ else min(gaps) # NA if nothing is within search_m
}

homes <- homes |>
  mutate(
    spacing_m = map_dbl(seq_len(n()), nearest_gap),
    spacing_class = cut(
      spacing_m,
      breaks = c(0, 10, 15, 20, 30, 50, Inf),
      labels = c("Under 10", "10-15", "15-20", "20-30", "30-50", "50+"),
      right  = FALSE # 10 m goes in "10-15", not "Under 10"
    )
  )

map_dbl() runs nearest_gap() once for each home and returns the results as one column of numbers. cut() then sorts those distances into classes. The class breaks are my own choice: narrow at the short end, where most homes are, and wider above 30 meters, where there are fewer homes.

The median home was 18 meters from its nearest neighbor. That distance is measured between DINS points, which sit near the middle of each building, so the gap between walls would naturally be shorter. A home 15 meters from its neighbor’s point might have only a few meters of open ground between the two buildings.

Percent destroyed by spacing

spacing_summary <- homes |>
  st_drop_geometry() |>
  filter(!is.na(spacing_class)) |>
  group_by(spacing_class) |>
  summarize(homes = n(), pct_destroyed = mean(destroyed) * 100)

spacing_summary |>
  knitr::kable(digits = 0, format.args = list(big.mark = ","),
               col.names = c("Distance to nearest structure (m)", "Homes", "% destroyed"))
Distance to nearest structure (m) Homes % destroyed
Under 10 307 83
10-15 2,334 78
15-20 2,877 63
20-30 2,441 43
30-50 1,208 32
50+ 290 33
ggplot(spacing_summary, aes(spacing_class, pct_destroyed)) +
  geom_col(fill = "#D7191C", width = 0.7) + # same red as destroyed homes on the map
  geom_text(aes(label = str_glue("{round(pct_destroyed)}%")),
            vjust = -0.4, size = 3.5) + # percent above each bar
  geom_text(aes(y = 2, label = str_glue("n = {scales::comma(homes)}")),
            vjust = 0, size = 3, color = "white") + # home count inside each bar
  scale_y_continuous(limits = c(0, 100), expand = expansion(mult = c(0, 0.02))) +
  labs(
    x       = "Distance to nearest structure (m)",
    y       = "Homes destroyed (%)",
    caption = "Data: CAL FIRE Damage Inspection (DINS)"
  ) +
  theme_minimal(base_size = 12) +
  theme(panel.grid.major.x = element_blank()) # vertical gridlines add nothing to bars

Figure 1. Percent of single-family homes destroyed in the 2025 Palisades Fire, by distance from each home to the nearest other structure (n = 9,457 homes). Data: CAL FIRE Damage Inspection (DINS), downloaded October 16th, 2026.

The share of homes destroyed falls steadily with distance, from 83% for homes less than 10 meters from another structure to 32% for homes 30 to 50 meters away, and beyond 50 meters, it stops falling (33%). That flattening fits Syphard et al. (2012), as the most isolated homes are often on the edge of the neighborhood where they face the wildland fire directly.

Is spacing just a stand-in for where the fire burned hardest?

Closely spaced homes might also be in the parts of the fire that burned hardest, in which case spacing would look influential even if it didn’t actually cause it. One way to check that assumption is to compare only homes on “mixed” blocks, where some neighbors burned and some didn’t, since those homes went through roughly the same fire. I counted a home as being on a mixed block if 25% to 75% of the other homes within 50 meters were destroyed.

# for each home, the row numbers of the other homes within 50 meters
neighbors <- st_is_within_distance(homes, dist = 50, remove_self = TRUE)

homes <- homes |>
  mutate(
    # share of each home's neighbors that were destroyed (NaN if it has no neighbors)
    neighbors_destroyed = map_dbl(neighbors, \(rows) mean(homes$destroyed[rows])),
    mixed_block = replace_na(between(neighbors_destroyed, 0.25, 0.75), FALSE)
  )

mixed_summary <- homes |>
  st_drop_geometry() |>
  filter(mixed_block, !is.na(spacing_class)) |>
  group_by(spacing_class) |>
  summarize(homes = n(), pct_destroyed = mean(destroyed) * 100)

mixed_summary |>
  knitr::kable(digits = 0, format.args = list(big.mark = ","),
               col.names = c("Distance to nearest structure (m)",
                             "Homes on mixed blocks", "% destroyed"))
Distance to nearest structure (m) Homes on mixed blocks % destroyed
Under 10 28 54
10-15 293 63
15-20 723 56
20-30 776 53
30-50 273 47

On mixed blocks, the pattern is much weaker but still there: homes 10 to 15 meters from another structure were destroyed 63% of the time, compared with 47% for homes 30 to 50 meters away, a gap of 16 percentage points instead of 46 across the whole fire. The “Under 10” class has only 28 homes on mixed blocks, too few to read much into, and there’s no “50+” class at all, because a mixed block requires neighbors within 50 meters.

Most of the difference in the bar chart comes from where closely spaced homes were, as the dense parts of the neighborhood were also where the fire burned through whole blocks. Even among neighbors who faced roughly the same fire, the homes with more space around them still did better, however, which matches Kenny et al. (2026), who found structure density was the strongest predictor of loss in a model that also included tree canopy and wind.

Hover labels and popup text

Each home’s hover label and click popup are built as columns before mapping. Because leaflet reads popups as HTML, htmlEscape() is applied to text from the data so that a character such as < in an address is displayed rather than read as markup.

homes_map <- homes |>
  st_transform(4326) |> # back to longitude and latitude for leaflet
  mutate(
    spacing_text = if_else(is.na(spacing_m),
                           str_glue("over {search_m} m"),
                           str_glue("{round(spacing_m)} m")),
    label = str_glue("{address}: {spacing_text} to nearest structure ({status})"),
    popup = str_glue(
      "<b>{htmlEscape(address)}</b><br>",
      "Outcome: {outcome} ({htmlEscape(damage)})<br>",
      "Nearest structure: {spacing_text}<br>",
      "Year built: {replace_na(as.character(year_built), 'Unknown')}"
    ),
    # circle radius in meters: half the distance to the nearest structure, capped
    radius_m = pmin(replace_na(spacing_m / 2, max_radius_m), max_radius_m)
  ) |>
  arrange(desc(radius_m)) # draw small circles last so they sit on top of big ones

The map

Design choices

Each home is drawn as a circle whose radius is half the distance to its nearest structure. The radius is capped at 25 meters so that the few isolated homes do not cover the map. I used addCircles(), which sizes circles in meters, rather than addCircleMarkers() from earlier labs, which sizes them in screen pixels, so each circle covers the same ground at every zoom level.

Color shows outcome, the main variable. Survived and destroyed are categories rather than amounts, so they use two contrasting hues, and I used blue and red so they remain distinguishable for those with color blindness. The fire perimeter is a semi-transparent gray fill without an outline, which keeps it visually beneath the homes. The basemap combines three Esri layers that do not require an API key: a light gray base, a semi-transparent hillshade to show the canyons and ridges, and street labels drawn above the circles.

outcome_colors <- colorFactor(palette = c("#2C7BB6", "#D7191C"),
                              levels  = levels(homes$outcome))
esri_gray_base      <- "https://server.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Light_Gray_Base/MapServer/tile/{z}/{y}/{x}"
esri_gray_reference <- "https://server.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Light_Gray_Reference/MapServer/tile/{z}/{y}/{x}"
esri_hillshade      <- "https://server.arcgisonline.com/ArcGIS/rest/services/Elevation/World_Hillshade/MapServer/tile/{z}/{y}/{x}"

Building the map

The label tiles sit in their own map pane above the circles so street names remain readable, and the onRender() call at the end disables mouse interaction on that pane so the labels do not block hovers and clicks on the circles beneath them.

leaflet(homes_map) |>
  # a pane for the labels above the circles (the default layer pane is z-index 400)
  addMapPane("labels", zIndex = 650) |>

  # 1. light basemap without labels
  addTiles(urlTemplate = esri_gray_base,
           attribution = "Basemap: Esri, HERE, Garmin, OpenStreetMap contributors",
           options = tileOptions(maxNativeZoom = 16, maxZoom = 19)) |>
  # 2. hillshade, semi-transparent, so the terrain shows through
  addTiles(urlTemplate = esri_hillshade,
           attribution = "Hillshade: Esri, USGS, NGA",
           options = tileOptions(opacity = 0.3, maxNativeZoom = 15, maxZoom = 19)) |>
  # 3. street and place labels, in the pane above the circles
  addTiles(urlTemplate = esri_gray_reference,
           options = tileOptions(pane = "labels", maxNativeZoom = 16, maxZoom = 19)) |>

  # 4. fire perimeter under the circles: gray fill, no outline, not clickable
  addPolygons(data        = perimeter,
              stroke      = FALSE,
              fillColor   = "#565555",
              fillOpacity = 0.35,
              options     = pathOptions(interactive = FALSE)) |>

  # 5. homes as "personal space" circles
  addCircles(
    radius      = ~radius_m, # meters on the ground, so circles scale with zoom
    fillColor   = ~outcome_colors(outcome),
    fillOpacity = 0.75,
    color       = "white", # thin white outline to separate touching circles
    weight      = 0.5,
    label       = ~label, # shown on hover
    popup       = ~popup # shown on click
  ) |>

  # legends and scale bar
  addLegend(
    position = "bottomright",
    pal      = outcome_colors,
    values   = ~outcome,
    title    = "Outcome",
    opacity  = 0.9
  ) |>
  addLegend(
    position = "bottomright",
    colors   = "#565555",
    labels   = "Fire perimeter",
    opacity  = 0.35
  ) |>
  addScaleBar(position = "bottomleft") |>

  # let hovers and clicks pass through the label tiles to the circles below
  htmlwidgets::onRender("function(el, x) { this.getPane('labels').style.pointerEvents = 'none'; }")

Figure 2. Single-family homes inspected by CAL FIRE after the January 2025 Palisades Fire (n = 9,457). Each circle’s radius is half the distance from the home to its nearest other structure, capped at 25 meters, and color shows whether the home survived or was destroyed. The gray area is the fire’s final perimeter. Data: CAL FIRE Damage Inspection (DINS), downloaded October 16th, 2026; fire perimeter from NIFC WFIGS Interagency Fire Perimeters, downloaded October 8th, 2026.


What the map shows

At the full extent, destroyed homes form large, contiguous patches across the flatter, more densely built parts of Pacific Palisades, and most surviving homes lie near the edges of the burned area, where the fire either stopped or was stopped.

At street scale, the circles show the spacing behind Figure 1. In the densest blocks, the circles are small and nearly all red. Where homes are spread out along the canyon roads, the circles are larger and more of them are blue.

This supports the conclusions of the studies cited in the introduction (Knapp et al., 2021; Kenny et al., 2026). Those studies measured spacing from building outlines and imagery, whereas this report measures between DINS points, so the distances here are longer than theirs, but the direction of the relationship is the same.

Limitations

  • Cause and effect: Spacing is associated with survival, but this report does not prove that spacing caused it. The mixed-block check indicates that most of the fire-wide pattern reflects where closely spaced homes were, and only part of it holds among neighbors. DINS also does not record vegetation, wind, or whether firefighters defended a home.
  • Point distances, not wall-to-wall gaps: DINS records one point per structure, so two pairs of homes at the same point-to-point distance receive the same circles even if the gaps between their walls differ–building footprints would give a better measure.
  • Neighbors outside the inspection area: DINS includes only structures the inspectors visited, so homes near the fire’s edge may have uninspected neighbors, which means their spacing is overestimated.
  • One fire: These results come from one wind-driven fire, so fires with less wind or more wildland around the homes may show a different pattern.

Conclusion

In the Palisades Fire, homes close to other buildings were destroyed far more often than homes with space around them: 83% of homes less than 10 meters from their nearest neighbor were destroyed, compared with 32% of homes 30 to 50 meters away. Much of that gap reflects where the dense blocks were, but a smaller gap of 16 percentage points remains among neighbors on mixed blocks. This is consistent with findings from the Camp and Eaton fires, and it suggests that once a fire is inside a neighborhood, each home’s risk depends partly on its neighbors. Neighborhood-scale measures, such as hardening the homes on both sides of a narrow gap, keeping sheds and fences from bridging the space between homes, and setting minimum spacing for new homes, deserve attention in this area, and this underpins the importance of community-scale mitigation programs like Firewise and Fire Safe Councils.

Data sources and references

  • CAL FIRE, Office of the State Fire Marshal. CAL FIRE Damage Inspection (DINS) Data. ArcGIS Online feature layer. Item page. Downloaded 16 October 2026 as a CSV from the ArcGIS Hub export.
  • National Interagency Fire Center (NIFC). WFIGS Interagency Fire Perimeters. ArcGIS Online feature layer. Feature service. Palisades Fire final perimeter, downloaded 8 October 2026.
  • Kenny, R., Johns, J., Pawlak, C., Fricker, A., Yost, J., & Ritter, M. (2026). Urban trees and structure loss in the 2025 Eaton and Palisades fires. Urban Forestry & Urban Greening, 121, 129470. https://doi.org/10.1016/j.ufug.2026.129470
  • Knapp, E. E., Valachovic, Y. S., Quarles, S. L., & Johnson, N. G. (2021). Housing arrangement and vegetation factors associated with single-family home survival in the 2018 Camp Fire, California. Fire Ecology, 17, 25. https://doi.org/10.1186/s42408-021-00117-0
  • Syphard, A. D., Keeley, J. E., Massada, A. B., Brennan, T. J., & Radeloff, V. C. (2012). Housing arrangement and location determine the likelihood of housing loss due to wildfire. PLoS ONE, 7(3), e33954. https://doi.org/10.1371/journal.pone.0033954
  • Basemaps: Esri World Light Gray Canvas (base and reference layers) and Esri World Hillshade.
  • Johnson, M. Interactive Mapping in R: Leaflet (course tutorial).
  • R packages: tidyverse, janitor, sf, leaflet, htmltools, here, and knitr.