mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-22 08:48:38 +00:00
ADR-0030 commits Component Accuracy to ~19 categorical components (5 today + 8 heating + glazing/renewables). Flat *_correct dataclass fields don't scale — each needs manual runner wiring. Collapse them into a single `categorical_hits: dict[str, Optional[bool]]` keyed by component name, which also matches the runner's name-keyed aggregation (now generic: it tallies whatever components the comparison reports). No behaviour change; the classification rates are identical (wall n 578->575 is the 3 certs whose actual wall is None, now correctly counted as not-applicable via _classify). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
100 lines
3.8 KiB
Python
100 lines
3.8 KiB
Python
"""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 typing import Optional
|
||
|
||
from datatypes.epc.domain.epc_property_data import EpcPropertyData, SapBuildingPart
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class PredictionComparison:
|
||
"""One Property's prediction accuracy: per-component classification hits +
|
||
geometry residuals (predicted − actual). `categorical_hits` maps a component
|
||
name to its hit: True / False, or `None` ("not applicable") when the actual
|
||
lodges no value there, so the harness can keep it out of the
|
||
classification-rate denominator rather than score a free win. Keyed by name
|
||
(not flat fields) so the component set can grow without reshaping the
|
||
runner — see ADR-0030 Component Accuracy."""
|
||
|
||
categorical_hits: dict[str, Optional[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 _main_floor_construction(epc: EpcPropertyData) -> Optional[int]:
|
||
"""The main building part's ground-floor construction code, or None when no
|
||
floor dimension is lodged."""
|
||
dims = _main(epc).sap_floor_dimensions
|
||
return dims[0].floor_construction if dims else None
|
||
|
||
|
||
def _classify(predicted: object, actual: object) -> Optional[bool]:
|
||
"""A categorical hit: None ("not applicable") when the actual is absent,
|
||
else whether the predicted value matches it."""
|
||
if actual is None:
|
||
return None
|
||
return predicted == actual
|
||
|
||
|
||
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(
|
||
categorical_hits={
|
||
"wall_construction": _classify(
|
||
_main(predicted).wall_construction,
|
||
_main(actual).wall_construction,
|
||
),
|
||
"wall_insulation_type": _classify(
|
||
_main(predicted).wall_insulation_type,
|
||
_main(actual).wall_insulation_type,
|
||
),
|
||
"construction_age_band": _classify(
|
||
_main(predicted).construction_age_band,
|
||
_main(actual).construction_age_band,
|
||
),
|
||
"roof_construction": _classify(
|
||
_main(predicted).roof_construction,
|
||
_main(actual).roof_construction,
|
||
),
|
||
"floor_construction": _classify(
|
||
_main_floor_construction(predicted),
|
||
_main_floor_construction(actual),
|
||
),
|
||
},
|
||
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)
|
||
),
|
||
)
|