Working on LHP asset list review

This commit is contained in:
Khalim Conn-Kowlessar 2025-05-07 15:09:58 +01:00
parent 9b869063d1
commit 96fb10390b
7 changed files with 570 additions and 100 deletions

2
.idea/Model.iml generated
View file

@ -7,7 +7,7 @@
<sourceFolder url="file://$MODULE_DIR$/open_uprn" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/recommendations" isTestSource="false" />
</content>
<orderEntry type="jdk" jdkName="Fastapi-backend" jdkType="Python SDK" />
<orderEntry type="jdk" jdkName="AssetList" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PyNamespacePackagesService">

2
.idea/misc.xml generated
View file

@ -3,7 +3,7 @@
<component name="Black">
<option name="sdkName" value="Python 3.10 (backend)" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Fastapi-backend" project-jdk-type="Python SDK" />
<component name="ProjectRootManager" version="2" project-jdk-name="AssetList" project-jdk-type="Python SDK" />
<component name="PyCharmProfessionalAdvertiser">
<option name="shown" value="true" />
</component>

View file

@ -19,6 +19,7 @@ import asset_list.mappings.heating_systems as heating_mappings
import asset_list.mappings.exising_pv as existing_pv_mappings
import asset_list.mappings.built_form as built_form_mappings
import asset_list.mappings.roof as roof_mappings
import asset_list.mappings.outcomes as outcomes_mappings
from recommendations.recommendation_utils import (
estimate_perimeter,
@ -1139,21 +1140,29 @@ class AssetList:
# We add a SAP category for all work type identification
self.standardised_asset_list["SAP Category"] = np.where(
(
(self.standardised_asset_list[self.EPC_API_DATA_NAMES["current-energy-efficiency"]] <= 68) |
(self.standardised_asset_list[self.STANDARD_SAP] <= 68)
(self.standardised_asset_list[self.EPC_API_DATA_NAMES["current-energy-efficiency"]] <= 54) |
(self.standardised_asset_list[self.STANDARD_SAP] <= 54)
),
"SAP Rating 68 or less",
"SAP Rating 54 or less",
np.where(
(
(
self.standardised_asset_list[self.EPC_API_DATA_NAMES["current-energy-efficiency"]] <=
self.EMPTY_CAVITY_SAP_THRESHOLD
) | (self.standardised_asset_list[self.STANDARD_SAP] <= self.EMPTY_CAVITY_SAP_THRESHOLD)
(self.standardised_asset_list[self.EPC_API_DATA_NAMES["current-energy-efficiency"]] <= 68) |
(self.standardised_asset_list[self.STANDARD_SAP] <= 68)
),
"SAP Rating 55-68",
np.where(
(
(
self.standardised_asset_list[self.EPC_API_DATA_NAMES["current-energy-efficiency"]] <=
self.EMPTY_CAVITY_SAP_THRESHOLD
) | (self.standardised_asset_list[self.STANDARD_SAP] <= self.EMPTY_CAVITY_SAP_THRESHOLD)
),
f"SAP Rating 69-{self.EMPTY_CAVITY_SAP_THRESHOLD}",
f"SAP Rating {self.EMPTY_CAVITY_SAP_THRESHOLD + 1} or more"
),
f"SAP Rating 69-{self.EMPTY_CAVITY_SAP_THRESHOLD}",
f"SAP Rating {self.EMPTY_CAVITY_SAP_THRESHOLD + 1} or more"
)
)
else:
# We add a SAP category for all work type identification
# We break into 4 categories (54 or less, 55-68, 69-74, 75 or more)
@ -1724,8 +1733,8 @@ class AssetList:
~self.standardised_asset_list["epc_indicates_empty_cavity"] &
pd.isnull(self.standardised_asset_list["cavity_reason"])
),
"Landlord Data Shows Empty Cavity, EPC & Inspections Shows Filled: " + self.standardised_asset_list[
"SAP Category"],
"Landlord Data Shows Empty Cavity, EPC & Inspections Shows Filled or Non-cavity: " +
self.standardised_asset_list["SAP Category"],
self.standardised_asset_list["cavity_reason"]
)
@ -2172,10 +2181,7 @@ class AssetList:
# TODO: Fetch from Sharepoint
ecosurv_filepath = "/Users/khalimconn-kowlessar/Documents/hestia/Ecosurv/15.04.csv"
logger.info("Getting Ecosurv data from %s", ecosurv_filepath)
self.ecosurv = pd.read_csv(
ecosurv_filepath,
encoding="cp437"
)
self.ecosurv = pd.read_csv(ecosurv_filepath, encoding="cp437")
landlords = self.ecosurv["Landlord"].value_counts().reset_index(drop=False)
landlord_references = landlords[
@ -2260,46 +2266,82 @@ class AssetList:
def flag_outcomes(
self,
outcomes_filepath,
outcomes_filepaths,
outcomes_sheetname,
outcomes_address,
outcomes_postcode,
outcomes_houseno,
outcomes_id
):
if outcomes_filepath is None:
if not outcomes_filepaths:
return
self.outcomes = pd.read_excel(outcomes_filepath, sheet_name=outcomes_sheetname)
self.outcomes["row_id"] = self.outcomes.index
if outcomes_houseno is None:
outcomes_houseno = "houseno"
self.outcomes["houseno"] = self.outcomes[outcomes_address].apply(
lambda x: SearchEpc.get_house_number(x, self.outcomes[outcomes_postcode])
)
logger.info("Matching outcomes to asset list")
# Merge the outcomes onto the asset list - we check we're able to match sufficiently well
self.outcomes = []
outcomes_no_match = []
lookup = []
nomatch = []
for _, x in tqdm(self.outcomes.iterrows(), total=len(self.outcomes)):
for idx, outcomes_filepath in enumerate(outcomes_filepaths):
outcomes = pd.read_excel(outcomes_filepath, sheet_name=outcomes_sheetname[idx])
outcomes["row_id"] = outcomes.index
if pd.isnull(x[outcomes_address]):
continue
if outcomes_houseno[idx] is None:
outcomes_houseno = "houseno"
outcomes["houseno"] = outcomes[outcomes_address[idx]].apply(
lambda x: SearchEpc.get_house_number(x, outcomes[outcomes_postcode])
)
# Check if we have an id
oid = x[outcomes_id] if outcomes_id is not None else None
# We handle an edge case that occured for LHP
if "Notes / Outcomes" in outcomes.columns and "Outcome" not in outcomes.columns:
# We use the re-mapper to handle this:
outcomes["Notes / Outcomes"] = outcomes["Notes / Outcomes"].str.strip()
values_to_remap = outcomes["Notes / Outcomes"].unique()
# We want to map this to our standardised list of property types we're interested in
remapper = DataRemapper(
standard_values=outcomes_mappings.outcomes_values, standard_map=outcomes_mappings.outcomes_map
)
remap_dictionary = remapper.standardize_list(values_to_remap=values_to_remap.tolist())
# Perform the remap
outcomes["Outcome"] = outcomes["Notes / Outcomes"].map(remap_dictionary)
outcomes["Outcome"] = outcomes["Outcome"].str.lower()
logger.info("Matching outcomes to asset list")
# Merge the outcomes onto the asset list - we check we're able to match sufficiently well
lookup_i = []
nomatch_i = []
for _, x in tqdm(outcomes.iterrows(), total=len(outcomes)):
if pd.isnull(x[outcomes_address[idx]]):
continue
# Check if we have an id
oid = x[outcomes_id[idx]] if outcomes_id[idx] is not None else None
if oid is not None:
matched = self.standardised_asset_list[
(self.standardised_asset_list[
self.STANDARD_LANDLORD_PROPERTY_ID
].str.strip() == oid)
]
if matched.shape[0] == 1:
lookup_i.append(
{
"row_id": x["row_id"],
self.DOMNA_PROPERTY_ID: matched[self.DOMNA_PROPERTY_ID].values[0]
}
)
continue
address_clean = x[outcomes_address[idx]].lower().replace(",", "").replace(" ", " ")
if oid is not None:
matched = self.standardised_asset_list[
(self.standardised_asset_list[
self.STANDARD_LANDLORD_PROPERTY_ID
].str.strip() == oid)
self.STANDARD_FULL_ADDRESS
].str.lower().str.replace(",", "").str.replace(" ", " ") == address_clean)
]
if matched.shape[0] == 1:
lookup.append(
lookup_i.append(
{
"row_id": x["row_id"],
self.DOMNA_PROPERTY_ID: matched[self.DOMNA_PROPERTY_ID].values[0]
@ -2307,65 +2349,65 @@ class AssetList:
)
continue
address_clean = x[outcomes_address].lower().replace(",", "").replace(" ", " ")
self.outcomes["Outcome"] = self.outcomes["Outcome"].str.lower()
matched = self.standardised_asset_list[
(self.standardised_asset_list[
self.STANDARD_FULL_ADDRESS
].str.lower().str.replace(",", "").str.replace(" ", " ") == address_clean)
]
if matched.shape[0] == 1:
lookup.append(
{
"row_id": x["row_id"],
self.DOMNA_PROPERTY_ID: matched[self.DOMNA_PROPERTY_ID].values[0]
}
)
continue
matched = self.standardised_asset_list[
(self.standardised_asset_list[self.STANDARD_POSTCODE].str.strip() == x[outcomes_postcode])
].copy()
if not matched.empty:
matched["houseno"] = matched.apply(
lambda x: SearchEpc.get_house_number(
str(x[self.STANDARD_ADDRESS_1]), str(x[self.STANDARD_POSTCODE])
),
axis=1
)
matched = matched[
matched["houseno"].astype(str) == str(x[outcomes_houseno])
]
if matched.shape[0] == 1:
lookup.append(
{
"row_id": x["row_id"],
self.DOMNA_PROPERTY_ID: matched[self.DOMNA_PROPERTY_ID].values[0]
}
matched = self.standardised_asset_list[
(self.standardised_asset_list[self.STANDARD_POSTCODE].str.strip() == x[outcomes_postcode[idx]])
].copy()
if not matched.empty:
matched["houseno"] = matched.apply(
lambda x: SearchEpc.get_house_number(
str(x[self.STANDARD_ADDRESS_1]), str(x[self.STANDARD_POSTCODE])
),
axis=1
)
continue
elif not matched.empty:
# Use levenstein distance to match
matched["address"] = matched[self.STANDARD_ADDRESS_1] + " " + matched[self.STANDARD_POSTCODE]
best_match = process.extractOne(x["Address"], matched[self.STANDARD_FULL_ADDRESS].values)[0]
matched = matched[matched[self.STANDARD_FULL_ADDRESS] == best_match]
lookup.append(
{
"row_id": x["row_id"],
self.DOMNA_PROPERTY_ID: matched[self.DOMNA_PROPERTY_ID].values[0]
}
)
continue
if pd.isnull(x[outcomes_houseno[idx]]):
house_no_to_match = SearchEpc.get_house_number(
str(x[outcomes_address[idx]]), str(x[outcomes_postcode[idx]])
)
if isinstance(house_no_to_match, str):
house_no_to_match = house_no_to_match.lower()
else:
house_no_to_match = str(x[outcomes_houseno[idx]]).strip()
nomatch.append(x["row_id"])
matched = matched[matched["houseno"].astype(str) == house_no_to_match]
if matched.shape[0] == 1:
lookup_i.append(
{
"row_id": x["row_id"],
self.DOMNA_PROPERTY_ID: matched[self.DOMNA_PROPERTY_ID].values[0]
}
)
continue
elif not matched.empty:
# Use levenstein distance to match
matched["address"] = (
matched[self.STANDARD_ADDRESS_1] + " " + matched[self.STANDARD_POSTCODE]
)
self.outcomes_no_match = self.outcomes[self.outcomes["row_id"].isin(nomatch)]
lookup = pd.DataFrame(lookup)
best_match = process.extractOne(
x[outcomes_address[idx]], matched[self.STANDARD_FULL_ADDRESS].values
)[0]
matched = matched[matched[self.STANDARD_FULL_ADDRESS] == best_match]
lookup_i.append(
{
"row_id": x["row_id"],
self.DOMNA_PROPERTY_ID: matched[self.DOMNA_PROPERTY_ID].values[0]
}
)
continue
nomatch_i.append(x["row_id"])
outcomes_no_match_i = outcomes[outcomes["row_id"].isin(nomatch_i)]
lookup_i = pd.DataFrame(lookup_i)
outcomes_no_match.append(outcomes_no_match_i)
lookup.append(lookup_i)
self.outcomes.append(outcomes)
lookup = pd.concat(lookup)
outcomes_no_match = pd.concat(outcomes_no_match)
self.outcomes = pd.concat(self.outcomes)
if lookup.empty:
return
@ -2376,10 +2418,19 @@ class AssetList:
# that the surveyor had a detailed explanation as to why they couldn't gain access so if this has
# happened multiple times, in this case we judge that the work may not be viable
date_col = "Week Commencing" if "Week Commencing" in self.outcomes else "Survey Date"
if "Week Commencing" in self.outcomes.columns:
date_col = "Week Commencing"
elif "Survey Date" in self.outcomes.columns:
date_col = "Survey Date"
elif "Date letters sent" in self.outcomes.columns:
date_col = "Date letters sent"
else:
raise NotImplementedError("Invalid date in outcomes - implement me")
notes_col = "Notes" if "Notes" in outcomes.columns else "Notes / Outcomes"
lookup = lookup.merge(
self.outcomes[["row_id", "Outcome", "Notes", date_col]], how="left", on="row_id"
self.outcomes[["row_id", "Outcome", notes_col, date_col]], how="left", on="row_id"
)
visit_counts = (
@ -2390,11 +2441,33 @@ class AssetList:
.sort_values("visit_count", ascending=False)
)
def extract_date(s):
if isinstance(s, str):
match = re.search(r"(\d{2}\.\d{2}\.\d{4})", s)
if match:
return pd.to_datetime(match.group(1), format="%d.%m.%Y", errors="coerce")
return pd.NaT
lookup['parsed_date'] = lookup['Date letters sent'].apply(extract_date)
def get_latest_note(group):
surveyed = group[group['Outcome'] == 'surveyed']
if not surveyed.empty:
return surveyed.sort_values('parsed_date', ascending=False).iloc[0]
else:
return group.sort_values('parsed_date', ascending=False).iloc[0]
latest_note = lookup.groupby('domna_property_id', group_keys=False).apply(get_latest_note).reset_index(
drop=True)
latest_note = latest_note[["domna_property_id", notes_col]]
pivot_df = lookup.groupby(["domna_property_id", "Outcome"]).size().unstack(fill_value=0).reset_index()
pivot_df = pivot_df.merge(
visit_counts, how="left", on="domna_property_id"
)
# We want the latest note
if pivot_df[self.DOMNA_PROPERTY_ID].duplicated().sum():
raise Exception("We have duplicated property IDs in the outcomes data")
@ -2406,6 +2479,14 @@ class AssetList:
self.standardised_asset_list = self.standardised_asset_list.merge(
pivot_df, how="left", left_on=self.DOMNA_PROPERTY_ID, right_on="domna_property_id"
)
# Merge the latest note
self.standardised_asset_list = self.standardised_asset_list.merge(
latest_note.rename(columns={notes_col: "Latest Route March Note"}),
how="left", left_on=self.DOMNA_PROPERTY_ID, right_on="domna_property_id"
)
if self.standardised_asset_list[self.DOMNA_PROPERTY_ID].duplicated().sum():
raise ValueError("Duplicates appreared - something went wrong")
self.outcomes = self.outcomes.sort_values("domna_property_id", ascending=False)

View file

@ -89,6 +89,103 @@ def app():
# - We want: fully insulated property (all wall types), EPC D or below (floors should be solid)
# - Or the insulation required is loft/cavity (floors should be solid)
# LHP:
data_folder = "/Users/khalimconn-kowlessar/Documents/hestia/Customers/LHP"
data_filename = "LHP.xlsx"
sheet_name = "Decent Homes Stock"
postcode_column = 'Postcode'
fulladdress_column = "Address"
address1_column = None
address1_method = "house_number_extraction"
address_cols_to_concat = []
missing_postcodes_method = None
landlord_year_built = "Build Date"
landlord_os_uprn = None
landlord_property_type = "Property Type"
landlord_built_form = None
landlord_wall_construction = None
landlord_roof_construction = None
landlord_heating_system = "Heating Type"
landlord_existing_pv = None
landlord_property_id = "Property ID"
landlord_sap = None
outcomes_filename = [
"/Users/khalimconn-kowlessar/Documents/hestia/Customers/LHP/LHP Outcomes.xlsx",
"/Users/khalimconn-kowlessar/Documents/hestia/Customers/LHP/Lincolnshire Housing Partnership - Outcomes 20th "
"Feb 2024.xlsx",
]
outcomes_sheetname = ["Sheet1", "LHP"]
outcomes_postcode = ["Postcode", "Postcode"]
outcomes_houseno = ["No.", "No."]
outcomes_id = [None, None]
outcomes_address = ["Address", "Address"]
master_filepaths = [os.path.join(data_folder, "LHP Rolling Master for analysis.csv")]
master_to_asset_list_filepath = None
phase = False
ecosurv_landlords = "lhp"
# Soverign
data_folder = "/Users/khalimconn-kowlessar/Documents/hestia/Customers/Sovereign"
data_filename = "Warmfront - Quote for CWI.xlsx"
sheet_name = "Sheet2"
postcode_column = 'Postcode'
fulladdress_column = None
address1_column = "Address Line 1"
address1_method = None
address_cols_to_concat = ["Address Line 1", "Address Line 2", "Address Line 3"]
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 = "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_to_asset_list_filepath = None
phase = False
ecosurv_landlords = None
# NCHA
data_folder = "/Users/khalimconn-kowlessar/Documents/hestia/Customers/NCHA"
data_filename = "Energy Info Copy.xlsx"
sheet_name = "Data"
postcode_column = 'Postcode'
fulladdress_column = "Address"
address1_column = None
address1_method = "house_number_extraction"
address_cols_to_concat = []
missing_postcodes_method = None
landlord_year_built = "Build Date (HAR10)"
landlord_os_uprn = None
landlord_property_type = "Property Type (HAR10)"
landlord_built_form = "Build Form (EPC)"
landlord_wall_construction = "Wall Description"
landlord_roof_construction = None
landlord_heating_system = "Heating System"
landlord_existing_pv = None
landlord_property_id = "Place ref"
landlord_sap = "EPC SAP"
outcomes_filename = None
outcomes_sheetname = None
outcomes_postcode = None
outcomes_houseno = None
outcomes_id = None
outcomes_address = None
master_filepaths = []
master_to_asset_list_filepath = None
phase = False
ecosurv_landlords = None
# Torus
data_folder = "/Users/khalimconn-kowlessar/Documents/hestia/Customers/Torus/Phase 1"
data_filename = "Torus Property Asset List - Phase 1.xlsx"
@ -482,7 +579,7 @@ def app():
# We now flag properties that have been treated under existing programmes
asset_list.flag_outcomes(
outcomes_filepath=os.path.join(data_folder, outcomes_filename) if outcomes_filename else None,
outcomes_filepaths=outcomes_filename,
outcomes_sheetname=outcomes_sheetname,
outcomes_address=outcomes_address,
outcomes_postcode=outcomes_postcode,
@ -611,6 +708,12 @@ def app():
transformed_data.append(row_data)
transformed_df = pd.DataFrame(transformed_data)
for col in [
"Floor insulation (solid floor)",
"Floor insulation", "Floor insulation (suspended floor)"
]:
if col not in transformed_df.columns:
transformed_df[col] = False
transformed_df = transformed_df[
[
asset_list.DOMNA_PROPERTY_ID, "Floor insulation (solid floor)",

View file

@ -220,5 +220,48 @@ HEATING_MAPPINGS = {
'Boiler/ underfloor': 'electric underfloor',
'Storage system': "non-electric underfloor",
'BOILER': 'gas combi boiler',
'SPACE_HEATER': 'room heaters'
'SPACE_HEATER': 'room heaters',
'AIR': 'air source heat pump',
'FSOL': 'solid fuel',
'PDEV': 'unknown',
'GASF': 'gas boiler, radiators',
'CONO': 'no heating',
'FELE HRSH': 'high heat retention storage heaters',
'FOIL': 'oil boiler',
'FDEV': 'unknown',
'FNON': 'non-electric underfloor',
'FGAS': 'gas combi boiler',
'FELE': 'electric fuel',
'GRNE': 'ground source heat pump',
'High Heat Storage Heaters': 'high heat retention storage heaters',
'Electric Radiators': 'electric radiators',
'Electric Air Source Heat Pump': 'air source heat pump',
'Gas Combi Condensing Boiler': 'gas condensing combi',
'Electric Boiler Heating': 'electric boiler',
'Solid Fuel Open Back Boiler Heating': 'solid fuel',
'Solid Fuel Closed Back Boiler Heating': 'solid fuel',
'Oil Boiler': 'oil boiler',
'Electric Storage Heaters': 'electric storage heaters',
'Gas Combi Boiler Heating': 'gas combi boiler',
'Electric NIBE Heating System': 'air source heat pump',
'Gas Back Boiler': 'gas boiler, radiators',
'Electric Gel/Oil Filled Radiators': 'electric radiators',
'No Information': 'unknown',
'Oil Combination Boiler Heating': 'oil boiler',
'Electric DSR Heat Retention Radiators': 'high heat retention storage heaters',
'Communal Heating System': 'communal heating',
'Description': 'unknown',
'Oil Combi Condensing Boiler Heating': 'oil boiler',
'Gas Combi Condensing Boiler Heating': 'gas condensing combi',
'Electric Warm Air Heating': 'electric fuel',
'Gas System Boiler Heating': 'gas boiler, radiators',
'Gas Back Boiler Heating': 'gas boiler, radiators',
'Electric Gel/Oil Fllled Radiators': 'electric radiators',
'Gas Condensing Boiler Heating': 'gas condensing combi',
'Gas Combi Condensing Boiler Heatiner': 'gas condensing combi',
'Oil Standard Boiler Heating': 'oil boiler',
'Oil Condensing Boiler Heating': 'oil boiler',
'Electric ASHP': 'air source heat pump',
'Modern Slimline Storage Heaters': 'electric storage heaters'
}

View file

@ -0,0 +1,231 @@
"""
This script was produced to handle the non-standard outcomes, observed in the LHP outcomes sheet
"""
import numpy as np
outcomes_values = [
"Access Issues", "No Outcome", "Asked for a later date", "Customer Refusal",
"Installer Refusal", "No Answer", "Not Viable", "Surveyed",
"Rescheduled", "Not Knocked", "Void"
]
outcomes_map = {
'Access issues, shed against rear wall. Sent photos to Matt JJC, declined': 'Access Issues',
'NO ANSWER /TICKET LEFT': 'No Answer',
'Looks Void - No Answer': 'No Answer',
'No Answer - they were in - No response to my drop card': 'No Answer', 'No Answer': 'No Answer',
'No Answer - Even they were in - No response to my drop card': 'No Answer', 'no answer': 'No Answer',
'NO ANSWER': 'No Answer', 'No answer': 'No Answer',
np.nan: 'unknown',
'Access Issues Health reasons try another time': 'Access Issues',
'LOFT FULL, CUSTOMER WONT REMOVE': 'Access Issues',
'Failed Appointment - Ivy': 'Access Issues',
'Failed Appointment - Void soon': 'Void',
'Hoarding in loft': 'Access Issues',
'Non Complained - Extension at rear and side': 'Not Viable',
'Said No letter - then texted me I can only do outside but cant come in': 'Customer Refusal',
'Hoarding - unwilling to shift from loft': 'Customer Refusal',
'Overgrown vegatation - Happy for HA to deal with': 'Access Issues',
'No access to side of property': 'Not Viable',
'Very rude': 'Customer Refusal',
'REFUSED ACCESS': 'Customer Refusal',
'SURVEYED': 'Surveyed',
'ELECTRIC ROOM HEATERS. Kieran to check re funding and possible PV?': 'Not Viable',
'SUBMITTED': 'Surveyed',
'2 single storey extensions': 'Not Viable',
'Rebook': 'Rescheduled',
'surveyed': 'Surveyed',
'not intrested': 'Customer Refusal',
'Fixed seating area against rear elevation': 'Not Viable',
"Matt said can't install": 'Installer Refusal',
'Gave excuses to come this and that time and no reponse': 'No Answer',
'NOT KNOCKED': 'Not Knocked',
'VOID PROPERTY': 'Void',
'Glass lean to. JJC declined': 'Installer Refusal',
'Left slip Overgrown vegatation': 'No Answer',
'covid': 'Rescheduled',
'Lean-to on side elevation': 'Not Viable',
'Opted out as moving out': 'Customer Refusal',
'Surveyed': 'Surveyed',
'refused': 'Customer Refusal',
'COVID': 'Rescheduled',
'Said No letter received and didnt answer again': 'No Answer',
'Survey completed': 'Surveyed',
'Loft fully boarded': 'Access Issues',
'Not Available during the day': 'No Answer',
'Conservatory. JJC declined.': 'Installer Refusal',
'Booked for 19.10.23': 'Rescheduled',
'LETTER LEFT': 'No Answer',
'Knocked/lettered': 'No Answer',
'Survey Complete': 'Surveyed',
'Refused by calling office': 'Customer Refusal',
'Extension on rear elevation': 'No Viable',
'Left Slip - Potential access issue with conservatory': 'Access Issues',
'Overgrown vegatation': 'Access Issues',
'Left slip Overgrown Ivy and Hedge': 'No Answer',
'NOT AVAILABLE THIS WEEK': 'No Answer',
'Unwilling to clear loft': 'Access Issues',
'survey complete': 'Surveyed',
'ivy on wall': 'Access Issues',
'not in': 'No Answer',
'Covid shrub very close to building': 'Rescheduled',
'ON HOLIDAY, UNDER 18 IN HOUSE': 'Rescheduled',
'wont do as extention': 'Not Viable',
'IN, WONT ANSWER': 'Customer Refusal',
'Too many plants next to the walls': 'Access Issues',
'obstructions': 'Access Issues',
'Left slip -Wall plant': 'Access Issues',
'On holiday': 'No Answer',
'Failed appointment': 'No Answer',
'LOFT FULLY BOARDED': 'Access Issues',
'ivy and didnt want people inside the house': 'Customer Refusal',
'Partly IWI': 'Not Viable',
'Covid': 'Rescheduled',
'REFUSE TO REMOVE IVY': 'Access Issues',
'Insulated 2 years ago. Carbon bead in walls, 300mm rock wool in loft': 'Not Viable',
'INCONVIENIENT TIME': 'No Answer',
'EXT TO REAR': 'Not Viable',
'Not In': 'No Answer',
'Damp issues.Black mould on walls': 'Access Issues',
'Lean to. JJC declined': 'Installer Refusal',
'DISABLED CHILD / INCONVIENIENT': 'Customer Refusal',
'Plants on wall': 'Access Issues',
'Left Slip': 'No Answer',
'Never answered': 'No Answer',
'SOLAR PV CONNECTED TO MAINS': 'Not Viable',
'Bungalow': 'unknown',
'call back': 'No Answer',
'Message from WFT OFFICE; tenant unavailable this week, no telephone number provided': 'Rescheduled',
'LEAN TO PRESENT': 'Not Viable',
'She said come Tuesday and never answered': 'Rescheduled',
'Sold': 'Surveyed',
'Too much mould and cluttered house': 'Access Issues',
'Overgrown vegatation will call when clear': 'Access Issues',
'LOFT DEC 2013': 'Not Viable',
'Ivy': 'Access Issues',
'Booked for next week': 'Rescheduled',
'empty': 'Void',
'Been told property is empty as tenant has passed away': 'Void',
'Non Complianced - Single Storey Extension to the front and rear': 'Not Viable',
'Going back this week': 'Rescheduled',
'Loft insulated in last few months. Ongoing damp issues in bathroom, black mould up wall': 'Access Issues',
'rear Extension': 'Not Viable',
'DECKING AROUND PROPERTY IN BREACH OF DPC BY 300MM': 'Not Viable',
'Said no letter received': 'Customer Refusal',
'Unwell, not convenient this week': 'Rescheduled',
'IVY on Wall': 'Access Issues',
'REFUSED EXTRACTOR': 'Customer Refusal',
'ON HOLIDAY': 'Rescheduled',
'COVID. Not this week.': 'Rescheduled',
'COVID POSITIVE': 'Rescheduled',
'VOID. Appears to be under refurbishment': 'Void',
'Survey Completed': 'Surveyed',
'INCONVIENIENT': 'Rescheduled',
'Knocked/lettered. 07598 112360': 'No Answer',
'Single skin lean to. JJC declined': 'Installer Refusal',
'DENIES LETER, REFUSED ACCESS': 'Customer Refusal',
'Loft hoard unable to clear': 'Access Issues',
'Left Slip - Look Void': 'Void',
'EXCESSIVE IVY GROWTH, CUSOMER UNABLE TO REMOVE, ELDERLEY': 'Access Issues',
'Refused': 'Customer Refusal',
'REFUSED / INCONVENIENT': 'Customer Refusal',
'AGGRESSIVE DOGS LOOSE IN FRONT GARDEN': 'Access Issues',
'EXCESSIVE IVY': 'Access Issues',
"Won't remove plastic roof": 'Access Issues',
'SURVEY COMPLETED': 'Surveyed',
'VOID. Under refurbishment. Electric storage heating currently removed for refurbishment': 'Void',
'Surveyed ECO4': 'Surveyed',
'after 5.30': 'Rescheduled',
'CUSTOMER IN, WONT ANSWER DOOR': 'No Answer',
'IVY': 'Access Issues',
'Single storey extension on gable': 'Not Viable',
'No answer.': 'No Answer',
'Full extension at rear. Not viable.': 'Not Viable',
'Access issues': 'Access Issues',
'VOID PROPERTY NOW': 'Void',
'Not viable': 'Not Viable',
'Looks like a VOID property': 'Void',
'NOT VIABLE': 'Not Viable',
'No Answer.': 'No Answer',
'Not viable.': 'Not Viable',
'Looks to be void.': 'Void',
'Access issues and loft fully boarded/full': 'Access Issues',
'Extension on property. Not Viable': 'Not Viable',
'No good. Serious Access issues.': 'Access Issues',
'Surveyed and Submitted': 'Surveyed',
'UNSANITARY CONDITIONS, RUBBISH EVERYWHERE': 'Access Issues',
'Will call when rubbish removed.': 'Access Issues',
'Covered in Ivy': 'Access Issues',
'CUSTOMER REFUSED': 'Customer Refusal',
'Still covered in ivy': 'Access Issues',
'CUSTOMER SHOUTED OUT OF WINDOW TO COME BACK ANOTHER TIME': 'Customer Refusal',
"Extension on property, can't be done.": 'Not Viable',
'Will be looking to do Survey WC 19.02': 'Rescheduled',
"Tenant was working, couldn't do survey.": 'No Answer',
'PROPERTY EMPTY, SPOKE TO EX TENNANT WHO LEFT 3 WEEKS AGO?': 'Void',
'Will call back.': 'Rescheduled',
"Tenant not interested. Won't empty loft.": 'Customer Refusal',
"Won't answer door.": 'Customer Refusal',
"Tenant 'Doesn't want anything to do with LHP'": 'Customer Refusal',
"Loft full. Tenant won't empty.": 'Access Issues',
'Covered in foliage': 'Access Issues',
'Customer not home for appointment.': 'No Answer',
'Blown in bead': 'Not Viable',
'Distance to property to far from road.': 'Access Issues',
'LOFT FULL, CUSTOMER UNABLE TO CLEAR': 'Access Issues',
'Stuff against rear wall. Will call when removed.': 'Access Issues',
'Will call when rubbish is removed': 'Access Issues',
'Mid Terrace': 'unknown',
'Tile Hung areas.': 'Not Viable',
'REFUSED / UNABLE TO CLEAR LOFT': 'Customer Refusal',
'Calling back on Monday (19.02)': 'Rescheduled',
'Solid Wall': 'Not Viable',
'FAULTY PHONE NUMBER, 3 X KNOCK, LETTER LEFT ON FIRST ATTEMPT, NO REPLY OR CALL BACK': 'No Answer',
'Not interested': 'Customer Refusal',
'ACCESS DENIED': 'Customer Refusal',
'Covered in Ivy.': 'Access Issues',
'UNABLE TO GENERATE SAP GAIN WITH EXTENSIONS FRONT AND REAR': 'Not Viable',
'Extension on the property.': 'Not Viable',
"Covered in Ivy. Can't remove it.": 'Access Issues',
'Booked in, but not in when called back': 'No Answer',
'EXCESSIVE IVY ON WALLS (SEE PICS)': 'Access Issues',
'Moved out': 'Void',
'Buying the property. Not interested.': 'Customer Refusal',
'Not been to yet': 'No Answer',
'CUSTOMER STATES LOFT WAS INSULATED A FEW MONTHS AGO BY LHP': 'Customer Refusal',
'Will try again.': 'No Answer',
'HOUSE MARTINS NESTING IN EAVES OF 3 ADJOINING PROPERTIES': 'Access Issues',
'Told me to call back': 'Rescheduled',
'CUSTOMER SAYS PROPERTY ALREADY REFUSED AT PREVIOUS SURVEY, NO REASON GIVEN': 'Customer Refusal',
"Won't answer the door.": 'Customer Refusal',
'Tenant not interested.': 'Customer Refusal',
'Keep trying, keeps putting me off.': 'Customer Refusal',
'Already insulated.': 'Not Viable',
'Works all day.': 'No Answer',
'PROPERTY COVER IN FOILAGE AND SHRUBS': 'Access Issues',
'ACCESS IVY GROWTH, LEAN TO / CONSERVATORY IN WAY OF REAR': 'Not Viable',
"Tenant unwell. Doesn't want survey.": 'No Answer',
'Wont empty loft.': 'Access Issues',
'LOFT FULLY BOARDED AS PREVIOUSLY DISCUSSED WITH CUSTOMER BY PREVIOUS SURVEYOR': 'Access Issues',
"Property can't be done.": 'Not Viable',
'Works everyday. Will call.': 'No Answer',
'A LOT OF FOLIAGE IN WAY, PROPERTY LOOKS EMPTY FROM OUTSIDE?': 'Void',
"Very old tenant. Said they didn't want it.": 'Customer Refusal',
'Covered in ivy. Unable to remove.': 'Access Issues',
'Climbers on walls': 'Access Issues',
'Will not remove foliage': 'Access Issues',
'Not Interested.': 'Customer Refusal',
'OFF GAS': 'unknown',
'Tenant not interested': 'Customer Refusal',
'Will call me. Left my number.': 'Rescheduled',
'Keep trying but keeps putting me off': 'Customer Refusal',
'Moving out.': 'Void',
'Booked in': 'Recheduled',
'Refused Survey': 'Customr Refusal',
'Big dogs running around front garden.': 'Access Issues',
'CUSTOMER HAS CLADDED WALL AT REAR IN CONSERVATORY, REFUSED INTERNAL DRILL': 'Customer Refusal',
'Booked in.': 'Rescheduled',
'WRONG ADDRESS?': 'unknown',
'Works everyday. Will call me.': 'No Answer',
'Will not remove foliage.': 'Access Issues'
}

View file

@ -194,5 +194,17 @@ PROPERTY_MAPPING = {
'Maisonette 2 Ext. Wall': 'maisonette',
'5 Ext. Wall Flat': 'flat',
'Bungalow Semi Detached': 'bungalow',
'COMINT': 'unknown'
'COMINT': 'unknown',
'12 SBEDSIT': 'bedsit',
'01 HOUSE': 'house',
'05 BEDSIT': 'bedsit',
'14 SFLAT': 'flat',
'09 PBEDSIT': 'bedsit',
'10 PBUNGALOW': 'bungalow',
'13 SBUNGALOW': 'bungalow',
'11 PFLAT': 'flat',
'02 FLAT': 'flat',
'04 MAISONETTE': 'maisonette',
'01 HOUSE MID': 'house',
'03 BUNGALOW': 'bungalow'
}