Introduction

California has lost thousands of homes to wildfire in recent years. One way that state building code regulations, insurers, and grant programs have responded is by promoting home hardening: building or retrofitting homes so that embers and heat are less likely to ignite them. Some of these changes are expensive, like replacing roofs, while others are cheap, like covering attic vents. CAL FIRE has collected this Damage Inspection (DINS) data to document losses, but because they include construction details like roof material and vent screens, they can also be used to compare how hardened and unhardened homes fared. We’ll interpret their hand-collected data to determine which strategies are most effective, answering the question

Did homes with hardening features survive wildfires more often than homes without them, and which kinds of home hardening matter most?

This report breaks the research questions into three parts: how much the share of homes destroyed varies from fire to fire, how home hardening features are associated with home survival within the same fire, and which features made the most difference, studying whether small, inexpensive fixes hold up against larger ones.

Data

Source

CAL FIRE’s Office of the State Fire Marshal publishes the inspections as the CAL FIRE Damage Inspection (DINS) data layer on ArcGIS Online. The copy used here was downloaded on October 16th, 2026, and is the same file cleaned in Lab 2, with 132,522 structures from fires that started between 2013 and 2025. Each row is one structure inspected after the fire.

What an inspection records

For every structure, inspectors fill in:

  • Outcome: a damage class, from No Damage through Affected (1-10%), Minor (10-25%), Major (25-50%), and Destroyed (more than 50%).
  • Structure: its type and category (single residence, multi-family, commercial, outbuilding).
  • Construction: roof material, exterior siding, window panes, vent screens, and eaves.
  • Attached and nearby fuels: decks and porches, patio covers and carports, fences attached to the house, the distance to a propane tank, and the distance to the nearest outbuilding.
  • Parcel data joined from county records: year built, assessed value, and parcel number.
  • Fire and location: incident name and start date, county, and coordinates.

The construction and fuels fields describe the hardening features this report compares. I grouped them into four categories, plus year built as a proxy for the updated building code changes in 2008:

Category DINS field Values inspectors choose from Hardened if
Roof Roof Construction Asphalt, Wood, Tile, Metal, Concrete, Fire Resistant, Combustible, Other Tile, metal, concrete, or fire resistant
Home materials Exterior Siding Wood, Vinyl, Stucco/Brick/Cement, Metal, Ignition Resistant, Combustible, Other Not wood, vinyl, or combustible
Home materials Window Pane Single Pane, Multi Pane, No Windows Multi-pane
Small mitigations Vent Screen Mesh Screen <= 1/8”, Mesh Screen > 1/8”, Unscreened, No Vents Fine mesh, or no vents
Small mitigations Eaves Enclosed, Unenclosed, No Eaves Enclosed
Defensible space Deck/Porch (on grade and elevated) Wood, Composite, Masonry/Concrete, Metal, No Deck/Porch No wood deck or porch
Defensible space Fence Attached to Structure Combustible, Non Combustible, No Fence No fence, or non-combustible
Defensible space Patio Cover/Carport Attached Combustible, Non Combustible, None No cover, or non-combustible
Defensible space Distance to Propane Tank 0-10, 11-20, 21-30, >30 ft 21 ft or more
Defensible space Distance to Outbuilding <30’, 30-50’, >50’ 30 ft or more
Building code Year Built (parcel) Year 2008 or later

Every field also allows “Unknown”, and many are left blank.

Known limitations

Three limitations should be noted before any analysis:

  • The attached-fuels and distance fields were added to the inspection form in 2018, and before 2018 inspectors recorded few undamaged homes (Lab 2). For that reason I use fires from 2018 onward only.
  • There is no field for vegetation clearance around the home, which is what “defensible space” usually means. The attached fuels and two distance fields are the closest substitutes in this layer.
  • Inspectors record roof material, not fire rating. Most asphalt shingle roofs carry a Class A fire rating, so treating asphalt as “not hardened” is a bit of a simplification.

Data preparation

Load packages and data

Besides the Lab 2 packages, patchwork allows for the combination of several ggplot2 plots into one figure, and RColorBrewer provides color palettes designed for maps and charts.

