Model/domain/epc_prediction/prediction_comparison.py
Khalim Conn-Kowlessar f3ad6343a3 feat(epc-prediction): leave-one-out validation harness (ADR-0029)
Pure compare_prediction (TDD): wall-construction classification hit + signed
residuals on floor area, window count, total window area, building-parts count.
Plus validate_epc_prediction.py (IO plumbing): drops each cert from its postcode
cohort, predicts from the rest on guaranteed inputs only, aggregates the metrics,
and reports SAP three ways (pred-calc vs lodged / vs calc-on-actual / vs the
neighbour-mean baseline). Smoke run: wall 90.9%, floor-area mean|·| 42.6 m2 (a
real signal — template-copied floor area is noisy), SAP pred-calc edges baseline.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 23:55:05 +00:00

60 lines
2.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Per-Property prediction comparison for the EPC Prediction validation harness
(ADR-0029).
`compare_prediction` scores a predicted `EpcPropertyData` against the actual one
on the accuracy signals the leave-one-out harness aggregates: classification
matches on the key categoricals (wall / roof / floor construction + insulation,
construction age band) and residuals on the geometry (window area + count,
building-parts count, floor area). Pure — the SAP residual is computed in the
runner, which has the calculator and the lodged SAP.
"""
from __future__ import annotations
from dataclasses import dataclass
from datatypes.epc.domain.epc_property_data import EpcPropertyData, SapBuildingPart
@dataclass(frozen=True)
class PredictionComparison:
"""One Property's prediction accuracy: classification hits + geometry
residuals (predicted actual)."""
wall_construction_correct: bool
floor_area_residual: float
building_parts_residual: int
window_count_residual: int
total_window_area_residual: float
def _main(epc: EpcPropertyData) -> SapBuildingPart:
return epc.sap_building_parts[0]
def _total_window_area(epc: EpcPropertyData) -> float:
return sum(w.window_width * w.window_height for w in epc.sap_windows)
def compare_prediction(
predicted: EpcPropertyData, actual: EpcPropertyData
) -> PredictionComparison:
"""Compare a predicted picture against the actual one, field by field. All
residuals are signed, predicted actual."""
return PredictionComparison(
wall_construction_correct=(
_main(predicted).wall_construction == _main(actual).wall_construction
),
floor_area_residual=(
predicted.total_floor_area_m2 - actual.total_floor_area_m2
),
building_parts_residual=(
len(predicted.sap_building_parts) - len(actual.sap_building_parts)
),
window_count_residual=(
len(predicted.sap_windows) - len(actual.sap_windows)
),
total_window_area_residual=(
_total_window_area(predicted) - _total_window_area(actual)
),
)