From 5078368faf2e38c7cc3700bd590f2e82ae2d7280 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Mon, 7 Oct 2024 11:42:03 +0100 Subject: [PATCH 1/6] fixed generate_scenarios_data --- backend/Property.py | 19 ++++---- etl/epc/generate_scenarios_data.py | 53 ++++++++++------------- recommendations/SolarPvRecommendations.py | 4 +- 3 files changed, 33 insertions(+), 43 deletions(-) diff --git a/backend/Property.py b/backend/Property.py index be5479c5..ab8930c5 100644 --- a/backend/Property.py +++ b/backend/Property.py @@ -566,8 +566,6 @@ class Property: if not self.data: raise ValueError("Property does not contain data") - self.set_basic_property_dimensions() - for description, attribute in cleaned.items(): if self.data[description] in self.DATA_ANOMALY_MATCHES: @@ -615,6 +613,7 @@ class Property: setattr(self, self.ATTRIBUTE_MAP[description], attributes[0]) + self.set_basic_property_dimensions() self.set_wall_type() self.set_floor_type() self.set_floor_level() @@ -629,15 +628,6 @@ class Property: """ self.solar_panel_configuration = solar_panel_configuration - if not self.roof["is_flat"]: - default_roof_area = estimate_pitched_roof_area( - floor_area=self.insulation_floor_area, - ) - else: - default_roof_area = self.insulation_floor_area - - self.roof_area = default_roof_area - def set_current_energy(self, kwh_client, kwh_predictions): """ Given what we know about the property now, estimates the current energy consumption using the UCL paper @@ -972,6 +962,13 @@ class Property: self.floor_area / self.number_of_floors ) + if not self.roof["is_flat"]: + self.roof_area = estimate_pitched_roof_area( + floor_area=self.insulation_floor_area, + ) + else: + self.roof_area = self.insulation_floor_area + def set_floor_level(self): self.floor_level = ( FLOOR_LEVEL_MAP[self.data["floor-level"]] diff --git a/etl/epc/generate_scenarios_data.py b/etl/epc/generate_scenarios_data.py index df1f9452..3497225c 100644 --- a/etl/epc/generate_scenarios_data.py +++ b/etl/epc/generate_scenarios_data.py @@ -3,25 +3,24 @@ import itertools import pandas as pd from etl.epc.Record import EPCRecord +from etl.bill_savings.KwhData import KwhData from backend.SearchEpc import SearchEpc from sqlalchemy.orm import sessionmaker -from backend.app.config import get_settings +from backend.app.config import get_settings, get_prediction_buckets from backend.app.db.connection import db_engine from backend.app.db.functions.materials_functions import get_materials +from backend.ml_models.api import ModelApi from backend.app.plan.utils import get_cleaned from backend.Property import Property -from etl.solar.SolarPhotoSupply import SolarPhotoSupply from recommendations.Recommendations import Recommendations from utils.logger import setup_logger from utils.s3 import read_dataframe_from_s3_parquet, save_dataframe_to_s3_parquet -from datetime import datetime - now = datetime.now().strftime("%d-%m-%Y-%H-%M-%S") logger = setup_logger() @@ -41,21 +40,16 @@ cleaning_data = read_dataframe_from_s3_parquet( materials = get_materials(session) cleaned = get_cleaned() -# TODO: THIS IS A TEMPORARY FIX -new_walls_description_mapping = pd.DataFrame(cleaned["walls-description"]) -new_walls_description_mapping.loc[ - ~new_walls_description_mapping["thermal_transmittance_unit"].isnull(), - "thermal_transmittance_unit", -] = "w/m-¦k" - -cleaned["walls-description"] = new_walls_description_mapping.to_dict(orient="records") - uprn_filenames = read_dataframe_from_s3_parquet( bucket_name=get_settings().DATA_BUCKET, file_key="spatial/filename_meta.parquet" ) -photo_supply_lookup, floor_area_decile_thresholds = SolarPhotoSupply.load( - bucket=get_settings().DATA_BUCKET + +kwh_client = KwhData(bucket="retrofit-data-dev", read_consumption_data=False) +kwh_client.retail_price_comparison = pd.DataFrame( + [{"Date": datetime.today().strftime("%Y-%m-%d"), + 'Average standard variable tariff (Large legacy suppliers)': 1}] ) +kwh_client.retail_price_comparison["Date"] = pd.to_datetime(kwh_client.retail_price_comparison["Date"]) scenario_properties = [ { @@ -132,7 +126,6 @@ scenario_properties = [ }, ] - recommendations_scoring_data = [] for scenario_property in scenario_properties: @@ -173,7 +166,17 @@ for scenario_property in scenario_properties: ) p.get_spatial_data(uprn_filenames) - p.get_components(cleaned, photo_supply_lookup, floor_area_decile_thresholds) + + kwh_predictions = { + "heating_kwh_predictions": pd.DataFrame([{"id": p.uprn, "predictions": 12000}]), + "hotwater_kwh_predictions": pd.DataFrame([{"id": p.uprn, "predictions": 3000}]), + } + p.set_features(cleaned, kwh_client, kwh_predictions) + p.solar_panel_configuration = { + "panel_performance": pd.DataFrame( + [{"panneled_roof_area": 34, "n_panels": 10, "array_wattage": 4000, "initial_ac_kwh_per_year": 3800}] + ) + } recommender = Recommendations(property_instance=p, materials=materials) property_recommendations = recommender.recommend() @@ -277,20 +280,12 @@ recommendations_scoring_data.insert(0, "impact", impact_col) id_col = recommendations_scoring_data.pop("id") recommendations_scoring_data.insert(0, "id", id_col) -from backend.ml_models.api import ModelApi - -model_api = ModelApi(portfolio_id="generate-scenarios-data", timestamp=created_at) - -all_predictions = model_api.predict_all( - df=recommendations_scoring_data, - bucket=get_settings().DATA_BUCKET, - prediction_buckets={ - "sap_change_predictions": get_settings().SAP_PREDICTIONS_BUCKET, - "heat_demand_predictions": get_settings().HEAT_PREDICTIONS_BUCKET, - "carbon_change_predictions": get_settings().CARBON_PREDICTIONS_BUCKET, - }, +model_api = ModelApi( + portfolio_id="generate-scenarios-data", timestamp=created_at, prediction_buckets=get_prediction_buckets() ) +all_predictions = model_api.predict_all(df=recommendations_scoring_data, bucket=get_settings().DATA_BUCKET) + save_dataframe_to_s3_parquet( recommendations_scoring_data, "retrofit-data-dev", diff --git a/recommendations/SolarPvRecommendations.py b/recommendations/SolarPvRecommendations.py index 08f077d2..1eb584ca 100644 --- a/recommendations/SolarPvRecommendations.py +++ b/recommendations/SolarPvRecommendations.py @@ -175,9 +175,7 @@ class SolarPvRecommendations: if self.property.roof["is_flat"]: roof_area = self.property.insulation_floor_area else: - roof_area = estimate_pitched_roof_area( - floor_area=self.property.insulation_floor_area, floor_height=self.property.data["floor-height"] - ) + roof_area = estimate_pitched_roof_area(floor_area=self.property.insulation_floor_area, ) solar_configurations = pd.DataFrame( [ { From 98a91a5a35944ab9bb1a29e2528dde80d33665f1 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Tue, 8 Oct 2024 11:14:13 +0100 Subject: [PATCH 2/6] adding simulation with default u-values to see impact on generate_scenarios_data --- etl/epc/generate_scenarios_data.py | 11 +++++++- recommendations/Recommendations.py | 5 +++- recommendations/WallRecommendations.py | 35 ++++++++++++++++++++------ 3 files changed, 41 insertions(+), 10 deletions(-) diff --git a/etl/epc/generate_scenarios_data.py b/etl/epc/generate_scenarios_data.py index 3497225c..15661d99 100644 --- a/etl/epc/generate_scenarios_data.py +++ b/etl/epc/generate_scenarios_data.py @@ -178,7 +178,7 @@ for scenario_property in scenario_properties: ) } - recommender = Recommendations(property_instance=p, materials=materials) + recommender = Recommendations(property_instance=p, materials=materials, default_u_values=True) property_recommendations = recommender.recommend() wall_recommendations = recommender.wall_recomender.recommendations @@ -286,6 +286,15 @@ model_api = ModelApi( all_predictions = model_api.predict_all(df=recommendations_scoring_data, bucket=get_settings().DATA_BUCKET) +sap_impact = pd.concat( + [ + all_predictions["sap_change_predictions"], + recommendations_scoring_data[["uprn", "sap_starting"]], + ], + axis=1 +) +sap_impact["predicted_impact"] = sap_impact["predictions"] - sap_impact["sap_starting"] + save_dataframe_to_s3_parquet( recommendations_scoring_data, "retrofit-data-dev", diff --git a/recommendations/Recommendations.py b/recommendations/Recommendations.py index 1b152238..ac7635c5 100644 --- a/recommendations/Recommendations.py +++ b/recommendations/Recommendations.py @@ -34,6 +34,7 @@ class Recommendations: materials: List, exclusions: List[str] = None, inclusions: List[str] = None, + default_u_values: bool = False, ): """ :param property_instance: Instance of the Property class, for the home associated to property_id @@ -42,12 +43,14 @@ class Recommendations: None, meaning no exclusions to be applied :param inclusions: List of specific measures of measure types to include. Defaulted to None, meaning all measures are included + :param default_u_values: Boolean, if True, the recommendations will use the default u-values for the property """ self.property_instance = property_instance self.materials = materials self.exclusions = exclusions if exclusions else [] self.inclusions = inclusions if inclusions else [] + self.default_u_values = default_u_values self.all_specific_measures = SPECIFIC_MEASURES self.all_non_invase_measures = NON_INVASIVE_SPECIFIC_MEASURES @@ -120,7 +123,7 @@ class Recommendations: non_invasive_recommendation_types = [r["type"] for r in self.property_instance.non_invasive_recommendations] # Building Fabric - self.wall_recomender.recommend(phase=phase, measures=measures) + self.wall_recomender.recommend(phase=phase, measures=measures, default_u_values=self.default_u_values) if self.wall_recomender.recommendations: property_recommendations.append(self.wall_recomender.recommendations) phase += 1 diff --git a/recommendations/WallRecommendations.py b/recommendations/WallRecommendations.py index dd5e861c..28e35584 100644 --- a/recommendations/WallRecommendations.py +++ b/recommendations/WallRecommendations.py @@ -179,7 +179,7 @@ class WallRecommendations(Definitions): return ewi_recommendations - def recommend(self, phase=0, measures=None): + def recommend(self, phase=0, measures=None, default_u_values=False): # if building built after 1990 + we're able to identify U-value + # U-value less than 0.18 and if in or close to a conversation area, # recommend internal wall insulation as a possible measure @@ -255,19 +255,19 @@ class WallRecommendations(Definitions): if (is_cavity_wall and "cavity_wall_insulation" in measures) or "cavity_extract_and_refill" in measures: if u_value >= self.BUILDING_REGULATIONS_PART_L_MAX_U_VALUE: # Test filling cavity - self.find_cavity_insulation(u_value, insulation_thickness, phase, measures) + self.find_cavity_insulation(u_value, insulation_thickness, phase, measures, default_u_values) return # Remaining wall types are treated with IWI or EWI if (u_value >= self.BUILDING_REGULATIONS_PART_L_MAX_U_VALUE) and self.is_suitable_for_solid_insulation(): - self.find_insulation(u_value, phase, measures=measures) + self.find_insulation(u_value, phase, measures=measures, default_u_values=default_u_values) return # If the u-value is within regulations, we don't do anything return - def find_cavity_insulation(self, u_value, insulation_thickness, phase, measures): + def find_cavity_insulation(self, u_value, insulation_thickness, phase, measures, default_u_values): """ This method tests different materials to fill the cavity wall, determining which material will give us the best U-value. @@ -289,6 +289,7 @@ class WallRecommendations(Definitions): filled cavity wall :param phase: The phase of the recommendation :param measures: The measures we're considering + :param default_u_values: If we should use default u values """ insulation_materials = pd.DataFrame(self.cavity_wall_insulation_materials) @@ -344,7 +345,15 @@ class WallRecommendations(Definitions): description = self._make_description(material) # updated the new u-value with the best possible our installers have - new_u_value = max(0.31, new_u_value) + if default_u_values: + new_u_value = get_wall_u_value( + clean_description="Cavity wall, filled cavity", + age_band="G", + is_granite_or_whinstone=self.property.walls["is_granite_or_whinstone"], + is_sandstone_or_limestone=self.property.walls["is_sandstone_or_limestone"], + ) + else: + new_u_value = max(0.31, new_u_value) wall_ending_config = WallAttributes("Cavity wall, filled cavity").process() @@ -359,7 +368,7 @@ class WallRecommendations(Definitions): simulation_config = { **simulation_config, **walls_simulation_config, - "walls_thermal_transmittance_ending": new_u_value, + "walls_thermal_transmittance_ending": new_u_value if not default_u_values else 0.7, } recommendations.append( @@ -439,7 +448,7 @@ class WallRecommendations(Definitions): return simulation_config - def _find_insulation(self, u_value, insulation_materials, phase): + def _find_insulation(self, u_value, insulation_materials, phase, default_u_values): lowest_selected_u_value = None recommendations = [] @@ -534,6 +543,14 @@ class WallRecommendations(Definitions): "walls_thermal_transmittance_ending": new_u_value } + if default_u_values: + new_u_value = get_wall_u_value( + clean_description=new_description, + age_band=self.property.age_band, + is_granite_or_whinstone=self.property.walls["is_granite_or_whinstone"], + is_sandstone_or_limestone=self.property.walls["is_sandstone_or_limestone"], + ) + recommendations.append( { "phase": phase, @@ -564,7 +581,7 @@ class WallRecommendations(Definitions): return recommendations - def find_insulation(self, u_value, phase, measures): + def find_insulation(self, u_value, phase, measures, default_u_values): """ This function contains the logic for finding potential insulation measures for a property, depending on the parts available and whether the property can have external wall insulation installed @@ -584,6 +601,7 @@ class WallRecommendations(Definitions): self.external_wall_insulation_materials ), phase=phase, + default_u_values=default_u_values ) iwi_recommendations = [] @@ -592,6 +610,7 @@ class WallRecommendations(Definitions): u_value=u_value, insulation_materials=pd.DataFrame(self.internal_wall_insulation_materials), phase=phase, + default_u_values=default_u_values ) self.recommendations += ewi_recommendations + iwi_recommendations From 35911c0db7b511969fd3691eb4fc44da54eef20e Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Tue, 8 Oct 2024 14:17:24 +0100 Subject: [PATCH 3/6] added test cases for roof u-values --- recommendations/Recommendations.py | 2 +- recommendations/RoofRecommendations.py | 40 +++++++++-- recommendations/WallRecommendations.py | 1 + recommendations/rdsap_tables.py | 9 +++ recommendations/recommendation_utils.py | 67 ++++++++++++++++--- .../tests/test_recommendation_utils.py | 21 ++++++ 6 files changed, 124 insertions(+), 16 deletions(-) diff --git a/recommendations/Recommendations.py b/recommendations/Recommendations.py index ac7635c5..684b0915 100644 --- a/recommendations/Recommendations.py +++ b/recommendations/Recommendations.py @@ -128,7 +128,7 @@ class Recommendations: property_recommendations.append(self.wall_recomender.recommendations) phase += 1 - self.roof_recommender.recommend(phase=phase, measures=measures) + self.roof_recommender.recommend(phase=phase, measures=measures, default_u_values=self.default_u_values) if self.roof_recommender.recommendations: property_recommendations.append(self.roof_recommender.recommendations) phase += 1 diff --git a/recommendations/RoofRecommendations.py b/recommendations/RoofRecommendations.py index 3e266cee..e8a43db0 100644 --- a/recommendations/RoofRecommendations.py +++ b/recommendations/RoofRecommendations.py @@ -108,7 +108,7 @@ class RoofRecommendations: return full_insulated_room_roof or room_roof_insulated_at_rafters - def recommend(self, phase, measures=None): + def recommend(self, phase, measures=None, default_u_values=False): if self.property.roof["has_dwelling_above"]: return @@ -171,7 +171,8 @@ class RoofRecommendations: insulation_thickness=self.insulation_thickness, phase=phase, is_flat=False, - is_pitched=True + is_pitched=True, + default_u_values=default_u_values ) return @@ -184,7 +185,8 @@ class RoofRecommendations: insulation_thickness=0, phase=phase, is_flat=True, - is_pitched=False + is_pitched=False, + default_u_values=default_u_values ) return @@ -193,7 +195,7 @@ class RoofRecommendations: if self.property.roof["is_roof_room"] and ("room_roof_insulation" in measures) or ( "room_roof_insulation" in [x["type"] for x in non_invasive_recommendations] ): - self.recommend_room_roof_insulation(u_value, phase) + self.recommend_room_roof_insulation(u_value, phase, default_u_values) return raise NotImplementedError("Implement me") @@ -215,7 +217,7 @@ class RoofRecommendations: raise ValueError("Invalid material type") def recommend_roof_insulation( - self, u_value, insulation_thickness, phase, is_pitched, is_flat + self, u_value, insulation_thickness, phase, is_pitched, is_flat, default_u_values ): """ @@ -241,6 +243,7 @@ class RoofRecommendations: :param phase: Phase of the recommendation :param is_pitched: Is the roof pitched :param is_flat: Is the roof flat + :param default_u_values: Use default u-values :return: """ @@ -266,7 +269,6 @@ class RoofRecommendations: recommendations = [] for _, insulation_material_group in insulation_materials.groupby("description"): for _, material in insulation_material_group.iterrows(): - # We make sure we hit a depth of 270mm. We should factor in any existing insulation if the # loft is already partially insulated. # Note: This requirement is only for loft insulation @@ -340,9 +342,35 @@ class RoofRecommendations: new_description = f"Pitched, {int(proposed_depth)}mm loft insulation" + if default_u_values: + # We update the u-value with the default if we're using default u-values + new_u_value = get_roof_u_value( + insulation_thickness=str(int(new_thickness)), + has_dwelling_above=self.property.roof["has_dwelling_above"], + is_loft=self.property.roof["is_loft"], + is_roof_room=self.property.roof["is_roof_room"], + is_thatched=self.property.roof["is_thatched"], + age_band=self.property.age_band, + is_flat=self.property.roof["is_flat"], + is_pitched=self.property.roof["is_pitched"], + is_at_rafters=self.property.roof["is_at_rafters"], + ) + elif material["type"] == "flat_roof_insulation": new_description = "Flat, insulated" new_efficiency = "Good" + if default_u_values: + new_u_value = get_roof_u_value( + insulation_thickness="100", + has_dwelling_above=self.property.roof["has_dwelling_above"], + is_loft=self.property.roof["is_loft"], + is_roof_room=self.property.roof["is_roof_room"], + is_thatched=self.property.roof["is_thatched"], + age_band=self.property.age_band, + is_flat=self.property.roof["is_flat"], + is_pitched=self.property.roof["is_pitched"], + is_at_rafters=self.property.roof["is_at_rafters"], + ) else: raise ValueError("Invalid material type") diff --git a/recommendations/WallRecommendations.py b/recommendations/WallRecommendations.py index 28e35584..358547dc 100644 --- a/recommendations/WallRecommendations.py +++ b/recommendations/WallRecommendations.py @@ -544,6 +544,7 @@ class WallRecommendations(Definitions): } if default_u_values: + # If we're using default U-values, we overwrite new_u_value new_u_value = get_wall_u_value( clean_description=new_description, age_band=self.property.age_band, diff --git a/recommendations/rdsap_tables.py b/recommendations/rdsap_tables.py index 5110764b..16c7d26e 100644 --- a/recommendations/rdsap_tables.py +++ b/recommendations/rdsap_tables.py @@ -340,6 +340,7 @@ s9_list = [ s10_list = [ { "Age_band": "A, B, C, D", + "Insulation_Thickness": "none", "Pitched_slates_or_tiles_insulation_between_joists_or_unknown": 2.3, "Pitched_slates_or_tiles_insulation_at_rafters": 2.3, "Flat_roof": 2.3, @@ -350,6 +351,7 @@ s10_list = [ }, { "Age_band": "E", + "Insulation_Thickness": 12, "Pitched_slates_or_tiles_insulation_between_joists_or_unknown": 1.5, "Pitched_slates_or_tiles_insulation_at_rafters": 1.5, "Flat_roof": 1.5, @@ -360,6 +362,7 @@ s10_list = [ }, { "Age_band": "F", + "Insulation_Thickness": 50, "Pitched_slates_or_tiles_insulation_between_joists_or_unknown": 0.68, "Pitched_slates_or_tiles_insulation_at_rafters": 0.68, "Flat_roof": 0.68, @@ -370,6 +373,7 @@ s10_list = [ }, { "Age_band": "G", + "Insulation_Thickness": 100, "Pitched_slates_or_tiles_insulation_between_joists_or_unknown": 0.40, "Pitched_slates_or_tiles_insulation_at_rafters": 0.40, "Flat_roof": 0.40, @@ -380,6 +384,7 @@ s10_list = [ }, { "Age_band": "H", + "Insulation_Thickness": 150, "Pitched_slates_or_tiles_insulation_between_joists_or_unknown": 0.30, "Pitched_slates_or_tiles_insulation_at_rafters": 0.35, "Flat_roof": 0.35, @@ -390,6 +395,7 @@ s10_list = [ }, { "Age_band": "I", + "Insulation_Thickness": 150, "Pitched_slates_or_tiles_insulation_between_joists_or_unknown": 0.26, "Pitched_slates_or_tiles_insulation_at_rafters": 0.35, "Flat_roof": 0.35, @@ -400,6 +406,7 @@ s10_list = [ }, { "Age_band": "J", + "Insulation_Thickness": 270, "Pitched_slates_or_tiles_insulation_between_joists_or_unknown": 0.16, "Pitched_slates_or_tiles_insulation_at_rafters": 0.20, "Flat_roof": 0.25, @@ -410,6 +417,7 @@ s10_list = [ }, { "Age_band": "K", + "Insulation_Thickness": 270, "Pitched_slates_or_tiles_insulation_between_joists_or_unknown": 0.16, "Pitched_slates_or_tiles_insulation_at_rafters": 0.20, "Flat_roof": 0.25, @@ -420,6 +428,7 @@ s10_list = [ }, { "Age_band": "L", + "Insulation_Thickness": 270, "Pitched_slates_or_tiles_insulation_between_joists_or_unknown": 0.16, "Pitched_slates_or_tiles_insulation_at_rafters": 0.18, "Flat_roof": 0.18, diff --git a/recommendations/recommendation_utils.py b/recommendations/recommendation_utils.py index dcdd9c06..8ddfe1ef 100644 --- a/recommendations/recommendation_utils.py +++ b/recommendations/recommendation_utils.py @@ -239,12 +239,7 @@ def get_wall_u_value( return float(mapped_value) -def get_u_value_from_s9( - thickness, s9, is_loft, is_roof_room, is_thatched, is_at_rafters -): - """Get the U-value from table S9 based on the insulation thickness.""" - - # If the roof as pitched & insulated at the rafters, it's a room roof +def extract_thickness(thickness, is_roof_room, is_at_rafters, is_loft, is_flat): if is_roof_room or is_at_rafters: # TODO: We get None instead of a string none, this should be fixed if thickness is None: @@ -258,19 +253,40 @@ def get_u_value_from_s9( } thickness = thickness_map[thickness] + if is_flat: + try: + thickness = int(thickness) + return thickness + except ValueError: + # If thickness is not a valid number (could be a string or None), return None + return None + if thickness in ["below average", "average", "above average", "none", None] or ( not is_loft and not is_roof_room and not is_at_rafters ): return None elif thickness.endswith("+"): thickness = int(thickness[:-1]) + return thickness else: try: thickness = int(thickness) + return thickness except ValueError: # If thickness is not a valid number (could be a string or None), return None return None + +def get_u_value_from_s9( + thickness, s9, is_loft, is_roof_room, is_thatched, is_at_rafters +): + """Get the U-value from table S9 based on the insulation thickness.""" + + if thickness in ["below average", "average", "above average", "none", None, "0", 0] or ( + not is_loft and not is_roof_room and not is_at_rafters + ): + return None + # Determine the column to refer based on the roof type column = ( "Thatched_roof_U_value_W_m2K" @@ -323,6 +339,14 @@ def get_roof_u_value( if has_dwelling_above: return 0.0 + thickness = extract_thickness( + thickness=insulation_thickness, + is_roof_room=is_roof_room, + is_at_rafters=is_at_rafters, + is_loft=is_loft, + is_flat=is_flat + ) + # Step 1: Try to get the U-value from table S9 based on the insulation thickness # The conditions for using table S9 are: # - The insulation thickness is known @@ -330,7 +354,7 @@ def get_roof_u_value( # The criteria for using this table is predominately defined by insulation around joists which is predominately # a feature of lofts and roof rooms u_value = get_u_value_from_s9( - thickness=insulation_thickness, + thickness=thickness, s9=s9, is_loft=is_loft, is_roof_room=is_roof_room, @@ -363,9 +387,34 @@ def get_roof_u_value( column = "Pitched_slates_or_tiles_insulation_between_joists_or_unknown" # Get the U-value from table S10 based on the age band and the determined column - u_value = s10.loc[s10["Age_band"].str.contains(age_band), column].values[0] + if is_flat and thickness is not None: + u_value = s10.loc[ + (s10["Insulation_Thickness"] == thickness) | + s10["Age_band"].str.contains(age_band), + column + ].values.min() + else: + u_value = s10.loc[s10["Age_band"].str.contains(age_band), column].values[0] - return float(u_value) + u_value = float(u_value) + + # As per the documentation here: https://bregroup.com/documents/d/bre-group/rdsap_2012_9-94-20-09-2019 + # Table s.10 + # "The value from the table applies for unknown and as built. If the roof is known to have more insulation than + # would normally be expected for the age band, either observed or on the basis of documentary evidence, use the + # lower of the value in the table and: + # 50 mm insulation 0.68 + # 100 mm insulation: 0.40 + # 150 mm or more insulation: 0.30" + if thickness is not None: + if thickness == 50: + u_value = min(u_value, 0.68) + if thickness == 100: + u_value = min(u_value, 0.40) + if thickness >= 150: + u_value = min(u_value, 0.30) + + return u_value def estimate_number_of_floors(property_type): diff --git a/recommendations/tests/test_recommendation_utils.py b/recommendations/tests/test_recommendation_utils.py index 24ea6482..b445a798 100644 --- a/recommendations/tests/test_recommendation_utils.py +++ b/recommendations/tests/test_recommendation_utils.py @@ -6,6 +6,7 @@ from recommendations import recommendation_utils from datatypes.enums import QuantityUnits from recommendations.tests.test_data.wall_uvalue_test_cases import wall_uvalue_test_cases from recommendations.tests.test_data.floor_uvalue_test_cases import floor_uvalue_test_cases +from recommendations.tests.test_data.roof_uvalue_test_cases import roof_uvalue_test_cases class TestRecommendationUtils: @@ -222,6 +223,26 @@ class TestRecommendationUtils: u_value = recommendation_utils.get_roof_u_value(**inputs) assert u_value == 0.0, f"Expected 0.0, but got {u_value}" + @pytest.mark.parametrize( + "test_case", + roof_uvalue_test_cases + ) + def test_roof_uvalues(self, test_case): + expected_uvalue = test_case["uvalue"] + inputs = test_case.copy() + del inputs["uvalue"] + # insulation_thickness = inputs["insulation_thickness"] + # has_dwelling_above = inputs["has_dwelling_above"] + # is_loft = inputs["is_loft"] + # is_roof_room = inputs["is_roof_room"] + # is_thatched = inputs["is_thatched"] + # age_band = inputs["age_band"] + # is_flat = inputs["is_flat"] + # is_pitched = inputs["is_pitched"] + # is_at_rafters = inputs["is_at_rafters"] + uvalue = recommendation_utils.get_roof_u_value(**inputs) + assert expected_uvalue == uvalue, f"Expected u value {expected_uvalue}, recieved {uvalue}" + @pytest.mark.parametrize( "test_case", wall_uvalue_test_cases From e1593445ac8065cb82e1fbb9daa2deb18807ef98 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Tue, 8 Oct 2024 14:18:45 +0100 Subject: [PATCH 4/6] removing temp code --- recommendations/tests/test_recommendation_utils.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/recommendations/tests/test_recommendation_utils.py b/recommendations/tests/test_recommendation_utils.py index b445a798..38322c41 100644 --- a/recommendations/tests/test_recommendation_utils.py +++ b/recommendations/tests/test_recommendation_utils.py @@ -231,15 +231,6 @@ class TestRecommendationUtils: expected_uvalue = test_case["uvalue"] inputs = test_case.copy() del inputs["uvalue"] - # insulation_thickness = inputs["insulation_thickness"] - # has_dwelling_above = inputs["has_dwelling_above"] - # is_loft = inputs["is_loft"] - # is_roof_room = inputs["is_roof_room"] - # is_thatched = inputs["is_thatched"] - # age_band = inputs["age_band"] - # is_flat = inputs["is_flat"] - # is_pitched = inputs["is_pitched"] - # is_at_rafters = inputs["is_at_rafters"] uvalue = recommendation_utils.get_roof_u_value(**inputs) assert expected_uvalue == uvalue, f"Expected u value {expected_uvalue}, recieved {uvalue}" From ab05082829bd9e92b852d30752840a71c23ab1e2 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Tue, 8 Oct 2024 15:07:04 +0100 Subject: [PATCH 5/6] updated scenario data --- etl/epc/generate_scenarios_data.py | 18 +++++++++--------- recommendations/RoofRecommendations.py | 6 ++++-- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/etl/epc/generate_scenarios_data.py b/etl/epc/generate_scenarios_data.py index 15661d99..d4833baa 100644 --- a/etl/epc/generate_scenarios_data.py +++ b/etl/epc/generate_scenarios_data.py @@ -79,8 +79,8 @@ scenario_properties = [ "measures": [ [ ["cavity_wall_insulation", "loft_insulation"], - "15", - {"walls_insulation_thickness_ending": "average"}, + "11", + {}, [0, 1], ], ], @@ -92,8 +92,8 @@ scenario_properties = [ "measures": [ [ ["cavity_wall_insulation", "loft_insulation"], - "15", - {"walls_insulation_thickness_ending": "average"}, + "10", + {}, [0, 1], ], ], @@ -105,8 +105,8 @@ scenario_properties = [ "measures": [ [ ["cavity_wall_insulation", "loft_insulation"], - "15", - {"walls_insulation_thickness_ending": "average"}, + "11", + {}, [0, 1], ], ], @@ -118,8 +118,8 @@ scenario_properties = [ "measures": [ [ ["cavity_wall_insulation", "loft_insulation"], - "15", - {"walls_insulation_thickness_ending": "average"}, + "10", + {}, [0, 1], ], ], @@ -289,7 +289,7 @@ all_predictions = model_api.predict_all(df=recommendations_scoring_data, bucket= sap_impact = pd.concat( [ all_predictions["sap_change_predictions"], - recommendations_scoring_data[["uprn", "sap_starting"]], + recommendations_scoring_data[["uprn", "sap_starting", "impact"]], ], axis=1 ) diff --git a/recommendations/RoofRecommendations.py b/recommendations/RoofRecommendations.py index e8a43db0..c86c5b30 100644 --- a/recommendations/RoofRecommendations.py +++ b/recommendations/RoofRecommendations.py @@ -307,7 +307,9 @@ class RoofRecommendations: cost_result = override_costs(cost_result) if material["type"] == "loft_insulation": - new_thickness = insulation_thickness + material["depth"] + # We take the new thickness as just the thickness of the insulation, to be conservative + # and assume that any existing insulation will be replaced + new_thickness = material["depth"] # This is based on the values we have in the training data valid_numeric_values = [ @@ -332,7 +334,7 @@ class RoofRecommendations: valid_numeric_values, key=lambda x: abs(x - proposed_depth) ) - if proposed_depth >= 270: + if proposed_depth >= 300: new_efficiency = "Very Good" else: if self.property.data["walls-energy-eff"] not in ["Good", "Very Good"]: From 87879d54336316b7876017c16e1a7476e255a5b8 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Tue, 8 Oct 2024 15:26:15 +0100 Subject: [PATCH 6/6] minor --- etl/epc/generate_scenarios_data.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/etl/epc/generate_scenarios_data.py b/etl/epc/generate_scenarios_data.py index d4833baa..b61618b1 100644 --- a/etl/epc/generate_scenarios_data.py +++ b/etl/epc/generate_scenarios_data.py @@ -60,13 +60,13 @@ scenario_properties = [ [ ["internal_wall_insulation"], "11", - {"walls_insulation_thickness_ending": "average"}, + {}, [0], ], [ ["external_wall_insulation"], "10", - {"walls_insulation_thickness_ending": "average"}, + {}, [0], ], [["solar", "windows"], "15", {"photo_supply_ending": 50}, [0, 1]],