library(tidyverse) # data wrangling
library(here) # builds file paths from project root
library(janitor) # clean_names() for tidy column names
library(patchwork) # combines ggplots into one figure
library(RColorBrewer) # color palettes

# set one theme for every plot in the report
theme_set(
  theme_minimal(base_size = 12) +
    theme(
      plot.title    = element_text(hjust = 0.5), # center titles over the plot
      plot.subtitle = element_text(hjust = 0.5)
    )
)
dins <- read_csv(here("data", "dins_raw.csv"))

Check categories before classifying them

Before sorting each feature into hardened/not hardened, I counted the values inspectors entered. Vent screen is a good example of why:

dins |>
  clean_names() |>
  count(vent_screen, sort = TRUE)
## # A tibble: 11 × 2
##    vent_screen                  n
##    <chr>                    <int>
##  1 "Unknown"                40139
##  2 "Mesh Screen > 1/8\"\""  34365
##  3 "No Vents"               26071
##  4 "Mesh Screen <= 1/8\"\"" 18226
##  5 "Screened"                5736
##  6  <NA>                     5302
##  7 "Unscreened"              2679
##  8 "21-30"                      1
##  9 ">30"                        1
## 10 "Attached Fence"             1
## 11 "Deck Elevated"              1

The mesh labels have some stray quotation marks, so matching the full label could fail. I ran the same check on the other fields and found three more issues:

  • The 2018 Camp Fire used different words for siding (“Combustible” and “Ignition Resistant”) than later fires (“Wood” or “Stucco Brick Cement”) for example.
  • A few values seem to have been entered in the wrong column, like “Asphalt” under windows. Those are excluded below.
  • The Camp Fire has about 3,000 homes with every construction field blank, so those drop out of every feature comparison.

Classify each feature

This wrangles each feature into “hardened”, “not_hardened”, or missing (NA), following the table in the Data section above. I chose to do this in a small function rather than a long series of case_when(), since it seemed more efficient.

# Sort raw inspector categories into "hardened" / "not hardened"
# Values that are in neither list become NA

classify <- function(x, hardened, not_hardened) {
  case_when(
    x %in% hardened ~ "hardened",
    x %in% not_hardened ~ "not_hardened"
  )
}

no_wood_deck <- c("No Deck/Porch", "Masonry/Concrete", "Composite", "Metal")

homes <- dins |>
  clean_names() |>
  rename(
    year_built       = year_built_parcel,
    propane_dist     = starts_with("distance_propane"),
    outbuilding_dist = starts_with("distance_residence")
  ) |>
  mutate(
    fire_year = as.integer(str_extract(incident_start_date, "\\d{4}")),
    fire_id   = paste(incident_name, fire_year),   # Lab 2: fire names repeat across years
    destroyed = damage == "Destroyed (>50%)"
  ) |>
  # single homes in 2018+ fires since attachment fields were not recorded before 2018
  filter(
    structure_category == "Single Residence",
    fire_year >= 2018,
    damage != "Inaccessible"
  ) |>
  mutate(
    eaves_unknown = eaves == "Unknown",   # kept to check recording quality later
    # Roof
    roof    = classify(roof_construction,
                       c("Tile", "Metal", "Concrete", "Fire Resistant"),
                       c("Asphalt", "Wood", "Combustible")),

    # Home materials
    siding  = classify(exterior_siding,
                       c("Stucco Brick Cement", "Stucco/Brick/Cement", "Metal",
                         "Fire Resistant", "Ignition Resistant"),
                       c("Wood", "Vinyl", "Combustible")),
    windows = classify(window_pane, "Multi Pane", "Single Pane"),

    # Small mitigations
    vents   = case_when(
      str_detect(vent_screen, "<= 1/8") | vent_screen == "No Vents"   ~ "hardened",
      str_detect(vent_screen, "> 1/8")  | vent_screen == "Unscreened" ~ "not_hardened"
    ),
    eaves   = classify(eaves, "Enclosed", "Unenclosed"),

    # Defensible space (attached fuels and separation)
    deck    = case_when(
      deck_porch_on_grade == "Wood" | deck_porch_elevated == "Wood" ~ "not_hardened",
      deck_porch_on_grade %in% no_wood_deck &
        deck_porch_elevated %in% no_wood_deck                       ~ "hardened"
    ),
    fence   = classify(fence_attached_to_structure,
                       c("No Fence", "Non Combustible"), "Combustible"),
    patio   = classify(patio_cover_carport_attached_to_structure,
                       c("No Patio Cover/Carport", "Non Combustible"), "Combustible"),
    propane     = classify(propane_dist, c("21-30", ">30"), c("0-10", "11-20")),
    outbuilding = classify(outbuilding_dist, c("30-50'", ">50'"), "<30'"),

    # Building code: California's WUI code (Chapter 7A) applies to homes built 2008 on.
    # Placeholder zeros and impossible years (Lab 2) match neither line and become NA.
    built_2008 = case_when(
      year_built >= 2008 & year_built <= fire_year ~ "hardened",
      year_built >= 1850 & year_built <  2008      ~ "not_hardened"
    )
  )

