diff --git a/.idea/Model.iml b/.idea/Model.iml
index b0f9c00d..4413bb06 100644
--- a/.idea/Model.iml
+++ b/.idea/Model.iml
@@ -7,7 +7,7 @@
-
+
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 1122b380..6f308057 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -3,7 +3,7 @@
-
+
diff --git a/backend/app/db/models/materials.py b/backend/app/db/models/materials.py
index 1a41f14f..f887fc25 100644
--- a/backend/app/db/models/materials.py
+++ b/backend/app/db/models/materials.py
@@ -35,7 +35,7 @@ class MaterialType(enum.Enum):
low_energy_lighting_installation = "low_energy_lighting_installation"
flat_roof_preparation = "flat_roof_preparation"
flat_roof_vapour_barrier = "flat_roof_vapour_barrier"
- flat_roof_waterpoofing = "flat_roof_waterpoofing"
+ flat_roof_waterproofing = "flat_roof_waterproofing"
class DepthUnit(enum.Enum):
diff --git a/backend/ml_models/Valuation.py b/backend/ml_models/Valuation.py
index ad296409..6888e45e 100644
--- a/backend/ml_models/Valuation.py
+++ b/backend/ml_models/Valuation.py
@@ -1,22 +1,117 @@
+import numpy as np
+
+
class PropertyValuation:
"""
This is a placeholder class for the property valuation model
"""
UPRN_VALUE_LOOKUP = {
- 15038202: {"current_value": 202000, "increase_percentage": 0.05725},
- 37024763: {"current_value": 213000, "increase_percentage": 0.025},
+ 15038202: 202000,
+ 37024763: 213000,
+ 100070478545: 212000,
+ 100070297696: 235000,
+ 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
}
+ # 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},
+ ]
+
+ 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},
+ ]
+
+ 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):
- data = cls.UPRN_VALUE_LOOKUP.get(property_instance.uprn)
+ value = cls.UPRN_VALUE_LOOKUP.get(property_instance.uprn)
- if not data:
+ if not value:
raise ValueError("Have not implemented valuation for this property")
- new_valuation = (1 + data["increase_percentage"]) * data["current_value"]
+ current_epc = property_instance.data["current-energy-rating"]
+ # 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]
- increase = round(new_valuation - data["current_value"], 2)
+ msm_increase, lloyds_increase = cls.get_increase(epc_band_range)
- return increase
+ # We now use the knight frank and nationwide 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]
+
+ kf_increase = kf_increase[0]["increase_percentage"] if kf_increase else None
+ nw_increase = nw_increase[0]["increase_percentage"] if nw_increase else None
+
+ all_increases = [x for x in [msm_increase, lloyds_increase, kf_increase, nw_increase] if x is not None]
+
+ max_increase = max(all_increases)
+ min_increase = min(all_increases)
+ avg_increase = np.mean(all_increases)
+
+ return {
+ "current_value": value,
+ "lower_bound_increased_value": value * (1 + min_increase),
+ "upper_bound_increased_value": value * (1 + max_increase),
+ "average_increased_value": value * (1 + avg_increase),
+ }