From e9366c72e891b5405607714f064d7a0326772d08 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Thu, 27 Jun 2024 12:41:56 +0100 Subject: [PATCH 1/9] added handling of some additional cases in sap description cleaning --- .idea/Model.iml | 2 +- .idea/misc.xml | 2 +- .../stonewater/outputs 27th June 2024.py | 48 +++++ etl/customers/stonewater/shdf_3_clustering.py | 200 +++++------------- .../epc_attributes/FloorAttributes.py | 2 +- .../epc_attributes/HotWaterAttributes.py | 4 +- .../epc_attributes/LightingAttributes.py | 11 +- .../epc_attributes/MainheatAttributes.py | 15 +- .../MainheatControlAttributes.py | 4 +- .../epc_attributes/WindowAttributes.py | 2 +- 10 files changed, 126 insertions(+), 164 deletions(-) create mode 100644 etl/customers/stonewater/outputs 27th June 2024.py diff --git a/.idea/Model.iml b/.idea/Model.iml index 4413bb06..b0f9c00d 100644 --- a/.idea/Model.iml +++ b/.idea/Model.iml @@ -7,7 +7,7 @@ - + diff --git a/.idea/misc.xml b/.idea/misc.xml index 6f308057..1122b380 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -3,7 +3,7 @@ - + diff --git a/etl/customers/stonewater/outputs 27th June 2024.py b/etl/customers/stonewater/outputs 27th June 2024.py new file mode 100644 index 00000000..ebb6fc5b --- /dev/null +++ b/etl/customers/stonewater/outputs 27th June 2024.py @@ -0,0 +1,48 @@ +""" +This script prepares some outputs for the stonewater project, 27th June 2024 + +The work done so far has been data cleaning and clustering. +In this script, we do the following things: + +1) Match the clustering data to the archetypes +2) Do some basic analysis on the data +3) Mapping of the archetypes +""" +import pandas as pd +from utils.s3 import read_pickle_from_s3 + +archetyped_asset_list = pd.read_csv("Stonewater asset list with archetypes.csv") +archetyped_asset_list = archetyped_asset_list[ + [ + "internal_id", "customer_asset_id", "udprn", "uprn", "cluster", "archetype_representative", "rank" + ] +] +archetyped_asset_list = archetyped_asset_list[archetyped_asset_list["rank"] != "NO ARCHETYPE"] +archetyped_asset_list["rank"] = archetyped_asset_list["rank"].astype(int) +# Sort +archetyped_asset_list = archetyped_asset_list.sort_values(by=["cluster", "rank"]) + +# Read in and merge on clustering features +clustering_features = read_pickle_from_s3( + bucket_name="retrofit-data-dev", + s3_file_name="customers/Stonewater/clustering/clustering_dataframe.pkl" +) + +archetyped_asset_list = archetyped_asset_list.merge( + clustering_features, + on="internal_id", + how="inner" +) + +property_type_archetypes = archetyped_asset_list[ + ["cluster", "rank", "property-type", "built-form", "walls-description"]] + +# Key variables for separation: +# - property-type +# - built-form +# - walls-description +# - roof-description + +clustering_features[["property-type", "built-form", "walls-description"]].drop_duplicates().shape + +clustering_features["walls-description"].value_counts() diff --git a/etl/customers/stonewater/shdf_3_clustering.py b/etl/customers/stonewater/shdf_3_clustering.py index 6c7a0fc6..b8e71ae7 100644 --- a/etl/customers/stonewater/shdf_3_clustering.py +++ b/etl/customers/stonewater/shdf_3_clustering.py @@ -1633,58 +1633,60 @@ def compile_data_final(): # ) # from utils.s3 import read_pickle_from_s3 - # data = read_pickle_from_s3( + # property_attributes = read_pickle_from_s3( # bucket_name="retrofit-data-dev", # s3_file_name="customers/Stonewater/clustering/clustering_dataframe.pkl" # ) - # CLUSTERING!! + # We perform some additional cleaning on the data + import msgpack + cleaned = read_from_s3( + s3_file_name="cleaned_epc_data/cleaned.bson", + bucket_name="retrofit-data-dev" + ) - # from sklearn.cluster import KMeans - # from sklearn.preprocessing import OneHotEncoder - # from scipy.spatial.distance import cdist - # - # property_attributes.set_index('internal_id', inplace=True) - # - # # Step 1: Prepare the data - # # Identify categorical columns (you might need to adjust this) - # categorical_cols = property_attributes.select_dtypes(include=['object', 'category']).columns.tolist() - # for col in categorical_cols: - # property_attributes[col] = property_attributes[col].astype(str) - # - # # Applying OneHotEncoder - # encoder = OneHotEncoder(sparse=False) - # encoded_cats = encoder.fit_transform(property_attributes[categorical_cols]) - # - # # Creating a new DataFrame with encoded categorical data and original numerical data - # numerical_data = property_attributes.select_dtypes(include=[np.number]) - # data_for_clustering = pd.concat([numerical_data, pd.DataFrame(encoded_cats, index=numerical_data.index)], axis=1) - # - # # Convert all column names to strings to satisfy KMeans requirements - # data_for_clustering.columns = data_for_clustering.columns.astype(str) - # - # # Step 2: K-Means Clustering - # k = 450 # number of clusters - # kmeans = KMeans(n_clusters=k, random_state=0) - # property_attributes['cluster'] = kmeans.fit_predict(data_for_clustering) - # - # # Extracting centroids - # centroids = kmeans.cluster_centers_ - # - # # Step 3: Assign clusters and rank rows - # # Calculating distances from each point to its cluster's centroid - # distances = cdist(data_for_clustering, centroids, 'euclidean') - # min_distances = distances.min(axis=1) - # property_attributes['distance_to_centroid'] = min_distances - # - # # Ranking rows by distance within each cluster - # property_attributes['rank'] = property_attributes.groupby('cluster')['distance_to_centroid'].rank(method='first') - # - # # Sorting to verify - # property_attributes.sort_values(by=['cluster', 'rank'], inplace=True) - # - # # Optional: Displaying the dataframe - # print(property_attributes.head()) + cleaned = msgpack.unpackb(cleaned, raw=False) + from etl.epc_clean.epc_attributes.FloorAttributes import FloorAttributes + from etl.epc_clean.epc_attributes.HotWaterAttributes import HotWaterAttributes + from etl.epc_clean.epc_attributes.MainFuelAttributes import MainFuelAttributes + from etl.epc_clean.epc_attributes.MainheatAttributes import MainHeatAttributes + from etl.epc_clean.epc_attributes.MainheatControlAttributes import MainheatControlAttributes + from etl.epc_clean.epc_attributes.RoofAttributes import RoofAttributes + from etl.epc_clean.epc_attributes.WallAttributes import WallAttributes + from etl.epc_clean.epc_attributes.WindowAttributes import WindowAttributes + from etl.epc_clean.epc_attributes.LightingAttributes import LightingAttributes + + cleaners = { + "floor-description": FloorAttributes, + 'hotwater-description': HotWaterAttributes, + 'main-fuel': MainFuelAttributes, + 'mainheat-description': MainHeatAttributes, + 'mainheatcont-description': MainheatControlAttributes, + 'roof-description': RoofAttributes, + 'walls-description': WallAttributes, + 'windows-description': WindowAttributes, + 'lighting-description': LightingAttributes + } + for variable_to_clean in cleaned.keys(): + unique_descriptions = property_attributes[variable_to_clean].unique() + clean_df = pd.DataFrame(cleaned[variable_to_clean]) + # Check if we have any + missed = [x for x in unique_descriptions if x not in clean_df["original_description"].values] + if missed: + descriptions_to_append = [] + for description in missed: + if variable_to_clean == "lighting-description": + cln = cleaners[variable_to_clean](description, **{"averages": pd.DataFrame()}) + else: + cln = cleaners[variable_to_clean](description) + to_append = { + "original_description": description, + "clean_description": cln.description.replace("(assumed)", "").rstrip().capitalize(), + **cln.process() + } + descriptions_to_append.append(to_append) + + # CLUSTERING!! from sklearn.cluster import KMeans from sklearn.preprocessing import StandardScaler, OneHotEncoder @@ -1777,110 +1779,6 @@ def compile_data_final(): stonewater_uprn_lookup.to_excel("Stonewater uprn lookup table.xlsx") - ################################################ - # Agglomertive Clustering - ################################################ - - # from sklearn.cluster import KMeans, AgglomerativeClustering - # from sklearn.preprocessing import StandardScaler, OneHotEncoder - # from sklearn.compose import ColumnTransformer - # from sklearn.pipeline import Pipeline - # from scipy.spatial.distance import cdist - # import numpy as np - # from collections import Counter - # - # id_column = 'internal_id' - # property_attributes.set_index(id_column, inplace=True) - # - # # Define the preprocessing for numerical and categorical features - # numerical_features = property_attributes.select_dtypes(include=['int64', 'float64']).columns.tolist() - # categorical_features = property_attributes.select_dtypes(include=['object', 'category']).columns.tolist() - # - # for col in categorical_features: - # property_attributes[col] = property_attributes[col].astype(str) - # - # preprocessor = ColumnTransformer( - # transformers=[ - # ('num', StandardScaler(), numerical_features), - # ('cat', OneHotEncoder(sparse_output=False), categorical_features) - # ] - # ) - # - # # Function to perform clustering and merge small clusters - # def cluster_with_min_size(data, preprocessor, n_clusters=10, min_size=5): - # while True: - # # Preprocess the data - # processed_data = preprocessor.fit_transform(data) - # - # # Initial clustering - # clustering = AgglomerativeClustering(n_clusters=n_clusters) - # labels = clustering.fit_predict(processed_data) - # - # # Check cluster sizes - # cluster_counts = Counter(labels) - # - # # Find clusters smaller than min_size - # small_clusters = {cluster for cluster, count in cluster_counts.items() if count < min_size} - # - # if not small_clusters: - # break - # - # # Merge small clusters - # for cluster in small_clusters: - # # Find the nearest cluster to merge with - # cluster_data = processed_data[labels == cluster] - # other_clusters = [i for i in range(n_clusters) if i not in small_clusters] - # other_cluster_data = [processed_data[labels == i] for i in other_clusters] - # other_centroids = np.vstack([data.mean(axis=0) for data in other_cluster_data]) - # - # distances = cdist(cluster_data, other_centroids).mean(axis=0) - # closest_cluster = other_clusters[np.argmin(distances)] - # - # labels[labels == cluster] = closest_cluster - # - # n_clusters -= len(small_clusters) - # - # return labels - # - # # Perform clustering with minimum size constraint - # n_clusters = 10 - # min_size = 5 - # property_attributes['cluster'] = cluster_with_min_size(property_attributes, preprocessor, n_clusters, min_size) - # - # # Filter out empty clusters - # valid_clusters = property_attributes['cluster'].unique() - # - # # Get centroids for the resulting clusters - # processed_data = preprocessor.transform(property_attributes.drop(columns=["cluster"])) - # centroids = np.vstack([processed_data[property_attributes['cluster'] == i].mean(axis=0) for i in valid_clusters]) - # - # # Calculate distances from each point to the centroid of its cluster - # distances_to_centroids = [ - # cdist(processed_data[i].reshape(1, -1), - # centroids[valid_clusters.tolist().index(label)].reshape(1, -1)).flatten()[0] - # for i, label in enumerate(property_attributes['cluster']) - # ] - # - # property_attributes['distance_to_centroid'] = distances_to_centroids - # - # # Verify that at least one point in each cluster has zero distance to the centroid - # for cluster_id in valid_clusters: - # cluster_data = property_attributes[property_attributes['cluster'] == cluster_id] - # min_distance = cluster_data['distance_to_centroid'].min() - # print(f"Cluster {cluster_id} minimum distance to centroid: {min_distance}") - # if min_distance != 0: - # print(f"No point with zero distance found in cluster {cluster_id}") - # - # # Rank the distances within each cluster - # property_attributes['rank_within_cluster'] = property_attributes.groupby('cluster')['distance_to_centroid'] \ - # .rank(method='first') - # - # # Reset index to get 'internal_id' back - # property_attributes.reset_index(inplace=True) - # - # # Display the DataFrame - # print(property_attributes) - def pull_ideal_postcodes(missing_uprn_with_udprn): api_key = "" # Log into the platform the get the API key: https://account.ideal-postcodes.co.uk/ diff --git a/etl/epc_clean/epc_attributes/FloorAttributes.py b/etl/epc_clean/epc_attributes/FloorAttributes.py index 245a91bc..817c2b43 100644 --- a/etl/epc_clean/epc_attributes/FloorAttributes.py +++ b/etl/epc_clean/epc_attributes/FloorAttributes.py @@ -38,7 +38,7 @@ class FloorAttributes(Definitions): self.description: str = description.lower() self.nodata = (not description) or (description in self.DATA_ANOMALY_MATCHES) or ( - description in self.OBSERVED_ERRORS) + description in self.OBSERVED_ERRORS) or (self.description == "sap05:floor") # Try and perform a translation, incase it's in welsh self.translate_welsh_text() diff --git a/etl/epc_clean/epc_attributes/HotWaterAttributes.py b/etl/epc_clean/epc_attributes/HotWaterAttributes.py index 54deaa09..f9cec48b 100644 --- a/etl/epc_clean/epc_attributes/HotWaterAttributes.py +++ b/etl/epc_clean/epc_attributes/HotWaterAttributes.py @@ -129,7 +129,9 @@ class HotWaterAttributes(Definitions): def __init__(self, description: str): self.description: str = clean_description(description.lower()).strip() - self.nodata = not self.description or description in self.DATA_ANOMALY_MATCHES + self.nodata = not self.description or description in self.DATA_ANOMALY_MATCHES or ( + self.description == "sap05 hot-water" + ) translation = self.WELSH_TEXT.get(self.description) diff --git a/etl/epc_clean/epc_attributes/LightingAttributes.py b/etl/epc_clean/epc_attributes/LightingAttributes.py index 0fe3db16..18475b2d 100644 --- a/etl/epc_clean/epc_attributes/LightingAttributes.py +++ b/etl/epc_clean/epc_attributes/LightingAttributes.py @@ -1,15 +1,18 @@ import re +from BaseUtility import Definitions from etl.epc_clean.epc_attributes.attribute_utils import clean_description from etl.epc_clean.utils import correct_spelling -class LightingAttributes: +class LightingAttributes(Definitions): WELSH_TEXT = { "goleuadau ynni-isel ym mhob un ogçör mannau gosod": "low energy lighting in all fixed outlets", "dim goleuadau ynni-isel": "no low energy lighting", "goleuadau ynni-isel ym mhob un o'r mannau gosod": 'Low energy lighting in all fixed outlets' } + OBSERVED_ERRORS = [] + def __init__(self, description, averages): self.description: str = clean_description(description.lower()) @@ -18,6 +21,9 @@ class LightingAttributes: self.description = correct_spelling(self.description) self.averages = averages + self.nodata = (not description) or (description in self.DATA_ANOMALY_MATCHES) or ( + description in self.OBSERVED_ERRORS) or (description == "SAP05:Lighting") + def welsh_translation_search(self): """ For welsh text describing the percentage of low energy lighting, we match the regular @@ -40,6 +46,9 @@ class LightingAttributes: description = self.description + if self.nodata: + return {"low_energy_proportion": None} + if 'no low energy lighting' in description: return {"low_energy_proportion": 0} diff --git a/etl/epc_clean/epc_attributes/MainheatAttributes.py b/etl/epc_clean/epc_attributes/MainheatAttributes.py index 9f0931a3..56115dca 100644 --- a/etl/epc_clean/epc_attributes/MainheatAttributes.py +++ b/etl/epc_clean/epc_attributes/MainheatAttributes.py @@ -77,7 +77,9 @@ class MainHeatAttributes(Definitions): self.description: str = clean_description(self.description).strip() # Remove special characters - self.nodata = not description or description in self.DATA_ANOMALY_MATCHES + self.nodata = not description or description in self.DATA_ANOMALY_MATCHES or ( + description == "SAP05:Main-Heating" + ) translation = self.WELSH_TEXT.get(self.description) if translation: @@ -97,11 +99,12 @@ class MainHeatAttributes(Definitions): self.process_edge_cases() - if (not description or not any( - rt in self.description for rt in - self.HEAT_SYSTEMS + self.FUEL_TYPES + self.DISTRIBUTION_SYSTEMS + self.OTHERS - ) and not self.is_edge_case): - raise ValueError('Invalid description') + if not self.nodata: + if (not description or not any( + rt in self.description for rt in + self.HEAT_SYSTEMS + self.FUEL_TYPES + self.DISTRIBUTION_SYSTEMS + self.OTHERS + ) and not self.is_edge_case): + raise ValueError('Invalid description') def process_edge_cases(self) -> (dict, bool): """ diff --git a/etl/epc_clean/epc_attributes/MainheatControlAttributes.py b/etl/epc_clean/epc_attributes/MainheatControlAttributes.py index 887bdda7..46fff6d8 100644 --- a/etl/epc_clean/epc_attributes/MainheatControlAttributes.py +++ b/etl/epc_clean/epc_attributes/MainheatControlAttributes.py @@ -117,7 +117,9 @@ class MainheatControlAttributes(Definitions): def __init__(self, description: str): self.description: str = clean_description(description.lower()).strip() - self.nodata = not self.description or description in self.DATA_ANOMALY_MATCHES + self.nodata = not self.description or description in self.DATA_ANOMALY_MATCHES or ( + description == "SAP05:Main-Heating-Controls" + ) translation = self.WELSH_TEXT.get(self.description) if translation: diff --git a/etl/epc_clean/epc_attributes/WindowAttributes.py b/etl/epc_clean/epc_attributes/WindowAttributes.py index 5286fc5a..e9139510 100644 --- a/etl/epc_clean/epc_attributes/WindowAttributes.py +++ b/etl/epc_clean/epc_attributes/WindowAttributes.py @@ -38,7 +38,7 @@ class WindowAttributes(Definitions): # In the case of an empty description, we want to return a dictionary with all values set to False # and indicate there was no data - self.nodata = not description or description in self.DATA_ANOMALY_MATCHES + self.nodata = not description or description in self.DATA_ANOMALY_MATCHES or description == "SAP05:Windows" translation = self.WELSH_TEXT.get(self.description) if translation: From 4e85d1380edcee2b7a54dced036790d9c269cb03 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Thu, 27 Jun 2024 14:17:39 +0100 Subject: [PATCH 2/9] cleaning columns for stonewater clustering --- etl/customers/stonewater/shdf_3_clustering.py | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/etl/customers/stonewater/shdf_3_clustering.py b/etl/customers/stonewater/shdf_3_clustering.py index b8e71ae7..8b878f26 100644 --- a/etl/customers/stonewater/shdf_3_clustering.py +++ b/etl/customers/stonewater/shdf_3_clustering.py @@ -1668,6 +1668,7 @@ def compile_data_final(): 'lighting-description': LightingAttributes } for variable_to_clean in cleaned.keys(): + unique_descriptions = property_attributes[variable_to_clean].unique() clean_df = pd.DataFrame(cleaned[variable_to_clean]) # Check if we have any @@ -1686,6 +1687,67 @@ def compile_data_final(): } descriptions_to_append.append(to_append) + descriptions_to_append = pd.DataFrame(descriptions_to_append) + clean_df = pd.concat([clean_df, descriptions_to_append]) + + starting_size = len(property_attributes) + property_attributes = property_attributes.merge( + clean_df, how="left", left_on=variable_to_clean, right_on="original_description" + ) + if starting_size != property_attributes.shape[0]: + raise Exception("something went wrong") + property_attributes = property_attributes.drop(columns=["original_description", "clean_description"]) + # Fill missings + for k in clean_df.columns: + if k in property_attributes.columns: + property_attributes[k] = property_attributes[k].fillna("missing") + + # We group some variables such as thermal transmittance for walls, roof, floors + ranges = { + "< 0.1": (0, 0.1), + "0.1 - 0.3": (0.1, 0.3), + "0.3 - 0.5": (0.3, 0.5), + "0.5 - 0.7": (0.5, 0.7), + "0.9 - 1": (0.9, 1), + "1 - 1.5": (1, 1.5), + "1.5 - 2": (1.5, 2), + "2+": (2, 2.5) + } + + # Generate the lookup table + thermal_transmittance_lookup_table = [] + for i in range(1, 251): + value = i / 100 + for label, (low, high) in ranges.items(): + if low < value <= high: + thermal_transmittance_lookup_table.append({"from": value, "to": label}) + break + + # Convert to DataFrame for display + thermal_transmittance_lookup_table = pd.DataFrame(thermal_transmittance_lookup_table) + thermal_transmittance_lookup_table["from"] = thermal_transmittance_lookup_table["from"].astype(str) + + thermal_transmittance_cols = [ + c for c in property_attributes.columns if "thermal_transmittance" in c and "unit" not in c + ] + for i, col in enumerate(thermal_transmittance_cols): + # Perform the mapping + to_col = f"to_{i}" + property_attributes[col] = property_attributes[col].astype(str) + property_attributes = property_attributes.merge( + thermal_transmittance_lookup_table.rename(columns={"to": to_col}), + how="left", + left_on=col, + right_on="from", + suffixes=("", f"_{i}") + ) + property_attributes = property_attributes.drop(columns=["from", col]) + property_attributes[to_col] = property_attributes[to_col].fillna("unknown") + + # Drop the description columns that are the keys in cleaned + property_attributes = property_attributes.drop(columns=list(cleaned.keys())) + # Perform the mapping + # CLUSTERING!! from sklearn.cluster import KMeans From 07ddf8383b1b290c4855b6e12f6dc60b74be9456 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Thu, 27 Jun 2024 17:58:50 +0100 Subject: [PATCH 3/9] extended the capturing of u-values from thermal transmittance descriptions --- etl/customers/stonewater/shdf_3_clustering.py | 1 + etl/epc_clean/epc_attributes/attribute_utils.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/etl/customers/stonewater/shdf_3_clustering.py b/etl/customers/stonewater/shdf_3_clustering.py index 8b878f26..caaf84a6 100644 --- a/etl/customers/stonewater/shdf_3_clustering.py +++ b/etl/customers/stonewater/shdf_3_clustering.py @@ -1667,6 +1667,7 @@ def compile_data_final(): 'windows-description': WindowAttributes, 'lighting-description': LightingAttributes } + for variable_to_clean in cleaned.keys(): unique_descriptions = property_attributes[variable_to_clean].unique() diff --git a/etl/epc_clean/epc_attributes/attribute_utils.py b/etl/epc_clean/epc_attributes/attribute_utils.py index 60f4653e..a5326207 100644 --- a/etl/epc_clean/epc_attributes/attribute_utils.py +++ b/etl/epc_clean/epc_attributes/attribute_utils.py @@ -2,8 +2,8 @@ import re import string from typing import Tuple, Union, Dict, List -THERMAL_TRANSMITTANCE_STR = r"average thermal transmittance (-?\d+(\.\d+)?)\s(w/m\S+k)" -THERMAL_TRANSMITTANCE_REGEX = re.compile(THERMAL_TRANSMITTANCE_STR) +THERMAL_TRANSMITTANCE_STR = r"average thermal transmittance\s*[=:-]?\s*(-?\d+(\.\d+)?)\s*[wW]/m\S*[kK]" +THERMAL_TRANSMITTANCE_REGEX = re.compile(THERMAL_TRANSMITTANCE_STR, re.IGNORECASE) DOUBLE_SPACE_PATTERN = re.compile(r"\s+") From 6f32aa672bdeaa043a9cf3f81c5c35801bdd741c Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Thu, 27 Jun 2024 18:03:57 +0100 Subject: [PATCH 4/9] Added corrections to walls cleaning class --- etl/epc_clean/epc_attributes/WallAttributes.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/etl/epc_clean/epc_attributes/WallAttributes.py b/etl/epc_clean/epc_attributes/WallAttributes.py index 09eac215..49252552 100644 --- a/etl/epc_clean/epc_attributes/WallAttributes.py +++ b/etl/epc_clean/epc_attributes/WallAttributes.py @@ -75,12 +75,19 @@ class WallAttributes(Definitions): 'insulation_thickness', 'external_insulation', 'internal_insulation' ] + CORRECTIONS = { + "Granite or whin, as built, no insulation (assumed)": "Granite or whinstone, as built, no insulation (assumed)", + } + def __init__(self, description: str): """ :param description: Description of the walls. """ self.description: str = description + if self.description in self.CORRECTIONS: + self.description = self.CORRECTIONS[self.description] + self.welsh_translation_search() self.nodata = not description or description in self.DATA_ANOMALY_MATCHES From 4456ab29eeac9a3407408d84b39ccc328dd8983a Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Fri, 28 Jun 2024 11:03:22 +0100 Subject: [PATCH 5/9] added the grouped clustering --- .../stonewater/outputs 27th June 2024.py | 31 ++- etl/customers/stonewater/shdf_3_clustering.py | 206 ++++++++++++------ 2 files changed, 161 insertions(+), 76 deletions(-) diff --git a/etl/customers/stonewater/outputs 27th June 2024.py b/etl/customers/stonewater/outputs 27th June 2024.py index ebb6fc5b..d8bf43be 100644 --- a/etl/customers/stonewater/outputs 27th June 2024.py +++ b/etl/customers/stonewater/outputs 27th June 2024.py @@ -11,7 +11,7 @@ In this script, we do the following things: import pandas as pd from utils.s3 import read_pickle_from_s3 -archetyped_asset_list = pd.read_csv("Stonewater asset list with archetypes.csv") +archetyped_asset_list = pd.read_csv("Stonewater asset list with archetypes V2.csv") archetyped_asset_list = archetyped_asset_list[ [ "internal_id", "customer_asset_id", "udprn", "uprn", "cluster", "archetype_representative", "rank" @@ -34,15 +34,22 @@ archetyped_asset_list = archetyped_asset_list.merge( how="inner" ) +# Look at number of combinations +# - If we look at the number of combinations of property type & built form, we have 25 unique combinations +# - If we look at the number of combinations of property type, built form, and walls description, this jumps +# massively to 237 unique combinations +# - Adding roof description to the mix, we have 857 unique combinations +# - Adding floor description, we have 1278 unique combinations +# This doesn't even begin to consider the other variables that we have in the dataset, such as the property dimensions, +# location, and other factors. +# Ideally, we would perfectly separate these variables but this is not possible, given the constraint of needing ~450 +# archetypes. We will need to make some compromises here. This is where a clustering algorithm can help us. +# We don't end up with perfect separation but we can get a good enough separation to make the archetypes useful, and can +# base the archetypes on a number of energy performance metrics, as well as location and other factors. +# archetyped_asset_list[ +# ["property-type", "built-form", "walls-description", "roof-description", +# "floor-description"]].drop_duplicates().shape + property_type_archetypes = archetyped_asset_list[ - ["cluster", "rank", "property-type", "built-form", "walls-description"]] - -# Key variables for separation: -# - property-type -# - built-form -# - walls-description -# - roof-description - -clustering_features[["property-type", "built-form", "walls-description"]].drop_duplicates().shape - -clustering_features["walls-description"].value_counts() + ["cluster", "rank", "property-type", "built-form", "walls-description"] +] diff --git a/etl/customers/stonewater/shdf_3_clustering.py b/etl/customers/stonewater/shdf_3_clustering.py index caaf84a6..fa6551b7 100644 --- a/etl/customers/stonewater/shdf_3_clustering.py +++ b/etl/customers/stonewater/shdf_3_clustering.py @@ -14,6 +14,11 @@ import pandas as pd import time from utils.s3 import save_data_to_s3, read_excel_from_s3, read_from_s3, read_dataframe_from_s3_parquet, \ save_dataframe_to_s3_parquet, save_pickle_to_s3 +from sklearn.cluster import KMeans +from sklearn.preprocessing import StandardScaler, OneHotEncoder +from sklearn.compose import ColumnTransformer +from sklearn.pipeline import Pipeline +from scipy.spatial.distance import cdist load_dotenv(dotenv_path="backend/.env") EPC_AUTH_TOKEN = os.getenv("EPC_AUTH_TOKEN") @@ -1090,6 +1095,26 @@ def concatenate_row(row): return ', '.join(row.dropna().replace('', None).dropna().astype(str)) +def adjust_clusters(cluster_allocation, total_clusters): + current_total = sum(cluster_allocation.values()) + adjustment = total_clusters - current_total + if adjustment > 0: + # Increase clusters, start from the largest group + for group in sorted(cluster_allocation, key=lambda x: -cluster_allocation[x]): + cluster_allocation[group] += 1 + adjustment -= 1 + if adjustment == 0: + break + elif adjustment < 0: + # Decrease clusters, start from the largest group + for group in sorted(cluster_allocation, key=lambda x: -cluster_allocation[x]): + cluster_allocation[group] -= 1 + adjustment += 1 + if adjustment == 0: + break + return cluster_allocation + + def compile_data_final(): # Updated version: @@ -1667,7 +1692,7 @@ def compile_data_final(): 'windows-description': WindowAttributes, 'lighting-description': LightingAttributes } - + for variable_to_clean in cleaned.keys(): unique_descriptions = property_attributes[variable_to_clean].unique() @@ -1691,28 +1716,45 @@ def compile_data_final(): descriptions_to_append = pd.DataFrame(descriptions_to_append) clean_df = pd.concat([clean_df, descriptions_to_append]) - starting_size = len(property_attributes) - property_attributes = property_attributes.merge( - clean_df, how="left", left_on=variable_to_clean, right_on="original_description" - ) - if starting_size != property_attributes.shape[0]: - raise Exception("something went wrong") - property_attributes = property_attributes.drop(columns=["original_description", "clean_description"]) - # Fill missings - for k in clean_df.columns: - if k in property_attributes.columns: - property_attributes[k] = property_attributes[k].fillna("missing") + clean_df = clean_df.rename( + columns={ + "thermal_transmittance": f"{variable_to_clean}_thermal_transmittance", + "is_assumed": f"{variable_to_clean}_is_assumed", + } + ) + + if 'thermal_transmittance_unit' in clean_df.columns: + clean_df = clean_df.drop(columns=['thermal_transmittance_unit']) + + starting_size = len(property_attributes) + property_attributes = property_attributes.merge( + clean_df, how="left", left_on=variable_to_clean, right_on="original_description" + ) + if starting_size != property_attributes.shape[0]: + raise Exception("something went wrong") + property_attributes = property_attributes.drop(columns=["original_description", "clean_description"]) + # Fill missings + for k in clean_df.columns: + if k in property_attributes.columns: + property_attributes[k] = property_attributes[k].fillna("missing") # We group some variables such as thermal transmittance for walls, roof, floors + # ranges = { + # "< 0.1": (0, 0.1), + # "0.1 - 0.3": (0.1, 0.3), + # "0.3 - 0.5": (0.3, 0.5), + # "0.5 - 0.7": (0.5, 0.7), + # "0.9 - 1": (0.9, 1), + # "1 - 1.5": (1, 1.5), + # "1.5 - 2": (1.5, 2), + # "2+": (2, 2.5) + # } + ranges = { "< 0.1": (0, 0.1), "0.1 - 0.3": (0.1, 0.3), "0.3 - 0.5": (0.3, 0.5), - "0.5 - 0.7": (0.5, 0.7), - "0.9 - 1": (0.9, 1), - "1 - 1.5": (1, 1.5), - "1.5 - 2": (1.5, 2), - "2+": (2, 2.5) + "0.5+": (0.5, 2.5), } # Generate the lookup table @@ -1733,7 +1775,7 @@ def compile_data_final(): ] for i, col in enumerate(thermal_transmittance_cols): # Perform the mapping - to_col = f"to_{i}" + to_col = f"to_{col}" property_attributes[col] = property_attributes[col].astype(str) property_attributes = property_attributes.merge( thermal_transmittance_lookup_table.rename(columns={"to": to_col}), @@ -1750,72 +1792,108 @@ def compile_data_final(): # Perform the mapping # CLUSTERING!! - - from sklearn.cluster import KMeans - from sklearn.preprocessing import StandardScaler, OneHotEncoder - from sklearn.compose import ColumnTransformer - from sklearn.pipeline import Pipeline - from scipy.spatial.distance import cdist - id_column = 'internal_id' - property_attributes.set_index(id_column, inplace=True) + grouping_columns = [ + 'is_cavity_wall', 'is_solid_brick', 'built-form', 'property-type' + ] # Define the preprocessing for numerical and categorical features numerical_features = property_attributes.select_dtypes(include=['int64', 'float64']).columns.tolist() categorical_features = property_attributes.select_dtypes(include=['object', 'category']).columns.tolist() + categorical_features = [c for c in categorical_features if c not in ["internal_id", grouping_columns]] for col in categorical_features: property_attributes[col] = property_attributes[col].astype(str) - preprocessor = ColumnTransformer( - transformers=[ - ('num', StandardScaler(), numerical_features), - ('cat', OneHotEncoder(), categorical_features) + id_column = 'internal_id' + n_clusters = 450 + random_state = 0 + + training_data_grouped = property_attributes.groupby(grouping_columns) + group_sizes = {name: len(group) for name, group in training_data_grouped} + total_size = sum(group_sizes.values()) + cluster_allocation = { + name: max(1, int(round(n_clusters * (size / total_size)))) for name, size in group_sizes.items() + } + + # Adjust cluster allocation to ensure total clusters sum to 450 + cluster_allocation = adjust_clusters(cluster_allocation, n_clusters) + + # TODO: This code throws many warnings because of the highly fragmented dataframe. We should re-factor this to + # collect the results of the clustering and then perform the transformations afterwards + + final_clusters = [] + for group_variables, group_data in tqdm(training_data_grouped, total=len(training_data_grouped)): + + group_n_clusters = cluster_allocation[group_variables] + group_data.set_index(id_column, inplace=True) + + preprocessor = ColumnTransformer( + transformers=[ + ('num', StandardScaler(), numerical_features), + ('cat', OneHotEncoder(), categorical_features) + ] + ) + + pipeline = Pipeline(steps=[('preprocessor', preprocessor), + ('kmeans', KMeans(n_clusters=group_n_clusters, random_state=random_state))]) + + # Fit the pipeline to the data + pipeline.fit(group_data) + + # Transform the data using the fitted pipeline + processed_data = pipeline.named_steps['preprocessor'].transform(group_data) + + # Get cluster labels + group_data['cluster'] = pipeline.named_steps['kmeans'].labels_ + + # Get centroids (already in the same transformed space) + centroids = pipeline.named_steps['kmeans'].cluster_centers_ + + # if the data isn't an array, make it one + if not isinstance(processed_data, np.ndarray): + processed_data = processed_data.toarray() + + # Calculate distances from each point to the centroid of its cluster + distances_to_centroids = [ + cdist(processed_data[i].reshape(1, -1), centroids[label].reshape(1, -1)).flatten()[0] + for i, label in enumerate(group_data['cluster']) ] - ) - pipeline = Pipeline(steps=[('preprocessor', preprocessor), - ('kmeans', KMeans(n_clusters=450, random_state=0))]) + group_data['distance_to_centroid'] = distances_to_centroids - # Fit the pipeline to the data - pipeline.fit(property_attributes) + # for cluster_id in group_data['cluster'].unique(): + # cluster_data = group_data[group_data['cluster'] == cluster_id] + # min_distance = cluster_data['distance_to_centroid'].min() + # print(f"Cluster {cluster_id} minimum distance to centroid: {min_distance}") + # if min_distance != 0: + # print(f"No point with zero distance found in cluster {cluster_id}") - # Transform the data using the fitted pipeline - processed_data = pipeline.named_steps['preprocessor'].transform(property_attributes) + # Ranking rows by distance within each cluster + group_data['rank'] = group_data.groupby('cluster')['distance_to_centroid'].rank(method='first') - # Get cluster labels - property_attributes['cluster'] = pipeline.named_steps['kmeans'].labels_ + # Sorting to verify + group_data.sort_values(by=['cluster', 'rank'], inplace=True) + group_data.reset_index(inplace=True) - # Get centroids (already in the same transformed space) - centroids = pipeline.named_steps['kmeans'].cluster_centers_ + to_append = group_data[["internal_id", "cluster", "rank"]].copy() + to_append["cluster"] = to_append["cluster"].astype(str) + str(group_variables) + final_clusters.append(to_append) - processed_data = processed_data.toarray() + final_clusters = pd.concat(final_clusters) + # remap the clusters from the current names to 1 -> n_clusters - # Calculate distances from each point to the centroid of its cluster - distances_to_centroids = [ - cdist(processed_data[i].reshape(1, -1), centroids[label].reshape(1, -1)).flatten()[0] - for i, label in enumerate(property_attributes['cluster']) - ] - - property_attributes['distance_to_centroid'] = distances_to_centroids - - for cluster_id in property_attributes['cluster'].unique(): - cluster_data = property_attributes[property_attributes['cluster'] == cluster_id] - min_distance = cluster_data['distance_to_centroid'].min() - print(f"Cluster {cluster_id} minimum distance to centroid: {min_distance}") - if min_distance != 0: - print(f"No point with zero distance found in cluster {cluster_id}") - - # Ranking rows by distance within each cluster - property_attributes['rank'] = property_attributes.groupby('cluster')['distance_to_centroid'].rank( - method='first') - - # Sorting to verify - property_attributes.sort_values(by=['cluster', 'rank'], inplace=True) + cluster_mapping = {cluster: i for i, cluster in enumerate(final_clusters["cluster"].unique())} + final_clusters["cluster"] = final_clusters["cluster"].map(cluster_mapping) + final_clusters["cluster"] = final_clusters["cluster"].astype(str) ################################################ # Prepare outputs!!!! ################################################ + property_attributes.reset_index(inplace=True) + property_attributes = property_attributes.merge( + final_clusters, how="left", on="internal_id" + ) property_attributes["archetype_representative"] = property_attributes["rank"] == 1 asset_list_with_archetypes = asset_list.merge( @@ -1834,7 +1912,7 @@ def compile_data_final(): asset_list_with_archetypes["archetype_representative"] = asset_list_with_archetypes[ "archetype_representative"].fillna(False) - asset_list_with_archetypes.to_csv("Stonewater asset list with archetypes.csv", index=False) + asset_list_with_archetypes.to_csv("Stonewater asset list with archetypes V2.csv", index=False) stonewater_uprn_lookup = asset_list_with_archetypes[ ["external_address_id", "udprn", "uprn", "match_type", "standardised_address", "standardised_postcode"] From 37780687eb4db19738091dd22e6b17e0e15a5c5a Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Fri, 28 Jun 2024 20:34:55 +0100 Subject: [PATCH 6/9] Basic setup of stonewater map app --- .../map_app/Stonewater Mapping Data.json | 1 + etl/customers/stonewater/map_app/callbacks.py | 0 etl/customers/stonewater/map_app/config.py | 8 ++ etl/customers/stonewater/map_app/map_page.py | 94 +++++++++++++++++++ .../stonewater/map_app/requirements.txt | 12 +++ etl/customers/stonewater/map_app/server.py | 45 +++++++++ etl/customers/stonewater/map_app/wsgi.py | 8 ++ 7 files changed, 168 insertions(+) create mode 100644 etl/customers/stonewater/map_app/Stonewater Mapping Data.json create mode 100644 etl/customers/stonewater/map_app/callbacks.py create mode 100644 etl/customers/stonewater/map_app/config.py create mode 100644 etl/customers/stonewater/map_app/map_page.py create mode 100644 etl/customers/stonewater/map_app/requirements.txt create mode 100644 etl/customers/stonewater/map_app/server.py create mode 100644 etl/customers/stonewater/map_app/wsgi.py diff --git a/etl/customers/stonewater/map_app/Stonewater Mapping Data.json b/etl/customers/stonewater/map_app/Stonewater Mapping Data.json new file mode 100644 index 00000000..0d2978c4 --- /dev/null +++ b/etl/customers/stonewater/map_app/Stonewater Mapping Data.json @@ -0,0 +1 @@ +[{"uprn": 100050346525.0, "standardised_address": "32 VICTORIA ROAD, CROSS HILLS, KEIGHLEY", "standardised_postcode": "BD20 8SY", "LONGITUDE": -1.995112, "LATITUDE": 53.8997423}, {"uprn": 100050344995.0, "standardised_address": "20, Lang Kirk Close, Farnhill", "standardised_postcode": "BD20 9AR", "LONGITUDE": -1.9846986, "LATITUDE": 53.9106527}, {"uprn": 100050344996.0, "standardised_address": "22 LANG KIRK CLOSE, FARNHILL, KEIGHLEY", "standardised_postcode": "BD20 9AR", "LONGITUDE": -1.984729, "LATITUDE": 53.9107785}, {"uprn": 100050354230.0, "standardised_address": "34, Neville Road, Gargrave", "standardised_postcode": "BD23 3RE", "LONGITUDE": -2.1094417, "LATITUDE": 53.9833594}, {"uprn": 100050354235.0, "standardised_address": "39, NEVILLE ROAD, GARGRAVE, SKIPTON, BD23 3RE", "standardised_postcode": "BD23 3RE", "LONGITUDE": -2.1090613, "LATITUDE": 53.9836833}, {"uprn": 100071236000.0, "standardised_address": "3, Sorrell Place", "standardised_postcode": "CV10 7AY", "LONGITUDE": -1.4620998, "LATITUDE": 52.5044784}, {"uprn": 100030002639.0, "standardised_address": "6 St. Martins Court, Church Street, Amber Valley, DE55 7AH", "standardised_postcode": "DE55 7AH", "LONGITUDE": -1.3916142, "LATITUDE": 53.0973039}, {"uprn": 83008532.0, "standardised_address": "66, Belmont Terrace, Linthwaite", "standardised_postcode": "HD7 5SF", "LONGITUDE": -1.8505805, "LATITUDE": 53.6249856}, {"uprn": 83146613.0, "standardised_address": "35, Thorpes Crescent, Skelmanthorpe", "standardised_postcode": "HD8 9DH", "LONGITUDE": -1.6465264, "LATITUDE": 53.5871495}, {"uprn": 10010168062.0, "standardised_address": "2, HARVEST COURT, HALIFAX, HX1 5DU", "standardised_postcode": "HX1 5DU", "LONGITUDE": -1.869757, "LATITUDE": 53.722287}, {"uprn": 10006738671.0, "standardised_address": "6, DEAN COURT, HALIFAX, HX3 0UX", "standardised_postcode": "HX3 0UX", "LONGITUDE": -1.8767188, "LATITUDE": 53.6993657}, {"uprn": 100051314061.0, "standardised_address": "21, CHAPEL CLOSE, HOLYWELL GREEN, HALIFAX, HX4 9BF", "standardised_postcode": "HX4 9BF", "LONGITUDE": -1.8679148, "LATITUDE": 53.6762656}, {"uprn": 100051314049.0, "standardised_address": "2, Chapel Close, Holywell Green", "standardised_postcode": "HX4 9BF", "LONGITUDE": -1.8673862, "LATITUDE": 53.6758786}, {"uprn": 200002737104.0, "standardised_address": "35, Bradley View, Holywell Green", "standardised_postcode": "HX4 9DN", "LONGITUDE": -1.8707939, "LATITUDE": 53.675307}, {"uprn": 200001826125.0, "standardised_address": "56, BRADLEY VIEW, HOLYWELL GREEN, HALIFAX, HX4 9DN", "standardised_postcode": "HX4 9DN", "LONGITUDE": -1.871579, "LATITUDE": 53.675982}, {"uprn": 100052043238.0, "standardised_address": "50 Marton Heights, Hollins Lane", "standardised_postcode": "HX6 2RZ", "LONGITUDE": -1.9191337, "LATITUDE": 53.7123626}, {"uprn": 100052043248.0, "standardised_address": "6 Marton Heights, Hollins Lane", "standardised_postcode": "HX6 2RZ", "LONGITUDE": -1.9174519, "LATITUDE": 53.7124064}, {"uprn": 100052043251.0, "standardised_address": "8 Marton Heights, Hollins Lane", "standardised_postcode": "HX6 2RZ", "LONGITUDE": -1.9177397, "LATITUDE": 53.7124695}, {"uprn": 100052043217.0, "standardised_address": "31 Marton Heights, Hollins Lane", "standardised_postcode": "HX6 2RZ", "LONGITUDE": -1.9200123, "LATITUDE": 53.712489}, {"uprn": 10004000472.0, "standardised_address": "26, BENTLEY CLOSE, LOUGHBOROUGH, LE11 1SY", "standardised_postcode": "LE11 1SY", "LONGITUDE": -1.1965799, "LATITUDE": 52.7702045}, {"uprn": 10004000480.0, "standardised_address": "37, BENTLEY CLOSE, LOUGHBOROUGH, LE11 1SY", "standardised_postcode": "LE11 1SY", "LONGITUDE": -1.1970509, "LATITUDE": 52.7698738}, {"uprn": 200000844913.0, "standardised_address": "17, SADDLERS CLOSE, LOUGHBOROUGH, LE11 5HD", "standardised_postcode": "LE11 5HD", "LONGITUDE": -1.2189761, "LATITUDE": 52.7809828}, {"uprn": 100030549024.0, "standardised_address": "7, YEW TREE CRESCENT, MELTON MOWBRAY, LE13 1LN", "standardised_postcode": "LE13 1LN", "LONGITUDE": -0.8849264, "LATITUDE": 52.7756315}, {"uprn": 100030549068.0, "standardised_address": "51, YEW TREE CRESCENT, MELTON MOWBRAY, LE13 1LN", "standardised_postcode": "LE13 1LN", "LONGITUDE": -0.8856134, "LATITUDE": 52.7748558}, {"uprn": 2465087590.0, "standardised_address": "69, LOUGHBOROUGH ROAD, LEICESTER, LE4 5LL", "standardised_postcode": "LE4 5LL", "LONGITUDE": -1.1223645, "LATITUDE": 52.6532303}, {"uprn": 10033732069.0, "standardised_address": "118, HIGH STREET, EARL SHILTON, LEICESTER, LE9 7DG", "standardised_postcode": "LE9 7DG", "LONGITUDE": -1.3115505, "LATITUDE": 52.5764223}, {"uprn": 100032081116.0, "standardised_address": "22 Peter Dyer Court, Seacroft Road", "standardised_postcode": "LN12 2DT", "LONGITUDE": 0.2614919, "LATITUDE": 53.3389736}, {"uprn": 72266102.0, "standardised_address": "24, Petersfield Avenue", "standardised_postcode": "LS10 3PF", "LONGITUDE": -1.5282803, "LATITUDE": 53.7589478}, {"uprn": 72266110.0, "standardised_address": "35, PETERSFIELD AVENUE, LEEDS, LS10 3PF", "standardised_postcode": "LS10 3PF", "LONGITUDE": -1.5279768, "LATITUDE": 53.7589556}, {"uprn": 72304733.0, "standardised_address": "2, Warwick Court, Horsforth", "standardised_postcode": "LS18 4TB", "LONGITUDE": -1.6366988, "LATITUDE": 53.8328644}, {"uprn": 100080154916.0, "standardised_address": "16, IVY ROAD, LUTON, LU1 1DN", "standardised_postcode": "LU1 1DN", "LONGITUDE": -0.4296342, "LATITUDE": 51.8852366}, {"uprn": 100080163981.0, "standardised_address": "51, Newcombe Road", "standardised_postcode": "LU1 1LH", "LONGITUDE": -0.4315661, "LATITUDE": 51.880245}, {"uprn": 100080171727.0, "standardised_address": "85, Runley Road", "standardised_postcode": "LU1 1TX", "LONGITUDE": -0.4477962, "LATITUDE": 51.881881}, {"uprn": 100080141071.0, "standardised_address": "22, DELPHINE CLOSE, LUTON, LU1 5RE", "standardised_postcode": "LU1 5RE", "LONGITUDE": -0.4396275, "LATITUDE": 51.8756855}, {"uprn": 100080152967.0, "standardised_address": "187, HITCHIN ROAD, LUTON, LU2 0EP", "standardised_postcode": "LU2 0EP", "LONGITUDE": -0.4046343, "LATITUDE": 51.8875165}, {"uprn": 100080139727.0, "standardised_address": "89, CROMWELL ROAD, LUTON, LU3 1DP", "standardised_postcode": "LU3 1DP", "LONGITUDE": -0.4244653, "LATITUDE": 51.8859587}, {"uprn": 100080177405.0, "standardised_address": "81, SWAN MEAD, LUTON, LU4 0YP", "standardised_postcode": "LU4 0YP", "LONGITUDE": -0.4837081, "LATITUDE": 51.9040007}, {"uprn": 100080173090.0, "standardised_address": "28, Selbourne Road", "standardised_postcode": "LU4 8LP", "LONGITUDE": -0.4331933, "LATITUDE": 51.8894382}, {"uprn": 100080100758.0, "standardised_address": "15 MILL ROAD, HOUGHTON REGIS", "standardised_postcode": "LU5 5BD", "LONGITUDE": -0.5277522, "LATITUDE": 51.9026985}, {"uprn": 10001025868.0, "standardised_address": "43 KINGSLAND CLOSE, HOUGHTON REGIS", "standardised_postcode": "LU5 5UT", "LONGITUDE": -0.5022206, "LATITUDE": 51.9055144}, {"uprn": 10001025856.0, "standardised_address": "37 Kingsland Close, Houghton Regis", "standardised_postcode": "LU5 5UT", "LONGITUDE": -0.5027665, "LATITUDE": 51.9057101}, {"uprn": 10001023485.0, "standardised_address": "16, WILLOUGHBY CLOSE, DUNSTABLE, LU6 3TF", "standardised_postcode": "LU6 3TF", "LONGITUDE": -0.5131682, "LATITUDE": 51.8799288}, {"uprn": 100080120241.0, "standardised_address": "58, VIMY ROAD, LEIGHTON BUZZARD, LU7 1FQ", "standardised_postcode": "LU7 1FQ", "LONGITUDE": -0.6684013, "LATITUDE": 51.9218008}, {"uprn": 10001024491.0, "standardised_address": "11 Stephenson Close", "standardised_postcode": "LU7 2NE", "LONGITUDE": -0.6776464, "LATITUDE": 51.9125089}, {"uprn": 25002516.0, "standardised_address": "19, COTTESLOE COURT, STONY STRATFORD, MILTON KEYNES, MK11 1NL", "standardised_postcode": "MK11 1NL", "LONGITUDE": -0.8374092, "LATITUDE": 52.0540273}, {"uprn": 100080016298.0, "standardised_address": "11, Grafton Road", "standardised_postcode": "MK40 1DH", "LONGITUDE": -0.4762835, "LATITUDE": 52.1356448}, {"uprn": 100081334233.0, "standardised_address": "24, CHAUCER ROAD, BEDFORD, MK40 2AJ", "standardised_postcode": "MK40 2AJ", "LONGITUDE": -0.4820862, "LATITUDE": 52.1402846}, {"uprn": 10002971391.0, "standardised_address": "ROOM 5, 13, FOSTER HILL ROAD, BEDFORD, MK40 2ES", "standardised_postcode": "MK40 2ES", "LONGITUDE": -0.4680169, "LATITUDE": 52.141748}, {"uprn": 100080995116.0, "standardised_address": "27C, KIMBOLTON ROAD, BEDFORD, MK40 2NY", "standardised_postcode": "MK40 2NY", "LONGITUDE": -0.4587805, "LATITUDE": 52.1433879}, {"uprn": 10033179536.0, "standardised_address": "95, CROWE ROAD, BEDFORD, MK40 4FY", "standardised_postcode": "MK40 4FY", "LONGITUDE": -0.4812176, "LATITUDE": 52.1366436}, {"uprn": 10033179410.0, "standardised_address": "95, Henley Road", "standardised_postcode": "MK40 4FZ", "LONGITUDE": -0.4803204, "LATITUDE": 52.1355261}, {"uprn": 10033179403.0, "standardised_address": "81, HENLEY ROAD, BEDFORD, MK40 4FZ", "standardised_postcode": "MK40 4FZ", "LONGITUDE": -0.480418, "LATITUDE": 52.1356622}, {"uprn": 100080010125.0, "standardised_address": "19, CROMWELL ROAD, BEDFORD, MK40 4LR", "standardised_postcode": "MK40 4LR", "LONGITUDE": -0.482858, "LATITUDE": 52.1335177}, {"uprn": 100080025762.0, "standardised_address": "95, Mallard Hill", "standardised_postcode": "MK41 7QU", "LONGITUDE": -0.4682452, "LATITUDE": 52.1531728}, {"uprn": 10002966181.0, "standardised_address": "22, LEWES GARDENS, BEDFORD, MK41 8NW", "standardised_postcode": "MK41 8NW", "LONGITUDE": -0.4331692, "LATITUDE": 52.1566033}, {"uprn": 10002966183.0, "standardised_address": "26, Lewes Gardens", "standardised_postcode": "MK41 8NW", "LONGITUDE": -0.4333292, "LATITUDE": 52.1566506}, {"uprn": 10002966213.0, "standardised_address": "58, EXETER WALK, BEDFORD, MK41 8QN", "standardised_postcode": "MK41 8QN", "LONGITUDE": -0.4327389, "LATITUDE": 52.1573743}, {"uprn": 100080011160.0, "standardised_address": "12 Devizes Avenue, Bedford, MK41 8QT", "standardised_postcode": "MK41 8QT", "LONGITUDE": -0.4345664, "LATITUDE": 52.1569295}, {"uprn": 100080993516.0, "standardised_address": "11 RAGLAN COURT, DEVIZES AVENUE, BEDFORD, MK41 8QT", "standardised_postcode": "MK41 8QT", "LONGITUDE": -0.4341117, "LATITUDE": 52.1565548}, {"uprn": 10002966248.0, "standardised_address": "112, EXETER WALK, BEDFORD, MK41 8QW", "standardised_postcode": "MK41 8QW", "LONGITUDE": -0.4350041, "LATITUDE": 52.1573586}, {"uprn": 100080023488.0, "standardised_address": "1, Kirkman Close", "standardised_postcode": "MK42 0HY", "LONGITUDE": -0.4560605, "LATITUDE": 52.1251283}, {"uprn": 100080994421.0, "standardised_address": "16, BARTRAM COURT, 123, HIGH STREET, KEMPSTON, BEDFORD, MK42 7BP", "standardised_postcode": "MK42 7BP", "LONGITUDE": -0.505862, "LATITUDE": 52.1144562}, {"uprn": 100080020671.0, "standardised_address": "227, Hillgrounds Road, Kempston", "standardised_postcode": "MK42 8HW", "LONGITUDE": -0.4966793, "LATITUDE": 52.1269081}, {"uprn": 100080020676.0, "standardised_address": "237, Hillgrounds Road, Kempston", "standardised_postcode": "MK42 8HW", "LONGITUDE": -0.4962226, "LATITUDE": 52.1270414}, {"uprn": 100080037472.0, "standardised_address": "98, St. Johns Avenue, Kempston", "standardised_postcode": "MK42 8JR", "LONGITUDE": -0.4971843, "LATITUDE": 52.1133086}, {"uprn": 100081210880.0, "standardised_address": "64, AMPTHILL ROAD, BEDFORD, MK42 9HP", "standardised_postcode": "MK42 9HP", "LONGITUDE": -0.4711524, "LATITUDE": 52.1257766}, {"uprn": 10002970239.0, "standardised_address": "17, Davis Close", "standardised_postcode": "MK42 9LG", "LONGITUDE": -0.467977, "LATITUDE": 52.1232364}, {"uprn": 100080049331.0, "standardised_address": "26, Burridge Close, Marston Moretaine", "standardised_postcode": "MK43 0SG", "LONGITUDE": -0.5481185, "LATITUDE": 52.0673686}, {"uprn": 100080008976.0, "standardised_address": "5, Colley Close, Colmworth", "standardised_postcode": "MK44 2HE", "LONGITUDE": -0.3845779, "LATITUDE": 52.2116856}, {"uprn": 100080047408.0, "standardised_address": "86, Ailesbury Road, Ampthill", "standardised_postcode": "MK45 2XD", "LONGITUDE": -0.4882962, "LATITUDE": 52.0291749}, {"uprn": 10014614558.0, "standardised_address": "1 Pembroke Close, Houghton Conquest", "standardised_postcode": "MK45 3FH", "LONGITUDE": -0.4761796, "LATITUDE": 52.0615268}, {"uprn": 100080051765.0, "standardised_address": "2 FISHERS CLOSE, UPPER GRAVENHURST", "standardised_postcode": "MK45 4LJ", "LONGITUDE": -0.3766392, "LATITUDE": 52.0106445}, {"uprn": 25068208.0, "standardised_address": "14, Colne, Tinkers Bridge", "standardised_postcode": "MK6 3DJ", "LONGITUDE": -0.7241388, "LATITUDE": 52.0203873}, {"uprn": 25069053.0, "standardised_address": "185, BEADLEMEAD, NETHERFIELD, MILTON KEYNES, MK6 4HU", "standardised_postcode": "MK6 4HU", "LONGITUDE": -0.7286182, "LATITUDE": 52.0202019}, {"uprn": 25052926.0, "standardised_address": "56, CHERVIL, BEANHILL, MILTON KEYNES, MK6 4LG", "standardised_postcode": "MK6 4LG", "LONGITUDE": -0.7383444, "LATITUDE": 52.0184541}, {"uprn": 25054338.0, "standardised_address": "12, Osprey Close, Eaglestone", "standardised_postcode": "MK6 5BQ", "LONGITUDE": -0.7403678, "LATITUDE": 52.0299488}, {"uprn": 10093919581.0, "standardised_address": "9, ABIGAR CLOSE, WHITEHOUSE, MILTON KEYNES, MK8 1EN", "standardised_postcode": "MK8 1EN", "LONGITUDE": -0.8157866, "LATITUDE": 52.0314226}, {"uprn": 100031517329.0, "standardised_address": "37 Abbey Lodge, Baslow Drive, Beeston, Nottingham, NG9 2RZ", "standardised_postcode": "NG9 2RZ", "LONGITUDE": -1.207631, "LATITUDE": 52.9383028}, {"uprn": 100031531710.0, "standardised_address": "68 Abbey Lodge, Charles Avenue, Beeston, Nottingham, NG9 2SY", "standardised_postcode": "NG9 2SY", "LONGITUDE": -1.2088379, "LATITUDE": 52.938212}, {"uprn": 100031074426.0, "standardised_address": "13, ST. DUNSTANS CLOSE, KETTERING, NN15 5JE", "standardised_postcode": "NN15 5JE", "LONGITUDE": -0.6934448, "LATITUDE": 52.3923711}, {"uprn": 100031074430.0, "standardised_address": "17, St. Dunstans Close", "standardised_postcode": "NN15 5JE", "LONGITUDE": -0.6933767, "LATITUDE": 52.3921905}, {"uprn": 15007202.0, "standardised_address": "4, Kirton End", "standardised_postcode": "NN3 8FD", "LONGITUDE": -0.8198817, "LATITUDE": 52.267948}, {"uprn": 15007203.0, "standardised_address": "5, Kirton End", "standardised_postcode": "NN3 8FD", "LONGITUDE": -0.8198954, "LATITUDE": 52.2679841}, {"uprn": 10000861159.0, "standardised_address": "8, CLUNY WAY, ARLESEY, SG15 6ZB", "standardised_postcode": "SG15 6ZB", "LONGITUDE": -0.2630433, "LATITUDE": 52.0124857}, {"uprn": 100080067685.0, "standardised_address": "20, Reynolds Close", "standardised_postcode": "SG18 0QL", "LONGITUDE": -0.2602507, "LATITUDE": 52.0913964}, {"uprn": 100080067686.0, "standardised_address": "21 REYNOLDS CLOSE, BIGGLESWADE", "standardised_postcode": "SG18 0QL", "LONGITUDE": -0.2603335, "LATITUDE": 52.091385}, {"uprn": 100080080505.0, "standardised_address": "24, SKIPTON CLOSE, SANDY, SG19 1UB", "standardised_postcode": "SG19 1UB", "LONGITUDE": -0.2865906, "LATITUDE": 52.1338632}, {"uprn": 83104312.0, "standardised_address": "59, Johnson Street", "standardised_postcode": "WF14 8PQ", "LONGITUDE": -1.6989579, "LATITUDE": 53.670334}, {"uprn": 83177389.0, "standardised_address": "11, Primrose Gardens", "standardised_postcode": "WF17 0PZ", "LONGITUDE": -1.6201405, "LATITUDE": 53.7163994}, {"uprn": 83142904.0, "standardised_address": "64, Low Lane, Birstall", "standardised_postcode": "WF17 9HD", "LONGITUDE": -1.6661726, "LATITUDE": 53.7324005}, {"uprn": 83143336.0, "standardised_address": "7, MUSGRAVE STREET, BIRSTALL, BATLEY, WF17 9PF", "standardised_postcode": "WF17 9PF", "LONGITUDE": -1.6664882, "LATITUDE": 53.7322235}, {"uprn": 63013649.0, "standardised_address": "13, Broadacre Road", "standardised_postcode": "WF5 0QR", "LONGITUDE": -1.5691375, "LATITUDE": 53.6774393}, {"uprn": 10000832751.0, "standardised_address": "3, JUBILEE VILLAS, MAMBLE, KIDDERMINSTER, DY14 9JH", "standardised_postcode": "DY14 9JH", "LONGITUDE": -2.4549656, "LATITUDE": 52.3407598}, {"uprn": 10014089919.0, "standardised_address": "4, Spilsbury View, Mamble", "standardised_postcode": "DY14 9JJ", "LONGITUDE": -2.4544103, "LATITUDE": 52.3410046}, {"uprn": 10000830555.0, "standardised_address": "6, The Leasowes, Bayton", "standardised_postcode": "DY14 9NA", "LONGITUDE": -2.4474051, "LATITUDE": 52.3566115}, {"uprn": 100120589226.0, "standardised_address": "7 The Beeches, Mamble", "standardised_postcode": "DY14 9PD", "LONGITUDE": -2.4558902, "LATITUDE": 52.3407472}, {"uprn": 100121247799.0, "standardised_address": "14, Wesley Court, All Saints Road", "standardised_postcode": "GL1 4EF", "LONGITUDE": -2.2377897, "LATITUDE": 51.8605575}, {"uprn": 100120502788.0, "standardised_address": "28, BRAMBLE DRIVE, CAM, DURSLEY, GL11 5PX", "standardised_postcode": "GL11 5PX", "LONGITUDE": -2.362587, "LATITUDE": 51.6908247}, {"uprn": 100120502765.0, "standardised_address": "5, Bramble Drive, Cam", "standardised_postcode": "GL11 5PX", "LONGITUDE": -2.3621281, "LATITUDE": 51.6913296}, {"uprn": 10006832500.0, "standardised_address": "65, Midland Court", "standardised_postcode": "GL7 1JZ", "LONGITUDE": -1.9623299, "LATITUDE": 51.7089224}, {"uprn": 10009135124.0, "standardised_address": "45, Sapperton", "standardised_postcode": "GL7 6LQ", "LONGITUDE": -2.0804444, "LATITUDE": 51.7270088}, {"uprn": 200002589431.0, "standardised_address": "6, Caldervale, Bodenham", "standardised_postcode": "HR1 3LB", "LONGITUDE": -2.6677089, "LATITUDE": 52.1597419}, {"uprn": 200002599852.0, "standardised_address": "6, Cornewall Close, Moccas", "standardised_postcode": "HR2 9LG", "LONGITUDE": -2.9400679, "LATITUDE": 52.0787247}, {"uprn": 200002600043.0, "standardised_address": "12, Gosmore Road, Clehonger", "standardised_postcode": "HR2 9SN", "LONGITUDE": -2.8030098, "LATITUDE": 52.0335617}, {"uprn": 200002600713.0, "standardised_address": "6, THE COURTLANDS, WINFORTON, HEREFORD, HR3 6EF", "standardised_postcode": "HR3 6EF", "LONGITUDE": -3.0308025, "LATITUDE": 52.117301}, {"uprn": 200002600714.0, "standardised_address": "7, The Courtlands, Winforton", "standardised_postcode": "HR3 6EF", "LONGITUDE": -3.0309782, "LATITUDE": 52.1173174}, {"uprn": 200002600633.0, "standardised_address": "25 WEST VIEW, ALMELEY", "standardised_postcode": "HR3 6LE", "LONGITUDE": -2.9765856, "LATITUDE": 52.1599482}, {"uprn": 200002600640.0, "standardised_address": "9 WEST VIEW, ALMELEY", "standardised_postcode": "HR3 6LE", "LONGITUDE": -2.9776456, "LATITUDE": 52.1602901}, {"uprn": 10009580850.0, "standardised_address": "Questmore Cottage, Eardisley", "standardised_postcode": "HR3 6LW", "LONGITUDE": -3.0174005, "LATITUDE": 52.1638316}, {"uprn": 200002600667.0, "standardised_address": "2, Manor Close, Almeley", "standardised_postcode": "HR3 6NF", "LONGITUDE": -2.9777219, "LATITUDE": 52.159768}, {"uprn": 200002600745.0, "standardised_address": "18 ORCHARD CLOSE, EARDISLEY", "standardised_postcode": "HR3 6NP", "LONGITUDE": -3.0049396, "LATITUDE": 52.1369089}, {"uprn": 200002630372.0, "standardised_address": "7 NEW BARNFIELDS, STRETTON SUGWAS", "standardised_postcode": "HR4 7AZ", "LONGITUDE": -2.7937164, "LATITUDE": 52.0784081}, {"uprn": 200002630153.0, "standardised_address": "15 Brookside, Canon Pyon", "standardised_postcode": "HR4 8NY", "LONGITUDE": -2.7860426, "LATITUDE": 52.1366665}, {"uprn": 10007371180.0, "standardised_address": "1 CUCKOO PEN, KINGS PYON, HEREFORD, HR4 8PT", "standardised_postcode": "HR4 8PT", "LONGITUDE": -2.8223066, "LATITUDE": 52.1517035}, {"uprn": 10007360456.0, "standardised_address": "Flat 3, Whitehill House, Kington Road, Weobley, Herefordshire, County of, HR4 8QT", "standardised_postcode": "HR4 8QT", "LONGITUDE": -2.8871351, "LATITUDE": 52.1637664}, {"uprn": 10007360557.0, "standardised_address": "4 The Close, Burton Gardens, Weobley, Herefordshire, County of, HR4 8RQ", "standardised_postcode": "HR4 8RQ", "LONGITUDE": -2.8713531, "LATITUDE": 52.158175}, {"uprn": 200002606366.0, "standardised_address": "26, Burton Gardens, Weobley", "standardised_postcode": "HR4 8SR", "LONGITUDE": -2.870072, "LATITUDE": 52.1584452}, {"uprn": 200002606471.0, "standardised_address": "19 BURTON CRESCENT, WEOBLEY", "standardised_postcode": "HR4 8TB", "LONGITUDE": -2.8686872, "LATITUDE": 52.157898}, {"uprn": 200002610232.0, "standardised_address": "Flat 18, Caldwell Court, Walmer Street, Herefordshire, County of, HR4 9JD", "standardised_postcode": "HR4 9JD", "LONGITUDE": -2.7209647, "LATITUDE": 52.0587273}, {"uprn": 200002610239.0, "standardised_address": "Flat 25, Caldwell Court, Walmer Street, Herefordshire, County of, HR4 9JD", "standardised_postcode": "HR4 9JD", "LONGITUDE": -2.720922, "LATITUDE": 52.0587905}, {"uprn": 10007369692.0, "standardised_address": "29, GREENFIELDS, KINGTON, HR5 3AA", "standardised_postcode": "HR5 3AA", "LONGITUDE": -3.0276747, "LATITUDE": 52.2055563}, {"uprn": 200002611192.0, "standardised_address": "2, THE CRESCENT, KINGTON, HR5 3AS", "standardised_postcode": "HR5 3AS", "LONGITUDE": -3.03446, "LATITUDE": 52.2028267}, {"uprn": 200002610772.0, "standardised_address": "14, PARK ROAD, KINGTON, HR5 3AW", "standardised_postcode": "HR5 3AW", "LONGITUDE": -3.0366914, "LATITUDE": 52.2025104}, {"uprn": 200002611451.0, "standardised_address": "Flat 6 The Old Mill, 1, The Square", "standardised_postcode": "HR5 3BA", "LONGITUDE": -3.0321263, "LATITUDE": 52.2040909}, {"uprn": 200002610986.0, "standardised_address": "41, HATTON GARDENS, KINGTON, HR5 3DD", "standardised_postcode": "HR5 3DD", "LONGITUDE": -3.0211028, "LATITUDE": 52.2054965}, {"uprn": 200002611663.0, "standardised_address": "10, PASSEY COURT, THE SQUARE, KINGTON, HR5 3EE", "standardised_postcode": "HR5 3EE", "LONGITUDE": -3.0313311, "LATITUDE": 52.2042027}, {"uprn": 200002611138.0, "standardised_address": "6 Markwick Close", "standardised_postcode": "HR5 3UE", "LONGITUDE": -3.0305986, "LATITUDE": 52.2022671}, {"uprn": 200002631485.0, "standardised_address": "15 Westland View, Luston", "standardised_postcode": "HR6 0EA", "LONGITUDE": -2.7559424, "LATITUDE": 52.2641453}, {"uprn": 200002611869.0, "standardised_address": "6, Stockton Rock, Kimbolton", "standardised_postcode": "HR6 0JE", "LONGITUDE": -2.705673, "LATITUDE": 52.2478961}, {"uprn": 200002611690.0, "standardised_address": "6, Hengrave Green, Ivington", "standardised_postcode": "HR6 0JL", "LONGITUDE": -2.771841, "LATITUDE": 52.2051174}, {"uprn": 200002611760.0, "standardised_address": "9, HENGRAVE GREEN, IVINGTON, LEOMINSTER, HR6 0JL", "standardised_postcode": "HR6 0JL", "LONGITUDE": -2.7726156, "LATITUDE": 52.2050583}, {"uprn": 200002631301.0, "standardised_address": "102, Humber Close, Steensbridge", "standardised_postcode": "HR6 0LT", "LONGITUDE": -2.6696194, "LATITUDE": 52.2115774}, {"uprn": 200002631307.0, "standardised_address": "108, HUMBER CLOSE, STEENSBRIDGE, LEOMINSTER, HR6 0LT", "standardised_postcode": "HR6 0LT", "LONGITUDE": -2.6688721, "LATITUDE": 52.2115187}, {"uprn": 200002631317.0, "standardised_address": "119, HUMBER CLOSE, STEENSBRIDGE, LEOMINSTER, HR6 0LT", "standardised_postcode": "HR6 0LT", "LONGITUDE": -2.6702553, "LATITUDE": 52.2110433}, {"uprn": 200002611946.0, "standardised_address": "19, Cherrybrook Close, Hope-under-Dinmore", "standardised_postcode": "HR6 0PW", "LONGITUDE": -2.7212073, "LATITUDE": 52.1716237}, {"uprn": 200002611963.0, "standardised_address": "34, Cherrybrook Close, Hope-under-Dinmore", "standardised_postcode": "HR6 0PW", "LONGITUDE": -2.7207064, "LATITUDE": 52.171393}, {"uprn": 200002611958.0, "standardised_address": "3, Cherrybrook Close, Hope-under-Dinmore", "standardised_postcode": "HR6 0PW", "LONGITUDE": -2.7198309, "LATITUDE": 52.1715062}, {"uprn": 200002611938.0, "standardised_address": "11, CHERRYBROOK CLOSE, HOPE-UNDER-DINMORE, LEOMINSTER, HR6 0PW", "standardised_postcode": "HR6 0PW", "LONGITUDE": -2.7205116, "LATITUDE": 52.1720055}, {"uprn": 10007361923.0, "standardised_address": "1, JOHN ABEL CLOSE, LEOMINSTER, HR6 8AG", "standardised_postcode": "HR6 8AG", "LONGITUDE": -2.7349774, "LATITUDE": 52.2227768}, {"uprn": 200002612332.0, "standardised_address": "1 FALCONER PLACE", "standardised_postcode": "HR6 8AP", "LONGITUDE": -2.7342632, "LATITUDE": 52.2265124}, {"uprn": 200002612389.0, "standardised_address": "31, Worcester Road", "standardised_postcode": "HR6 8AU", "LONGITUDE": -2.731483, "LATITUDE": 52.2248304}, {"uprn": 200002612795.0, "standardised_address": "7, Kenwater Close", "standardised_postcode": "HR6 8DL", "LONGITUDE": -2.7422995, "LATITUDE": 52.2299145}, {"uprn": 200002612771.0, "standardised_address": "12, Kenwater Close", "standardised_postcode": "HR6 8DL", "LONGITUDE": -2.7418443, "LATITUDE": 52.2298365}, {"uprn": 200002612893.0, "standardised_address": "22, PARADISE COURT, LEOMINSTER, HR6 8DY", "standardised_postcode": "HR6 8DY", "LONGITUDE": -2.7391676, "LATITUDE": 52.2308872}, {"uprn": 200002631238.0, "standardised_address": "97A, BRIDGE STREET, LEOMINSTER, HR6 8EA", "standardised_postcode": "HR6 8EA", "LONGITUDE": -2.74386, "LATITUDE": 52.2339146}, {"uprn": 200002612984.0, "standardised_address": "35, Ridgemoor Road", "standardised_postcode": "HR6 8EJ", "LONGITUDE": -2.7411285, "LATITUDE": 52.2331731}, {"uprn": 200002612993.0, "standardised_address": "51, RIDGEMOOR ROAD, LEOMINSTER, HR6 8EJ", "standardised_postcode": "HR6 8EJ", "LONGITUDE": -2.7409404, "LATITUDE": 52.2335823}, {"uprn": 200002613041.0, "standardised_address": "64, Ridgemoor Road", "standardised_postcode": "HR6 8EL", "LONGITUDE": -2.7395478, "LATITUDE": 52.2334921}, {"uprn": 200002613014.0, "standardised_address": "16, Ridgemoor Road", "standardised_postcode": "HR6 8EL", "LONGITUDE": -2.740019, "LATITUDE": 52.2327699}, {"uprn": 200002613032.0, "standardised_address": "48, RIDGEMOOR ROAD, LEOMINSTER, HR6 8EL", "standardised_postcode": "HR6 8EL", "LONGITUDE": -2.7390232, "LATITUDE": 52.2336483}, {"uprn": 200002613051.0, "standardised_address": "15, CHEATON CLOSE, LEOMINSTER, HR6 8EN", "standardised_postcode": "HR6 8EN", "LONGITUDE": -2.7391605, "LATITUDE": 52.233099}, {"uprn": 200002613143.0, "standardised_address": "66, CHEATON CLOSE, LEOMINSTER, HR6 8EW", "standardised_postcode": "HR6 8EW", "LONGITUDE": -2.7384096, "LATITUDE": 52.2328519}, {"uprn": 200002614594.0, "standardised_address": "73, BARGATES, LEOMINSTER, HR6 8HB", "standardised_postcode": "HR6 8HB", "LONGITUDE": -2.7446336, "LATITUDE": 52.22678}, {"uprn": 200002613305.0, "standardised_address": "5, PUMP PIECE, LEOMINSTER, HR6 8HR", "standardised_postcode": "HR6 8HR", "LONGITUDE": -2.7462416, "LATITUDE": 52.2248908}, {"uprn": 200002614704.0, "standardised_address": "34, SANDPITS, LEOMINSTER, HR6 8HT", "standardised_postcode": "HR6 8HT", "LONGITUDE": -2.7462558, "LATITUDE": 52.2239917}, {"uprn": 200002613457.0, "standardised_address": "2, GEORGE STREET, LEOMINSTER, HR6 8JZ", "standardised_postcode": "HR6 8JZ", "LONGITUDE": -2.741903, "LATITUDE": 52.2219602}, {"uprn": 200002613508.0, "standardised_address": "42, CROFT STREET, LEOMINSTER, HR6 8LA", "standardised_postcode": "HR6 8LA", "LONGITUDE": -2.7433093, "LATITUDE": 52.2228864}, {"uprn": 200002613514.0, "standardised_address": "50, Croft Street", "standardised_postcode": "HR6 8LA", "LONGITUDE": -2.7438936, "LATITUDE": 52.2228108}, {"uprn": 200002613493.0, "standardised_address": "29, Croft Street", "standardised_postcode": "HR6 8LA", "LONGITUDE": -2.7422535, "LATITUDE": 52.2227852}, {"uprn": 200002613536.0, "standardised_address": "3, CONINGSBY ROAD, LEOMINSTER, HR6 8LL", "standardised_postcode": "HR6 8LL", "LONGITUDE": -2.7388428, "LATITUDE": 52.2241091}, {"uprn": 200002613531.0, "standardised_address": "11, CONINGSBY ROAD, LEOMINSTER, HR6 8LL", "standardised_postcode": "HR6 8LL", "LONGITUDE": -2.7383178, "LATITUDE": 52.2239957}, {"uprn": 200002613553.0, "standardised_address": "1, Eaton Close", "standardised_postcode": "HR6 8LQ", "LONGITUDE": -2.7378856, "LATITUDE": 52.2242331}, {"uprn": 200002631267.0, "standardised_address": "14, EATON CLOSE, LEOMINSTER, HR6 8LQ", "standardised_postcode": "HR6 8LQ", "LONGITUDE": -2.7374908, "LATITUDE": 52.2251436}, {"uprn": 200002613566.0, "standardised_address": "22, EATON CLOSE, LEOMINSTER, HR6 8LQ", "standardised_postcode": "HR6 8LQ", "LONGITUDE": -2.7370288, "LATITUDE": 52.224652}, {"uprn": 200002616236.0, "standardised_address": "FLAT 7, NEWMAN HOUSE, RYELANDS ROAD, LEOMINSTER, HR6 8PD", "standardised_postcode": "HR6 8PD", "LONGITUDE": -2.7448232, "LATITUDE": 52.2235533}, {"uprn": 200002613680.0, "standardised_address": "29, HOLLAND ROAD, LEOMINSTER, HR6 8PF", "standardised_postcode": "HR6 8PF", "LONGITUDE": -2.7437325, "LATITUDE": 52.224556}, {"uprn": 200002613705.0, "standardised_address": "31, Mortimer Street", "standardised_postcode": "HR6 8PG", "LONGITUDE": -2.7425265, "LATITUDE": 52.2233499}, {"uprn": 200002613737.0, "standardised_address": "21, WIGMORE STREET, LEOMINSTER, HR6 8PJ", "standardised_postcode": "HR6 8PJ", "LONGITUDE": -2.7446163, "LATITUDE": 52.2223539}, {"uprn": 200002613761.0, "standardised_address": "15, Wigmore Street", "standardised_postcode": "HR6 8PL", "LONGITUDE": -2.7443457, "LATITUDE": 52.2227091}, {"uprn": 10007370335.0, "standardised_address": "8, MORTIMER CLOSE, LEOMINSTER, HR6 8PQ", "standardised_postcode": "HR6 8PQ", "LONGITUDE": -2.7423152, "LATITUDE": 52.2238457}, {"uprn": 10023974314.0, "standardised_address": "ROOM 2, LENDOR, 9, BUCKFIELD ROAD, LEOMINSTER, HEREFORDSHIRE, HR6 8SF", "standardised_postcode": "HR6 8SF", "LONGITUDE": -2.7549361, "LATITUDE": 52.2258208}, {"uprn": 200002616762.0, "standardised_address": "50, Portna Way", "standardised_postcode": "HR6 9AD", "LONGITUDE": -2.7679625, "LATITUDE": 52.2220362}, {"uprn": 200002616765.0, "standardised_address": "56, PORTNA WAY, LEOMINSTER, HR6 9AD", "standardised_postcode": "HR6 9AD", "LONGITUDE": -2.7681663, "LATITUDE": 52.221972}, {"uprn": 200002616784.0, "standardised_address": "6, Portna Way", "standardised_postcode": "HR6 9AE", "LONGITUDE": -2.7661266, "LATITUDE": 52.2216886}, {"uprn": 200002616770.0, "standardised_address": "13, Portna Way", "standardised_postcode": "HR6 9AE", "LONGITUDE": -2.7667581, "LATITUDE": 52.2218013}, {"uprn": 200002616815.0, "standardised_address": "28, FOOTWAY CROFT, LEOMINSTER, HR6 9AG", "standardised_postcode": "HR6 9AG", "LONGITUDE": -2.7674867, "LATITUDE": 52.2216078}, {"uprn": 200002616876.0, "standardised_address": "2, Curl View, Pembridge", "standardised_postcode": "HR6 9ET", "LONGITUDE": -2.8940396, "LATITUDE": 52.2194034}, {"uprn": 200002616880.0, "standardised_address": "6, Curl View, Pembridge", "standardised_postcode": "HR6 9ET", "LONGITUDE": -2.894712, "LATITUDE": 52.2193533}, {"uprn": 200002617122.0, "standardised_address": "11, Curl View Crescent, Pembridge", "standardised_postcode": "HR6 9HQ", "LONGITUDE": -2.8945024, "LATITUDE": 52.2198494}, {"uprn": 200002631408.0, "standardised_address": "39 THE GROVE, SHOBDON", "standardised_postcode": "HR6 9NF", "LONGITUDE": -2.8869065, "LATITUDE": 52.2511987}, {"uprn": 200002616906.0, "standardised_address": "18 MOOR MEADOW, SHOBDON", "standardised_postcode": "HR6 9NT", "LONGITUDE": -2.8840648, "LATITUDE": 52.2515397}, {"uprn": 10009574826.0, "standardised_address": "2 The Village, Yatton", "standardised_postcode": "HR6 9TL", "LONGITUDE": -2.8369525, "LATITUDE": 52.2958029}, {"uprn": 200002622624.0, "standardised_address": "10, MILLER CRADDOCK WAY, LEDBURY, HR8 2XT", "standardised_postcode": "HR8 2XT", "LONGITUDE": -2.4284905, "LATITUDE": 52.0307915}, {"uprn": 200002622657.0, "standardised_address": "10, ASTON CLOSE, LEDBURY, HR8 2XU", "standardised_postcode": "HR8 2XU", "LONGITUDE": -2.4271925, "LATITUDE": 52.0307153}, {"uprn": 200002622662.0, "standardised_address": "16, Aston Close", "standardised_postcode": "HR8 2XU", "LONGITUDE": -2.4278182, "LATITUDE": 52.0306051}, {"uprn": 100071536782.0, "standardised_address": "2, Findon Way", "standardised_postcode": "SY3 5NA", "LONGITUDE": -2.8027488, "LATITUDE": 52.7101477}, {"uprn": 100071536880.0, "standardised_address": "113, Lambourn Drive", "standardised_postcode": "SY3 5NF", "LONGITUDE": -2.8030195, "LATITUDE": 52.7106552}, {"uprn": 10009574764.0, "standardised_address": "2 THE BROOK, LINGEN, BUCKNELL, SY7 0DY", "standardised_postcode": "SY7 0DY", "LONGITUDE": -2.9317576, "LATITUDE": 52.2975198}, {"uprn": 200002628293.0, "standardised_address": "21 ROSEMARY, LEINTWARDINE", "standardised_postcode": "SY7 0LR", "LONGITUDE": -2.8731711, "LATITUDE": 52.3614144}, {"uprn": 200002628296.0, "standardised_address": "24 ROSEMARY, LEINTWARDINE", "standardised_postcode": "SY7 0LR", "LONGITUDE": -2.8730416, "LATITUDE": 52.360804}, {"uprn": 200002628400.0, "standardised_address": "10 Lowe Croft, Leintwardine", "standardised_postcode": "SY7 0NP", "LONGITUDE": -2.8736761, "LATITUDE": 52.3609612}, {"uprn": 200002628392.0, "standardised_address": "2, Lowe Croft, Leintwardine", "standardised_postcode": "SY7 0NP", "LONGITUDE": -2.8736537, "LATITUDE": 52.3605657}, {"uprn": 200002628536.0, "standardised_address": "4, HALLETS WELL, ORLETON, LUDLOW, SY8 4HH", "standardised_postcode": "SY8 4HH", "LONGITUDE": -2.7409679, "LATITUDE": 52.3020458}, {"uprn": 200002628506.0, "standardised_address": "10 ST GEORGES CRESCENT, ORLETON", "standardised_postcode": "SY8 4HL", "LONGITUDE": -2.7420943, "LATITUDE": 52.3018769}, {"uprn": 200002628470.0, "standardised_address": "8 GREEN LANE, ORLETON", "standardised_postcode": "SY8 4JE", "LONGITUDE": -2.7581608, "LATITUDE": 52.3023411}, {"uprn": 10007370967.0, "standardised_address": "15 The Avenue, Wyson, Brimfield, Herefordshire, County of, SY8 4NJ", "standardised_postcode": "SY8 4NJ", "LONGITUDE": -2.7028498, "LATITUDE": 52.3093186}, {"uprn": 100120590397.0, "standardised_address": "6, Spring Gardens", "standardised_postcode": "WR15 8BE", "LONGITUDE": -2.5918475, "LATITUDE": 52.3107187}, {"uprn": 100120590377.0, "standardised_address": "8, SCOTLAND PLACE, TENBURY WELLS, WR15 8BT", "standardised_postcode": "WR15 8BT", "LONGITUDE": -2.5949436, "LATITUDE": 52.3118989}, {"uprn": 100120590206.0, "standardised_address": "24, KYRESIDE, TENBURY WELLS, WR15 8BU", "standardised_postcode": "WR15 8BU", "LONGITUDE": -2.5948807, "LATITUDE": 52.3050216}, {"uprn": 100120590212.0, "standardised_address": "30, Kyreside", "standardised_postcode": "WR15 8BU", "LONGITUDE": -2.5946937, "LATITUDE": 52.306389}, {"uprn": 100120590223.0, "standardised_address": "41, KYRESIDE, TENBURY WELLS, WR15 8BU", "standardised_postcode": "WR15 8BU", "LONGITUDE": -2.5946878, "LATITUDE": 52.3059485}, {"uprn": 100120590253.0, "standardised_address": "71, KYRESIDE, TENBURY WELLS, WR15 8BX", "standardised_postcode": "WR15 8BX", "LONGITUDE": -2.5932704, "LATITUDE": 52.3041486}, {"uprn": 100120589910.0, "standardised_address": "1, Bromyard Road", "standardised_postcode": "WR15 8BZ", "LONGITUDE": -2.5950627, "LATITUDE": 52.3043733}, {"uprn": 100120590015.0, "standardised_address": "19, CRESCENT PLACE, TENBURY WELLS, WR15 8DF", "standardised_postcode": "WR15 8DF", "LONGITUDE": -2.5947732, "LATITUDE": 52.3024688}, {"uprn": 100120590467.0, "standardised_address": "13, THE CRESCENT, TENBURY WELLS, WR15 8DG", "standardised_postcode": "WR15 8DG", "LONGITUDE": -2.5943843, "LATITUDE": 52.3030012}, {"uprn": 100120590458.0, "standardised_address": "4, THE CRESCENT, TENBURY WELLS, WR15 8DG", "standardised_postcode": "WR15 8DG", "LONGITUDE": -2.5934407, "LATITUDE": 52.3026284}, {"uprn": 100120590555.0, "standardised_address": "61, WHEELER ORCHARD, TENBURY WELLS, WR15 8DQ", "standardised_postcode": "WR15 8DQ", "LONGITUDE": -2.5955391, "LATITUDE": 52.3016289}, {"uprn": 100120590504.0, "standardised_address": "10, Wheeler Orchard", "standardised_postcode": "WR15 8DQ", "LONGITUDE": -2.5972002, "LATITUDE": 52.303005}, {"uprn": 100120590505.0, "standardised_address": "11, Wheeler Orchard", "standardised_postcode": "WR15 8DQ", "LONGITUDE": -2.5973032, "LATITUDE": 52.3030315}, {"uprn": 100120590513.0, "standardised_address": "19 Wheeler Orchard", "standardised_postcode": "WR15 8DQ", "LONGITUDE": -2.5962211, "LATITUDE": 52.3032707}, {"uprn": 100120590539.0, "standardised_address": "45 Wheeler Orchard", "standardised_postcode": "WR15 8DQ", "LONGITUDE": -2.5953731, "LATITUDE": 52.3023669}, {"uprn": 100120590126.0, "standardised_address": "4, GRASSY BANK, TENBURY WELLS, WR15 8DR", "standardised_postcode": "WR15 8DR", "LONGITUDE": -2.5936679, "LATITUDE": 52.3020698}, {"uprn": 100120590355.0, "standardised_address": "22, PEMBROKE AVENUE, TENBURY WELLS, WR15 8EH", "standardised_postcode": "WR15 8EH", "LONGITUDE": -2.5950524, "LATITUDE": 52.3090753}, {"uprn": 100120594666.0, "standardised_address": "32 Pembroke Gardens, Pembroke Avenue", "standardised_postcode": "WR15 8EH", "LONGITUDE": -2.5954208, "LATITUDE": 52.3091994}, {"uprn": 10000832235.0, "standardised_address": "2 Cutmill Bridge, Eardiston", "standardised_postcode": "WR15 8JN", "LONGITUDE": -2.4455624, "LATITUDE": 52.3121523}, {"uprn": 10000832237.0, "standardised_address": "4 Cutmill Bridge, Eardiston", "standardised_postcode": "WR15 8JN", "LONGITUDE": -2.4453422, "LATITUDE": 52.3121352}, {"uprn": 10000832267.0, "standardised_address": "25, CUTMILL BRIDGE, EARDISTON, TENBURY WELLS, WR15 8JN", "standardised_postcode": "WR15 8JN", "LONGITUDE": -2.4442501, "LATITUDE": 52.311945}, {"uprn": 100120606489.0, "standardised_address": "3, Astley Orchard, Eastham", "standardised_postcode": "WR15 8NR", "LONGITUDE": -2.4883154, "LATITUDE": 52.3102573}, {"uprn": 100120606492.0, "standardised_address": "6 Astley Orchard, Eastham", "standardised_postcode": "WR15 8NR", "LONGITUDE": -2.4887428, "LATITUDE": 52.3104443}, {"uprn": 10000830371.0, "standardised_address": "1 Church Close, Stoke Bliss", "standardised_postcode": "WR15 8QJ", "LONGITUDE": -2.5145315, "LATITUDE": 52.2613192}, {"uprn": 100120590485.0, "standardised_address": "5 The Oaks, Stoke Bliss", "standardised_postcode": "WR15 8RR", "LONGITUDE": -2.5435427, "LATITUDE": 52.2649832}, {"uprn": 10014092735.0, "standardised_address": "5, Malt House Mews", "standardised_postcode": "WR15 8TZ", "LONGITUDE": -2.5954096, "LATITUDE": 52.312769}, {"uprn": 90120326.0, "standardised_address": "Flat 7, Beverley Court, Clarendon Place, Dudley, B62 9BE", "standardised_postcode": "B62 9BE", "LONGITUDE": -2.0159488, "LATITUDE": 52.4625896}, {"uprn": 90118998.0, "standardised_address": "FLAT 78, VICTORIA COURT, BINSWOOD ROAD, HALESOWEN, B62 9BQ", "standardised_postcode": "B62 9BQ", "LONGITUDE": -2.0145066, "LATITUDE": 52.4631112}, {"uprn": 32015926.0, "standardised_address": "62 Petford Street", "standardised_postcode": "B64 6DY", "LONGITUDE": -2.0714993, "LATITUDE": 52.4736718}, {"uprn": 32004728.0, "standardised_address": "16, RED BRICK CLOSE, CRADLEY HEATH, B64 7DR", "standardised_postcode": "B64 7DR", "LONGITUDE": -2.0758424, "LATITUDE": 52.4652633}, {"uprn": 32148048.0, "standardised_address": "2, Brailsford Drive", "standardised_postcode": "B66 3NH", "LONGITUDE": -1.9679894, "LATITUDE": 52.4929906}, {"uprn": 10008537133.0, "standardised_address": "FLAT 8, 45, CORBETT STREET, SMETHWICK, B66 3PU", "standardised_postcode": "B66 3PU", "LONGITUDE": -1.9616889, "LATITUDE": 52.4893028}, {"uprn": 32144722.0, "standardised_address": "67, Talbot Road", "standardised_postcode": "B66 4DX", "LONGITUDE": -1.9667612, "LATITUDE": 52.4810605}, {"uprn": 32146056.0, "standardised_address": "Flat 6, 179 Bearwood Road, Sandwell, B66 4LN", "standardised_postcode": "B66 4LN", "LONGITUDE": -1.9670378, "LATITUDE": 52.4852949}, {"uprn": 32146371.0, "standardised_address": "10, Clent View, Gilbert Road", "standardised_postcode": "B66 4PU", "LONGITUDE": -1.9602366, "LATITUDE": 52.4829554}, {"uprn": 32129152.0, "standardised_address": "120, VICARAGE ROAD, SMETHWICK, B67 7AP", "standardised_postcode": "B67 7AP", "LONGITUDE": -1.9765168, "LATITUDE": 52.4936489}, {"uprn": 32129140.0, "standardised_address": "105, Vicarage Road", "standardised_postcode": "B67 7AP", "LONGITUDE": -1.9763696, "LATITUDE": 52.4934421}, {"uprn": 32131232.0, "standardised_address": "6 The Oaks, South Road, Sandwell, B67 7BY", "standardised_postcode": "B67 7BY", "LONGITUDE": -1.9747203, "LATITUDE": 52.4930012}, {"uprn": 32131213.0, "standardised_address": "FLAT 1, 41, SOUTH ROAD, SMETHWICK, B67 7BZ", "standardised_postcode": "B67 7BZ", "LONGITUDE": -1.9742786, "LATITUDE": 52.4926236}, {"uprn": 200001483798.0, "standardised_address": "Flat 1, 5 North Street, Sandwell, B67 7DA", "standardised_postcode": "B67 7DA", "LONGITUDE": -1.975265, "LATITUDE": 52.4933789}, {"uprn": 32131139.0, "standardised_address": "16 St. Albans Close", "standardised_postcode": "B67 7PD", "LONGITUDE": -1.9806101, "LATITUDE": 52.4966882}, {"uprn": 32130313.0, "standardised_address": "31, White Road", "standardised_postcode": "B67 7PG", "LONGITUDE": -1.978032, "LATITUDE": 52.4974969}, {"uprn": 32067588.0, "standardised_address": "12, COTSWOLD CLOSE, OLDBURY, B69 1FB", "standardised_postcode": "B69 1FB", "LONGITUDE": -2.0284188, "LATITUDE": 52.490672}, {"uprn": 32067603.0, "standardised_address": "40, Wolverley Crescent", "standardised_postcode": "B69 1FD", "LONGITUDE": -2.0298473, "LATITUDE": 52.4906267}, {"uprn": 32067644.0, "standardised_address": "22, WITLEY CRESCENT, OLDBURY, B69 1FF", "standardised_postcode": "B69 1FF", "LONGITUDE": -2.0285806, "LATITUDE": 52.4904562}, {"uprn": 32086384.0, "standardised_address": "45 COYNE ROAD, WEST BROMWICH", "standardised_postcode": "B70 7HJ", "LONGITUDE": -2.0035014, "LATITUDE": 52.5127458}, {"uprn": 100120639219.0, "standardised_address": "21 MUSKETTS COURT, BIRCHFIELD ROAD, REDDITCH", "standardised_postcode": "B97 4NA", "LONGITUDE": -1.958538, "LATITUDE": 52.2974523}, {"uprn": 100120639206.0, "standardised_address": "8 MUSKETTS COURT, BIRCHFIELD ROAD, REDDITCH", "standardised_postcode": "B97 4NA", "LONGITUDE": -1.9586844, "LATITUDE": 52.2972294}, {"uprn": 100070622454.0, "standardised_address": "24, Boyd Close", "standardised_postcode": "CV2 2NF", "LONGITUDE": -1.4423782, "LATITUDE": 52.431865}, {"uprn": 100070665874.0, "standardised_address": "4 JONATHAN ROAD, COVENTRY", "standardised_postcode": "CV2 2NQ", "LONGITUDE": -1.4423682, "LATITUDE": 52.4326561}, {"uprn": 100070665876.0, "standardised_address": "6 JONATHAN ROAD", "standardised_postcode": "CV2 2NQ", "LONGITUDE": -1.4423971, "LATITUDE": 52.4326922}, {"uprn": 100071505169.0, "standardised_address": "192, Leam Terrace", "standardised_postcode": "CV31 1DW", "LONGITUDE": -1.5185861, "LATITUDE": 52.2852891}, {"uprn": 10013181859.0, "standardised_address": "121 Radford Road", "standardised_postcode": "CV31 1JZ", "LONGITUDE": -1.5185861, "LATITUDE": 52.2852891}, {"uprn": 100071252300.0, "standardised_address": "21 EPPERSTONE COURT, AVENUE ROAD, LEAMINGTON SPA, CV31 3NH", "standardised_postcode": "CV31 3NH", "LONGITUDE": -1.5363462, "LATITUDE": 52.2854703}, {"uprn": 100070246806.0, "standardised_address": "38 Whittle Court, Upper Holly Walk", "standardised_postcode": "CV32 4LB", "LONGITUDE": -1.5250688, "LATITUDE": 52.2917237}, {"uprn": 100070246778.0, "standardised_address": "9 Whittle Court, Upper Holly Walk", "standardised_postcode": "CV32 4LB", "LONGITUDE": -1.5250688, "LATITUDE": 52.2917237}, {"uprn": 100070246783.0, "standardised_address": "15 Whittle Court, Upper Holly Walk", "standardised_postcode": "CV32 4LB", "LONGITUDE": -1.5250688, "LATITUDE": 52.2917237}, {"uprn": 100071254640.0, "standardised_address": "8 Leicester Court, Leicester Street", "standardised_postcode": "CV32 4UD", "LONGITUDE": -1.523885, "LATITUDE": 52.2947491}, {"uprn": 100071256971.0, "standardised_address": "20 STUART COURT, WARWICK TERRACE, LEAMINGTON SPA, CV32 5NU", "standardised_postcode": "CV32 5NU", "LONGITUDE": -1.5418657, "LATITUDE": 52.2922215}, {"uprn": 10003791898.0, "standardised_address": "17 Goode Close", "standardised_postcode": "CV34 5LP", "LONGITUDE": -1.6014186, "LATITUDE": 52.2866675}, {"uprn": 100070262028.0, "standardised_address": "9, Grange Close", "standardised_postcode": "CV34 5PE", "LONGITUDE": -1.5601131, "LATITUDE": 52.2884158}, {"uprn": 100070645529.0, "standardised_address": "28, Ensign Close", "standardised_postcode": "CV4 9TU", "LONGITUDE": -1.5962218, "LATITUDE": 52.4031891}, {"uprn": 10023037940.0, "standardised_address": "57 Dunster Place", "standardised_postcode": "CV6 4JD", "LONGITUDE": -1.4997454, "LATITUDE": 52.4441734}, {"uprn": 100071341257.0, "standardised_address": "45, THE FIRS, MAXSTOKE LANE, MERIDEN, COVENTRY, CV7 7NT", "standardised_postcode": "CV7 7NT", "LONGITUDE": -1.6515841, "LATITUDE": 52.4397024}, {"uprn": 100071341250.0, "standardised_address": "38, The Firs, Maxstoke Lane", "standardised_postcode": "CV7 7NT", "LONGITUDE": -1.651866, "LATITUDE": 52.4394065}, {"uprn": 90114588.0, "standardised_address": "Flat 2, 20 Wellington Road, Dudley, DY1 1RB", "standardised_postcode": "DY1 1RB", "LONGITUDE": -2.0956881, "LATITUDE": 52.5076633}, {"uprn": 90000273.0, "standardised_address": "36, Edenbridge View", "standardised_postcode": "DY1 2JJ", "LONGITUDE": -2.1174047, "LATITUDE": 52.5187103}, {"uprn": 100120753988.0, "standardised_address": "Flat 1, Hargreaves Court, Parry Road, Wyre Forest, DY11 6LZ", "standardised_postcode": "DY11 6LZ", "LONGITUDE": -2.2655377, "LATITUDE": 52.3760041}, {"uprn": 90094063.0, "standardised_address": "11, IVANHOE STREET, DUDLEY, DY2 0YB", "standardised_postcode": "DY2 0YB", "LONGITUDE": -2.1002305, "LATITUDE": 52.5032005}, {"uprn": 90094168.0, "standardised_address": "129, Ivanhoe Street", "standardised_postcode": "DY2 0YD", "LONGITUDE": -2.0989799, "LATITUDE": 52.5038938}, {"uprn": 90126107.0, "standardised_address": "FLAT 1, 26, NORTH STREET, DUDLEY, DY2 7DU", "standardised_postcode": "DY2 7DU", "LONGITUDE": -2.0756107, "LATITUDE": 52.5100603}, {"uprn": 90009856.0, "standardised_address": "30, Lauder Close", "standardised_postcode": "DY3 3XN", "LONGITUDE": -2.1305593, "LATITUDE": 52.5466641}, {"uprn": 10013825938.0, "standardised_address": "21, SHEPHERDS DROVE, WEST ASHTON, TROWBRIDGE, BA14 6DG", "standardised_postcode": "BA14 6DG", "LONGITUDE": -2.1703744, "LATITUDE": 51.2959889}, {"uprn": 100121084534.0, "standardised_address": "68, BARN GLEBE, TROWBRIDGE, BA14 7JZ", "standardised_postcode": "BA14 7JZ", "LONGITUDE": -2.194204, "LATITUDE": 51.3212819}, {"uprn": 250045056.0, "standardised_address": "116, WOOKEY HOLE ROAD, WELLS, BA5 2NQ", "standardised_postcode": "BA5 2NQ", "LONGITUDE": -2.6618111, "LATITUDE": 51.2154525}, {"uprn": 100040783250.0, "standardised_address": "210, WINDHAM ROAD, BOURNEMOUTH, BH1 4QX", "standardised_postcode": "BH1 4QX", "LONGITUDE": -1.8485583, "LATITUDE": 50.7304686}, {"uprn": 100040728126.0, "standardised_address": "34, Copper Beech Gardens", "standardised_postcode": "BH10 5DB", "LONGITUDE": -1.8917906, "LATITUDE": 50.7540873}, {"uprn": 100040753089.0, "standardised_address": "Flat 2, Portiere House, 10 Moore Avenue, Bournemouth, Christchurch and Poole, BH11 8AY", "standardised_postcode": "BH11 8AY", "LONGITUDE": -1.9112837, "LATITUDE": 50.7610641}, {"uprn": 100040761568.0, "standardised_address": "11 RAGLAN GARDENS, BOURNEMOUTH", "standardised_postcode": "BH11 8QU", "LONGITUDE": -1.9130825, "LATITUDE": 50.7542938}, {"uprn": 100040820003.0, "standardised_address": "41, Merrow Avenue", "standardised_postcode": "BH12 1PY", "LONGITUDE": -1.9063404, "LATITUDE": 50.7368871}, {"uprn": 100040827450.0, "standardised_address": "335b, Ringwood Road", "standardised_postcode": "BH12 3JN", "LONGITUDE": -1.9462039, "LATITUDE": 50.7421399}, {"uprn": 100040813791.0, "standardised_address": "18 Jellicoe Close, Bournemouth, Christchurch and Poole, BH14 0PX", "standardised_postcode": "BH14 0PX", "LONGITUDE": -1.960793, "LATITUDE": 50.72879}, {"uprn": 100040821208.0, "standardised_address": "Flat 6, Ashley Mount, 7 Mount Road, Bournemouth, Christchurch and Poole, BH14 0QW", "standardised_postcode": "BH14 0QW", "LONGITUDE": -1.9495564, "LATITUDE": 50.7293428}, {"uprn": 10001086391.0, "standardised_address": "10 MANTON CLOSE, POOLE", "standardised_postcode": "BH15 4QA", "LONGITUDE": -2.0098437, "LATITUDE": 50.7207753}, {"uprn": 100040827073.0, "standardised_address": "1, RICE TERRACE, POOLE, BH16 5DH", "standardised_postcode": "BH16 5DH", "LONGITUDE": -2.0197173, "LATITUDE": 50.7266588}, {"uprn": 100040827063.0, "standardised_address": "3 Rice Gardens", "standardised_postcode": "BH16 5DJ", "LONGITUDE": -2.01989, "LATITUDE": 50.7264753}, {"uprn": 100040607845.0, "standardised_address": "16 WARBLER CLOSE, UPTON", "standardised_postcode": "BH16 5RL", "LONGITUDE": -2.0316176, "LATITUDE": 50.742003}, {"uprn": 100040824517.0, "standardised_address": "Flat 14, Tanglewood Lodge, 89a Petersham Road, Bournemouth, Christchurch and Poole, BH17 7DW", "standardised_postcode": "BH17 7DW", "LONGITUDE": -2.0037117, "LATITUDE": 50.742439}, {"uprn": 10001087039.0, "standardised_address": "59, DEWLISH CLOSE, POOLE, BH17 8AQ", "standardised_postcode": "BH17 8AQ", "LONGITUDE": -1.9568173, "LATITUDE": 50.7539593}, {"uprn": 10001087048.0, "standardised_address": "77, Dewlish Close", "standardised_postcode": "BH17 8AQ", "LONGITUDE": -1.9563924, "LATITUDE": 50.7535454}, {"uprn": 100040799312.0, "standardised_address": "60, Chaldon Road", "standardised_postcode": "BH17 8DB", "LONGITUDE": -1.9598665, "LATITUDE": 50.7524496}, {"uprn": 100040799318.0, "standardised_address": "66, Chaldon Road", "standardised_postcode": "BH17 8DB", "LONGITUDE": -1.9600812, "LATITUDE": 50.7526369}, {"uprn": 100040799326.0, "standardised_address": "74, Chaldon Road", "standardised_postcode": "BH17 8DB", "LONGITUDE": -1.9604614, "LATITUDE": 50.7528353}, {"uprn": 100040799344.0, "standardised_address": "92, CHALDON ROAD, POOLE, BH17 8DB", "standardised_postcode": "BH17 8DB", "LONGITUDE": -1.9604477, "LATITUDE": 50.7524138}, {"uprn": 200004823359.0, "standardised_address": "12, Marsh Way", "standardised_postcode": "BH19 2TE", "LONGITUDE": -1.9770611, "LATITUDE": 50.6078225}, {"uprn": 200004825497.0, "standardised_address": "67 MARSH WAY, SWANAGE", "standardised_postcode": "BH19 2TE", "LONGITUDE": -1.9787943, "LATITUDE": 50.6087743}, {"uprn": 200004825492.0, "standardised_address": "57 MARSH WAY, SWANAGE", "standardised_postcode": "BH19 2TE", "LONGITUDE": -1.978921, "LATITUDE": 50.6084065}, {"uprn": 100040612353.0, "standardised_address": "22 Steppes, Langton Matravers, Dorset, BH19 3EY", "standardised_postcode": "BH19 3EY", "LONGITUDE": -1.9982872, "LATITUDE": 50.6114398}, {"uprn": 100040615235.0, "standardised_address": "18, FOLLY LANE, WAREHAM, BH20 4HH", "standardised_postcode": "BH20 4HH", "LONGITUDE": -2.1091065, "LATITUDE": 50.6892606}, {"uprn": 100040613496.0, "standardised_address": "11, BELLS ORCHARD LANE, WAREHAM, BH20 4HP", "standardised_postcode": "BH20 4HP", "LONGITUDE": -2.1076971, "LATITUDE": 50.6876586}, {"uprn": 200004818272.0, "standardised_address": "8 CHRISTMAS CLOSE, WAREHAM", "standardised_postcode": "BH20 4RG", "LONGITUDE": -2.1182555, "LATITUDE": 50.6868481}, {"uprn": 10011953366.0, "standardised_address": "3 Long Ground Cottages, Church Knowle", "standardised_postcode": "BH20 5NH", "LONGITUDE": -2.0886006, "LATITUDE": 50.6358618}, {"uprn": 10011953315.0, "standardised_address": "2 Tollgate Cottages, Kimmeridge", "standardised_postcode": "BH20 5PE", "LONGITUDE": -2.1203866, "LATITUDE": 50.6155815}, {"uprn": 200004827674.0, "standardised_address": "5 Hardy Cottages, School Lane, West Lulworth, Dorset, BH20 5SA", "standardised_postcode": "BH20 5SA", "LONGITUDE": -2.2402216, "LATITUDE": 50.6254701}, {"uprn": 100040619240.0, "standardised_address": "43B WEST STREET, BERE REGIS", "standardised_postcode": "BH20 7HS", "LONGITUDE": -2.2223413, "LATITUDE": 50.7545276}, {"uprn": 100041099402.0, "standardised_address": "3 LYS COTTAGES, SOUTHBROOK, BERE REGIS, WAREHAM, BH20 7LH", "standardised_postcode": "BH20 7LH", "LONGITUDE": -2.2180587, "LATITUDE": 50.7499967}, {"uprn": 100040715693.0, "standardised_address": "219, BEAUFORT ROAD, BOURNEMOUTH, BH6 5AF", "standardised_postcode": "BH6 5AF", "LONGITUDE": -1.8032153, "LATITUDE": 50.7350516}, {"uprn": 24102341.0, "standardised_address": "32 MARCONI CLOSE, WESTON-SUPER-MARE", "standardised_postcode": "BS23 3HH", "LONGITUDE": -2.9532424, "LATITUDE": 51.346858}, {"uprn": 539294.0, "standardised_address": "18, CARMARTHEN GROVE, WILLSBRIDGE, BRISTOL, BS30 6UY", "standardised_postcode": "BS30 6UY", "LONGITUDE": -2.477798, "LATITUDE": 51.4318312}, {"uprn": 100041033195.0, "standardised_address": "DORCHESTER YOUTH & COMMUNITY CENTRE, KINGS ROAD, DORCHESTER, DT1 1NJ", "standardised_postcode": "DT1 1NJ", "LONGITUDE": -2.4256275, "LATITUDE": 50.712939}, {"uprn": 100040630187.0, "standardised_address": "35 ALFRED PLACE, DORCHESTER", "standardised_postcode": "DT1 1NW", "LONGITUDE": -2.4289537, "LATITUDE": 50.7124378}, {"uprn": 100040603020.0, "standardised_address": "9 Thrift Close, Stalbridge", "standardised_postcode": "DT10 2LE", "LONGITUDE": -2.371142, "LATITUDE": 50.9560518}, {"uprn": 100040603027.0, "standardised_address": "16, THRIFT CLOSE, STALBRIDGE, STURMINSTER NEWTON, DT10 2LE", "standardised_postcode": "DT10 2LE", "LONGITUDE": -2.3710023, "LATITUDE": 50.9563849}, {"uprn": 10013220368.0, "standardised_address": "16 PORTMAN MEWS, BRYANSTON, BLANDFORD FORUM, DT11 0PR", "standardised_postcode": "DT11 0PR", "LONGITUDE": -2.1854767, "LATITUDE": 50.8609862}, {"uprn": 100040589446.0, "standardised_address": "1 YARDE FARM, PIMPERNE", "standardised_postcode": "DT11 8XF", "LONGITUDE": -2.1359685, "LATITUDE": 50.878473}, {"uprn": 100040589449.0, "standardised_address": "4 YARDE FARM, PIMPERNE", "standardised_postcode": "DT11 8XF", "LONGITUDE": -2.1359404, "LATITUDE": 50.8786079}, {"uprn": 100040589455.0, "standardised_address": "10 YARDE FARM, PIMPERNE", "standardised_postcode": "DT11 8XF", "LONGITUDE": -2.1360556, "LATITUDE": 50.8790934}, {"uprn": 100040588938.0, "standardised_address": "3 PLUMBLEY MEADOWS, WINTERBORNE KINGSTON", "standardised_postcode": "DT11 9BY", "LONGITUDE": -2.1991201, "LATITUDE": 50.7784828}, {"uprn": 100040603895.0, "standardised_address": "5, Woodsford Lane, Moreton", "standardised_postcode": "DT2 8AY", "LONGITUDE": -2.3122792, "LATITUDE": 50.7027633}, {"uprn": 100040640575.0, "standardised_address": "3 WOODBURY DROVE, CROSSWAYS", "standardised_postcode": "DT2 8XT", "LONGITUDE": -2.3343597, "LATITUDE": 50.6988929}, {"uprn": 100040659720.0, "standardised_address": "44, Buddleia Close", "standardised_postcode": "DT3 6SG", "LONGITUDE": -2.4455732, "LATITUDE": 50.6450829}, {"uprn": 100040662263.0, "standardised_address": "138, Corporation Road", "standardised_postcode": "DT4 0LQ", "LONGITUDE": -2.4683463, "LATITUDE": 50.6151519}, {"uprn": 100040662275.0, "standardised_address": "150, Corporation Road", "standardised_postcode": "DT4 0LQ", "LONGITUDE": -2.4681304, "LATITUDE": 50.6149443}, {"uprn": 100040626274.0, "standardised_address": "2 Pitchers, Salwayash, Dorset, DT6 5QS", "standardised_postcode": "DT6 5QS", "LONGITUDE": -2.7730072, "LATITUDE": 50.7662791}, {"uprn": 100040626501.0, "standardised_address": "12, RIVERVALE, BRIDPORT, DT6 5RN", "standardised_postcode": "DT6 5RN", "LONGITUDE": -2.7586317, "LATITUDE": 50.740939}, {"uprn": 100040621177.0, "standardised_address": "54 REDLANDS LANE, BROADWINDSOR", "standardised_postcode": "DT8 3ST", "LONGITUDE": -2.7955262, "LATITUDE": 50.8187693}, {"uprn": 10094240246.0, "standardised_address": "43, Willow Rise, Witheridge", "standardised_postcode": "EX16 8FD", "LONGITUDE": -3.6994066, "LATITUDE": 50.9120479}, {"uprn": 10004844899.0, "standardised_address": "WILTSHIRE HOUSE HOSTEL, 64, EXMOUTH STREET, KINGSHILL, SWINDON, SN1 3PU", "standardised_postcode": "SN1 3PU", "LONGITUDE": -1.7943904, "LATITUDE": 51.5551385}, {"uprn": 10094328457.0, "standardised_address": "ROOM 10, 2, SWINDON FOYER, 17-21, BATH ROAD, OLD TOWN, SWINDON, SN1 4AS", "standardised_postcode": "SN1 4AS", "LONGITUDE": -1.7778769, "LATITUDE": 51.5523398}, {"uprn": 100120980894.0, "standardised_address": "10, WINE STREET, DEVIZES, SN10 1AP", "standardised_postcode": "SN10 1AP", "LONGITUDE": -1.9947743, "LATITUDE": 51.3518889}, {"uprn": 100120977834.0, "standardised_address": "19C, MONDAY MARKET STREET, DEVIZES, SN10 1DN", "standardised_postcode": "SN10 1DN", "LONGITUDE": -1.9921895, "LATITUDE": 51.3520506}, {"uprn": 200001300706.0, "standardised_address": "39, HURRICANE ROAD, BOWERHILL, MELKSHAM, SN12 6SZ", "standardised_postcode": "SN12 6SZ", "LONGITUDE": -2.1236125, "LATITUDE": 51.3578666}, {"uprn": 200001300707.0, "standardised_address": "4, HURRICANE ROAD, BOWERHILL, MELKSHAM, SN12 6SZ", "standardised_postcode": "SN12 6SZ", "LONGITUDE": -2.1248482, "LATITUDE": 51.3580721}, {"uprn": 100121013791.0, "standardised_address": "4, SOUTHMEAD, CHIPPENHAM, SN14 0RU", "standardised_postcode": "SN14 0RU", "LONGITUDE": -2.1380925, "LATITUDE": 51.4540985}, {"uprn": 100121013888.0, "standardised_address": "134, SOUTHMEAD, CHIPPENHAM, SN14 0SB", "standardised_postcode": "SN14 0SB", "LONGITUDE": -2.1339064, "LATITUDE": 51.4547597}, {"uprn": 10010426772.0, "standardised_address": "21, CLIVE PARADE, CRICKLADE ROAD, SWINDON, SN2 1AJ", "standardised_postcode": "SN2 1AJ", "LONGITUDE": -1.7755952, "LATITUDE": 51.5844431}, {"uprn": 100121161218.0, "standardised_address": "44 Tulip Tree Close", "standardised_postcode": "SN2 1RR", "LONGITUDE": -1.782982, "LATITUDE": 51.5790352}, {"uprn": 100121344047.0, "standardised_address": "46, Harber Court, May Close", "standardised_postcode": "SN2 1XD", "LONGITUDE": -1.7793342, "LATITUDE": 51.5772052}, {"uprn": 10004840648.0, "standardised_address": "FLAT 3, 6, CAMDALE PARADE, CRICKLADE ROAD, SWINDON, SN2 8AH", "standardised_postcode": "SN2 8AH", "LONGITUDE": -1.7769189, "LATITUDE": 51.573548}, {"uprn": 10004842231.0, "standardised_address": "4, Huddleston Close", "standardised_postcode": "SN2 8BG", "LONGITUDE": -1.7727817, "LATITUDE": 51.5707465}, {"uprn": 100121141804.0, "standardised_address": "96, LENNOX DRIVE, SWINDON, SN3 3BD", "standardised_postcode": "SN3 3BD", "LONGITUDE": -1.7635782, "LATITUDE": 51.5611407}, {"uprn": 100121345610.0, "standardised_address": "14 BRAIN COURT, BUNCE ROAD, SWINDON, SN3 4QT", "standardised_postcode": "SN3 4QT", "LONGITUDE": -1.7510497, "LATITUDE": 51.5782255}, {"uprn": 100121345614.0, "standardised_address": "18 BRAIN COURT, BUNCE ROAD, SWINDON, SN3 4QT", "standardised_postcode": "SN3 4QT", "LONGITUDE": -1.7510497, "LATITUDE": 51.5782255}, {"uprn": 100121345636.0, "standardised_address": "38 BRAIN COURT, BUNCE ROAD, SWINDON, SN3 4QT", "standardised_postcode": "SN3 4QT", "LONGITUDE": -1.7510497, "LATITUDE": 51.5782255}, {"uprn": 100121345678.0, "standardised_address": "74 BRAIN COURT, BUNCE ROAD, SWINDON, SN3 4QU", "standardised_postcode": "SN3 4QU", "LONGITUDE": -1.7510497, "LATITUDE": 51.5782255}, {"uprn": 100121134536.0, "standardised_address": "6, Goulding Close", "standardised_postcode": "SN3 4QY", "LONGITUDE": -1.7486145, "LATITUDE": 51.5801714}, {"uprn": 10008541709.0, "standardised_address": "44, THORNEY PARK, WROUGHTON, SWINDON, SN4 0QS", "standardised_postcode": "SN4 0QS", "LONGITUDE": -1.7872903, "LATITUDE": 51.5116279}, {"uprn": 10008541885.0, "standardised_address": "100, Thorney Park, Wroughton", "standardised_postcode": "SN4 0QT", "LONGITUDE": -1.789516, "LATITUDE": 51.5108628}, {"uprn": 10008541880.0, "standardised_address": "105, Thorney Park, Wroughton", "standardised_postcode": "SN4 0QT", "LONGITUDE": -1.7888605, "LATITUDE": 51.5108736}, {"uprn": 10008541889.0, "standardised_address": "112, Thorney Park, Wroughton", "standardised_postcode": "SN4 0QT", "LONGITUDE": -1.7878319, "LATITUDE": 51.5108692}, {"uprn": 100121169562.0, "standardised_address": "Flat 15, Windmill Court, Uxbridge Road, Freshbrook, Swindon, SN5 8RT", "standardised_postcode": "SN5 8RT", "LONGITUDE": -1.8418052, "LATITUDE": 51.550096}, {"uprn": 100121169564.0, "standardised_address": "Flat 17, Windmill Court, Uxbridge Road, Freshbrook, Swindon, SN5 8RT", "standardised_postcode": "SN5 8RT", "LONGITUDE": -1.8418052, "LATITUDE": 51.550096}, {"uprn": 100121055967.0, "standardised_address": "93-95, ST. EDMUNDS CHURCH STREET, SALISBURY, SP1 1EQ", "standardised_postcode": "SP1 1EQ", "LONGITUDE": -1.7924578, "LATITUDE": 51.0713354}, {"uprn": 100121044735.0, "standardised_address": "44 Glyndebourne Close", "standardised_postcode": "SP2 9EY", "LONGITUDE": -1.8269895, "LATITUDE": 51.0812824}, {"uprn": 200001120569.0, "standardised_address": "10 COOKS CLOSE, SALISBURY", "standardised_postcode": "SP2 9PS", "LONGITUDE": -1.8317301, "LATITUDE": 51.0888336}, {"uprn": 100121047096.0, "standardised_address": "26, Hops Close, Chilmark", "standardised_postcode": "SP3 5BE", "LONGITUDE": -2.0471076, "LATITUDE": 51.0932517}, {"uprn": 100121046662.0, "standardised_address": "12, Hill Close, Tisbury", "standardised_postcode": "SP3 6TB", "LONGITUDE": -2.0788121, "LATITUDE": 51.0669769}, {"uprn": 100121046667.0, "standardised_address": "23, Hill Close, Tisbury", "standardised_postcode": "SP3 6TB", "LONGITUDE": -2.0786986, "LATITUDE": 51.0671501}, {"uprn": 100121046668.0, "standardised_address": "25, Hill Close, Tisbury", "standardised_postcode": "SP3 6TB", "LONGITUDE": -2.0786213, "LATITUDE": 51.0671395}, {"uprn": 100120987378.0, "standardised_address": "113, HIGH STREET, NETHERAVON, SALISBURY, SP4 9PJ", "standardised_postcode": "SP4 9PJ", "LONGITUDE": -1.7907184, "LATITUDE": 51.2385674}, {"uprn": 100121055592.0, "standardised_address": "40, SPIDERS ISLAND, ALDERBURY, WILTSHIRE, SP5 3BG", "standardised_postcode": "SP5 3BG", "LONGITUDE": -1.7204383, "LATITUDE": 51.0379501}, {"uprn": 100121045369.0, "standardised_address": "11, GRIMSTEAD ROAD, WHADDON, SALISBURY, SP5 3EE", "standardised_postcode": "SP5 3EE", "LONGITUDE": -1.7206544, "LATITUDE": 51.0375909}, {"uprn": 100121045372.0, "standardised_address": "19, Grimstead Road, Whaddon", "standardised_postcode": "SP5 3EE", "LONGITUDE": -1.7204116, "LATITUDE": 51.0376623}, {"uprn": 200002927526.0, "standardised_address": "6, Stanley Close, Bishopstone", "standardised_postcode": "SP5 4BH", "LONGITUDE": -1.9035983, "LATITUDE": 51.0312746}, {"uprn": 10010447328.0, "standardised_address": "17 HILLVIEW, EBBESBOURNE WAKE", "standardised_postcode": "SP5 5JJ", "LONGITUDE": -2.0155382, "LATITUDE": 51.0160085}, {"uprn": 100040690924.0, "standardised_address": "2, Lime Tree Close, Alderholt", "standardised_postcode": "SP6 3RQ", "LONGITUDE": -1.8334067, "LATITUDE": 50.9123707}, {"uprn": 100040900005.0, "standardised_address": "26 TURNER CLOSE, BRIDGWATER", "standardised_postcode": "TA6 3PA", "LONGITUDE": -2.9996053, "LATITUDE": 51.1249647}, {"uprn": 10002989537.0, "standardised_address": "CARTREF, WINNINGS WAY, TORQUAY, TQ1 3GZ", "standardised_postcode": "TQ1 3GZ", "LONGITUDE": -3.513879, "LATITUDE": 50.4785659}, {"uprn": 100061879598.0, "standardised_address": "11 Blackdown Road", "standardised_postcode": "BN13 2EZ", "LONGITUDE": -0.4017112, "LATITUDE": 50.8371451}, {"uprn": 100061879600.0, "standardised_address": "14, BLACKDOWN ROAD, WORTHING, BN13 2EZ", "standardised_postcode": "BN13 2EZ", "LONGITUDE": -0.401944, "LATITUDE": 50.8371204}, {"uprn": 100061899941.0, "standardised_address": "25, QUANTOCK ROAD, WORTHING, BN13 2HQ", "standardised_postcode": "BN13 2HQ", "LONGITUDE": -0.4024977, "LATITUDE": 50.8375507}, {"uprn": 100062191147.0, "standardised_address": "40 Hurst Cottages, East Street, Amberley, Horsham, BN18 9NP", "standardised_postcode": "BN18 9NP", "LONGITUDE": -0.5285661, "LATITUDE": 50.9086612}, {"uprn": 100062191157.0, "standardised_address": "50 Hurst Cottages, East Street, Amberley, Horsham, BN18 9NP", "standardised_postcode": "BN18 9NP", "LONGITUDE": -0.5284547, "LATITUDE": 50.9096733}, {"uprn": 200002880107.0, "standardised_address": "9a Hurst Cottages, East Street, Amberley, Horsham, BN18 9NP", "standardised_postcode": "BN18 9NP", "LONGITUDE": -0.5298463, "LATITUDE": 50.9097002}, {"uprn": 100061911692.0, "standardised_address": "Flat 4, Highland Lodge, 17 Carew Road, Eastbourne, BN21 2JQ", "standardised_postcode": "BN21 2JQ", "LONGITUDE": 0.2759205, "LATITUDE": 50.7765092}, {"uprn": 10010653970.0, "standardised_address": "3 GLADSTONE CLOSE, EASTBOURNE", "standardised_postcode": "BN22 9BP", "LONGITUDE": 0.2792262, "LATITUDE": 50.7975267}, {"uprn": 100061914793.0, "standardised_address": "Flat 2, Lakeside Court, 6 Lakelands Close, Eastbourne, BN22 9EQ", "standardised_postcode": "BN22 9EQ", "LONGITUDE": 0.2878669, "LATITUDE": 50.7989404}, {"uprn": 10010655892.0, "standardised_address": "5, Britten Close", "standardised_postcode": "BN23 7TR", "LONGITUDE": 0.3111695, "LATITUDE": 50.8061167}, {"uprn": 10010655608.0, "standardised_address": "5, Laughton Close", "standardised_postcode": "BN23 8JU", "LONGITUDE": 0.287275, "LATITUDE": 50.8121013}, {"uprn": 100060019188.0, "standardised_address": "23 Rotherfield Avenue", "standardised_postcode": "BN23 8JZ", "LONGITUDE": 0.2887367, "LATITUDE": 50.8109304}, {"uprn": 100061916123.0, "standardised_address": "12 The Rookery, Eastbourne, BN23 8LD", "standardised_postcode": "BN23 8LD", "LONGITUDE": 0.3010123, "LATITUDE": 50.8114353}, {"uprn": 10004614538.0, "standardised_address": "10 BUTTS FIELD, HAILSHAM", "standardised_postcode": "BN27 2BZ", "LONGITUDE": 0.2641057, "LATITUDE": 50.8558489}, {"uprn": 10004614505.0, "standardised_address": "1 BUTTS FIELD, HAILSHAM", "standardised_postcode": "BN27 2BZ", "LONGITUDE": 0.2630997, "LATITUDE": 50.8564551}, {"uprn": 22136962.0, "standardised_address": "16, BLUEBIRD COURT 12-14, HOVE STREET, HOVE, BN3 2TU", "standardised_postcode": "BN3 2TU", "LONGITUDE": -0.1806456, "LATITUDE": 50.826389}, {"uprn": 100062006747.0, "standardised_address": "19 Nelson House, Short Street, Rushmoor, GU11 1HX", "standardised_postcode": "GU11 1HX", "LONGITUDE": -0.7667619, "LATITUDE": 51.2496261}, {"uprn": 100062006748.0, "standardised_address": "20 Nelson House, Short Street, Rushmoor, GU11 1HX", "standardised_postcode": "GU11 1HX", "LONGITUDE": -0.7667619, "LATITUDE": 51.2496261}, {"uprn": 100060533152.0, "standardised_address": "5, Raglan Close", "standardised_postcode": "GU12 4PG", "LONGITUDE": -0.7516103, "LATITUDE": 51.2440431}, {"uprn": 100061765042.0, "standardised_address": "1 RANVILLE CLOSE, PETWORTH", "standardised_postcode": "GU28 0EN", "LONGITUDE": -0.6124007, "LATITUDE": 50.9814381}, {"uprn": 100061762685.0, "standardised_address": "21 JUNE MEADOWS, MIDHURST", "standardised_postcode": "GU29 9ER", "LONGITUDE": -0.7509855, "LATITUDE": 50.9904527}, {"uprn": 100061762706.0, "standardised_address": "42 JUNE MEADOWS, MIDHURST", "standardised_postcode": "GU29 9ER", "LONGITUDE": -0.7517059, "LATITUDE": 50.9906852}, {"uprn": 200001064783.0, "standardised_address": "17 Chestnut Close", "standardised_postcode": "GU29 9TT", "LONGITUDE": -0.7473381, "LATITUDE": 50.9766817}, {"uprn": 200001064789.0, "standardised_address": "2, HORNBEAM WAY, MIDHURST, GU29 9TU", "standardised_postcode": "GU29 9TU", "LONGITUDE": -0.7483841, "LATITUDE": 50.9769806}, {"uprn": 100062161165.0, "standardised_address": "Flat 7, Brook House, Park Drive, Waverley, GU6 7EH", "standardised_postcode": "GU6 7EH", "LONGITUDE": -0.4741177, "LATITUDE": 51.1453322}, {"uprn": 100062367433.0, "standardised_address": "COPTHORNE COTTAGE, BRIGHTON ROAD, KINGSWOOD, TADWORTH, KT20 6BQ", "standardised_postcode": "KT20 6BQ", "LONGITUDE": -0.2205069, "LATITUDE": 51.3005374}, {"uprn": 100062145269.0, "standardised_address": "Flat 1, Copthorne House, Brighton Road, Kingswood, Reigate and Banstead, KT20 6BQ", "standardised_postcode": "KT20 6BQ", "LONGITUDE": -0.2203005, "LATITUDE": 51.3003094}, {"uprn": 100062145274.0, "standardised_address": "Flat 6, Copthorne House, Brighton Road, Kingswood, Reigate and Banstead, KT20 6BQ", "standardised_postcode": "KT20 6BQ", "LONGITUDE": -0.2203005, "LATITUDE": 51.3003094}, {"uprn": 100062145280.0, "standardised_address": "Flat 11, Copthorne House, Brighton Road, Kingswood, Reigate and Banstead, KT20 6BQ", "standardised_postcode": "KT20 6BQ", "LONGITUDE": -0.22026, "LATITUDE": 51.3006146}, {"uprn": 10007059816.0, "standardised_address": "2, OLD ST. MARYS, WEST HORSLEY, LEATHERHEAD, KT24 6JG", "standardised_postcode": "KT24 6JG", "LONGITUDE": -0.4571645, "LATITUDE": 51.2643896}, {"uprn": 100120914390.0, "standardised_address": "42, Wordsworth Road", "standardised_postcode": "OX14 5NX", "LONGITUDE": -1.2998371, "LATITUDE": 51.6633308}, {"uprn": 10011922279.0, "standardised_address": "60, Goldings Road, Hook Norton", "standardised_postcode": "OX15 5FG", "LONGITUDE": -1.4854983, "LATITUDE": 52.0000434}, {"uprn": 200001511761.0, "standardised_address": "78G, PARK STREET, THAME, OX9 3HX", "standardised_postcode": "OX9 3HX", "LONGITUDE": -0.9726332, "LATITUDE": 51.744209}, {"uprn": 100061693227.0, "standardised_address": "65, ESSEX ROAD, BOGNOR REGIS, PO21 2BY", "standardised_postcode": "PO21 2BY", "LONGITUDE": -0.6808471, "LATITUDE": 50.7923111}, {"uprn": 100061699117.0, "standardised_address": "64A, LINDEN ROAD, BOGNOR REGIS, PO21 2DT", "standardised_postcode": "PO21 2DT", "LONGITUDE": -0.6799074, "LATITUDE": 50.7883884}, {"uprn": 100061694176.0, "standardised_address": "22, FLETCHER WAY, BOGNOR REGIS, PO21 2NU", "standardised_postcode": "PO21 2NU", "LONGITUDE": -0.6808653, "LATITUDE": 50.7916638}, {"uprn": 100061694158.0, "standardised_address": "3, FLETCHER WAY, BOGNOR REGIS, PO21 2NU", "standardised_postcode": "PO21 2NU", "LONGITUDE": -0.6814028, "LATITUDE": 50.7922275}, {"uprn": 100061708497.0, "standardised_address": "66A, VICTORIA DRIVE, BOGNOR REGIS, PO21 2TG", "standardised_postcode": "PO21 2TG", "LONGITUDE": -0.6821079, "LATITUDE": 50.7868215}, {"uprn": 100061692981.0, "standardised_address": "7, ELM TREE CLOSE, BOGNOR REGIS, PO21 5BF", "standardised_postcode": "PO21 5BF", "LONGITUDE": -0.6893863, "LATITUDE": 50.7974796}, {"uprn": 100061688879.0, "standardised_address": "32, Birdham Close", "standardised_postcode": "PO21 5TD", "LONGITUDE": -0.6958816, "LATITUDE": 50.7960865}, {"uprn": 100061707577.0, "standardised_address": "32, The Croft", "standardised_postcode": "PO21 5TH", "LONGITUDE": -0.6968881, "LATITUDE": 50.7950995}, {"uprn": 1775024104.0, "standardised_address": "15, EDENBRIDGE ROAD, SOUTHSEA, PO4 8PE", "standardised_postcode": "PO4 8PE", "LONGITUDE": -1.0477756, "LATITUDE": 50.7983904}, {"uprn": 310063074.0, "standardised_address": "2, OPAL COURT, LOWER FIELD ROAD, READING, RG1 6BW", "standardised_postcode": "RG1 6BW", "LONGITUDE": -0.97969, "LATITUDE": 51.4489452}, {"uprn": 310006357.0, "standardised_address": "Flat 1, Galloway House, Rembrandt Way, Reading, RG1 6QU", "standardised_postcode": "RG1 6QU", "LONGITUDE": -0.9915054, "LATITUDE": 51.4449921}, {"uprn": 310056378.0, "standardised_address": "112, ADMIRALS COURT, ROSE KILN LANE, READING, RG1 6SS", "standardised_postcode": "RG1 6SS", "LONGITUDE": -0.9773714, "LATITUDE": 51.4470907}, {"uprn": 200004733000.0, "standardised_address": "Flat 6, Lynton Court, Pelican Lane, West Berkshire, RG14 1NN", "standardised_postcode": "RG14 1NN", "LONGITUDE": -1.3238052, "LATITUDE": 51.4073996}, {"uprn": 200004733014.0, "standardised_address": "Flat 20, Lynton Court, Pelican Lane, West Berkshire, RG14 1NN", "standardised_postcode": "RG14 1NN", "LONGITUDE": -1.3242952, "LATITUDE": 51.4073215}, {"uprn": 10007903996.0, "standardised_address": "11 Donnington Lodge, Oxford Road, Donnington, West Berkshire, RG14 3AA", "standardised_postcode": "RG14 3AA", "LONGITUDE": -1.3286281, "LATITUDE": 51.4177589}, {"uprn": 100081226752.0, "standardised_address": "23 DONNINGTON LODGE, OXFORD ROAD, DONNINGTON, NEWBURY, RG14 3AA", "standardised_postcode": "RG14 3AA", "LONGITUDE": -1.3287324, "LATITUDE": 51.4175077}, {"uprn": 310001598.0, "standardised_address": "14, Hagley Road", "standardised_postcode": "RG2 0DN", "LONGITUDE": -0.9682675, "LATITUDE": 51.4411482}, {"uprn": 310032425.0, "standardised_address": "26, Hagley Road", "standardised_postcode": "RG2 0DN", "LONGITUDE": -0.9686028, "LATITUDE": 51.4409533}, {"uprn": 100080226372.0, "standardised_address": "3, Butts Furlong, Brightwalton", "standardised_postcode": "RG20 7DH", "LONGITUDE": -1.3881378, "LATITUDE": 51.5113557}, {"uprn": 100062458850.0, "standardised_address": "3 DAYS MEADOW, WOOLTON HILL", "standardised_postcode": "RG20 9US", "LONGITUDE": -1.3912259, "LATITUDE": 51.35076}, {"uprn": 100060223249.0, "standardised_address": "9, DANKWORTH ROAD, BASINGSTOKE, RG22 4LJ", "standardised_postcode": "RG22 4LJ", "LONGITUDE": -1.120265, "LATITUDE": 51.2424042}, {"uprn": 100060223353.0, "standardised_address": "133, Dankworth Road", "standardised_postcode": "RG22 4LJ", "LONGITUDE": -1.120787, "LATITUDE": 51.2420754}, {"uprn": 100060225935.0, "standardised_address": "36, Foxs Furlong, Chineham", "standardised_postcode": "RG24 8WN", "LONGITUDE": -1.0443702, "LATITUDE": 51.2967794}, {"uprn": 10001320962.0, "standardised_address": "17, Longs Court", "standardised_postcode": "RG28 7BU", "LONGITUDE": -1.3400448, "LATITUDE": 51.2300347}, {"uprn": 310009363.0, "standardised_address": "38, STRATHY CLOSE, READING, RG30 2PP", "standardised_postcode": "RG30 2PP", "LONGITUDE": -1.0145665, "LATITUDE": 51.4587161}, {"uprn": 310048525.0, "standardised_address": "21, COLLIERS WAY, READING, RG30 2QS", "standardised_postcode": "RG30 2QS", "LONGITUDE": -1.0103715, "LATITUDE": 51.4549271}, {"uprn": 310059914.0, "standardised_address": "42, COLLIERS WAY, READING, RG30 2QT", "standardised_postcode": "RG30 2QT", "LONGITUDE": -1.0103342, "LATITUDE": 51.4553225}, {"uprn": 310008254.0, "standardised_address": "3 Brook Lea, Caversham, Reading, RG4 8EP", "standardised_postcode": "RG4 8EP", "LONGITUDE": -0.9594228, "LATITUDE": 51.464404}, {"uprn": 310040251.0, "standardised_address": "7 Brook Lea, Caversham, Reading, RG4 8EP", "standardised_postcode": "RG4 8EP", "LONGITUDE": -0.9588334, "LATITUDE": 51.4643718}, {"uprn": 100121306362.0, "standardised_address": "4, Smith Close, Sonning Common", "standardised_postcode": "RG4 9TL", "LONGITUDE": -0.9851249, "LATITUDE": 51.5186171}, {"uprn": 14049181.0, "standardised_address": "3, Kendrick Close", "standardised_postcode": "RG40 2LZ", "LONGITUDE": -0.8363485, "LATITUDE": 51.4070013}, {"uprn": 100080247057.0, "standardised_address": "12, The Glebe, Aldworth", "standardised_postcode": "RG8 9SH", "LONGITUDE": -1.1981048, "LATITUDE": 51.5116775}, {"uprn": 100080247061.0, "standardised_address": "17, The Glebe, Aldworth", "standardised_postcode": "RG8 9SH", "LONGITUDE": -1.1985385, "LATITUDE": 51.5115996}, {"uprn": 100061824929.0, "standardised_address": "16, VINALL GARDENS, OLD GUILDFORD ROAD, BROADBRIDGE HEATH, HORSHAM, RH12 3HX", "standardised_postcode": "RH12 3HX", "LONGITUDE": -0.3640146, "LATITUDE": 51.0719231}, {"uprn": 10003085392.0, "standardised_address": "18 St. Marks Lane, Horsham, RH12 5PU", "standardised_postcode": "RH12 5PU", "LONGITUDE": -0.3145139, "LATITUDE": 51.0829412}, {"uprn": 100062482574.0, "standardised_address": "Flat 1, Wigmore House, Keymer Road, Mid Sussex, RH15 0AH", "standardised_postcode": "RH15 0AH", "LONGITUDE": -0.1263008, "LATITUDE": 50.9487666}, {"uprn": 100062483148.0, "standardised_address": "12 OAKENFIELD, BURGESS HILL", "standardised_postcode": "RH15 8SJ", "LONGITUDE": -0.1355338, "LATITUDE": 50.9656152}, {"uprn": 100062483149.0, "standardised_address": "14 OAKENFIELD, BURGESS HILL", "standardised_postcode": "RH15 8SJ", "LONGITUDE": -0.1360491, "LATITUDE": 50.9655873}, {"uprn": 100062483150.0, "standardised_address": "15, Oakenfield", "standardised_postcode": "RH15 8SJ", "LONGITUDE": -0.1361846, "LATITUDE": 50.9655929}, {"uprn": 100061832309.0, "standardised_address": "9B STANE STREET CLOSE, CODMORE HILL, PULBOROUGH", "standardised_postcode": "RH20 1BD", "LONGITUDE": -0.5026947, "LATITUDE": 50.9661759}, {"uprn": 100061831182.0, "standardised_address": "3, Piers Secomb Close, Coldwaltham", "standardised_postcode": "RH20 1QA", "LONGITUDE": -0.5421865, "LATITUDE": 50.9370885}, {"uprn": 100061826991.0, "standardised_address": "57, Beech Grove, Storrington", "standardised_postcode": "RH20 3NP", "LONGITUDE": -0.4416205, "LATITUDE": 50.9245994}, {"uprn": 100062512568.0, "standardised_address": "FLAT 41, KERRIGAN COURT, 16, WESTWOOD ROAD, SOUTHAMPTON, SO17 1JT", "standardised_postcode": "SO17 1JT", "LONGITUDE": -1.4022734, "LATITUDE": 50.9223801}, {"uprn": 100062512088.0, "standardised_address": "FLAT 6, RAGLAN COURT, 11, WINN ROAD, SOUTHAMPTON, SO17 1WU", "standardised_postcode": "SO17 1WU", "LONGITUDE": -1.4025758, "LATITUDE": 50.9243151}, {"uprn": 10034867337.0, "standardised_address": "6a, Epping Close", "standardised_postcode": "SO18 5SE", "LONGITUDE": -1.3526289, "LATITUDE": 50.9247678}, {"uprn": 100060305293.0, "standardised_address": "21, MOUNTBATTEN ROAD, EASTLEIGH, SO50 4RQ", "standardised_postcode": "SO50 4RQ", "LONGITUDE": -1.3550759, "LATITUDE": 50.9819271}, {"uprn": 100061985990.0, "standardised_address": "Flat 10, Raglan Court, Mountbatten Road, Eastleigh, SO50 4RR", "standardised_postcode": "SO50 4RR", "LONGITUDE": -1.3541953, "LATITUDE": 50.9822382}, {"uprn": 200000713806.0, "standardised_address": "2 NUTBANE CLOSE, ANDOVER", "standardised_postcode": "SP10 3WA", "LONGITUDE": -1.5003561, "LATITUDE": 51.2059018}, {"uprn": 100060566549.0, "standardised_address": "9, Vespasian Road", "standardised_postcode": "SP10 5JP", "LONGITUDE": -1.476821, "LATITUDE": 51.2249974}, {"uprn": 100060566550.0, "standardised_address": "10, VESPASIAN ROAD, ANDOVER, SP10 5JP", "standardised_postcode": "SP10 5JP", "LONGITUDE": -1.4767158, "LATITUDE": 51.2254375}, {"uprn": 100060566565.0, "standardised_address": "25, VESPASIAN ROAD, ANDOVER, SP10 5JP", "standardised_postcode": "SP10 5JP", "LONGITUDE": -1.4776377, "LATITUDE": 51.224965}, {"uprn": 100060563379.0, "standardised_address": "52, ROMAN WAY, ANDOVER, SP10 5JU", "standardised_postcode": "SP10 5JU", "LONGITUDE": -1.4764479, "LATITUDE": 51.2250676}, {"uprn": 100060049602.0, "standardised_address": "5, Salisbury Road", "standardised_postcode": "TN37 6RX", "LONGITUDE": 0.5616196, "LATITUDE": 50.862757}] \ No newline at end of file diff --git a/etl/customers/stonewater/map_app/callbacks.py b/etl/customers/stonewater/map_app/callbacks.py new file mode 100644 index 00000000..e69de29b diff --git a/etl/customers/stonewater/map_app/config.py b/etl/customers/stonewater/map_app/config.py new file mode 100644 index 00000000..1dbd5d04 --- /dev/null +++ b/etl/customers/stonewater/map_app/config.py @@ -0,0 +1,8 @@ +import os +import json +import dotenv + +# When running locally, we'll need to load the .env file +dotenv.load_dotenv() + +MAPBOX_ACCESS_TOKEN = os.getenv("MAPBOX_ACCESS_TOKEN") diff --git a/etl/customers/stonewater/map_app/map_page.py b/etl/customers/stonewater/map_app/map_page.py new file mode 100644 index 00000000..c39a53af --- /dev/null +++ b/etl/customers/stonewater/map_app/map_page.py @@ -0,0 +1,94 @@ +import dash_bootstrap_components as dbc +from dash import html, dcc +import json +import plotly.graph_objects as go +import pandas as pd + +from config import MAPBOX_ACCESS_TOKEN + + +def make_map(locations): + if not locations: + return None + + df = pd.DataFrame(locations) + + # Create custom hover text + df['hover_text'] = df.apply( + lambda row: f"UPRN: {int(row['uprn'])}
Address: {row['standardised_address']}
Postcode: " + f"{row['standardised_postcode']}
Latitude: {row['LATITUDE']}
Longitude: {row['LONGITUDE']}", + axis=1) + + data = [ + go.Scattermapbox( + lat=df["LATITUDE"].tolist(), + lon=df["LONGITUDE"].tolist(), + mode="markers", + marker=go.scattermapbox.Marker(size=10, color="#027fa6"), + text=df["hover_text"], # Use the custom hover text + hoverinfo='text' + ) + ] + + layout = go.Layout( + autosize=True, + hovermode="closest", + mapbox=go.layout.Mapbox( + accesstoken=MAPBOX_ACCESS_TOKEN, + bearing=0, + center=go.layout.mapbox.Center(lat=53, lon=-1.5), + pitch=0, + zoom=4, + ), + margin={"t": 0}, + ) + + fig = go.Figure(data=data, layout=layout) + + plot = dcc.Graph(figure=fig, config={"displayModeBar": False}) + + return plot + + +def layout(): + # Get the data + with open("Stonewater Mapping Data.json", "r") as file: + locations = json.load(file) + + page = dbc.Container( + [ + dbc.Row( + dbc.Col( + html.Div( + [ + html.H1( + "Stonewater Survey Map", + style={"font-size": "2.5rem", "font-weight": "bold", "margin-bottom": "20px"} + ), + html.P( + "This map shows the location of the properties that are to be surveyed by Osmosis.", + style={"font-size": "1.25rem", "margin-bottom": "40px"} + ), + ], + className="text-center" + ), + width=12 + ), + className="mt-5" + ), + dbc.Row( + dbc.Col( + make_map(locations=locations), + width=12, + align="center", + className="text-center" + ), + className="metric-row", + justify="center" + ) + ], + fluid=True, + className="p-5" + ) + + return page diff --git a/etl/customers/stonewater/map_app/requirements.txt b/etl/customers/stonewater/map_app/requirements.txt new file mode 100644 index 00000000..81943dd1 --- /dev/null +++ b/etl/customers/stonewater/map_app/requirements.txt @@ -0,0 +1,12 @@ +dash==2.8.1 +gunicorn +pandas +dash-bootstrap-components==1.3.1 +boto3 +dropbox +Flask-Caching +dash-extensions +mysql-connector-python +sqlalchemy +werkzeug==2.3.7 +python-dotenv \ No newline at end of file diff --git a/etl/customers/stonewater/map_app/server.py b/etl/customers/stonewater/map_app/server.py new file mode 100644 index 00000000..87f10e21 --- /dev/null +++ b/etl/customers/stonewater/map_app/server.py @@ -0,0 +1,45 @@ +import logging +import secrets + +import dash_bootstrap_components as dbc +from dash import html +from dash_extensions.enrich import DashProxy, MultiplexerTransform +import flask +from map_page import layout + +logger = logging.getLogger(__name__) + +# We just use a simple secret key for the moment + +SECRET_KEY = secrets.token_hex(24) + + +def init_app(): + app = DashProxy( + __name__, + server=flask.Flask(__name__), + suppress_callback_exceptions=True, + external_stylesheets=[ + dbc.themes.BOOTSTRAP, + dbc.icons.FONT_AWESOME, + "https://fonts.googleapis.com/css?family=Comfortaa", + ], + transforms=[MultiplexerTransform()] + ) + + server = app.server + + # Set app config + server.config.update( + SECRET_KEY=SECRET_KEY, + ) + + app.title = "Hesta X Stonewater" + + # Define the layout + app.layout = layout() + + return app + + +app = init_app() diff --git a/etl/customers/stonewater/map_app/wsgi.py b/etl/customers/stonewater/map_app/wsgi.py new file mode 100644 index 00000000..3390e6ff --- /dev/null +++ b/etl/customers/stonewater/map_app/wsgi.py @@ -0,0 +1,8 @@ +# Callbacks must be imported to run the app +import callbacks # NOQA +from server import app + +application = app.server + +if __name__ == "__main__": + app.run_server(port=8080, debug=True, host="0.0.0.0") From f6adb3619bdb19da43d856ee71fcaa70d09cbacb Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Mon, 1 Jul 2024 11:26:18 +0100 Subject: [PATCH 7/9] Added some logo and styling to app --- .../stonewater/map_app/assets/hestia-logo.png | Bin 0 -> 17967 bytes .../map_app/assets/osmosis-Logo.svg | 1 + .../map_app/assets/stonewater-logo.png | Bin 0 -> 19000 bytes etl/customers/stonewater/map_app/map_page.py | 39 +++++++++++++++++- 4 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 etl/customers/stonewater/map_app/assets/hestia-logo.png create mode 100644 etl/customers/stonewater/map_app/assets/osmosis-Logo.svg create mode 100644 etl/customers/stonewater/map_app/assets/stonewater-logo.png diff --git a/etl/customers/stonewater/map_app/assets/hestia-logo.png b/etl/customers/stonewater/map_app/assets/hestia-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..8a49c95b9671ffce2f6ceff29c7bcec766939bd1 GIT binary patch literal 17967 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