After filtering, the data holds 75,676 single-family homes from fires in 2018 or later, and 49.2% of them were destroyed.

Reshape to one row per home per feature

To plot all eleven features with one ggplot() call, I had to stack them in a single column. A lookup table gives each feature a readable label and its category, and pivot_longer() does the stacking.

features <- tribble(
  ~feature,      ~label,                                     ~category,
  "roof",        "Roof: tile, metal, or concrete",           "Roof",
  "siding",      "Siding: non-combustible",                  "Home materials",
  "windows",     "Windows: multi-pane",                      "Home materials",
  "vents",       "Vents: fine mesh or none",                 "Small mitigations",
  "eaves",       "Eaves: enclosed",                          "Small mitigations",
  "deck",        "Deck/porch: no wood",                      "Defensible space",
  "fence",       "Attached fence: none or non-combustible",  "Defensible space",
  "patio",       "Patio cover: none or non-combustible",     "Defensible space",
  "propane",     "Propane tank: 21+ ft away",                "Defensible space",
  "outbuilding", "Outbuilding: 30+ ft away",                 "Defensible space",
  "built_2008",  "Built 2008 or later (WUI code)",           "Building code"
)

homes_long <- homes |>
  select(fire_id, destroyed, all_of(features$feature)) |>
  pivot_longer(-c(fire_id, destroyed), names_to = "feature", values_to = "status") |>
  drop_na(status) |> # drop unknowns per feature, not per home
  left_join(features, by = "feature")

# one color per category, shared by Plots 2 and 3 so the legend means the same thing
category_colors <- setNames(brewer.pal(5, "Dark2"), unique(features$category))

Dropping missing values after pivoting was on purpose, because for example a home with unknown eaves still counts in the vent comparison. If I dropped every home with any unknown field first, I would keep about half the homes, and the ones kept would lean toward survivors, because destroyed homes are recorded less completely.

How complete is each feature once unknowns are removed?

homes_long |>
  count(category, label, name = "n_homes") |>
  mutate(pct_of_homes = n_homes / nrow(homes) * 100) |>
  arrange(desc(pct_of_homes)) |>
  knitr::kable(digits = 0, col.names = c("Category", "Feature", "Homes", "% of homes"))
Category Feature Homes % of homes
Defensible space Attached fence: none or non-combustible 63151 83
Home materials Siding: non-combustible 62976 83
Roof Roof: tile, metal, or concrete 62020 82
Defensible space Deck/porch: no wood 60112 79
Building code Built 2008 or later (WUI code) 59154 78
Defensible space Patio cover: none or non-combustible 59140 78
Home materials Windows: multi-pane 56798 75
Small mitigations Vents: fine mesh or none 53717 71
Small mitigations Eaves: enclosed 39605 52
Defensible space Propane tank: 21+ ft away 12619 17
Defensible space Outbuilding: 30+ ft away 12419 16

Most features are known for three-quarters or more of the homes. Eaves are known for about half, and the propane and outbuilding distances for less than a fifth, so results for those three are a bit weaker in their sample size.

Analysis

1. Does where a home burned matter more than how it was built?

Before comparing homes with and without a feature, I checked how much the incident itself matters, because if some fires destroyed nearly all homes and others almost none, grouping all together would mix up fires with features.

