mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
commit
ee0edec07c
11 changed files with 617 additions and 21 deletions
2
.idea/Model.iml
generated
2
.idea/Model.iml
generated
|
|
@ -7,7 +7,7 @@
|
|||
<sourceFolder url="file://$MODULE_DIR$/open_uprn" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/recommendations" isTestSource="false" />
|
||||
</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" />
|
||||
</component>
|
||||
<component name="PyNamespacePackagesService">
|
||||
|
|
|
|||
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
|
|
@ -3,7 +3,7 @@
|
|||
<component name="Black">
|
||||
<option name="sdkName" value="Python 3.10 (backend)" />
|
||||
</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">
|
||||
<option name="shown" value="true" />
|
||||
</component>
|
||||
|
|
|
|||
295
backend/Funding.py
Normal file
295
backend/Funding.py
Normal file
|
|
@ -0,0 +1,295 @@
|
|||
import pandas as pd
|
||||
import numpy as np
|
||||
from typing import List
|
||||
|
||||
from backend.app.plan.schemas import HousingType
|
||||
|
||||
|
||||
class Funding:
|
||||
"""
|
||||
Given a property, this class identifies if the home is possibly eligible for funding under
|
||||
the various funding schemes. It will also calculate the expected amount of funding available
|
||||
and flag any tenant specific requirements that need to be considered to the funding to be attained
|
||||
"""
|
||||
|
||||
ECO_SAP_SCORE_THREHOLDS = [
|
||||
{'Band': 'High_A', 'From': 96.0, 'Up to': 100.0, 'Mid-point': 98.0},
|
||||
{'Band': 'Low_A', 'From': 92.0, 'Up to': 96.0, 'Mid-point': 94.0},
|
||||
{'Band': 'High_B', 'From': 86.0, 'Up to': 91.0, 'Mid-point': 88.5},
|
||||
{'Band': 'Low_B', 'From': 81.0, 'Up to': 86.0, 'Mid-point': 83.5},
|
||||
{'Band': 'High_C', 'From': 74.5, 'Up to': 80.0, 'Mid-point': 77.25},
|
||||
{'Band': 'Low_C', 'From': 69.0, 'Up to': 74.5, 'Mid-point': 71.75},
|
||||
{'Band': 'High_D', 'From': 61.5, 'Up to': 68.0, 'Mid-point': 64.75},
|
||||
{'Band': 'Low_D', 'From': 55.0, 'Up to': 61.5, 'Mid-point': 58.25},
|
||||
{'Band': 'High_E', 'From': 46.5, 'Up to': 54.0, 'Mid-point': 50.25},
|
||||
{'Band': 'Low_E', 'From': 39.0, 'Up to': 46.5, 'Mid-point': 42.75},
|
||||
{'Band': 'High_F', 'From': 29.5, 'Up to': 38.0, 'Mid-point': 33.75},
|
||||
{'Band': 'Low_F', 'From': 21.0, 'Up to': 29.5, 'Mid-point': 25.25},
|
||||
{'Band': 'High_G', 'From': 10.5, 'Up to': 20.0, 'Mid-point': 15.25},
|
||||
{'Band': 'Low_G', 'From': 1.0, 'Up to': 10.5, 'Mid-point': 5.75}
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
tenure: HousingType,
|
||||
starting_epc,
|
||||
starting_sap,
|
||||
floor_area,
|
||||
council_tax_band,
|
||||
property_recommendations,
|
||||
project_scores_matrix,
|
||||
gbis_abs_rate: int,
|
||||
eco4_abs_rate: int,
|
||||
):
|
||||
"""
|
||||
Use Pydantic to validate the parameter types
|
||||
:param tenure: Indicates if the property is a social or private home
|
||||
:param starting_epc: The current EPC rating of the property
|
||||
:param starting_sap: The current SAP score for the property
|
||||
:param floor_area: The total floor area of the property
|
||||
:param gbis_abs_rate: The assumed £/abs achieved by the installer for GBIS
|
||||
:param eco4_abs_rate: The assumed £/abs achieved by the installer for ECO4
|
||||
"""
|
||||
|
||||
# TODO: Things we need to include:
|
||||
# 1) Amount of funding
|
||||
# 2) Fundable measures, as a subset of measures may be fundable, not all
|
||||
|
||||
self.tenure = tenure
|
||||
self.starting_epc = starting_epc
|
||||
self.starting_sap = starting_sap
|
||||
self.starting_eco_band = self.sap_to_eco_band(self.starting_sap)
|
||||
self.floor_area_segment = self.classify_floor_area(floor_area)
|
||||
self.gbis_abs_rate = gbis_abs_rate
|
||||
self.eco4_abs_rate = eco4_abs_rate
|
||||
self.council_tax_band = council_tax_band
|
||||
|
||||
self.recommendations = property_recommendations
|
||||
|
||||
self.measure_types = list({r["measure_type"] for r in property_recommendations if r["default"]})
|
||||
|
||||
# Load in the eco4 project scores matrix
|
||||
# Filter the matrix on scores relevant to this property
|
||||
self.project_scores_matrix = project_scores_matrix[
|
||||
(project_scores_matrix["Floor Area Segment"] == self.floor_area_segment) &
|
||||
(project_scores_matrix["Starting Band"] == self.starting_eco_band)
|
||||
]
|
||||
|
||||
# Store the final outputs
|
||||
self.gbis_eligibiltiy = {}
|
||||
self.eco4_eligibility = {}
|
||||
self.whlg_eligibility = {}
|
||||
|
||||
def output(
|
||||
self,
|
||||
measure_types: List[str],
|
||||
estimated_funding: float,
|
||||
notify_tenant_benefits_requirements: bool,
|
||||
notify_council_tax_band_requirements: bool,
|
||||
notify_tenant_low_income_requirements: bool,
|
||||
):
|
||||
""""
|
||||
"""
|
||||
return {
|
||||
"measure_types": measure_types,
|
||||
"estimated_funding": estimated_funding,
|
||||
"notify_tenant_benefits_requirements": notify_tenant_benefits_requirements,
|
||||
"notify_council_tax_band_requirements": notify_council_tax_band_requirements,
|
||||
"notify_tenant_low_income_requirements": notify_tenant_low_income_requirements
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def classify_floor_area(floor_area):
|
||||
if floor_area <= 72:
|
||||
return "0-72"
|
||||
|
||||
if floor_area <= 97:
|
||||
return "73-97"
|
||||
|
||||
if floor_area <= 199:
|
||||
return "98-199"
|
||||
|
||||
return "200"
|
||||
|
||||
def eco4(self):
|
||||
"""
|
||||
Checks if a property is eligible for ECO4
|
||||
:return:
|
||||
"""
|
||||
pass
|
||||
|
||||
def find_best_gbis_measure(self, measures):
|
||||
"""
|
||||
The best measure is one that:
|
||||
1) Creates some SAP movement, therefore enables eligiblity
|
||||
2) Generates the most funding
|
||||
3) Has a reasonable ROI
|
||||
:return:
|
||||
"""
|
||||
measure_table = pd.DataFrame([
|
||||
m for m in self.recommendations if m in measures and m["default"]
|
||||
])
|
||||
|
||||
measure_table["post_install_sap"] = measure_table["sap_points"] + self.starting_sap
|
||||
# We classify the movement
|
||||
measure_table["Finishing Band"] = np.floor(measure_table["post_install_sap"]).apply(
|
||||
lambda points: self.sap_to_eco_band(points)
|
||||
)
|
||||
# Remove any measures that generate zero SAP movement
|
||||
measure_table = measure_table[measure_table["Finishing Band"] != self.starting_eco_band]
|
||||
|
||||
if measure_table.empty:
|
||||
raise NotImplementedError("No measures available, handle me!")
|
||||
|
||||
# We merge on the project matrix, on post install band
|
||||
measure_table = measure_table.merge(
|
||||
self.project_scores_matrix, how="left", on="Finishing Band"
|
||||
)
|
||||
# Cost Savings is the abs
|
||||
measure_table["estimated_funding"] = measure_table["Cost Savings"] * self.gbis_abs_rate
|
||||
# We cap any estimated funding at the install cost
|
||||
measure_table["estimated_funding"] = np.where(
|
||||
measure_table["estimated_funding"] >= measure_table["total"],
|
||||
measure_table["total"],
|
||||
measure_table["estimated_funding"]
|
||||
)
|
||||
|
||||
# Sort by the measure that will cost the client the least, per sap point
|
||||
measure_table["cost_minus_funding"] = measure_table["total"] - measure_table["estimated_funding"]
|
||||
measure_table["cost_minus_funding_per_sap"] = measure_table["cost_minus_funding"] / measure_table["sap_points"]
|
||||
measure_table = measure_table.sort_values(["cost_minus_funding_per_sap", "total"], ascending=[True, False])
|
||||
# Recommend the measure, with estimated funding amount
|
||||
recommended_measure = measure_table.head(1)
|
||||
|
||||
return {
|
||||
"measure_type": recommended_measure["measure_type"],
|
||||
"estimated_funding": recommended_measure["estimated_funding"]
|
||||
}
|
||||
|
||||
def sap_to_eco_band(self, sap_points):
|
||||
"""
|
||||
Giuven a sap point score, this function will classify the points into the SAP half-band
|
||||
:param sap_points:
|
||||
:return:
|
||||
"""
|
||||
|
||||
if sap_points > 100:
|
||||
return "High_A"
|
||||
|
||||
classification = [
|
||||
x for x in self.ECO_SAP_SCORE_THREHOLDS if (x["From"] <= sap_points) and (sap_points <= x["Up to"])
|
||||
]
|
||||
|
||||
if len(classification) != 1:
|
||||
raise Exception("We should have a single classifcation for SAP points to half band")
|
||||
|
||||
return classification[0]['Band']
|
||||
|
||||
def gbis_prs(self):
|
||||
"""
|
||||
Checks if a private rental is eligible for GBIS. There are the following possible options
|
||||
1) General Eligibilty, contigent on EPC D-G and council tax band A-D. Excludes CWI, LI and heating
|
||||
controls
|
||||
2) Low income group - contigent on EPC D-G and tenant must receive benefits. Excludes heating controls
|
||||
3) GBIS Flex route 1, 3 - Great British Insulation Scheme Routes 1 and 3 are for pre-installation
|
||||
SAP bands D-G for owner-occupied households, D-E for private rented sector households
|
||||
(Including F & G if exempt from MEES). If houseold is low income. Excludes heating controls
|
||||
4) GBIS Flex route 2 - EPC E - G and low income household. Excludes heating controls
|
||||
|
||||
Eligible measures:
|
||||
• Solid wall
|
||||
• pitched roof
|
||||
• flat roof
|
||||
• under floor
|
||||
• solid floor park home and
|
||||
• room in-roof insulation
|
||||
|
||||
:return:
|
||||
"""
|
||||
|
||||
valid_measures = [
|
||||
"internal_wall_insulation",
|
||||
"external_wall_insulation",
|
||||
"flat_roof_insulation",
|
||||
"suspended_floor_insulation",
|
||||
"room_roof_insulation",
|
||||
# Not available for every eligiblity type
|
||||
"cavity_wall_insulation",
|
||||
"loft_insulation",
|
||||
]
|
||||
|
||||
# General Eligibility
|
||||
if (
|
||||
(self.starting_epc in ["G", "D", "E", "F"]) and
|
||||
any(
|
||||
[measure in valid_measures for measure in self.measure_types
|
||||
if measure not in ["cavity_wall_insulation", "loft_insulation"]]
|
||||
) and
|
||||
(self.council_tax_band in [None, "A", "B", "C", "D"])
|
||||
):
|
||||
# We find the best measure for GBIS
|
||||
recommended_measure = self.find_best_gbis_measure(
|
||||
measures=[m for m in valid_measures if m not in ["cavity_wall_insulation", "loft_insulation"]]
|
||||
)
|
||||
# If the council tax band is missing, we nofify the customer that this is a requirement that
|
||||
# should be checked
|
||||
return self.output(
|
||||
measure_types=[recommended_measure["measure_type"]],
|
||||
estimated_funding=recommended_measure["estimated_funding"],
|
||||
notify_tenant_benefits_requirements=False,
|
||||
notify_council_tax_band_requirements=self.council_tax_band is None,
|
||||
notify_tenant_low_income_requirements=False,
|
||||
)
|
||||
|
||||
# Low income/flex
|
||||
if (
|
||||
(self.starting_sap in ["G", "D", "E", "F"]) and
|
||||
any([measure in valid_measures for measure in self.measure_types])
|
||||
):
|
||||
# Find the best measure, and can also include CWI/LI but requires the tenant to be
|
||||
# low inome or on benefits
|
||||
# We find the best measure for GBIS
|
||||
recommended_measure = self.find_best_gbis_measure(measures=valid_measures)
|
||||
return self.output(
|
||||
measure_types=[recommended_measure["measure_type"]],
|
||||
estimated_funding=recommended_measure["estimated_funding"],
|
||||
notify_tenant_benefits_requirements=True,
|
||||
notify_council_tax_band_requirements=False,
|
||||
notify_tenant_low_income_requirements=True,
|
||||
)
|
||||
|
||||
# Otherwise, no funding availability
|
||||
return self.output(
|
||||
measure_types=[],
|
||||
estimated_funding=0,
|
||||
notify_tenant_benefits_requirements=False,
|
||||
notify_council_tax_band_requirements=False,
|
||||
notify_tenant_low_income_requirements=False
|
||||
)
|
||||
|
||||
def gbis(self):
|
||||
"""
|
||||
Check if a property is eligible for GBIS
|
||||
:return:
|
||||
"""
|
||||
|
||||
if self.tenure == "Private":
|
||||
self.gbis_eligibiltiy = self.gbis_prs()
|
||||
return
|
||||
|
||||
raise NotImplementedError("Implement social/oo")
|
||||
|
||||
def eco4(self):
|
||||
if self.tenure == "Private":
|
||||
self.eco4_eligibiltiy = self.eco4_prs()
|
||||
return
|
||||
|
||||
def check_eligibiltiy(self):
|
||||
"""
|
||||
This function instigates the checking process
|
||||
:return:
|
||||
"""
|
||||
|
||||
self.gbis()
|
||||
# self.eco4()
|
||||
# self.whlg()
|
||||
|
|
@ -22,6 +22,7 @@ from recommendations.recommendation_utils import (
|
|||
)
|
||||
from backend.ml_models.AnnualBillSavings import AnnualBillSavings
|
||||
from backend.app.utils import sap_to_epc
|
||||
from backend.Funding import Funding
|
||||
import backend.app.assumptions as assumptions
|
||||
|
||||
ENVIRONMENT = os.environ.get("ENVIRONMENT", "dev")
|
||||
|
|
@ -202,6 +203,11 @@ class Property:
|
|||
# TODO: We keep this but only temporarily until we add bathrooms, bedrooms, building id to the condition data
|
||||
self.parse_kwargs(kwargs)
|
||||
|
||||
# Funding
|
||||
self.gbis_eligibiltiy = None
|
||||
self.eco4_eligibility = None
|
||||
self.whlg_eligibility = None
|
||||
|
||||
@classmethod
|
||||
def extract_kwargs(cls, kwargs):
|
||||
"""
|
||||
|
|
@ -1306,3 +1312,11 @@ class Property:
|
|||
)
|
||||
|
||||
return electric_consumption
|
||||
|
||||
def insert_funding(self, funding_calulator: Funding):
|
||||
"""
|
||||
This method inserts the funding into the property object
|
||||
"""
|
||||
self.gbis_eligibiltiy = funding_calulator.gbis_eligibiltiy
|
||||
self.eco4_eligibility = funding_calulator.eco4_eligibility
|
||||
self.whlg_eligibility = funding_calulator.whlg_eligibility
|
||||
|
|
|
|||
|
|
@ -138,7 +138,7 @@ def upload_recommendations(session: Session, recommendations_to_upload, property
|
|||
"recommendation_id": recommendation_id,
|
||||
"material_id": part["id"],
|
||||
"depth": int(part["depth"]) if part["depth"] else None,
|
||||
"quantity": part["quantity"],
|
||||
"quantity": float(part["quantity"]),
|
||||
"quantity_unit": part["quantity_unit"],
|
||||
"estimated_cost": part["total"],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ from backend.app.utils import epc_to_sap_lower_bound, sap_to_epc
|
|||
|
||||
from backend.ml_models.api import ModelApi
|
||||
from backend.Property import Property
|
||||
from backend.Funding import Funding
|
||||
from backend.apis.GoogleSolarApi import GoogleSolarApi
|
||||
|
||||
from recommendations.optimiser.CostOptimiser import CostOptimiser
|
||||
|
|
@ -373,6 +374,17 @@ def extract_property_request_data(
|
|||
return patch, property_already_installed, property_non_invasive_recommendations, property_valution
|
||||
|
||||
|
||||
def get_eco_project_scores_matrix():
|
||||
data = read_csv_from_s3(
|
||||
bucket_name=get_settings().DATA_BUCKET,
|
||||
filepath="funding/ECO4 Full Project Scores Matrix.csv",
|
||||
)
|
||||
df = pd.DataFrame(data)
|
||||
df.columns = ['Floor Area Segment', 'Starting Band', 'Finishing Band', 'Cost Savings']
|
||||
df["Cost Savings"] = df["Cost Savings"].astype(float)
|
||||
return df
|
||||
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/plan",
|
||||
tags=["plan"],
|
||||
|
|
@ -438,6 +450,12 @@ async def trigger_plan(body: PlanTriggerRequest):
|
|||
if not is_new and not body.multi_plan:
|
||||
continue
|
||||
|
||||
if epc_searcher.newest_epc is None:
|
||||
raise ValueError(
|
||||
"No EPCs found for this property and did not estimate - likely need to provide a"
|
||||
"property type and built form"
|
||||
)
|
||||
|
||||
if is_new:
|
||||
create_property_targets(
|
||||
session,
|
||||
|
|
@ -508,6 +526,7 @@ async def trigger_plan(body: PlanTriggerRequest):
|
|||
logger.info("Reading in materials and cleaned datasets")
|
||||
materials = get_materials(session)
|
||||
cleaned = get_cleaned()
|
||||
eco_project_scores_matrix = get_eco_project_scores_matrix()
|
||||
|
||||
kwh_client = KwhData(bucket=get_settings().DATA_BUCKET, read_consumption_data=True)
|
||||
|
||||
|
|
@ -730,6 +749,26 @@ async def trigger_plan(body: PlanTriggerRequest):
|
|||
]
|
||||
recommendations[p.id] = final_recommendations
|
||||
|
||||
# ~~~~~~~~~~~~~~~~
|
||||
# Funding
|
||||
# ~~~~~~~~~~~~~~~~
|
||||
|
||||
# for p in input_properties:
|
||||
# funding_calulator = Funding(
|
||||
# tenure=body.housing_type,
|
||||
# starting_epc=p.data["current-energy-rating"],
|
||||
# starting_sap=int(p.data["current-energy-efficiency"]),
|
||||
# floor_area=p.floor_area,
|
||||
# council_tax_band=None, # This is seemingly always None at the moment
|
||||
# property_recommendations=recommendations[p.id],
|
||||
# project_scores_matrix=eco_project_scores_matrix,
|
||||
# gbis_abs_rate=20,
|
||||
# eco4_abs_rate=20,
|
||||
# )
|
||||
# funding_calulator.check_eligibiltiy()
|
||||
# # Insert finding
|
||||
# p.insert_funding(funding_calulator)
|
||||
|
||||
logger.info("Uploading recommendations to the database")
|
||||
# If we have any work to do, we create a new scenario
|
||||
engine_scenario = create_scenario(
|
||||
|
|
|
|||
138
etl/customers/cambridge/remote_assessment.py
Normal file
138
etl/customers/cambridge/remote_assessment.py
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
import os
|
||||
import time
|
||||
|
||||
from tqdm import tqdm
|
||||
import pandas as pd
|
||||
from dotenv import load_dotenv
|
||||
from etl.find_my_epc.RetrieveFindMyEpc import RetrieveFindMyEpc
|
||||
from backend.SearchEpc import SearchEpc
|
||||
from utils.s3 import save_csv_to_s3
|
||||
|
||||
load_dotenv(dotenv_path="backend/.env")
|
||||
EPC_AUTH_TOKEN = os.getenv("EPC_AUTH_TOKEN")
|
||||
USER_ID = 8
|
||||
PORTFOLIO_ID = 122
|
||||
|
||||
|
||||
def app():
|
||||
asset_list = [
|
||||
{
|
||||
"address": "12 Church Lane", "postcode": "CB23 8AF", "uprn": 100090136018,
|
||||
"property_type": "House", "built-form": "Semi-Detached"
|
||||
},
|
||||
{
|
||||
"address": "21 High Street", "postcode": "CB23 8AB", "uprn": 100090136026
|
||||
},
|
||||
{
|
||||
"address": "22 High Street", "postcode": "CB23 8AB", "uprn": 100090136027
|
||||
},
|
||||
{
|
||||
"address": "5 Bunkers Hill", "postcode": "CB3 0LY", "uprn": 10008078615
|
||||
},
|
||||
{
|
||||
"address": "6 Bunkers Hill", "postcode": "CB3 0LY", "uprn": 10008078616
|
||||
},
|
||||
{
|
||||
"address": "7 Bunkers Hill", "postcode": "CB3 0LY", "uprn": 10008078617
|
||||
},
|
||||
{
|
||||
"address": "32 George Nuttall Close", "postcode": "CB4 1YE", "uprn": 200004200075
|
||||
},
|
||||
{
|
||||
"address": "33 George Nuttall Close", "postcode": "CB4 1YE", "uprn": 200004200076
|
||||
},
|
||||
{
|
||||
"address": "35 George Nuttall Close", "postcode": "CB4 1YE", "uprn": 200004200078
|
||||
},
|
||||
{
|
||||
"address": "36 George Nuttall Close", "postcode": "CB4 1YE", "uprn": 200004200079
|
||||
}
|
||||
]
|
||||
asset_list = pd.DataFrame(asset_list)
|
||||
|
||||
valuations_data = [
|
||||
{'uprn': 100090136018, "valuation": 586_000},
|
||||
{'uprn': 100090136026, "valuation": 551_000},
|
||||
{'uprn': 100090136027, "valuation": 844_000},
|
||||
{'uprn': 10008078615, "valuation": 763_000},
|
||||
{'uprn': 10008078616, "valuation": 616_000},
|
||||
{'uprn': 10008078617, "valuation": 593_000},
|
||||
{'uprn': 200004200075, "valuation": 450_000},
|
||||
{'uprn': 200004200076, "valuation": 457_000},
|
||||
{'uprn': 200004200078, "valuation": 304_000},
|
||||
{'uprn': 200004200079, "valuation": 313_000}
|
||||
]
|
||||
|
||||
# Pull the additional data
|
||||
extracted_data = []
|
||||
for _, home in tqdm(asset_list.iterrows(), total=len(asset_list)):
|
||||
add1 = home["address"]
|
||||
pc = home["postcode"]
|
||||
# Retrieve the EPC data
|
||||
epc_searcher = SearchEpc(
|
||||
address1=add1,
|
||||
postcode=pc, uprn=home["uprn"], auth_token=EPC_AUTH_TOKEN, os_api_key=""
|
||||
)
|
||||
epc_searcher.find_property(skip_os=True)
|
||||
if epc_searcher.newest_epc is None:
|
||||
continue
|
||||
|
||||
find_epc_searcher = RetrieveFindMyEpc(address=epc_searcher.newest_epc["address1"],
|
||||
postcode=epc_searcher.newest_epc["postcode"])
|
||||
find_epc_data = find_epc_searcher.retrieve_newest_find_my_epc_data()
|
||||
time.sleep(0.5)
|
||||
# We need uprn
|
||||
|
||||
extracted_data.append(
|
||||
{
|
||||
"uprn": home["uprn"],
|
||||
**find_epc_data,
|
||||
}
|
||||
)
|
||||
|
||||
non_invasive_recommendations = [
|
||||
{
|
||||
"uprn": r["uprn"],
|
||||
"recommendations": r["recommendations"]
|
||||
} for r in extracted_data
|
||||
]
|
||||
|
||||
filename = f"{USER_ID}/{PORTFOLIO_ID}/asset_list.csv"
|
||||
save_csv_to_s3(
|
||||
dataframe=pd.DataFrame(asset_list),
|
||||
bucket_name="retrofit-plan-inputs-dev",
|
||||
file_name=filename
|
||||
)
|
||||
|
||||
# Store the non-invasive recommendations in s3
|
||||
non_invasive_recommendations_filename = f"{USER_ID}/{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
|
||||
)
|
||||
|
||||
# Store the valuations data in s3
|
||||
valuations_filename = f"{USER_ID}/{PORTFOLIO_ID}/valuations.csv"
|
||||
save_csv_to_s3(
|
||||
dataframe=pd.DataFrame(valuations_data),
|
||||
bucket_name="retrofit-plan-inputs-dev",
|
||||
file_name=valuations_filename
|
||||
)
|
||||
|
||||
body = {
|
||||
"portfolio_id": str(PORTFOLIO_ID),
|
||||
"housing_type": "Private",
|
||||
"goal": "Increasing EPC",
|
||||
"goal_value": "B",
|
||||
"trigger_file_path": filename,
|
||||
"already_installed_file_path": "",
|
||||
"patches_file_path": "",
|
||||
"non_invasive_recommendations_file_path": non_invasive_recommendations_filename,
|
||||
"valuation_file_path": valuations_filename,
|
||||
"scenario_name": "Wave 3 Packages",
|
||||
"multi_plan": True,
|
||||
"budget": None,
|
||||
"exclusions": []
|
||||
}
|
||||
print(body)
|
||||
108
etl/customers/connells/pilot_remote_assessments.py
Normal file
108
etl/customers/connells/pilot_remote_assessments.py
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
import os
|
||||
import time
|
||||
|
||||
from tqdm import tqdm
|
||||
import pandas as pd
|
||||
from dotenv import load_dotenv
|
||||
from etl.find_my_epc.RetrieveFindMyEpc import RetrieveFindMyEpc
|
||||
from backend.SearchEpc import SearchEpc
|
||||
from utils.s3 import save_csv_to_s3
|
||||
|
||||
load_dotenv(dotenv_path="backend/.env")
|
||||
EPC_AUTH_TOKEN = os.getenv("EPC_AUTH_TOKEN")
|
||||
USER_ID = 8
|
||||
PORTFOLIO_ID = 123
|
||||
|
||||
|
||||
def app():
|
||||
asset_list = [
|
||||
{"address": "1 Raven Crescent", "postcode": "WV11 2EX", "uprn": 100071188496},
|
||||
|
||||
{"address": "13 Bayliss Avenue", "postcode": "WV11 2EX", "uprn": 100071136271},
|
||||
|
||||
{"address": "30 Southbourne Road", "postcode": "WV10 6ET", "uprn": 100071194376},
|
||||
|
||||
{"address": "96 Marsh Lane", "postcode": "WV10 6RX", "uprn": 100071176297},
|
||||
]
|
||||
asset_list = pd.DataFrame(asset_list)
|
||||
|
||||
valuations_data = [
|
||||
{'uprn': 100071188496, "valuation": 175_000},
|
||||
{'uprn': 100071136271, "valuation": 183_000},
|
||||
{'uprn': 100071194376, "valuation": 221_000},
|
||||
{'uprn': 100071176297, "valuation": 208_000},
|
||||
]
|
||||
|
||||
# Pull the additional data
|
||||
extracted_data = []
|
||||
for _, home in tqdm(asset_list.iterrows(), total=len(asset_list)):
|
||||
add1 = home["address"]
|
||||
pc = home["postcode"]
|
||||
# Retrieve the EPC data
|
||||
epc_searcher = SearchEpc(
|
||||
address1=add1,
|
||||
postcode=pc, uprn=home["uprn"], auth_token=EPC_AUTH_TOKEN, os_api_key=""
|
||||
)
|
||||
epc_searcher.find_property(skip_os=True)
|
||||
if epc_searcher.newest_epc is None:
|
||||
continue
|
||||
|
||||
find_epc_searcher = RetrieveFindMyEpc(address=epc_searcher.newest_epc["address1"],
|
||||
postcode=epc_searcher.newest_epc["postcode"])
|
||||
find_epc_data = find_epc_searcher.retrieve_newest_find_my_epc_data()
|
||||
time.sleep(0.5)
|
||||
# We need uprn
|
||||
|
||||
extracted_data.append(
|
||||
{
|
||||
"uprn": home["uprn"],
|
||||
**find_epc_data,
|
||||
}
|
||||
)
|
||||
|
||||
non_invasive_recommendations = [
|
||||
{
|
||||
"uprn": r["uprn"],
|
||||
"recommendations": r["recommendations"]
|
||||
} for r in extracted_data
|
||||
]
|
||||
|
||||
filename = f"{USER_ID}/{PORTFOLIO_ID}/asset_list.csv"
|
||||
save_csv_to_s3(
|
||||
dataframe=pd.DataFrame(asset_list),
|
||||
bucket_name="retrofit-plan-inputs-dev",
|
||||
file_name=filename
|
||||
)
|
||||
|
||||
# Store the non-invasive recommendations in s3
|
||||
non_invasive_recommendations_filename = f"{USER_ID}/{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
|
||||
)
|
||||
|
||||
# Store the valuations data in s3
|
||||
valuations_filename = f"{USER_ID}/{PORTFOLIO_ID}/valuations.csv"
|
||||
save_csv_to_s3(
|
||||
dataframe=pd.DataFrame(valuations_data),
|
||||
bucket_name="retrofit-plan-inputs-dev",
|
||||
file_name=valuations_filename
|
||||
)
|
||||
|
||||
body = {
|
||||
"portfolio_id": str(PORTFOLIO_ID),
|
||||
"housing_type": "Private",
|
||||
"goal": "Increasing EPC",
|
||||
"goal_value": "B",
|
||||
"trigger_file_path": filename,
|
||||
"already_installed_file_path": "",
|
||||
"patches_file_path": "",
|
||||
"non_invasive_recommendations_file_path": non_invasive_recommendations_filename,
|
||||
"valuation_file_path": valuations_filename,
|
||||
"scenario_name": "Wave 3 Packages",
|
||||
"multi_plan": True,
|
||||
"budget": None,
|
||||
"exclusions": []
|
||||
}
|
||||
print(body)
|
||||
|
|
@ -2826,9 +2826,10 @@ def identify_incorrect_packages():
|
|||
"estimated": "EPC Estimated based on Nearby Properties"
|
||||
}
|
||||
)
|
||||
# Find entries where the SAP score is not an integer
|
||||
non_integer_sap = epc_data_to_append[~epc_data_to_append["EPC: SAP Score"].astype(str).str.isnumeric()]
|
||||
non_integer_sap["UPRN"].values[0]
|
||||
# Take non-estimated EPCs?
|
||||
# epc_data_to_append = epc_data_to_append[epc_data_to_append["EPC Estimated based on Nearby Properties"] != True]
|
||||
# Take the newest EPC per UPRN, based on lodgement date
|
||||
epc_data_to_append = epc_data_to_append.sort_values("EPC: Date of EPC", ascending=False).drop_duplicates("UPRN")
|
||||
|
||||
epc_data_to_append["EPC: Date of EPC"] = pd.to_datetime(epc_data_to_append["EPC: Date of EPC"])
|
||||
# Years since the EPC was lodged
|
||||
|
|
|
|||
|
|
@ -758,32 +758,31 @@ class Costs:
|
|||
else:
|
||||
system_cost = [c for c in INSTALLER_SOLAR_COSTS if c["n_panels"] == n_panels][0]["cost"]
|
||||
|
||||
total_cost = array_cost if array_cost is not None else system_cost
|
||||
subtotal = array_cost if array_cost is not None else system_cost
|
||||
|
||||
if has_battery:
|
||||
battery_cost = [c for c in INSTALLER_SOLAR_BATTERY_COSTS if c["capacity_kwh"] == battery_kwh][0]["cost"]
|
||||
total_cost += battery_cost
|
||||
subtotal += battery_cost
|
||||
|
||||
scaffolding_cost = [c for c in INSTALLER_SCAFFOLDING_COSTS if c["stories"] == n_floors][0]["cost"]
|
||||
total_cost += scaffolding_cost
|
||||
subtotal += scaffolding_cost
|
||||
|
||||
if needs_inverter:
|
||||
total_cost += INSTALLER_SOLAR_PV_INVERTER_COST
|
||||
subtotal += INSTALLER_SOLAR_PV_INVERTER_COST
|
||||
# We also add an additional labour cost
|
||||
total_cost += INSTALLER_SOLAR_PV_INVERTER_LABOUR_COST
|
||||
subtotal += INSTALLER_SOLAR_PV_INVERTER_LABOUR_COST
|
||||
|
||||
# We add an additional cost for scaffolding
|
||||
|
||||
subtotal_before_vat = total_cost / (1 + self.VAT_RATE)
|
||||
|
||||
vat = total_cost - subtotal_before_vat
|
||||
# The costs from installers exclude VAT
|
||||
vat = subtotal * self.VAT_RATE
|
||||
total_cost = subtotal + vat
|
||||
|
||||
# Labour hours are based on estimates from online research but an average team seems to consist of 3 people
|
||||
# and most jobs take around 2 days. Assuming an 8 hour day for 3 people across 2 days, gives us 48 hours of
|
||||
# labour
|
||||
return {
|
||||
"total": total_cost,
|
||||
"subtotal": subtotal_before_vat,
|
||||
"subtotal": subtotal,
|
||||
"vat": vat,
|
||||
"labour_hours": 48,
|
||||
"labour_days": 2,
|
||||
|
|
@ -1163,17 +1162,18 @@ class Costs:
|
|||
cost = [x for x in INSTALLER_ASHP_COSTS if x][0]["cost"]
|
||||
|
||||
# We add some contingency since there are additional costs such as resizing radiators, that could be required
|
||||
total_cost = cost * (1 + self.CONTINGENCY)
|
||||
subtotal_before_vat = total_cost / (1 + self.VAT_RATE)
|
||||
vat = total_cost - subtotal_before_vat
|
||||
subtotal = cost * (1 + self.CONTINGENCY)
|
||||
# The costs from installers exclude VAT
|
||||
vat = subtotal * self.VAT_RATE
|
||||
total_cost = subtotal + vat
|
||||
|
||||
# We assume 5 days installation
|
||||
labour_days = 5
|
||||
labour_hours = labour_days * 8
|
||||
|
||||
return {
|
||||
"total": total_cost,
|
||||
"subtotal": subtotal_before_vat,
|
||||
"total": subtotal,
|
||||
"subtotal": subtotal,
|
||||
"vat": vat,
|
||||
"labour_hours": labour_hours,
|
||||
"labour_days": labour_days,
|
||||
|
|
|
|||
|
|
@ -496,6 +496,7 @@ class RoofRecommendations:
|
|||
roof_roof_insulation_materials = [
|
||||
{
|
||||
"type": "room_roof_insulation",
|
||||
"measure_type": "room_roof_insulation",
|
||||
"description": "Insulating the ceiling of the roof roof and re-decorate",
|
||||
"depths": [100],
|
||||
"depth_unit": "mm",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue