Model/etl/eligibility/Eligibility.py
2023-12-27 11:05:12 +00:00

622 lines
24 KiB
Python

from recommendations.recommendation_utils import convert_thickness_to_numeric
from etl.epc_clean.epc_attributes.RoofAttributes import RoofAttributes
from etl.epc_clean.epc_attributes.WallAttributes import WallAttributes
from etl.epc_clean.epc_attributes.FloorAttributes import FloorAttributes
class Eligibility:
"""
Given the epc data about a property, this class holds the logic for determining if the home
is eligible for a specific retrofit measure.
For example, this could be whether the loft has insulation below a standardised threshold, or
if it has an empty cavity
Further to this, this class is responsible for determining if the property is suitable for specific funding
schemes
"""
loft = None
cavity = None
solid_wall = None
room_roof = None
flat_roof = None
suspended_floor = None
solid_floor = None
# schemes based on Warmfront now
gbis_warmfront = None
eco4_warmfront = None
# Schemes based on full eligibility
gbis = None
eco4 = None
# If the loft has less than 100mm of insulation, we classify the home has needing loft insulation
LOFT_INSULATION_THRESHOLD = 100
# Because EPCS have different values for tenure, we need to remap them to a common set of values
tenure_remap = {
'NO DATA!': "unknown",
'Not defined - use in the case of a new dwelling for which the intended tenure in not known. It is no':
"unknown",
'Owner-occupied': 'Owner-occupied',
'Rented (private)': 'Rented (private)',
'Rented (social)': 'Rented (social)',
'owner-occupied': 'Owner-occupied',
'rental (private)': 'Rented (private)',
'rental (social)': 'Rented (social)',
'unknown': "unknown",
}
def __init__(self, epc, cleaned):
self.epc = epc
self.cleaned = cleaned
self.walls = self.parse_fabric("walls-description")
self.roof = self.parse_fabric("roof-description")
self.floor = self.parse_fabric("floor-description")
self.tenure = self.tenure_remap.get(self.epc["tenure"], None)
def parse_fabric(self, key):
# Get the cleaned version of the description
remapped = [
data for data in self.cleaned[key] if
data["original_description"] == self.epc[key]
]
if remapped:
return remapped[0]
if "SAP05:" in self.epc[key]:
# This is a placeholder method for handling this but this will occur in the case of a very old
# EPC and therefore we just skip
self.epc[key] = "(assumed)"
if key == "walls-description":
cleaner_cls = WallAttributes(self.epc[key])
elif key == "roof-description":
cleaner_cls = RoofAttributes(self.epc[key])
elif key == "floor-description":
cleaner_cls = FloorAttributes(self.epc[key])
else:
raise ValueError("Invalid key")
output = cleaner_cls.process()
output["clean_description"] = cleaner_cls.description.replace("(assumed)", "").rstrip().capitalize()
return output
def loft_insulation(self, loft_thickness_threshold: int = None):
"""
Given the description of roof, this function determines whether or not the property is suitable for loft
insulation. A loft existing insulation with a thickness below loft_thickness_threshold, is deemed to
be suitable for loft insulation
:param loft_thickness_threshold: Integer, Optional. If provided, any loft found with insulation lower than
this thickness is deemed to be suitable for loft insulation. If this
parameter is not provided, this method will default to the variable specified
in LOFT_INSULATION_THRESHOLD
"""
loft_thickness_threshold = (
self.LOFT_INSULATION_THRESHOLD if loft_thickness_threshold is None else loft_thickness_threshold
)
# We firstly check if the roof is a loft
is_loft = self.roof["is_pitched"] and (not self.roof["is_roof_room"])
if not is_loft:
self.loft = {
"suitability": False,
"thickness": None,
"reason": "roof not loft"
}
return
# If it is a loft, we'll convert the textual thickenss to a numerical value we can easily use
insulation_thickness = convert_thickness_to_numeric(
string_thickness=self.roof["insulation_thickness"],
is_pitched=self.roof["is_pitched"],
is_flat=self.roof["is_flat"]
)
if insulation_thickness > loft_thickness_threshold:
# Insulation is already thick enough
self.loft = {
"suitability": False,
"thickness": insulation_thickness,
"reason": "existing insulation"
}
return
self.loft = {
"suitability": True,
"thickness": insulation_thickness,
"reason": None
}
def cavity_insulation(self):
"""
Given the description of the walls, this function determines if the property is suitable for cavity wall
insulation
:return:
"""
is_cavity = self.walls["is_cavity_wall"]
is_empty = (not self.walls["is_filled_cavity"]) or (
self.walls["is_as_built"] and self.walls["insulation_thickness"] not in ["average", "above average"]
)
is_partial_filled = (
self.walls["is_as_built"] and self.walls["insulation_thickness"] not in ["below average"]
)
# We look for potentially under performing cavities - anything that is assumed, as built and insulated
is_underperforming = (
self.walls["is_as_built"] and self.walls["insulation_thickness"] in ["average"] and self.walls["is_assumed"]
)
is_unfilled_cavity = is_cavity and is_empty
is_partial_filled_cavity = is_cavity and is_partial_filled
is_underperforming_cavity = is_cavity and is_underperforming
if is_unfilled_cavity:
self.cavity = {
"suitability": True,
"type": "empty",
}
return
if is_partial_filled_cavity:
self.cavity = {
"suitability": True,
"type": "partial"
}
return
if is_underperforming_cavity:
self.cavity = {
"suitability": True,
"type": "underperforming"
}
return
self.cavity = {
"suitability": False,
"type": "full"
}
def solid_wall_insulation(self):
"""
Given the description of the walls, this function determines if the property is suitable for solid wall
insulation
:return:
"""
is_solid = self.walls["is_solid_brick"]
is_insulated = self.walls["insulation_thickness"] in ["average", "above average"]
if is_solid and is_insulated:
self.solid_wall = {
"suitability": True,
}
return
self.solid_wall = {
"suitability": False,
}
def room_roof_insulation(self):
is_room_roof = self.roof["is_roof_room"]
insulation_thickness = convert_thickness_to_numeric(
self.roof["insulation_thickness"],
self.roof["is_pitched"],
self.roof["is_flat"]
)
self.room_roof = {
"suitability": is_room_roof and insulation_thickness == 0,
"thickness": insulation_thickness
}
def flat_roof_insulation(self):
is_flat = self.roof["is_flat"]
insulation_thickness = convert_thickness_to_numeric(
self.roof["insulation_thickness"],
self.roof["is_pitched"],
self.roof["is_flat"]
)
self.flat_roof = {
"suitability": is_flat and insulation_thickness <= 100,
"thickness": insulation_thickness
}
def suspended_floor_insulation(self):
if "no_data" in self.floor.keys():
if self.floor["no_data"]:
self.suspended_floor = {
"suitability": False,
}
return
is_suspended = self.floor["is_suspended"]
is_insulated = self.floor["insulation_thickness"] in ["average", "above average"]
self.suspended_floor = {
"suitability": is_suspended and (not is_insulated),
}
return
def solid_floor_insulation(self):
if "no_data" in self.floor.keys():
if self.floor["no_data"]:
self.solid_floor = {
"suitability": False,
}
return
is_solid = self.floor["is_solid"]
is_insulated = self.floor["insulation_thickness"] in ["average", "above average"]
self.solid_floor = {
"suitability": is_solid and (not is_insulated),
}
return
def check_gbis_warmfront(self):
"""
The Eligibility criteria for the Great British Insulation Scheme (GBIS) can be found here:
https://www.ofgem.gov.uk/environmental-and-social-schemes/great-british-insulation-scheme/homeowners-and-tenants
At a high level, the criteria is the following:
- The home must be within council tax bands A-D in England, A-E in Scotland, A-E in Wales
- It must have an EPC rating of D or below
For the moment, we won't check whether a property is in the correct council tax band. There is likely
to be public data for this since there is a govenment website which allows you to search for properties:
https://www.gov.uk/council-tax-bands
This data is possibly contained on the council tax valuation list but it remains to be see (seems unlikely)
whether or not the data is openly accessible
https://www.gov.uk/government/statistics/quality-assurance-of-administrative-data-in-the-uk-house-price-index
/valuation-office-agency-council-tax-valuation-lists
Currently, we tailor this module to the Warmfront Team and their delivery capabilities (both practically and
commercially). Therefore, we will check:
1) Whether the property is an EPC D or below
2) Whether the property is suitible for cavity wall insulation
However, GBIS applies to many insulation measures, which can be seen in the ofgem document
GBIS does not have any minimum upgrade requirement so we don't need to simulate the post retrofit sap score
using the machine learning model
"""
# Check if the property is suitable for cavity wall
self.cavity_insulation()
self.loft_insulation()
self.gbis_warmfront = (self.cavity["suitability"]) and (
int(self.epc["current-energy-efficiency"]) <= 68
)
def check_eco4_warmfront(self, post_retrofit_sap=None):
"""
This funciton will check if the property is eligible for funding under the ECO4 scheme
For the moment, this function will consider just measures that can be implemented by the
Warmfront team, therefore we will only check if a property has an uninsulated loft AND uninsulated
cavity
We use Ofgem's V1.1 ECO 4 guidance document for the conditions under which a property is elligible
This document can be found here:
https://www.ofgem.gov.uk/sites/default/files/2023-02/ECO4%20Delivery%20Guidance%20v1.1%20%281%29.pdf
The conditions (to be reviewed) to be eligible for retrofit, under ECO4, are the following:
1) The property is a social home (This is assumed prior to this function as this code will often
be run on property lists provided by a HA
2) The property is an EPC E or below
3) The property has an unfilled cavity and uninsulated loft
4) After retrofit, the property will hit an EPC C
Note: This criteria will likely be adjusted depending on the properties that can be served right now
If the post_retrofit_sap is provided, then is this value is 69 or higher, the property will be deemed
to be eligible for ECO4 funding. If the post_retrofit_sap is not provided, the property will be
deemed to be eligible, conditional to the post_retrofit_sap score check
:param post_retrofit_sap:
:return:
"""
current_sap = int(self.epc["current-energy-efficiency"])
if current_sap > 54:
self.eco4_warmfront = {
"eligible": False,
"message": "sap too high"
}
return
self.cavity_insulation()
self.loft_insulation()
# make sure conditions 2 and 3 are true
is_eligible = self.cavity["suitability"] & self.loft["suitability"]
if post_retrofit_sap is None:
message = "subject to post retrofit sap" if is_eligible else "not eligible"
self.eco4_warmfront = {
"eligible": is_eligible,
"message": message
}
return
is_eligible = is_eligible & (post_retrofit_sap >= 69)
self.eco4_warmfront = {
"eligible": is_eligible,
"message": None
}
return
def check_gbis(self):
"""
The Eligibility criteria for the Great British Insulation Scheme (GBIS) can be found here:
https://www.ofgem.gov.uk/environmental-and-social-schemes/great-british-insulation-scheme/homeowners-and-tenants
Full delivery guidance and be downloaded here:
https://www.ofgem.gov.uk/sites/default/files/2023-08/Great%20British%20Insulation%20Scheme%20Delivery
%20Guidance%20V101693416860968.pdf
For social housing, the criteria is the following:
If the property is currently an EPC D:
- It's valid for innovation measures only but not a heating control measure
- The property must be rented at below the market rate. All eligible social housing is treated based on the
low income group, therefore the tennant must be in receipt of one the eligible benefits
If the property is currently an EPC E or below:
- It's valid for all eligible insulation measures
- The property must be rented at below the market rate. All eligible social housing is treated based on the
low income group, therefore the tennant must be in receipt of one the eligible benefits
From GBIS guidance document:
Determining whether the premises are let below market rate
3.101 Social housing under this provision will only be eligible where the housing is let below
the market rate. The supplier must produce a declaration signed by a social landlord
providing confirmation that the social housing premises are let below the market rate,
or where the premises are currently void, have previously and will be let below the
market rate. The declaration to be signed by a social landlord is included within the
Eligibility and Pre-Retrofit Declaration form. This declaration form must be retained by
suppliers and be available on request for audit purposes.
3.102 Where social housing is let at or above the market rate, the property can be treated as
a private domestic premises, where the occupant meets the eligibility requirements.
See section on PRS from paragraph 1.13 for more information.
This method searches ALL of the possible measures that can be implemented under GBIS. This includes:
- cavity wall (including party wall)
- loft
- solid wall
- pitched roof
- flat roof
- under-floor
- solid floor
- park home
- room-in-roof
: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
needs_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 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"]) > 68,
"message": "contingent on innovation measure delivery"
}
return
elif is_below_e:
self.gbis = {
"eligible": needs_measure,
"message": "eligible under fabric measure"
}
return
else:
self.gbis = {
"eligible": False,
"message": "not eligible"
}
return
elif self.tenure == "Rented (private)":
self.gbis = {
"eligible": is_below_c and needs_measure,
"message": "eligible under fabric measure"
}
return
elif self.tenure == "Owner-occupied":
self.gbis = {
"eligible": False,
"message": "Out-of-scope"
}
return
elif (self.tenure is None) or self.tenure == "unknown":
self.gbis = {
"eligible": needs_measure,
"message": "unknown tenure"
}
return
else:
raise ValueError("Implement me other tenure types")
def check_eco4(self):
"""
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"
}