# one row per fire: homes inspected and proportion destroyed
fires <- homes |>
  group_by(fire_id, fire_year) |>
  summarize(n = n(), pct_destroyed = mean(destroyed) * 100, .groups = "drop") |>
  filter(n >= 20) # small fires are less conclusive for percentages
# left panel: one point per fire, by year
p1 <- fires |>
  ggplot(aes(x = factor(fire_year), y = pct_destroyed)) +
  geom_jitter(aes(size = n), width = 0.2, alpha = 0.5, color = "#B2182B") +
  scale_size_area(max_size = 8, breaks = c(100, 1000, 10000), labels = scales::comma) +
  scale_y_continuous(labels = scales::label_percent(scale = 1), limits = c(0, 100)) +
  labs(
    x    = "Fire year",
    y    = "Homes destroyed",
    size = "Homes inspected"
  )

# right panel: histogram of the same values on the same vertical scale
p1_hist <- fires |>
  ggplot(aes(y = pct_destroyed)) +
  geom_histogram(binwidth = 10, boundary = 0, fill = "#B2182B", alpha = 0.5, color = "white") +
  scale_y_continuous(limits = c(0, 100)) +
  labs(x = "Number of fires", y = NULL) +
  theme(axis.text.y = element_blank()) # left panel already labels this axis

# combine side by side with patchwork; the histogram is narrower
p1 + p1_hist +
  plot_layout(widths = c(3, 1), guides = "collect") +
  plot_annotation(
    title = "The share of homes destroyed varied widely from fire to fire",
    theme = theme(plot.title = element_text(hjust = 0.5))
  )
Figure 1. Percent of single-family homes destroyed in each of 95 California fires with at least 20 inspected homes, 2018-2025. Left: each point is a fire, sized by the number of homes inspected and spread sideways within its year so points don’t overlap. Right: the number of fires in each 10-percentage-point band.
Figure 1. Percent of single-family homes destroyed in each of 95 California fires with at least 20 inspected homes, 2018-2025. Left: each point is a fire, sized by the number of homes inspected and spread sideways within its year so points don’t overlap. Right: the number of fires in each 10-percentage-point band.

Outcomes vary far more between fires than I expected. The typical fire destroyed 26% of the homes inspected, but individual fires range from 0% to 96%. The histogram shows that 53 of the 95 fires destroyed less than 30% of homes, while 10 destroyed 70% or more. The 2018 fires, which include the Camp, Woolsey, and Carr fires, sit well above every other year, with a median of 81%. Hazard components like wind, terrain, and how quickly a fire reached town drive these differences, none of which are properties of the home.

In terms of the rest of the analysis, hardening features are not spread evenly across fires, as the various incidents in this dataset affected homes that had been built in wildly different eras, price points, and landscapes. The Camp Fire burned through an older town with mostly asphalt roofs, while the 2025 Los Angeles fires burned newer, more expensive homes. If I pooled every home, a feature that happens to be common in the worst fires would look harmful no matter what it does. As a result, from here on, I compare homes within the same fire only (the percent destroyed among homes with a feature against the percent among homes without it). As an added bonus, this also cancels out differences in how inspections were recorded from year to year, since both groups in a fire were inspected under the same rules.

2. Within the same fire, did homes with each feature survive more often?

Fire research describes three ways a home ignites: embers blown ahead of the fire that enter openings or land on something combustible, radiant heat that breaks windows, and direct flame from fuels next to the house. Embers cause the majority of losses. Each hardening feature examined here blocks one of these pathways, so I expect homes with each feature to be destroyed less often than their neighbors without it.

For each fire and each feature, I computed the percent destroyed with and without the feature. I kept only fires with at least 20 homes in each group, so that we don’t get cases where a handful of homes produced a 0% or 100% rate by chance.

# one row per fire per feature: % destroyed with and without the feature
by_fire <- homes_long |>
  group_by(category, feature, label, fire_id, status) |>
  summarize(n = n(), pct = mean(destroyed) * 100, .groups = "drop") |>
  pivot_wider(names_from = status, values_from = c(n, pct)) |>
  filter(n_hardened >= 20, n_not_hardened >= 20) |>   # both groups big enough
  mutate(gap = pct_not_hardened - pct_hardened) # positive = feature did better

