From 7ebfb3b99c5096e5b47a16c771e7bbf4cfb1cbc4 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Tue, 28 Nov 2023 12:39:11 +0000 Subject: [PATCH] Added labour days to cavity walls costs --- .../app/db/functions/portfolio_functions.py | 17 +++++++++------ .../db/functions/recommendations_functions.py | 3 ++- backend/app/db/models/portfolio.py | 1 + backend/app/db/models/recommendations.py | 1 + backend/app/plan/router.py | 21 +++++++++++++++++-- backend/ml_models/Valuation.py | 6 +++--- recommendations/Costs.py | 12 +++++++++-- recommendations/FireplaceRecommendations.py | 3 ++- recommendations/VentilationRecommendations.py | 3 ++- 9 files changed, 51 insertions(+), 16 deletions(-) diff --git a/backend/app/db/functions/portfolio_functions.py b/backend/app/db/functions/portfolio_functions.py index fc9add42..b3936eb9 100644 --- a/backend/app/db/functions/portfolio_functions.py +++ b/backend/app/db/functions/portfolio_functions.py @@ -3,15 +3,16 @@ from backend.app.db.models.recommendations import Plan, PlanRecommendations, Rec from backend.app.db.models.portfolio import Portfolio -def aggregate_portfolio_recommendations(session, portfolio_id: int): +def aggregate_portfolio_recommendations(session, portfolio_id: int, total_valuation_increase: float): # Aggregate multiple fields aggregates = ( session.query( func.sum(Recommendation.estimated_cost).label("cost"), func.sum(Recommendation.total_work_hours).label("total_work_hours"), - func.sum(Recommendation.heat_demand).label("total_heat_demand"), - func.sum(Recommendation.energy_savings).label("total_energy_savings"), - func.sum(Recommendation.energy_cost_savings).label("energy_cost_savings") + func.sum(Recommendation.heat_demand).label("energy_savings"), + func.sum(Recommendation.co2_equivalent_savings).label("co2_equivalent_savings"), + func.sum(Recommendation.energy_cost_savings).label("energy_cost_savings"), + func.sum(Recommendation.labour_days).label("labour_days"), ) .join(PlanRecommendations, PlanRecommendations.recommendation_id == Recommendation.id) .join(Plan, Plan.id == PlanRecommendations.plan_id) @@ -22,9 +23,10 @@ def aggregate_portfolio_recommendations(session, portfolio_id: int): aggregates_dict = { "cost": aggregates.cost or 0, "total_work_hours": aggregates.total_work_hours or 0, - "total_heat_demand": aggregates.total_heat_demand or 0, - "total_energy_savings": aggregates.total_energy_savings or 0, + "energy_savings": aggregates.energy_savings or 0, + "co2_equivalent_savings": aggregates.co2_equivalent_savings or 0, "energy_cost_savings": aggregates.energy_cost_savings or 0, + "labour_days": aggregates.labour_days or 0, } # Get the portfolio and update the fields @@ -33,6 +35,9 @@ def aggregate_portfolio_recommendations(session, portfolio_id: int): for key, value in aggregates_dict.items(): setattr(portfolio, key, value) + # Insert total valuation increase + portfolio.property_valuation_increase = total_valuation_increase + # Merge the updated portfolio back into the session session.merge(portfolio) session.flush() diff --git a/backend/app/db/functions/recommendations_functions.py b/backend/app/db/functions/recommendations_functions.py index 772e1184..f7fcb7a3 100644 --- a/backend/app/db/functions/recommendations_functions.py +++ b/backend/app/db/functions/recommendations_functions.py @@ -83,7 +83,8 @@ def upload_recommendations(session: Session, recommendations_to_upload, property "heat_demand": rec["heat_demand"], "co2_equivalent_savings": rec["co2_equivalent_savings"], "total_work_hours": rec["labour_hours"], - "energy_cost_savings": rec["energy_cost_savings"] + "energy_cost_savings": rec["energy_cost_savings"], + "labour_days": rec["labour_days"] } for rec in recommendations_to_upload ] diff --git a/backend/app/db/models/portfolio.py b/backend/app/db/models/portfolio.py index 8279a978..efcda359 100644 --- a/backend/app/db/models/portfolio.py +++ b/backend/app/db/models/portfolio.py @@ -42,6 +42,7 @@ class Portfolio(Base): property_valuation_increase = Column(Float) # Unit is always £ so we don't need to store the unit for the moment rental_yield_increase = Column(Float) # Unit is always £ so we don't need to store the unit for the moment total_work_hours = Column(Float) + labour_days = Column(Float) created_at = Column(DateTime, nullable=False, default=datetime.datetime.now(pytz.utc)) updated_at = Column(DateTime, nullable=False, default=datetime.datetime.now(pytz.utc)) diff --git a/backend/app/db/models/recommendations.py b/backend/app/db/models/recommendations.py index 5515b90d..ff7aa642 100644 --- a/backend/app/db/models/recommendations.py +++ b/backend/app/db/models/recommendations.py @@ -28,6 +28,7 @@ class Recommendation(Base): property_valuation_increase = Column(Float) rental_yield_increase = Column(Float) total_work_hours = Column(Float) + labour_days = Column(Float) class RecommendationMaterials(Base): diff --git a/backend/app/plan/router.py b/backend/app/plan/router.py index 408b4101..ed568bdd 100644 --- a/backend/app/plan/router.py +++ b/backend/app/plan/router.py @@ -21,7 +21,7 @@ from backend.app.db.models.portfolio import rating_lookup from backend.app.dependencies import validate_token from backend.app.plan.schemas import PlanTriggerRequest from backend.app.plan.utils import create_recommendation_scoring_data, get_cleaned -from backend.app.utils import epc_to_sap_lower_bound, read_csv_from_s3, read_parquet_from_s3 +from backend.app.utils import epc_to_sap_lower_bound, read_csv_from_s3, read_parquet_from_s3, sap_to_epc from backend.ml_models.api import ModelApi from backend.Property import Property @@ -33,6 +33,7 @@ from recommendations.optimiser.optimiser_functions import prepare_input_measures from recommendations.Recommendations import Recommendations from utils.logger import setup_logger from utils.s3 import read_dataframe_from_s3_parquet +from backend.ml_models.Valuation import PropertyValuation logger = setup_logger() @@ -250,6 +251,7 @@ async def trigger_plan(body: PlanTriggerRequest): # 3) the recommendations logger.info("Uploading recommendations to the database") + property_valuation_increases = [] session.commit() for i in range(0, len(input_properties), BATCH_SIZE): try: @@ -289,6 +291,16 @@ async def trigger_plan(body: PlanTriggerRequest): session, plan_id=new_plan_id, recommendation_ids=uploaded_recommendation_ids ) + # Get defaults + default_recommendations = [r for r in recommendations_to_upload if r["default"]] + total_sap_points = sum([r["sap_points"] for r in default_recommendations]) + new_sap_points = float(p.data["current-energy-efficiency"]) + total_sap_points + new_epc = sap_to_epc(new_sap_points) + + property_valuation_increases.append( + PropertyValuation.estimate(property_instance=p, target_epc=new_epc) + ) + # Commit the session after each batch session.commit() @@ -304,7 +316,12 @@ async def trigger_plan(body: PlanTriggerRequest): # way to do this, but it's the simplest and will be a process that we can re-use since when we change a # recommendation from being default to not default, we'll need to re-run this process to re-calculate the # the portfolion level impact - aggregate_portfolio_recommendations(session, portfolio_id=body.portfolio_id) + + total_valuation_increase = sum(property_valuation_increases) + + aggregate_portfolio_recommendations( + session, portfolio_id=body.portfolio_id, total_valuation_increase=total_valuation_increase + ) # Commit final changes session.commit() diff --git a/backend/ml_models/Valuation.py b/backend/ml_models/Valuation.py index 1b2f8cf4..92d019a4 100644 --- a/backend/ml_models/Valuation.py +++ b/backend/ml_models/Valuation.py @@ -12,12 +12,12 @@ class PropertyValuation: { "starting_epc": "D", "ending_epc": "C", - "increase_percentage": 0.057, + "increase_percentage": 0.03625, }, { "starting_epc": "D", "ending_epc": "B", - "increase_percentage": 0.057, + "increase_percentage": 0.05725, }, ] @@ -30,7 +30,7 @@ class PropertyValuation: valuation_increases = [ v for v in cls.VALUE_INCREASE_MAPPING if - v["starting_epc"] == property_instance.epc_band and v["ending_epc"] == target_epc + v["starting_epc"] == property_instance.data["current-energy-rating"] and v["ending_epc"] == target_epc ] if len(valuation_increases) != 1: diff --git a/recommendations/Costs.py b/recommendations/Costs.py index 6fc62db2..c9ead002 100644 --- a/recommendations/Costs.py +++ b/recommendations/Costs.py @@ -115,6 +115,9 @@ class Costs: labour_hours = material["labour_hours_per_unit"] * wall_area + # Assume a team of 2 + labour_days = (labour_hours / 8) / 2 + return { "total": total_cost, "subtotal": subtotal_before_vat, @@ -124,7 +127,8 @@ class Costs: "material": base_material_cost, "profit": profit_cost, "labour_hours": labour_hours, - "labour_cost": labour_cost + "labour_cost": labour_cost, + "labour_days": labour_days } def loft_insulation(self, floor_area, material): @@ -153,6 +157,9 @@ class Costs: labour_hours = material["labour_hours_per_unit"] * floor_area + # Assume a team of 1 person + labour_days = labour_hours / 8 + return { "total": total_cost, "subtotal": subtotal_before_vat, @@ -162,7 +169,8 @@ class Costs: "material": base_material_cost, "profit": profit_cost, "labour_hours": labour_hours, - "labour_cost": labour_cost + "labour_cost": labour_cost, + "labour_days": labour_days } def internal_wall_insulation(self, wall_area, material, non_insulation_materials): diff --git a/recommendations/FireplaceRecommendations.py b/recommendations/FireplaceRecommendations.py index 30ab1ad2..c193b7ce 100644 --- a/recommendations/FireplaceRecommendations.py +++ b/recommendations/FireplaceRecommendations.py @@ -45,6 +45,7 @@ class FireplaceRecommendations(Definitions): "sap_points": None, "total": estimated_cost, # Take a very basic estimate of 6 hours, multipled by the number of open fireplaces to seal - "labour_hours": 6 * number_open_fireplaces + "labour_hours": 6 * number_open_fireplaces, + "labour_days": 6 * number_open_fireplaces / 8, # Assume 8 hour day } ] diff --git a/recommendations/VentilationRecommendations.py b/recommendations/VentilationRecommendations.py index 419029a3..b42d136f 100644 --- a/recommendations/VentilationRecommendations.py +++ b/recommendations/VentilationRecommendations.py @@ -67,6 +67,7 @@ class VentilationRecommendations(Definitions): "sap_points": None, "total": estimated_cost, # We use a very simple and rough estimate of 4 hours per unit - "labour_hours": 4 * n_units + "labour_hours": 4 * n_units, + "labour_days": 4 * n_units / 8.0 # Assume 8 hour day } ]