Added in the sequential scoring code

This commit is contained in:
Khalim Conn-Kowlessar 2024-02-14 19:15:41 +00:00
parent 925a6c1887
commit 0f60082ba1
4 changed files with 207 additions and 114 deletions

View file

@ -150,158 +150,195 @@ class Property:
# self.base_difference_record.df
def adjust_difference_record_with_recommendations(self, property_recommendations):
def adjust_difference_record_with_recommendations(
self, property_recommendations,
property_representative_recommendations
):
"""
This method will adjust the difference record, based on the recommendations made for the property
In order to score the measures, we need to consider the phase of the retrofit.
:param property_recommendations: dictionary of recommendations for the property
:param property_representative_recommendations: dictionary of representative recommendations for the property
"""
self.recommendations_scoring_data = []
phases = sorted([r[0]["phase"] for r in property_recommendations if r[0]["phase"] is not None])
for phase in phases:
property_recommendations_by_phase = [r for r in property_recommendations if r[0]["phase"] == phase][0]
previous_phases = [p for p in phases if p < phase]
previous_phase_representatives = [
r for r in property_representative_recommendations if r["phase"] in previous_phases
]
recommendation_record = self.base_difference_record.df.to_dict("records")[0].copy()
for rec in property_recommendations_by_phase:
# We simulate the impact of the recommendation at this current phase, and all of the prior phases
for recommendations_by_type in property_recommendations:
for i, rec in enumerate(recommendations_by_type):
recommendation_record = self.base_difference_record.df.to_dict("records")[0].copy()
scoring_dict = self.create_recommendation_scoring_data(
property_id=self.id, recommendation_record=recommendation_record, recommendation=rec,
property_id=self.id,
recommendation_record=recommendation_record,
recommendations=previous_phase_representatives + [rec],
primary_recommendation_id=rec["recommendation_id"]
)
self.recommendations_scoring_data.append(scoring_dict)
@staticmethod
def create_recommendation_scoring_data(property_id, recommendation_record, recommendation: dict):
def create_recommendation_scoring_data(
property_id, recommendation_record, recommendations: list, primary_recommendation_id: int
):
"""
This function will iterate through a list of recommendations and apply a simulation for each recommendation
This allows us to later multiple measures and see the impact of the measures on the property
:param property_id: The id of the property
:param recommendation_record: The record of the property, which will be updated
:param recommendations: The list of recommendations to apply
:param primary_recommendation_id: The id of the primary recommendation, which is used to identify the record
:return: The updated recommendation record
"""
output = recommendation_record.copy()
for col in [
"walls_insulation_thickness", "floor_insulation_thickness", "roof_insulation_thickness"
]:
if recommendation_record[col] is None:
recommendation_record[col] = "none"
if output[col] is None:
output[col] = "none"
# We update the description to indicate it's insulated
if recommendation["type"] in ["internal_wall_insulation", "external_wall_insulation", "cavity_wall_insulation"]:
# The upgrade made here is to the u-value of the walls and the description of the
# insulation thickness
recommendation_record["walls_thermal_transmittance_ending"] = recommendation["new_u_value"]
recommendation_record["walls_insulation_thickness_ending"] = "above average"
recommendation_record["walls_energy_eff_ending"] = "Good"
for recommendation in recommendations:
# For the list of recommendations we have, we iteratively update the output
# Note: often when the wall is insulatied, the internal/external insulation is not noted so we should
# test the impact of using these booleans
if recommendation["type"] == "external_wall_insulation":
recommendation_record["external_insulation"] = True
recommendation_record["internal_insulation"] = False
# We update the description to indicate it's insulated
if recommendation["type"] in [
"internal_wall_insulation", "external_wall_insulation", "cavity_wall_insulation"
]:
# The upgrade made here is to the u-value of the walls and the description of the
# insulation thickness
output["walls_thermal_transmittance_ending"] = recommendation["new_u_value"]
output["walls_insulation_thickness_ending"] = "above average"
output["walls_energy_eff_ending"] = "Good"
if recommendation["type"] == "internal_wall_insulation":
recommendation_record["external_insulation"] = False
recommendation_record["internal_insulation"] = True
# Note: often when the wall is insulatied, the internal/external insulation is not noted so we should
# test the impact of using these booleans
if recommendation["type"] == "external_wall_insulation":
output["external_insulation"] = True
output["internal_insulation"] = False
else:
if recommendation_record["walls_thermal_transmittance_ending"] is None:
raise ValueError("We should not have a None value for the u value")
if recommendation["type"] == "internal_wall_insulation":
output["external_insulation"] = False
output["internal_insulation"] = True
if recommendation_record["walls_insulation_thickness_ending"] is None:
recommendation_record["walls_insulation_thickness_ending"] = "none"
# When making a recommendation for the wall, we will also update the ventilation
if output["mechanical_ventilation_ending"] == 'natural':
output["mechanical_ventilation_ending"] = 'mechanical, extract only'
# Update description to indicate it's insulate
if recommendation["type"] in [
"solid_floor_insulation", "suspended_floor_insulation", "exposed_floor_insulation"
]:
if len(recommendation["parts"]) > 1:
raise NotImplementedError("Have more than 1 floor insulation part - handle this case")
else:
if output["walls_thermal_transmittance_ending"] is None:
raise ValueError("We should not have a None value for the u value")
# recommendation_record["floor_thermal_transmittance_ending"] = recommendation["new_u_value"]
# We don't really see above average for this in the training data
recommendation_record["floor_insulation_thickness_ending"] = "average"
# This is rarely ever populated in the training data
# recommendation_record["floor_energy_eff_ending"] = "Good"
else:
if recommendation_record["floor_thermal_transmittance_ending"] is None:
raise ValueError("We should not have a None value for the u value")
if output["walls_insulation_thickness_ending"] is None:
output["walls_insulation_thickness_ending"] = "none"
if recommendation_record["floor_insulation_thickness_ending"] is None:
recommendation_record["floor_insulation_thickness_ending"] = "none"
# Update description to indicate it's insulate
if recommendation["type"] in [
"solid_floor_insulation", "suspended_floor_insulation", "exposed_floor_insulation"
]:
if len(recommendation["parts"]) > 1:
raise NotImplementedError("Have more than 1 floor insulation part - handle this case")
if recommendation["type"] in ["loft_insulation", "room_roof_insulation", "flat_roof_insulation"]:
recommendation_record["roof_thermal_transmittance_ending"] = recommendation["new_u_value"]
# output["floor_thermal_transmittance_ending"] = recommendation["new_u_value"]
# We don't really see above average for this in the training data
output["floor_insulation_thickness_ending"] = "average"
# This is rarely ever populated in the training data
# output["floor_energy_eff_ending"] = "Good"
else:
if output["floor_thermal_transmittance_ending"] is None:
raise ValueError("We should not have a None value for the u value")
parts = recommendation["parts"]
if len(parts) != 1:
raise ValueError("More than one part for roof insulation - investiage me")
if output["floor_insulation_thickness_ending"] is None:
output["floor_insulation_thickness_ending"] = "none"
# This is based on the values we have in the training data
valid_numeric_values = [
12, 25, 50, 75, 100, 150, 200, 250, 270, 300, 350, 400
]
if recommendation["type"] in ["loft_insulation", "room_roof_insulation", "flat_roof_insulation"]:
output["roof_thermal_transmittance_ending"] = recommendation["new_u_value"]
proposed_depth = int(parts[0]["depth"])
if proposed_depth not in valid_numeric_values:
# Take the nearest value for scoring
proposed_depth = min(valid_numeric_values, key=lambda x: abs(x - proposed_depth))
parts = recommendation["parts"]
if len(parts) != 1:
raise ValueError("More than one part for roof insulation - investiage me")
recommendation_record["roof_insulation_thickness_ending"] = str(proposed_depth)
if recommendation["type"] == "loft_insulation":
if proposed_depth >= 270:
recommendation_record["roof_energy_eff_ending"] = "Very Good"
# This is based on the values we have in the training data
valid_numeric_values = [
12, 25, 50, 75, 100, 150, 200, 250, 270, 300, 350, 400
]
proposed_depth = int(parts[0]["depth"])
if proposed_depth not in valid_numeric_values:
# Take the nearest value for scoring
proposed_depth = min(valid_numeric_values, key=lambda x: abs(x - proposed_depth))
output["roof_insulation_thickness_ending"] = str(proposed_depth)
if recommendation["type"] == "loft_insulation":
if proposed_depth >= 270:
output["roof_energy_eff_ending"] = "Very Good"
else:
output["roof_energy_eff_ending"] = "Good"
else:
recommendation_record["roof_energy_eff_ending"] = "Good"
output["roof_energy_eff_ending"] = "Very Good"
else:
recommendation_record["roof_energy_eff_ending"] = "Very Good"
else:
# Fill missing roof u-values - this fill is not based on recommended upgrades
if recommendation_record["roof_thermal_transmittance_ending"] is None:
raise ValueError("We should not have a None value for the u value")
# Fill missing roof u-values - this fill is not based on recommended upgrades
if output["roof_thermal_transmittance_ending"] is None:
raise ValueError("We should not have a None value for the u value")
if recommendation_record["roof_insulation_thickness_ending"] is None:
recommendation_record["roof_insulation_thickness_ending"] = "none"
if output["roof_insulation_thickness_ending"] is None:
output["roof_insulation_thickness_ending"] = "none"
if recommendation["type"] == "mechanical_ventilation":
recommendation_record["mechanical_ventilation_ending"] = 'mechanical, extract only'
if recommendation["type"] == "sealing_open_fireplace":
output["number_open_fireplaces_ending"] = 0
if recommendation["type"] == "sealing_open_fireplace":
recommendation_record["number_open_fireplaces_ending"] = 0
if recommendation["type"] == "low_energy_lighting":
output["low_energy_lighting_ending"] = 100
output["lighting_energy_eff_starting"] = "Very Good"
if recommendation["type"] == "low_energy_lighting":
recommendation_record["low_energy_lighting_ending"] = 100
recommendation_record["lighting_energy_eff_starting"] = "Very Good"
if recommendation["type"] == "windows_glazing":
output["multi_glaze_proportion_ending"] = 100
output["windows_energy_eff_ending"] = "Average"
if recommendation["type"] == "windows_glazing":
recommendation_record["multi_glaze_proportion_ending"] = 100
recommendation_record["windows_energy_eff_ending"] = "Average"
is_secondary_glazing = recommendation["is_secondary_glazing"]
is_secondary_glazing = recommendation["is_secondary_glazing"]
if output["glazing_type_ending"] == "multiple":
pass
elif output["glazing_type_ending"] == "single":
output["glazing_type_ending"] = "secondary" if is_secondary_glazing else "double"
elif output["glazing_type_ending"] == "double":
output["glazing_type_ending"] = "multiple" if is_secondary_glazing else "double"
elif output["glazing_type_ending"] == "secondary":
output["glazing_type_ending"] = "secondary" if is_secondary_glazing else "multiple"
elif output["glazing_type_ending"] in ["triple", "high performance"]:
output["glazing_type_ending"] = "multiple"
else:
raise ValueError("Invalid glazing type - implement me")
if recommendation_record["glazing_type_ending"] == "multiple":
pass
elif recommendation_record["glazing_type_ending"] == "single":
recommendation_record["glazing_type_ending"] = "secondary" if is_secondary_glazing else "double"
elif recommendation_record["glazing_type_ending"] == "double":
recommendation_record["glazing_type_ending"] = "multiple" if is_secondary_glazing else "double"
elif recommendation_record["glazing_type_ending"] == "secondary":
recommendation_record["glazing_type_ending"] = "secondary" if is_secondary_glazing else "multiple"
elif recommendation_record["glazing_type_ending"] in ["triple", "high performance"]:
recommendation_record["glazing_type_ending"] = "multiple"
else:
raise ValueError("Invalid glazing type - implement me")
if is_secondary_glazing:
output["glazed_type_ending"] = "secondary glazing"
else:
output["glazed_type_ending"] = "double glazing installed during or after 2002 "
if is_secondary_glazing:
recommendation_record["glazed_type_ending"] = "secondary glazing"
else:
recommendation_record["glazed_type_ending"] = "double glazing installed during or after 2002 "
if recommendation["type"] == "solar_pv":
output["photo_supply_ending"] = recommendation["photo_supply"]
if recommendation["type"] == "solar_pv":
recommendation_record["photo_supply_ending"] = recommendation["photo_supply"]
if recommendation["type"] not in [
"mechanical_ventilation", "sealing_open_fireplace", "low_energy_lighting",
"internal_wall_insulation", "external_wall_insulation", "cavity_wall_insulation",
"loft_insulation", "room_roof_insulation", "flat_roof_insulation",
"solid_floor_insulation", "suspended_floor_insulation", "exposed_floor_insulation",
"windows_glazing", "solar_pv"
]:
raise NotImplementedError("Implement me")
if recommendation["type"] not in [
"mechanical_ventilation", "sealing_open_fireplace", "low_energy_lighting",
"internal_wall_insulation", "external_wall_insulation", "cavity_wall_insulation",
"loft_insulation", "room_roof_insulation", "flat_roof_insulation",
"solid_floor_insulation", "suspended_floor_insulation", "exposed_floor_insulation",
"windows_glazing", "solar_pv"
]:
raise NotImplementedError("Implement me")
output['id'] = "+".join([str(property_id), str(primary_recommendation_id)])
recommendation_record['id'] = "+".join([str(property_id), str(recommendation["recommendation_id"])])
return recommendation_record
return output
def get_components(self, cleaned, photo_supply_lookup, floor_area_decile_thresholds):
"""

View file

@ -136,22 +136,32 @@ async def trigger_plan(body: PlanTriggerRequest):
recommendations = {}
recommendations_scoring_data = []
representive_recommendations = {}
for p in input_properties:
# Property recommendations
p.get_components(cleaned, photo_supply_lookup, floor_area_decile_thresholds)
# TODO: For the private customer, we should probably NOT allow floor insulation, because it often requires
# decanting the tenant
recommender = Recommendations(property_instance=p, materials=materials)
property_recommendations = recommender.recommend()
property_recommendations, property_representative_recommendations = recommender.recommend()
if not property_recommendations:
continue
recommendations[p.id] = property_recommendations
representive_recommendations[p.id] = property_representative_recommendations
p.create_base_difference_epc_record(cleaned_lookup=cleaned)
p.adjust_difference_record_with_recommendations(property_recommendations)
p.adjust_difference_record_with_recommendations(
property_recommendations, property_representative_recommendations
)
p.recommendations_scoring_data[0]["id"]
p.recommendations_scoring_data[0]["walls_thermal_transmittance"]
p.recommendations_scoring_data[0]["walls_thermal_transmittance_ending"]
p.recommendations_scoring_data[0]["walls_thermal_transmittance_ending"]
recommendations_scoring_data.extend(p.recommendations_scoring_data)

View file

@ -118,6 +118,7 @@ class FloorRecommendations(Definitions):
if self.property.floor["is_suspended"]:
# Given the U-value, we recommend underfloor insulation
self.recommend_floor_insulation(
phase=phase,
u_value=u_value,
insulation_materials=self.suspended_floor_insulation_materials,
non_insulation_materials=self.suspended_floor_non_insulation_materials

View file

@ -107,7 +107,52 @@ class Recommendations:
# We insert temporary ids into the recommendations which is important for the optimiser later
property_recommendations = self.insert_temp_recommendation_id(property_recommendations)
return property_recommendations
# We also need to create the representative recommendations for each recommendation type
property_representative_recommendations = self.create_representative_recommendations(property_recommendations)
return property_recommendations, property_representative_recommendations
@staticmethod
def create_representative_recommendations(property_recommendations):
"""
This method will create a representative recommendation for each recommendation type
In order to create a representative recommendation, we choose the recommendation that has:
1) Where a U-value is available, has the best U-value to cost ratio
2) Where SAP points are available, has the best SAP points to cost ratio
We don't include mechanical ventilation in the representative recommendations, since we don't attribute a
SAP impact to this recommendation
:return:
"""
property_representative_recommendations = []
for recommendations_by_type in property_recommendations:
if recommendations_by_type[0].get("type") == "mechanical_ventilation":
continue
has_u_value = recommendations_by_type[0].get("new_u_value") is not None
has_sap_points = recommendations_by_type[0].get("sap_points") is not None
if has_u_value:
# We sort by the cost per U-value improvement - the lower the better
recommendations_by_type.sort(
key=lambda x: x["total"] / x["starting_u_value"] - x["new_u_value"]
)
elif not has_u_value and has_sap_points:
# Sort the options by the cost per SAP point improvement - the lower the better
recommendations_by_type.sort(
key=lambda x: x["total"] / x["sap_points"]
)
else:
# Sort the options by cost - the lower the better
recommendations_by_type.sort(
key=lambda x: x["total"]
)
property_representative_recommendations.append(recommendations_by_type[0])
return property_representative_recommendations
@staticmethod
def insert_temp_recommendation_id(property_recommendations):