From ad82b15c6efc59d60f3ab184392d16abcd9b3cbf Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Wed, 2 Oct 2024 17:29:04 +0100 Subject: [PATCH] added measure matrix code --- backend/Outputs.py | 153 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 151 insertions(+), 2 deletions(-) diff --git a/backend/Outputs.py b/backend/Outputs.py index 0be2ad3b..4e300e81 100644 --- a/backend/Outputs.py +++ b/backend/Outputs.py @@ -1,5 +1,7 @@ +import pandas as pd from sqlalchemy.orm import sessionmaker +from backend.app.utils import sap_to_epc from backend.app.db.connection import db_engine from backend.app.db.models.portfolio import PropertyModel, PropertyDetailsEpcModel from backend.app.db.models.recommendations import Recommendation, Plan, PlanRecommendations @@ -8,6 +10,37 @@ from backend.app.db.models.recommendations import Recommendation, Plan, PlanReco class Outputs: FORMATS = ["mds"] + MDS_MEASURE_MAPPING = { + "external_wall_insulation": "EWI (Trad Const)", + "cavity_wall_insulation": "CWI", + "loft_insulation": "LI", + "party_wall_insulation": "Party Wall Insu", + "internal_wall_insulation": "IWI (POA - Prov Sum Only)", + "suspended_floor_insulation": "U/F Insu (Manual install)", + "solid_floor_insulation": "Solid floor insl (Out of scope - Prov sum only)", + "air_source_heat_pump": "ASHP Htg", + "ground_source_heat_pump": "GSHP Htg", + "shared_ground_loops": "Shared ground loops", + "communal_heat_networks": "Communal heat networks", + "district_heating_networks": "District heating networks", + "high_heat_retention_storage_heaters": "Elec Storage Htrs (Out of scope -Prov sum only)", + "low_energy_lighting": "Low Energy Bulbs", + "cylinder_insulation": "Cyl Insulation", + "smart_controls": "Smart controls", + "zone_controls": "Zone controls", + "trvs": "Upgrade TRV's", + "solar_pv": "Solar PV", + "solar_thermal": "Solar Thermal", + "double_glazing": "Double Glazing (POA - Prov sum only)", + "draught_proofing": "Draught Proofing", + "mechanical_ventilation": "Ventilation upgrade", + "gas_boiler": "Gas Boiler Replacement", + "flat_roof_insulation": "Flat roof (Out of scope - prov sum only)", + "room_in_roof_insulation": "RIR (POA - Prov sum only)", + "ev_charging": "EV Charging", + "battery": "Battery" + } + def __init__(self, format, portfolio_id): """ This class handles the creation of standard outputs for the backend. For example, creation of @@ -50,7 +83,7 @@ class Outputs: def get_plans_from_db(self): - plans_query = self.session.query(Plan).all() + plans_query = self.session.query(Plan).filter(Plan.portfolio_id == self.portfolio_id).all() # Transform plans data to include all fields dynamically plans_data = [ {col.name: getattr(plan, col.name) for col in Plan.__table__.columns} @@ -87,6 +120,41 @@ class Outputs: return recommendations_data + def make_mds_measure_matrix(self, scenario_recommendations): + all_measures = list(self.MDS_MEASURE_MAPPING.values()) + + # Collect rows in a list + rows = [] + + # Populate the rows list + for idx, row in scenario_recommendations.iterrows(): + property_id = row["property_id"] + measure_type = row["measure_type"] + + # Get the label for the current type + measure_label = self.MDS_MEASURE_MAPPING.get(measure_type, None) + + # If the property_id already exists in the collected rows, update it + existing_row = next((item for item in rows if item["property_id"] == property_id), None) + if existing_row is None: + # Create a new row if the property_id doesn't exist + new_row = {measure: None for measure in all_measures} + new_row["property_id"] = property_id + rows.append(new_row) + else: + new_row = existing_row + + # Set the corresponding measure label in the row + new_row[measure_label] = measure_label + + # Convert the list of dictionaries to a DataFrame + matrix = pd.DataFrame(rows) + + # Reset the index for cleanliness + matrix.reset_index(drop=True, inplace=True) + + return matrix + def export_mds(self): """ This function will export the data in the MDS format @@ -115,12 +183,93 @@ class Outputs: properties_data = self.get_properties_from_db() plans_data = self.get_plans_from_db() - plan_ids = [plan['id'] for plan in plans_data] recommendations_data = self.get_recommendations_from_db(plan_ids) self.session.close() + # Convert these tables to dataframes + properties_df = pd.DataFrame(properties_data) + plans_df = pd.DataFrame(plans_data) + recommendations_df = pd.DataFrame(recommendations_data) + + scenario_ids = plans_df["scenario_id"].unique() + + # We start to create the MDS sheet + mds = properties_df[ + [ + "property_id", + "address", + "postcode", + "uprn", + "current_epc_rating", + "current_sap_points", + # TODO: Need to add current heat demand + "property_type", + "built_form", + "total_floor_area", + "walls", + "tenure", + "mainfuel", + # TODO: For estimated bill, this should probably be without the cost of appliances + ] + ].copy().rename( + columns={ + "address": "Address", + "postcode": "Postcode", + "uprn": "UPRN", + "current_epc_rating": "Pre EPC", + "current_sap_points": "EPC Source", + # TODO: Need to add current heat demand + "property_type": "Property Type", + "built_form": "Built Form", + "total_floor_area": "Floor area m2 (If known)", + "walls": "Wall Type (Mandatory field)", + "tenure": "Tenure", + "mainfuel": "Existing Fuel Type" + # TODO: For estimated bill, this should probably be without the cost of appliances + } + ) + + # TODO - format + # 1) property type + # 2) walls + # 3) tenure + # 4) mainfuel + # 5) Epc Rating + + mds_output_by_scenario = {} + for scenario_id in scenario_ids: + scenario_recommendations = recommendations_df[recommendations_df["Scenario ID"] == scenario_id] + + # For each measure, we create the measure matrix + scenario_measure_matrix = self.make_mds_measure_matrix(scenario_recommendations) + + # Calculate the predicted impact on: SAP, heat demand, bills, kwh + recommendation_impacts = scenario_recommendations.groupby("property_id")[ + ["sap_points", "heat_demand", "kwh_savings", "energy_cost_savings"] + ].sum().reset_index() + + scenario_mds = mds.merge( + recommendation_impacts, how="left", on="property_id" + ) + # If we have no recommendations, sap_points, kwh_savings, head_demand will be NaN + scenario_mds.fillna(0, inplace=True) + scenario_mds["Post SAP"] = scenario_mds["EPC Source"] + scenario_mds["sap_points"] + # Round Post SAP down to the nearest integer + scenario_mds["Post SAP"] = scenario_mds["Post SAP"].apply(lambda x: int(x)) + scenario_mds["Post EPC"] = scenario_mds["Post SAP"].apply(lambda x: sap_to_epc(x)) + + # TODO: Post heat demand + + scenario_mds = scenario_mds.rename( + columns={ + "sap_points": "Predicted SAP Points", + "kwh_savings": "Energy Saving (Kwh)", + "energy_cost_savings": "Bill Reduction (£ per yr)" + } + ) + def export(self): """ This function will export the data in the required format