Model/tests/scripts/test_neighbour_divergence.py
Jun-te Kim 009394bb19 neighbour_divergence: key cohorts on main-heating fuel
The neighbour SAP-divergence cohort was (postcode, property_type, built_form),
which mixed electric- and gas-heated dwellings. Electricity scores materially
lower in SAP than mains gas for identical fabric, so a mixed-fuel postcode
produced cross-fuel false outliers — an electric dwelling flagged only for being
electric among gas neighbours.

Add the main-heating fuel class to the cohort key (electricity variants — 29/30
+ off-peak 31-40 — collapse to one class; every other code is its own bucket).
Now every flagged divergence is a same-fuel comparison worth investigating. On
portfolio 814 this refined 20 raw divergences to 17 same-fuel ones while keeping
the genuine within-fuel outliers (e.g. the all-electric WC2B 4AW flats at SAP
26/38 vs a cohort median of 67).

Reaches the fuel via epc_main_heating_detail through the indexed property_id —
still never touches the recommendation table. Surfaced by the portfolio-814
recommendation audit (Work item B, #1388).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 14:24:31 +00:00

109 lines
3.6 KiB
Python

from typing import Optional
from scripts.audit.neighbour_divergence import Neighbour, find_divergences
def _n(
property_id: int,
*,
effective_sap: Optional[float],
postcode: str = "AB1 2CD",
property_type: str = "Flat",
built_form: str = "Mid-Terrace",
floor_area_m2: float = 60.0,
main_fuel_type: Optional[int] = 26, # default: mains gas
) -> Neighbour:
return Neighbour(
property_id=property_id,
uprn=None,
postcode=postcode,
property_type=property_type,
built_form=built_form,
floor_area_m2=floor_area_m2,
effective_sap=effective_sap,
effective_band=None,
main_fuel_type=main_fuel_type,
)
class TestFindDivergences:
def test_flags_the_outlier_neighbour(self) -> None:
# Arrange — three identical-cohort flats, one sits a clear band below.
cohort = [
_n(1, effective_sap=70.0),
_n(2, effective_sap=72.0),
_n(3, effective_sap=50.0), # Δ-20 vs median 70
]
# Act
result = find_divergences(cohort, min_gap=12.0)
# Assert — only the outlier is flagged.
assert [d.property_id for d in result] == [3]
def test_silent_when_cohort_agrees(self) -> None:
# Arrange — neighbours within a few SAP points of each other.
cohort = [_n(1, effective_sap=70.0), _n(2, effective_sap=68.0)]
# Act
result = find_divergences(cohort, min_gap=12.0)
# Assert
assert result == []
def test_singletons_never_flagged(self) -> None:
# Arrange — one flat here, one house there: neither has a peer to diverge from.
lonely = [
_n(1, effective_sap=30.0, property_type="Flat"),
_n(2, effective_sap=90.0, property_type="House"),
]
# Act
result = find_divergences(lonely, min_gap=12.0)
# Assert
assert result == []
def test_different_postcode_is_a_different_cohort(self) -> None:
# Arrange — same type/form but different postcodes: not neighbours.
split = [
_n(1, effective_sap=40.0, postcode="AB1 2CD"),
_n(2, effective_sap=80.0, postcode="ZZ9 9ZZ"),
]
# Act
result = find_divergences(split, min_gap=12.0)
# Assert
assert result == []
def test_electric_and_gas_are_separate_cohorts(self) -> None:
# Arrange — same postcode/type/form, but an electric dwelling (fuel 29)
# scores a band below its gas neighbours (fuel 26). Electricity is
# inherently lower-SAP, so this is expected stock, not a divergence.
mixed = [
_n(1, effective_sap=70.0, main_fuel_type=26), # gas
_n(2, effective_sap=72.0, main_fuel_type=26), # gas
_n(3, effective_sap=52.0, main_fuel_type=29), # electric — its own cohort
]
# Act
result = find_divergences(mixed, min_gap=12.0)
# Assert — the electric singleton has no same-fuel peers, so nothing fires.
assert result == []
def test_electric_outlier_still_flagged_within_its_own_fuel_cohort(self) -> None:
# Arrange — three electric dwellings; one genuinely diverges. Keying on
# fuel must not blind the detector to real within-fuel outliers.
electric = [
_n(1, effective_sap=55.0, main_fuel_type=29),
_n(2, effective_sap=57.0, main_fuel_type=30), # 30 also electric-class
_n(3, effective_sap=30.0, main_fuel_type=29), # Δ-25 vs median
]
# Act
result = find_divergences(electric, min_gap=12.0)
# Assert
assert [d.property_id for d in result] == [3]