diff --git a/backend/Property.py b/backend/Property.py index 418a35a1..faf2a864 100644 --- a/backend/Property.py +++ b/backend/Property.py @@ -238,12 +238,15 @@ class Property: # 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 + output["external_insulation_ending"] = True + output["internal_insulation_ending"] = False if recommendation["type"] == "internal_wall_insulation": - output["external_insulation"] = False - output["internal_insulation"] = True + output["external_insulation_ending"] = False + output["internal_insulation_ending"] = True + + if recommendation["type"] == "cavity_wall_insulation": + output["is_filled_cavity_ending"] = True # TODO: perhaps detrimental # When making a recommendation for the wall, we will also update the ventilation @@ -314,7 +317,7 @@ class Property: if recommendation["type"] == "low_energy_lighting": output["low_energy_lighting_ending"] = 100 - output["lighting_energy_eff_starting"] = "Very Good" + output["lighting_energy_eff_ending"] = "Very Good" if recommendation["type"] == "windows_glazing": output["multi_glaze_proportion_ending"] = 100 @@ -340,6 +343,10 @@ class Property: else: output["glazed_type_ending"] = "double glazing installed during or after 2002 " + if recommendation["type"] == "heating_control": + # We update the data, as defined in the recommendaton + output.update(recommendation["simulation_config"]) + if recommendation["type"] == "solar_pv": output["photo_supply_ending"] = recommendation["photo_supply"] @@ -348,7 +355,7 @@ class Property: "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" + "windows_glazing", "solar_pv", "heating_control", ]: raise NotImplementedError("Implement me") diff --git a/backend/app/plan/router.py b/backend/app/plan/router.py index 20e72b8d..a0603172 100644 --- a/backend/app/plan/router.py +++ b/backend/app/plan/router.py @@ -144,6 +144,9 @@ async def trigger_plan(body: PlanTriggerRequest): recommender = Recommendations(property_instance=p, materials=materials) property_recommendations, property_representative_recommendations = recommender.recommend() + # TODO: Re-run property_dimensions + # Do the simulation in adjust_difference_record_with_recommendations, to update the values + if not property_recommendations: continue diff --git a/etl/epc/Record.py b/etl/epc/Record.py index fc670e5e..c793716f 100644 --- a/etl/epc/Record.py +++ b/etl/epc/Record.py @@ -467,8 +467,7 @@ class EPCRecord: ] if ( - self.construction_age_band is not None - and self.construction_age_band not in DATA_ANOMALY_MATCHES + self.construction_age_band not in DATA_ANOMALY_MATCHES ): result = result[ (result["CONSTRUCTION_AGE_BAND"] == self.construction_age_band) @@ -481,7 +480,7 @@ class EPCRecord: result = result[(result["BUILT_FORM"] == self.prepared_epc["built-form"])] return result[ - ["NUMBER_HABITABLE_ROOMS", "TOTAL_FLOOR_AREA", "FLOOR_HEIGHT"] + ["NUMBER_HABITABLE_ROOMS", "NUMBER_HEATED_ROOMS", "TOTAL_FLOOR_AREA", "FLOOR_HEIGHT"] ].mean() def _clean_property_dimensions(self): @@ -490,12 +489,11 @@ class EPCRecord: """ if not self.prepared_epc: - raise ValueError("EPC Recrod doesn not contain epc data") + raise ValueError("EPC Record doesn not contain epc data") - if not self.prepared_epc["number-habitable-rooms"] or ( - self.prepared_epc["floor-height"] == "" - or self.prepared_epc["floor-height"] in DATA_ANOMALY_MATCHES - ): + if (self.prepared_epc["number-habitable-rooms"] in DATA_ANOMALY_MATCHES) or ( + self.prepared_epc["floor-height"] in DATA_ANOMALY_MATCHES + ) or (self.prepared_epc["number-heated-rooms"] in DATA_ANOMALY_MATCHES): property_dimensions = read_dataframe_from_s3_parquet( bucket_name=DATA_BUCKET, file_key=f"property_dimensions/{self.prepared_epc['local-authority']}.parquet", @@ -504,14 +502,17 @@ class EPCRecord: property_dimensions ) - if not self.prepared_epc["number-habitable-rooms"]: + if self.prepared_epc["number-habitable-rooms"] in DATA_ANOMALY_MATCHES: self.prepared_epc["number-habitable-rooms"] = float( self.property_dimensions["NUMBER_HABITABLE_ROOMS"].round() ) else: - self.prepared_epc["number-habitable-rooms"] = float( - self.prepared_epc["number-habitable-rooms"] - ) + self.prepared_epc["number-habitable-rooms"] = float(self.prepared_epc["number-habitable-rooms"]) + + if self.prepared_epc["number-heated-rooms"] in DATA_ANOMALY_MATCHES: + self.prepared_epc["number-heated-rooms"] = float(self.property_dimensions["NUMBER_HEATED_ROOMS"].round()) + else: + self.prepared_epc["number-heated-rooms"] = float(self.prepared_epc["number-heated-rooms"]) self.number_of_floors = estimate_number_of_floors( self.prepared_epc["property-type"] @@ -729,7 +730,7 @@ class EPCRecord: old_record["lodgement-datetime"] for old_record in self.old_data if old_record["construction-age-band"] - not in DATA_ANOMALY_MATCHES + not in DATA_ANOMALY_MATCHES ] ) diff --git a/etl/property_dimensions/app.py b/etl/property_dimensions/app.py index 876d67e2..08d19943 100644 --- a/etl/property_dimensions/app.py +++ b/etl/property_dimensions/app.py @@ -7,7 +7,7 @@ from pathlib import Path import pandas as pd from tqdm import tqdm from etl.epc.settings import EARLIEST_EPC_DATE -from etl.epc.DataProcessor import DataProcessor +from etl.epc.DataProcessor import EPCDataProcessor from BaseUtility import Definitions from utils.s3 import save_dataframe_to_s3_parquet @@ -28,17 +28,21 @@ def app(): data["TOTAL_FLOOR_AREA"] = data["TOTAL_FLOOR_AREA"].astype(float) data["CONSTRUCTION_AGE_BAND"] = data["CONSTRUCTION_AGE_BAND"].apply( - lambda x: DataProcessor.clean_construction_age_band(x) + lambda x: EPCDataProcessor.clean_construction_age_band(x) ) data = data[~pd.isnull(data["CONSTRUCTION_AGE_BAND"])] data = data[~data["CONSTRUCTION_AGE_BAND"].isin(Definitions.DATA_ANOMALY_MATCHES)] data = data[~pd.isnull(data["TOTAL_FLOOR_AREA"])] data = data[~pd.isnull(data["NUMBER_HABITABLE_ROOMS"])] data = data[~pd.isnull(data["FLOOR_HEIGHT"])] + data = data[~pd.isnull(data["NUMBER_HEATED_ROOMS"])] df = ( data.groupby(GROUPBY) - .agg({"NUMBER_HABITABLE_ROOMS": "median", "TOTAL_FLOOR_AREA": "mean", "FLOOR_HEIGHT": "mean"}) + .agg( + {"NUMBER_HEATED_ROOMS": "median", "NUMBER_HABITABLE_ROOMS": "median", "TOTAL_FLOOR_AREA": "mean", + "FLOOR_HEIGHT": "mean"} + ) .reset_index() ) diff --git a/recommendations/HeatingRecommender.py b/recommendations/HeatingRecommender.py index 5c386b84..c1b0df37 100644 --- a/recommendations/HeatingRecommender.py +++ b/recommendations/HeatingRecommender.py @@ -1,5 +1,6 @@ from recommendations.Costs import Costs from backend.Property import Property +from etl.epc_clean.epc_attributes.MainheatControlAttributes import MainheatControlAttributes class HeatingRecommender: @@ -12,10 +13,23 @@ class HeatingRecommender: def recommend(self, phase=0): # This first iteration of the recommender will provide very basic recommendation - if self.property.main_heating == "Room heaters, electric": + if self.property.main_heating["clean_description"] == "Room heaters, electric": self.recommend_room_heaters_electric(phase=phase) return + @staticmethod + def check_simulation_difference(old_config, new_config): + """ + Given two dictionaries, that describe the heating control configurations, this method will compare the two + and pick out the differences. These differences will be things that have been added and things that have been + removed. This will be used to determine how we should be updating the configuration in the simulation + :return: + """ + + differences = {key + "_ending": new_config[key] for key in new_config if old_config[key] != new_config[key]} + + return differences + def recommend_room_heaters_electric(self, phase): """ If the home has Room heaters, electric, we start by identifying potential heating controls that could @@ -40,6 +54,15 @@ class HeatingRecommender: # In order to cost, we check if the property already has a programmer, and therefor we will just need to # add the cost of the appliance thermostats has_programmer = self.property.main_heating_controls["switch_system"] == "programmer" + + ending_config = MainheatControlAttributes("Programmer and appliance thermostats").process() + # We look at what has changed in the ending config, and compare it to the current config + + # We use this to determine how we should be updating the config + simulation_config = self.check_simulation_difference( + new_config=ending_config, old_config=self.property.main_heating_controls + ) + self.recommendations.append( { "phase": phase, @@ -52,7 +75,8 @@ class HeatingRecommender: "starting_u_value": None, "new_u_value": None, "sap_points": None, - **self.costs.programmer_and_appliance_thermostat(has_programmer=has_programmer) + **self.costs.programmer_and_appliance_thermostat(has_programmer=has_programmer), + "simulation_config": simulation_config } )