Oracle3
Oracle3: 예측시장 거래 엔진·MCP 서버
영국 2,736개 우편구역의 침하·날씨·홍수·지하수·해안 침식 위험 지도
Subsidence, weather, flood, groundwater and coastal erosion across 2,736 UK postcode districts — a 5-dim vine copula calibrated to ABI payouts, plus a climate-change repricing. All open data.
금융Python · HTMLGitHub @SMcode-source온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기Four-peril UK home insurance risk model (subsidence, weather, flood, groundwater) joined by a vine copula, built entirely on open data. Interactive map + methodology.
저장소 README에서 가져옴
README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
Interactive choropleth of 2,736 UK postcode districts rating eight home-insurance
perils — subsidence, weather damage, flood (rivers, sea and surface
water), groundwater flooding, theft, escape of water, fire and
accidental damage — the
four weather-driven perils joined by a C-vine copula, the attritional four
priced as independent legs (burglary does not arrive on the storm track, burst
pipes answer to cold snaps, dwelling fires follow deprivation and dwelling
type, and accidental damage follows the household itself), banded into 10
rating groups.README 전문 보기 ↗What happened · The model · Sector map · Relative risk · Temperature · Methodology
Interactive choropleth of 2,736 UK postcode districts rating eight home-insurance perils — subsidence, weather damage, flood (rivers, sea and surface water), groundwater flooding, theft, escape of water, fire and accidental damage — the four weather-driven perils joined by a C-vine copula, the attritional four priced as independent legs (burglary does not arrive on the storm track, burst pipes answer to cold snaps, dwelling fires follow deprivation and dwelling type, and accidental damage follows the household itself), banded into 10 rating groups. A companion page decomposes good years vs bad years for the whole portfolio.
Coastal erosion is modelled in the vine but deliberately kept out of the premium — see Coastal erosion: modelled, not priced.
Every hazard input is open data, fetched by the scripts here — see DATA_SOURCES.md for all 29 datasets with their endpoints, licences, access quirks and the dead ends worth avoiding. Each peril's loss level is calibrated to published ABI claims payouts, and districts are weighted by census household counts.
⚠️ These are eight-peril technical premiums, not home insurance premiums. A real policy also carries expenses, commission and profit. Expect these figures to be a fraction of what you actually pay. This is a methodology demonstration, not a rating product.
The published site lives in docs/ and is regenerated by
scripts/build_site.py; the standalone artefacts it wraps are built by
build_map.py and build_analysis.py (both gitignored build outputs — run the
pipeline below to produce them).
docs/ the published website (GitHub Pages source)
index.html landing page: findings + top-risk districts
map.html interactive map (districts), site nav injected
sectors.html the same map at postcode-sector resolution
relative.html the premium as a multiple of the UK median
years.html what happened: ABI record + good-vs-bad-years
temperature.html what 66 years of measured weather did to the model
methodology.html full methodology write-up
site/ page templates + shared stylesheet for docs/
tests/test_copula.py property tests for the copula machinery
tests/test_spectral.py guards on the shallow-water dynamical core
.github/workflows/tests.yml CI: tests + site rebuild + docs/ freshness
data/ downloaded + derived data (see DATA_SOURCES.md)
scripts/
fetch_bgs.py BGS 625k bedrock geology (OGC API, paged)
fetch_metoffice.py 5 Met Office climate grids (ArcGIS services),
incl. air-frost days for escape of water
fetch_flood.py river/sea flood extents -> per-district fractions
fetch_surface_water.py surface-water extents -> per-district fractions
fetch_sw_depth_postcodes.py EA surface-water depth bands at unit-postcode
centroids -> sw_depth.csv (England)
fetch_sw_depth.py the same bands as an AREA share (superseded as
model input 2026-09-20; kept for the record)
fetch_rs_depth_postcodes.py EA river/sea (RoFRS) depth bands at unit-postcode
centroids -> rs_depth.csv (England)
merge_sw_wales.py folds the 20m Wales re-render into sw_fractions
fetch_groundwater.py EA postcode groundwater flags -> district fractions
fetch_erosion.py EA NCERM coastal frontages -> erosion.csv (England)
fetch_erosion_scotland.py NatureScot Dynamic Coast eroded areas ->
erosion_scotland.csv (Scotland)
fetch_countries.py ONS country boundaries -> country.csv, the
coverage mask for every England-only dataset
fetch_midas.py mirrors MIDAS Open station wind obs from CEDA
gusts_from_midas.py station obs -> 1-in-50 gust per station (the
model's gust source since 2026-08-10)
fetch_gusts.py ERA5 gust extremes (fallback without a CEDA
account; the model's source before 2026-08-10)
fetch_history.py 35 years of ERA5 hazard drivers -> backtest
fetch_households.py ONS/NRS census households per district (exposure)
fetch_burglary.py police.uk street-level burglary points -> counts
fetch_premises.py VOA non-domestic premises -> theft's residential share
per district (36-month archive, resumable)
fetch_fires.py dwelling fires -> annual rate per district
(England LSOA incidents, Scotland councils,
Wales FRAs; reconciles against FIRE0201)
fetch_children.py census dependent-children shares per district
(E&W Census 2021 TS003 via NOMIS, Scotland's
Census 2022 UV113 at output-area grain)
scores_real.py data -> per-district peril scores
build_model.py scores -> marginals -> 5-dim C-vine Monte Carlo ->
districts_risk.geojson + year_analysis.json
sensitivity.py perturbed re-runs -> data/sensitivity.json
derive_sectors.py Code-Point centroids -> postcode-SECTOR polygons
validate_sectors_scotland.py scores them against NRS's official Scottish set
compare_sector_model.py sector model vs the published district one
build_map.py -> both map pages (district + sector)
build_tiles.py -> map/tiles/*.pmtiles, map/units/, map/*_index.json
build_analysis.py -> analysis/uk_risk_year_analysis.html
build_site.py wraps both + templates -> docs/
make_images.py favicon + social card, rendered from the geojson
dependence_check.py does the copula actually matter? -> dependence.json
check_*.py small sanity-check helpers (calibration, geojson,
surface water, year buckets)
nwp/ shallow-water model on the sphere; measures how
far ahead weather is forecastable (not in the
pipeline - evidence only, see HANDOFF 2026-09-12)
map/template.html map page (MapLibre GL, linked at build time)
analysis/template.html good/bad-years page (hand-rolled SVG charts)
assets/maplibre-gl.{js,css} MapLibre GL JS 5.6.1, linked from the map pages
assets/pmtiles.js PMTiles 4.3.0 protocol handler
| Layer | What it shows |
|---|---|
| Rating group (1–10) | Decile band of the technical premium |
| Technical premium | E[loss] + 6% cost-of-capital × (TVaR₉₉ − E[loss]) |
| Subsidence risk score | BGS shrink–swell clay geology (0–1) |
| Weather risk score | Met Office wind / rain exposure (0–1) |
| Flood risk score | EA / NRW / SEPA river, sea + surface-water zones (0–1) |
| Groundwater risk score | EA groundwater alert-area coverage (0–1) |
| 1-in-200 combined loss | 99.5% VaR of the eight-peril annual loss (C-vine + independent theft, escape of water, fire and accidental damage) |
| Capital charge | The district's Euler share of portfolio tail risk, in £ |
| Surface-water depth | Mean depth at the homes that flood, from the EA depth bands sampled at unit-postcode centroids (England) |
| Coastal erosion (blight) | Land projected lost by 2105 under the adopted Shoreline Management Plan (England) or by 2100 under Dynamic Coast's RCP8.5 case (Scotland) — not insured |
| Climate repricing | Premium under the EA's future flood scenario vs present day (England) |
The last three layers distinguish "not mapped" from "measured, and it is zero" — a distinction a single grey would destroy. 81% of districts have no projected coastal erosion, Wales and Scotland have no depth or climate product at all, and on the erosion layer a defended coast (Blackpool, Eastbourne) is a fourth state again: the sea would take land and the defences are stopping it. A plain quantile ramp would also have spent the whole colour scale on zeroes.
Erosion carries a fifth thing the others do not: two sources on different
bases. England is NCERM's defences-maintained case at 2105, Scotland is
Dynamic Coast's single "do nothing, defences standing" case at 2100 (added
2026-09-03), and Wales has no projection of its own — three border
districts carry a trace only because NCERM's Severn and Dee frontages cross
into them. er_head is the
column the model scores from and er_basis says per district which source it
came from — so Scotland's figures can never be mistaken for NCERM's, and the
Scottish popup shows an emissions ladder where the English one shows a
defended/undefended pair. See
Coastal erosion: modelled, not priced.
There is deliberately no per-district "copula uplift" layer: at calibrated frequencies that difference is inside Monte Carlo noise, so the layer would have been drawing noise across the country. The dependence effect is real but lives in the aggregate — see the methodology.
Hover any district for its value; click for the full breakdown (all scores and underlying data values, expected losses per peril, VaR/TVaR under vine / Gaussian / independence, each pair's θ and tail dependence λᵤ).
Peril scores — real open data (scripts/scores_real.py).
ogcapi.bgs.ac.uk,
fetched by scripts/fetch_bgs.py). Each of 11,244 formations is classified
for shrink–swell susceptibility (London Clay/Thames Group 1.0, Gault/Weald/
Oxford/Kimmeridge/Ampthill Clays 0.9, … Mercia Mudstone 0.45; generic
mudstone keywords down-weighted ×0.3 for pre-Mesozoic indurated rocks), then
read at every live unit postcode (ONSPD centroids, point-in-polygon in
British National Grid) and averaged over each district's postcodes, shrunk
toward the postcode area under 20 postcodes
(scripts/score_subsidence_postcodes.py; postcode share since 2026-09-06 —
area-weighted by polygon intersection before, which read a district whose
clay lies under fields and whose homes stand on gravel terraces as clay).
This mirrors how BGS GeoSure's shrink–swell layer is derived.
The 625k superficial map (--superficial) is then blended in as a
modifier, because shrink–swell happens in the soil shallow foundations bear
on and drift covers ~58% of GB: sub = (1 − W·cover)·bedrock + W·cover·drift
with W = 0.5, where cover is the share of the district's postcodes on
a classified deposit (57% of households; 50% by area). Its 14 deposits are enumerated explicitly rather than
keyword-matched (lacustrine clay 0.75, clay-with-flints 0.70, brickearth
0.60, alluvium 0.50, till 0.45, granular deposits 0.05), so an unrecognised
one raises instead of defaulting. Peat is excluded — it subsides by
consolidation and oxidation, not shrink–swell — as is unmapped drift, and
both fall back to bedrock. The half weight is the honest treatment of the
one thing 625k does not publish: thickness. Effect: 1,452 districts
raised, 749 lowered, exposure-weighted premium −0.2% (the ABI calibration
re-pins the level), but 42.5% change rating group.
Geology says which ground can shrink; since 2026-08-31 a drought
climatology says how often it is made to: each district's 1991–2020 mean
annual peak of the running water deficit max(cumsum(PET − rain), 0), reset
every 1 January, from HadUK-Grid 1 km daily observations streamed on CI
(174 GB that never touched a disk whole — scripts/haduk_1km_stream.py,
reduced by scripts/make_smd_climatology.py to
data/smd_climatology.csv). It enters claim frequency as a blend:
sub_rel = (1 − 0.565) + 0.565 · deficit / mean, the share being the
ABI's own drought-attribution arithmetic (13,000 of 2022's 23,000 claims;
DATA_SOURCES.md #34), normalised to an exposure-weighted mean of 1 so the
national level is untouched. Nationally the same index recovers (1976, 1995, 2003, 2018, 2022). The
PET inside it is Hargreaves–Samani, ~a third high in a maritime climate —
measured (, both 1 km and 5 km grids) to
move the deficit's , which calibration re-pins, and not its
(rank correlation ≥ +0.998 at PET × 0.85/0.70). Dependence is untouched:
θ_WS still reads the geology score.Re-run 2026-09-26, after seven weeks stale (the script had been failing since the council-tax severity landed — see HANDOFF). Four readings:
Good vs bad years (analysis/uk_risk_year_analysis.html, built by
scripts/build_analysis.py). The 20,000 simulated portfolio years are
ranked and bucketed (good = best 50%, typical = 50–90th pct, bad = 90–99th,
catastrophic = worst 1%), with per-peril cost composition, claim incidence
(share of districts claiming), an exceedance curve (vine vs independence),
and the worst simulated year decomposed. The year view uses a Gaussian
factor model per peril: each district mixes the national systemic factor
(which carries the vine dependence) with idiosyncratic noise — spatial
loadings weather 50% / flood 40% / subsidence 60% / groundwater 70%,
theft 0.13% (derived, not assumed: the CV of the national claim count is
≈2.75·√w at theft's frequency, and the observed burglary series varies
±10–15% a year, which pins w near 0.001; a "felt reasonable" 0.20 would
have implied worst years of 8–14× average claims and ~£13/policy of
phantom capital), escape of water 2.6% (same derivation, different
record: winter 2010's freeze doubled the year's burst-pipe bill, and at
1.06% frequency CV ≈ 0.43 pins w at 0.026 — twenty times theft's, which
is why EoW buys real capital and theft does not), fire 0.004% (same
derivation a third time: FIRE0201's 45-year series, detrended, has CV
≈2% with no spike years, so fire is priced as pure expected loss),
accidental damage 0.012% (a fourth time: lockdown 2020–21 moved home AD
declarations ~6%, CV ≈3%, so AD is pure expected loss too) —
preserving district marginals exactly while giving realistic cross-district
clustering (a pure common-random-numbers view is degenerate: no claims
anywhere in 90% of years, then everything at once).
Backtest against real years (scripts/fetch_history.py, shown on the
year-analysis page). The loss model can't be validated without claims
data, but its hazard drivers can: 35 years of ERA5 reanalysis over
twelve UK locations are reduced to per-year storm days, peak gust,
wettest 5-day total and summer rainfall deficit. The years the model
would call bad are the ones Britain remembers — 1990 (Burns' Day storm),
2015 (Desmond/Eva/Frank) and 2020 (Ciara & Dennis) top the storm
rankings; 1995, 2018 and 2022 top the drought summers, which are exactly
the known subsidence-surge years.
The EA publishes a climate-change edition of both flood products this model already
uses. Re-running on those editions with the calibration held fixed — nothing moves
but the hazard map — gives a per-district repricing (premium_cc, cc_uplift_pct,
and a map layer).
Choosing the right pair mattered. A comparison between two differently derived
products folds a method change into the climate change. Since 2026-09-25 England's
rivers and sea read NaFRA2 rofrs_4band, and its climate sibling is rofrs_cc01_4band
— same product, same legend, same 13 m/px — so that is the pair used. (Until then the
model read the defended extents and paired them with their _CCP1 climate layers,
for the same reason.) Where the climate edition is not published at all — "Unavailable"
over 1.9% of English postcodes, all of PE11-PE25 and CB6, the Somerset Levels and the
Lincolnshire coast — the postcode keeps its present-day band: no uplift where none was
modelled, rather than the fall to zero that reading "Unavailable" as dry would give.
Two normalisations had to be pinned to the present day, or the exercise would have measured nothing: the depth multiplier (renormalising on future data divides out "water gets deeper everywhere") and the flood score's 95th-percentile anchor. Both runs share their random draws, so it is a paired comparison.
Stated plainly, four limits:
GetCapabilities sweep of both climate services returns exactly one future layer
per product — rofrs_cc01_4band for rivers/sea and rofsw_cc01 (plus its five depth bands)
for surface water. So the model cannot show a 2050-vs-2080 or central-vs-upper
progression the way the NCERM erosion columns do, where the EA really does publish
two epochs and three allowances. This is a publishing limit, not a modelling choice.The direction of travel is not subtle. Summed over England's districts, the share of a district's homes (unit postcodes) in the river/sea high band grows +60.4% (+87.7% on the extent pair before 2026-09-25) and the share of homes inside the surface-water ≥1% AEP zone +35.4%. Estuary and defended lowland carry the fluvial/tidal side — ME11 (Queenborough, Sheppey) goes from 0% to 47% of its postcodes, DN32 (Grimsby) 19% → 52%, YO8 (Selby) 15% → 49%, HU1 (Hull) 21% → 49%, NG2 (Nottingham, on the Trent) 7% → 40%, E6 (East Ham) 0% → 32% — while surface water concentrates on dense urban drainage: SW8 (Nine Elms) 14% → 33%, N1C (King's Cross) 10% → 26%, E8 (Hackney) 32% → 48%, RM9 (Dagenham) 38% → 52%.
The model carries one further peril — coastal erosion, from the EA's
NCERM National 2024 frontages (scripts/fetch_erosion.py) — and it is
deliberately kept out of premium.
Why it is excluded. Gradual coastal erosion is not covered by standard UK
household policies. Putting it in the technical premium would price a loss no
policy pays, and would quietly wreck the rating: erosion is a total loss of
the property rather than a repair, so where it bites it dwarfs everything else.
It is carried instead as blight and valuation exposure — the thing that
actually shows up in mortgage and insurability decisions on an eroding coast —
in its own columns (el_er, er_*) and its own map layer.
Why it is still in the vine. Erosion is storm-driven: cliffs and beaches retreat in a handful of surge events, not on calm days. So it belongs in the dependence structure even though it is not priced. It enters the C-vine as a fifth dimension with a Gumbel pair to weather (θ_we) and, given weather, the strongest of the tree-2 Gaussian pairs to flood (ρ_FE|W = 0.35) — coastal flooding and coastal erosion are driven by the same storm surges.
What is modelled. A property inside the strip projected to be lost by the 2105 epoch is lost within that horizon, so the annual hazard is the district's zone fraction spread over 80 years, at a sum-insured severity. Two assumptions worth stating plainly: households are taken as spread uniformly across the district (coastal populations actually cluster towards the shore, so this if anything understates it), and the SMP scenario is used — defences maintained as currently planned. The unmanaged NFI case is carried alongside for contrast, and is a sensitivity scenario.
Why it is absent from the good-vs-bad-years view. Erosion is chronic, not episodic. Including it would add a near-constant charge to every simulated year and blur precisely the contrast that page exists to show. Capital is likewise allocated against insured losses only.
The scenario contrast is stark and is the interesting output: nationally the adopted plans hold the 2105 land loss to 250 km² against 435 km² undefended, and several major seafronts — Blackpool (FY1), Eastbourne (BN21), Bognor (PO21/PO22), Crosby (L22) — go from substantial unmanaged erosion to exactly zero under their "Hold the Line" plans. Where the defences are not there, the fastest-eroding coast in Europe shows up correctly: Hornsea (HU18) and Withernsea (HU19) on Holderness top the table, followed by Southwold (IP18) on the Suffolk coast.
Worth asking of any model that spends this much effort on dependence.
scripts/dependence_check.py measures it (60 districts × 400,000 years) and the
answer splits in two:
This is why there is no per-district "copula uplift" map layer — at calibrated frequencies it would be drawing Monte Carlo noise. Removing it is the finding.
The site is checked, not assumed:
--ink-muted and link colours were darkened after that audit failed 23
elements; chart series colours are unchanged, since those are marks not text..venv/Scripts/python -m pytest tests -q
Property tests for the machinery the model rests on: Gumbel h-function inversion round-trips to 1e-8, sampled margins pass Kolmogorov–Smirnov against U(0,1), every vine pair recovers its target Kendall's τ, Gumbel shows upper-tail dependence where the tau-matched Gaussian does not, the 5-dim vine-implied correlation matrix stays positive definite across the whole θ range, and the severity distributions reproduce the published ABI means.
The newer perils get their own guards: erosion's frequency must annualise over the horizon and must not be touched by the ABI frequency scaling (it has no published payout to calibrate against), the flood–erosion tree-2 pair must come out as the strongest of the three, and the depth multiplier must leave the exposure-weighted national mean at exactly 1.0 while still ranking a deep district above a shallow one — plus a fallback test proving the model still runs when the depth product is absent, as it is outside England. Theft's guards pin its severity mean to the ABI average, its claim frequency to the scaled burglary rate, and its expected loss to the analytic p × E[severity] — the last one exists because the simulated estimate was +17% biased (all districts share one theft draw stream) and would silently drift the level. Escape of water carries the same three guards (severity mean, rate pass-through — including the 0.5 clip a raw relativity would have saturated — and the analytic EL), for the same reasons. Fire carries them too, plus one more learned at its own expense: the end-of-run identity test (el_total = Σ el_*) must gain the new leg as part of the wiring — the fire evidence run failed that check at minute 55 with the artifact upload skipped, because the leg was in the fixtures but not the identity. Accidental damage carries the full set (severity mean, child-share rate pass-through, analytic EL), and its identity tolerance widened with the leg count in the same commit as the wiring: the budget is (N legs + 1) × 0.05 of 1dp roundings, 0.45 for eight.
CI additionally rebuilds the whole site from committed data and fails if
docs/ has drifted from its templates.
Layout regression tests (tests/test_layout.py) render the built site in
headless Chromium and assert what static checks cannot see: no page scrolls
sideways at phone or laptop width, no SVG text renders below 9px (an SVG
scales its type down with its viewBox — "nothing overflows" is not "it is
readable"), the map's zoom/layer/popup-close controls actually receive a
tap at their own centre (elementFromPoint, which is how two controls were
once found buried under panels), the district popup stays inside the map and
scrolls when it overflows, and the map page keeps its accessible routes to
the data. They need extra dependencies and skip cleanly without them:
.venv/Scripts/python -m pip install pytest playwright
.venv/Scripts/python -m playwright install chromium
.venv/Scripts/python -m pytest tests/test_layout.py -q
py -3.13 -m venv .venv
.venv/Scripts/python -m pip install -r requirements.txt
git clone --depth 1 https://github.com/missinglink/uk-postcode-polygons.git data/uk-postcode-polygons
.venv/Scripts/python scripts/fetch_bgs.py # BGS 625k bedrock -> data/bgs_625k_bedrock.geojson (~32MB)
.venv/Scripts/python scripts/fetch_metoffice.py # Met Office grids -> data/metoffice/*.csv
# The three flood fraction products are POSTCODE shares (river/sea and surface
# water since 2026-09-06, the depth conditional since 2026-09-20): the same EA /
# NRW / SEPA masks, sampled at ONSPD unit-postcode centroids instead of measured
# over a polygon's area. Every flood share (frequency and depth, both perils)
# weighs each postcode by its Census households since 2026-09-28 (Nomis P002 +
# NRS 2022 index, fetched into data/cache before the first tile). fetch_onspd.py
# writes the centroids and needs OSTN15.
# The area fetchers below them still work and are how the masks were validated,
# but their output is no longer model input - see DATA_SOURCES #41.
.venv/Scripts/python -u scripts/fetch_onspd.py # -> data/postcode_centroids.csv
.venv/Scripts/python -u scripts/fetch_flood_postcodes.py # river/sea -> data/flood_fractions.csv
.venv/Scripts/python -u scripts/fetch_sw_postcodes.py --flags england # and --flags wales, --flags scotland
.venv/Scripts/python -u scripts/fetch_sw_postcodes.py # surface water -> data/sw_fractions.csv
.venv/Scripts/python -u scripts/fetch_sw_depth_postcodes.py --flags # five depth layers, England (~1 h)
.venv/Scripts/python -u scripts/fetch_sw_depth_postcodes.py # -> data/sw_depth.csv
.venv/Scripts/python -u scripts/fetch_rs_depth_postcodes.py --flags # band + five depth layers, England (~2 h; rs-depth.yml does it in 4 parts)
.venv/Scripts/python -u scripts/fetch_rs_depth_postcodes.py # -> data/rs_depth.csv
.venv/Scripts/python scripts/fetch_erosion.py # NCERM coastal erosion -> data/erosion.csv (~3 min, England)
.venv/Scripts/python scripts/fetch_countries.py # ONS country boundaries -> data/country.csv (~1 min).
# Run this BEFORE build_model: it is the coverage mask that
# stops the England-only layers being read as GB-wide.
# Climate-change scenario (optional; without these the repricing view is simply absent):
.venv/Scripts/python -u scripts/fetch_flood_postcodes.py --climate # -> data/flood_fractions_cc.csv
.venv/Scripts/python -u scripts/fetch_sw_postcodes.py --flags england --climate
.venv/Scripts/python -u scripts/fetch_sw_postcodes.py --climate # -> data/sw_fractions_cc.csv
.venv/Scripts/python -u scripts/fetch_sw_depth_postcodes.py --flags --climate
.venv/Scripts/python -u scripts/fetch_sw_depth_postcodes.py --climate # -> data/sw_depth_cc.csv
.venv/Scripts/python -u scripts/fetch_rs_depth_postcodes.py --flags --climate
.venv/Scripts/python -u scripts/fetch_rs_depth_postcodes.py --climate # -> data/rs_depth_cc.csv
# download Postcodes_Risk_Assessment_All.csv (see DATA_SOURCES.md #13) to data/ea_postcode_risk.csv, then:
.venv/Scripts/python scripts/fetch_groundwater.py # groundwater flags -> data/gw_fractions.csv
# Gusts - data/gusts.csv is MIDAS station extremes (since 2026-08-10). To
# refresh it you need a (free) CEDA account and token (DATA_SOURCES.md #24):
.venv/Scripts/python -u scripts/fetch_midas.py # mirror uk-mean-wind-obs -> data/midas/ (~8 GB, resumable)
.venv/Scripts/python scripts/gusts_from_midas.py data/midas/uk-mean-wind-obs
# # -> data/gusts_midas.csv; copy over data/gusts.csv
# No CEDA account? The previous source still works as a fallback:
# .venv/Scripts/python scripts/fetch_gusts.py # ERA5 -> data/gusts.csv (Open-Meteo free tier has a daily
# # quota - if it 429s, rerun next day; the weather blend
# # auto-includes gusts once >=60 points exist)
.venv/Scripts/python scripts/fetch_history.py # 35yr ERA5 hazard drivers -> data/history.csv (backtest,
# # and the anchor for TAIL_FREQ_RATIO - DATA_SOURCES #15)
# data/abi_annual.csv is HAND-TRANSCRIBED from the ABI's full-year property
# releases (one every February) - there is no feed and the site bot-blocks
# scrapers. When the next one lands, add its rows with the source URL and a
# published/derived flag, DO NOT overwrite prior years (the ABI restates
# them, and both readings are kept side by side), then re-run:
# .venv/Scripts/python scripts/anchor_budget.py # instant
# .venv/Scripts/python scripts/backtest_coverage.py # ~40 min, one full sim
# Optional, not a model input: derive postcode-SECTOR polygons (open data;
# only Scotland publishes official ones) - DATA_SOURCES.md #23:
# .venv/Scripts/python scripts/fetch_codepoint.py
# .venv/Scripts/python -u scripts/derive_sectors.py # -> data/sectors_gb.gpkg
.venv/Scripts/python scripts/fetch_households.py # ONS lookup + census -> data/households.csv (exposure, ~22MB dl)
.venv/Scripts/python -u scripts/fetch_burglary.py # police.uk archive (1.7 GB, resumable download) -> data/burglary.csv
.venv/Scripts/python -u scripts/fetch_premises.py # VOA NDR stock by LSOA -> data/premises.csv (theft denominator)
# (~6 min once the archive is cached; checkpoints every 100 files)
.venv/Scripts/python -u scripts/fetch_fires.py # Home Office LSOA incidents + SFRS + Welsh FRAs -> data/fires.csv
# (~40 MB of ODS; parses content.xml directly - pandas' odf
# engine takes >10 min on these files)
.venv/Scripts/python -u scripts/fetch_children.py # NOMIS TS003 (E&W LSOA) + Scotland UV113 (OA) -> data/children.csv
# (dependent-children household shares; conserves 27.29m households)
.venv/Scripts/python scripts/build_model.py # calibrate + vine sim -> districts_risk.geojson + year_analysis.json (~55 min;
# the 5th vine dimension roughly doubled this - the extra Gumbel
# h-inverse bisection for erosion is the dominant cost)
.venv/Scripts/python scripts/sensitivity.py # perturbed re-runs -> data/sensitivity.json (~60 min, optional)
.venv/Scripts/python scripts/make_images.py # favicon + 1200x630 social card, rendered from the data
.venv/Scripts/python scripts/build_map.py # -> both map pages (district and sector)
.venv/Scripts/python scripts/build_tiles.py # -> vector tiles, popup shards, name index (~3 min).
# The pages read these over HTTP, by byte range, so they
# need a server: `python -m http.server` inside docs/
.venv/Scripts/python scripts/build_analysis.py # -> analysis/uk_risk_year_analysis.html
Every step is deterministic (fixed RNG seed 42); re-running build_model.py
reproduces the shipped outputs exactly given the same inputs. CI enforces this:
it rebuilds the whole site from committed data and fails if docs/ has drifted
from its templates.
Two notes on running it:
build_model.py holds ~25 transient (BATCH × N_SIM) arrays, so BATCH is
set to 80. Raising it to 250 needs ~1 GB and thrashes on an 8 GB machine —
slower in wall-clock terms despite fewer batches.fetch_gusts.py and
fetch_history.py are resumable; if they start returning HTTP 429, rerun the
next day. The weather score folds gusts in automatically once ≥60 grid points
exist and falls back cleanly below that.fetch_sw_depth.py (the superseded area fetch) writes a checkpoint to
data/cache/ after every tile column, so a machine that sleeps mid-run costs
you one column, not the whole fetch — rerun it plain to resume, or pass
--restart to start over. fetch_sw_depth_postcodes.py, which writes the
table the model reads, splits instead with --part i/n. Both
fetchers degrade gracefully: if sw_depth.csv is missing the model falls back
to a flat surface-water severity, and if erosion.csv is missing the erosion
peril is simply zero everywhere.<name>.partial instead of the real filename so an incomplete fetch cannot
quietly become model input. This is not hypothetical: a DNS blip mid-run
silently cost one tile before the guard existed.build_model.py runs the simulation twice
(present and future), so budget ~110 min rather than ~55.GitHub Pages serves main + /docs, so a push deploys. The one thing that is
not automated is the repository's own social preview image: GitHub exposes no
API for it (usesCustomOpenGraphImage is read-only in GraphQL). Upload
docs/assets/social-github.png by hand under Settings → General → Social
preview — Edit opens a menu, and the file dialog is behind
"Upload an image…" inside it. That file is generated at 1280×640 opaque RGB
specifically for this: GitHub asks for that size, and its image processing has
been seen to reject RGBA PNGs without reporting an error. (social.png stays
1200×630 with alpha — the Open Graph size the site's own pages reference.)
To check whether it took:
gh api graphql -f query='{repository(owner:"SMcode-source",name:"uk-home-insurance-risk-map"){usesCustomOpenGraphImage}}'
false means it did not stick; a custom image is also served from
repository-images.githubusercontent.com rather than
opengraph.githubassets.com.
Set and verified 2026-08-02. Worth recording why it took several
attempts: the 1200×630 RGBA card was rejected silently every time, and the
1280×640 opaque RGB copy worked immediately. If it ever needs redoing, use
social-github.png. Either way this is cosmetic — the site's pages carry their
own Open Graph tags and preview correctly already, so links to the map itself
are unaffected.
The hazard inputs are real (BGS geology, Met Office climatologies, EA/NRW/ SEPA flood maps, EA groundwater alert areas — all OGL), but this is still not a priced product:
bgsgeology625ksuperficial on the
same open OGC API as the bedrock layer this model already uses: 10,651
features, same paging, carrying lex_d/rock_d in the same vocabulary the
classifier keys off (TILL, ALLUVIUM, CLAY, SILT AND SAND). What is
genuinely missing is thickness. Without it there is no principled way
to decide whether the superficial cover or the bedrock governs foundation
behaviour, and a thin gravel over London Clay would score like a thick one.
Peat is a further complication: a serious subsidence hazard, but through
consolidation and oxidation rather than shrink-swell, so folding it into
this peril would conflate two mechanisms. Adding the layer is therefore a
modelling decision, not a fetch — but it is not licence-blocked, and the
earlier wording here implied it was;scripts/check_pet_sensitivity.pyscripts/fetch_metoffice.py), interpolated to district
centroids by inverse-distance weighting: winter mean wind speed 5 km (UKCP18
baseline), annual wind-driven rain index for SW-facing walls 5 km (UKCP18
baseline), annual count of ≥10 mm rain days and annual precipitation
(HadUK-Grid observations 1991–2020) — plus a true extreme-value
component: the 1-in-50-year gust per district, from Gumbel fits to
annual gust maxima at 191 Met Office MIDAS stations (1970–2025,
≥20 years' coverage, stations above 300 m excluded — Cairngorm summit
measures a climate nobody lives in; scripts/fetch_midas.py +
gusts_from_midas.py, CEDA account needed). Before 2026-08-10 this
component was ERA5 reanalysis (scripts/fetch_gusts.py, kept as the
no-account fallback); stations move the relativities toward measured
coastal exposure — Fraserburgh, Blyth and the Whitby/Norfolk coasts
up, sheltered Welsh valleys down — at an unchanged national level.
Score = 0.30·wind + 0.25·WDR + 0.20·gust₅₀ + 0.15·rain days +
0.10·precipitation, each normalised 5th–95th percentile.Flood — rivers & sea (scripts/fetch_flood_postcodes.py). Official
flood maps sampled at every live unit-postcode centroid: EA NaFRA2 Risk
of Flooding from Rivers and Sea (rofrs_4band, legend colours decoded at
6.5 m on three tile grids a third of a tile apart, because the service
drops whole polygons from some renders depending on where the tile falls;
high band = High + Medium, >= 1% a year, with High (>= 3.3%) priced
separately since 2026-09-27, at 1.67x Medium since 2026-09-29, the ratio
the EA's recorded floods show (scripts/validate_flood_history.py); low envelope adds Low, >= 0.1%) for England
since 2026-09-25 (the defended extents before, which
ranked constituencies worse against the EA's own properties at risk),
NRW FRAW for Wales (its rivers and sea polygons over WFS,
point-in-polygon at each postcode, since 2026-09-27; 100 m WMS masks
before, which buffered every polygon and put 2.5x as many Welsh
postcodes in the >= 1% band), SEPA
river + coastal maps for Scotland (FeatureServer vector queries at a 5 m
generalisation tolerance — 100 m before 2026-09-25, which inflated coastal
flags by a third; their map services have a 1:85k scale limit). Each district gets f_high / f_low
/ f_top (High alone; Wales from FRAW's own High, Scotland - whose SEPA
High is 1 in 10, not 1 in 30 - carrying England's High share of the zone):
the share of its homes inside each band, read at its unit postcodes (ONSPD
centroids) with each postcode weighted by its Census households, and shrunk
toward its postcode area when it has fewer than 20 postcodes (households
since 2026-09-28, when counting postcodes put England's High band at 1.20x
the EA's own count of homes in it, because flood postcodes are the sparse
ones; postcode share since 2026-09-06; area share before, which put valley
towns at the wrong end of the ranking). These drive flood claim frequency directly (2.5%/yr on
High homes and 1.5%/yr on Medium - one band at 1.5% before 2026-09-27, and
High at its band's floor, 3.3%, until 2026-09-29, when the EA's Recorded
Flood Outlines showed High homes flooding 1.67x as often as Medium ones,
not 2.2x - 0.78%/yr in the rest of the envelope, 0.05%/yr background; Low
was 0.3%, a 5:1 ratio to Medium, until 2026-10-06, when the same records
put Medium homes at 1.92x Low ones).
Flood — surface water (scripts/fetch_surface_water.py). EA NaFRA2
RoFSW for England (WMS at 13 m/px — the layer only draws below 1:50,000 —
with the High/Medium/Low category colours decoded per pixel), NRW FRAW
surface water for Wales (20 m/px; see merge_sw_wales.py), SEPA
surface-water likelihood maps for Scotland (20 m/px, layers=show:<id>
because the sublayers are default-hidden). An EA pixel counts only when at
least 25% of it is covered: its alpha is coverage (2 m flow paths in 13 m
pixels), and counting faint pixels put England at 1.6x the EA's own count
of homes at risk. 25% was fitted to those counts on half the constituencies
and held out on the rest; built nationally it reads 1.18x / 1.00x. NRW keeps
its old rule, which NRW's own counts favour (MIN_ALPHA, HANDOFF
2026-09-26). The masks are sampled at every
live unit-postcode centroid (scripts/fetch_sw_postcodes.py), so sw_high /
sw_low are the share of a district's homes in the ≥1% AEP band and in
the whole envelope - each postcode weighted by its Census households, as
for river/sea - shrunk toward the postcode area under 20 postcodes
(households since 2026-09-28, which moved mostly city centres, where office
postcodes hold few homes; postcode share since 2026-09-06; area share
before). The 25% threshold was kept: weighting moved England's level from
1.19x to 1.15x (High) and 1.01x to 0.99x (envelope), small next to the
step between thresholds. sw_high adds ~1%/yr
claim frequency at full coverage; surface-water severity is modelled cheaper
(median ~£15k vs ~£30k river/sea) via a probability-weighted lognormal mix.
scripts/fetch_sw_depth_postcodes.py).
The same EA WMS carries five nested depth layers
(>0.2/0.3/0.6/0.9/1.2 m), sampled at the same unit-postcode centroids.
Their differences give, for each district, the depth distribution
within the water its homes are standing in — conditioned on the same
household-weighted sw_high / sw_low the frequency uses, so the peril
has one denominator (households since 2026-09-28, postcode share since
2026-09-20; the depth distribution
was taken over the flooded area before, against a second envelope kept
as sw_fractions_area.csv). People build on the higher ground of a
floodplain, so the water under the homes is not the water's average:
moving the denominator moved 98 districts a rating group, Bury St Edmunds
and the Thames-estuary towns down, inner London and the Pennine mill
towns up. It is turned into an expected-damage relativity using
the usual UK depth–damage shape (damage climbs steeply through the first
half-metre as water passes floor level and reaches sockets, then flattens
once the ground floor is written off). The multiplier is renormalised
to a mean of 1.0 per claim (households × band frequency), so it
re-shapes severity across districts without moving the level the ABI
calibration fixed — the same discipline the frequency calibration
follows. Until 2026-09-26 it was 1.0 per household, which is 1.022
per claim because deep water sits where the claims are; with the flood
total pinned nationally, that 2.2% was paid for out of Welsh and
Scottish flood and everyone's groundwater. England only; Wales and
Scotland have no equivalent product and keep the flat severity.scripts/fetch_rs_depth_postcodes.py, since
2026-09-26). The EA publishes the same five depth thresholds for the
RoFRS risk bands, so river/sea claims — the larger half of flood,
priced at one flat ABI claim size until then — get the same
depth-damage core, per claim, on the zone frequency only (the 0.05%
background has no mapped depth). Goole and Thorne/Fishlake (1.0–1.3 m)
rise most; Grimsby, Cleethorpes and Weston-super-Mare, wide shallow
tidal plains at ~0.2 m, fall. The depth layers are "Unavailable" at
16.7% of homes in the ≥1% band, mostly the Fens: those homes are left
out of the conditional, and PE11/PE13/PE14, with none of their own,
take their postcode area's distribution. Nothing external measures
depth at homes, so neither depth multiplier is validated.
Coverage comes from the country boundary, not from the numbers
(scripts/fetch_countries.py → data/country.csv). The depth file
has a row for every district, zero-filled outside England, so judging
by presence reads "no depth mapped" as "nothing over 0.2 m" — the
shallowest severity — for all of Wales and Scotland; and border
districts that clip into England on a sliver read as the shallowest
in the country. A district must be England and ≥95% of its area
inside it, which leaves 2,084 districts with usable depth and sends
the ~20 genuine straddlers to the neutral fallback.Groundwater (scripts/fetch_groundwater.py). EA flood-risk postcode
search tool data: GWTR_RISK flags each unit postcode 'Possible' if any
address sits in a groundwater flood alert target area. District fraction =
share of unit postcodes flagged (Salisbury, Dorchester and the chalk downs
hit 100%). England only — UK groundwater flooding concentrates on the
English chalk; elsewhere a nominal 0.02 background. Claim frequency up to
~0.8%/yr, median severity ~£20k (long-duration events).
Marginal losses, calibrated per peril to published ABI payouts. Annual aggregate loss per policy per peril is Bernoulli(p) × LogNormal. The hazard scores set relative frequencies within a peril; each peril's level is pinned to what the ABI reports it paid in 2025 across ~15.5m policies:
| Peril | Paid | Avg claim | Implied claims | Frequency |
|---|---|---|---|---|
| Storm | £244m (2025) | £2,450 | ~99,600 | 0.64% |
| Flood | £312m (2025) | £30,000 | ~10,400 | 0.067% |
| Subsidence | £307m (2025) | £17,264 | ~17,800 | 0.115% |
| Groundwater | not published | £20,000 | — | pegged at 10% of flood |
| Theft | £342m (floor of a 0.58–0.97% envelope) | £3,800 | ~89,900 | 0.580% |
| Escape of water | ~£657m (ABI "£1.8m a day") | £4,000 | ~164,000 | 1.06% |
| Fire | ~£434m (triangle, no published total) | £14,000 | ~31,000 | 0.200% |
| Accidental damage | ~£227m (triangle, never published) | £1,650 | ~138,000 | 0.888% |
Together these come to £164 per policy per year — about 75% of the £219 that all home claims cost.
premiums, not home insurance premiums**: a real policy also carries
expenses, commission and profit. What is not fitted —
and is where the model earns its keep — is the geography, the dependence
structure, and the spread of frequency/severity within each peril.
(An earlier version calibrated to the all-claims figures — 560,000 claims,
3.6% frequency — which overstated the loss cost by ~77%, because most home
claims are the attritional perils rather than catastrophe perils. Theft,
escape of water, fire and accidental damage are now priced, but at their
own frequencies and their own severities.)
5b. Theft — burglary geography from police.uk (scripts/fetch_burglary.py).
Three years of street-level burglary points (36 monthly archives, ~670k
incidents) placed into districts by point-in-polygon become an annual
burglary rate per household. One national multiplier scales that rate so
the implied claim count hits the ABI theft anchors, which means the
burglary→claim propensity cancels out — the police data contributes
only relativities. Three corrections: the rate divides by households
plus VOA non-domestic premises (scripts/fetch_premises.py, NDR stock
of properties at LSOA), because the police "burglary" category includes
commercial premises — the denominator hands each district only its
residential share, so the City and Mayfair price at the rate their actual
residents face; rates are still winsorised at the exposure-weighted
99.9th percentile (~3.4%/yr) as a tiny-denominator backstop; and
Scotland is overridden (not filled) with recorded housebreaking from
Recorded Crime in Scotland, because police.uk lacks Police Scotland and its
only "Scottish" rows are British Transport Police railway incidents — at
32-council resolution (scripts/fetch_housebreaking.py, apportioned to
postcode geography by household share, three-year mean 2023-24…2025-26),
which replaced a single flat national rate on 2026-09-01.
Theft sits outside the vine — no meteorological driver — so it adds
~£29 of expected loss and almost no capital: an independent, stable peril
earns nearly a full diversification credit. Its expected loss is taken
analytically (p × E[severity]) rather than from the simulated draws: all
of a district's theft draws share one uniform stream, and at these
frequencies that Monte Carlo estimate is biased noise (it sat +17% over
the calibrated level) — same lesson as the Euler-capital conditioning.
5c. Escape of water — a flat base with a freeze slice
(scripts/fetch_metoffice.py frost). The UK's most frequent home claim
(ABI: £1.8m a day ≈ £657m/yr; ~29% of 2025's 560k home claims ≈ 164k
→ £4,000 average, 1.06% frequency — three public numbers that close into
one triangle). Plumbing failure has no open spatial predictor, so the
base frequency is flat; the ~15% freeze-attributable slice (insurers
put ~25% of claims in July–September — the no-freeze floor) scales with
each district's annual air-frost days (HadUK-Grid 1991–2020, ~1
day/yr on the Scilly coast to ~147 in the Highlands). eow_rate ships as
a rate (anchor × relativity, normalised in main() where the exposure
weights live) rather than an O(1) relativity — a raw relativity would
saturate the 0.5 frequency clip during the calibration pass and come out
2× hot with the geography flattened. Like theft it sits outside the
vine (cold is the one weather axis the storm-rooted vine does not
carry; its EL correlates −0.32 with the four-peril catastrophe EL) and
its EL is analytic for the same shared-stream reason — but unlike theft
it buys real capital: winter 2010's six-week freeze (~103k claims, £680m)
doubled the year's bill, pinning its factor loading at 2.6%, twenty times
theft's.
5d. Fire — LSOA-level incident geography (scripts/fetch_fires.py).
The strongest spatial signal of the attritional three, and the weakest
published level: the ABI stopped publishing a domestic fire total, so the
anchor is a documented triangle — 31,001 attended GB dwelling fires in
2024/25 (Home Office FIRE0201: England 25,465 + Scotland 4,104 + Wales
1,432) → 0.20%/policy; the ABI-attributed £10,200–£11,000 average fire
payout (~2019 vintage) indexed to £14,000; product ~£434m/yr ≈
13% of the £3.4bn all-home-claims total, inside the remainder envelope
after every published per-peril figure is subtracted. The geography:
England at LSOA level from the incident-level low-level-geography
datasets (8 complete years, 221,017 dwelling fires, reconciling with
FIRE0201 to the exact incident per year), Scotland by council
(statistics.gov.scot cube), Wales by FRA (stats.gov.wales), all
apportioned to districts through the ONS postcode lookup the household
counts already use. fire_rate ships as a rate for the same 0.5-clip
reason as eow_rate; rates are winsorised at the exposure-weighted
99.9th percentile (the same tiny-denominator guard theft keeps as its
backstop — W1J's fire rate prices at the cap, not at Mayfair's tiny
household denominator). Severity is
lognormal σ=1.3 (heaviest attritional tail, ~2% of claims above £100k),
EL analytic for the shared-stream reason. Fire buys no capital at
all: 45 years of FIRE0201 counts are a −2.5%/yr secular decline with
detrended residuals of ±2–3% and no spike years, pinning W_FIRE at
0.004% — pure expected loss, the purest diversifier in the book. One
parser trap for the record: ODS compresses identical consecutive rows
(table:number-rows-repeated), and sorted incident data hits it
constantly — the first parse silently dropped 39% of dwelling fires;
the FIRE0201 reconciliation is what caught it.
5e. Accidental damage — a census-children tilt on a flat base
(scripts/fetch_children.py). The peril with no published total,
ever — so the anchor is a third documented triangle: the ABI's 560,000
home claims (2025) × a 24.5% at-home AD share (a published claims-mix
table: 23.4% AD at home + 1.2% accidental loss at home; the 6.5%
away-from-home slice is excluded as personal-possessions cover) ×
Aviva's average AD payout interpolated to £1,650 for 2025 (from its
published £1,148/2022 and £1,869/2026 figures) ≈ £227m/yr, 0.89%
frequency. Cross-check: Aviva reports AD as 32% of its home claims, the
claims-mix table sums to 31% — two books, one share. The geography is
the gentlest in the model, because the only published demographic driver
is Aviva's "children cause 8% of AD claims": a flat base plus an 8%
slice scaled by each district's census share of households with
dependent children — E&W Census 2021 TS003 via NOMIS (LSOA,
paginated past the anonymous 25k-row cap), Scotland's Census 2022
UV113 at 2022 output-area grain joined through NRS's postcode→OA
index with per-postcode household weights ("dependent child" columns
matched by header text, not "non-dependent"; "-" cells are zeros).
27.29m households conserved through the joins (2% assert). ad_rate
ships as a rate for the same 0.5-clip reason as eow_rate; no
winsorisation, because a share of households is bounded by construction.
Severity lognormal σ=0.90 (narrowest attritional tail, ~0.7% of claims
above £10k), EL analytic for the shared-stream reason. AD buys no
capital: the 2020–21 lockdowns — the hardest a behavioural peril can
be shocked — moved home AD declarations only ~6%, pinning W_AD at
0.012%. One search trap for the record: the widely quoted "+39% AD
claims after lockdown" is motor insurance, not home
(DATA_SOURCES.md #28).
Vine copula. The four weather-driven annual-loss marginals (plus erosion) are joined by a C-vine (pair-copula construction) with weather W at the root:
The Gumbel pairs are upper-tail dependent (λᵤ = 2 − 2^(1/θ)): extreme years hit perils together. Sampling: Marshall–Olkin (positive-stable frailty) for the (W,F) root pair, then closed-form Gaussian h-inverses for tree 2 and numeric bisection of the Gumbel h-function for the remaining margins — 20,000 simulated years per district with common random numbers. A pairwise τ-matched 5-dim Gaussian copula (correlation matrix implied by the vine via partial-correlation recursion, batched Cholesky) and an independence run are simulated for comparison — quantifying how much of the tail an independence assumption hides.
Risk measure — TVaR, deliberately. With rare compound losses (flood claims at ~1%/yr), positive tail dependence concentrates claims into fewer, worse years — losses get pushed beyond the 99.5% quantile, so plain VaR can fall as dependence rises (VaR is not monotone under concordance ordering; expected shortfall is). Premiums and every dependence comparison therefore use TVaR 99% (mean of the worst 1% of years); the 1-in-200 VaR is still shown in popups.
Exposure weighting (scripts/fetch_households.py). Districts enter the
portfolio weighted by their census household count, not equally:
ONS Census 2021 households by LSOA for England & Wales, apportioned to
postcode districts through the ONS postcode→small-area lookup, plus
Scotland's Census 2022 national total spread across Scottish postcodes,
counting live postcodes only (ONSPD retains terminated ones — a third of
its rows — and apportioning across them credited ~730k homes to dead
addresses until 2026-08-12) —
27.3m households over 2,866 districts. This matters because the ABI
anchors are national totals: the calibration average, the portfolio tail
and the capital allocation are all household-weighted, so Croydon
(56,000 households) counts 56,000× a single-household district rather
than 1×.
Capital, allocated from the portfolio (Euler). An insurer holds capital against the portfolio, not against each policy's private worst year, so the charge is the district's expected loss given the portfolio is in its worst 1% of years — TVaRᵢ = E[Lᵢ | L_portfolio ≥ VaR₉₉]. These allocations sum exactly to the portfolio TVaR, crediting diversification. Technical premium = E[loss] + 6% × (allocated TVaR − E[loss]); districts banded into deciles → rating groups 1–10. (An earlier version charged 6% of each district's standalone TVaR, which double-counted risk and inflated premiums roughly tenfold.)
The allocation averages conditional expectations, not realised losses — and that is not a detail. The worst 1% of 20,000 years is 200 draws, and at calibrated frequencies a district claims in one or two of them, so averaging its realised loss there made capital, and therefore the published rating group, largely Monte Carlo noise. Measured head to head on the same fixture, the realised estimator correlated just 0.49 and 0.39 with itself across RNG seeds; a +0.4% change in flood expected loss moved one district 8 rating deciles. Conditioning on each year's systemic draw instead — Φ((√w·z − Φ⁻¹(1−p))/√(1−w)) × E[severity], which the factor model already gives in closed form — removes the Bernoulli noise and lifts that to 0.97, and to 0.9985 at the production 20,000 years. It costs nothing in runtime, is exact rather than approximate (the bad-year ranking uses the same smoothed quantity, so the selection is systemic-measurable and the tower property applies), and is guarded by a test. This is the same lesson as the removed copula-uplift layer: at these frequencies, per-district Monte Carlo estimates of tail quantities are noise unless you condition. The spatial loading behind the portfolio view is likewise solved rather than assumed: it is set so a 1-in-100 year has ~2× the claim frequency of an average year. The solved value is far weaker than the assumed one — UK weather is strongly correlated nationally, but UK claims are not.
Sensitivity analysis (scripts/sensitivity.py →
data/sensitivity.json, rendered as a table on the analysis page).
Re-runs the simulation on a 1-in-3 district sample with perturbed
assumptions — Gumbel dependence ±25%, tree-2 correlations zeroed/doubled,
severity σ ×1.1 at a held mean, flood frequency ×1.5, erosion switched
from the adopted defence plan to no-further-intervention, four
alternative flood depth-damage curves, and the other unanchored
constants (flood band frequencies, groundwater share and background,
systemic loadings) — reporting the impact on
expected loss, premium, catastrophic-year cost and rating-group churn. This
quantifies which of the documented assumptions actually move the answer:
the copula parameters govern the tail, the marginals govern the ranking.
The churn column measures each perturbation, not a noise floor, because
capital is a conditional expectation rather than a Monte Carlo average.
pyvinecopulib) instead;