diff --git a/.idea/Model.iml b/.idea/Model.iml
index 96ad7a95..df6c4faa 100644
--- a/.idea/Model.iml
+++ b/.idea/Model.iml
@@ -7,7 +7,7 @@
-
+
diff --git a/.idea/misc.xml b/.idea/misc.xml
index fb10c6b0..50cad4ca 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -3,7 +3,7 @@
-
+
diff --git a/asset_list/AssetList.py b/asset_list/AssetList.py
index eddeabdc..a6b8f973 100644
--- a/asset_list/AssetList.py
+++ b/asset_list/AssetList.py
@@ -1249,20 +1249,19 @@ class AssetList:
(self.standardised_asset_list["non-intrusives: Insulated"].isin(["RETRO DRILLED", "FILLED AT BUILD"])) &
(~self.standardised_asset_list['non-intrusives: Material'].isin(
["GREY LOOSE BEAD", "COMPACTED BEAD", "FIBRE BATT NO CAVITY", "EMPTY NARROW BELOW 30mm"]
- )
- )
+ ))
)
self.standardised_asset_list["non_intrusive_indicates_cavity_extraction"] = (
extraction_wall_filter & (
self.standardised_asset_list[self.ATTRIBUTE_SAP_THRESHOLD_AND_BELOW]
- )
- )
+ ))
# Also include work without the SAP filter as optimistic
self.standardised_asset_list["non_intrusive_indicates_cavity_extraction_no_sap_filter"] = (
- extraction_wall_filter
- )
+ extraction_wall_filter & (
+ ~self.standardised_asset_list[self.ATTRIBUTE_SAP_THRESHOLD_AND_BELOW]
+ ))
elif self.old_format_non_intrusives_present:
print("Review these categories with Kieran")
diff --git a/asset_list/app.py b/asset_list/app.py
index 13621448..f2a85ac3 100644
--- a/asset_list/app.py
+++ b/asset_list/app.py
@@ -418,7 +418,7 @@ def app():
epc_df = pd.concat(epc_data)
epc_df["estimated"] = epc_df["estimated"].fillna(False)
- z = epc_df[epc_df["domna_property_id"] == eg["domna_property_id"].values[0]]
+ epc_df["number-habitable-rooms"].mean() + 1
# We expand out the recommendations
recommendations_df = epc_df[[asset_list.DOMNA_PROPERTY_ID, "recommendations"]]
@@ -545,26 +545,19 @@ def app():
right_on=asset_list.STANDARD_LANDLORD_PROPERTY_ID
)
cavity_fills["cavity_reason"] = cavity_fills["cavity_reason"].fillna("Not identified")
- cavity_fills["cavity_reason"].value_counts()
+ print(cavity_fills["cavity_reason"].value_counts())
# Didn't identify 3 properties because they're bedsits
# 4 properties were identified, not based on the non-intrusives but instead because
# Westward said they were built in 2003/2007. Have adjusted this to use the age from the
# epc as well, as EPC says 1975 and they look like 1975 properties
- # 58 properties flagged as already having solar:
- #
-
- z = cavity_fills[
- cavity_fills["cavity_reason"] == "Non-Intrusive Data Showed Empty Cavity - property already has solar"
- ]
-
- df = asset_list.standardised_asset_list[
- asset_list.standardised_asset_list[asset_list.STANDARD_LANDLORD_PROPERTY_ID].isin(
- z[asset_list.landlord_property_id].values)
- ]
- eg = df[df[asset_list.STANDARD_LANDLORD_PROPERTY_ID] == "TOTNEWINA0102300"]
-
- z[["Address", "WFT EDIT Postcode", asset_list.landlord_property_id]]
- z[[asset_list.STANDARD_FULL_ADDRESS, asset_list.STANDARD_POSTCODE, asset_list.ATTRIBUTE_HAS_SOLAR]]
+ # 37 properties flagged as already having solar - these are all because the landlord said they have solar
+ # e.g.
+ # https://earth.google.com/web/search/11+Winsland+Avenue+TOTNES+TQ9+5FT/@50.43354465,-3.71318276,46.57468503a,
+ # 59.14004365d,35y,0h,0t,
+ # 0r/data=CpABGmISXAolMHg0ODZkMWQxOGE4NWRiZjdkOjB4YjBhM2E5M2Q3YWVlMWEwYhlZYgp7fzdJQCHFfC9027QNwCohMTEgV2luc2xhbmQgQXZlbnVlIFRPVE5FUyBUUTkgNUZUGAIgASImCiQJbxsQEoo3SUARXQcp_HE3SUAZBmiZGJ6yDcAhCA0fqq63DcBCAggBOgMKATBCAggASg0I____________ARAA
+ # https://earth.google.com/web/search/15+St+Anne%27s+Ct,+Newton+Abbot+TQ12+1TL/@50.53068337,-3.61611128,
+ # 11.74908956a,135.73212429d,35y,0h,0t,
+ # 0r/data=CpUBGmcSYQolMHg0ODZkMDVkMjFhODhjZjgxOjB4MjBmMzE2Zjc3MGI2NGMwYxlCxHLw8UNJQCFZqyzALe4MwComMTUgU3QgQW5uZSdzIEN0LCBOZXd0b24gQWJib3QgVFExMiAxVEwYAiABIiYKJAm-r6U2iDdJQBHS5ICRdDdJQBmYGVpmiLINwCG8wcrtqbYNwEICCAE6AwoBMEICCABKDQj___________8BEAA
# Check 2)
cavity_fills_with_solar = pd.read_excel(
@@ -580,37 +573,51 @@ def app():
right_on=asset_list.STANDARD_LANDLORD_PROPERTY_ID
)
cavity_fills_with_solar["cavity_reason"] = cavity_fills_with_solar["cavity_reason"].fillna("Not identified")
+ print(cavity_fills_with_solar["cavity_reason"].value_counts())
# 203 properties total
# 140 properties were flagged up based on non-intrusives (Non-Intrusive Data Showed Empty Cavity)
+ # 63 property already has solar
- check = cavity_fills_with_solar[
- cavity_fills_with_solar["cavity_reason"] == "Non-Intrusive Data Showed Empty Cavity"
- ]
- z = asset_list.standardised_asset_list[
- asset_list.standardised_asset_list[asset_list.STANDARD_LANDLORD_PROPERTY_ID].isin(
- check[asset_list.landlord_property_id].values)
+ # Check 3) RDF
+ rdf = pd.read_excel(
+ os.path.join(data_folder, "WESTWARD - Route March Prep.xlsx"),
+ sheet_name="RDF CIGA checks"
+ )
+ rdf = rdf.merge(
+ asset_list.standardised_asset_list[
+ [asset_list.STANDARD_LANDLORD_PROPERTY_ID, "cavity_reason", "solar_reason"]
+ ],
+ how="left",
+ left_on=asset_list.landlord_property_id,
+ right_on=asset_list.STANDARD_LANDLORD_PROPERTY_ID
+ )
+ rdf["cavity_reason"] = rdf["cavity_reason"].fillna("Not identified")
+ print(rdf["cavity_reason"].value_counts())
+ # 264 properties are not identified, 261 of which are due to the fact they contain materials
+ # The other 3 were determined to be eligible for solar instead
+ # Many of these units that were identified for rdf works could be solar jobs
+
+ rdf_with_solar = pd.read_excel(
+ os.path.join(data_folder, "WESTWARD - Route March Prep.xlsx"),
+ sheet_name="Solar PV - RDF CIGA Checks"
+ )
+ rdf_with_solar = rdf_with_solar.merge(
+ asset_list.standardised_asset_list[
+ [asset_list.STANDARD_LANDLORD_PROPERTY_ID, "cavity_reason", "solar_reason"]
+ ],
+ how="left",
+ left_on=asset_list.landlord_property_id,
+ right_on=asset_list.STANDARD_LANDLORD_PROPERTY_ID
+ )
+ rdf_with_solar["cavity_reason"] = rdf_with_solar["cavity_reason"].fillna("Not identified")
+ rdf_with_solar["cavity_reason"].value_counts()
+
+ # All others identified - some flagged as empties due to EPC or landlord data suggesting as much
+ # 5 not identified due to containing COMPACTED BEAD
+
+ asset_list.standardised_asset_list = asset_list.standardised_asset_list[
+ asset_list.standardised_asset_list[asset_list.landlord_property_id]
]
- z[asset_list.ATTRIBUTE_HAS_SOLAR].value_counts()
- pd.set_option('display.max_columns', None)
- z[[asset_list.STANDARD_FULL_ADDRESS, asset_list.STANDARD_POSTCODE, asset_list.ATTRIBUTE_HAS_SOLAR]]
-
- not_flagged = asset_list.standardised_asset_list[
- pd.isnull(asset_list.standardised_asset_list["solar_reason"])
- ]
- # For everything not flagged for solar, identify why
- reasons = []
- for _, x in not_flagged.iterrows():
- if x[asset_list.STANDARD_PROPERTY_TYPE] == "flat":
- reason = "property is a flat"
- else:
- x[asset_list.EPC_API_DATA_NAMES["mainheat-description"]]
-
- reasons.append(
- {
- asset_list.DOMNA_PROPERTY_ID: x["asset_list.DOMNA_PROPERTY_ID"],
- "solar_exclusion_reason": reason,
- }
- )
asset_list.load_contact_details(
local_filepath=os.path.join(data_folder, "Full property list wth D&V report V look up 12.2.25.xlsx"),
diff --git a/backend/Property.py b/backend/Property.py
index e6e43efe..5dcc76da 100644
--- a/backend/Property.py
+++ b/backend/Property.py
@@ -462,7 +462,7 @@ class Property:
if self.simulation_epcs is None:
raise ValueError("Simulation EPCs have not been created")
- rec_ids = sorted(list(self.simulation_epcs.keys()))
+ rec_ids = list(self.simulation_epcs.keys())
updated_simulation_epcs = []
for rec_id in rec_ids:
sim_epc = self.simulation_epcs[rec_id].copy()
@@ -488,8 +488,6 @@ class Property:
# Now we havet this data inthe
self.updated_simulation_epcs = updated_simulation_epcs
- return updated_simulation_epcs
-
@staticmethod
def create_recommendation_scoring_data(
property_id,
diff --git a/etl/customers/mod/pilot/2. Create Excel Model.py b/etl/customers/mod/pilot/2. Create Excel Model.py
index e656c96e..a74e22ec 100644
--- a/etl/customers/mod/pilot/2. Create Excel Model.py
+++ b/etl/customers/mod/pilot/2. Create Excel Model.py
@@ -78,7 +78,7 @@ def app():
# Set the inputs:
portfolio_id = 139
- scenario_ids = [233, 234]
+ scenario_ids = [237, 238]
properties_data, plans_data, recommendations_data = get_data(
portfolio_id=portfolio_id, scenario_ids=scenario_ids
@@ -299,6 +299,9 @@ def app():
[
"property_id", "uprn", "address", "postcode", "property_type", "walls", "roof", "heating", "windows",
"current_epc_rating", "current_sap_points", "total_floor_area", "number_of_rooms",
+ "co2_emissions", "current_energy_demand", "current_energy_demand_heating_hotwater",
+ "heating_cost_current", "hot_water_cost_current", "lighting_cost_current",
+ "appliances_cost_current", "gas_standing_charge", "electricity_standing_charge"
]
].merge(
recommendations_measures_pivot, how="left", on="property_id"
@@ -306,6 +309,11 @@ def app():
aggregated_metrics, how="left", on="property_id"
)
+ df["bills_total_cost"] = (
+ df["heating_cost_current"] + df["hot_water_cost_current"] + df["lighting_cost_current"] +
+ df["appliances_cost_current"] + df["gas_standing_charge"] + df["electricity_standing_charge"]
+ )
+
df = df.drop(columns=["property_id"])
for c in ["sap_points", "co2_equivalent_savings", "energy_cost_savings", "kwh_savings"]:
df[c] = df[c].fillna(0)
@@ -332,6 +340,11 @@ def app():
df["Predicted Post Works SAP"] = df["Predicted Post Works SAP"].round()
df["Predicted Post Works EPC"] = df["Predicted Post Works SAP"].apply(lambda x: sap_to_epc(x))
+ # Calculate the relative savings on carbon, kwh, and bills
+ df["relative_carbon_savings"] = df["co2_equivalent_savings"] / df["co2_emissions"]
+ df["relative_kwh_savings"] = df["kwh_savings"] / df["current_energy_demand"]
+ df["relative_bill_savings"] = df["energy_cost_savings"] / df["bills_total_cost"]
+
# For properties that don't make it to EPC B, check why. E.g. for a property that has an oil boiler, it
# the bills go up recommending HHRSH, so it doesn't make it to EPC B
# For mid-terrace units, use the ordnance survey API to check if there is space for a heat pump?
@@ -360,13 +373,47 @@ def app():
scenario_data[scenario] = df
- measure_counts = {}
- for scenario in scenario_ids:
- recommendation_cols = [c for c in scenario_data[scenario].columns if "Recommendation:" in c]
- measure_counts[scenario] = scenario_data[scenario][recommendation_cols].sum().to_dict()
+ printing_scenario_id = scenario_ids[0]
+ # EPC breakdown
+ print(scenario_data[printing_scenario_id]['Predicted Post Works EPC'].value_counts())
+ # Cost
+ # Total cost
+ print(scenario_data[printing_scenario_id]["total_cost"].sum())
+ # Base cost
+ print(scenario_data[printing_scenario_id]["estimated_cost"].sum())
+ # Contingency
+ print(scenario_data[printing_scenario_id]["contingency"].sum())
+ # Costs averaged per unit
+ print(scenario_data[printing_scenario_id]["total_cost"].mean())
+ print(scenario_data[printing_scenario_id]["estimated_cost"].mean())
+ print(scenario_data[printing_scenario_id]["contingency"].mean())
- pprint(measure_counts[scenario_ids[0]])
- pprint(measure_counts[scenario_ids[1]])
+ # Average relative savings
+ print(scenario_data[printing_scenario_id]["relative_carbon_savings"].mean())
+ print(scenario_data[printing_scenario_id]["relative_kwh_savings"].mean())
+ print(scenario_data[printing_scenario_id]["relative_bill_savings"].mean())
+
+ measure_details = {}
+ for scenario in scenario_ids:
+ measure_details[scenario] = {}
+ recommendation_cols = [c for c in scenario_data[scenario].columns if "Recommendation:" in c]
+ measure_details[scenario]["count"] = scenario_data[scenario][recommendation_cols].sum().to_dict()
+ # Get average cost per measure
+ measure_columns = [
+ c.split("Recommendation: ")[1] for c in scenario_data[scenario].columns if "Recommendation:" in c
+ ]
+ # Take the mean, drop zero columns
+ measure_costs = {}
+ for m in measure_columns:
+ measure_costs[m] = float(scenario_data[scenario][scenario_data[scenario][m] > 0][m].mean())
+ measure_details[scenario]["cost_per_measure"] = measure_costs
+
+ pprint(measure_details[scenario_ids[0]]["count"])
+ pprint(measure_details[scenario_ids[1]]["count"])
+
+ # Cost per measures
+ pprint(measure_details[scenario_ids[0]]["cost_per_measure"])
+ pprint(measure_details[scenario_ids[1]]["cost_per_measure"])
# Do not get to EPC B:
# 5 are flats
@@ -392,13 +439,20 @@ def app():
scenario_metrics = {}
for scenario in scenario_ids:
df = scenario_data[scenario].copy()
- df["cost_per_sap_point"] = df["total_cost"] / df["sap_points"]
- df["cost_per_carbon"] = df["total_cost"] / df["co2_equivalent_savings"]
+
avg_savings = df[
["sap_points", "co2_equivalent_savings", "energy_cost_savings", "kwh_savings", "estimated_cost",
- "cost_per_sap_point", "cost_per_carbon", "total_cost", "contingency"]
+ "total_cost", "contingency"]
].mean().to_dict()
+ avg_savings["cost_per_sap_point"] = avg_savings["total_cost"] / avg_savings["sap_points"]
+ avg_savings["cost_per_carbon"] = avg_savings["total_cost"] / avg_savings["co2_equivalent_savings"]
+ scenario_metrics[scenario] = avg_savings
+
+ pprint(scenario_metrics[scenario_ids[0]])
+ pprint(scenario_metrics[scenario_ids[1]])
# TODO: Add a slide on valuation improvement, on a sample of properties?
# TODO: Read in costing data and breakdown
+
+ zz = scenario_recommendations_df[scenario_recommendations_df["type"] == "mechanical_ventilation"]
diff --git a/recommendations/Costs.py b/recommendations/Costs.py
index 5e90be79..2d486191 100644
--- a/recommendations/Costs.py
+++ b/recommendations/Costs.py
@@ -104,7 +104,7 @@ INSTALLER_ASHP_COSTS = [
BOILER_UPGRADE_SCHEME_ASHP_VALUE = 7500
INSTALLER_SOLAR_BATTERY_COSTS = [
- {'capacity_kwh': 5, 'description': 'Battery Add on', 'cost': 2030.40, 'installer': 'CEG'},
+ {'capacity_kwh': 5, 'description': 'Battery Add on', 'cost': 3769.89, 'installer': 'JJC'},
# {'capacity_kwh': 10, 'description': 'Battery Add on', 'cost': 4300.00, 'installer': 'CEG'},
# {'capacity_kwh': 5, 'description': 'Battery Retrofit existing system', 'cost': 4250.00, 'installer': 'CEG'},
# {'capacity_kwh': 10, 'description': 'Battery Retrofit Existing system', 'cost': 5950.00, 'installer': 'CEG'}
@@ -193,6 +193,8 @@ class Costs:
# fittings and trimming doors, as well as scope for damage to the existing wall during preparation.
IWI_CONTINGENCY = 0.2
+ # For air source heat pumps, we inflate the assume cost by quite a bit to account for design and installation
+ ASHP_CONTINGENCY = 0.35
# Where there is more uncertainty, a higher contingency rate is used
HIGH_RISK_CONTINGENCY = 0.2
# When there is less uncertainty, a lower contingency rate is used
@@ -1168,7 +1170,7 @@ class Costs:
cost = [x for x in INSTALLER_ASHP_COSTS if x][0]["cost"]
# We add some contingency since there are additional costs such as resizing radiators, that could be required
- subtotal = cost * (1 + self.HIGH_RISK_CONTINGENCY)
+ subtotal = cost * (1 + self.ASHP_CONTINGENCY)
# The costs from installers exclude VAT
vat = subtotal * self.VAT_RATE
total_cost = subtotal + vat
diff --git a/recommendations/Recommendations.py b/recommendations/Recommendations.py
index 8a6b01ab..2e044e12 100644
--- a/recommendations/Recommendations.py
+++ b/recommendations/Recommendations.py
@@ -793,13 +793,26 @@ class Recommendations:
]
).sort_values(["phase", "recommendation_id"], ascending=True).reset_index(drop=True)
+ # We need the recommendaion type
+ rec_id_to_type = {
+ rec["recommendation_id"]: rec["type"] for recs in property_recommendations for rec in recs
+ }
+ rec_id_to_type[STARTING_DUMMY_ID_VALUE] = "starting_dummy"
+
for i in range(0, len(kwh_impact_table)):
- current_phase = kwh_impact_table.loc[i, 'phase']
+ current = kwh_impact_table.loc[i]
+ current_phase = current['phase']
previous_phase_id = (current_phase - 1) if (current_phase > 0) else -9999
previous_phase = kwh_impact_table[kwh_impact_table['phase'] == previous_phase_id]
if not previous_phase.empty:
for col in ["predictions_heating", "predictions_hotwater"]:
+ # Check if the recommendation type is ventilation
+ if rec_id_to_type[current["recommendation_id"]] == "mechanical_ventilation":
+ # We expect the kwh to increase
+ if kwh_impact_table.loc[i, col] > previous_phase[col].max():
+ continue
+
if kwh_impact_table.loc[i, col] > previous_phase[col].max():
kwh_impact_table.loc[i, col] = previous_phase[col].max()