mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
A focused sibling to audit-ara-portfolio: that skill audits baselines/plans/SAP; this one audits the *recommendations themselves* — why a measure was or wasn't offered. Motivated by the portfolio-814 review (Khalim's HHRSH-on-community- heating, missing-HHRSH, missing-secondary-heating-removal, and a neighbour split). Adds: - .claude/skills/find-weird-recommendations/SKILL.md — scan -> neighbour scan -> live re-model deep-dive -> root-cause -> codify, with a seeded known-bug catalogue and the query-safety rules inherited from audit-ara-portfolio. - scripts/audit/anomalies.py: new `plan-stops-short-of-goal` HIGH check — the default plan ends below the goal band on an unlimited-budget scenario (the deterministic worklist for "why didn't this get recommended X"). Adds scenario_budget to the bundle/query so budget-capped scenarios are excluded. - scripts/audit/neighbour_divergence.py: groups a portfolio by (postcode, property_type, built_form) and flags effective-SAP outliers vs the cohort median. Never touches the 26m-row recommendation table, so it is safe portfolio-wide. - Tests for both (12 passing). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
151 lines
4.5 KiB
Python
151 lines
4.5 KiB
Python
from typing import Optional
|
|
|
|
import pytest
|
|
|
|
from scripts.audit.anomalies import (
|
|
PropertyAudit,
|
|
_effective_lodged_divergence,
|
|
_implausible_lodged_score,
|
|
_plan_stops_short_of_goal,
|
|
)
|
|
|
|
|
|
def _make_audit(
|
|
*,
|
|
lodged_sap: Optional[float] = None,
|
|
effective_sap: Optional[float] = None,
|
|
rebaseline_reason: str = "both",
|
|
scenario_goal_band: Optional[str] = None,
|
|
scenario_budget: Optional[float] = None,
|
|
post_band: Optional[str] = None,
|
|
post_sap: Optional[float] = None,
|
|
cost_of_works: Optional[float] = None,
|
|
) -> PropertyAudit:
|
|
return PropertyAudit(
|
|
property_id=1,
|
|
uprn=None,
|
|
portfolio_id=796,
|
|
scenario_id=None,
|
|
scenario_goal_band=scenario_goal_band,
|
|
scenario_budget=scenario_budget,
|
|
lodged_sap=lodged_sap,
|
|
lodged_band=None,
|
|
effective_sap=effective_sap,
|
|
effective_band=None,
|
|
rebaseline_reason=rebaseline_reason,
|
|
post_sap=post_sap,
|
|
post_band=post_band,
|
|
cost_of_works=cost_of_works,
|
|
energy_bill_savings=None,
|
|
energy_consumption_savings=None,
|
|
solar_sap_points=None,
|
|
solar_bill_savings=None,
|
|
n_measures=0,
|
|
)
|
|
|
|
|
|
class TestEffectiveLodgedDivergence:
|
|
def test_silent_when_lodged_sap_below_floor(self) -> None:
|
|
# Arrange — Class C exemplar: lodged=1, effective=76 (gap +75, but lodged is implausible upstream data)
|
|
audit = _make_audit(lodged_sap=1.0, effective_sap=76.0)
|
|
|
|
# Act
|
|
result = _effective_lodged_divergence(audit)
|
|
|
|
# Assert
|
|
assert result is None
|
|
|
|
def test_fires_when_lodged_sap_at_or_above_floor_with_large_gap(self) -> None:
|
|
# Arrange — lodged=20 (band G, plausible), effective=65 (band D), gap +45
|
|
audit = _make_audit(lodged_sap=20.0, effective_sap=65.0)
|
|
|
|
# Act
|
|
result = _effective_lodged_divergence(audit)
|
|
|
|
# Assert
|
|
assert result is not None
|
|
assert "65" in result
|
|
assert "20" in result
|
|
|
|
|
|
class TestImplausibleLodgedScore:
|
|
def test_fires_when_lodged_sap_below_floor_and_gap_large(self) -> None:
|
|
# Arrange — Class C exemplar: lodged=1, effective=76 (gap +75)
|
|
audit = _make_audit(lodged_sap=1.0, effective_sap=76.0)
|
|
|
|
# Act
|
|
result = _implausible_lodged_score(audit)
|
|
|
|
# Assert
|
|
assert result is not None
|
|
assert "1" in result
|
|
|
|
def test_silent_when_gap_below_threshold(self) -> None:
|
|
# Arrange — lodged=9, effective=5 (gap -4, below ±15): both low, not a divergence anomaly
|
|
audit = _make_audit(lodged_sap=9.0, effective_sap=5.0)
|
|
|
|
# Act
|
|
result = _implausible_lodged_score(audit)
|
|
|
|
# Assert
|
|
assert result is None
|
|
|
|
def test_silent_when_lodged_sap_at_or_above_floor(self) -> None:
|
|
# Arrange — lodged=13 is on the floor boundary; effective=70 gives large gap
|
|
audit = _make_audit(lodged_sap=13.0, effective_sap=70.0)
|
|
|
|
# Act
|
|
result = _implausible_lodged_score(audit)
|
|
|
|
# Assert
|
|
assert result is None
|
|
|
|
|
|
class TestPlanStopsShortOfGoal:
|
|
def test_fires_when_short_of_goal_on_unlimited_budget(self) -> None:
|
|
# Arrange — goal C, plan lands at D, budget unlimited (None): a shortfall
|
|
# the engine should be able to explain (Khalim's 742121 class).
|
|
audit = _make_audit(
|
|
scenario_goal_band="C",
|
|
scenario_budget=None,
|
|
post_band="D",
|
|
post_sap=63.0,
|
|
cost_of_works=8000.0,
|
|
)
|
|
|
|
# Act
|
|
result = _plan_stops_short_of_goal(audit)
|
|
|
|
# Assert
|
|
assert result is not None
|
|
assert "D" in result
|
|
assert "C" in result
|
|
|
|
def test_silent_when_plan_meets_goal(self) -> None:
|
|
# Arrange — goal C, plan reaches C: nothing to explain.
|
|
audit = _make_audit(
|
|
scenario_goal_band="C", scenario_budget=None, post_band="C", post_sap=70.0
|
|
)
|
|
|
|
# Act
|
|
result = _plan_stops_short_of_goal(audit)
|
|
|
|
# Assert
|
|
assert result is None
|
|
|
|
def test_silent_when_budget_capped(self) -> None:
|
|
# Arrange — goal C, plan short at D, but the scenario is budget-capped:
|
|
# falling short is expected once the money runs out, not an anomaly.
|
|
audit = _make_audit(
|
|
scenario_goal_band="C",
|
|
scenario_budget=5000.0,
|
|
post_band="D",
|
|
post_sap=63.0,
|
|
cost_of_works=5000.0,
|
|
)
|
|
|
|
# Act
|
|
result = _plan_stops_short_of_goal(audit)
|
|
|
|
# Assert
|
|
assert result is None
|