# how many fires each feature was compared in, and how many it did better in
fire_counts <- by_fire |>
  group_by(feature) |>
  summarize(fires = n(), better = sum(gap > 0), pct_better = better / fires * 100)

pct_better <- setNames(round(fire_counts$pct_better), fire_counts$feature)
n_better   <- setNames(fire_counts$better, fire_counts$feature)
n_compared <- setNames(fire_counts$fires,  fire_counts$feature)
# triangle below the 1:1 line = fires where homes with the feature were destroyed less often
helped_zone <- tibble(x = c(0, 100, 100), y = c(0, 0, 100))

p2 <- by_fire |>
  mutate(label = factor(label, levels = features$label)) |>   # keep the category order
  ggplot(aes(x = pct_not_hardened, y = pct_hardened)) +
  geom_polygon(data = helped_zone, aes(x = x, y = y), fill = "grey92") +
  geom_abline(slope = 1, intercept = 0, color = "grey60") +
  annotate("text", x = 97, y = 4, label = "feature helped", hjust = 1,
           size = 2.8, color = "grey45") +
  geom_point(aes(size = n_hardened + n_not_hardened, color = category), alpha = 0.6) +
  # axes = "all" repeats the axis numbers on every panel, not just the outer ones
  facet_wrap(~ label, ncol = 4, labeller = label_wrap_gen(width = 24), axes = "all") +
  scale_color_manual(values = category_colors) +
  scale_size_area(max_size = 5, guide = "none") +
  coord_equal(xlim = c(0, 100), ylim = c(0, 100)) +
  labs(
    title    = "Within the same fire, windows, vents, eaves, and newer homes fared better",
    subtitle = "Points in the shaded area are fires where homes with the feature were destroyed less often",
    x        = "% destroyed without the feature",
    y        = "% destroyed with the feature",
    color    = NULL
  ) +
  theme(legend.position = "bottom")
p2
Figure 2. Percent of homes destroyed with and without each hardening feature, one point per fire, sized by the number of homes compared. Only fires with at least 20 homes in each group are shown.
Figure 2. Percent of homes destroyed with and without each hardening feature, one point per fire, sized by the number of homes compared. Only fires with at least 20 homes in each group are shown.

Several features match the expectation. Homes with multi-pane windows were destroyed less often than single-pane homes in 94% of the fires where both could be compared, and homes with fine-mesh vents or no vents did better in 62%. Homes built in 2008 or later did better in 13 of 16 fires. Their points fall in the shaded area in nearly every fire.

Three features appear to contradict it. Homes with no attached fence or a non-combustible one sit above the line in most fires: they did better in only 5% of fires. Non-combustible siding did better in 24% and homes without wood decks in 32%. If we were to take that at face value, this says a wood fence or wood siding protects a home, which contradicts fire research and basic ideas of combustion.

The Camp Fire, the largest point near the upper right of most panels, shows almost no difference for any single construction feature: nearly everything burned. Even there, year built still separates the homes, as the next chunk shows.

homes |>
  filter(fire_id == "Camp 2018") |>
  mutate(era = case_when(
    year_built >= 1850 & year_built < 1960       ~ "Built before 1960",
    year_built >= 2008 & year_built <= fire_year ~ "Built 2008 or later"
  )) |>
  drop_na(era) |>
  group_by(era) |>
  summarize(homes = n(), pct_destroyed = mean(destroyed) * 100) |>
  knitr::kable(digits = 0, col.names = c("Camp Fire homes", "Homes", "% destroyed"))
Camp Fire homes Homes % destroyed
Built 2008 or later 412 47
Built before 1960 4042 89

Fences, siding, and decks may be reversed because inspections happen after the fire. For a home that survived, the inspector can see the fence and the siding, but for a home that burned to its foundation, the inspector has to judge from debris, and a wood fence that burned along with the house could be recorded as “No Fence”.

The data supports this explanation–destroyed homes are recorded less completely: eaves were marked “Unknown” for 52% of destroyed homes but only 20% of survivors. Features that burn away with the house (fences, decks, wood siding) are the ones most susceptible to this problem. Windows, vents, and year built are less affected, because a window frame or vent opening leaves evidence and year built comes from county records rather than the inspection.

