diff --git a/backend/Property.py b/backend/Property.py index faf2a864..1dc94331 100644 --- a/backend/Property.py +++ b/backend/Property.py @@ -345,7 +345,15 @@ class Property: if recommendation["type"] == "heating_control": # We update the data, as defined in the recommendaton - output.update(recommendation["simulation_config"]) + + simulation_config = recommendation["simulation_config"] + # If any entries in simulation_config are None, we will set them to "Unknown" which is the cleaning + # value + for key, value in simulation_config.items(): + if value is None: + simulation_config[key] = "Unknown" + + output.update(simulation_config) if recommendation["type"] == "solar_pv": output["photo_supply_ending"] = recommendation["photo_supply"] diff --git a/backend/app/plan/router.py b/backend/app/plan/router.py index a0603172..f737c2ee 100644 --- a/backend/app/plan/router.py +++ b/backend/app/plan/router.py @@ -97,6 +97,8 @@ async def trigger_plan(body: PlanTriggerRequest): 'old_data': epc_searcher.older_epcs.copy(), } + # We can patch the data if we are provided data from the customer + prepared_epc = EPCRecord( epc_records=epc_records, run_mode="newdata", diff --git a/etl/customers/urban_splash.py b/etl/customers/urban_splash.py index 6c371879..48116864 100644 --- a/etl/customers/urban_splash.py +++ b/etl/customers/urban_splash.py @@ -78,11 +78,12 @@ def app(): "uprn": newest_epc["uprn"], "address": newest_epc["address1"], "postcode": newest_epc["postcode"], - "walls-description": newest_epc["walls-description"], - "roof-description": newest_epc["roof-description"], - "floor-description": newest_epc["floor-description"], - "total-floor-area": newest_epc["total-floor-area"], - "full-address": newest_epc["address"] + # "walls-description": newest_epc["walls-description"], + # "roof-description": newest_epc["roof-description"], + # "floor-description": newest_epc["floor-description"], + # "total-floor-area": newest_epc["total-floor-area"], + "full-address": newest_epc["address"], + } processed_asset_list.append(to_append) @@ -91,6 +92,23 @@ def app(): processed_asset_list_df = pd.DataFrame(processed_asset_list) epc_data_df = pd.DataFrame(epc_data) + example = epc_data_df.iloc[11, :] + rest = epc_data_df[epc_data_df["address1"] != example["address1"]] + z = rest[ + (rest["total-floor-area"] == example["total-floor-area"]) & + (rest["current-energy-rating"] == "C") + ] + # Walls better in the example + z["walls-description"] + example["walls-description"] + + # Example has a property above + z["roof-description"] + example["roof-description"] + + compare = pd.concat([pd.DataFrame(example).T, z]) + compare["mainheat-description"] + # We store this data # Store the data in s3 filename = f"{USER_ID}/{PORTFOLIO_ID}/test_inputs.csv" diff --git a/etl/epc_clean/epc_attributes/MainheatAttributes.py b/etl/epc_clean/epc_attributes/MainheatAttributes.py index 813e15a6..673b460a 100644 --- a/etl/epc_clean/epc_attributes/MainheatAttributes.py +++ b/etl/epc_clean/epc_attributes/MainheatAttributes.py @@ -16,7 +16,6 @@ class MainHeatAttributes(Definitions): "solar assisted heat pump", "exhaust source heat pump", "community heat pump", - "portable electric heating" ] FUEL_TYPES = ["electric", "mains gas", "wood logs", "coal", "oil", "wood pellets", "anthracite", "dual fuel mineral and wood", "smokeless fuel", "lpg", "b30k"] @@ -62,7 +61,8 @@ class MainHeatAttributes(Definitions): REMAP = { "electric ceiling": "electric ceiling heating", "electric heat pumps": "electric heat pump", - "solar-assisted heat pump": "solar assisted heat pump" + "solar-assisted heat pump": "solar assisted heat pump", + "portable electric heating": "portable electric heaters", } edge_case_result = {} @@ -139,6 +139,8 @@ class MainHeatAttributes(Definitions): result.update({f'has_{ft.replace(" ", "_")}': False for ft in self.FUEL_TYPES}) result.update({f'has_{ot.replace(" ", "_")}': False for ot in self.OTHERS}) result['has_underfloor_heating'] = False + # We re-map entries that are the same + # We just drop those keys if self.nodata: return result diff --git a/etl/property_dimensions/app.py b/etl/property_dimensions/app.py index 08d19943..d3a43695 100644 --- a/etl/property_dimensions/app.py +++ b/etl/property_dimensions/app.py @@ -21,8 +21,11 @@ BUCKET = os.environ.get("BUCKET", "retrofit-data-dev") def app(): directories = [entry for entry in DATA_DIRECTORY.iterdir() if entry.is_dir()] + sample = [] for directory in tqdm(directories): + data = pd.read_csv(directory / "certificates.csv", low_memory=False) + data = data[data["LODGEMENT_DATE"] >= EARLIEST_EPC_DATE] data = data[~pd.isnull(data["UPRN"])] data["TOTAL_FLOOR_AREA"] = data["TOTAL_FLOOR_AREA"].astype(float) diff --git a/recommendations/Costs.py b/recommendations/Costs.py index da34d087..505b4a32 100644 --- a/recommendations/Costs.py +++ b/recommendations/Costs.py @@ -908,3 +908,56 @@ class Costs: "labour_hours": labour_hours, "labour_days": 1, } + + def electric_room_heaters(self, number_heated_rooms): + """ + We base the estimates for the cost of electric room heaters on the cost per room as estimated by the + following article: + https://www.bestelectricradiators.co.uk/blog/cost-to-install-a-new-heating-system-uk/ + + :param number_heated_rooms: int, number of rooms to be heated + :return: + """ + + total_cost = 500 * number_heated_rooms + subtotal_before_vat = total_cost / (1 + self.VAT_RATE) + vat = total_cost - subtotal_before_vat + + # TODO: Rough estimate to be reviewed + labour_hours = 1 * number_heated_rooms + labour_days = np.ceil(labour_hours / 8) + + return { + "total": total_cost, + "subtotal": subtotal_before_vat, + "vat": vat, + "labour_hours": labour_hours, + "labour_days": labour_days, + } + + def electric_storage_heaters(self, number_heated_rooms): + + """ + We base the estimates for the cost of electric storage heaters on the cost per room as estimated by the + energy saving trust + https://energysavingtrust.org.uk/advice/electric-heating/ + + The cost is based on the number of heated rooms + :param number_heated_rooms: int, number of rooms to be heated + """ + + total_cost = 1000 * number_heated_rooms + subtotal_before_vat = total_cost / (1 + self.VAT_RATE) + vat = total_cost - subtotal_before_vat + + # TODO: Rough estimate to be reviewed + labour_hours = 3 * number_heated_rooms + labour_days = np.ceil(labour_hours / 8) + + return { + "total": total_cost, + "subtotal": subtotal_before_vat, + "vat": vat, + "labour_hours": labour_hours, + "labour_days": labour_days, + } diff --git a/recommendations/HeatingControlRecommender.py b/recommendations/HeatingControlRecommender.py new file mode 100644 index 00000000..84d37931 --- /dev/null +++ b/recommendations/HeatingControlRecommender.py @@ -0,0 +1,78 @@ +from recommendations.Costs import Costs +from recommendations.recommendation_utils import check_simulation_difference +from backend.Property import Property +from etl.epc_clean.epc_attributes.MainheatControlAttributes import MainheatControlAttributes + + +class HeatingControlRecommender: + + def __init__(self, property_instance: Property): + self.property = property_instance + self.costs = Costs(self.property) + + self.recommendations = [] + + def recommend(self, phase=0): + # This first iteration of the recommender will provide very basic recommendation + # We recommend heating controls based on the main heating system + if self.property.main_heating["clean_description"] == "Room heaters, electric": + self.recommend_room_heaters_electric_controls(phase=phase) + return + + def recommend_room_heaters_electric_controls(self, phase): + """ + If the home has Room heaters, electric, we start by identifying potential heating controls that could + be upgraded, that would provide a practical impact. This will be the least invasive improvement. + + We can then consider the heating system itself + :return: + """ + if (self.property.data["mainheatc-energy-eff"] in ["Poor", "Very Poor", "Average"]) or ( + self.property.main_heating_controls["clean_description"] in ["Programmer and room thermostat"] + ): + # We recommend Programmer and appliance thermostats as the heating control. This has an average energy + # efficiency rating, and is likely to be more efficient than the current heating controls. if the + # rating is poor or very poor, the home may have a Programmer and room thermostat, which is less efficient + # than a Programmer and appliance thermostats, because it allows for much more granular control at not + # just a room level but individual heater/appliance level + + # Note: A room thermostat is commonly placed in a hallway, and it measures the temperature of the air + # surrounding it. It then sends a signal to the heating system to turn on or off, depending on the + # temperature. An appliance thermostat, on the other hand, is placed on the heater/appliance itself, and + # measures the temperature of the heater/appliance. This allows for much more granular control, and + # prevents overheating. + + # 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 = check_simulation_difference( + new_config=ending_config, old_config=self.property.main_heating_controls + ) + # This upgrade will only take the heating system to average energy efficiency + simulation_config["mainheatc_energy_eff_ending"] = "Good" + + self.recommendations.append( + { + "phase": phase, + "parts": [ + # TODO + ], + "type": "heating_control", + "description": "Upgrade heating controls to Programmer and Appliance or Smart " + "Thermostats for more precise heating control, and prevention of overheating", + "starting_u_value": None, + "new_u_value": None, + "sap_points": None, + **self.costs.programmer_and_appliance_thermostat(has_programmer=has_programmer), + "simulation_config": simulation_config + } + ) + + # We don't implement any other recommendations right now + return diff --git a/recommendations/HeatingRecommender.py b/recommendations/HeatingRecommender.py index c1b0df37..71d5e381 100644 --- a/recommendations/HeatingRecommender.py +++ b/recommendations/HeatingRecommender.py @@ -1,6 +1,7 @@ from recommendations.Costs import Costs +from recommendations.recommendation_utils import check_simulation_difference from backend.Property import Property -from etl.epc_clean.epc_attributes.MainheatControlAttributes import MainheatControlAttributes +from etl.epc_clean.epc_attributes.MainheatAttributes import MainHeatAttributes class HeatingRecommender: @@ -13,6 +14,7 @@ class HeatingRecommender: def recommend(self, phase=0): # This first iteration of the recommender will provide very basic recommendation + # We recommend heating controls based on the main heating system if self.property.main_heating["clean_description"] == "Room heaters, electric": self.recommend_room_heaters_electric(phase=phase) return @@ -39,45 +41,50 @@ class HeatingRecommender: :return: """ if self.property.data["mainheat-energy-eff"] in ["Poor", "Very Poor"]: - # We recommend Programmer and appliance thermostats as the heating control. This has an average energy - # efficiency rating, and is likely to be more efficient than the current heating controls. if the - # rating is poor or very poor, the home may have a Programmer and room thermostat, which is less efficient - # than a Programmer and appliance thermostats, because it allows for much more granular control at not - # just a room level but individual heater/appliance level + # Re recommend two possible upgrades: + # 1) Installation of more efficient electic room heaters + # 2) Installation of electric storage heaters - # Note: A room thermostat is commonly placed in a hallway, and it measures the temperature of the air - # surrounding it. It then sends a signal to the heating system to turn on or off, depending on the - # temperature. An appliance thermostat, on the other hand, is placed on the heater/appliance itself, and - # measures the temperature of the heater/appliance. This allows for much more granular control, and - # prevents overheating. + room_heater_recommendation = { + "phase": phase, + "parts": [ + # TODO + ], + "type": "heating", + "description": "Upgrade electric room heaters to more electric radiators", + "starting_u_value": None, + "new_u_value": None, + "sap_points": None, + **self.costs.electric_room_heaters(number_heated_rooms=self.property.data["number-heated-rooms"]), + "simulation_config": {"mainheat_energy_eff_ending": "Average"} + } - # 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 + ending_config = MainHeatAttributes("Electric storage heaters, radiators").process() + simulation_config = check_simulation_difference( + new_config=ending_config, old_config=self.property.main_heating ) + # This upgrade will only take the heating system to average energy efficiency + simulation_config["mainheatc_energy_eff_ending"] = "Good" - self.recommendations.append( - { - "phase": phase, - "parts": [ - # TODO - ], - "type": "heating_control", - "description": "Upgrade heating controls to Programmer and Appliance or Smart" - "Thermostats for more precise heating control, and prevention of overheating", - "starting_u_value": None, - "new_u_value": None, - "sap_points": None, - **self.costs.programmer_and_appliance_thermostat(has_programmer=has_programmer), - "simulation_config": simulation_config + electric_storage_heaters_recommendation = { + "phase": phase, + "parts": [ + # TODO + ], + "type": "heating", + "description": "Install electric storage heaters", + "starting_u_value": None, + "new_u_value": None, + "sap_points": None, + **self.costs.electric_storage_heaters(number_heated_rooms=self.property.data["number-heated-rooms"]), + "simulation_config": { + "TODO" # TODO + "mainheat_energy_eff_ending": "Average" } + } + + self.recommendations.extend( + [room_heater_recommendation, electric_storage_heaters_recommendation] ) # We don't implement any other recommendations right now diff --git a/recommendations/Recommendations.py b/recommendations/Recommendations.py index 1d0fdab6..938debe1 100644 --- a/recommendations/Recommendations.py +++ b/recommendations/Recommendations.py @@ -1,5 +1,3 @@ -import numpy as np - from backend.Property import Property from typing import List from itertools import groupby @@ -12,6 +10,7 @@ from recommendations.LightingRecommendations import LightingRecommendations from recommendations.SolarPvRecommendations import SolarPvRecommendations from recommendations.WindowsRecommendations import WindowsRecommendations from recommendations.HeatingRecommender import HeatingRecommender +from recommendations.HeatingControlRecommender import HeatingControlRecommender from backend.ml_models.AnnualBillSavings import AnnualBillSavings @@ -44,6 +43,7 @@ class Recommendations: self.windows_recommender = WindowsRecommendations(property_instance=property_instance, materials=materials) self.solar_recommender = SolarPvRecommendations(property_instance=property_instance) self.heating_recommender = HeatingRecommender(property_instance=property_instance) + self.heating_control_recommender = HeatingControlRecommender(property_instance=property_instance) def recommend(self): @@ -99,6 +99,11 @@ class Recommendations: property_recommendations.append(self.heating_recommender.recommendations) phase += 1 + self.heating_control_recommender.recommend(phase=phase) + if self.heating_control_recommender.recommendations: + property_recommendations.append(self.heating_control_recommender.recommendations) + phase += 1 + self.lighting_recommender.recommend(phase=phase) if self.lighting_recommender.recommendation: property_recommendations.append(self.lighting_recommender.recommendation) diff --git a/recommendations/recommendation_utils.py b/recommendations/recommendation_utils.py index 395cd2ea..0d5f9743 100644 --- a/recommendations/recommendation_utils.py +++ b/recommendations/recommendation_utils.py @@ -754,3 +754,16 @@ def calculate_cavity_age(newest_epc, older_epcs, cleaned): cavity_age = (datetime.now() - pd.to_datetime(df["inspection-date"].max())).days return cavity_age + + +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