working on slides, debugging recommendations

This commit is contained in:
Khalim Conn-Kowlessar 2024-08-14 21:57:50 +01:00
parent 7e973d7955
commit 6e802b1f58
5 changed files with 240 additions and 15 deletions

View file

@ -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

View file

@ -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"

View file

@ -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()
)

View file

@ -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(

View file

@ -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