Two coverage measurements over California's public wildfire datasets, published as counts.
CAL FIRE and FRAP document the limits of these datasets carefully, in their own metadata. What is not published alongside them is the arithmetic behind those sentences: how many records per year, how many carry a federal identifier, how many cells in each field hold a value, how many hold a code meaning the value could not be determined, and how many hold nothing at all. This project counts exactly that and publishes it beside the sentence it answers.
Unofficial. Not affiliated with or endorsed by CAL FIRE, FRAP, or any California state agency. This is not a review of an agency's work. Every measurement operationalizes a limitation the publisher already states.
Status: Beta. Version 0.1.0, first signed tag not yet cut. Both measurements are
computed, tested, and published against pinned dataset retrievals (FRAP firep25_1 and CAL
FIRE DINS, retrieved 2026-08-07). The figures move only when those retrievals are deliberately
refreshed.
Historical fire perimeter completeness (site/perimeters.html) over FRAP's
California Historical Fire Perimeters, version firep25_1, retrieved 2026-08-07:
- 23,334 perimeter records, covering fire years 1878 to 2025. 77 records carry no year and are counted as their own cohort rather than being attached to a neighbouring year.
- 3,633 records carry an IRWIN ID (15.6%), reported per year so the transition is visible rather than asserted. FRAP's own release note describes the new Global ID as covering records "pre-dating IRWIN IDs".
- Cause has no empty cells and 10,514 records carrying the published code for
Unknown / Unidentified, so it is 54.9% recorded rather than complete. Collection method is the same shape: no empty cells, 15,081 recorded asUnknown, 35.4% recorded. Counting only nulls would call both fields complete. - 12,469 records carry an all-zeros local incident number, counted apart from the 9,910
recorded numbers. FRAP publishes no domain for that field, so this reading is an
inference and the page says so. Reading the value as a number instead would report
12,230 records as sharing an incident key rather than 376. Both counts are published;
the evidence is in
docs/MARKERS.md. - Records sharing an identifier, counted as candidates: 8 IRWIN IDs used by more than one record, covering 23 records.
- Surviving records counted per decade against the 10, 50 and 300 acre figures in FRAP's published collection criteria.
Damage inspection coverage (site/dins.html) over CAL FIRE's DINS data, retrieved
2026-08-07:
- 132,522 structure records across 451 incidents.
- Damage is recorded on every record. 54,414 of them (41.1%) say
No Damage, which is an inspection finding rather than a null and rather than a zero. 591 sayInaccessible: identified, could not be reached. - Field completeness reported separately for assessed records and inaccessible ones, so a blank on a structure nobody could get to is not counted as the same fact as a blank on one that was inspected.
- Construction attributes are where the three-state split matters most. Eaves is recorded
on 56.6% of records, carries
Unknownon 52,364, and is blank on 5,214. - Per-incident completeness for every incident, because an average across the file describes no incident in particular.
- The distance from a residence to a utility or miscellaneous structure is recorded on
37,783 records (28.5%). CAL FIRE publishes Not Applicable for that field spelled
NAand publishes it for the propane-tank field spelledN/A; the file carries both spellings in the utility field, in eras that do not overlap, and both are counted as the published finding. Seedocs/MARKERS.md.
Every measured cell is counted in one of three states, and the three are never collapsed:
| State | Meaning |
|---|---|
| Recorded value | A value the agency wrote down. Includes a genuine zero, and includes findings of absence such as No Damage, No Eaves or Not Applicable, which are observations. |
| Recorded as unknown | A published code or a reviewed marker meaning the value could not be determined. Somebody recorded that determination failed. |
| Empty cell | Nothing was written. CAL FIRE states for DINS that "Attributes with null values could not be determined." |
A null is not a zero. A structure not inspected is not a structure without damage. Where a share would have no denominator, the output says so in words instead of printing a number.
The build fails closed. A column this project measures going missing raises
SchemaDriftError. A cell holding something that reads like a missing-data marker, in a
field that has not declared that exact marker, raises SentinelDriftError rather than
being guessed at. Every marker currently declared was read in its own field's context and
carries a note saying why: None is a published street-type finding, while None in a
parcel APN is a marker standing in for an absent parcel match.
An ordinary value outside a published domain is deliberately not an error. It is counted
and published as outside_published_domain, because it is a real thing about the file and
crashing on it would hide it.
Twenty-seven of the fifty-four measured fields declare a marker, a code or a finding of absence. Twelve of those are published: the value is in the layer's own coded-value domain, or in FRAP's metadata document, or in CAL FIRE's DINS database dictionary. The other fifteen are inferred: the field is free text, or the value is one the published domain does not carry, and this project read it off the acquired file.
Both are counted the same way, and neither is a defect in either dataset. Both publishers
document the domains they constrain and say which fields are free text. What differs is
how much weight a reader should put on the call, so every field carries its basis in
schema.py, in the JSON artifacts as marker_basis, and on the pages beside its marker
list.
docs/MARKERS.md is the audit: per field, the declared values, the evidence, the URL it
can be checked against, the effect on the published figures, and a confidence. Where a
call has a counterfactual worth counting, it is counted rather than described, and
published in the artifact under marker_counterfactuals.
uv sync
npm ci
make verify # lint, format, types, tests, SCA, and the page checks
make site-offline # build from committed fixtures; runs anywhere, no networkmake verify ends in make pages, which builds the pages from the committed fixtures and
checks them two ways: html-validate for HTML conformance and the markup-level
accessibility rules, and axe-core in a headless DOM for the WCAG 2.0, 2.1 and 2.2 A and
AA rule sets. Nothing is served and nothing is deployed; both read the files off disk. The
same gate runs in CI.
Gated, in CI:
- HTML conformance, heading order, duplicate ids, landmark structure,
lang, and every table header carrying ascopeand every table a caption. Checked twice, byhtml-validateand by parser-based assertions intests/test_pages_html.py. - The WCAG A and AA rule sets that axe-core can decide in a DOM with no layout.
- Contrast. Both palettes are data in
render.py, so every foreground and background the stylesheet puts together is measured against the WCAG thresholds, in both themes, arithmetically. This is the one criterion axe cannot check headlessly, because jsdom paints nothing. - Every number in a table cell or a tile traces to the JSON artifact, and every number in
prose is either one of those, part of a quote from the publisher, or on a reviewed list
in
tests/test_pages_html.pywith a reason.
Not checked, and needing human eyes:
- Visual layout. Nothing here renders the pages. Column widths, the wide tables inside their scroll containers, the tile grid at its wrapping points, and whether the sticky table headers behave are all unverified.
- Small screens and reflow. WCAG 2.2 SC 1.4.10 needs a viewport. The pages are built to reflow, and that has not been observed.
- Print. No print stylesheet is defined and no print output has been looked at.
- Focus appearance in practice. A focus ring is defined and the skip link is present and points at the main landmark, but SC 2.4.11 is about how the indicator looks against what is behind it, which needs a renderer.
- Target size. SC 2.5.8 needs box geometry, which jsdom does not compute.
- A screen reader. Conformant markup is not the same as a good listening experience. Nothing here substitutes for reading a page with one.
To build from CAL FIRE's real files, acquire them first (see PROVENANCE.md):
uv run python -m perimeter.acquire --out data/raw
make siteOutput is deterministic: the same inputs produce byte-identical JSON and HTML. There is no
wall clock anywhere in the artifacts, retrieval dates come from the reviewed constants in
src/perimeter/sources.py, and every published share is computed with integer arithmetic.
data/raw/ is gitignored and CI never touches the network. A build from fixtures stamps
is_fixture: true and publishes null for every acquisition fact, so fixture output
cannot pass itself off as a measurement of the real files.
Coverage measurement only. This project does not model fire risk, does not track incidents, does not compute damage or loss totals, and does not republish any address, parcel number or assessed value. Those lanes are well served by others. What is measured here is how much of each published field is actually filled in, and what the blanks mean.
| Path | |
|---|---|
src/perimeter/cells.py |
The three states, and the rule that a non-present cell will not hand over a value |
src/perimeter/schema.py |
The reviewed field registry: every domain, marker and finding-of-absence, per field |
src/perimeter/records.py |
Classify every measured cell at the edge, or refuse the file |
src/perimeter/perimeters.py |
FRAP year cohorts, duplicate signals, acreage against the published criteria |
src/perimeter/dins.py |
DINS incident grouping and the assessed/inaccessible split |
src/perimeter/coverage.py |
The two reports |
src/perimeter/artifacts.py |
Deterministic JSON |
src/perimeter/render.py |
The static pages |
src/perimeter/acquire.py |
The only code that touches the network. Run by hand, never in CI |
tools/a11y.mjs |
axe-core over the built pages in a headless DOM |
site/ |
The built pages and their JSON artifacts |
PROVENANCE.md |
Per-source detail, quoted caveats, and what is excluded |
docs/MARKERS.md |
The marker audit: every judgment call, its evidence, and what it costs |
Apache-2.0. Source data is published by CAL FIRE under a Creative Commons Attribution licence and is reproduced here only as counts.