import numpy as np import pandas as pd class PropertyValuation: """ This is a placeholder class for the property valuation model """ UPRN_VALUE_LOOKUP = { 15038202: 202000, 37024763: 213000, 100070478545: 212000, 100070297696: 662000, # Based on Zoopla's estimation of nearby house, 8 bloomfield road 100070476394: 222000, # Based on Zoopla's estimation of next door, 20 Parkside 100071264896: 128000, # Based on next door neighbour: https://themovemarket.com/tools/propertyprices/flat-2-queens-wood-house-219 # -brandwood-road-birmingham-b14-6pu 100070533688: 218000, # Based on Zoopla's estimation of 95 Tenby Road, which is also mid terrace 100070505235: 344000, # Based on Zoopla's estimation of 131 School road, which is also semi-detached 100070513306: 182000, # Based on Zoopla's estimation of 61 Simmons Drive 100071306896: 77000, # Based on Flat 2 of 44 Wedgewood Road on Zoopla 100021192109: 650000, # Based on Zoopla 766249482: 358000, # Based on Zoopla estimate for 19 Spring Lane, 3 bedroom semi-detached 100120703802: 277000, # Based on Zoopla 10014469685: 286000, # Based on Zoopla 10001328782: 196000, # Based on Zoopla # Urban Splash - valuations from The Move Market 10023345430: 74_000, 10023345435: 99_000, 10023345436: 62_000, 10023345441: 62_000, 10094183503: 2_988_000, 10094183499: 123_000, 10070056824: 70_000, 110070056242: 100_000, 10070056243: 130_000, 10070056817: 130_000, 10094183501: 185_000, 10070056250: 71_000, 10094183500: 185_000, 10070056843: 67_000, 10070056844: 67_000, 10070056241: 76_000, 10070056834: 63_000, 10023345439: 62_000, 10070056815: 101_000, 10070056816: 101_000, 10094183498: 101_000, 10070056840: 673_000, 10070056848: 76_000, 10070056849: 76_000, 10070056829: 76_000, 10070056920: 76_000, 10023345463: 76_000, # IMMO Dudley Pilot - search by going to https://www.zoopla.co.uk/property/uprn/{uprn}/ 90070461: 172_000, # Based on Zoopla 90022227: 181_000, # Based on Zoopla 90106884: 180_000, # Based on Zoopla 90051858: 201_000, # Based on Zoopla 90060989: 172_000, # Based on Zoopla 90048026: 196_000, # Based on Zoopla 90077535: 192_000, # Based on Zoopla 90093693: 279_000, # Based on Zoopla 90055152: 149_000, # Based on Zoopla 90028499: 238_000, # Based on Zoopla # IMMO Dudley Pilot 2- search by going to https://www.zoopla.co.uk/property/uprn/{uprn}/ 90039318: 177_000, # Based on Zoopla 90038384: 170_000, # Based on Zoopla 90105380: 185_000, # Based on Zoopla 90124001: 165_000, # Based on Zoopla 90013980: 148_000, # Based on Zoopla 90087154: 184_000, # Based on Zoopla 90046817: 167_000, # Based on Zoopla # Goldman Sachs Pilot for inrto - search by going to https://www.zoopla.co.uk/property/uprn/{uprn}/ 100070358888: 153_000, # Based on Zoopla 10090436544: 282_000, # Based on Zoopla 100070365751: 177_000, # Based on Zoopla 10095952767: 168_000, # Based on Zoopla 100070520130: 177_000, # Based on Zoopla 100070333957: 185_000, # Based on Zoopla 100070543258: 211_000, # Based on Zoopla # Vander Elliot Pilot - search by going to https://www.zoopla.co.uk/property/uprn/{uprn}/ 41018850: 104_000, # Based on Zoopla 38237316: 74_000, # Based on Zoopla 38237317: 74_000, # Based on Zoopla 41052320: 70_000, # Based on Zoopla 41052321: 70_000, # Based on Zoopla 41052322: 38_000, # Based on Zoopla 41222759: 38_000, # Based on Zoopla 41222760: 46_000, # Based on Zoopla 41222761: 270_000, # Based on Zoopla 41212534: 38_000, # Based on Zoopla # Northern Group Pilot - search by going to https://www.zoopla.co.uk/property/uprn/{uprn}/ 10070868263: 194_000, # Based on Zoopla 10070868244: 195_000, # Based on Zoopla # Places For People Pilot 200140644: 385_000, 200140645: 481_000, 200140646: 372_000, 200140647: 481_000, 200140648: 373_000, 200140649: 373_000, # Vander Elliot Intrusive surveys 12103116: 1_537_000, 12103117: 1_404_000, # GLA Proposal 100020606627: 409_000 } # We base our valuation uplifts on a number of sources # https://www.moneysupermarket.com/gas-and-electricity/value-of-efficiency/ MSM_MAPPING = [ {"start": "G", "end": "F", "increase_percentage": 0.06}, {"start": "F", "end": "E", "increase_percentage": 0.01}, {"start": "E", "end": "D", "increase_percentage": 0.01}, {"start": "D", "end": "C", "increase_percentage": 0.02}, {"start": "C", "end": "B", "increase_percentage": 0.04}, {"start": "B", "end": "A", "increase_percentage": 0.0}, ] # https://www.lloydsbankinggroup.com/media/press-releases/2021/halifax/homebuyers-pay-a-green-premium-of-40000 # -for-the-most-energy-efficient-properties.html LLOYDS_MAPPING = [ {"start": "G", "end": "F", "increase_percentage": 0.038}, {"start": "F", "end": "E", "increase_percentage": 0.029}, {"start": "E", "end": "D", "increase_percentage": 0.024}, {"start": "D", "end": "C", "increase_percentage": 0.02}, {"start": "C", "end": "B", "increase_percentage": 0.02}, {"start": "B", "end": "A", "increase_percentage": 0.018}, ] KNIGHT_FRANK_MAPPING = [ {"start": "D", "end": "C", "increase_percentage": 0.03}, {"start": "D", "end": "B", "increase_percentage": 0.088}, {"start": "D", "end": "A", "increase_percentage": 0.088}, ] NATIONWIDE_MAPPING = [ # {"start": "G", "end": "D", "increase_percentage": 0.035}, # {"start": "F", "end": "D", "increase_percentage": 0.035}, # {"start": "D", "end": "B", "increase_percentage": 0.017}, # {"start": "D", "end": "A", "increase_percentage": 0.017}, ] # Found here: https://www.rightmove.co.uk/news/articles/property-news/green-premium-epc-ratings/ # F -> C is + 15% # E -> C is +7% # D -> C is +3% RIGHTMOVE_MAPPING = [ {"start": "G", "end": "C", "increase_percentage": 0.15}, {"start": "G", "end": "B", "increase_percentage": 0.15}, {"start": "G", "end": "A", "increase_percentage": 0.15}, {"start": "F", "end": "C", "increase_percentage": 0.15}, {"start": "F", "end": "B", "increase_percentage": 0.15}, {"start": "F", "end": "A", "increase_percentage": 0.15}, {"start": "E", "end": "C", "increase_percentage": 0.07}, {"start": "E", "end": "B", "increase_percentage": 0.07}, {"start": "E", "end": "A", "increase_percentage": 0.07}, {"start": "D", "end": "C", "increase_percentage": 0.03}, {"start": "D", "end": "B", "increase_percentage": 0.03}, {"start": "D", "end": "A", "increase_percentage": 0.03}, ] # Additional sources: # https://superhomes.org.uk/wp-content/uploads/2024/05/The-Impact-of-Retrofit-on-Residential-Property-Market # -Values-7-rotated-1.pdf EPC_BANDS = ["G", "F", "E", "D", "C", "B", "A"] @classmethod def get_increase(cls, epc_band_range): increases = [] for i in range(len(epc_band_range)): if i == len(epc_band_range) - 1: break current = epc_band_range[i] next = epc_band_range[i + 1] msm_increase = [x for x in cls.MSM_MAPPING if x["start"] == current and x["end"] == next][0] lloyds_increase = [x for x in cls.LLOYDS_MAPPING if x["start"] == current and x["end"] == next][0] increases.append( { "start": current, "end": next, "msm_increase": msm_increase["increase_percentage"], "lloyds_increase": lloyds_increase["increase_percentage"], } ) # We now aggregate the increases. The should be compound increases so we multiply them together msm_increase = np.prod([1 + x["msm_increase"] for x in increases]) - 1 lloyds_increase = np.prod([1 + x["lloyds_increase"] for x in increases]) - 1 return msm_increase, lloyds_increase @classmethod def estimate(cls, property_instance, target_epc, total_cost=None): """ This function estimates the value of a property based on the current EPC rating and the target EPC rating :param property_instance: An instance of the Property class :param target_epc: The target EPC rating :param total_cost: The total cost of the retrofit :return: """ current_value = ( property_instance.valuation if property_instance.valuation else cls.UPRN_VALUE_LOOKUP.get(property_instance.uprn) ) current_epc = property_instance.epc_record.current_energy_rating if not current_value: # In this case, we return a % improvement rather than an absolute relative_improvement = cls.estimate_valuation_improvement( current_value=1, current_epc=current_epc, target_epc=target_epc, total_cost=1 ) return { "current_value": 0, "lower_bound_increased_value": relative_improvement["lower_bound_increased_value"] - 1, "upper_bound_increased_value": relative_improvement["upper_bound_increased_value"] - 1, "average_increased_value": relative_improvement["average_increased_value"] - 1, "average_increase": relative_improvement["average_increase"] } return cls.estimate_valuation_improvement(current_value, current_epc, target_epc, total_cost) @classmethod def estimate_valuation_improvement(cls, current_value, current_epc, target_epc, total_cost=None): """ This function estimates the value of a property based on the current EPC rating and the target EPC rating :param current_value: :param current_epc: :param target_epc: :param total_cost: :return: """ if not current_value: return { "current_value": 0, "lower_bound_increased_value": 0, "upper_bound_increased_value": 0, "average_increased_value": 0, "average_increase": 0 } # We get the spectrum of ratings between the current and target EPC epc_band_range = cls.EPC_BANDS[cls.EPC_BANDS.index(current_epc): cls.EPC_BANDS.index(target_epc) + 1] msm_increase, lloyds_increase = cls.get_increase(epc_band_range) # We now use the knight frank, nationwide and Rightmove data to get further valuation evidence, if we have it kf_increase = [x for x in cls.KNIGHT_FRANK_MAPPING if x["start"] == current_epc and x["end"] == target_epc] nw_increase = [x for x in cls.NATIONWIDE_MAPPING if x["start"] == current_epc and x["end"] == target_epc] rm_increase = [x for x in cls.RIGHTMOVE_MAPPING if x["start"] == current_epc and x["end"] == target_epc] kf_increase = kf_increase[0]["increase_percentage"] if kf_increase else None nw_increase = nw_increase[0]["increase_percentage"] if nw_increase else None rm_increase = rm_increase[0]["increase_percentage"] if rm_increase else None all_increases = [ x for x in [msm_increase, lloyds_increase, kf_increase, nw_increase, rm_increase] if x is not None ] max_increase = max(all_increases) min_increase = min(all_increases) avg_increase = np.mean(all_increases) if total_cost is not None: # We CAP the retrofit ROI at 2 avg_increase_value = current_value * avg_increase if avg_increase_value / total_cost > 2: # We re-scale the % so that the average value increase is no more than 2 times the total cost double_cost = 2 * total_cost new_avg_increase = double_cost / current_value scalar = new_avg_increase / avg_increase # We scale the min and max increases by the same scalar min_increase *= scalar max_increase *= scalar avg_increase = new_avg_increase return { "current_value": current_value, "lower_bound_increased_value": float(current_value * (1 + min_increase)), "upper_bound_increased_value": float(current_value * (1 + max_increase)), "average_increased_value": float(current_value * (1 + avg_increase)), "average_increase": float(current_value * (1 + avg_increase) - current_value) }