added re-baselining to the property model

This commit is contained in:
Khalim Conn-Kowlessar 2026-01-10 14:59:46 +00:00
parent b156513524
commit 808a5122ee
6 changed files with 118 additions and 32 deletions

View file

@ -800,13 +800,19 @@ class Property:
to_update[k] = None
return to_update
def get_full_property_data(self, current_valuation=None):
def get_full_property_data(self, current_valuation=None, needs_rebaselining=False, rebaselining_sap=0):
"""
This method extracts the data which is pushed to the database, containing core information, from the EPC
about a property
:return:
"""
current_sap_rating = self.data["current-energy-efficiency"]
if needs_rebaselining:
current_sap_rating += rebaselining_sap
current_epc_rating = sap_to_epc(current_sap_rating)
property_data = {
"creation_status": "READY",
"uprn": int(self.data["uprn"]),
@ -823,9 +829,12 @@ class Property:
"number_of_rooms": self.number_of_rooms,
"year_built": self.year_built,
"tenure": self.data["tenure"],
"current_epc_rating": self.data["current-energy-rating"],
"current_sap_points": self.data["current-energy-efficiency"],
"current_epc_rating": current_epc_rating,
"current_sap_points": current_sap_rating,
"current_valuation": current_valuation,
"original_sap_points": self.data["current-energy-efficiency"],
"is_sap_points_adjusted_for_installed_measures": needs_rebaselining,
"installed_measures_sap_point_adjustment": rebaselining_sap,
}
property_data = self._clean_upload_data(property_data)

View file

