added additional secondary heating recommendation

This commit is contained in:
Khalim Conn-Kowlessar 2024-11-13 12:13:37 +00:00
parent 6d01490962
commit b01635ddd6
9 changed files with 169 additions and 25 deletions

2
.idea/Model.iml generated
View file

@ -7,7 +7,7 @@
<sourceFolder url="file://$MODULE_DIR$/open_uprn" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/open_uprn" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/recommendations" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/recommendations" isTestSource="false" />
</content> </content>
<orderEntry type="jdk" jdkName="Stonewater-wave-3" jdkType="Python SDK" /> <orderEntry type="jdk" jdkName="Fastapi-backend" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />
</component> </component>
<component name="PyNamespacePackagesService"> <component name="PyNamespacePackagesService">

2
.idea/misc.xml generated
View file

@ -3,7 +3,7 @@
<component name="Black"> <component name="Black">
<option name="sdkName" value="Python 3.10 (backend)" /> <option name="sdkName" value="Python 3.10 (backend)" />
</component> </component>
<component name="ProjectRootManager" version="2" project-jdk-name="Stonewater-wave-3" project-jdk-type="Python SDK" /> <component name="ProjectRootManager" version="2" project-jdk-name="Fastapi-backend" project-jdk-type="Python SDK" />
<component name="PyCharmProfessionalAdvertiser"> <component name="PyCharmProfessionalAdvertiser">
<option name="shown" value="true" /> <option name="shown" value="true" />
</component> </component>

View file

@ -792,9 +792,14 @@ class GoogleSolarApi:
property_instance = [p for p in input_properties if p.id == unit["property_id"]][0] property_instance = [p for p in input_properties if p.id == unit["property_id"]][0]
# At this level, we check if the property is suitable for solar and if now, skip # At this level, we check if the property is suitable for solar and if now, skip
# Or if we have a solar non-invasive recommendation # Or if we have a solar non-invasive recommendation
non_invasive_rec = next(
(r for r in property_instance.non_invasive_recommendations if r["type"] == "solar_pv"), {}
).get("array_wattage")
if ( if (
(not property_instance.is_solar_pv_valid()) or (not property_instance.is_solar_pv_valid()) or
[r for r in property_instance.non_invasive_recommendations if r["type"] == "solar_pv"] non_invasive_rec is not None
): ):
continue continue

View file

@ -394,7 +394,7 @@ async def trigger_plan(body: PlanTriggerRequest):
logger.info("Getting the inputs") logger.info("Getting the inputs")
plan_input = read_csv_from_s3(bucket_name=get_settings().PLAN_TRIGGER_BUCKET, filepath=body.trigger_file_path) plan_input = read_csv_from_s3(bucket_name=get_settings().PLAN_TRIGGER_BUCKET, filepath=body.trigger_file_path)
# Check for duplicate UPRNS # Check for duplicate UPRNS
input_uprns = [x.get("uprn") for x in plan_input if "uprn" in x] input_uprns = [x.get("uprn") for x in plan_input if "uprn" in x and x.get("uprn")]
if input_uprns: if input_uprns:
# Check for dupes # Check for dupes
if len(input_uprns) != len(set(input_uprns)): if len(input_uprns) != len(set(input_uprns)):

View file