Within the same fire, homes with better windows, vents, and newer construction survived more often, and this data can’t quite answer the question for fences, siding, or decks.

3. Which features matter most?

Figure 2 shows direction, but not size. To rank the features, I reduced each panel to one number: the average difference across fires weighted by the number of homes so that large fires count more. The 95% interval shows how consistent the difference is from fire to fire. Each fire counts as one comparison, not each home, because homes in the same fire burned under the same conditions and are not independent of each other.

For comparison, I also computed the pooled difference that results from ignoring which fire a home was in. The distance between the two shows how much Figure 1’s warning matters for each feature.

# pooled difference: all homes together, ignoring which fire they were in
pooled <- homes_long |>
  group_by(feature) |>
  summarize(gap_pooled = 100 * (mean(destroyed[status == "not_hardened"]) -
                                mean(destroyed[status == "hardened"])))

# within-fire difference: weighted average of the per-fire differences in Figure 2
gaps <- by_fire |>
  group_by(category, feature, label) |>
  summarize(
    fires      = n(),
    gap_within = weighted.mean(gap, n_hardened + n_not_hardened),  # big fires count more
    ci         = 1.96 * sd(gap) / sqrt(fires), # fires as repeats
    .groups    = "drop"
  ) |>
  left_join(pooled, by = "feature")

# named vectors for the inline numbers in the text below
gap_within <- setNames(round(gaps$gap_within, 1), gaps$feature)
gap_pooled <- setNames(round(gaps$gap_pooled, 1), gaps$feature)
gap_low    <- setNames(round(gaps$gap_within - gaps$ci, 1), gaps$feature)

gaps |>
  arrange(desc(gap_within)) |>
  transmute(label, fires, gap_within, low = gap_within - ci, high = gap_within + ci, gap_pooled) |>
  knitr::kable(digits = 1, col.names = c("Feature", "Fires", "Within-fire gap",
                                         "95% low", "95% high", "Pooled gap"))
Feature Fires Within-fire gap 95% low 95% high Pooled gap
Built 2008 or later (WUI code) 16 19.0 13.5 24.5 23.1
Windows: multi-pane 53 13.1 9.2 16.9 12.7
Vents: fine mesh or none 52 11.3 8.0 14.5 16.6
Eaves: enclosed 45 6.7 3.9 9.6 9.7
Roof: tile, metal, or concrete 61 3.8 1.1 6.5 15.3
Propane tank: 21+ ft away 29 3.1 -0.3 6.5 8.1
Outbuilding: 30+ ft away 18 1.7 -2.1 5.5 11.6
Patio cover: none or non-combustible 59 -2.3 -4.3 -0.3 -1.5
Deck/porch: no wood 62 -7.0 -10.2 -3.9 -4.4
Siding: non-combustible 54 -15.3 -18.8 -11.8 -7.2
Attached fence: none or non-combustible 38 -15.6 -21.0 -10.2 -19.0
p3 <- gaps |>
  mutate(label = fct_reorder(label, gap_within)) |>
  ggplot(aes(y = label)) +
  geom_vline(xintercept = 0, color = "grey40") +
  geom_segment(aes(x = gap_pooled, xend = gap_within, yend = label), color = "grey80") +
  geom_point(aes(x = gap_pooled), shape = 21, fill = "white", color = "grey40", size = 2.5) +
  geom_pointrange(aes(x = gap_within, xmin = gap_within - ci, xmax = gap_within + ci,
                      color = category)) +
  scale_color_manual(values = category_colors) +
  labs(
    title    = "Which hardening features were linked to more homes surviving?",
    subtitle = "Filled: within the same fire, with 95% interval, hollow: all fires pooled",
    x        = "Percentage points fewer homes destroyed when the feature is present",
    y        = NULL,
    color    = NULL
  )
p3
Figure 3. Percentage-point difference in homes destroyed between homes without and with each feature. Within-fire values are averaged across fires, weighted by the number of homes inspected in each fire.
Figure 3. Percentage-point difference in homes destroyed between homes without and with each feature. Within-fire values are averaged across fires, weighted by the number of homes inspected in each fire.

