mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
Merge pull request #680 from Hestia-Homes/portfolio-diagnostics
Portfolio diagnostics
This commit is contained in:
commit
07a5f3ce44
18 changed files with 4002 additions and 1394 deletions
|
|
@ -65,7 +65,7 @@ data["Wall Insulation"].value_counts()
|
|||
data["Wall Construction"].value_counts()
|
||||
|
||||
as_built_map = {
|
||||
"Cavity": {"insulated_age_bands":[], "partial_insulated_age_bands": []},
|
||||
"Cavity": {"insulated_age_bands": [], "partial_insulated_age_bands": []},
|
||||
"Solid Brick": {"insulated_age_bands": [], "partial_insulated_age_bands": []},
|
||||
"System": {"insulated_age_bands": [], "partial_insulated_age_bands": []},
|
||||
"Timber Frame": {"insulated_age_bands": [], "partial_insulated_age_bands": []},
|
||||
|
|
@ -74,6 +74,7 @@ as_built_map = {
|
|||
"Cob": {"insulated_age_bands": [], "partial_insulated_age_bands": []},
|
||||
}
|
||||
|
||||
|
||||
def map_wall_construction(wall_constuction, wall_insulation, construction_age_band):
|
||||
if wall_insulation == "AsBuilt":
|
||||
# Deduce based on wall construction and age band
|
||||
|
|
@ -83,13 +84,10 @@ def map_wall_construction(wall_constuction, wall_insulation, construction_age_ba
|
|||
|
||||
# We check if the age band is in insulated or partial insulated, and if neither, we assume uninsulated
|
||||
|
||||
|
||||
|
||||
|
||||
# Variables we want to map
|
||||
'Org Ref', 'Address 1', 'Address 2', 'Address 3', 'Postcode', 'Type',
|
||||
'Attachment', 'Construction Years', 'Wall Construction',
|
||||
'Wall Insulation', 'Roof Construction', 'Roof Insulation',
|
||||
'Floor Construction', 'Floor Insulation', 'Glazing', 'Heating',
|
||||
'Boiler Efficiency', 'Main Fuel', 'Controls Adequacy', 'UPRN',
|
||||
'Total Floor Area (m2)'
|
||||
# 'Org Ref', 'Address 1', 'Address 2', 'Address 3', 'Postcode', 'Type',
|
||||
# 'Attachment', 'Construction Years', 'Wall Construction',
|
||||
# 'Wall Insulation', 'Roof Construction', 'Roof Insulation',
|
||||
# 'Floor Construction', 'Floor Insulation', 'Glazing', 'Heating',
|
||||
# 'Boiler Efficiency', 'Main Fuel', 'Controls Adequacy', 'UPRN',
|
||||
# 'Total Floor Area (m2)'
|
||||
|
|
|
|||
|
|
@ -1395,7 +1395,7 @@ def test_private_epc_e_solar_with_heating_and_minimum_insulation_produces_uplift
|
|||
assert funding.eco4_funding and funding.eco4_funding > 0
|
||||
|
||||
|
||||
def test_existing_gshp_to_ashp():
|
||||
def test_existing_gshp_to_ashp(mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes):
|
||||
r = {'phase': 3, 'parts': [], 'type': 'heating', 'measure_type': 'air_source_heat_pump',
|
||||
'description': 'Install a 5KW air source heat pump, and upgrade heating controls to Smart Thermostats, '
|
||||
'room sensors and smart radiator valves (time & temperature zone control). Ensure you have a '
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -114,14 +114,16 @@ from backend.app.db.models.recommendations import Recommendation, Plan, PlanReco
|
|||
from backend.app.db.models.portfolio import PropertyModel, PropertyDetailsEpcModel
|
||||
from collections import defaultdict
|
||||
|
||||
PORTFOLIO_ID = 434 # Peabody
|
||||
PORTFOLIO_ID = 435 # Peabody
|
||||
SCENARIOS = [
|
||||
904,
|
||||
905
|
||||
908,
|
||||
909,
|
||||
910,
|
||||
]
|
||||
scenario_names = {
|
||||
904: "EPC C - no solid floor, ashp 3.0",
|
||||
905: "EPC B - no solid floor, ashp 3.0",
|
||||
908: "EPC C - no solid floor, ashp 3.0",
|
||||
909: "EPC C - no solid floor, no EWI or IWI, ashp 3.0",
|
||||
910: "EPC B - no solid floor, no EWI, ashp 3.0"
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -232,9 +234,58 @@ properties_data, plans_data, recommendations_data = get_data(
|
|||
|
||||
recommendations_df = pd.DataFrame(recommendations_data)
|
||||
properties_df = pd.DataFrame(properties_data)
|
||||
plans_df = pd.DataFrame(plans_data)
|
||||
|
||||
solar_pv_recommendations = recommendations_df[recommendations_df["measure_type"] == "solar_pv"]
|
||||
s_id = 910
|
||||
ps_w_a_plan = plans_df[plans_df["scenario_id"] == s_id].copy()
|
||||
# Take the newest by scenario id
|
||||
ps_w_a_plan = ps_w_a_plan.sort_values("created_at", ascending=False).drop_duplicates(
|
||||
subset=["property_id"]
|
||||
)
|
||||
z = ps_w_a_plan[
|
||||
ps_w_a_plan["cost_of_works"] > 0
|
||||
].copy()
|
||||
z2 = properties_df[properties_df["property_id"].isin(z["property_id"].values)]
|
||||
# '', 'hot_water_cost_current',
|
||||
# 'lighting_cost_current', 'appliances_cost_current',
|
||||
# 'gas_standing_charge', 'electricity_standing_charge'
|
||||
z2["total_bills"] = z2["heating_cost_current"] + z2["hot_water_cost_current"] + z2["lighting_cost_current"] + z2[
|
||||
"appliances_cost_current"
|
||||
] + z2["gas_standing_charge"] + z2["electricity_standing_charge"]
|
||||
|
||||
from tqdm import tqdm
|
||||
|
||||
# For a property ID, find a property where the no EWI/IWI approach is more expensive than the EWI approach
|
||||
pids = properties_df["property_id"].unique()
|
||||
for pid in tqdm(pids):
|
||||
|
||||
if pid in [603272, 550550, 574493]:
|
||||
continue
|
||||
|
||||
# get the plans
|
||||
property_plan = plans_df[plans_df["property_id"] == int(pid)]
|
||||
# Take the newest plan by scenario id
|
||||
property_plan = property_plan.sort_values("created_at", ascending=False).drop_duplicates(
|
||||
subset=["scenario_id"]
|
||||
)
|
||||
a = property_plan[property_plan["scenario_id"] == 909].squeeze() # no EWI/IWI
|
||||
b = property_plan[property_plan["scenario_id"] == 908].squeeze() # EWI
|
||||
if (a["cost_of_works"] > b["cost_of_works"]) and (
|
||||
a["post_epc_rating"].value == "C") and (b["cost_of_works"] > 5000):
|
||||
bah
|
||||
|
||||
solar_pv_recommendations = recommendations_df[
|
||||
recommendations_df["measure_type"] == "solar_pv"
|
||||
]
|
||||
|
||||
solid_wall_recommendation = recommendations_df[
|
||||
recommendations_df["scenario_id"].isin([908]) &
|
||||
recommendations_df["measure_type"].isin(["internal_wall_insulation"]) &
|
||||
recommendations_df["default"]
|
||||
]
|
||||
average_savings = solar_pv_recommendations.groupby("scenario_id")["energy_cost_savings"].mean().reset_index()
|
||||
# Add on scenarion names
|
||||
average_savings["scenario_name"] = average_savings["scenario_id"].map(scenario_names)
|
||||
|
||||
# Check tenures
|
||||
initial_asset_data = pd.read_excel(
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ from etl.customers.cambridge.surveys import current_epc
|
|||
with db_session() as session:
|
||||
# We need installed measures, where the measure type is ewi or iwi
|
||||
installed_measures = session.query(InstalledMeasure).filter(
|
||||
InstalledMeasure.measure_type.in_(["cavity_wall_insulation"])
|
||||
).all()
|
||||
# Get the uprns
|
||||
installed_uprns = [x.uprn for x in installed_measures]
|
||||
|
|
@ -32,7 +31,7 @@ needing_retry = sal[sal["epc_os_uprn"].isin(installed_uprns)]
|
|||
# Store
|
||||
needing_retry.to_excel(
|
||||
"/Users/khalimconn-kowlessar/Documents/hestia/Customers/Peabody/Nov 2025 Consulting Project/Final "
|
||||
"SAL/properties_needing_retry_20260115 - cavity wall insulation.xlsx",
|
||||
"SAL/properties_needing_retry_20260115 - all already installed.xlsx",
|
||||
sheet_name="Standardised Asset List",
|
||||
index=False
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
# get all properties that have an IWI recommendation
|
||||
import pandas as pd
|
||||
|
||||
r1 = pd.read_excel(
|
||||
"/Users/khalimconn-kowlessar/Documents/hestia/Customers/Peabody/Nov 2025 Consulting Project/Final SAL/EPC B - no "
|
||||
"solid floor, no EWI, ashp 3.0 - 20250113 final.xlsx"
|
||||
)
|
||||
|
||||
r2 = pd.read_excel(
|
||||
"/Users/khalimconn-kowlessar/Documents/hestia/Customers/Peabody/Nov 2025 Consulting Project/Final SAL/EPC C - no "
|
||||
"solid floor, ashp 3.0 - 20250113 final.xlsx"
|
||||
)
|
||||
|
||||
r3 = pd.read_excel(
|
||||
"/Users/khalimconn-kowlessar/Documents/hestia/Customers/Peabody/Nov 2025 Consulting Project/Final SAL/EPC C - no "
|
||||
"solid floor, no EWI or IWI, ashp 3.0 - 20250113 final.xlsx"
|
||||
)
|
||||
|
||||
s1 = r1[~pd.isnull(r1["internal_wall_insulation"])]
|
||||
s2 = r2[~pd.isnull(r2["internal_wall_insulation"])]
|
||||
|
||||
# Combined uprns
|
||||
uprns = s1["uprn"].tolist() + s2["uprn"].tolist()
|
||||
uprns = list(set(uprns))
|
||||
|
||||
# Create SAL of these uprns
|
||||
sal = pd.read_excel(
|
||||
"/Users/khalimconn-kowlessar/Documents/hestia/Customers/Peabody/Nov 2025 Consulting Project/Final SAL/20260113 - "
|
||||
"final asset list.xlsx",
|
||||
sheet_name="Standardised Asset List"
|
||||
)
|
||||
|
||||
needing_retry = sal[sal["epc_os_uprn"].isin(uprns)]
|
||||
|
||||
# Store
|
||||
needing_retry.to_excel(
|
||||
"/Users/khalimconn-kowlessar/Documents/hestia/Customers/Peabody/Nov 2025 Consulting Project/Final "
|
||||
"SAL/properties_needing_retry_20260115 - internal wall insulation.xlsx",
|
||||
sheet_name="Standardised Asset List",
|
||||
index=False
|
||||
)
|
||||
|
|
@ -378,7 +378,7 @@ clean_floor_cases = [
|
|||
},
|
||||
{
|
||||
# This example gets remapped to another dwelling below
|
||||
"description": "Above unheated space or full exposed",
|
||||
"original_description": "Above unheated space or full exposed",
|
||||
'thermal_transmittance': 0, 'thermal_transmittance_unit': 'w/m-¦k', 'is_assumed': False,
|
||||
'is_to_unheated_space': False, 'is_to_external_air': False, 'is_suspended': False, 'is_solid': False,
|
||||
'another_property_below': True, 'insulation_thickness': None
|
||||
|
|
|
|||
|
|
@ -226,7 +226,7 @@ hotwater_cases = [
|
|||
{'original_description': 'Single-point gas water heater, standard tariff',
|
||||
'heater_type': 'single-point gas', 'system_type': "water heater", 'thermostat_characteristics': None,
|
||||
'heating_scope': None, 'energy_recovery': None, 'tariff_type': 'standard tariff', 'extra_features': None,
|
||||
'chp_systems': None, 'distribution_system': None, 'no_system_present': None, 'appliance': None
|
||||
'chp_systems': None, 'distribution_system': None, 'no_system_present': None, 'appliance': None, "assumed": False
|
||||
}
|
||||
|
||||
]
|
||||
|
|
|
|||
|
|
@ -15,7 +15,12 @@ class TestCleanFloor:
|
|||
empty = FloorAttributes('')
|
||||
assert empty.nodata
|
||||
output = empty.process()
|
||||
assert output == {"no_data": True}
|
||||
assert output == {
|
||||
'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_assumed': True,
|
||||
'is_to_unheated_space': False, 'is_to_external_air': False, 'is_suspended': True,
|
||||
'is_solid': False, 'another_property_below': False, 'insulation_thickness': 'none',
|
||||
'no_data': True
|
||||
}
|
||||
|
||||
# Test initialization with a description that contains none of the keywords
|
||||
with pytest.raises(ValueError):
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import pandas as pd
|
||||
import numpy as np
|
||||
from backend.Property import Property
|
||||
from typing import List
|
||||
from typing import List, Mapping, Any
|
||||
from itertools import groupby
|
||||
from recommendations.FloorRecommendations import FloorRecommendations
|
||||
from recommendations.WallRecommendations import WallRecommendations
|
||||
|
|
@ -31,6 +31,18 @@ class Recommendations:
|
|||
High level recommendations class, which sits above the measure specific recommendation classes
|
||||
"""
|
||||
|
||||
# Used in calculation of recommendation impact - increasing variables are features where
|
||||
# a higher value indicates an improvement. Decreasing is the opposite
|
||||
INCREASING_VARIABLES = ["sap"]
|
||||
DECREASING_VARIABLES = ["carbon", "heat_demand"]
|
||||
|
||||
# If the recommendation is mechanical ventilation, we don't apply the rule that the new value should be higher
|
||||
MV_INCREASING_VARIABLES = ["carbon", "heat_demand"]
|
||||
MV_DECREASING_VARIABLES = ["sap"]
|
||||
|
||||
# List of models we expect predictions for, when calculation recommendation impact
|
||||
PREDICTION_PREFIXES = ["sap_change", "heat_demand", "carbon_change"]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
property_instance: Property,
|
||||
|
|
@ -486,19 +498,354 @@ class Recommendations:
|
|||
|
||||
return predicted_appliances_cost_reduction, predicted_appliances_kwh_reduction
|
||||
|
||||
@staticmethod
|
||||
def _check_ventilation_out_of_bounds(sap_impact, ventilation_sap_limit):
|
||||
return (sap_impact < ventilation_sap_limit) or (sap_impact >= 0)
|
||||
|
||||
@staticmethod
|
||||
def _adjust_ventilation_sap(sap_impact, ventilation_sap_limit):
|
||||
|
||||
if sap_impact >= 0:
|
||||
return -1
|
||||
if sap_impact < ventilation_sap_limit:
|
||||
return ventilation_sap_limit
|
||||
|
||||
return sap_impact
|
||||
|
||||
@staticmethod
|
||||
def _filter_phase_adjustment(phase_adjustments):
|
||||
"""
|
||||
Utility function to select the entry from the dictionary, by phase, with the largest
|
||||
phase adjustment
|
||||
:param phase_adjustments: List of phase adjustments, in the form
|
||||
[{"recommendation_id": str, "phase": int, "sap_adjustment": float}]
|
||||
:return:
|
||||
"""
|
||||
filtered_adjustments = []
|
||||
phase_adjustments = sorted(phase_adjustments, key=lambda x: x["phase"])
|
||||
for phase, adjustments in groupby(phase_adjustments, key=lambda x: x["phase"]):
|
||||
adjustments = list(adjustments)
|
||||
adjustments.sort(key=lambda x: x["sap_adjustment"], reverse=True)
|
||||
filtered_adjustments.append(adjustments[0])
|
||||
return filtered_adjustments
|
||||
|
||||
@classmethod
|
||||
def _filter_predictions_for_property(
|
||||
cls,
|
||||
all_predictions: Mapping[str, pd.DataFrame],
|
||||
property_id: str,
|
||||
) -> dict:
|
||||
"""
|
||||
Utility function to filter predictions for a specific property
|
||||
:param all_predictions: Dictionary of all predictions from the model apis
|
||||
:param property_id: The property id to filter for
|
||||
:return:
|
||||
"""
|
||||
|
||||
return {
|
||||
f"{prefix}_predictions": (
|
||||
all_predictions[f"{prefix}_predictions"]
|
||||
.loc[
|
||||
all_predictions[f"{prefix}_predictions"]["property_id"] == property_id
|
||||
]
|
||||
.copy()
|
||||
)
|
||||
for prefix in cls.PREDICTION_PREFIXES
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_monotonic_variables(cls, rec_type: str) -> tuple[List[str], List[str]]:
|
||||
"""
|
||||
Utility function to get the monotonic variables for a specific recommendation type
|
||||
:param rec_type: The recommendation type
|
||||
:return:
|
||||
"""
|
||||
if rec_type == "mechanical_ventilation":
|
||||
return cls.MV_INCREASING_VARIABLES, cls.MV_DECREASING_VARIABLES
|
||||
return cls.INCREASING_VARIABLES, cls.DECREASING_VARIABLES
|
||||
|
||||
@staticmethod
|
||||
def _get_previous_phase_values(
|
||||
rec_phase: int,
|
||||
starting_phase: int,
|
||||
impact_summary: list[dict],
|
||||
property_instance: Property,
|
||||
) -> dict:
|
||||
if rec_phase == starting_phase:
|
||||
return {
|
||||
"sap": float(property_instance.data["current-energy-efficiency"]),
|
||||
"carbon": float(property_instance.data["co2-emissions-current"]),
|
||||
"heat_demand": float(property_instance.data["energy-consumption-current"]),
|
||||
}
|
||||
|
||||
previous_phase_reps = [
|
||||
x for x in impact_summary
|
||||
if x["phase"] == rec_phase - 1 and x["representative"]
|
||||
]
|
||||
|
||||
if len(previous_phase_reps) == 1:
|
||||
return previous_phase_reps[0]
|
||||
|
||||
# It's unlikely that this will occur but this fallback will ensure that we don't
|
||||
# run the next step and run a median of nothing, which will return None
|
||||
if not previous_phase_reps:
|
||||
return {
|
||||
"sap": float(property_instance.data["current-energy-efficiency"]),
|
||||
"carbon": float(property_instance.data["co2-emissions-current"]),
|
||||
"heat_demand": float(property_instance.data["energy-consumption-current"]),
|
||||
}
|
||||
|
||||
# Median fallback (including zero-length case)
|
||||
keys = ("sap", "carbon", "heat_demand")
|
||||
return {
|
||||
key: np.median([item[key] for item in previous_phase_reps])
|
||||
for key in keys
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _get_phase_predictions(
|
||||
cls,
|
||||
property_predictions: dict,
|
||||
recommendation_id: str,
|
||||
) -> dict:
|
||||
return {
|
||||
prefix: (
|
||||
property_predictions[f"{prefix}_predictions"]
|
||||
.loc[
|
||||
property_predictions[f"{prefix}_predictions"]["recommendation_id"]
|
||||
== str(recommendation_id),
|
||||
"predictions",
|
||||
]
|
||||
.values[0]
|
||||
)
|
||||
for prefix in cls.PREDICTION_PREFIXES
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _resolve_current_phase_sap(
|
||||
cls,
|
||||
rec: Mapping[str, Any],
|
||||
previous_phase_values: Mapping[str, Any],
|
||||
phase_energy_efficiency_metrics: Mapping[str, Any],
|
||||
adjustments: list[dict],
|
||||
) -> float:
|
||||
if rec.get("survey", False):
|
||||
return rec["sap_points"] + previous_phase_values["sap"]
|
||||
|
||||
sap = phase_energy_efficiency_metrics["sap_change"]
|
||||
|
||||
prior_adjustments = [a for a in adjustments if a["phase"] < rec["phase"]]
|
||||
if not prior_adjustments:
|
||||
return sap
|
||||
|
||||
filtered = cls._filter_phase_adjustment(prior_adjustments)
|
||||
return sap - sum(a["sap_adjustment"] for a in filtered)
|
||||
|
||||
@classmethod
|
||||
def _compute_phase_impact(
|
||||
cls,
|
||||
rec_type: str,
|
||||
previous_phase_values: dict,
|
||||
current_phase_values: dict,
|
||||
) -> dict:
|
||||
"""
|
||||
Utility function for computing the impact of a recommendation phase, enforcing monotonicity
|
||||
|
||||
:param rec_type: string, the recommendation type
|
||||
:param previous_phase_values: dict, the previous phase values
|
||||
:param current_phase_values: dict, the current phase values
|
||||
:return: dict, the impact of the phase
|
||||
"""
|
||||
phase_increasing, phase_decreasing = cls.get_monotonic_variables(rec_type)
|
||||
|
||||
# Enforce monotonicity
|
||||
for v in phase_increasing:
|
||||
current_phase_values[v] = max(current_phase_values[v], previous_phase_values[v])
|
||||
|
||||
for v in phase_decreasing:
|
||||
current_phase_values[v] = min(current_phase_values[v], previous_phase_values[v])
|
||||
|
||||
# Compute impact
|
||||
impact = {
|
||||
"sap": current_phase_values["sap"] - previous_phase_values["sap"],
|
||||
"carbon": previous_phase_values["carbon"] - current_phase_values["carbon"],
|
||||
"heat_demand": previous_phase_values["heat_demand"] - current_phase_values["heat_demand"],
|
||||
}
|
||||
|
||||
# Clamp values
|
||||
for metric in impact:
|
||||
if rec_type != "mechanical_ventilation":
|
||||
impact[metric] = max(0, impact[metric])
|
||||
if metric == "sap":
|
||||
impact[metric] = round(impact[metric], 2)
|
||||
else:
|
||||
impact[metric] = min(0, impact[metric])
|
||||
|
||||
return impact
|
||||
|
||||
@classmethod
|
||||
def _apply_measure_specific_rules(
|
||||
cls,
|
||||
rec: dict,
|
||||
property_phase_impact: dict,
|
||||
previous_phase_values: dict,
|
||||
current_phase_values: dict,
|
||||
adjustments: list,
|
||||
property_instance,
|
||||
):
|
||||
# For the moment, we cap the number of SAP points that can be achieved by LEDs at 2
|
||||
if rec["type"] == "low_energy_lighting":
|
||||
lighting_sap_limit = LightingRecommendations.get_sap_limit(
|
||||
property_instance.data["lighting-energy-eff"],
|
||||
property_instance.lighting["low_energy_proportion"]
|
||||
)
|
||||
|
||||
# add an adjustment
|
||||
proposed_sap_impact = min(property_phase_impact["sap"], lighting_sap_limit)
|
||||
if proposed_sap_impact != property_phase_impact["sap"]:
|
||||
# Store the sap adjustment. The proposed sap impact will always be less
|
||||
# than the current sap impact, so the adjustment is always positive
|
||||
# as we subtract it from the future phases
|
||||
adjustments.append(
|
||||
{
|
||||
"recommendation_id": rec["recommendation_id"],
|
||||
"phase": rec["phase"],
|
||||
"sap_adjustment": property_phase_impact["sap"] - proposed_sap_impact,
|
||||
}
|
||||
)
|
||||
|
||||
property_phase_impact["sap"] = proposed_sap_impact
|
||||
property_phase_impact["carbon"] = min(
|
||||
property_phase_impact["carbon"], rec["co2_equivalent_savings"]
|
||||
)
|
||||
|
||||
# Update the current phase values
|
||||
current_phase_values["sap"] = previous_phase_values["sap"] + property_phase_impact["sap"]
|
||||
current_phase_values["carbon"] = previous_phase_values["carbon"] - property_phase_impact["carbon"]
|
||||
elif rec["type"] == "mechanical_ventilation":
|
||||
# ventilation is capped by having no greater and a -4 impact
|
||||
ventilation_sap_limit = -4
|
||||
ventilation_out_of_bounds = cls._check_ventilation_out_of_bounds(
|
||||
property_phase_impact["sap"], ventilation_sap_limit
|
||||
)
|
||||
|
||||
if ventilation_out_of_bounds:
|
||||
previous_modelled_sap = previous_phase_values.get("sap_prediction", 0)
|
||||
proposed_sap_impact = current_phase_values["sap"] - previous_modelled_sap
|
||||
proposal_out_of_bounds = cls._check_ventilation_out_of_bounds(
|
||||
proposed_sap_impact, ventilation_sap_limit
|
||||
)
|
||||
if proposal_out_of_bounds:
|
||||
proposed_sap_impact = cls._adjust_ventilation_sap(
|
||||
proposed_sap_impact, ventilation_sap_limit
|
||||
)
|
||||
|
||||
# We keep track of the adjustment
|
||||
# In this case, if the SAP impact has increased, then the adustment should be negative
|
||||
# otherwise it should be positive
|
||||
# When we add the total adjustment, it's an addition
|
||||
# Example
|
||||
# Before: 60, impact -2 => 58
|
||||
# After: 60, impact -1 (So the impact is bigger) => 59
|
||||
# So in this case, we need to make sure we add 1 to all future predictions so
|
||||
# the adjustment should be positive
|
||||
# Before: 60, impact 1 => 61
|
||||
# After: 60, impact -1 => 59
|
||||
# So in this case, we need to make sure we subtract 1 to all future predictions so
|
||||
# the adjustment should be negative
|
||||
# Both cases are reflected in sap adjustment
|
||||
sap_adjustment = proposed_sap_impact - float(property_phase_impact["sap"])
|
||||
|
||||
adjustments.append(
|
||||
{
|
||||
"recommendation_id": rec["recommendation_id"],
|
||||
"phase": rec["phase"],
|
||||
"sap_adjustment": sap_adjustment,
|
||||
}
|
||||
)
|
||||
|
||||
property_phase_impact["sap"] = proposed_sap_impact
|
||||
|
||||
# Update the current phase values
|
||||
current_phase_values["sap"] = previous_phase_values["sap"] + property_phase_impact["sap"]
|
||||
elif rec["type"] == "loft_insulation":
|
||||
# When we have a loft insulation recommendation, where there is an extension and the existing
|
||||
# amount of loft insulation is already good, we limit the SAP points
|
||||
# By limiting here, we don't change the value in current_phase_values. This means that the
|
||||
# future recommendations won't have an impact that is too large
|
||||
li_sap_limit = RoofRecommendations.get_loft_insulation_sap_limit(
|
||||
property_instance.data["roof-energy-eff"], property_instance.roof["insulation_thickness"]
|
||||
)
|
||||
if li_sap_limit is not None:
|
||||
new_value = min(property_phase_impact["sap"], li_sap_limit)
|
||||
# If we've made an adjustment, keep track of it
|
||||
if new_value != property_phase_impact["sap"]:
|
||||
adjustments.append(
|
||||
{
|
||||
"recommendation_id": rec["recommendation_id"],
|
||||
"phase": rec["phase"],
|
||||
# If we've made an adjustment, it will be negative
|
||||
"sap_adjustment": property_phase_impact["sap"] - new_value,
|
||||
}
|
||||
)
|
||||
property_phase_impact["sap"] = new_value
|
||||
# Update the current phase values
|
||||
current_phase_values["sap"] = previous_phase_values["sap"] + property_phase_impact["sap"]
|
||||
elif rec["type"] == "solar_pv":
|
||||
# We use the SAP points in the recommendation as a minimum
|
||||
proposed_impact = (
|
||||
rec["sap_points"] if property_phase_impact["sap"] < rec["sap_points"] else
|
||||
property_phase_impact["sap"]
|
||||
)
|
||||
|
||||
# SAP adjustments should be negative
|
||||
if proposed_impact != property_phase_impact["sap"]:
|
||||
adjustments.append(
|
||||
{
|
||||
"recommendation_id": rec["recommendation_id"],
|
||||
"phase": rec["phase"],
|
||||
# If we've made an adjustment, we will be increasing the number of SAP
|
||||
# points. Since, we subtract adjustments, this number should be negative
|
||||
"sap_adjustment": property_phase_impact["sap"] - proposed_impact,
|
||||
}
|
||||
)
|
||||
property_phase_impact["sap"] = proposed_impact
|
||||
|
||||
# Update the current phase values
|
||||
current_phase_values["sap"] = previous_phase_values["sap"] + property_phase_impact["sap"]
|
||||
|
||||
return property_phase_impact, current_phase_values, adjustments
|
||||
|
||||
@staticmethod
|
||||
def _validate_recommendation_updates(rec: Mapping[str, Any]):
|
||||
"""
|
||||
Utility function to validate that the recommendation updates have been applied correctly
|
||||
:param rec: updated recommendation
|
||||
:return:
|
||||
"""
|
||||
if (
|
||||
(rec["sap_points"] is None) and (rec["co2_equivalent_savings"] is None) or
|
||||
(rec["heat_demand"] is None)
|
||||
):
|
||||
raise ValueError("sap points, co2 or heat demand is missing")
|
||||
|
||||
@classmethod
|
||||
def calculate_recommendation_impact(
|
||||
cls,
|
||||
property_instance,
|
||||
all_predictions,
|
||||
recommendations,
|
||||
representative_recommendations,
|
||||
):
|
||||
property_instance: Property,
|
||||
all_predictions: Mapping[str, Any],
|
||||
recommendations: Mapping[int, List],
|
||||
representative_recommendations: Mapping[int, List],
|
||||
debug: bool = False
|
||||
) -> (Mapping[int, List], List[Mapping[str, Any]]):
|
||||
|
||||
"""
|
||||
Given predictions from the model apis, with method will update the recommendations with the predicted
|
||||
impact of the recommendation on the property
|
||||
|
||||
This algorithm is structured as a large loop, but this is due to the fact that it's sequential in nature -
|
||||
each phase depends on the previous, with adjustments and constraints being allied along the way
|
||||
|
||||
This function will return two objects:
|
||||
1) Updated recommendations with the predicted impact of the recommendation
|
||||
2) A list of impacts by phase, which will be used for the kwh model scoring
|
||||
|
|
@ -507,49 +854,43 @@ class Recommendations:
|
|||
:param all_predictions: dictionary of predictions from the model apis
|
||||
:param recommendations: dictionary of recommendations for the property
|
||||
:param representative_recommendations: dictionary of representative recommendations for the property
|
||||
:return:
|
||||
:param debug: boolean, indicating if the function is running in debug mode. The only difference is that
|
||||
adjustments are returned for testing
|
||||
|
||||
:return: Updated recommendations with predicted impact, and a list of impacts by phase
|
||||
"""
|
||||
|
||||
property_predictions = {
|
||||
prefix + "_predictions": all_predictions[prefix + "_predictions"][
|
||||
all_predictions[prefix + "_predictions"]["property_id"] == str(property_instance.id)
|
||||
].copy() for prefix in ["sap_change", "heat_demand", "carbon_change"]
|
||||
}
|
||||
|
||||
property_recommendations = recommendations[property_instance.id].copy()
|
||||
|
||||
representative_recs = representative_recommendations[property_instance.id].copy()
|
||||
representative_ids = [r["recommendation_id"] for r in representative_recs]
|
||||
|
||||
increasing_variables = ["sap"]
|
||||
decreasing_variables = ["carbon", "heat_demand"]
|
||||
|
||||
# If the recommendation is mechanical ventilation, we don't apply the rule that the new value should be higher
|
||||
mv_increasing_variables = ["carbon", "heat_demand"]
|
||||
mv_decreasing_variables = ["sap"]
|
||||
|
||||
# We allow for negative phase
|
||||
starting_phase = min(
|
||||
rec["phase"] for recs in property_recommendations for rec in recs
|
||||
property_predictions = cls._filter_predictions_for_property(
|
||||
all_predictions, str(property_instance.id)
|
||||
)
|
||||
|
||||
impact_summary = []
|
||||
# shallow copy intentional - we're going to modify the internals
|
||||
property_recommendations = recommendations[property_instance.id].copy()
|
||||
|
||||
representative_ids = [
|
||||
r["recommendation_id"] for r in representative_recommendations[property_instance.id]
|
||||
]
|
||||
|
||||
# We allow for negative phase
|
||||
starting_phase = min(rec["phase"] for recs in property_recommendations for rec in recs)
|
||||
|
||||
# We keep a history of adjustments we have made, so that we ensure that we adjust future
|
||||
# phases for SAP
|
||||
impact_summary, adjustments = [], []
|
||||
for recommendations_by_type in property_recommendations:
|
||||
for rec in recommendations_by_type:
|
||||
if rec["type"] in ["trickle_vents", "draught_proofing", "extension_cavity_wall_insulation"]:
|
||||
# We don't have a percieved sap impact of mechanical ventilation or trickle vents, and we don't
|
||||
# have the capacity to score draught proofing
|
||||
# --- Special-case: non-modelled measures -------------------------
|
||||
if rec["type"] in {
|
||||
"trickle_vents",
|
||||
"draught_proofing",
|
||||
"extension_cavity_wall_insulation",
|
||||
}:
|
||||
if rec["type"] == "extension_cavity_wall_insulation":
|
||||
|
||||
previous_phase = [x for x in impact_summary if x["phase"] == (rec["phase"] - 1)]
|
||||
if previous_phase:
|
||||
sap = previous_phase[0]["sap"]
|
||||
carbon = previous_phase[0]["carbon"]
|
||||
heat_demand = previous_phase[0]["heat_demand"]
|
||||
else:
|
||||
sap = float(property_instance.data["current-energy-efficiency"])
|
||||
carbon = float(property_instance.data["co2-emissions-current"])
|
||||
heat_demand = float(property_instance.data["energy-consumption-current"])
|
||||
previous = cls._get_previous_phase_values(
|
||||
rec_phase=rec["phase"],
|
||||
starting_phase=starting_phase,
|
||||
impact_summary=impact_summary,
|
||||
property_instance=property_instance,
|
||||
)
|
||||
|
||||
impact_summary.append(
|
||||
{
|
||||
|
|
@ -557,62 +898,29 @@ class Recommendations:
|
|||
"representative": rec["recommendation_id"] in representative_ids,
|
||||
"recommendation_id": rec["recommendation_id"],
|
||||
"measure_type": rec["measure_type"],
|
||||
"sap": sap + rec["sap_points"],
|
||||
"carbon": carbon - rec["co2_equivalent_savings"],
|
||||
"heat_demand": heat_demand - rec["heat_demand"],
|
||||
"sap": previous["sap"] + rec["sap_points"],
|
||||
"carbon": previous["carbon"] - rec["co2_equivalent_savings"],
|
||||
"heat_demand": previous["heat_demand"] - rec["heat_demand"],
|
||||
}
|
||||
)
|
||||
continue
|
||||
|
||||
phase_energy_efficiency_metrics = {
|
||||
prefix: property_predictions[prefix + "_predictions"][
|
||||
property_predictions[prefix + "_predictions"]["recommendation_id"] == str(
|
||||
rec["recommendation_id"]
|
||||
)]["predictions"].values[0] for prefix in ["sap_change", "heat_demand", "carbon_change"]
|
||||
}
|
||||
phase_energy_efficiency_metrics = cls._get_phase_predictions(
|
||||
property_predictions=property_predictions,
|
||||
recommendation_id=rec["recommendation_id"],
|
||||
)
|
||||
|
||||
# We structure this so that depending on the phase, we capture the previous phase impacts and
|
||||
# then just have one piece of code to calculate the difference
|
||||
if rec["phase"] == starting_phase:
|
||||
# These are just the starting values, from the EPC. When we score the ML models,
|
||||
# heating_cost_starting and heating_cost_ending are just the values in the EPC. However, with
|
||||
# heating_cost_ending, we expect that the EPC will predict a heating cost based on what would happen
|
||||
# if we implemented the recommendation today, so our starting value is the EPC
|
||||
|
||||
previous_phase_values = {
|
||||
"sap": float(property_instance.data["current-energy-efficiency"]),
|
||||
# For carbon, even though we generally use the updated figure which includes the carbon
|
||||
# associated to appliances, for this scoring process we use the EPC carbon value. This means
|
||||
# that we don't overestimate the impact since the model uses the EPC carbon value
|
||||
"carbon": float(property_instance.data["co2-emissions-current"]),
|
||||
"heat_demand": float(property_instance.data["energy-consumption-current"]),
|
||||
}
|
||||
|
||||
else:
|
||||
|
||||
previous_phase_values_multiple = [
|
||||
x for x in impact_summary if x["phase"] == (rec["phase"] - 1) and x["representative"]
|
||||
]
|
||||
if len(previous_phase_values_multiple) != 1:
|
||||
# Take an average of each of the previous phases
|
||||
keys_to_median = ["sap", "carbon", "heat_demand"]
|
||||
|
||||
previous_phase_values = {}
|
||||
for key in keys_to_median:
|
||||
values = [item[key] for item in previous_phase_values_multiple]
|
||||
previous_phase_values[key] = np.median(values)
|
||||
|
||||
else:
|
||||
previous_phase_values = previous_phase_values_multiple[0]
|
||||
|
||||
# We extract the values for the current phase
|
||||
if rec.get("survey", False):
|
||||
current_phase_sap = rec["sap_points"] + previous_phase_values["sap"]
|
||||
else:
|
||||
current_phase_sap = phase_energy_efficiency_metrics["sap_change"]
|
||||
previous_phase_values = cls._get_previous_phase_values(
|
||||
rec_phase=rec["phase"],
|
||||
starting_phase=starting_phase,
|
||||
impact_summary=impact_summary,
|
||||
property_instance=property_instance
|
||||
)
|
||||
|
||||
current_phase_values = {
|
||||
"sap": current_phase_sap,
|
||||
"sap": cls._resolve_current_phase_sap(
|
||||
rec, previous_phase_values, phase_energy_efficiency_metrics, adjustments
|
||||
),
|
||||
"carbon": phase_energy_efficiency_metrics["carbon_change"],
|
||||
"heat_demand": phase_energy_efficiency_metrics["heat_demand"],
|
||||
}
|
||||
|
|
@ -625,113 +933,20 @@ class Recommendations:
|
|||
# However, if the recommendation is mechanical ventilation, this can have a negative SAP impact so
|
||||
# we don't apply this rule
|
||||
|
||||
if rec["type"] == "mechanical_ventilation":
|
||||
phase_increasing_variables = mv_increasing_variables
|
||||
phase_decreasing_variables = mv_decreasing_variables
|
||||
else:
|
||||
phase_increasing_variables = increasing_variables
|
||||
phase_decreasing_variables = decreasing_variables
|
||||
property_phase_impact = cls._compute_phase_impact(
|
||||
rec_type=rec["type"],
|
||||
previous_phase_values=previous_phase_values,
|
||||
current_phase_values=current_phase_values,
|
||||
)
|
||||
|
||||
for v in phase_increasing_variables:
|
||||
current_phase_values[v] = (
|
||||
current_phase_values[v] if current_phase_values[v] > previous_phase_values[v] else
|
||||
previous_phase_values[v]
|
||||
)
|
||||
for v in previous_phase_values:
|
||||
if v in phase_decreasing_variables:
|
||||
current_phase_values[v] = (
|
||||
current_phase_values[v] if current_phase_values[v] < previous_phase_values[v] else
|
||||
previous_phase_values[v]
|
||||
)
|
||||
|
||||
property_phase_impact = {
|
||||
# Increasing
|
||||
"sap": current_phase_values["sap"] - previous_phase_values["sap"],
|
||||
# Decreasing
|
||||
"carbon": previous_phase_values["carbon"] - current_phase_values["carbon"],
|
||||
# Decreasing
|
||||
"heat_demand": previous_phase_values["heat_demand"] - current_phase_values["heat_demand"],
|
||||
}
|
||||
|
||||
# Prevent from being negative - apart from ventilation
|
||||
for metric in ["sap", "carbon", "heat_demand"]:
|
||||
if rec["type"] != "mechanical_ventilation":
|
||||
property_phase_impact[metric] = (
|
||||
0 if property_phase_impact[metric] < 0 else property_phase_impact[metric]
|
||||
)
|
||||
if metric == "sap":
|
||||
property_phase_impact[metric] = round(property_phase_impact[metric], 2)
|
||||
else:
|
||||
# We prevent mechanical ventilation from being positive
|
||||
property_phase_impact[metric] = (
|
||||
0 if property_phase_impact[metric] > 0 else property_phase_impact[metric]
|
||||
)
|
||||
|
||||
# For the moment, we cap the number of SAP points that can be achieved by LEDs at 2
|
||||
if rec["type"] == "low_energy_lighting":
|
||||
lighting_sap_limit = LightingRecommendations.get_sap_limit(
|
||||
property_instance.data["lighting-energy-eff"],
|
||||
property_instance.lighting["low_energy_proportion"]
|
||||
)
|
||||
|
||||
property_phase_impact["sap"] = min(property_phase_impact["sap"], lighting_sap_limit)
|
||||
property_phase_impact["carbon"] = min(
|
||||
property_phase_impact["carbon"], rec["co2_equivalent_savings"]
|
||||
)
|
||||
|
||||
# Update the current phase values
|
||||
current_phase_values["sap"] = previous_phase_values["sap"] + property_phase_impact["sap"]
|
||||
current_phase_values["carbon"] = previous_phase_values["carbon"] - property_phase_impact["carbon"]
|
||||
|
||||
# We also ensure that mechanical ventilation doesn't have an ovely strong negative SAP impact
|
||||
if rec["type"] == "mechanical_ventilation":
|
||||
# ventilation is capped by having no greater and a -4 impact
|
||||
ventilation_sap_limit = -4
|
||||
|
||||
def _check_veniltation_out_of_bounds(sap_impact):
|
||||
return (sap_impact < ventilation_sap_limit) or (sap_impact >= 0)
|
||||
|
||||
def _adjust_ventilation_sap(sap_impact):
|
||||
if sap_impact >= 0:
|
||||
return -1
|
||||
if sap_impact < ventilation_sap_limit:
|
||||
return ventilation_sap_limit
|
||||
|
||||
ventilation_out_of_bounds = _check_veniltation_out_of_bounds(property_phase_impact["sap"])
|
||||
|
||||
if ventilation_out_of_bounds:
|
||||
previous_modelled_sap = previous_phase_values.get("sap_prediction", 0)
|
||||
proposed_sap_impact = current_phase_sap - previous_modelled_sap
|
||||
proposal_out_of_bounds = _check_veniltation_out_of_bounds(proposed_sap_impact)
|
||||
if proposal_out_of_bounds:
|
||||
property_phase_impact["sap"] = _adjust_ventilation_sap(proposed_sap_impact)
|
||||
else:
|
||||
property_phase_impact["sap"] = proposed_sap_impact
|
||||
|
||||
# Update the current phase values
|
||||
current_phase_values["sap"] = previous_phase_values["sap"] + property_phase_impact["sap"]
|
||||
|
||||
if rec["type"] == "loft_insulation":
|
||||
# When we have a loft insulation recommendation, where there is an extension and the existing
|
||||
# amount of loft insulation is already good, we limit the SAP points
|
||||
# By limiting here, we don't change the value in current_phase_values. This means that the
|
||||
# future recommendations won't have an impact that is too large
|
||||
li_sap_limit = RoofRecommendations.get_loft_insulation_sap_limit(
|
||||
property_instance.data["roof-energy-eff"], property_instance.roof["insulation_thickness"]
|
||||
)
|
||||
if li_sap_limit is not None:
|
||||
property_phase_impact["sap"] = min(property_phase_impact["sap"], li_sap_limit)
|
||||
# Update the current phase values
|
||||
current_phase_values["sap"] = previous_phase_values["sap"] + property_phase_impact["sap"]
|
||||
|
||||
if rec["type"] == "solar_pv":
|
||||
# We use the SAP points in the recommendation as a minimum
|
||||
property_phase_impact["sap"] = (
|
||||
rec["sap_points"] if property_phase_impact["sap"] < rec["sap_points"] else
|
||||
property_phase_impact["sap"]
|
||||
)
|
||||
# Update the current phase values
|
||||
current_phase_values["sap"] = previous_phase_values["sap"] + property_phase_impact["sap"]
|
||||
property_phase_impact, current_phase_values, adjustments = cls._apply_measure_specific_rules(
|
||||
rec=rec,
|
||||
property_phase_impact=property_phase_impact,
|
||||
previous_phase_values=previous_phase_values,
|
||||
current_phase_values=current_phase_values,
|
||||
adjustments=adjustments,
|
||||
property_instance=property_instance
|
||||
)
|
||||
|
||||
# Insert this information into the recommendation.
|
||||
if not rec.get("survey", False):
|
||||
|
|
@ -740,11 +955,7 @@ class Recommendations:
|
|||
rec["co2_equivalent_savings"] = property_phase_impact["carbon"]
|
||||
rec["heat_demand"] = property_phase_impact["heat_demand"]
|
||||
|
||||
if (
|
||||
(rec["sap_points"] is None) and (rec["co2_equivalent_savings"] is None) or
|
||||
(rec["heat_demand"] is None)
|
||||
):
|
||||
raise ValueError("sap points, co2 or heat demand is missing")
|
||||
cls._validate_recommendation_updates(rec)
|
||||
|
||||
impact_summary.append(
|
||||
{
|
||||
|
|
@ -757,6 +968,9 @@ class Recommendations:
|
|||
}
|
||||
)
|
||||
|
||||
if debug:
|
||||
return property_recommendations, impact_summary, adjustments
|
||||
|
||||
return property_recommendations, impact_summary
|
||||
|
||||
@staticmethod
|
||||
|
|
|
|||
|
|
@ -711,9 +711,12 @@ def optimise_with_scenarios(
|
|||
if kept:
|
||||
remaining_measures.append(kept)
|
||||
|
||||
remaining_budget = budget - fabric_cost if budget is not None else None
|
||||
remaining_budget = 0 if remaining_budget < 0 else remaining_budget
|
||||
|
||||
picked_extra, extra_cost, extra_gain = run_optimizer(
|
||||
remaining_measures,
|
||||
budget=budget - fabric_cost if budget is not None else None,
|
||||
budget=remaining_budget,
|
||||
sub_target_gain=(
|
||||
target_gain - fabric_gain
|
||||
if target_gain is not None
|
||||
|
|
@ -769,6 +772,12 @@ def optimise_with_scenarios(
|
|||
|
||||
fixed_cost, fixed_gain = sum_cost_gain(fixed_items)
|
||||
|
||||
if budget is not None:
|
||||
# If we have a budget, we cannot exceed it via our fixed cost. If we do,
|
||||
# this is not a viable solution
|
||||
if fixed_cost > budget:
|
||||
continue
|
||||
|
||||
# Remaining measures (all other groups)
|
||||
remaining_measures = [
|
||||
grp for gi, grp in enumerate(optimisation_measures)
|
||||
|
|
|
|||
0
recommendations/tests/test_data/__init__.py
Normal file
0
recommendations/tests/test_data/__init__.py
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,11 +1,28 @@
|
|||
import pytest
|
||||
import datetime
|
||||
from backend.Property import Property
|
||||
from recommendations.FireplaceRecommendations import FireplaceRecommendations
|
||||
from etl.epc.Record import EPCRecord
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fireplace_materials():
|
||||
return [
|
||||
{'id': 3591, 'type': 'sealing_fireplace', 'description': 'Sealing of an open fireplace', 'depth': 0.0,
|
||||
'depth_unit': None, 'cost': None, 'cost_unit': 'gbp_per_unit', 'r_value_per_mm': None,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': None,
|
||||
'thermal_conductivity_unit': None, 'link': 'Warm Front',
|
||||
'created_at': datetime.datetime(2025, 8, 15, 16, 31, 52, 995292), 'is_active': True,
|
||||
'prime_material_cost': None, 'material_cost': 0.0, 'labour_cost': 0.0, 'labour_hours_per_unit': 0.0,
|
||||
'plant_cost': 0.0, 'total_cost': 185.0, 'notes': None, 'is_installer_quote': True, 'innovation_rate': 0.0,
|
||||
'size': None, 'size_unit': None, 'includes_scaffolding': False, 'includes_battery': False,
|
||||
'battery_size': None}
|
||||
]
|
||||
|
||||
|
||||
class TestFirepaceRecommendations:
|
||||
|
||||
def test_no_fireplaces(self):
|
||||
def test_no_fireplaces(self, fireplace_materials):
|
||||
epc_record = EPCRecord()
|
||||
epc_record.prepared_epc = {
|
||||
"number-open-fireplaces": 0,
|
||||
|
|
@ -13,9 +30,7 @@ class TestFirepaceRecommendations:
|
|||
|
||||
property_instance = Property(id=0, address="fake", postcode="fake", epc_record=epc_record)
|
||||
|
||||
recommender = FireplaceRecommendations(
|
||||
property_instance=property_instance
|
||||
)
|
||||
recommender = FireplaceRecommendations(property_instance=property_instance, materials=fireplace_materials)
|
||||
|
||||
assert recommender.recommendation is None
|
||||
|
||||
|
|
@ -23,16 +38,15 @@ class TestFirepaceRecommendations:
|
|||
|
||||
assert recommender.recommendation is None
|
||||
|
||||
def test_one_fireplace(self):
|
||||
def test_one_fireplace(self, fireplace_materials):
|
||||
epc_record = EPCRecord()
|
||||
epc_record.prepared_epc = {
|
||||
"number-open-fireplaces": 1,
|
||||
}
|
||||
property_instance = Property(id=0, address="fake", postcode="fake", epc_record=epc_record)
|
||||
property_instance.already_installed = []
|
||||
|
||||
recommender = FireplaceRecommendations(
|
||||
property_instance=property_instance
|
||||
)
|
||||
recommender = FireplaceRecommendations(property_instance=property_instance, materials=fireplace_materials)
|
||||
|
||||
assert recommender.recommendation is None
|
||||
|
||||
|
|
@ -40,18 +54,17 @@ class TestFirepaceRecommendations:
|
|||
|
||||
assert recommender.recommendation
|
||||
assert recommender.recommendation[0]["type"] == "sealing_open_fireplace"
|
||||
assert recommender.recommendation[0]["total"] == 235
|
||||
assert recommender.recommendation[0]["total"] == 185
|
||||
|
||||
def test_multiple_fireplaces(self):
|
||||
def test_multiple_fireplaces(self, fireplace_materials):
|
||||
epc_record = EPCRecord()
|
||||
epc_record.prepared_epc = {
|
||||
"number-open-fireplaces": 3,
|
||||
}
|
||||
property_instance = Property(id=0, address="fake", postcode="fake", epc_record=epc_record)
|
||||
property_instance.already_installed = []
|
||||
|
||||
recommender = FireplaceRecommendations(
|
||||
property_instance=property_instance
|
||||
)
|
||||
recommender = FireplaceRecommendations(property_instance=property_instance, materials=fireplace_materials)
|
||||
|
||||
assert recommender.recommendation is None
|
||||
|
||||
|
|
@ -59,4 +72,4 @@ class TestFirepaceRecommendations:
|
|||
|
||||
assert recommender.recommendation
|
||||
assert recommender.recommendation[0]["type"] == "sealing_open_fireplace"
|
||||
assert recommender.recommendation[0]["total"] == 235 * 3
|
||||
assert recommender.recommendation[0]["total"] == 185 * 3
|
||||
|
|
|
|||
1480
recommendations/tests/test_recommendations.py
Normal file
1480
recommendations/tests/test_recommendations.py
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,5 +1,6 @@
|
|||
import pytest
|
||||
import pickle
|
||||
import numpy as np
|
||||
from recommendations.WindowsRecommendations import WindowsRecommendations
|
||||
from backend.Property import Property
|
||||
from recommendations.tests.test_data.materials import materials
|
||||
|
|
@ -44,11 +45,13 @@ class TestWindowRecommendations:
|
|||
epc_record=epc_record
|
||||
)
|
||||
property_1.windows = {
|
||||
'original_description': 'Single glazed', 'has_glazing': False, 'glazing_coverage': 'full',
|
||||
'original_description': 'Single glazed', 'clean_description': 'Single glazed',
|
||||
'has_glazing': False, 'glazing_coverage': 'full',
|
||||
'glazing_type': 'single',
|
||||
'no_data': False
|
||||
}
|
||||
property_1.number_of_windows = 7
|
||||
property_1.already_installed = []
|
||||
|
||||
recommender = WindowsRecommendations(property_instance=property_1, materials=materials)
|
||||
|
||||
|
|
@ -58,25 +61,16 @@ class TestWindowRecommendations:
|
|||
|
||||
# The home is going from single glazing (v poor energy eff) -> double glazing (average energy eff)
|
||||
|
||||
assert recommender.recommendation == [
|
||||
{
|
||||
'phase': 0, 'parts': [], 'type': 'windows_glazing', "measure_type": "double_glazing",
|
||||
'description': 'Install double glazing to all windows',
|
||||
'starting_u_value': None, 'new_u_value': None, 'sap_points': None, 'already_installed': False,
|
||||
'total': 7980.0, 'labour_hours': 0.0, 'labour_days': 0.0, 'is_secondary_glazing': False,
|
||||
'description_simulation': {
|
||||
'multi-glaze-proportion': 100, 'windows-energy-eff': 'Good',
|
||||
'windows-description': 'Fully double glazed',
|
||||
'glazed-type': 'double glazing installed during or after 2002'
|
||||
},
|
||||
'simulation_config': {
|
||||
'has_glazing_ending': True, 'glazing_type_ending': 'double',
|
||||
'multi_glaze_proportion_ending': 100, 'windows_energy_eff_ending': 'Good',
|
||||
'glazed_type_ending': 'double glazing installed during or after 2002'
|
||||
},
|
||||
"survey": None
|
||||
}
|
||||
]
|
||||
assert len(recommender.recommendation) == 1
|
||||
assert recommender.recommendation[0]["total"] == np.float64(7980.0)
|
||||
assert recommender.recommendation[0]["phase"] == 0
|
||||
assert recommender.recommendation[0]["description"] == 'Install double glazing to all windows'
|
||||
assert recommender.recommendation[0]["contingency"] == np.float64(1197.0)
|
||||
assert recommender.recommendation[0]["simulation_config"] == {
|
||||
'has_glazing_ending': True, 'glazing_type_ending': 'double',
|
||||
'multi_glaze_proportion_ending': 100, 'windows_energy_eff_ending': 'Good',
|
||||
'glazed_type_ending': 'double glazing installed during or after 2002'
|
||||
}
|
||||
|
||||
def test_partial_double_glazed(self):
|
||||
"""
|
||||
|
|
@ -97,10 +91,15 @@ class TestWindowRecommendations:
|
|||
address='1',
|
||||
epc_record=epc_record
|
||||
)
|
||||
property_2.windows = {'original_description': 'Mostly double glazing', 'has_glazing': True,
|
||||
'glazing_coverage': 'most',
|
||||
'glazing_type': 'double', 'no_data': False}
|
||||
property_2.windows = {
|
||||
'original_description': 'Mostly double glazing',
|
||||
'clean_description': 'Mostly double glazing',
|
||||
'has_glazing': True,
|
||||
'glazing_coverage': 'most',
|
||||
'glazing_type': 'double', 'no_data': False
|
||||
}
|
||||
property_2.number_of_windows = 7
|
||||
property_2.already_installed = []
|
||||
|
||||
recommender2 = WindowsRecommendations(property_instance=property_2, materials=materials)
|
||||
|
||||
|
|
@ -108,26 +107,16 @@ class TestWindowRecommendations:
|
|||
|
||||
recommender2.recommend()
|
||||
|
||||
assert recommender2.recommendation == [
|
||||
{
|
||||
'phase': 0, 'parts': [], 'type': 'windows_glazing', "measure_type": "double_glazing",
|
||||
'description': 'Install double glazing to the remaining windows', 'starting_u_value': None,
|
||||
'new_u_value': None, 'sap_points': None, 'already_installed': False, 'total': 5700.0,
|
||||
'labour_hours': 0.0,
|
||||
'labour_days': 0.0, 'is_secondary_glazing': False,
|
||||
'description_simulation': {
|
||||
'multi-glaze-proportion': 100, 'windows-energy-eff': 'Good',
|
||||
'windows-description': 'Fully double glazed',
|
||||
'glazed-type': 'double glazing installed during or after 2002'
|
||||
},
|
||||
'simulation_config': {
|
||||
'glazing_coverage_ending': 'full', 'multi_glaze_proportion_ending': 100,
|
||||
'windows_energy_eff_ending': 'Good', 'glazing_type_ending': 'double',
|
||||
'glazed_type_ending': 'double glazing installed during or after 2002'
|
||||
},
|
||||
"survey": None
|
||||
}
|
||||
]
|
||||
assert len(recommender2.recommendation) == 1
|
||||
assert recommender2.recommendation[0]["total"] == np.float64(5700.0)
|
||||
assert recommender2.recommendation[0]["phase"] == 0
|
||||
assert recommender2.recommendation[0]["description"] == 'Install double glazing to all windows'
|
||||
assert recommender2.recommendation[0]["contingency"] == np.float64(855.0)
|
||||
assert recommender2.recommendation[0]["simulation_config"] == {
|
||||
'glazing_coverage_ending': 'full', 'multi_glaze_proportion_ending': 100,
|
||||
'windows_energy_eff_ending': 'Good', 'glazing_type_ending': 'double',
|
||||
'glazed_type_ending': 'double glazing installed during or after 2002'
|
||||
}
|
||||
|
||||
def test_fully_double_glazed(self):
|
||||
"""
|
||||
|
|
@ -146,10 +135,14 @@ class TestWindowRecommendations:
|
|||
address='1',
|
||||
epc_record=epc_record
|
||||
)
|
||||
property_3.windows = {'original_description': 'Fully double glazed', 'has_glazing': True,
|
||||
'glazing_coverage': 'full',
|
||||
'glazing_type': 'double', 'no_data': False}
|
||||
property_3.windows = {
|
||||
'original_description': 'Fully double glazed', 'clean_description': 'Fully double glazed',
|
||||
'has_glazing': True,
|
||||
'glazing_coverage': 'full',
|
||||
'glazing_type': 'double', 'no_data': False
|
||||
}
|
||||
property_3.number_of_windows = 7
|
||||
property_3.already_installed = []
|
||||
|
||||
recommender3 = WindowsRecommendations(property_instance=property_3, materials=materials)
|
||||
|
||||
|
|
@ -172,10 +165,15 @@ class TestWindowRecommendations:
|
|||
address='1',
|
||||
epc_record=epc_record
|
||||
)
|
||||
property_4.windows = {'original_description': 'Full secondary glazing', 'has_glazing': True,
|
||||
'glazing_coverage': 'full',
|
||||
'glazing_type': 'secondary', 'no_data': False}
|
||||
property_4.windows = {
|
||||
'original_description': 'Full secondary glazing',
|
||||
'clean_description': 'Full secondary glazing',
|
||||
'has_glazing': True,
|
||||
'glazing_coverage': 'full',
|
||||
'glazing_type': 'secondary', 'no_data': False
|
||||
}
|
||||
property_4.number_of_windows = 7
|
||||
property_4.already_installed = []
|
||||
|
||||
recommender4 = WindowsRecommendations(property_instance=property_4, materials=materials)
|
||||
|
||||
|
|
@ -199,10 +197,15 @@ class TestWindowRecommendations:
|
|||
address='1',
|
||||
epc_record=epc_record
|
||||
)
|
||||
property_5.windows = {'original_description': 'Partial secondary glazing', 'has_glazing': True,
|
||||
'glazing_coverage': 'partial',
|
||||
'glazing_type': 'secondary', 'no_data': False}
|
||||
property_5.windows = {
|
||||
'original_description': 'Partial secondary glazing',
|
||||
'clean_description': 'Partial secondary glazing',
|
||||
'has_glazing': True,
|
||||
'glazing_coverage': 'partial',
|
||||
'glazing_type': 'secondary', 'no_data': False
|
||||
}
|
||||
property_5.number_of_windows = 7
|
||||
property_5.already_installed = []
|
||||
|
||||
recommender5 = WindowsRecommendations(property_instance=property_5, materials=materials)
|
||||
|
||||
|
|
@ -210,25 +213,15 @@ class TestWindowRecommendations:
|
|||
|
||||
recommender5.recommend()
|
||||
|
||||
assert recommender5.recommendation == [
|
||||
{
|
||||
'phase': 0, 'parts': [], 'type': 'windows_glazing', 'measure_type': 'secondary_glazing',
|
||||
'description': 'Install secondary glazing to the remaining windows', 'starting_u_value': None,
|
||||
'new_u_value': None, 'sap_points': None, 'already_installed': False, 'total': 4560.0,
|
||||
'labour_hours': 0.0, 'labour_days': 0.0, 'is_secondary_glazing': True,
|
||||
'description_simulation': {
|
||||
'multi-glaze-proportion': 100, 'windows-energy-eff': 'Good',
|
||||
'windows-description': 'Full secondary glazing',
|
||||
'glazed-type': 'secondary glazing'
|
||||
},
|
||||
'simulation_config': {
|
||||
'glazing_coverage_ending': 'full', 'multi_glaze_proportion_ending': 100,
|
||||
'windows_energy_eff_ending': 'Good', 'glazing_type_ending': 'secondary',
|
||||
'glazed_type_ending': 'secondary glazing'
|
||||
},
|
||||
"survey": None
|
||||
}
|
||||
]
|
||||
assert len(recommender5.recommendation) == 1
|
||||
assert recommender5.recommendation[0]["total"] == np.float64(4560.0)
|
||||
assert recommender5.recommendation[0]["phase"] == 0
|
||||
assert recommender5.recommendation[0]["description"] == 'Install double glazing to all windows'
|
||||
assert recommender5.recommendation[0]["contingency"] == np.float64(684.0)
|
||||
assert recommender5.recommendation[0]["simulation_config"] == {
|
||||
'glazing_coverage_ending': 'full', 'glazing_type_ending': 'multiple', 'multi_glaze_proportion_ending': 100,
|
||||
'windows_energy_eff_ending': 'Average', 'glazed_type_ending': 'secondary glazing'
|
||||
}
|
||||
|
||||
def test_single_glazed_restricted_measures(self):
|
||||
epc_record = EPCRecord()
|
||||
|
|
@ -245,12 +238,16 @@ class TestWindowRecommendations:
|
|||
address='1',
|
||||
epc_record=epc_record
|
||||
)
|
||||
property_6.windows = {'original_description': 'Single glazed', 'has_glazing': False, 'glazing_coverage': None,
|
||||
'glazing_type': 'single',
|
||||
'no_data': False}
|
||||
property_6.windows = {
|
||||
'original_description': 'Single glazed', 'clean_description': 'Single glazed',
|
||||
'has_glazing': False, 'glazing_coverage': None,
|
||||
'glazing_type': 'single',
|
||||
'no_data': False
|
||||
}
|
||||
property_6.number_of_windows = 7
|
||||
property_6.restricted_measures = True
|
||||
property_6.is_heritage = True
|
||||
property_6.already_installed = []
|
||||
|
||||
recommender6 = WindowsRecommendations(property_instance=property_6, materials=materials)
|
||||
|
||||
|
|
@ -258,26 +255,18 @@ class TestWindowRecommendations:
|
|||
|
||||
recommender6.recommend()
|
||||
|
||||
assert recommender6.recommendation == [
|
||||
{
|
||||
'phase': 0, 'parts': [], 'type': 'windows_glazing', 'measure_type': 'secondary_glazing',
|
||||
'description': 'Install secondary glazing to all windows. Secondary glazing recommended due to '
|
||||
'herigate building status',
|
||||
'starting_u_value': None, 'new_u_value': None, 'sap_points': None, 'already_installed': False,
|
||||
'total': 7980.0, 'labour_hours': 0.0, 'labour_days': 0.0, 'is_secondary_glazing': True,
|
||||
'description_simulation': {
|
||||
'multi-glaze-proportion': 100, 'windows-energy-eff': 'Good',
|
||||
'windows-description': 'Full secondary glazing',
|
||||
'glazed-type': 'secondary glazing'
|
||||
},
|
||||
'simulation_config': {
|
||||
'has_glazing_ending': True, 'glazing_coverage_ending': 'full',
|
||||
'glazing_type_ending': 'secondary', 'multi_glaze_proportion_ending': 100,
|
||||
'windows_energy_eff_ending': 'Good', 'glazed_type_ending': 'secondary glazing'
|
||||
},
|
||||
"survey": None
|
||||
},
|
||||
]
|
||||
assert len(recommender6.recommendation) == 1
|
||||
assert recommender6.recommendation[0]["total"] == np.float64(7980.0)
|
||||
assert recommender6.recommendation[0]["phase"] == 0
|
||||
assert recommender6.recommendation[0]["contingency"] == np.float64(1197.0)
|
||||
assert recommender6.recommendation[0]["description"] == (
|
||||
'Install secondary glazing to all windows. Secondary glazing recommended due to herigate building status'
|
||||
)
|
||||
assert recommender6.recommendation[0]["simulation_config"] == {
|
||||
'has_glazing_ending': True, 'glazing_coverage_ending': 'full', 'glazing_type_ending': 'secondary',
|
||||
'multi_glaze_proportion_ending': 100, 'windows_energy_eff_ending': 'Good',
|
||||
'glazed_type_ending': 'secondary glazing'
|
||||
}
|
||||
|
||||
def test_full_triple_glazed(self):
|
||||
epc_record = EPCRecord()
|
||||
|
|
@ -292,10 +281,14 @@ class TestWindowRecommendations:
|
|||
address='1',
|
||||
epc_record=epc_record
|
||||
)
|
||||
property_7.windows = {'original_description': 'Fully triple glazed', 'has_glazing': True,
|
||||
'glazing_coverage': 'full',
|
||||
'glazing_type': 'triple', 'no_data': False}
|
||||
property_7.windows = {
|
||||
'original_description': 'Fully triple glazed', 'clean_description': 'Fully triple glazed',
|
||||
'has_glazing': True,
|
||||
'glazing_coverage': 'full',
|
||||
'glazing_type': 'triple', 'no_data': False
|
||||
}
|
||||
property_7.number_of_windows = 7
|
||||
property_7.already_installed = []
|
||||
|
||||
recommender7 = WindowsRecommendations(property_instance=property_7, materials=materials)
|
||||
|
||||
|
|
@ -321,10 +314,15 @@ class TestWindowRecommendations:
|
|||
address='1',
|
||||
epc_record=epc_record
|
||||
)
|
||||
property_8.windows = {'original_description': 'Mostly triple glazing', 'has_glazing': True,
|
||||
'glazing_coverage': 'most',
|
||||
'glazing_type': 'triple', 'no_data': False}
|
||||
property_8.windows = {
|
||||
'original_description': 'Mostly triple glazing',
|
||||
'clean_description': 'Mostly triple glazing',
|
||||
'has_glazing': True,
|
||||
'glazing_coverage': 'most',
|
||||
'glazing_type': 'triple', 'no_data': False
|
||||
}
|
||||
property_8.number_of_windows = 7
|
||||
property_8.already_installed = []
|
||||
|
||||
recommender8 = WindowsRecommendations(property_instance=property_8, materials=materials)
|
||||
|
||||
|
|
@ -394,7 +392,9 @@ class TestWindowRecommendations:
|
|||
epc_record=epc_record
|
||||
)
|
||||
property_9.windows = {
|
||||
'original_description': 'Single glazed', 'has_glazing': False, 'glazing_coverage': None,
|
||||
'original_description': 'Single glazed',
|
||||
'clean_description': 'Single glazed',
|
||||
'has_glazing': False, 'glazing_coverage': None,
|
||||
'glazing_type': 'single',
|
||||
'no_data': False
|
||||
}
|
||||
|
|
@ -403,6 +403,7 @@ class TestWindowRecommendations:
|
|||
property_9.number_of_windows = 7
|
||||
property_9.restricted_measures = False
|
||||
property_9.is_heritage = False
|
||||
property_9.already_installed = []
|
||||
|
||||
recommender9 = WindowsRecommendations(property_instance=property_9, materials=materials)
|
||||
|
||||
|
|
@ -410,26 +411,10 @@ class TestWindowRecommendations:
|
|||
|
||||
recommender9.recommend()
|
||||
|
||||
assert recommender9.recommendation == [
|
||||
{
|
||||
'phase': 0, 'parts': [], 'type': 'windows_glazing', 'measure_type': 'double_glazing',
|
||||
'description': 'Install double glazing to all windows', 'starting_u_value': None, 'new_u_value': None,
|
||||
'sap_points': None, 'already_installed': False, 'total': 7980.0, 'labour_hours': 0.0,
|
||||
'labour_days': 0.0, 'is_secondary_glazing': False,
|
||||
'description_simulation': {
|
||||
'multi-glaze-proportion': 100, 'windows-energy-eff': 'Good',
|
||||
'windows-description': 'Fully double glazed',
|
||||
'glazed-type': 'double glazing installed during or after 2002'
|
||||
},
|
||||
'simulation_config': {
|
||||
'has_glazing_ending': True, 'glazing_coverage_ending': 'full',
|
||||
'glazing_type_ending': 'double', 'multi_glaze_proportion_ending': 100,
|
||||
'windows_energy_eff_ending': 'Good',
|
||||
'glazed_type_ending': 'double glazing installed during or after 2002'
|
||||
},
|
||||
"survey": None
|
||||
}
|
||||
]
|
||||
assert recommender9.recommendation[0]["total"] == np.float64(7980.0)
|
||||
assert recommender9.recommendation[0]["phase"] == 0
|
||||
assert recommender9.recommendation[0]["description"] == 'Install double glazing to all windows'
|
||||
assert recommender9.recommendation[0]["contingency"] == np.float64(1197.0)
|
||||
|
||||
# We now simulate the outcome
|
||||
windows_rec = recommender9.recommendation.copy()
|
||||
|
|
@ -537,8 +522,10 @@ class TestWindowRecommendations:
|
|||
'mainheatc_energy_eff_ending': 'Average', 'lighting_energy_eff_starting': 'Very Good',
|
||||
'lighting_energy_eff_ending': 'Very Good', 'number_habitable_rooms_starting': 4.0,
|
||||
'number_habitable_rooms_ending': 4.0, 'number_heated_rooms_starting': 4.0,
|
||||
'number_heated_rooms_ending': 4.0, 'days_to_starting': 3642, 'days_to_ending': 3642,
|
||||
'estimated_perimeter_starting': 23.430749027719962, 'estimated_perimeter_ending': 23.430749027719962
|
||||
'number_heated_rooms_ending': 4.0, 'is_post_sap10_starting': False, 'is_post_sap10_ending': False,
|
||||
'lodgement_date_starting': '2024-07-21', 'lodgement_date_ending': '2024-07-21', 'days_to_starting': 3642,
|
||||
'days_to_ending': 3642, 'estimated_perimeter_starting': 23.430749027719962,
|
||||
'estimated_perimeter_ending': 23.430749027719962
|
||||
}
|
||||
|
||||
assert starting_record == expected_base_difference_record
|
||||
|
|
@ -553,104 +540,168 @@ class TestWindowRecommendations:
|
|||
assert len(simulated_data) == 1
|
||||
|
||||
expected_simulated_outcome = {
|
||||
'uprn': 200001041444, 'rdsap_change': 0, 'heat_demand_change': 0, 'carbon_change': 0.0,
|
||||
'uprn': 200001041444, 'rdsap_change': 0, 'heat_demand_change': 0,
|
||||
'carbon_change': 0.0,
|
||||
'potential_energy_efficiency': 82.0, 'environment_impact_potential': 79.0,
|
||||
'energy_consumption_potential': 155.0, 'co2_emissions_potential': 1.7, 'property_type': 'House',
|
||||
'built_form': 'Semi-Detached', 'constituency': 'E14000909', 'number_habitable_rooms': 4.0,
|
||||
'number_heated_rooms': 4.0, 'construction_age_band': 'England and Wales: before 1900',
|
||||
'energy_consumption_potential': 155.0, 'co2_emissions_potential': 1.7,
|
||||
'property_type': 'House',
|
||||
'built_form': 'Semi-Detached', 'constituency': 'E14000909',
|
||||
'number_habitable_rooms': 4.0,
|
||||
'number_heated_rooms': 4.0,
|
||||
'construction_age_band': 'England and Wales: before 1900',
|
||||
'fixed_lighting_outlets_count': 7.0, 'walls_thermal_transmittance': 1.7,
|
||||
'walls_thermal_transmittance_unit': 'Unknown', 'is_cavity_wall': False, 'is_filled_cavity': False,
|
||||
'walls_thermal_transmittance_unit': 'Unknown', 'is_cavity_wall': False,
|
||||
'is_filled_cavity': False,
|
||||
'is_solid_brick': True, 'is_system_built': False, 'is_timber_frame': False,
|
||||
'is_granite_or_whinstone': False, 'is_as_built': True, 'is_cob': False, 'walls_is_assumed': True,
|
||||
'is_sandstone_or_limestone': False, 'is_park_home': False, 'walls_insulation_thickness': 'none',
|
||||
'external_insulation': False, 'internal_insulation': False, 'floor_thermal_transmittance': 0.96,
|
||||
'is_to_unheated_space': False, 'is_to_external_air': False, 'is_suspended': False, 'is_solid': True,
|
||||
'another_property_below': False, 'floor_insulation_thickness': 'none', 'roof_thermal_transmittance': 2.3,
|
||||
'is_pitched': True, 'is_roof_room': False, 'is_loft': False, 'is_flat': False, 'is_thatched': False,
|
||||
'is_at_rafters': False, 'has_dwelling_above': False, 'roof_insulation_thickness': 'none',
|
||||
'heater_type': 'Unknown', 'system_type': 'from main system', 'thermostat_characteristics': 'Unknown',
|
||||
'heating_scope': 'Unknown', 'energy_recovery': 'Unknown', 'hotwater_tariff_type': 'Unknown',
|
||||
'extra_features': 'Unknown', 'chp_systems': 'Unknown', 'distribution_system': 'Unknown',
|
||||
'no_system_present': 'Unknown', 'appliance': 'Unknown', 'has_radiators': True, 'has_fan_coil_units': False,
|
||||
'has_pipes_in_screed_above_insulation': False, 'has_pipes_in_insulated_timber_floor': False,
|
||||
'has_pipes_in_concrete_slab': False, 'has_boiler': True, 'has_air_source_heat_pump': False,
|
||||
'has_room_heaters': False, 'has_electric_storage_heaters': False, 'has_warm_air': False,
|
||||
'is_granite_or_whinstone': False,
|
||||
'is_as_built': True, 'is_cob': False, 'walls_is_assumed': True,
|
||||
'is_sandstone_or_limestone': False,
|
||||
'is_park_home': False, 'walls_insulation_thickness': 'none',
|
||||
'external_insulation': False,
|
||||
'internal_insulation': False, 'floor_thermal_transmittance': 0.96,
|
||||
'is_to_unheated_space': False,
|
||||
'is_to_external_air': False, 'is_suspended': False, 'is_solid': True,
|
||||
'another_property_below': False,
|
||||
'floor_insulation_thickness': 'none', 'roof_thermal_transmittance': 2.3,
|
||||
'is_pitched': True,
|
||||
'is_roof_room': False, 'is_loft': False, 'is_flat': False, 'is_thatched': False,
|
||||
'is_at_rafters': False,
|
||||
'has_dwelling_above': False, 'roof_insulation_thickness': 'none',
|
||||
'heater_type': 'Unknown',
|
||||
'system_type': 'from main system', 'thermostat_characteristics': 'Unknown',
|
||||
'heating_scope': 'Unknown',
|
||||
'energy_recovery': 'Unknown', 'hotwater_tariff_type': 'Unknown',
|
||||
'extra_features': 'Unknown',
|
||||
'chp_systems': 'Unknown', 'distribution_system': 'Unknown',
|
||||
'no_system_present': 'Unknown',
|
||||
'appliance': 'Unknown', 'has_radiators': True, 'has_fan_coil_units': False,
|
||||
'has_pipes_in_screed_above_insulation': False,
|
||||
'has_pipes_in_insulated_timber_floor': False,
|
||||
'has_pipes_in_concrete_slab': False, 'has_boiler': True,
|
||||
'has_air_source_heat_pump': False,
|
||||
'has_room_heaters': False, 'has_electric_storage_heaters': False,
|
||||
'has_warm_air': False,
|
||||
'has_electric_underfloor_heating': False, 'has_electric_ceiling_heating': False,
|
||||
'has_community_scheme': False, 'has_ground_source_heat_pump': False, 'has_no_system_present': False,
|
||||
'has_community_scheme': False, 'has_ground_source_heat_pump': False,
|
||||
'has_no_system_present': False,
|
||||
'has_portable_electric_heaters': False, 'has_water_source_heat_pump': False,
|
||||
'has_electric_heat_pump': False, 'has_micro-cogeneration': False, 'has_solar_assisted_heat_pump': False,
|
||||
'has_exhaust_source_heat_pump': False, 'has_community_heat_pump': False, 'has_electric': False,
|
||||
'has_mains_gas': True, 'has_wood_logs': False, 'has_coal': False, 'has_oil': False,
|
||||
'has_wood_pellets': False, 'has_anthracite': False, 'has_dual_fuel_mineral_and_wood': False,
|
||||
'has_smokeless_fuel': False, 'has_lpg': False, 'has_b30k': False, 'has_electricaire': False,
|
||||
'has_assumed_for_most_rooms': False, 'has_underfloor_heating': False,
|
||||
'thermostatic_control': 'room thermostat', 'charging_system': 'Unknown', 'switch_system': 'programmer',
|
||||
'has_electric_heat_pump': False,
|
||||
'has_micro-cogeneration': False, 'has_solar_assisted_heat_pump': False,
|
||||
'has_exhaust_source_heat_pump': False,
|
||||
'has_community_heat_pump': False, 'has_electric': False, 'has_mains_gas': True,
|
||||
'has_wood_logs': False,
|
||||
'has_coal': False, 'has_oil': False, 'has_wood_pellets': False,
|
||||
'has_anthracite': False,
|
||||
'has_dual_fuel_mineral_and_wood': False, 'has_smokeless_fuel': False,
|
||||
'has_lpg': False, 'has_b30k': False,
|
||||
'has_electricaire': False, 'has_assumed_for_most_rooms': False,
|
||||
'has_underfloor_heating': False,
|
||||
'thermostatic_control': 'room thermostat', 'charging_system': 'Unknown',
|
||||
'switch_system': 'programmer',
|
||||
'no_control': 'Unknown', 'dhw_control': 'Unknown', 'community_heating': 'Unknown',
|
||||
'multiple_room_thermostats': False, 'auxiliary_systems': 'Unknown', 'trvs': 'Unknown',
|
||||
'multiple_room_thermostats': False, 'auxiliary_systems': 'Unknown',
|
||||
'trvs': 'Unknown',
|
||||
'rate_control': 'Unknown', 'glazing_type': 'single', 'fuel_type': 'mains gas',
|
||||
'main-fuel_tariff_type': 'Unknown', 'is_community': False,
|
||||
'no_individual_heating_or_community_network': False, 'complex_fuel_type': 'Unknown',
|
||||
'walls_thermal_transmittance_ending': 1.7, 'walls_thermal_transmittance_unit_ending': 'Unknown',
|
||||
'is_filled_cavity_ending': False, 'is_as_built_ending': True, 'walls_is_assumed_ending': True,
|
||||
'no_individual_heating_or_community_network': False,
|
||||
'complex_fuel_type': 'Unknown',
|
||||
'walls_thermal_transmittance_ending': 1.7,
|
||||
'walls_thermal_transmittance_unit_ending': 'Unknown',
|
||||
'is_filled_cavity_ending': False, 'is_as_built_ending': True,
|
||||
'walls_is_assumed_ending': True,
|
||||
'is_park_home_ending': False, 'walls_insulation_thickness_ending': 'none',
|
||||
'external_insulation_ending': False, 'internal_insulation_ending': False,
|
||||
'floor_thermal_transmittance_ending': 0.96, 'floor_insulation_thickness_ending': 'none',
|
||||
'floor_thermal_transmittance_ending': 0.96,
|
||||
'floor_insulation_thickness_ending': 'none',
|
||||
'roof_thermal_transmittance_ending': 2.3, 'is_at_rafters_ending': False,
|
||||
'roof_insulation_thickness_ending': 'none', 'heater_type_ending': 'Unknown',
|
||||
'system_type_ending': 'from main system', 'thermostat_characteristics_ending': 'Unknown',
|
||||
'system_type_ending': 'from main system',
|
||||
'thermostat_characteristics_ending': 'Unknown',
|
||||
'heating_scope_ending': 'Unknown', 'energy_recovery_ending': 'Unknown',
|
||||
'hotwater_tariff_type_ending': 'Unknown', 'extra_features_ending': 'Unknown',
|
||||
'chp_systems_ending': 'Unknown', 'distribution_system_ending': 'Unknown',
|
||||
'no_system_present_ending': 'Unknown', 'appliance_ending': 'Unknown', 'has_radiators_ending': True,
|
||||
'has_fan_coil_units_ending': False, 'has_pipes_in_screed_above_insulation_ending': False,
|
||||
'has_pipes_in_insulated_timber_floor_ending': False, 'has_pipes_in_concrete_slab_ending': False,
|
||||
'has_boiler_ending': True, 'has_air_source_heat_pump_ending': False, 'has_room_heaters_ending': False,
|
||||
'chp_systems_ending': 'Unknown',
|
||||
'distribution_system_ending': 'Unknown', 'no_system_present_ending': 'Unknown',
|
||||
'appliance_ending': 'Unknown',
|
||||
'has_radiators_ending': True, 'has_fan_coil_units_ending': False,
|
||||
'has_pipes_in_screed_above_insulation_ending': False,
|
||||
'has_pipes_in_insulated_timber_floor_ending': False,
|
||||
'has_pipes_in_concrete_slab_ending': False, 'has_boiler_ending': True,
|
||||
'has_air_source_heat_pump_ending': False, 'has_room_heaters_ending': False,
|
||||
'has_electric_storage_heaters_ending': False, 'has_warm_air_ending': False,
|
||||
'has_electric_underfloor_heating_ending': False, 'has_electric_ceiling_heating_ending': False,
|
||||
'has_electric_underfloor_heating_ending': False,
|
||||
'has_electric_ceiling_heating_ending': False,
|
||||
'has_community_scheme_ending': False, 'has_ground_source_heat_pump_ending': False,
|
||||
'has_no_system_present_ending': False, 'has_portable_electric_heaters_ending': False,
|
||||
'has_water_source_heat_pump_ending': False, 'has_electric_heat_pump_ending': False,
|
||||
'has_micro-cogeneration_ending': False, 'has_solar_assisted_heat_pump_ending': False,
|
||||
'has_exhaust_source_heat_pump_ending': False, 'has_community_heat_pump_ending': False,
|
||||
'has_electric_ending': False, 'has_mains_gas_ending': True, 'has_wood_logs_ending': False,
|
||||
'has_coal_ending': False, 'has_oil_ending': False, 'has_wood_pellets_ending': False,
|
||||
'has_no_system_present_ending': False,
|
||||
'has_portable_electric_heaters_ending': False,
|
||||
'has_water_source_heat_pump_ending': False,
|
||||
'has_electric_heat_pump_ending': False,
|
||||
'has_micro-cogeneration_ending': False,
|
||||
'has_solar_assisted_heat_pump_ending': False,
|
||||
'has_exhaust_source_heat_pump_ending': False,
|
||||
'has_community_heat_pump_ending': False,
|
||||
'has_electric_ending': False, 'has_mains_gas_ending': True,
|
||||
'has_wood_logs_ending': False,
|
||||
'has_coal_ending': False, 'has_oil_ending': False,
|
||||
'has_wood_pellets_ending': False,
|
||||
'has_anthracite_ending': False, 'has_dual_fuel_mineral_and_wood_ending': False,
|
||||
'has_smokeless_fuel_ending': False, 'has_lpg_ending': False, 'has_b30k_ending': False,
|
||||
'has_smokeless_fuel_ending': False, 'has_lpg_ending': False,
|
||||
'has_b30k_ending': False,
|
||||
'has_electricaire_ending': False, 'has_assumed_for_most_rooms_ending': False,
|
||||
'has_underfloor_heating_ending': False, 'thermostatic_control_ending': 'room thermostat',
|
||||
'charging_system_ending': 'Unknown', 'switch_system_ending': 'programmer', 'no_control_ending': 'Unknown',
|
||||
'has_underfloor_heating_ending': False,
|
||||
'thermostatic_control_ending': 'room thermostat',
|
||||
'charging_system_ending': 'Unknown', 'switch_system_ending': 'programmer',
|
||||
'no_control_ending': 'Unknown',
|
||||
'dhw_control_ending': 'Unknown', 'community_heating_ending': 'Unknown',
|
||||
'multiple_room_thermostats_ending': False, 'auxiliary_systems_ending': 'Unknown', 'trvs_ending': 'Unknown',
|
||||
'rate_control_ending': 'Unknown', 'glazing_type_ending': 'double', 'fuel_type_ending': 'mains gas',
|
||||
'multiple_room_thermostats_ending': False, 'auxiliary_systems_ending': 'Unknown',
|
||||
'trvs_ending': 'Unknown',
|
||||
'rate_control_ending': 'Unknown', 'glazing_type_ending': 'double',
|
||||
'fuel_type_ending': 'mains gas',
|
||||
'main-fuel_tariff_type_ending': 'Unknown', 'is_community_ending': False,
|
||||
'no_individual_heating_or_community_network_ending': False, 'complex_fuel_type_ending': 'Unknown',
|
||||
'sap_starting': 47, 'sap_ending': 47, 'heat_demand_starting': 478, 'heat_demand_ending': 478,
|
||||
'carbon_starting': 5.1, 'carbon_ending': 5.1, 'lighting_cost_starting': 91.0, 'lighting_cost_ending': 91.0,
|
||||
'heating_cost_starting': 1677.0, 'heating_cost_ending': 1677.0, 'hot_water_cost_starting': 161.0,
|
||||
'no_individual_heating_or_community_network_ending': False,
|
||||
'complex_fuel_type_ending': 'Unknown',
|
||||
'sap_starting': 47, 'sap_ending': 47, 'heat_demand_starting': 478,
|
||||
'heat_demand_ending': 478,
|
||||
'carbon_starting': 5.1, 'carbon_ending': 5.1, 'lighting_cost_starting': 91.0,
|
||||
'lighting_cost_ending': 91.0,
|
||||
'heating_cost_starting': 1677.0, 'heating_cost_ending': 1677.0,
|
||||
'hot_water_cost_starting': 161.0,
|
||||
'hot_water_cost_ending': 161.0, 'mechanical_ventilation_starting': 'natural',
|
||||
'mechanical_ventilation_ending': 'natural', 'secondheat_description_starting': 'None',
|
||||
'mechanical_ventilation_ending': 'natural',
|
||||
'secondheat_description_starting': 'None',
|
||||
'secondheat_description_ending': 'None', 'glazed_type_starting': 'not defined',
|
||||
'glazed_type_ending': 'double glazing installed during or after 2002',
|
||||
'multi_glaze_proportion_starting': 0.0, 'multi_glaze_proportion_ending': 100,
|
||||
'low_energy_lighting_starting': 100.0, 'low_energy_lighting_ending': 100.0,
|
||||
'number_open_fireplaces_starting': 0.0, 'number_open_fireplaces_ending': 0.0,
|
||||
'solar_water_heating_flag_starting': 'N', 'solar_water_heating_flag_ending': 'N',
|
||||
'photo_supply_starting': 0.0, 'photo_supply_ending': 0.0, 'transaction_type_starting': 'rental',
|
||||
'transaction_type_ending': 'rental', 'energy_tariff_starting': 'dual', 'energy_tariff_ending': 'dual',
|
||||
'extension_count_starting': 3.0, 'extension_count_ending': 3.0, 'total_floor_area_starting': 61.0,
|
||||
'total_floor_area_ending': 61.0, 'floor_height_starting': 2.37, 'floor_height_ending': 2.37,
|
||||
'hot_water_energy_eff_starting': 'Good', 'hot_water_energy_eff_ending': 'Good',
|
||||
'multi_glaze_proportion_starting': 0.0,
|
||||
'multi_glaze_proportion_ending': 100, 'low_energy_lighting_starting': 100.0,
|
||||
'low_energy_lighting_ending': 100.0, 'number_open_fireplaces_starting': 0.0,
|
||||
'number_open_fireplaces_ending': 0.0, 'solar_water_heating_flag_starting': 'N',
|
||||
'solar_water_heating_flag_ending': 'N', 'photo_supply_starting': 0.0,
|
||||
'photo_supply_ending': 0.0,
|
||||
'transaction_type_starting': 'rental', 'transaction_type_ending': 'rental',
|
||||
'energy_tariff_starting': 'dual',
|
||||
'energy_tariff_ending': 'dual', 'extension_count_starting': 3.0,
|
||||
'extension_count_ending': 3.0,
|
||||
'total_floor_area_starting': 61.0, 'total_floor_area_ending': 61.0,
|
||||
'floor_height_starting': 2.37,
|
||||
'floor_height_ending': 2.37, 'hot_water_energy_eff_starting': 'Good',
|
||||
'hot_water_energy_eff_ending': 'Good',
|
||||
'floor_energy_eff_starting': 'NO_RATING', 'floor_energy_eff_ending': 'NO_RATING',
|
||||
'windows_energy_eff_starting': 'Very Poor', 'windows_energy_eff_ending': 'Good',
|
||||
'walls_energy_eff_starting': 'Very Poor', 'walls_energy_eff_ending': 'Very Poor',
|
||||
'sheating_energy_eff_starting': 'NO_RATING', 'sheating_energy_eff_ending': 'NO_RATING',
|
||||
'sheating_energy_eff_starting': 'NO_RATING',
|
||||
'sheating_energy_eff_ending': 'NO_RATING',
|
||||
'roof_energy_eff_starting': 'Very Poor', 'roof_energy_eff_ending': 'Very Poor',
|
||||
'mainheat_energy_eff_starting': 'Good', 'mainheat_energy_eff_ending': 'Good',
|
||||
'mainheatc_energy_eff_starting': 'Average', 'mainheatc_energy_eff_ending': 'Average',
|
||||
'lighting_energy_eff_starting': 'Very Good', 'lighting_energy_eff_ending': 'Very Good',
|
||||
'mainheatc_energy_eff_starting': 'Average',
|
||||
'mainheatc_energy_eff_ending': 'Average',
|
||||
'lighting_energy_eff_starting': 'Very Good',
|
||||
'lighting_energy_eff_ending': 'Very Good',
|
||||
'number_habitable_rooms_starting': 4.0, 'number_habitable_rooms_ending': 4.0,
|
||||
'number_heated_rooms_starting': 4.0, 'number_heated_rooms_ending': 4.0, 'days_to_starting': 3642,
|
||||
'days_to_ending': 3713, 'estimated_perimeter_starting': 23.430749027719962,
|
||||
'number_heated_rooms_starting': 4.0, 'number_heated_rooms_ending': 4.0,
|
||||
'is_post_sap10_starting': False,
|
||||
'is_post_sap10_ending': False, 'lodgement_date_starting': '2024-07-21',
|
||||
'lodgement_date_ending': '2024-07-21',
|
||||
'days_to_starting': 3642, 'days_to_ending': 4189,
|
||||
'estimated_perimeter_starting': 23.430749027719962,
|
||||
'estimated_perimeter_ending': 23.430749027719962, 'has_glazing_ending': True,
|
||||
'glazing_coverage_ending': 'full', 'id': '1+1'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ from backend.app.db.models.recommendations import Recommendation, Plan, PlanReco
|
|||
from backend.app.db.models.portfolio import PropertyModel, PropertyDetailsEpcModel, PropertyDetailsSpatial
|
||||
from backend.app.db.functions.materials_functions import get_materials
|
||||
from collections import defaultdict
|
||||
from sqlalchemy import func
|
||||
|
||||
# PORTFOLIO_ID = 206
|
||||
# SCENARIOS = [389]
|
||||
|
|
@ -57,9 +58,44 @@ def get_data(portfolio_id, scenario_ids):
|
|||
# --------------------
|
||||
# Plans
|
||||
# --------------------
|
||||
plans_query = session.query(Plan).filter(
|
||||
Plan.scenario_id.in_(scenario_ids)
|
||||
).all()
|
||||
latest_plans_subq = (
|
||||
session.query(
|
||||
Plan.scenario_id,
|
||||
Plan.property_id,
|
||||
func.max(Plan.created_at).label("latest_created_at")
|
||||
)
|
||||
.filter(Plan.scenario_id.in_(scenario_ids))
|
||||
.group_by(
|
||||
Plan.scenario_id,
|
||||
Plan.property_id
|
||||
)
|
||||
.subquery()
|
||||
)
|
||||
|
||||
# plans_query = session.query(Plan).filter(
|
||||
# Plan.scenario_id.in_(scenario_ids)
|
||||
# ).all()
|
||||
|
||||
plans_query = (
|
||||
session.query(Plan)
|
||||
.join(
|
||||
latest_plans_subq,
|
||||
(Plan.scenario_id == latest_plans_subq.c.scenario_id) &
|
||||
(Plan.property_id == latest_plans_subq.c.property_id) &
|
||||
(Plan.created_at == latest_plans_subq.c.latest_created_at)
|
||||
)
|
||||
.all()
|
||||
)
|
||||
|
||||
# plans_query = (
|
||||
# session.query(Plan)
|
||||
# .join(
|
||||
# latest_plans_subq,
|
||||
# (Plan.scenario_id == latest_plans_subq.c.scenario_id) &
|
||||
# (Plan.created_at == latest_plans_subq.c.latest_created_at)
|
||||
# )
|
||||
# .all()
|
||||
# )
|
||||
|
||||
plans_data = [
|
||||
{col.name: getattr(plan, col.name) for col in Plan.__table__.columns}
|
||||
|
|
@ -73,7 +109,8 @@ def get_data(portfolio_id, scenario_ids):
|
|||
# --------------------
|
||||
recommendations_query = session.query(
|
||||
Recommendation,
|
||||
Plan.scenario_id
|
||||
Plan.scenario_id,
|
||||
PlanRecommendations.plan_id
|
||||
).join(
|
||||
PlanRecommendations,
|
||||
Recommendation.id == PlanRecommendations.recommendation_id
|
||||
|
|
@ -216,6 +253,7 @@ for scenario_id in SCENARIOS:
|
|||
[
|
||||
"landlord_property_id", "property_id", "uprn", "address", "postcode", "property_type", "walls", "roof",
|
||||
"heating", "windows", "current_epc_rating", "current_sap_points", "total_floor_area", "number_of_rooms",
|
||||
"id"
|
||||
]
|
||||
].merge(
|
||||
recommendations_measures_pivot, how="left", on="property_id"
|
||||
|
|
@ -223,17 +261,42 @@ for scenario_id in SCENARIOS:
|
|||
post_install_sap, how="left", on="property_id"
|
||||
)
|
||||
|
||||
df = df.drop(columns=["property_id"])
|
||||
# df = df.drop(columns=["property_id"])
|
||||
df["sap_points"] = df["sap_points"].fillna(0)
|
||||
|
||||
df["predicted_post_works_sap"] = df["current_sap_points"] + df["sap_points"]
|
||||
df["predicted_post_works_sap"] = df["predicted_post_works_sap"].round()
|
||||
df["predicted_post_works_sap"] = df["predicted_post_works_sap"]
|
||||
df["predicted_post_works_epc"] = df["predicted_post_works_sap"].apply(lambda x: sap_to_epc(x))
|
||||
df["uprn"] = df["uprn"].astype(str)
|
||||
|
||||
relevant_plans = plans_df[plans_df["scenario_id"] == scenario_id]
|
||||
df2 = df.merge(
|
||||
relevant_plans[["property_id", "post_sap_points", "post_epc_rating"]], how="left", on="property_id",
|
||||
suffixes=("", "_plan")
|
||||
)
|
||||
print(df2["predicted_post_works_epc"].value_counts())
|
||||
print(df2["post_epc_rating"].value_counts())
|
||||
|
||||
z = df2[
|
||||
(df2["predicted_post_works_epc"] != "D") &
|
||||
(df2["post_epc_rating"].astype(str) == "Epc.D")
|
||||
]
|
||||
|
||||
df2["predicted_post_works_epc"].value_counts()
|
||||
df2["post_epc_rating"].astype(str).value_counts()
|
||||
|
||||
df2[df2["total_retrofit_cost"] > 0].shape
|
||||
|
||||
getting_works = df[df["total_retrofit_cost"] > 0]
|
||||
getting_works["predicted_post_works_epc"].value_counts()
|
||||
|
||||
32565 / getting_works.shape[0]
|
||||
|
||||
df[df["predicted_post_works_sap"] == ""]
|
||||
|
||||
# Create excel to store to
|
||||
filename = ("/Users/khalimconn-kowlessar/Documents/hestia/Customers/Peabody/Nov 2025 Consulting "
|
||||
f"Project/Final SAL/{scenario_names[scenario_id]} - 20250113 final.xlsx")
|
||||
f"Project/Final SAL/scenarios/{scenario_names[scenario_id]} - 20250114 final.xlsx")
|
||||
with pd.ExcelWriter(filename) as writer:
|
||||
df.to_excel(writer, sheet_name="properties", index=False)
|
||||
|
||||
|
|
@ -388,3 +451,27 @@ asset_list.to_excel(
|
|||
condition_cost_comparison = asset_list[
|
||||
["condition_score", "decoration_sum_min ", "decoration_sum_max", "domna_condition_cost"]
|
||||
]
|
||||
|
||||
# Testing
|
||||
plans_df.head()
|
||||
|
||||
example = pd.read_excel(
|
||||
"/Users/khalimconn-kowlessar/Documents/hestia/Customers/Peabody/Nov 2025 Consulting Project/Final "
|
||||
"SAL/scenarios/EPC C - no solid floor, no EWI or IWI, ashp 3.0 - 20250114 final.xlsx"
|
||||
)
|
||||
|
||||
plans_df2 = plans_df.merge(
|
||||
properties_df[["property_id", "landlord_property_id"]],
|
||||
left_on="property_id",
|
||||
right_on="property_id",
|
||||
how="left"
|
||||
)
|
||||
|
||||
plans_df2 = plans_df2[plans_df2["scenario_id"] == 909]
|
||||
|
||||
dupes = plans_df2[plans_df2["property_id"].duplicated()]
|
||||
|
||||
# merge on plans
|
||||
example = example.merge(
|
||||
plans_df, how="left",
|
||||
)
|
||||
|
|
|
|||
3
tox.ini
3
tox.ini
|
|
@ -7,6 +7,7 @@ passenv = EPC_AUTH_TOKEN
|
|||
description = Install dependencies and run tests
|
||||
deps =
|
||||
-rbackend/engine/requirements.txt
|
||||
-rbackend/app/requirements/requirements.txt
|
||||
-rtest.requirements.txt
|
||||
commands = pytest
|
||||
commands = pytest {posargs}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue