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]