Do small, inexpensive fixes matter as much as major materials?

Since most homes are lost to embers, one might expect the small fixes that close openings (fine-mesh vent screens and enclosed eaves) to matter about as much as replacing a roof or siding, which the data supports. Among single features, multi-pane windows (13.1 points) and fine-mesh or no vents (11.3 points) rank highest, followed by enclosed eaves (6.7 points). All three rank above roof material (3.8 points). Vent screens are one of the cheapest retrofits on the list, which makes this a very actionable result.

The roof result is the main surprise. Pooled, a tile, metal, or concrete roof looks like one of the strongest features, at 15.3 points. Within fires, though, the difference falls to 3.8 points. Most of the pooled number comes from the Camp Fire, which is the problem Figure 1 warned about:

homes |>
  filter(fire_id == "Camp 2018", !is.na(roof)) |>
  summarize(
    homes             = n(),
    pct_destroyed     = mean(destroyed) * 100,
    pct_hardened_roof = mean(roof == "hardened") * 100
  ) |>
  knitr::kable(digits = 0, col.names = c("Camp Fire homes with known roof", "% destroyed",
                                         "% tile, metal, or concrete roof"))
Camp Fire homes with known roof % destroyed % tile, metal, or concrete roof
14039 97 15

The Camp Fire contributed thousands of homes that almost all burned and almost all had asphalt roofs to the dataset. Pooled with other fires, it makes asphalt look deadly, when inside the Camp Fire itself roof material made little difference.

Did homes built under the 2008 building code fare better?

California’s wildland-urban interface (WUI) building code, Chapter 7A, has applied to new homes since 2008 and requires many of these features at once, so newer homes should do better than older ones in the same fire. This dataset supports that conclusion: homes built in 2008 or later had the largest advantage of any feature, 19 percentage points fewer destroyed than older homes in the same fire. This feature actually bundles several: the code requires ember-resistant vents, enclosed eaves, multi-pane windows, and Class A roofs, and newer homes also have newer materials overall. However, it rests on only 16 fires, because most fires had too few newer homes to compare.

Does keeping potential fuels away from the house help?

Wood decks, attached fences, propane tanks, and sheds can carry flame directly to the house, so homes with fewer of them nearby should fare better. This data didn’t support a strong conclusion in that regard. Propane tanks 21 feet or more away (3.1 points) and outbuildings 30 feet or more away (1.7 points) point in the expected direction, but their intervals reach below zero (the propane interval starts at -0.3), and both are recorded for less than a fifth of homes. The attached fence, deck, and patio cover reverse sign for the recording-based reason described above.

Conclusion

In summary:

  1. The fire that a home burned in mattered more than how it was built: the share of homes destroyed ranged from near zero to nearly all of them depending on the fire, so hardening features have to be compared within fires.
  2. Within the same fire, homes with multi-pane windows, fine-mesh vents, enclosed eaves, and newer construction survived more often, but the data cannot answer the question for fences, siding, or decks.
  3. Homes built under the 2008 code had the largest advantage, and small fixes outranked roof material: fine-mesh vents and enclosed eaves are associated with larger differences than roof material once homes are compared within the same fire.

For a mitigation program, the data points to vent screens and window upgrades as the first priorities and enclosed eaves next, and it suggests that pooled statistics overstate the value of replacing a roof.

# save the ranking plot separately
dir.create(here("output"), showWarnings = FALSE)
ggsave(here("output", "hardening_ranking.png"), plot = p3,
       width = 9, height = 6, dpi = 300, bg = "white")

Data sources and packages

  • 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.
  • Syphard, A. D., and J. E. Keeley. 2019. Factors associated with structure loss in the 2013-2018 California wildfires. Fire 2(3):49.
  • Knapp, E. E., Y. S. Valachovic, S. L. Quarles, and N. G. Johnson. 2021. Housing arrangement and vegetation factors associated with single-family home survival in the 2018 Camp Fire, California. Fire Ecology 17:25.
  • R packages: tidyverse, here, janitor, patchwork, RColorBrewer, and knitr.