@ -27,7 +27,6 @@ def prepare_plan_data(
"""
# Plan carbon savings
co2_savings = sum([r["co2_equivalent_savings"] for r in default_recommendations])
raise Exception("CHECK ME")
post_co2_emissions = p.energy["co2_emissions"] - co2_savings
# Plan bill savings

View file

@ -929,9 +929,7 @@ async def model_engine(body: PlanTriggerRequest):
# any panel performance, we ensure that we have a 3kWp and 4kWp option for the property
logger.info("Identifying property recommendations")
recommendations = {}
recommendations_scoring_data = []
representative_recommendations = {}
recommendations, recommendations_scoring_data, representative_recommendations = {}, [], {}
for p in tqdm(input_properties):
# We set the ECO package data, if we have it
property_eco_package = eco_packages.get(p.id, (None, None, None))
@ -965,17 +963,15 @@ async def model_engine(body: PlanTriggerRequest):
recommendations_scoring_data.extend(p.recommendations_scoring_data)
logger.info("Preparing data for scoring in sap change api")
recommendations_scoring_data = pd.DataFrame(recommendations_scoring_data)
recommendations_scoring_data = pd.DataFrame(recommendations_scoring_data).drop(
columns=[
"rdsap_change", "heat_demand_change", "carbon_change", "sap_ending", "heat_demand_ending",
"carbon_ending"
]
)
# Temp putting this here
recommendations_scoring_data["is_post_sap10_ending"] = True
recommendations_scoring_data["sap_starting"] = 77
recommendations_scoring_data = recommendations_scoring_data.drop(
columns=["rdsap_change", "heat_demand_change", "carbon_change", "sap_ending", "heat_demand_ending",
"carbon_ending"]
)
all_predictions = await model_api.async_paginated_predictions(
data=recommendations_scoring_data,
bucket=get_settings().DATA_BUCKET,
@ -1015,19 +1011,19 @@ async def model_engine(body: PlanTriggerRequest):
# We now insert kwh estimates and costs into the recommendations
logger.info("Calculating tenant savings - kwh and bills")
for property_id in tqdm([p.id for p in input_properties]):
for p in tqdm(input_properties):
property_id = p.id
property_recommendations = recommendations.get(property_id, [])
property_instance = [p for p in input_properties if p.id == property_id][0]
property_current_energy_bill = (
Recommendations.calculate_recommendation_tenant_savings(
property_instance=property_instance,
property_instance=p,
kwh_simulation_predictions=kwh_simulation_predictions,
property_recommendations=property_recommendations,
ashp_cop=body.ashp_cop
)
)
property_instance.current_energy_bill = property_current_energy_bill
p.current_energy_bill = property_current_energy_bill
# Insert the predictions into the recommendations and run the optimiser
logger.info("Optimising measures")
@ -1195,23 +1191,40 @@ async def model_engine(body: PlanTriggerRequest):
property_updates, property_epc_details, property_spatial_updates = [], [], []
plans_to_create, recommendations_to_create = [], []
# TODO: Check the update to carbon
print("NEED TO CHECK THE UPDATE TO CARBON")
# Prepare the data that will need to be uploaded in bulk
for p in input_properties:
recommendations_for_property = recommendations.get(p.id, [])
default_recommendations = [r for r in recommendations_for_property if r["default"]]
# We need to:
# Get already installed measures
already_installed_default = [r for r in default_recommendations if r["already_installed"]]
# Property should be have increased SAP
needs_rebaselining = bool(len(already_installed_default))
rebaselining_sap = float(sum([r["sap_points"] for r in already_installed_default]))
rebaselining_carbon = float(sum([r["co2_equivalent_savings"] for r in already_installed_default]))
rebaselining_heat_demand = float(sum([r["heat_demand"] for r in already_installed_default]))
rebaselining_kwh = float(sum([r["kwh_savings"] for r in already_installed_default]))
rebaselining_bills = float(sum([r["energy_cost_savings"] for r in already_installed_default]))
# TODO - gotta apply the adjustments to the property table, and the property_details_epc table
# This will include everything, including already installed
total_sap_points = sum([r["sap_points"] for r in default_recommendations])
new_sap_points = float(p.data["current-energy-efficiency"]) + total_sap_points
new_epc = sap_to_epc(new_sap_points)
total_cost = sum([r["total"] for r in default_recommendations])
# Already installed measures do not have a cost but we remove anyway
total_cost = sum([r["total"] for r in default_recommendations if not r["already_installed"]])
valuations = PropertyValuation.estimate(property_instance=p, target_epc=new_epc, total_cost=total_cost)
# --- property-level updates (always) ---
property_updates.append({
"property_id": p.id,
"portfolio_id": body.portfolio_id,
"data": p.get_full_property_data(current_valuation=valuations["current_value"])
"data": p.get_full_property_data(
current_valuation=valuations["current_value"],
needs_rebaselining=needs_rebaselining,
rebaselining_sap=rebaselining_sap,
)
})
property_epc_details.append(p.get_property_details_epc(portfolio_id=body.portfolio_id))

View file

@ -142,7 +142,8 @@ class ModelApi:
@staticmethod
def extract_phase(recommendation_id):
if 'phase=' in recommendation_id:
return int(recommendation_id.split('phase=')[1][0])
extracted = recommendation_id.split('phase=')[1]
return int(extracted.strip())
else:
return None

View file

@ -1,5 +1,6 @@
import re
import backend.app.assumptions as assumptions
from etl.customers.immo.pilot.asset_list import already_installed
from recommendations.recommendation_utils import (
check_simulation_difference, override_costs, combine_recommendation_configs
)
@ -320,12 +321,6 @@ class HeatingRecommender:
measures = MEASURE_MAP["heating"] if measures is None else measures
# TODO: We could have a system flush recommendation for an existing boiler, where there is no need to replace
# the boiler, but instead flushing the system will make it run more efficiently. There is a cost for this
# in the Costs class, stored as SYSTEM_FLUSH_COST
# TODO: Right now, we don't have recommendations for electric boilers - we should probably have one
# if we have a non-invasive ashp recommendation, we get the configuration directly from the property instance
non_invasive_ashp_recommendation = next(
(r for r in self.property.non_invasive_recommendations if r["type"] == "air_source_heat_pump"),
@ -1115,6 +1110,7 @@ class HeatingRecommender:
"hot-water-energy-eff": heating_simulation_config["hot_water_energy_eff_ending"]
}
# TODO: Probably don't need to use this for HHRSH - simplify
recommendations = self.combine_heating_and_controls(
controls_recommendations=controls_recommender.recommendation,
heating_simulation_config=heating_simulation_config,
@ -1128,6 +1124,12 @@ class HeatingRecommender:
non_intrusive_recommendation=non_intrusive_recommendation,
heating_product=hhrsh_product
)
# Check if HHRSH are already installed
already_installed = "high_heat_retention_storage_heaters" in self.property.already_installed
for rec in recommendations:
rec["already_installed"] = already_installed
if _return:
return recommendations
@ -1347,7 +1349,7 @@ class HeatingRecommender:
n_rooms=self.property.number_of_rooms
)
already_installed = "heating" in self.property.already_installed
already_installed = "boiler_upgrade" in self.property.already_installed
if already_installed:
boiler_costs = override_costs(boiler_costs)
description = "Heating system has already been upgraded, no further action needed."

View file

@ -272,6 +272,36 @@ class Recommendations:
property_recommendations.append(self.solar_recommender.recommendation)
phase += 1
if self.property_instance.already_installed:
# We need to re-shuffle our measures
property_recommendations_removed_installed = []
already_installed_recs = []
for recs in property_recommendations:
phase_recs = []
phase_already_installed_recs = []
for rec in recs:
if rec["already_installed"]:
phase_already_installed_recs.append(rec)
else:
phase_recs.append(rec)
if phase_recs:
property_recommendations_removed_installed.append(phase_recs)
if phase_already_installed_recs:
already_installed_recs.append(phase_already_installed_recs)
# We re-set the phases
for i, recs in enumerate(property_recommendations_removed_installed):
for rec in recs:
rec["phase"] = i
# already installed recs get negative phasing
already_installed_phase = -len(already_installed_recs)
for recs in already_installed_recs:
for rec in recs:
rec["phase"] = already_installed_phase
already_installed_phase += 1
property_recommendations = already_installed_recs + property_recommendations_removed_installed
# We insert temporary ids into the recommendations which is important for the optimiser later
property_recommendations = self.insert_temp_recommendation_id(property_recommendations)
@ -486,6 +516,11 @@ class Recommendations:
mv_increasing_variables = ["carbon", "heat_demand"]
mv_decreasing_variables = ["sap"]
# We allow for negative phase
starting_phase = min(
rec["phase"] for recs in property_recommendations for rec in recs
)
impact_summary = []
for recommendations_by_type in property_recommendations:
for rec in recommendations_by_type:
@ -526,7 +561,7 @@ class Recommendations:
# We structure this so that depending on the phase, we capture the previous phase impacts and
# then just have one piece of code to calculate the difference
if rec["phase"] == 0:
if rec["phase"] == starting_phase:
# These are just the starting values, from the EPC. When we score the ML models,
# heating_cost_starting and heating_cost_ending are just the values in the EPC. However, with
# heating_cost_ending, we expect that the EPC will predict a heating cost based on what would happen
@ -954,6 +989,33 @@ class Recommendations:
pd.isnull(kwh_impact_table["hotwater_fuel_type"]).sum()):
raise Exception("Fuel type is missing")
# As one final adjustment, if we
# 1) have a boiler upgrade recommendation
# 2) Have an average efficiency boiler, we adjust the COP of the existing boiler down to 75%
heating_upgrades = [x for x in property_recommendations if x[0]["type"] == "heating"]
boiler_upgrade = [r for recs in heating_upgrades for r in recs if r["measure_type"] == "boiler_upgrade"]
existing_heating_efficiency = property_instance.data["mainheat-energy-eff"]
if len(boiler_upgrade) and existing_heating_efficiency in ["Very Poor", "Poor", "Average"]:
efficiency_map = {"Very Poor": 0.6, "Poor": 0.65, "Average": 0.7}
adjusted_cop = efficiency_map[existing_heating_efficiency]
boiler_phase = boiler_upgrade[0]["phase"]
heating_measure_types_to_id = [
{"recommendation_id": r["recommendation_id"], "measure_type": r["measure_type"]}
for r in heating_upgrades[0]
]
kwh_impact_table = kwh_impact_table.merge(
pd.DataFrame(heating_measure_types_to_id), how="left", on="recommendation_id"
)
for col in ["heating_cop", "hotwater_cop"]:
kwh_impact_table[col] = np.where(
(kwh_impact_table["phase"] <= boiler_phase) &
(kwh_impact_table["heating_fuel_type"] == "Natural Gas") &
(kwh_impact_table["measure_type"] != "boiler_upgrade"),
adjusted_cop, kwh_impact_table[col]
)
kwh_impact_table = kwh_impact_table.drop(columns=["measure_type"])
# We now calculate the fuel cost
for k in ["heating", "hotwater"]:
kwh_impact_table[f"{k}_cost"] = kwh_impact_table.apply(