mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
Merge pull request #612 from Hestia-Homes/eco-eligiblity-bug
minor debugging
This commit is contained in:
commit
6fdde5ee40
14 changed files with 722 additions and 217 deletions
|
|
@ -59,25 +59,26 @@ def app():
|
||||||
Property UPRN
|
Property UPRN
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Lambeth:
|
# Peabody data for cleaning
|
||||||
data_folder = "/Users/khalimconn-kowlessar/Documents/hestia/Customers/Lambeth/December 10th"
|
data_folder = ("/Users/khalimconn-kowlessar/Documents/hestia/Customers/Peabody/Nov 2025 Consulting "
|
||||||
data_filename = "lambeth_sw2_leigham court estate.xlsx"
|
"Project/data_validation")
|
||||||
|
data_filename = "to_standardise_uprns.xlsx"
|
||||||
sheet_name = "Sheet1"
|
sheet_name = "Sheet1"
|
||||||
postcode_column = 'Postcode'
|
postcode_column = 'Postcode'
|
||||||
address1_column = "Address"
|
address1_column = "Address 1"
|
||||||
address1_method = None
|
address1_method = None
|
||||||
fulladdress_column = None
|
fulladdress_column = None
|
||||||
address_cols_to_concat = ["Address"]
|
address_cols_to_concat = ["Address 1", "Address 2", "Address 3"]
|
||||||
missing_postcodes_method = None
|
missing_postcodes_method = None
|
||||||
landlord_year_built = None
|
landlord_year_built = None
|
||||||
landlord_os_uprn = None
|
landlord_os_uprn = None
|
||||||
landlord_property_type = None
|
landlord_property_type = "Type"
|
||||||
landlord_built_form = None
|
landlord_built_form = "Attachment"
|
||||||
landlord_wall_construction = None
|
landlord_wall_construction = None
|
||||||
landlord_roof_construction = None
|
landlord_roof_construction = None
|
||||||
landlord_heating_system = None
|
landlord_heating_system = None
|
||||||
landlord_existing_pv = None
|
landlord_existing_pv = None
|
||||||
landlord_property_id = "row_id"
|
landlord_property_id = "Org Ref"
|
||||||
landlord_sap = None
|
landlord_sap = None
|
||||||
outcomes_filename = None
|
outcomes_filename = None
|
||||||
outcomes_sheetname = None
|
outcomes_sheetname = None
|
||||||
|
|
@ -93,6 +94,40 @@ def app():
|
||||||
asset_list_header = 0
|
asset_list_header = 0
|
||||||
landlord_block_reference = None
|
landlord_block_reference = None
|
||||||
|
|
||||||
|
# Lambeth:
|
||||||
|
# data_folder = "/Users/khalimconn-kowlessar/Documents/hestia/Customers/Lambeth/December 10th"
|
||||||
|
# data_filename = "lambeth_sw2_leigham court estate.xlsx"
|
||||||
|
# sheet_name = "Sheet1"
|
||||||
|
# postcode_column = 'Postcode'
|
||||||
|
# address1_column = "Address"
|
||||||
|
# address1_method = None
|
||||||
|
# fulladdress_column = None
|
||||||
|
# address_cols_to_concat = ["Address"]
|
||||||
|
# missing_postcodes_method = None
|
||||||
|
# landlord_year_built = None
|
||||||
|
# landlord_os_uprn = None
|
||||||
|
# landlord_property_type = None
|
||||||
|
# landlord_built_form = None
|
||||||
|
# landlord_wall_construction = None
|
||||||
|
# landlord_roof_construction = None
|
||||||
|
# landlord_heating_system = None
|
||||||
|
# landlord_existing_pv = None
|
||||||
|
# landlord_property_id = "row_id"
|
||||||
|
# landlord_sap = None
|
||||||
|
# outcomes_filename = None
|
||||||
|
# outcomes_sheetname = None
|
||||||
|
# outcomes_postcode = None
|
||||||
|
# outcomes_houseno = None
|
||||||
|
# outcomes_id = None
|
||||||
|
# outcomes_address = None
|
||||||
|
# master_filepaths = []
|
||||||
|
# master_id_colnames = []
|
||||||
|
# master_to_asset_list_filepath = None
|
||||||
|
# phase = False
|
||||||
|
# ecosurv_landlords = None
|
||||||
|
# asset_list_header = 0
|
||||||
|
# landlord_block_reference = None
|
||||||
|
|
||||||
# Maps addresses to uprn in problematic cases
|
# Maps addresses to uprn in problematic cases
|
||||||
manual_uprn_map = {}
|
manual_uprn_map = {}
|
||||||
|
|
||||||
|
|
@ -230,22 +265,22 @@ def app():
|
||||||
)
|
)
|
||||||
|
|
||||||
# We now retrieve any failed properties
|
# We now retrieve any failed properties
|
||||||
chunk_failed = chunk[chunk[asset_list.DOMNA_PROPERTY_ID].isin(errors_chunk)]
|
# chunk_failed = chunk[chunk[asset_list.DOMNA_PROPERTY_ID].isin(errors_chunk)]
|
||||||
epc_data_failed, _, _ = get_data(
|
# epc_data_failed, _, _ = get_data(
|
||||||
df=chunk_failed,
|
# df=chunk_failed,
|
||||||
row_id_name=asset_list.DOMNA_PROPERTY_ID,
|
# row_id_name=asset_list.DOMNA_PROPERTY_ID,
|
||||||
uprn_column=AssetList.STANDARD_UPRN,
|
# uprn_column=AssetList.STANDARD_UPRN,
|
||||||
fulladdress_column=AssetList.STANDARD_FULL_ADDRESS,
|
# fulladdress_column=AssetList.STANDARD_FULL_ADDRESS,
|
||||||
address1_column=AssetList.STANDARD_ADDRESS_1,
|
# address1_column=AssetList.STANDARD_ADDRESS_1,
|
||||||
postcode_column=AssetList.STANDARD_POSTCODE,
|
# postcode_column=AssetList.STANDARD_POSTCODE,
|
||||||
property_type_column=AssetList.STANDARD_PROPERTY_TYPE,
|
# property_type_column=AssetList.STANDARD_PROPERTY_TYPE,
|
||||||
built_form_column=AssetList.STANDARD_BUILT_FORM,
|
# built_form_column=AssetList.STANDARD_BUILT_FORM,
|
||||||
manual_uprn_map=manual_uprn_map,
|
# manual_uprn_map=manual_uprn_map,
|
||||||
epc_api_only=epc_api_only,
|
# epc_api_only=epc_api_only,
|
||||||
epc_auth_token=EPC_AUTH_TOKEN
|
# epc_auth_token=EPC_AUTH_TOKEN
|
||||||
)
|
# )
|
||||||
|
#
|
||||||
epc_data_chunk.extend(epc_data_failed)
|
# epc_data_chunk.extend(epc_data_failed)
|
||||||
|
|
||||||
# Append the failed data to the main data
|
# Append the failed data to the main data
|
||||||
# Store the chunk locally as a csv
|
# Store the chunk locally as a csv
|
||||||
|
|
@ -422,3 +457,7 @@ def app():
|
||||||
|
|
||||||
if not asset_list.geographical_areas.empty:
|
if not asset_list.geographical_areas.empty:
|
||||||
asset_list.geographical_areas.to_excel(writer, sheet_name="Geographical Areas", index=False)
|
asset_list.geographical_areas.to_excel(writer, sheet_name="Geographical Areas", index=False)
|
||||||
|
|
||||||
|
# Store dupes
|
||||||
|
if not asset_list.duplicated_addresses.empty:
|
||||||
|
asset_list.duplicated_addresses.to_excel(writer, sheet_name="Duplicate Properties", index=False)
|
||||||
|
|
|
||||||
|
|
@ -458,6 +458,12 @@ BUILT_FORM_MAPPINGS = {
|
||||||
'Maisonette: Detached: Mid Floor': 'detached',
|
'Maisonette: Detached: Mid Floor': 'detached',
|
||||||
'Bungalow: EnclosedMidTerrace': 'enclosed mid-terrace',
|
'Bungalow: EnclosedMidTerrace': 'enclosed mid-terrace',
|
||||||
|
|
||||||
'House: EnclosedMidTerrace': 'enclosed mid-terrace'
|
'House: EnclosedMidTerrace': 'enclosed mid-terrace',
|
||||||
|
|
||||||
|
'EnclosedMidTerrace': 'enclosed mid-terrace',
|
||||||
|
'EnclosedEndTerrace': 'enclosed end-terrace',
|
||||||
|
'EndTerrace': 'end-terrace',
|
||||||
|
'SemiDetached': 'semi-detached',
|
||||||
|
'MidTerrace': 'mid-terrace'
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
from sqlalchemy import insert, delete, text
|
from sqlalchemy import insert, delete, select
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from sqlalchemy.exc import SQLAlchemyError
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
from backend.app.db.models.recommendations import (
|
from backend.app.db.models.recommendations import (
|
||||||
|
|
@ -242,20 +242,26 @@ def chunked(iterable, size=100):
|
||||||
yield iterable[i:i + size]
|
yield iterable[i:i + size]
|
||||||
|
|
||||||
|
|
||||||
|
# def fast_delete_recommendations(session, chunk):
|
||||||
|
# placeholders = ",".join(["(:p{})".format(i) for i in range(len(chunk))])
|
||||||
|
# params = {f"p{i}": chunk[i] for i in range(len(chunk))}
|
||||||
|
#
|
||||||
|
# sql = text(f"""
|
||||||
|
# WITH ids(property_id) AS (
|
||||||
|
# VALUES {placeholders}
|
||||||
|
# )
|
||||||
|
# DELETE FROM recommendation r
|
||||||
|
# USING ids
|
||||||
|
# WHERE r.property_id = ids.property_id;
|
||||||
|
# """)
|
||||||
|
#
|
||||||
|
# session.execute(sql, params, execution_options={"synchronize_session": False})
|
||||||
|
|
||||||
def fast_delete_recommendations(session, chunk):
|
def fast_delete_recommendations(session, chunk):
|
||||||
placeholders = ",".join(["(:p{})".format(i) for i in range(len(chunk))])
|
session.execute(
|
||||||
params = {f"p{i}": chunk[i] for i in range(len(chunk))}
|
delete(Recommendation)
|
||||||
|
.where(Recommendation.property_id.in_(chunk))
|
||||||
sql = text(f"""
|
)
|
||||||
WITH ids(property_id) AS (
|
|
||||||
VALUES {placeholders}
|
|
||||||
)
|
|
||||||
DELETE FROM recommendation r
|
|
||||||
USING ids
|
|
||||||
WHERE r.property_id = ids.property_id;
|
|
||||||
""")
|
|
||||||
|
|
||||||
session.execute(sql, params, execution_options={"synchronize_session": False})
|
|
||||||
|
|
||||||
|
|
||||||
def clear_portfolio(session: Session, portfolio_id: int, batch_size=100):
|
def clear_portfolio(session: Session, portfolio_id: int, batch_size=100):
|
||||||
|
|
@ -362,11 +368,19 @@ def clear_portfolio(session: Session, portfolio_id: int, batch_size=100):
|
||||||
# --------------------------
|
# --------------------------
|
||||||
# Recommendations (fast delete)
|
# Recommendations (fast delete)
|
||||||
# --------------------------
|
# --------------------------
|
||||||
rec_chunks = list(chunked(property_ids, batch_size))
|
# rec_chunks = list(chunked(property_ids, batch_size * 5)) # larger chunks for fast delete
|
||||||
|
# total = len(rec_chunks)
|
||||||
|
# for i, chunk in enumerate(rec_chunks, start=1):
|
||||||
|
# print_progress("Deleting Recommendations", i, total)
|
||||||
|
# fast_delete_recommendations(session, chunk)
|
||||||
|
rec_chunks = list(chunked(recommendation_ids, batch_size))
|
||||||
total = len(rec_chunks)
|
total = len(rec_chunks)
|
||||||
for i, chunk in enumerate(rec_chunks, start=1):
|
for i, chunk in enumerate(rec_chunks, start=1):
|
||||||
print_progress("Deleting Recommendations", i, total)
|
print_progress("Deleting Recommendations", i, total)
|
||||||
fast_delete_recommendations(session, chunk)
|
session.execute(
|
||||||
|
delete(Recommendation)
|
||||||
|
.where(Recommendation.id.in_(chunk))
|
||||||
|
)
|
||||||
|
|
||||||
# --------------------------
|
# --------------------------
|
||||||
# Inspections
|
# Inspections
|
||||||
|
|
@ -412,3 +426,114 @@ def clear_portfolio(session: Session, portfolio_id: int, batch_size=100):
|
||||||
|
|
||||||
session.commit()
|
session.commit()
|
||||||
print("Portfolio cleared.")
|
print("Portfolio cleared.")
|
||||||
|
|
||||||
|
|
||||||
|
def clear_portfolio_in_batches(
|
||||||
|
session: Session,
|
||||||
|
portfolio_id: int,
|
||||||
|
property_batch_size: int = 10
|
||||||
|
):
|
||||||
|
# Fetch all property IDs once
|
||||||
|
property_ids = [
|
||||||
|
pid for (pid,) in
|
||||||
|
session.query(PropertyModel.id)
|
||||||
|
.filter(PropertyModel.portfolio_id == portfolio_id)
|
||||||
|
.all()
|
||||||
|
]
|
||||||
|
|
||||||
|
def delete_for_property_batch(prop_ids):
|
||||||
|
# ----------------------------
|
||||||
|
# Recommendations → PlanRecommendations
|
||||||
|
# ----------------------------
|
||||||
|
rec_subq = (
|
||||||
|
select(Recommendation.id)
|
||||||
|
.where(Recommendation.property_id.in_(prop_ids))
|
||||||
|
)
|
||||||
|
|
||||||
|
session.execute(
|
||||||
|
delete(PlanRecommendations)
|
||||||
|
.where(PlanRecommendations.recommendation_id.in_(rec_subq))
|
||||||
|
)
|
||||||
|
|
||||||
|
session.execute(
|
||||||
|
delete(RecommendationMaterials)
|
||||||
|
.where(RecommendationMaterials.recommendation_id.in_(rec_subq))
|
||||||
|
)
|
||||||
|
|
||||||
|
session.execute(
|
||||||
|
delete(Recommendation)
|
||||||
|
.where(Recommendation.property_id.in_(prop_ids))
|
||||||
|
)
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# Inspections
|
||||||
|
# ----------------------------
|
||||||
|
session.execute(
|
||||||
|
delete(InspectionModel)
|
||||||
|
.where(InspectionModel.property_id.in_(prop_ids))
|
||||||
|
)
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# Plans (scoped to these properties)
|
||||||
|
# ----------------------------
|
||||||
|
plan_subq = (
|
||||||
|
select(Plan.id)
|
||||||
|
.where(Plan.property_id.in_(prop_ids))
|
||||||
|
)
|
||||||
|
|
||||||
|
session.execute(
|
||||||
|
delete(PlanRecommendations)
|
||||||
|
.where(PlanRecommendations.plan_id.in_(plan_subq))
|
||||||
|
)
|
||||||
|
|
||||||
|
session.execute(
|
||||||
|
delete(FundingPackageMeasures)
|
||||||
|
.where(
|
||||||
|
FundingPackageMeasures.funding_package_id.in_(
|
||||||
|
select(FundingPackage.id)
|
||||||
|
.where(FundingPackage.plan_id.in_(plan_subq))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
session.execute(
|
||||||
|
delete(FundingPackage)
|
||||||
|
.where(FundingPackage.plan_id.in_(plan_subq))
|
||||||
|
)
|
||||||
|
|
||||||
|
session.execute(
|
||||||
|
delete(Plan)
|
||||||
|
.where(Plan.id.in_(plan_subq))
|
||||||
|
)
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# Property-scoped auxiliary tables
|
||||||
|
# ----------------------------
|
||||||
|
session.execute(
|
||||||
|
delete(PropertyDetailsEpcModel)
|
||||||
|
.where(PropertyDetailsEpcModel.property_id.in_(prop_ids))
|
||||||
|
)
|
||||||
|
|
||||||
|
session.execute(
|
||||||
|
delete(PropertyTargetsModel)
|
||||||
|
.where(PropertyTargetsModel.property_id.in_(prop_ids))
|
||||||
|
)
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# Properties (last)
|
||||||
|
# ----------------------------
|
||||||
|
session.execute(
|
||||||
|
delete(PropertyModel)
|
||||||
|
.where(PropertyModel.id.in_(prop_ids))
|
||||||
|
)
|
||||||
|
|
||||||
|
# -------- BATCH DELETE LOOP --------
|
||||||
|
property_chunks = list(chunked(property_ids, property_batch_size))
|
||||||
|
total_batches = len(property_chunks)
|
||||||
|
|
||||||
|
for i, prop_ids in enumerate(property_chunks, start=1):
|
||||||
|
print(f"Deleting batch {i}/{total_batches} ({len(prop_ids)} properties)")
|
||||||
|
delete_for_property_batch(prop_ids)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
print("Portfolio cleared in batches.")
|
||||||
|
|
|
||||||
|
|
@ -662,7 +662,9 @@ async def model_engine(body: PlanTriggerRequest):
|
||||||
address1 = config.get("domna_address_1", None)
|
address1 = config.get("domna_address_1", None)
|
||||||
|
|
||||||
address1 = str(int(address1)) if isinstance(address1, float) else str(address1)
|
address1 = str(int(address1)) if isinstance(address1, float) else str(address1)
|
||||||
full_address = config.get("domna_full_address") if body.file_format == "domna_asset_list" else None
|
full_address = config.get("domna_full_address", "") if body.file_format == "domna_asset_list" else None
|
||||||
|
if not isinstance(full_address, str): # Catch for when the full address is nan
|
||||||
|
full_address = None
|
||||||
heating_system = parse_heating_system(config)
|
heating_system = parse_heating_system(config)
|
||||||
|
|
||||||
associated_uprns = []
|
associated_uprns = []
|
||||||
|
|
|
||||||
|
|
@ -290,6 +290,14 @@ class AnnualBillSavings:
|
||||||
# The solar thermal covers a % of the heating kwh, so we need to adjust the cost
|
# The solar thermal covers a % of the heating kwh, so we need to adjust the cost
|
||||||
return (kwh / cop) * assumptions.SOLAR_CONSUMPTION_PROPORTION * cls.ELECTRICITY_PRICE_CAP
|
return (kwh / cop) * assumptions.SOLAR_CONSUMPTION_PROPORTION * cls.ELECTRICITY_PRICE_CAP
|
||||||
|
|
||||||
|
if fuel in ['Oil + Solar Thermal']:
|
||||||
|
# The solar thermal covers a % of the heating kwh, so we need to adjust the cost
|
||||||
|
price_data = cls.FUEL_DATA[cls.FUEL_DATA["Fuel"] == "Kerosene"].squeeze()
|
||||||
|
cost_per_kwh = cls.cost_per_kwh(
|
||||||
|
price_data["Price (p)"], price_data["Energy Content, Net Calorific value (kWh/unit)"]
|
||||||
|
)
|
||||||
|
return (kwh / cop) * cost_per_kwh * assumptions.SOLAR_CONSUMPTION_PROPORTION
|
||||||
|
|
||||||
if fuel == "LPG + Solar Thermal":
|
if fuel == "LPG + Solar Thermal":
|
||||||
# The solar thermal covers a % of the heating kwh, so we need to adjust the cost
|
# The solar thermal covers a % of the heating kwh, so we need to adjust the cost
|
||||||
price_data = cls.FUEL_DATA[cls.FUEL_DATA["Fuel"] == "LPG"].squeeze()
|
price_data = cls.FUEL_DATA[cls.FUEL_DATA["Fuel"] == "LPG"].squeeze()
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,12 @@ costs_by_floor_area = epc_data[
|
||||||
][["TOTAL_FLOOR_AREA", "CURRENT_ENERGY_EFFICIENCY", "LIGHTING_COST_CURRENT", "HEATING_COST_CURRENT",
|
][["TOTAL_FLOOR_AREA", "CURRENT_ENERGY_EFFICIENCY", "LIGHTING_COST_CURRENT", "HEATING_COST_CURRENT",
|
||||||
"HOT_WATER_COST_CURRENT"]].copy()
|
"HOT_WATER_COST_CURRENT"]].copy()
|
||||||
|
|
||||||
|
epc_data = epc_data[
|
||||||
|
(epc_data["MAINHEAT_DESCRIPTION"].str.contains("SAP05:") == False) &
|
||||||
|
(~epc_data["LIGHTING_COST_CURRENT"].isin([None, ""])) &
|
||||||
|
(~pd.isnull(epc_data["LIGHTING_COST_CURRENT"]))
|
||||||
|
]
|
||||||
|
|
||||||
costs_by_floor_area.columns = [c.lower().replace("_", "-") for c in costs_by_floor_area.columns]
|
costs_by_floor_area.columns = [c.lower().replace("_", "-") for c in costs_by_floor_area.columns]
|
||||||
for c in ["lighting-cost-current", "heating-cost-current", "hot-water-cost-current"]:
|
for c in ["lighting-cost-current", "heating-cost-current", "hot-water-cost-current"]:
|
||||||
costs_by_floor_area[c + "_scaled"] = costs_by_floor_area[c] / costs_by_floor_area["total-floor-area"]
|
costs_by_floor_area[c + "_scaled"] = costs_by_floor_area[c] / costs_by_floor_area["total-floor-area"]
|
||||||
|
|
@ -92,8 +98,8 @@ costs_by_floor_area = costs_by_floor_area.groupby("current-energy-efficiency")[
|
||||||
|
|
||||||
epc_data = epc_data[~pd.isnull(epc_data["UPRN"])]
|
epc_data = epc_data[~pd.isnull(epc_data["UPRN"])]
|
||||||
|
|
||||||
sample_epc_data = epc_data[pd.to_datetime(epc_data["LODGEMENT_DATE"]) >= "2015-01-01"].drop_duplicates("UPRN").sample(
|
sample_epc_data = epc_data[pd.to_datetime(epc_data["LODGEMENT_DATE"]) >= "2008-01-01"].drop_duplicates("UPRN").sample(
|
||||||
10000).reset_index(drop=True)
|
50000).reset_index(drop=True)
|
||||||
|
|
||||||
# TODO: In Property find_energy_sources, sort out biomass community heating - what fuel type
|
# TODO: In Property find_energy_sources, sort out biomass community heating - what fuel type
|
||||||
# TODO: We might be able to remove find_energy_sources entirely and remove estimate_electrical_consumption. It's used
|
# TODO: We might be able to remove find_energy_sources entirely and remove estimate_electrical_consumption. It's used
|
||||||
|
|
@ -163,6 +169,8 @@ mocked_kwh_predictions["heating_kwh_predictions"] = pd.DataFrame(mocked_kwh_pred
|
||||||
mocked_kwh_predictions["hotwater_kwh_predictions"] = pd.DataFrame(mocked_kwh_predictions["hotwater_kwh_predictions"])
|
mocked_kwh_predictions["hotwater_kwh_predictions"] = pd.DataFrame(mocked_kwh_predictions["hotwater_kwh_predictions"])
|
||||||
|
|
||||||
# TODO: We might want to implement this generally, via an ETL process
|
# TODO: We might want to implement this generally, via an ETL process
|
||||||
|
for x in cleaned["mainheat-description"]:
|
||||||
|
x["has_wood_chips"] = False
|
||||||
for p in input_properties:
|
for p in input_properties:
|
||||||
for col in ["lighting-cost-current", "heating-cost-current", "hot-water-cost-current"]:
|
for col in ["lighting-cost-current", "heating-cost-current", "hot-water-cost-current"]:
|
||||||
if pd.isnull(p.data[col]):
|
if pd.isnull(p.data[col]):
|
||||||
|
|
@ -313,6 +321,10 @@ for p in tqdm(input_properties):
|
||||||
if not recommendations.get(p.id):
|
if not recommendations.get(p.id):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Temp allow to skip
|
||||||
|
if not isinstance(recommendations.get(p.id)[0], list):
|
||||||
|
continue
|
||||||
|
|
||||||
# we need to double unlist because we have a list of lists
|
# we need to double unlist because we have a list of lists
|
||||||
property_measure_types = {rec["type"] for recs in recommendations[p.id] for rec in recs}
|
property_measure_types = {rec["type"] for recs in recommendations[p.id] for rec in recs}
|
||||||
property_required_measures = [m for m in recommendations[p.id] if m[0]["type"] in body.required_measures]
|
property_required_measures = [m for m in recommendations[p.id] if m[0]["type"] in body.required_measures]
|
||||||
|
|
@ -336,32 +348,32 @@ for p in tqdm(input_properties):
|
||||||
)
|
)
|
||||||
gain = optimiser_functions.calculate_gain(body=body, p=p, fixed_gain=fixed_gain, eco_packages=eco_packages)
|
gain = optimiser_functions.calculate_gain(body=body, p=p, fixed_gain=fixed_gain, eco_packages=eco_packages)
|
||||||
|
|
||||||
funding = Funding(
|
# funding = Funding(
|
||||||
tenure=body.housing_type,
|
# tenure=body.housing_type,
|
||||||
project_scores_matrix=project_scores_matrix,
|
# project_scores_matrix=project_scores_matrix,
|
||||||
partial_project_scores_matrix=partial_project_scores_matrix,
|
# partial_project_scores_matrix=partial_project_scores_matrix,
|
||||||
whlg_eligible_postcodes=whlg_eligible_postcodes,
|
# whlg_eligible_postcodes=whlg_eligible_postcodes,
|
||||||
eco4_social_cavity_abs_rate=13,
|
# eco4_social_cavity_abs_rate=13,
|
||||||
eco4_social_solid_abs_rate=17,
|
# eco4_social_solid_abs_rate=17,
|
||||||
eco4_private_cavity_abs_rate=13,
|
# eco4_private_cavity_abs_rate=13,
|
||||||
eco4_private_solid_abs_rate=17,
|
# eco4_private_solid_abs_rate=17,
|
||||||
gbis_social_cavity_abs_rate=21,
|
# gbis_social_cavity_abs_rate=21,
|
||||||
gbis_social_solid_abs_rate=25,
|
# gbis_social_solid_abs_rate=25,
|
||||||
gbis_private_cavity_abs_rate=21,
|
# gbis_private_cavity_abs_rate=21,
|
||||||
gbis_private_solid_abs_rate=28,
|
# gbis_private_solid_abs_rate=28,
|
||||||
)
|
# )
|
||||||
|
#
|
||||||
li_thickness = convert_thickness_to_numeric(
|
# li_thickness = convert_thickness_to_numeric(
|
||||||
p.roof["insulation_thickness"], p.roof["is_pitched"], p.roof["is_flat"]
|
# p.roof["insulation_thickness"], p.roof["is_pitched"], p.roof["is_flat"]
|
||||||
)
|
# )
|
||||||
current_wall_u_value = p.walls["thermal_transmittance"]
|
# current_wall_u_value = p.walls["thermal_transmittance"]
|
||||||
if current_wall_u_value is None:
|
# if current_wall_u_value is None:
|
||||||
current_wall_u_value = get_wall_u_value(
|
# current_wall_u_value = get_wall_u_value(
|
||||||
clean_description=p.walls["clean_description"],
|
# clean_description=p.walls["clean_description"],
|
||||||
age_band=p.age_band,
|
# age_band=p.age_band,
|
||||||
is_granite_or_whinstone=p.walls["is_granite_or_whinstone"],
|
# is_granite_or_whinstone=p.walls["is_granite_or_whinstone"],
|
||||||
is_sandstone_or_limestone=p.walls["is_sandstone_or_limestone"],
|
# is_sandstone_or_limestone=p.walls["is_sandstone_or_limestone"],
|
||||||
)
|
# )
|
||||||
|
|
||||||
# We insert the innovation uplift
|
# We insert the innovation uplift
|
||||||
measures_to_optimise_with_uplift = deepcopy(measures_to_optimise)
|
measures_to_optimise_with_uplift = deepcopy(measures_to_optimise)
|
||||||
|
|
@ -369,35 +381,39 @@ for p in tqdm(input_properties):
|
||||||
# TODO: Turn this into a function and store the innovaiton uplift
|
# TODO: Turn this into a function and store the innovaiton uplift
|
||||||
for group in measures_to_optimise_with_uplift:
|
for group in measures_to_optimise_with_uplift:
|
||||||
for r in group:
|
for r in group:
|
||||||
|
(r["partial_project_score"], r["partial_project_funding"], r["innovation_uplift"],
|
||||||
if r["type"] in ["mechanical_ventilation", "low_energy_lighting", "secondary_heating",
|
r["uplift_project_score"]) = (
|
||||||
"extension_cavity_wall_insulation", "draught_proofing", "sealing_open_fireplace"]:
|
0, 0, 0, 0
|
||||||
(
|
|
||||||
r["partial_project_score"],
|
|
||||||
r["partial_project_funding"],
|
|
||||||
r["innovation_uplift"],
|
|
||||||
r["uplift_project_score"],
|
|
||||||
) = (
|
|
||||||
0, 0, 0, 0
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
|
|
||||||
(
|
|
||||||
r["partial_project_score"], r["partial_project_funding"], r["innovation_uplift"],
|
|
||||||
r["uplift_project_score"]
|
|
||||||
) = funding.get_innovation_uplift(
|
|
||||||
measure=r,
|
|
||||||
starting_sap=int(p.data["current-energy-efficiency"]),
|
|
||||||
floor_area=p.floor_area,
|
|
||||||
is_cavity=p.walls["is_cavity_wall"],
|
|
||||||
current_wall_uvalue=current_wall_u_value,
|
|
||||||
is_partial="partial" in p.walls["clean_description"].lower(),
|
|
||||||
existing_li_thickness=li_thickness,
|
|
||||||
mainheating=p.main_heating,
|
|
||||||
main_fuel=p.main_fuel,
|
|
||||||
mainheat_energy_eff=p.data["mainheat-energy-eff"],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# if r["type"] in ["mechanical_ventilation", "low_energy_lighting", "secondary_heating",
|
||||||
|
# "extension_cavity_wall_insulation", "draught_proofing", "sealing_open_fireplace"]:
|
||||||
|
# (
|
||||||
|
# r["partial_project_score"],
|
||||||
|
# r["partial_project_funding"],
|
||||||
|
# r["innovation_uplift"],
|
||||||
|
# r["uplift_project_score"],
|
||||||
|
# ) = (
|
||||||
|
# 0, 0, 0, 0
|
||||||
|
# )
|
||||||
|
# continue
|
||||||
|
#
|
||||||
|
# (
|
||||||
|
# r["partial_project_score"], r["partial_project_funding"], r["innovation_uplift"],
|
||||||
|
# r["uplift_project_score"]
|
||||||
|
# ) = funding.get_innovation_uplift(
|
||||||
|
# measure=r,
|
||||||
|
# starting_sap=int(p.data["current-energy-efficiency"]),
|
||||||
|
# floor_area=p.floor_area,
|
||||||
|
# is_cavity=p.walls["is_cavity_wall"],
|
||||||
|
# current_wall_uvalue=current_wall_u_value,
|
||||||
|
# is_partial="partial" in p.walls["clean_description"].lower(),
|
||||||
|
# existing_li_thickness=li_thickness,
|
||||||
|
# mainheating=p.main_heating,
|
||||||
|
# main_fuel=p.main_fuel,
|
||||||
|
# mainheat_energy_eff=p.data["mainheat-energy-eff"],
|
||||||
|
# )
|
||||||
|
|
||||||
if r["already_installed"]:
|
if r["already_installed"]:
|
||||||
# if already installed, we zero out the uplift and funding
|
# if already installed, we zero out the uplift and funding
|
||||||
(r["partial_project_score"], r["partial_project_funding"], r["innovation_uplift"],
|
(r["partial_project_score"], r["partial_project_funding"], r["innovation_uplift"],
|
||||||
|
|
@ -411,7 +427,7 @@ for p in tqdm(input_properties):
|
||||||
)
|
)
|
||||||
|
|
||||||
# When the goal is Increasing EPC, we can run the funding optimiser
|
# When the goal is Increasing EPC, we can run the funding optimiser
|
||||||
if body.goal == "Increasing EPC":
|
if body.goal == "Switch off":
|
||||||
|
|
||||||
solutions = optimise_with_funding_paths(
|
solutions = optimise_with_funding_paths(
|
||||||
p=p,
|
p=p,
|
||||||
|
|
@ -481,37 +497,43 @@ for p in tqdm(input_properties):
|
||||||
ROOF_INSULATION_MEASURES
|
ROOF_INSULATION_MEASURES
|
||||||
)
|
)
|
||||||
|
|
||||||
funding.check_funding(
|
# funding.check_funding(
|
||||||
measures=solution,
|
# measures=solution,
|
||||||
starting_sap=int(p.data["current-energy-efficiency"]),
|
# starting_sap=int(p.data["current-energy-efficiency"]),
|
||||||
ending_sap=int(p.data["current-energy-efficiency"]) + sum([x["gain"] for x in solution]),
|
# ending_sap=int(p.data["current-energy-efficiency"]) + sum([x["gain"] for x in solution]),
|
||||||
floor_area=p.floor_area,
|
# floor_area=p.floor_area,
|
||||||
mainheat_description=p.main_heating["clean_description"],
|
# mainheat_description=p.main_heating["clean_description"],
|
||||||
heating_control_description=p.main_heating_controls["clean_description"],
|
# heating_control_description=p.main_heating_controls["clean_description"],
|
||||||
is_cavity=p.walls["is_cavity_wall"],
|
# is_cavity=p.walls["is_cavity_wall"],
|
||||||
current_wall_uvalue=current_wall_u_value,
|
# current_wall_uvalue=current_wall_u_value,
|
||||||
is_partial="partial" in p.walls["clean_description"].lower(),
|
# is_partial="partial" in p.walls["clean_description"].lower(),
|
||||||
existing_li_thickness=li_thickness,
|
# existing_li_thickness=li_thickness,
|
||||||
mainheating=p.main_heating,
|
# mainheating=p.main_heating,
|
||||||
main_fuel=p.main_fuel,
|
# main_fuel=p.main_fuel,
|
||||||
mainheat_energy_eff=p.data["mainheat-energy-eff"],
|
# mainheat_energy_eff=p.data["mainheat-energy-eff"],
|
||||||
has_wall_insulation_recommendation=has_wall_insulation_recommendation,
|
# has_wall_insulation_recommendation=has_wall_insulation_recommendation,
|
||||||
has_roof_insulation_recommendation=has_roof_insulation_recommendation,
|
# has_roof_insulation_recommendation=has_roof_insulation_recommendation,
|
||||||
)
|
# )
|
||||||
|
|
||||||
# Determine the scheme
|
# Determine the scheme
|
||||||
scheme = "none"
|
scheme = "none"
|
||||||
if funding.eco4_eligible:
|
# if funding.eco4_eligible:
|
||||||
scheme = "eco4"
|
# scheme = "eco4"
|
||||||
if scheme == "none" and funding.gbis_eligible:
|
# if scheme == "none" and funding.gbis_eligible:
|
||||||
scheme = "gbis"
|
# scheme = "gbis"
|
||||||
|
|
||||||
funded_measures = solution if scheme in ["gbis", "eco4"] else []
|
funded_measures = []
|
||||||
project_funding = 0 if funding.full_project_abs is not None else funding.full_project_abs
|
# funded_measures = solution if scheme in ["gbis", "eco4"] else []
|
||||||
total_uplift = funding.eco4_uplift
|
# project_funding = 0 if funding.full_project_abs is not None else funding.full_project_abs
|
||||||
full_project_score = 0 if funding.full_project_abs is not None else funding.full_project_abs
|
project_funding = 0
|
||||||
partial_project_score = funding.partial_project_abs
|
# total_uplift = funding.eco4_uplift
|
||||||
uplift_project_score = funding.eco4_uplift if scheme == "eco4" else funding.gbis_uplift
|
total_uplift = 0
|
||||||
|
# full_project_score = 0 if funding.full_project_abs is not None else funding.full_project_abs
|
||||||
|
full_project_score = 0
|
||||||
|
# partial_project_score = funding.partial_project_abs
|
||||||
|
partial_project_score = 0
|
||||||
|
# uplift_project_score = funding.eco4_uplift if scheme == "eco4" else funding.gbis_uplift
|
||||||
|
uplift_project_score = 0
|
||||||
|
|
||||||
selected = {r["id"] for r in solution}
|
selected = {r["id"] for r in solution}
|
||||||
|
|
||||||
|
|
|
||||||
47
etl/customers/lincs_rural/get_missed.py
Normal file
47
etl/customers/lincs_rural/get_missed.py
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
# After going back to Lincs rural, they gave us some additional data that we can use to try to fetch missed UPRNs again
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
# missed = pd.read_excel(
|
||||||
|
# "/Users/khalimconn-kowlessar/Downloads/lincs_rural_missed_nov_2025.xlsx",
|
||||||
|
# sheet_name="Missed Properties"
|
||||||
|
# )
|
||||||
|
# missed = missed[~pd.isnull(missed["rrn"])]
|
||||||
|
|
||||||
|
prepared = pd.read_excel(
|
||||||
|
"/Users/khalimconn-kowlessar/Downloads/lincs_rural_standardised_ara_nov_2025.xlsx",
|
||||||
|
sheet_name="Standardised Asset List"
|
||||||
|
)
|
||||||
|
|
||||||
|
updated_data = pd.read_excel(
|
||||||
|
"/Users/khalimconn-kowlessar/Downloads/MASTER LIST EPCS UPDATED November 2025 Domna Homes - Copy.xlsx",
|
||||||
|
sheet_name="PROPERTY EPC RATINGS"
|
||||||
|
)
|
||||||
|
updated_data = updated_data[~pd.isnull(updated_data["Property Ref."])]
|
||||||
|
|
||||||
|
missed = updated_data[~updated_data["Property Ref."].isin(prepared["landlord_property_id"].values.tolist())].copy()
|
||||||
|
# missed.to_csv("/Users/khalimconn-kowlessar/Downloads/lincs_rural_missed_uprn.csv")
|
||||||
|
# We'll grab the UPRNs manually and then pull them in, and prepare for ARA
|
||||||
|
|
||||||
|
missing_uprns = pd.read_csv("/Users/khalimconn-kowlessar/Downloads/lincs_rural_missed_uprn.csv")
|
||||||
|
|
||||||
|
missing_uprns["landlord_property_id"] = missing_uprns["Property Ref."].copy()
|
||||||
|
missing_uprns["domna_property_id"] = missing_uprns["Property Ref."].copy()
|
||||||
|
missing_uprns["domna_address_1"] = missing_uprns['Unnamed: 1'].str.split(",").str[0].str.strip()
|
||||||
|
missing_uprns["postcode"] = missing_uprns['Unnamed: 1'].str.split(",").str[-1].str.strip()
|
||||||
|
missing_uprns["landlord_property_type"] = "unknown"
|
||||||
|
missing_uprns["landlord_built_form"] = "unknown"
|
||||||
|
missing_uprns["domna_full_address"] = missing_uprns['Unnamed: 1'].copy()
|
||||||
|
|
||||||
|
missed_standardised_for_ara = missing_uprns[
|
||||||
|
['landlord_property_id', 'domna_address_1', 'landlord_property_type', 'landlord_built_form', 'postcode',
|
||||||
|
'domna_property_id', 'UPRN']
|
||||||
|
].rename(
|
||||||
|
columns={"UPRN": "epc_os_uprn"}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Store
|
||||||
|
missed_standardised_for_ara.to_excel(
|
||||||
|
"/Users/khalimconn-kowlessar/Downloads/lincs_rural_missed_standardised_ara_nov_2025.xlsx",
|
||||||
|
index=False,
|
||||||
|
sheet_name="Standardised Asset List"
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,147 @@
|
||||||
|
"""
|
||||||
|
We have found, within the Peabody data, a large volume of properties with missing and incorrects
|
||||||
|
UPRNS and incorrect address data. We want to flag these records and also find missings where we can
|
||||||
|
|
||||||
|
We also have duplicate UPRNS that should be flagged
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import os
|
||||||
|
import pandas as pd
|
||||||
|
import numpy as np
|
||||||
|
from tqdm import tqdm
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from asset_list.utils import get_data_for_property
|
||||||
|
from utils.logger import setup_logger
|
||||||
|
from utils.s3 import read_io_from_s3, save_dataframe_to_s3_parquet, read_dataframe_from_s3_parquet
|
||||||
|
|
||||||
|
logger = setup_logger()
|
||||||
|
|
||||||
|
load_dotenv(dotenv_path="backend/.env")
|
||||||
|
EPC_AUTH_TOKEN = os.getenv("EPC_AUTH_TOKEN")
|
||||||
|
|
||||||
|
sustainability_data = pd.read_excel(
|
||||||
|
"/Users/khalimconn-kowlessar/Documents/hestia/Customers/Peabody/Nov 2025 Consulting Project/2025_11_11 - Peabody "
|
||||||
|
"- Data Extracts for Domna.xlsx",
|
||||||
|
sheet_name="Sustainability"
|
||||||
|
)
|
||||||
|
property_data = pd.read_excel(
|
||||||
|
"/Users/khalimconn-kowlessar/Documents/hestia/Customers/Peabody/Nov 2025 Consulting Project/2025_11_11 - Peabody "
|
||||||
|
"- Data Extracts for Domna.xlsx",
|
||||||
|
sheet_name="Properties"
|
||||||
|
)
|
||||||
|
|
||||||
|
missing_uprns = sustainability_data[pd.isnull(sustainability_data['UPRN'])].copy()
|
||||||
|
|
||||||
|
# Any non-numeric UPRNS or leading with 0s are invalid
|
||||||
|
non_numeric_uprns = sustainability_data[
|
||||||
|
~sustainability_data['UPRN'].astype(str).str.match(r'^[1-9][0-9]*$') & ~pd.isnull(sustainability_data['UPRN'])
|
||||||
|
].copy()
|
||||||
|
# 70 properties
|
||||||
|
leading_zero_uprns = sustainability_data[
|
||||||
|
sustainability_data['UPRN'].astype(str).str.startswith('0')
|
||||||
|
].copy()
|
||||||
|
|
||||||
|
# Flag duplicates
|
||||||
|
duplicate_uprns = sustainability_data[
|
||||||
|
sustainability_data.duplicated(subset=['UPRN'], keep=False) & ~pd.isnull(sustainability_data['UPRN'])
|
||||||
|
].copy()
|
||||||
|
|
||||||
|
# Store this data
|
||||||
|
# missing_uprns.to_csv("/Users/khalimconn-kowlessar/Documents/hestia/Customers/Peabody/Nov 2025 Consulting
|
||||||
|
# Project/data_validation/missing_uprns.csv", index=False)
|
||||||
|
# non_numeric_uprns.to_csv("/Users/khalimconn-kowlessar/Documents/hestia/Customers/Peabody/Nov 2025 Consulting
|
||||||
|
# Project/data_validation/non_numeric_uprns.csv", index=False)
|
||||||
|
# leading_zero_uprns.to_csv("/Users/khalimconn-kowlessar/Documents/hestia/Customers/Peabody/Nov 2025 Consulting
|
||||||
|
# Project/data_validation/leading_zero_uprns.csv", index=False)
|
||||||
|
# duplicate_uprns.to_csv("/Users/khalimconn-kowlessar/Documents/hestia/Customers/Peabody/Nov 2025 Consulting
|
||||||
|
# Project/data_validation/duplicate_uprns.csv", index=False)
|
||||||
|
|
||||||
|
# Take everything remaining
|
||||||
|
data_needing_validation = sustainability_data[
|
||||||
|
~sustainability_data["Org Ref"].isin(
|
||||||
|
missing_uprns["Org Ref"].values.tolist() + non_numeric_uprns["Org Ref"].values.tolist() +
|
||||||
|
leading_zero_uprns["Org Ref"].values.tolist() + duplicate_uprns["Org Ref"].values.tolist()
|
||||||
|
)
|
||||||
|
].copy()
|
||||||
|
|
||||||
|
# TODO: We should build a SAL for UPRNS that are missing, invalid or duplicated
|
||||||
|
|
||||||
|
# We check UPRN validity against our OS data
|
||||||
|
uprn_filenames = read_dataframe_from_s3_parquet(
|
||||||
|
bucket_name="retrofit-data-dev", file_key="spatial/filename_meta.parquet"
|
||||||
|
)
|
||||||
|
|
||||||
|
# We're going to:
|
||||||
|
# 1) Grab a filename
|
||||||
|
# 2) Read it in
|
||||||
|
# 3) Check which UPRNS from our data are in that file
|
||||||
|
# 4) Keep a record of which UPRNS were found where
|
||||||
|
|
||||||
|
for uprn_file in tqdm(uprn_filenames['filenames'].values, total=len(uprn_filenames)):
|
||||||
|
spatial_data = read_dataframe_from_s3_parquet(
|
||||||
|
bucket_name="retrofit-data-dev", file_key=f"spatial/{uprn_file}"
|
||||||
|
)
|
||||||
|
|
||||||
|
uprns_in_file = data_needing_validation[
|
||||||
|
data_needing_validation['UPRN'].astype('Int64').isin(spatial_data['UPRN'].astype('Int64').values)
|
||||||
|
].copy()
|
||||||
|
|
||||||
|
print("Found {} UPRNS in file {}".format(len(uprns_in_file), uprn_file))
|
||||||
|
if len(uprns_in_file) > 0:
|
||||||
|
# Store the found UPRNS in the validation cache
|
||||||
|
data_to_store = uprns_in_file[["Org Ref", "UPRN"]].copy()
|
||||||
|
data_to_store["Source File"] = uprn_file
|
||||||
|
# Store
|
||||||
|
data_to_store.to_csv(
|
||||||
|
"/Users/khalimconn-kowlessar/Documents/hestia/Customers/Peabody/Nov 2025 Consulting "
|
||||||
|
f"Project/data_validation/validation_cache/{uprn_file.split('.parquet')[0]}_found_uprns.csv",
|
||||||
|
index=False
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get all of the files:
|
||||||
|
storage_locations = ("/Users/khalimconn-kowlessar/Documents/hestia/Customers/Peabody/Nov 2025 Consulting "
|
||||||
|
"Project/data_validation/validation_cache")
|
||||||
|
# List contents
|
||||||
|
folder_contents = os.listdir(storage_locations)
|
||||||
|
# Grab files and concatenate
|
||||||
|
all_found_uprns = []
|
||||||
|
for file in folder_contents:
|
||||||
|
if file.endswith("_found_uprns.csv"):
|
||||||
|
df = pd.read_csv(os.path.join(storage_locations, file))
|
||||||
|
all_found_uprns.append(df)
|
||||||
|
|
||||||
|
all_found_uprns = pd.concat(all_found_uprns)
|
||||||
|
|
||||||
|
# We now flag any UPRNS that were not found in any of the OS datasets
|
||||||
|
os_missed_uprns = data_needing_validation[
|
||||||
|
~data_needing_validation['Org Ref'].isin(all_found_uprns['Org Ref'].values.tolist())
|
||||||
|
].copy()
|
||||||
|
|
||||||
|
# store
|
||||||
|
os_missed_uprns.to_csv(
|
||||||
|
"/Users/khalimconn-kowlessar/Documents/hestia/Customers/Peabody/Nov 2025 Consulting "
|
||||||
|
"Project/data_validation/os_missed_uprns.csv",
|
||||||
|
index=False
|
||||||
|
)
|
||||||
|
|
||||||
|
# Now build a larger table for standardisation
|
||||||
|
to_standardised = pd.concat(
|
||||||
|
[missing_uprns, non_numeric_uprns, leading_zero_uprns, duplicate_uprns, os_missed_uprns]
|
||||||
|
)
|
||||||
|
|
||||||
|
to_standardised.to_excel(
|
||||||
|
"/Users/khalimconn-kowlessar/Documents/hestia/Customers/Peabody/Nov 2025 Consulting "
|
||||||
|
"Project/data_validation/to_standardise_uprns.xlsx",
|
||||||
|
index=False)
|
||||||
|
|
||||||
|
# We prepare a finalised dataset to work with, that excludes all problematic properties and leaves us with
|
||||||
|
# properties for which we have the data we need
|
||||||
|
|
||||||
|
finalised_data = sustainability_data[
|
||||||
|
~sustainability_data["Org Ref"].isin(
|
||||||
|
to_standardised["Org Ref"].values.tolist()
|
||||||
|
)
|
||||||
|
].copy()
|
||||||
|
|
||||||
|
# Prepare with the column formats we need, as analogous to a_data_prep where we defined an initial working sample
|
||||||
|
|
@ -0,0 +1,114 @@
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
# import pandas as pd
|
||||||
|
#
|
||||||
|
# sal = pd.read_excel(
|
||||||
|
# "/Users/khalimconn-kowlessar/Documents/hestia/Customers/Peabody/Nov 2025 Consulting "
|
||||||
|
# "Project/data_validation/to_standardise_uprns - Standardised.xlsx",
|
||||||
|
# sheet_name="Standardised Asset List"
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
# # Quick breadown of missingness
|
||||||
|
# missing = sal[
|
||||||
|
# pd.isnull(sal["estimated"]) | (sal["estimated"] == True) | pd.isnull(sal["epc_os_uprn"])
|
||||||
|
# ]
|
||||||
|
#
|
||||||
|
# fetched = sal[(sal["estimated"] == False) | ~pd.isnull(sal["epc_os_uprn"])].copy()
|
||||||
|
# fetched = fetched[
|
||||||
|
# ["landlord_property_id", "domna_address_1", "domna_postcode", "domna_full_address", "epc_address1",
|
||||||
|
# "epc_postcode", "epc_address", "landlord_property_type", "epc_property_type"]
|
||||||
|
# ]
|
||||||
|
#
|
||||||
|
# known_issues = [
|
||||||
|
#
|
||||||
|
# ]
|
||||||
|
#
|
||||||
|
# # Missed postcodes
|
||||||
|
# missed_postcode_agg = missing.groupby("domna_postcode").size().reset_index(name="count")
|
||||||
|
# missed_postcode_agg = missed_postcode_agg.sort_values("count", ascending=False)
|
||||||
|
#
|
||||||
|
# multi_missed_postcode = missed_postcode_agg[missed_postcode_agg["count"] > 1]
|
||||||
|
|
||||||
|
### Prepare
|
||||||
|
sustainability_data = pd.read_excel(
|
||||||
|
"/Users/khalimconn-kowlessar/Documents/hestia/Customers/Peabody/Nov 2025 Consulting Project/2025_11_11 - Peabody "
|
||||||
|
"- Data Extracts for Domna.xlsx",
|
||||||
|
sheet_name="Sustainability"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Data we want to remove:
|
||||||
|
missing_uprns = sustainability_data[pd.isnull(sustainability_data['UPRN'])].copy()
|
||||||
|
|
||||||
|
# Any non-numeric UPRNS or leading with 0s are invalid
|
||||||
|
non_numeric_uprns = sustainability_data[
|
||||||
|
~sustainability_data['UPRN'].astype(str).str.match(r'^[1-9][0-9]*$') & ~pd.isnull(sustainability_data['UPRN'])
|
||||||
|
].copy()
|
||||||
|
# 70 properties
|
||||||
|
leading_zero_uprns = sustainability_data[
|
||||||
|
sustainability_data['UPRN'].astype(str).str.startswith('0')
|
||||||
|
].copy()
|
||||||
|
|
||||||
|
# Flag duplicates
|
||||||
|
duplicate_uprns = sustainability_data[
|
||||||
|
sustainability_data.duplicated(subset=['UPRN'], keep=False) & ~pd.isnull(sustainability_data['UPRN'])
|
||||||
|
].copy()
|
||||||
|
|
||||||
|
# Read in the UPRNs that were not valid based on the OS data
|
||||||
|
os_missed_uprns = pd.read_csv(
|
||||||
|
"/Users/khalimconn-kowlessar/Documents/hestia/Customers/Peabody/Nov 2025 Consulting "
|
||||||
|
"Project/data_validation/os_missed_uprns.csv",
|
||||||
|
)
|
||||||
|
|
||||||
|
modelling_data = sustainability_data[
|
||||||
|
~sustainability_data["Org Ref"].isin(
|
||||||
|
missing_uprns["Org Ref"].unique().tolist() + non_numeric_uprns["Org Ref"].unique().tolist() +
|
||||||
|
leading_zero_uprns["Org Ref"].unique().tolist() + duplicate_uprns["Org Ref"].unique().tolist() +
|
||||||
|
os_missed_uprns["Org Ref"].unique().tolist()
|
||||||
|
)
|
||||||
|
].copy()
|
||||||
|
|
||||||
|
# Need to prepare for upload
|
||||||
|
# Variables:
|
||||||
|
|
||||||
|
|
||||||
|
modelling_data["landlord_property_id"] = sustainability_data["Org Ref"].copy()
|
||||||
|
modelling_data["domna_property_id"] = sustainability_data["Org Ref"].copy()
|
||||||
|
|
||||||
|
modelling_data = modelling_data.rename(
|
||||||
|
{
|
||||||
|
"Address 1": "domna_address_1",
|
||||||
|
"Postcode": "postcode",
|
||||||
|
"Type": "landlord_property_type",
|
||||||
|
"Attachment": "landlord_built_form",
|
||||||
|
"Heating": "landlord_heating_system",
|
||||||
|
"UPRN": "epc_os_uprn"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
modelling_data = modelling_data[
|
||||||
|
[
|
||||||
|
"domna_address_1", "Address 2", "Address 3", "postcode", "landlord_property_type",
|
||||||
|
"landlord_built_form", "landlord_heating_system", "epc_os_uprn", "Total Floor Area (m2)",
|
||||||
|
"domna_property_id", "domna_full_address"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
modelling_data["landlord_built_form"] = modelling_data["landlord_built_form"].map(
|
||||||
|
{
|
||||||
|
"MidTerrace": "Mid-Terrace",
|
||||||
|
"EndTerrace": "End-Terrace",
|
||||||
|
"SemiDetached": "Semi-Detached",
|
||||||
|
"Detached": "Detached",
|
||||||
|
"EnclosedEndTerrace": "Enclosed End-Terrace",
|
||||||
|
"EnclosedMidTerrace": "Enclosed Mid-Terrace",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def make_full_address(x):
|
||||||
|
to_join = [x['domna_address_1'], x['Address 2'], x['Address 3']]
|
||||||
|
to_join = [x for x in to_join if not pd.isnull(x) and x != '']
|
||||||
|
return ", ".join(to_join)
|
||||||
|
|
||||||
|
|
||||||
|
modelling_data["domna_full_address"] = modelling_data.apply(lambda x: make_full_address(x), axis=1)
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
"""
|
|
||||||
We have found, within the Peabody data, a large volume of properties with missing and incorrects
|
|
||||||
UPRNS and incorrect address data. We want to flag these records and also find missings where we can
|
|
||||||
|
|
||||||
We also have duplicate UPRNS that should be flagged
|
|
||||||
"""
|
|
||||||
|
|
@ -844,7 +844,7 @@ class TrainingDataset(BaseDataset):
|
||||||
|
|
||||||
# Make sure they are all efficiency columns
|
# Make sure they are all efficiency columns
|
||||||
if any(~missings.index.str.contains("energy_eff")):
|
if any(~missings.index.str.contains("energy_eff")):
|
||||||
raise ValueError("Non efficiency columns are missing")
|
raise ValueError(f"Non efficiency columns are missing {missings.index}")
|
||||||
|
|
||||||
for m in missings.index:
|
for m in missings.index:
|
||||||
self.df[m] = self.df[m].fillna("NO_RATING")
|
self.df[m] = self.df[m].fillna("NO_RATING")
|
||||||
|
|
|
||||||
|
|
@ -15,25 +15,10 @@ os.makedirs(CACHE_DIR, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
def random_delay():
|
def random_delay():
|
||||||
"""Pause randomly between requests (0.5–2 s)."""
|
|
||||||
time.sleep(random.uniform(0.5, 2))
|
time.sleep(random.uniform(0.5, 2))
|
||||||
|
|
||||||
|
|
||||||
def extract_feature(soup, icon_id):
|
|
||||||
tag = soup.find("use", href=f"#{icon_id}")
|
|
||||||
if tag:
|
|
||||||
parent = tag.find_parent("div", class_="_1pbf8i53")
|
|
||||||
if parent:
|
|
||||||
text = parent.get_text(strip=True)
|
|
||||||
return text
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def extract_embedded_json(text):
|
def extract_embedded_json(text):
|
||||||
"""
|
|
||||||
Extract embedded property JSON containing attributes, energy, estimates, and sales history.
|
|
||||||
"""
|
|
||||||
# Try to grab everything after "attributes"
|
|
||||||
match = re.search(
|
match = re.search(
|
||||||
r'"attributes"\s*:\s*\{.*?\}\s*,.*?"historicSales".*?\]',
|
r'"attributes"\s*:\s*\{.*?\}\s*,.*?"historicSales".*?\]',
|
||||||
text,
|
text,
|
||||||
|
|
@ -48,13 +33,16 @@ def extract_embedded_json(text):
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# fallback for independent keys
|
|
||||||
result = {}
|
result = {}
|
||||||
for key in [
|
for key in [
|
||||||
"attributes", "energy", "rentEstimate",
|
"attributes", "energy", "rentEstimate",
|
||||||
"saleEstimate", "saleHistory", "historicSales"
|
"saleEstimate", "saleHistory", "historicSales"
|
||||||
]:
|
]:
|
||||||
key_match = re.search(rf'"{key}"\s*:\s*(\{{.*?\}}|\[.*?\])', text, re.DOTALL)
|
key_match = re.search(
|
||||||
|
rf'"{key}"\s*:\s*(\{{.*?\}}|\[.*?\])',
|
||||||
|
text,
|
||||||
|
re.DOTALL
|
||||||
|
)
|
||||||
if key_match:
|
if key_match:
|
||||||
try:
|
try:
|
||||||
result[key] = json.loads(key_match.group(1))
|
result[key] = json.loads(key_match.group(1))
|
||||||
|
|
@ -64,28 +52,23 @@ def extract_embedded_json(text):
|
||||||
|
|
||||||
|
|
||||||
def scrape_all_estimates(session, url):
|
def scrape_all_estimates(session, url):
|
||||||
"""Scrape valuation estimates for one Zoopla property URL."""
|
|
||||||
resp = session.get(url, impersonate=random.choice(ENGINES))
|
resp = session.get(url, impersonate=random.choice(ENGINES))
|
||||||
html = resp.text
|
html = resp.text
|
||||||
page_source = BeautifulSoup(resp.text, "html.parser")
|
soup = BeautifulSoup(html, "html.parser")
|
||||||
estimates = page_source.find_all("div", {"data-testid": "sale-estimate"})
|
estimates = soup.find_all("div", {"data-testid": "sale-estimate"})
|
||||||
|
|
||||||
data = extract_embedded_json(html)
|
data = extract_embedded_json(html)
|
||||||
|
|
||||||
is_blocked = len(estimates) == 0
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"estimates": estimates,
|
"estimates": estimates,
|
||||||
"is_blocked": is_blocked,
|
"is_blocked": len(estimates) == 0,
|
||||||
"response_html": html,
|
"response_html": html,
|
||||||
"attributes": data.get("attributes"),
|
"attributes": data.get("attributes", {}),
|
||||||
"rent": data.get("rentEstimate"),
|
"rentEstimate": data.get("rentEstimate", {}),
|
||||||
"historicSales": data.get("historicSales"),
|
"historicSales": data.get("historicSales", []),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def extract_estimates(estimates):
|
def extract_estimates(estimates):
|
||||||
"""Extract low, mid, and high estimates from parsed HTML."""
|
|
||||||
est = estimates[0]
|
est = estimates[0]
|
||||||
low = est.find("span", {"data-testid": "low-estimate-blurred"}).text
|
low = est.find("span", {"data-testid": "low-estimate-blurred"}).text
|
||||||
mid = est.find("p", {"data-testid": "estimate-blurred"}).text
|
mid = est.find("p", {"data-testid": "estimate-blurred"}).text
|
||||||
|
|
@ -94,110 +77,123 @@ def extract_estimates(estimates):
|
||||||
|
|
||||||
|
|
||||||
def cache_path_for_url(url):
|
def cache_path_for_url(url):
|
||||||
"""Return a deterministic local cache path for a URL."""
|
|
||||||
uprn = url.split("/")[-2]
|
uprn = url.split("/")[-2]
|
||||||
return os.path.join(CACHE_DIR, f"{uprn}.html")
|
return os.path.join(CACHE_DIR, f"{uprn}.html")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_cached_html(url, html):
|
||||||
|
soup = BeautifulSoup(html, "html.parser")
|
||||||
|
estimates = soup.find_all("div", {"data-testid": "sale-estimate"})
|
||||||
|
data = extract_embedded_json(html)
|
||||||
|
history = data.get("historicSales") or [{}]
|
||||||
|
|
||||||
|
if not estimates:
|
||||||
|
return None
|
||||||
|
|
||||||
|
low, mid, high = extract_estimates(estimates)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"URL": url,
|
||||||
|
"Low Estimate": low,
|
||||||
|
"Middle Estimate": mid,
|
||||||
|
"High Estimate": high,
|
||||||
|
**data.get("attributes", {}),
|
||||||
|
**data.get("rentEstimate", {}),
|
||||||
|
**history[0],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def parallel_task(url):
|
def parallel_task(url):
|
||||||
"""Main worker function executed in each process."""
|
|
||||||
cache_path = cache_path_for_url(url)
|
cache_path = cache_path_for_url(url)
|
||||||
|
|
||||||
# Use cached file if it exists
|
|
||||||
if os.path.exists(cache_path):
|
if os.path.exists(cache_path):
|
||||||
html = open(cache_path, "r").read()
|
with open(cache_path, "r", encoding="utf-8") as f:
|
||||||
page_source = BeautifulSoup(html, "html.parser")
|
html = f.read()
|
||||||
estimates = page_source.find_all("div", {"data-testid": "sale-estimate"})
|
cached = parse_cached_html(url, html)
|
||||||
data = extract_embedded_json(html)
|
if cached:
|
||||||
history_sales = data.get("historicSales", [{}])
|
return cached
|
||||||
if len(history_sales) == 0:
|
|
||||||
history_sales = [{}]
|
|
||||||
|
|
||||||
if estimates:
|
|
||||||
low, mid, high = extract_estimates(estimates)
|
|
||||||
return {
|
|
||||||
"URL": url, "Low Estimate": low, "Middle Estimate": mid, "High Estimate": high,
|
|
||||||
**data.get("attributes", {}), **data.get("rentEstimate", {}),
|
|
||||||
**history_sales[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
# Otherwise scrape live
|
|
||||||
with StealthSession() as session:
|
with StealthSession() as session:
|
||||||
attempts = 0
|
for attempt in range(5):
|
||||||
while attempts < 5:
|
|
||||||
output = scrape_all_estimates(session, url)
|
output = scrape_all_estimates(session, url)
|
||||||
|
|
||||||
if not output["is_blocked"] and output["estimates"]:
|
if not output["is_blocked"] and output["estimates"]:
|
||||||
open(cache_path, "w").write(output["html"])
|
html = output.get("response_html")
|
||||||
|
if html:
|
||||||
|
with open(cache_path, "w", encoding="utf-8") as f:
|
||||||
|
f.write(html)
|
||||||
|
|
||||||
|
history = output.get("historicSales") or [{}]
|
||||||
low, mid, high = extract_estimates(output["estimates"])
|
low, mid, high = extract_estimates(output["estimates"])
|
||||||
history_sales = output.get("historicSales", [{}])
|
|
||||||
if len(history_sales) == 0:
|
|
||||||
history_sales = [{}]
|
|
||||||
return {
|
return {
|
||||||
"URL": url, "Low Estimate": low, "Middle Estimate": mid, "High Estimate": high,
|
"URL": url,
|
||||||
|
"Low Estimate": low,
|
||||||
|
"Middle Estimate": mid,
|
||||||
|
"High Estimate": high,
|
||||||
**output.get("attributes", {}),
|
**output.get("attributes", {}),
|
||||||
**output.get("rent", {}),
|
**output.get("rentEstimate", {}),
|
||||||
**history_sales[0]
|
**history[0],
|
||||||
}
|
}
|
||||||
attempts += 1
|
|
||||||
print(f"[Attempt {attempts}] Blocked or empty for {url}")
|
|
||||||
random_delay()
|
random_delay()
|
||||||
|
|
||||||
# If still blocked, return placeholders
|
return {
|
||||||
return {"URL": url, "Low Estimate": None, "Middle Estimate": None, "High Estimate": None}
|
"URL": url,
|
||||||
|
"Low Estimate": None,
|
||||||
|
"Middle Estimate": None,
|
||||||
|
"High Estimate": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def parse_price(p):
|
def parse_price(p):
|
||||||
if p is None:
|
if not p:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
p = p.replace("£", "").strip().lower()
|
p = p.replace("£", "").strip().lower()
|
||||||
if not p:
|
|
||||||
return None
|
|
||||||
if p.endswith("k"):
|
if p.endswith("k"):
|
||||||
return float(p[:-1]) * 1_000
|
return float(p[:-1]) * 1_000
|
||||||
elif p.endswith("m"):
|
if p.endswith("m"):
|
||||||
return float(p[:-1]) * 1_000_000
|
return float(p[:-1]) * 1_000_000
|
||||||
else:
|
|
||||||
try:
|
try:
|
||||||
return float(p.replace(",", ""))
|
return float(p.replace(",", ""))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
# Load portfolio
|
|
||||||
asset_list = pd.read_excel(
|
asset_list = pd.read_excel(
|
||||||
"/Users/khalimconn-kowlessar/Documents/hestia/Customers/sfr/October 2025 AL portfolio/22.10 AL Portfolio - "
|
"/Users/khalimconn-kowlessar/Documents/hestia/Customers/Peabody/Nov 2025 Consulting "
|
||||||
"Standardised - partial UPRN fill.xlsx",
|
"Project/modelling_sample.xlsx",
|
||||||
sheet_name="Standardised Asset List"
|
sheet_name="Standardised Asset List"
|
||||||
)
|
)
|
||||||
|
|
||||||
asset_list = asset_list[~pd.isnull(asset_list["epc_os_uprn"])]
|
asset_list = asset_list[~pd.isnull(asset_list["epc_os_uprn"])]
|
||||||
|
asset_list = asset_list.drop_duplicates("epc_os_uprn")
|
||||||
asset_list["epc_os_uprn"] = asset_list["epc_os_uprn"].astype(int).astype(str)
|
asset_list["epc_os_uprn"] = asset_list["epc_os_uprn"].astype(int).astype(str)
|
||||||
|
|
||||||
uprns = asset_list["epc_os_uprn"].tolist()
|
uprns = asset_list["epc_os_uprn"].tolist()
|
||||||
urls = [f"https://www.zoopla.co.uk/property/uprn/{uprn}/" for uprn in uprns]
|
urls = [f"https://www.zoopla.co.uk/property/uprn/{uprn}/" for uprn in uprns]
|
||||||
|
|
||||||
# Limit concurrency to avoid blocks
|
with Pool(processes=2) as pool:
|
||||||
with Pool(processes=2) as pool: # fewer processes = fewer fingerprints
|
|
||||||
estimates_list = list(
|
estimates_list = list(
|
||||||
tqdm(pool.imap(parallel_task, urls), total=len(urls))
|
tqdm(pool.imap(parallel_task, urls), total=len(urls))
|
||||||
)
|
)
|
||||||
|
|
||||||
df = pd.DataFrame(estimates_list)
|
df = pd.DataFrame(estimates_list)
|
||||||
|
|
||||||
print(df.head())
|
|
||||||
|
|
||||||
df["uprn"] = df["URL"].str.extract(r"uprn/(\d+)/")
|
df["uprn"] = df["URL"].str.extract(r"uprn/(\d+)/")
|
||||||
df["valuation"] = df["Middle Estimate"].apply(parse_price)
|
df["valuation"] = df["Middle Estimate"].apply(parse_price)
|
||||||
|
|
||||||
df.to_csv("zoopla_estimates.csv", index=False)
|
df.to_csv("zoopla_estimates.csv", index=False)
|
||||||
|
|
||||||
# Merge with asset list
|
|
||||||
merged = asset_list.merge(
|
merged = asset_list.merge(
|
||||||
df[["uprn", "valuation"]],
|
df[["uprn", "valuation"]],
|
||||||
left_on="epc_os_uprn",
|
left_on="epc_os_uprn",
|
||||||
right_on="uprn",
|
right_on="uprn",
|
||||||
how="left"
|
how="left"
|
||||||
)
|
)
|
||||||
|
|
||||||
merged.to_excel(
|
merged.to_excel(
|
||||||
"20251029 AL Portfolio - Standardised - with valuations.xlsx",
|
"20251029 AL Portfolio - Standardised - with valuations.xlsx",
|
||||||
index=False
|
index=False
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,8 @@ from backend.app.db.models.portfolio import PropertyModel, PropertyDetailsEpcMod
|
||||||
|
|
||||||
# PORTFOLIO_ID = 206
|
# PORTFOLIO_ID = 206
|
||||||
# SCENARIOS = [389]
|
# SCENARIOS = [389]
|
||||||
PORTFOLIO_ID = 388
|
PORTFOLIO_ID = 404
|
||||||
SCENARIOS = [803]
|
SCENARIOS = [829]
|
||||||
|
|
||||||
|
|
||||||
def get_data(portfolio_id, scenario_ids):
|
def get_data(portfolio_id, scenario_ids):
|
||||||
|
|
@ -121,7 +121,8 @@ recommendations_measures_pivot["total_retrofit_cost"] = recommendations_measures
|
||||||
|
|
||||||
df = properties_df[
|
df = properties_df[
|
||||||
[
|
[
|
||||||
"property_id", "uprn", "address", "postcode", "property_type", "walls", "roof", "heating", "windows",
|
"landlord_property_id", "property_id", "uprn", "address", "postcode", "property_type", "walls", "roof",
|
||||||
|
"heating", "windows",
|
||||||
"current_epc_rating",
|
"current_epc_rating",
|
||||||
"current_sap_points", "total_floor_area", "number_of_rooms",
|
"current_sap_points", "total_floor_area", "number_of_rooms",
|
||||||
]
|
]
|
||||||
|
|
@ -143,7 +144,7 @@ from utils.s3 import read_csv_from_s3, read_excel_from_s3
|
||||||
|
|
||||||
# asset_list = read_csv_from_s3(bucket_name="retrofit-plan-inputs-dev", filepath='8/206/asset_list.csv')
|
# asset_list = read_csv_from_s3(bucket_name="retrofit-plan-inputs-dev", filepath='8/206/asset_list.csv')
|
||||||
asset_list = read_excel_from_s3(
|
asset_list = read_excel_from_s3(
|
||||||
bucket_name="retrofit-plan-inputs-dev", file_key='2/388/20251208T203603925Z/asset_list.xlsx',
|
bucket_name="retrofit-plan-inputs-dev", file_key="2/404/20251211T163200754Z/asset_list.xlsx",
|
||||||
header_row=0, sheet_name="Standardised Asset List"
|
header_row=0, sheet_name="Standardised Asset List"
|
||||||
)
|
)
|
||||||
asset_list = pd.DataFrame(asset_list)
|
asset_list = pd.DataFrame(asset_list)
|
||||||
|
|
@ -201,11 +202,15 @@ asset_list = asset_list.merge(
|
||||||
)
|
)
|
||||||
|
|
||||||
# For exporting
|
# For exporting
|
||||||
asset_list.to_excel(
|
df.to_excel(
|
||||||
"/Users/khalimconn-kowlessar/Documents/hestia/Customers/Peabody/Nov 2025 Consulting "
|
"/Users/khalimconn-kowlessar/Documents/hestia/Customers/Lincs Rural/EPC C -without floors proposed measures - "
|
||||||
"Project/20251209_sample_package_data.xlsx",
|
"with ID.xlsx",
|
||||||
index=False
|
index=False
|
||||||
)
|
)
|
||||||
|
# asset_list.to_excel(
|
||||||
|
# "/Users/khalimconn-kowlessar/Documents/hestia/Customers/Lincs Rural/epc_measures.xlsx",
|
||||||
|
# index=False
|
||||||
|
# )
|
||||||
|
|
||||||
condition_costs = pd.read_excel(
|
condition_costs = pd.read_excel(
|
||||||
"/Users/khalimconn-kowlessar/Documents/hestia/sfr/Spring JV/Condition costs.xlsx",
|
"/Users/khalimconn-kowlessar/Documents/hestia/sfr/Spring JV/Condition costs.xlsx",
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue