From 96fb10390b6526ff577d49b2dd9010b31580709d Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Wed, 7 May 2025 15:09:58 +0100 Subject: [PATCH] Working on LHP asset list review --- .idea/Model.iml | 2 +- .idea/misc.xml | 2 +- asset_list/AssetList.py | 271 ++++++++++++++++--------- asset_list/app.py | 105 +++++++++- asset_list/mappings/heating_systems.py | 45 +++- asset_list/mappings/outcomes.py | 231 +++++++++++++++++++++ asset_list/mappings/property_type.py | 14 +- 7 files changed, 570 insertions(+), 100 deletions(-) create mode 100644 asset_list/mappings/outcomes.py diff --git a/.idea/Model.iml b/.idea/Model.iml index df6c4faa..96ad7a95 100644 --- a/.idea/Model.iml +++ b/.idea/Model.iml @@ -7,7 +7,7 @@ - + diff --git a/.idea/misc.xml b/.idea/misc.xml index 50cad4ca..fb10c6b0 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 18e202b6..4586ae57 100644 --- a/asset_list/AssetList.py +++ b/asset_list/AssetList.py @@ -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) diff --git a/asset_list/app.py b/asset_list/app.py index 37e687fc..14322a97 100644 --- a/asset_list/app.py +++ b/asset_list/app.py @@ -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)", diff --git a/asset_list/mappings/heating_systems.py b/asset_list/mappings/heating_systems.py index b5cf500f..e255ba4d 100644 --- a/asset_list/mappings/heating_systems.py +++ b/asset_list/mappings/heating_systems.py @@ -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' } diff --git a/asset_list/mappings/outcomes.py b/asset_list/mappings/outcomes.py new file mode 100644 index 00000000..c376267f --- /dev/null +++ b/asset_list/mappings/outcomes.py @@ -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 didn’t 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 didn’t 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' +} diff --git a/asset_list/mappings/property_type.py b/asset_list/mappings/property_type.py index f01ab5eb..225d1a1f 100644 --- a/asset_list/mappings/property_type.py +++ b/asset_list/mappings/property_type.py @@ -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' }