cMODdC<9Cysd9<&GP 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 \ No newline at end of file diff --git a/etl/customers/stonewater/map_app/assets/stonewater-logo.png b/etl/customers/stonewater/map_app/assets/stonewater-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..0d05f58ff1486c7a0a9e1b8d09fd4b187de0c133 GIT binary patch literal 19000 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 literal 0 HcmV?d00001 diff --git a/etl/customers/stonewater/map_app/map_page.py b/etl/customers/stonewater/map_app/map_page.py index c39a53af..4fa2406c 100644 --- a/etl/customers/stonewater/map_app/map_page.py +++ b/etl/customers/stonewater/map_app/map_page.py @@ -61,6 +61,43 @@ def layout(): dbc.Col( html.Div( [ + # Banner with logos + dbc.Row( + [ + dbc.Col( + html.Img(src="assets/stonewater-logo.png", height="50px"), + width="auto" + ), + dbc.Col( + html.Img(src="assets/osmosis-Logo.svg", height="50px"), + width="auto" + ), + dbc.Col( + html.Div( + style={"color": "white", "font-size": "1.5rem", "font-weight": "bold"} + ), + width=True, + className="text-center" + ) + ], + className="align-items-center", + style={"background-color": "#027fa6", "padding": "10px"} + ), + dbc.Row( + [ + dbc.Col("Powered by", style={"color": "#027fa6", "fontSize": "1rem", 'zIndex': 10}, width="auto"), + dbc.Col( + html.A( + html.Img(src="assets/hestia-logo.png", height="50px"), + href="https://hestia.homes", + ), + width="auto", + style={"margin-left": "-60px"} + ), + ], + justify='left', + align="center" + ), html.H1( "Stonewater Survey Map", style={"font-size": "2.5rem", "font-weight": "bold", "margin-bottom": "20px"} @@ -79,7 +116,7 @@ def layout(): dbc.Row( dbc.Col( make_map(locations=locations), - width=12, + width=10, align="center", className="text-center" ), From c5693289c31e3185d5c65182ce599119c928c627 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Mon, 1 Jul 2024 13:25:51 +0100 Subject: [PATCH 8/9] preparing mapping page --- etl/customers/stonewater/map_app/map_page.py | 105 ++++++++++++++++++- 1 file changed, 102 insertions(+), 3 deletions(-) diff --git a/etl/customers/stonewater/map_app/map_page.py b/etl/customers/stonewater/map_app/map_page.py index 4fa2406c..bb85961e 100644 --- a/etl/customers/stonewater/map_app/map_page.py +++ b/etl/customers/stonewater/map_app/map_page.py @@ -7,6 +7,43 @@ import pandas as pd from config import MAPBOX_ACCESS_TOKEN +def make_real_epc_piechart(real_epc_breakdown): + labels = [x["is_real_epc"] for x in real_epc_breakdown] + values = [x["count"] for x in real_epc_breakdown] + + marker_colors = ["#027fa6", "rgb(225 225 225)"] + + fig = go.Figure( + data=[go.Pie(labels=labels, values=values, marker_colors=marker_colors)], + ) + + fig.update_layout(margin={"t": 0}) + + plot = dcc.Graph(figure=fig, config={"displayModeBar": False}) + + return plot + + +def make_epc_rating_piechart(epc_rating_breakdown): + # Re-order from G to A + epc_rating_breakdown = sorted(epc_rating_breakdown, key=lambda x: x["EPC"]) + + labels = [x["EPC"] for x in epc_rating_breakdown] + values = [x["count"] for x in epc_rating_breakdown] + + marker_colors = ["#117d58", "#2da55c", "#8dbd40", "#f7cd14", "#f3a96a", "#ef8026", "#e41e3b"] + + fig = go.Figure( + data=[go.Pie(labels=labels, values=values, marker_colors=marker_colors, sort=False)], + ) + + fig.update_layout(margin={"t": 0}) + + plot = dcc.Graph(figure=fig, config={"displayModeBar": False}) + + return plot + + def make_map(locations): if not locations: return None @@ -38,7 +75,7 @@ def make_map(locations): bearing=0, center=go.layout.mapbox.Center(lat=53, lon=-1.5), pitch=0, - zoom=4, + zoom=5, ), margin={"t": 0}, ) @@ -55,6 +92,14 @@ def layout(): with open("Stonewater Mapping Data.json", "r") as file: locations = json.load(file) + # Get the EPC breakdown data + with open("Stonewater real EPC breakdown.json") as file: + real_epc_breakdown = json.load(file) + + # Get the EPC ratings data + with open("Stonewater EPC rating breakdown.json") as file: + epc_rating_breakdown = json.load(file) + page = dbc.Container( [ dbc.Row( @@ -85,7 +130,8 @@ def layout(): ), dbc.Row( [ - dbc.Col("Powered by", style={"color": "#027fa6", "fontSize": "1rem", 'zIndex': 10}, width="auto"), + dbc.Col("Powered by", style={"color": "#027fa6", "fontSize": "1rem", 'zIndex': 10}, + width="auto"), dbc.Col( html.A( html.Img(src="assets/hestia-logo.png", height="50px"), @@ -120,7 +166,60 @@ def layout(): align="center", className="text-center" ), - className="metric-row", + justify="center" + ), + dbc.Row( + [ + dbc.Col( + [ + html.Div( + "Breakdown of real EPCs", + style={"fontSize": "1.5rem", "fontWeight": "bold", "marginBottom": "1em"}, + className='text-center' + ), + html.Div( + "This pie chart shows the proportion of real EPCs in the asset list. Currently, " + "there are EPCs for 3736 of the 5245 properties that have a UPRN in the asset list", + style={"marginBottom": "1em"} + ), + make_real_epc_piechart(real_epc_breakdown), + ], + width={"size": 5}, + ), + dbc.Col( + [ + html.Div( + "EPC Ratings for properties with an EPC", + style={"fontSize": "1.5rem", "fontWeight": "bold", "marginBottom": "1em"}, + className='text-center' + ), + html.Div( + [ + "This pie chart shows the breakdown of EPC ratings, for properties that currently " + "have an EPC. " + "The ratings range from A to G, where surprisingly, there are two EPC properties " + "that were initially " + "expected by Parity's modelled SAP, to be EPC D or below. These properties can be" + " seen ", + html.A("here", + href="https://find-energy-certificate.service.gov.uk/energy-certificate" + "/2708-5001-7327-6090-7284", + target="_blank"), + " and ", + html.A("here", + href="https://find-energy-certificate.service.gov.uk/energy-certificate" + "/1037-4032-1009-0361-7292", + target="_blank"), + "." + ], + style={"marginBottom": "1em"} + ), + make_epc_rating_piechart(epc_rating_breakdown), + ], + + width={"size": 5}, + ), + ], justify="center" ) ], From 51333ff31a5b89d1766342723f8a9408f324de7b Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Mon, 1 Jul 2024 13:35:00 +0100 Subject: [PATCH 9/9] minor --- .idea/misc.xml | 3 + .../stonewater/outputs 27th June 2024.py | 91 +++++++++++++++++-- etl/customers/stonewater/shdf_3_clustering.py | 9 +- 3 files changed, 93 insertions(+), 10 deletions(-) diff --git a/.idea/misc.xml b/.idea/misc.xml index 1122b380..78660f34 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -4,6 +4,9 @@