@ -2,6 +2,7 @@ import os
import time import time
import re import re
from etl.epc.settings import EARLIEST_EPC_DATE
from dotenv import load_dotenv from dotenv import load_dotenv
from tqdm import tqdm from tqdm import tqdm
import pandas as pd import pandas as pd
@ -236,7 +237,7 @@ def caha():
address = remap_address(address) address = remap_address(address)
find_epc_searcher = RetrieveFindMyEpc(address=address, postcode=postcode) find_epc_searcher = RetrieveFindMyEpc(address=address, postcode=postcode)
find_epc_data = find_epc_searcher.retrieve_newest_find_my_epc_data() find_epc_data = find_epc_searcher.retrieve_newest_find_my_epc_data(sap_2012_date=EARLIEST_EPC_DATE)
time.sleep(0.5) time.sleep(0.5)
# We need uprn # We need uprn
searcher = SearchEpc( searcher = SearchEpc(
@ -249,18 +250,102 @@ def caha():
searcher.find_property(skip_os=True) searcher.find_property(skip_os=True)
newest_epc = searcher.newest_epc newest_epc = searcher.newest_epc
uprn = newest_epc["uprn"]
if address in ["Flat D, 11 Victoria Avenue", "Flat B, 11 Victoria Avenue"]:
uprn = None
extracted_data.append( extracted_data.append(
{ {
"uprn": newest_epc["uprn"], "uprn": uprn,
**find_epc_data, **find_epc_data,
} }
) )
asset_list.append( asset_list.append(
{ {
"uprn": newest_epc["uprn"], "uprn": uprn,
"address": home["Address letter or number"], "address": address,
"postcode": home["Postcode"], "postcode": home["Postcode"],
"property_type": newest_epc["property-type"], "property_type": newest_epc["property-type"],
} }
) )
non_invasive_recommendations = [
{
"uprn": r["uprn"],
"recommendations": r["recommendations"]
} for r in extracted_data
]
# for r in non_invasive_recommendations:
# new_recommendations = []
# extracted = [r for r in extracted_data if r["uprn"] == r["uprn"]][0]
# for rec in r["recommendations"]:
# if extracted["hotwater-description"] == "Gas boiler/circulator, no cylinder thermostat":
# if rec["type"] in ["hot_water_tank_insulation", "cylinder_thermostat"]:
# continue
# rec["survey"] = False
# new_recommendations.append(rec)
# r["recommendations"] = new_recommendations
# We model the two properties separately
asset_list = pd.DataFrame(asset_list)
# Drop Flat D, 11 Victoria Avenue
asset_list1 = asset_list[asset_list["address"] != "Flat D, 11 Victoria Avenue"]
asset_list2 = asset_list[asset_list["address"] == "Flat D, 11 Victoria Avenue"]
# Store the asset list in s3
filename = f"{USER_ID}/{CAHA_PORTFOLIO_ID}/asset_list1.csv"
save_csv_to_s3(
dataframe=asset_list1,
bucket_name="retrofit-plan-inputs-dev",
file_name=filename
)
filename2 = f"{USER_ID}/{CAHA_PORTFOLIO_ID}/asset_list2.csv"
save_csv_to_s3(
dataframe=asset_list2,
bucket_name="retrofit-plan-inputs-dev",
file_name=filename2
)
# Store the non-invasive recommendations in s3
non_invasive_recommendations_filename = f"{USER_ID}/{CAHA_PORTFOLIO_ID}/non_invasive_recommendations.csv"
save_csv_to_s3(
dataframe=pd.DataFrame(non_invasive_recommendations),
bucket_name="retrofit-plan-inputs-dev",
file_name=non_invasive_recommendations_filename
)
body = {
"portfolio_id": str(CAHA_PORTFOLIO_ID),
"housing_type": "Social",
"goal": "Increasing EPC",
"goal_value": "C",
"trigger_file_path": filename,
"already_installed_file_path": "",
"patches_file_path": "",
"non_invasive_recommendations_file_path": non_invasive_recommendations_filename,
"valuation_file_path": "",
"scenario_name": "Wave 3 Packages",
"multi_plan": True,
"budget": None,
"exclusions": ["boiler_upgrade"]
}
print(body)
body2 = {
"portfolio_id": str(CAHA_PORTFOLIO_ID),
"housing_type": "Social",
"goal": "Increasing EPC",
"goal_value": "C",
"trigger_file_path": filename2,
"already_installed_file_path": "",
"patches_file_path": "",
"non_invasive_recommendations_file_path": non_invasive_recommendations_filename,
"valuation_file_path": "",
"scenario_name": "Wave 3 Packages",
"multi_plan": True,
"budget": None,
"exclusions": ["boiler_upgrade"]
}
print(body2)

View file

@ -1411,5 +1411,45 @@ def find_remaining_surveys():
assert needed.shape[0] + costed.shape[0] == surveyed.shape[0] assert needed.shape[0] + costed.shape[0] == surveyed.shape[0]
def append_stonewater_id():
"""
This completes an adhoc request from Stonewater to add in their organisation Reference onto the model
:return:
"""
model_proposed_sample = pd.read_excel(
"/Users/khalimconn-kowlessar/Documents/hestia/Customers/Stonewater/Stonewater - Bid Packages WIP 13.11.24.xlsx",
sheet_name="Modelled Packages",
header=13
)
model_proposed_sample = model_proposed_sample[~pd.isnull(model_proposed_sample["Address ID"])]
model_proposed_sample["Address ID"] = model_proposed_sample["Address ID"].astype(int)
original_archetypes = pd.read_excel(
"/Users/khalimconn-kowlessar/Documents/hestia/Customers/Stonewater/Stonewater SHDF_3_0_Board Triage 22.05.24 "
"- Archetyped V3.1.xlsx",
header=4
)
original_archetypes = original_archetypes[~pd.isnull(original_archetypes["Address ID"])]
original_archetypes = original_archetypes[original_archetypes["Address ID"] != "Address ID"]
original_archetypes["Address ID"] = original_archetypes["Address ID"].astype(int)
matched = model_proposed_sample.merge(
original_archetypes[["Address ID", 'Org. ref.']],
on="Address ID",
how="left"
)
if pd.isnull(matched["Org. ref."]).sum():
raise ValueError("Something went wrong")
# Save as CSV
matched.to_excel(
"/Users/khalimconn-kowlessar/Documents/hestia/Customers/Stonewater/Stonewater IDs.xlsx",
sheet_name="Proposed Wave 3 Sample",
index=False
)
# if __name__ == "__main__": # if __name__ == "__main__":
# main() # main()

View file

@ -1,3 +1,4 @@
import pandas as pd
import requests import requests
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from datetime import datetime from datetime import datetime
@ -25,7 +26,7 @@ class RetrieveFindMyEpc:
self.address_cleaned = self.address.replace(",", "").replace(" ", "").lower() self.address_cleaned = self.address.replace(",", "").replace(" ", "").lower()
def retrieve_newest_find_my_epc_data(self): def retrieve_newest_find_my_epc_data(self, sap_2012_date=None):
""" """
For a post code and address, we pull out all the required data from the find my epc website For a post code and address, we pull out all the required data from the find my epc website
""" """
@ -188,7 +189,7 @@ class RetrieveFindMyEpc:
raise ValueError(f"Missing key: {key}") raise ValueError(f"Missing key: {key}")
# Finally, we format the recommendations # Finally, we format the recommendations
recommendations = self.format_recommendations(recommendations) recommendations = self.format_recommendations(recommendations, assessment_data, sap_2012_date)
resulting_data = { resulting_data = {
'epc_certificate': epc_certificate, 'epc_certificate': epc_certificate,
@ -205,11 +206,12 @@ class RetrieveFindMyEpc:
return resulting_data return resulting_data
@staticmethod @staticmethod
def format_recommendations(recommendations): def format_recommendations(recommendations, assessment_data, sap_2012_date=None):
""" """
This function converts the recommendations to a format that we can use in the engine as a non-intrusive survey This function converts the recommendations to a format that we can use in the engine as a non-intrusive survey
:param recommendations: :param recommendations: The recommendations from the EPC
:return: :param assessment_data: The assessment data from the EPC
:param sap_2012_date: The date of the SAP 2012 update
""" """
measure_map = { measure_map = {
@ -246,17 +248,23 @@ class RetrieveFindMyEpc:
"Double glazing": ["double_glazing"], "Double glazing": ["double_glazing"],
} }
survey = True
if sap_2012_date is not None:
certificate_date = datetime.strptime(assessment_data["Date of certificate"], "%d %B %Y")
if certificate_date < pd.to_datetime(sap_2012_date):
survey = False
formatted_recommendations = [] formatted_recommendations = []
for rec in recommendations: for rec in recommendations:
mapped = measure_map[rec["measure"]] mapped = measure_map[rec["measure"]]
for measure in mapped: for measure in mapped:
formatted_recommendations.append( to_append = {
{ "type": measure,
"type": measure, "sap_points": rec["sap_points"],
"sap_points": rec["sap_points"], "survey": survey,
"survey": True }
} if measure == "solar_pv":
) to_append["suitable"] = True
formatted_recommendations.append(to_append)
return formatted_recommendations return formatted_recommendations

View file

@ -60,15 +60,21 @@ class HotwaterRecommendations:
# If there is no system present, but access to the mains, we # If there is no system present, but access to the mains, we
has_tank_recommendation = [r for r in self.recommendations if r["type"] == "hot_water_tank_insulation"]
if ( if (
(self.property.hotwater["heater_type"] in ["electric immersion"]) & (self.property.hotwater["heater_type"] in ["electric immersion"]) &
(self.property.data["hot-water-energy-eff"] == "Very Poor") & (self.property.data["hot-water-energy-eff"] == "Very Poor") &
(self.property.hotwater["no_system_present"] is None) (self.property.hotwater["no_system_present"] is None) &
len(has_tank_recommendation) == 0
): ):
self.recommend_tank_insulation(phase=phase) self.recommend_tank_insulation(phase=phase)
return return
if self.property.hotwater["clean_description"] == "From main system, no cylinder thermostat": has_cylinder_recommendation = [r for r in self.recommendations if r["type"] == "cylinder_thermostat"]
if ((self.property.hotwater["clean_description"] == "From main system, no cylinder thermostat") &
(len(has_cylinder_recommendation) == 0)):
self.recommend_cylinder_thermostat(phase=phase) self.recommend_cylinder_thermostat(phase=phase)
return return

View file

@ -13,7 +13,7 @@ class SecondaryHeating:
ACCEPTED_MAINHEAT_DESCRIPTIONS = ["Boiler and radiators, mains gas"] ACCEPTED_MAINHEAT_DESCRIPTIONS = ["Boiler and radiators, mains gas"]
ACCEPTED_SECONDHEAT_DESCRIPTIONS = ["Room heaters, electric"] ACCEPTED_SECONDHEAT_DESCRIPTIONS = ["Room heaters, electric"]
# These are the heaters where works are required to remove them # These are the heaters where works are required to remove them
FIXED_HEATER_DESCRIPTIONS = ["Room heaters, electric"] FIXED_HEATER_DESCRIPTIONS = ["Room heaters, electric", 'Portable electric heaters (assumed)']
def __init__(self, property_instance: Property): def __init__(self, property_instance: Property):
self.property = property_instance self.property = property_instance
@ -34,7 +34,7 @@ class SecondaryHeating:
if self.property.data['secondheat-description'] in self.FIXED_HEATER_DESCRIPTIONS: if self.property.data['secondheat-description'] in self.FIXED_HEATER_DESCRIPTIONS:
# We have an associated cost otherwise, there is no cost # We have an associated cost otherwise, there is no cost
n_rooms = self.property.data['number-heated-rooms'] n_rooms = self.property.data['number-habitable-rooms'] - self.property.data['number-heated-rooms']
else: else:
n_rooms = 0 n_rooms = 0