diff --git a/backend/app/assumptions.py b/backend/app/assumptions.py index ffc186df..f0ddf868 100644 --- a/backend/app/assumptions.py +++ b/backend/app/assumptions.py @@ -1,6 +1,6 @@ # Assumes that the average efficiency of an air source heat pump is 250%, taking the median of the 200-400% range, # which is often quoted as a sensible efficiency range for air source heat pumps. -PESSIMISTIC_ASHPY_EFFICIENCY = 200 +PESSIMISTIC_ASHP_EFFICIENCY = 200 AVERAGE_ASHP_EFFICIENCY = 300 # Conservative estimate of the proportion of electricity that will be consumed, whereas the rest will diff --git a/etl/customers/newhaven/newhaven_study.py b/etl/customers/newhaven/newhaven_study.py index e87705b8..e6871678 100644 --- a/etl/customers/newhaven/newhaven_study.py +++ b/etl/customers/newhaven/newhaven_study.py @@ -269,10 +269,13 @@ def make_asset_list(): # We add a patch to one of the units because there's no data for the built form # We would be able to handle this automatically in the future, when using OS API - patches = [{ - "uprn": "10033266220", - "built-form": "Semi-Detached", - }] + patches = [ + { + "uprn": "10033266220", + "built-form": "Semi-Detached", + }, + {'uprn': '10033266219', 'built-form': 'Semi-Detached'} + ] # Store patches in s3 patches_filename = f"{USER_ID}/{PORTFOLIO_ID}/patches.json" diff --git a/etl/customers/newhaven/slides.py b/etl/customers/newhaven/slides.py new file mode 100644 index 00000000..3fe27452 --- /dev/null +++ b/etl/customers/newhaven/slides.py @@ -0,0 +1,214 @@ +import pandas as pd +from sqlalchemy.orm import sessionmaker +from backend.app.db.connection import db_engine +from backend.app.db.models.recommendations import Recommendation, Plan, PlanRecommendations, Scenario +from backend.app.db.models.portfolio import PropertyModel, PropertyDetailsEpcModel + + +def get_data(portfolio_id, scenario_ids): + session = sessionmaker(bind=db_engine)() + session.begin() + + # Get properties and their details for a specific portfolio + properties_query = session.query( + PropertyModel, + PropertyDetailsEpcModel + ).join( + PropertyDetailsEpcModel, PropertyModel.id == PropertyDetailsEpcModel.property_id + ).filter( + PropertyModel.portfolio_id == portfolio_id # Filter by portfolio ID + ).all() + + # Transform properties data to include all fields dynamically + properties_data = [ + {**{col.name: getattr(prop.PropertyModel, col.name) for col in PropertyModel.__table__.columns}, + **{col.name: getattr(prop.PropertyDetailsEpcModel, col.name) for col in + PropertyDetailsEpcModel.__table__.columns}} + for prop in properties_query + ] + + # Get property IDs from fetched properties + + # Get plans linked to the fetched properties + plans_query = session.query(Plan).filter(Plan.scenario_id.in_(scenario_ids)).all() + + # Transform plans data to include all fields dynamically + plans_data = [ + {col.name: getattr(plan, col.name) for col in Plan.__table__.columns} + for plan in plans_query + ] + + # Extract plan IDs for filtering recommendations through PlanRecommendations + plan_ids = [plan['id'] for plan in plans_data] + + # Get recommendations through PlanRecommendations for those plans and that are default + recommendations_query = session.query( + Recommendation, + Plan.scenario_id + ).join( + PlanRecommendations, Recommendation.id == PlanRecommendations.recommendation_id + ).join( + Plan, Plan.id == PlanRecommendations.plan_id # Join with Plan to access scenario_id + ).filter( + PlanRecommendations.plan_id.in_(plan_ids), + Recommendation.default == True # Filtering for default recommendations + ).all() + + # Transform recommendations data to include all fields dynamically and include scenario_id + recommendations_data = [ + {**{col.name: getattr(rec.Recommendation, col.name) if hasattr(rec, 'Recommendation') else getattr(rec, + col.name) for + col in Recommendation.__table__.columns}, + "Scenario ID": rec.scenario_id} + for rec in recommendations_query + ] + + session.close() + + return properties_data, plans_data, recommendations_data + + +def slides(): + # Prepares the information required for the slides + + # Right now this is the second version of the nehaven portfolio + portfolio_id = 90 + # Look at one scenario at a time, otherwise this is agony + scenario_ids = [47, 48, 49] + + properties_data, plans_data, recommendations_data = get_data(portfolio_id, scenario_ids) + + properties_df = pd.DataFrame(properties_data) + plans_df = pd.DataFrame(plans_data) + recommendations_df = pd.DataFrame(recommendations_data) + + if properties_df.shape[0] != 2553: + raise ValueError("The number of unique properties is not 2553") + + def estimate_post_retrofit_heating_hotwater_kwh(recommendations_df, scenario_ids): + # Get the recommendations for the scenario, default + scenario_comparison_df = [] + scenario_comparison_df_2 = [] + for scenario_id in scenario_ids: + # Get the recommendations for the scenario, default + scenario_recommendations = recommendations_df[ + (recommendations_df["Scenario ID"] == scenario_id) & + (recommendations_df["default"] == True) + ].copy() + + scenario_recommendations['ligting_kwh'] = scenario_recommendations.apply( + lambda x: x['kwh_savings'] if x['type'] == 'low_energy_lighting' else 0, + axis=1) + scenario_recommendations['solar_kwh'] = scenario_recommendations.apply( + lambda x: x['kwh_savings'] if x['type'] == 'solar_pv' else 0, axis=1) + + if scenario_recommendations['solar_kwh'].sum() > 0: + blah + + # Set 'Estimated Kwh Savings' to zero where specific kwh columns are used + scenario_recommendations['Estimated Kwh Savings'] = scenario_recommendations.apply( + lambda x: 0 if x['type'] in ['low_energy_lighting', 'solar_pv'] else x[ + 'kwh_savings'], axis=1) + + grouped_data = scenario_recommendations.groupby(['property_id']).agg({ + 'Estimated Kwh Savings': 'sum', + 'ligting_kwh': 'sum', + 'solar_kwh': 'sum' + }).reset_index() + + comparison = properties_df.drop_duplicates().merge( + grouped_data, on=["property_id"], how="left" + ) + + comparison["Post Retrofit Heating & Hotwater kwh"] = ( + comparison["current_energy_demand_heating_hotwater"] - \ + comparison["Estimated Kwh Savings"] + ) + + avgs = comparison[['current_energy_demand_heating_hotwater', 'Post Retrofit Heating & Hotwater kwh']].mean() + + # We now, for properties that have a plan, do a before and after + with_savings = comparison[~pd.isnull(comparison["Estimated Kwh Savings"])] + + avgs2 = with_savings[ + ['current_energy_demand_heating_hotwater', 'Post Retrofit Heating & Hotwater kwh']].mean() + avgs2["difference"] = avgs2["current_energy_demand_heating_hotwater"] - avgs2[ + "Post Retrofit Heating & Hotwater kwh"] + avgs2["percentage_reduction"] = 100 * avgs2["difference"] / avgs2["current_energy_demand_heating_hotwater"] + + scenario_comparison_df.append({"scenario_id": scenario_id, **avgs}) + scenario_comparison_df_2.append({"scenario_id": scenario_id, **avgs2}) + + scenario_comparison_df = pd.DataFrame(scenario_comparison_df) + scenario_comparison_df_2 = pd.DataFrame(scenario_comparison_df_2) + + return scenario_comparison_df, scenario_comparison_df_2 + + # TODO: How do we factor in solar PV + + # Q1: What is the baseline heating and energy demand for the properties in the portfolio - baseline? + heating_hotwater_kwh = ( + properties_df[['current_energy_demand', 'current_energy_demand_heating_hotwater']] + .mean() + ) + + # Q2: For each scenario, what is the £ per kwh reduction? + # Calculate total kwh savings + kwh_plan_impact = estimate_post_retrofit_heating_hotwater_kwh(properties_df, recommendations_df) + + z = df[ + (df["Recommendation Default Status"] == True) & + (df["Plan Name"].isin(['Demand Reduction – cavity & roof insulation'])) + ] + z2 = z[z["Property ID"] == 25215] + # Find duplicated property ID, recommendationt type combos + z = z[z.duplicated(subset=["Property ID", "Recommendation Type"])] + + for plan_name in df["Plan Name"].unique(): + # Get default recs + default_recs = df[ + (df["Recommendation Default Status"] == True) & + (df["Plan Name"] == plan_name) + ].copy() + if default_recs["Recommendation ID"].duplicated().sum(): + raise Exception("somethign went wrong") + + default_recs["Recommendation Type"].unique() + + # We now calculate the total savings + total_savings = default_recs["Estimated Kwh Savings"].sum() + total_cost = default_recs["Recommendation Cost"].sum() + + kwh_savings = df[ + df["Recommendation Default Status"] == True + ].groupby("Plan Name")[["Estimated Kwh Savings", "Recommendation Cost"]].sum().rename( + columns={"Estimated Kwh Savings": "Total Kwh Savings", "Recommendation Cost": "Total Cost"} + ).reset_index() + + kwh_savings["Cost per Kwh Saved"] = kwh_savings["Total Cost"] / kwh_savings["Total Kwh Savings"] + + # Q3: For each scenario, we want to answer what the heating and hot water kwh looks like after retrofit + # We need to take recommndations that affect just the heating and hot water + + # By property + + df["Type Mapped"] = df["Recommendation Type"].copy().replace( + { + "loft_insulation": "roof_insulation", + "room_roof_insulation": "roof_insulation", + "flat_roof_insulation": "roof_insulation", + "hot_water_tank_insulation": "other", + "cylinder_thermostat": "other", + "sealing_open_fireplace": "other", + } + ) + + # Group by 'Plan Name' and 'Recommendation Type' and count unique 'Property ID' + recommendation_summary = df.groupby(['Plan Name', 'Type Mapped']).agg({ + 'Property ID': 'nunique' + }).reset_index() + + recommendation_summary.columns = ['Plan Name', 'Type Mapped', 'Number of Properties'] + recommendation_summary["Percentage of Properties"] = 100 * ( + recommendation_summary["Number of Properties"] / df["Property ID"].nunique() + ) diff --git a/recommendations/FloorRecommendations.py b/recommendations/FloorRecommendations.py index 74be7d41..c63d45c2 100644 --- a/recommendations/FloorRecommendations.py +++ b/recommendations/FloorRecommendations.py @@ -74,7 +74,6 @@ class FloorRecommendations(Definitions): u_value = self.property.floor["thermal_transmittance"] property_type = self.property.data["property-type"] floor_area = self.property.insulation_floor_area - year_built = self.property.year_built if self.property.floor["another_property_below"] | (self.property.floor["insulation_thickness"] in [ "average", "above average" @@ -95,14 +94,16 @@ class FloorRecommendations(Definitions): if u_value: - # By being built more recently than this, it means that the property was likely build with soild - # concrete floors with insulation already - if year_built < self.PART_L_YEAR_CUTOFF: - raise NotImplementedError("Not investigated this use case") - - if u_value <= self.BUILDING_REGULATIONS_PART_L_MAX_U_VALUE: - # The floor is already compliant - return + # In this case where we have the u-value of a floor, we likely don't have any other information about it + # so there is no recommendation that we can practically make + if ( + self.property.floor["is_suspended"] or + self.property.floor["is_to_unheated_space"] or + self.property.floor["is_to_external_air"] or + self.property.floor["is_solid"] + ): + raise ValueError("This should not be possible") + return if u_value is None: u_value = get_floor_u_value( diff --git a/recommendations/Recommendations.py b/recommendations/Recommendations.py index 33c8bee4..fef7472c 100644 --- a/recommendations/Recommendations.py +++ b/recommendations/Recommendations.py @@ -20,7 +20,9 @@ import backend.app.assumptions as assumptions ASHP_COP = 3 DESCRIPTIONS_TO_FUEL_TYPES = { - "Air source heat pump, radiators, electric": {"fuel": "Electricity", "cop": ASHP_COP}, + "Air source heat pump, radiators, electric": { + "fuel": "Electricity", "cop": assumptions.AVERAGE_ASHP_EFFICIENCY / 100 + }, "Boiler and radiators, mains gas": {"fuel": 'Natural Gas', "cop": 0.9}, 'Electric storage heaters': {"fuel": 'Electricity', "cop": 1}, "Electric immersion, off-peak": {"fuel": 'Electricity', "cop": 1}, @@ -46,6 +48,11 @@ DESCRIPTIONS_TO_FUEL_TYPES = { "Boiler and radiators, dual fuel (mineral and wood)": {"fuel": "Wood Logs", "cop": 0.9}, "Electric immersion, standard tariff, plus solar": {"fuel": "Electricity + Solar Thermal", "cop": 1}, "From main system, flue gas heat recovery": {"fuel": "Natural Gas", "cop": 0.9}, + "Electric underfloor heating": {"fuel": "Electricity", "cop": 1}, + "No system present: electric immersion assumed": {"fuel": "Electricity", "cop": 1}, + "Air source heat pump, underfloor, electric": { + "fuel": "Electricity", "cop": assumptions.AVERAGE_ASHP_EFFICIENCY / 100 + }, } STARTING_DUMMY_ID_VALUE = -9999