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'
}