diff --git a/etl/eligibility/Eligibility.py b/etl/eligibility/Eligibility.py index 8b205c79..9de09039 100644 --- a/etl/eligibility/Eligibility.py +++ b/etl/eligibility/Eligibility.py @@ -59,6 +59,8 @@ class Eligibility: self.loft_insulation() self.cavity_insulation() + self.tenure = self.tenure_remap.get(self.epc["tenure"], None) + def parse_fabric(self, key): # Get the cleaned version of the description @@ -388,7 +390,6 @@ class Eligibility: self.suspended_floor_insulation() self.solid_floor_insulation() - tenure = self.tenure_remap.get(self.epc["tenure"], None) current_sap = int(self.epc["current-energy-efficiency"]) is_below_e = current_sap <= 54 is_below_c = current_sap <= 68 @@ -403,19 +404,19 @@ class Eligibility: self.solid_floor["suitability"] ) - if tenure == "Rented (social)": + if self.tenure == "Rented (social)": if is_below_c and (not is_below_e): # this is a placeholder methodology self.gbis = { - "eligible": int(self.epc["potential-energy-efficiency"]) > current_sap, - "message": "proxy methodology until we complete innovation measure recommendations" + "eligible": int(self.epc["potential-energy-efficiency"]) > 68, + "message": "contingent on innovation measure delivery" } return - elif (not is_below_c) and is_below_e: + elif is_below_e: self.gbis = { "eligible": needs_measure, - "message": "proxy methodology until we complete innovation measure recommendations" + "message": "eligible under fabric measure" } return else: @@ -425,20 +426,20 @@ class Eligibility: } return - elif tenure == "Rented (private)": + elif self.tenure == "Rented (private)": self.gbis = { "eligible": is_below_c and needs_measure, - "message": "conditional tenant occupancy requirements and coucil tax band" + "message": "eligible under fabric measure" } return - elif tenure == "Owner-occupied": + elif self.tenure == "Owner-occupied": self.gbis = { "eligible": False, "message": "Out-of-scope" } return - elif (tenure is None) or tenure == "unknown": + elif (self.tenure is None) or self.tenure == "unknown": self.gbis = { "eligible": needs_measure, "message": "unknown tenure" @@ -447,8 +448,144 @@ class Eligibility: else: raise ValueError("Implement me other tenure types") - def check_eco4_potential(self): + def check_eco4(self): """ - Because ECO4 supports nearly all measures, if we have commercial agreements in place then we + Because ECO4 supports nearly all measures. If we have commercial agreements in place then a large number + of homes would be eligible for eco funding, if identified. + + These are the eligibility criteria we consider for this process: + Privately rented, Help to heat group + - Sap E-G + - Must receive one of solid wall insulation, first time central heating or district heating control + - The property must already have cavity walls and roof insulated + + Social Housing, SAP D + - Innovation measures and insulation measures to meet the minimum insulation requirement + - Improvement to at least band C + - Fabric measures + - If receiving any heating measures, must have at least one insulation measure first + + Social Housing, SAP E-G + - Insulation measures, first time central heating, renewable heating, district heating connection, + innovation measures + - Improvement to D (F & G properties) or C (E properties) + - If receiving any heating measure, must already have cavity and roof insulation + + Privately rented, ECO4 Flex route 1, 2, 3, 4 + - Must have SAP E-G + - Most measures eligible, but must receive one of solid wall insulation, first time central heating, + renewable heating and district heating control + - Improvement to D (F & G properties) or C (E properties) + - All homes receiving heating measures must first have insulated cavity/roof + + + The flex routes are given here: + https://so-eco.co.uk/what-is-eco4-flex/#:~:text=One%20way%20to%20gain%20ECO4, + including%20elderly%20residents%20and%20lodgers. + :return: """ + + self.cavity_insulation() + self.loft_insulation() + self.solid_wall_insulation() + self.room_roof_insulation() + self.flat_roof_insulation() + self.suspended_floor_insulation() + self.solid_floor_insulation() + + current_sap = int(self.epc["current-energy-efficiency"]) + is_below_e = current_sap <= 54 + is_below_c = current_sap <= 68 + sap_potential = int(self.epc["potential-energy-efficiency"]) + + first_time_central_heating = "boiler" not in self.epc["mainheat-description"].lower() + + needs_fabric_measure = ( + self.cavity["suitability"] or + self.loft["suitability"] or + self.solid_wall["suitability"] or + self.room_roof["suitability"] or + self.flat_roof["suitability"] or + self.suspended_floor["suitability"] or + self.solid_floor["suitability"] + ) + + if current_sap <= 38 and sap_potential >= 55: + # sap needs to get to at least a D + expected_to_meet_upgrades = True + elif current_sap <= 68 and sap_potential >= 69: + # sap needs to get to at least a C + expected_to_meet_upgrades = True + else: + expected_to_meet_upgrades = False + + if self.tenure == "Rented (social)": + if is_below_c and (not is_below_e) and expected_to_meet_upgrades: + # If the property is a D, then it's eligible under innovation measures but requires improvement to a + # band C + self.eco4 = { + "eligible": True, + "message": "eligible under innovation measure and improvement to band C" + } + elif is_below_e and expected_to_meet_upgrades: + # If the property is an E or below, then it's eligible under fabric measures or heating/innovation + # measures + + message = "eligible under fabric measures, with sufficient post retrofit sap improvement" if ( + needs_fabric_measure) else ( + "eligible under heating and innovation measures, with sufficient post retrofit sap improvement" + ) + + self.eco4 = {"eligible": True, "message": message} + else: + if (current_sap <= 68) and expected_to_meet_upgrades: + raise ValueError("something is wrong") + self.eco4 = { + "eligible": False, + "message": "not eligible, above EPC C" + } + + return + + if self.tenure == 'Rented (private)': + # For private homes, the property needs to be an E or below + + # For private homes, the cavity must be filled and the roof insulated + cavity_filled = not self.cavity["suitability"] + roof_insulated = (not self.loft["suitability"]) and (not self.room_roof["suitability"]) and ( + not self.flat_roof["suitability"]) + + if is_below_e and cavity_filled and roof_insulated and expected_to_meet_upgrades: + + if self.solid_wall["suitability"]: + self.eco4 = { + "eligible": True, + "message": "eligible under solid wall insulation, conditional on post retrofit sap and help " + "to heat/ECO flex route" + } + elif first_time_central_heating: + + self.eco4 = { + "eligible": True, + "message": "eligible under first time central heating, conditional on post retrofit sap and " + "help to heat/ECO flex route" + } + else: + self.eco4 = { + "eligible": False, + "message": "not eligible at this time" + } + + return + + else: + self.eco4 = { + "eligible": False, + "message": "not eligible at this time, EPC too high" + } + + self.eco4 = { + "eligible": False, + "message": "Out of scope" + } diff --git a/etl/eligibility/ha_15_32/app.py b/etl/eligibility/ha_15_32/app.py index 027b2c53..9d752d2d 100644 --- a/etl/eligibility/ha_15_32/app.py +++ b/etl/eligibility/ha_15_32/app.py @@ -472,6 +472,8 @@ def get_ha_32data(ha_data, cleaned, cleaning_data, created_at): "message": "No EPC found", "gbis_eligible_future": None, "gbis_eligible_future_message": None, + "tenure": None, + "heating_description": None, } ) continue @@ -503,7 +505,7 @@ def get_ha_32data(ha_data, cleaned, cleaning_data, created_at): # If the house is not identified, we do a full gbis and eco4 check # TODO: Add in ECO4 check eligibility.check_gbis() - # eligibility.check_eco4() + eligibility.check_eco4() if eligibility.eco4_warmfront["eligible"]: scoring_dictionary = prepare_model_data_row( @@ -527,6 +529,8 @@ def get_ha_32data(ha_data, cleaned, cleaning_data, created_at): "message": "eco4 conditional on post sap", "gbis_eligible_future": eligibility.gbis["eligible"], "gbis_eligible_future_message": eligibility.gbis["message"], + "tenure": eligibility.tenure, + "heating_description": eligibility.epc["mainheat-description"], } ) continue @@ -545,6 +549,8 @@ def get_ha_32data(ha_data, cleaned, cleaning_data, created_at): "message": None, "gbis_eligible_future": eligibility.gbis["eligible"], "gbis_eligible_future_message": eligibility.gbis["message"], + "tenure": eligibility.tenure, + "heating_description": eligibility.epc["mainheat-description"], } ) @@ -738,15 +744,28 @@ def get_ha_15data(ha_data, cleaned, cleaning_data, created_at): eligibility_assessment = [] for _, row in results_df[results_df["eco4_eligible"] == True].iterrows(): + # The upgrade requirements are dependent on the current SAP - if row["post_install_sap"] >= 71: - eligibility_classification = "highest confidence" - elif row["post_install_sap"] >= 69: - eligibility_classification = "high confidence" - elif row["post_install_sap"] >= 67: - eligibility_classification = "medium confidence" + # If the property is an F or G, it only needs to upgrade to an % + if row["sap"] <= 38: + if row["post_install_sap"] >= 57: + eligibility_classification = "highest confidence" + elif row["post_install_sap"] >= 55: + eligibility_classification = "high confidence" + elif row["post_install_sap"] >= 53: + eligibility_classification = "medium confidence" + else: + eligibility_classification = "unlikely" else: - eligibility_classification = "unlikely" + + if row["post_install_sap"] >= 71: + eligibility_classification = "highest confidence" + elif row["post_install_sap"] >= 69: + eligibility_classification = "high confidence" + elif row["post_install_sap"] >= 67: + eligibility_classification = "medium confidence" + else: + eligibility_classification = "unlikely" eligibility_assessment.append( { @@ -841,11 +860,19 @@ def analyse_ha_32_results(results, ha32, no_house_numbers): (results_df["gbis_eligible"] | results_df["eco4_eligible"]) ].copy() + new_possibilities_full_gbis = results_df[ + (~results_df["warmfront_identified"]) & + (results_df["gbis_eligible_future"] == True) + ].copy() + # We deem that Any EPC that is produced in the last 3 years gives us good confidence cutoff_date = datetime.now() - timedelta(days=3 * 365) new_possibilities["high_confidence"] = pd.to_datetime(new_possibilities["date_epc"]) >= cutoff_date + new_possibilities_full_gbis["high_confidence"] = pd.to_datetime( + new_possibilities_full_gbis["date_epc"]) >= cutoff_date + # We do a quick check on properties that didn't have a house number: no_house_numbers_ha32 = ha32[ha32["row_id"].isin(no_house_numbers)]["identified"].sum() if no_house_numbers_ha32: @@ -853,7 +880,9 @@ def analyse_ha_32_results(results, ha32, no_house_numbers): new = { "n_new_possibilities": new_possibilities.shape[0], - "new_possibilities_confidence": new_possibilities["high_confidence"].value_counts() + "new_possibilities_confidence": new_possibilities["high_confidence"].value_counts(), + "new_possibilities_full_gbis": new_possibilities_full_gbis.shape[0], + "new_possibilities_full_gbis_confidence": new_possibilities_full_gbis["high_confidence"].value_counts() } return success_rate, new