From fb6ab43b76c6e40b9ccc6d263fc040985fb63034 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Thu, 18 Apr 2024 11:07:15 +0100 Subject: [PATCH 1/3] minor initial scoping --- .idea/Model.iml | 2 +- .idea/misc.xml | 2 +- etl/customers/gla_croydon_demo/asset_list.py | 3 --- .../vander_elliot/initial_scoping.py | 23 +++++++++++++++++++ 4 files changed, 25 insertions(+), 5 deletions(-) create mode 100644 etl/customers/vander_elliot/initial_scoping.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/gla_croydon_demo/asset_list.py b/etl/customers/gla_croydon_demo/asset_list.py index 7dde8926..52e9422c 100644 --- a/etl/customers/gla_croydon_demo/asset_list.py +++ b/etl/customers/gla_croydon_demo/asset_list.py @@ -34,9 +34,6 @@ def app(): low_memory=False ) - z = epc_data.groupby(["WALLS_DESCRIPTION", "WALLS_ENERGY_EFF"]).size().reset_index(name="count") - z = z[z["MAINHEAT_DESCRIPTION"] == "Boiler and radiators, mains gas"] - # Filter on entries where we have a UPRN epc_data = epc_data[~pd.isnull(epc_data["UPRN"])] diff --git a/etl/customers/vander_elliot/initial_scoping.py b/etl/customers/vander_elliot/initial_scoping.py new file mode 100644 index 00000000..de212c7c --- /dev/null +++ b/etl/customers/vander_elliot/initial_scoping.py @@ -0,0 +1,23 @@ +import pandas as pd +from utils.s3 import save_csv_to_s3 + + +def app(): + # Check how many properties there are at EPC F/G in Birmingham + epc_data = pd.read_csv( + "local_data/all-domestic-certificates/domestic-E08000025-Birmingham/certificates.csv", + low_memory=False + ) + + # Filter on entries where we have a UPRN + epc_data = epc_data[~pd.isnull(epc_data["UPRN"])] + + # Get the newest EPC for each UPRN. We use LODGEMENT_DATE as a proxy for this + epc_data["LODGEMENT_DATE"] = pd.to_datetime(epc_data["LODGEMENT_DATE"]) + + epc_data = epc_data.sort_values("LODGEMENT_DATE", ascending=False).drop_duplicates("UPRN") + + epc_data = epc_data[epc_data["CURRENT_ENERGY_RATING"].isin(["F", "G"])] + + one_years_ago = pd.Timestamp.now() - pd.DateOffset(days=int(1 * 365)) + epc_data = epc_data[epc_data["LODGEMENT_DATE"] >= one_years_ago] From cc170e1fa3d7cfdbb138538b318581ee0df0d2b1 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Thu, 9 May 2024 16:32:22 +0100 Subject: [PATCH 2/3] set up vander elliot asset list --- .idea/Model.iml | 2 +- .idea/misc.xml | 2 +- backend/ml_models/Valuation.py | 11 +++ etl/customers/vander_elliot/pilot.py | 105 +++++++++++++++++++++++++++ 4 files changed, 118 insertions(+), 2 deletions(-) create mode 100644 etl/customers/vander_elliot/pilot.py diff --git a/.idea/Model.iml b/.idea/Model.iml index b0f9c00d..4413bb06 100644 --- a/.idea/Model.iml +++ b/.idea/Model.iml @@ -7,7 +7,7 @@ - + diff --git a/.idea/misc.xml b/.idea/misc.xml index 1122b380..6f308057 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -3,7 +3,7 @@ - + diff --git a/backend/ml_models/Valuation.py b/backend/ml_models/Valuation.py index 5c781979..cfd775e7 100644 --- a/backend/ml_models/Valuation.py +++ b/backend/ml_models/Valuation.py @@ -79,6 +79,17 @@ class PropertyValuation: 100070520130: 177_000, # Based on Zoopla 100070333957: 185_000, # Based on Zoopla 100070543258: 211_000, # Based on Zoopla + # Vander Elliot Pilot - search by going to https://www.zoopla.co.uk/property/uprn/{uprn}/ + 41018850: 104_000, # Based on Zoopla + 38237316: 74_000, # Based on Zoopla + 38237317: 74_000, # Based on Zoopla + 41052320: 70_000, # Based on Zoopla + 41052321: 70_000, # Based on Zoopla + 41052322: 38_000, # Based on Zoopla + 41222759: 38_000, # Based on Zoopla + 41222760: 46_000, # Based on Zoopla + 41222761: 270_000, # Based on Zoopla + 41212534: 38_000, # Based on Zoopla } # We base our valuation uplifts on a number of sources diff --git a/etl/customers/vander_elliot/pilot.py b/etl/customers/vander_elliot/pilot.py new file mode 100644 index 00000000..3c52869b --- /dev/null +++ b/etl/customers/vander_elliot/pilot.py @@ -0,0 +1,105 @@ +import pandas as pd +from utils.s3 import save_csv_to_s3 + +EPC_C_PORTFOLIO_ID = 78 +EPC_B_PORTFOLIO_ID = 79 +USER_ID = 8 + + +def app(): + """ + This code sets up the asset list for the 9 property portfolio for the pilot + :return: + """ + + asset_list = [ + { + "address": "79 Clare Road", + "postcode": "L20 9LZ", + "uprn": 41018850, # 3 bedroom property + }, + { + "address": "Flat 1, 29 Bedford Road", + "postcode": "L4 5PS", + "uprn": 38237316 # Single dewlling converted into two flats + }, + { + "address": "Flat 2, 29 Bedford Road", + "postcode": "L4 5PS", + "uprn": 38237317 # Single dewlling converted into two flats + }, + # 7 Flats above a domestic unit + { + "address": "Flat 1, 2 Linacre Lane", + "postcode": "L20 5AH", + "uprn": 41052320 + }, + { + "address": "Flat 2, 2 Linacre Lane", + "postcode": "L20 5AH", + "uprn": 41052321, + }, + { + "address": "Flat 3, 2 Linacre Lane", + "postcode": "L20 5AH", + "uprn": 41052322, + }, + { + "address": "Flat 4, 2 Linacre Lane", + "postcode": "L20 5AH", + "uprn": 41222759, + }, + { + "address": "Flat 1, 4 Linacre Lane", + "postcode": "L20 5AH", + "uprn": 41222760, + }, + { + "address": "Flat 2, 4 Linacre Lane", + "postcode": "L20 5AH", + "uprn": 41222761, + }, + { + "address": "Flat 3, 4 Linacre Lane", + "postcode": "L20 5AH", + "uprn": 41212534, + }, + ] + + asset_list = pd.DataFrame(asset_list) + + # Store the asset list in s3 + filename = f"{USER_ID}/{EPC_C_PORTFOLIO_ID}/pilot.csv" + save_csv_to_s3( + dataframe=asset_list, + bucket_name="retrofit-plan-inputs-dev", + file_name=filename + ) + + # EPC C portoflio + body = { + "portfolio_id": str(EPC_C_PORTFOLIO_ID), + "housing_type": "Private", + "goal": "Increase EPC", + "goal_value": "C", + "trigger_file_path": filename, + "already_installed_file_path": "", + "patches_file_path": "", + "non_invasive_recommendations_file_path": "", + "budget": None, + } + print(body) + + # EPC B portoflio + body = { + "portfolio_id": str(EPC_B_PORTFOLIO_ID), + "housing_type": "Private", + "goal": "Increase EPC", + "goal_value": "B", + "trigger_file_path": filename, + "already_installed_file_path": "", + "patches_file_path": "", + "non_invasive_recommendations_file_path": "", + "budget": None, + } + print(body) From 8d783c0e6d2dd2fc83fdafa52414fe451c7e4124 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Mon, 20 May 2024 18:05:47 +0100 Subject: [PATCH 3/3] added audit scenarios for ha analysis --- .idea/Model.iml | 2 +- .idea/misc.xml | 2 +- backend/apis/GoogleSolarApi.py | 334 ++++++++++++++++++ .../places_for_people/route_march.py | 165 ++++++++- .../ha_15_32/ha_analysis_batch_3.py | 204 ++++++----- 5 files changed, 618 insertions(+), 89 deletions(-) create mode 100644 backend/apis/GoogleSolarApi.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/backend/apis/GoogleSolarApi.py b/backend/apis/GoogleSolarApi.py new file mode 100644 index 00000000..86324c58 --- /dev/null +++ b/backend/apis/GoogleSolarApi.py @@ -0,0 +1,334 @@ +from backend.Property import Property +from backend.SearchEpc import SearchEpc +from etl.epc.Record import EPCRecord +from dotenv import load_dotenv +from utils.s3 import read_dataframe_from_s3_parquet +import os +import requests + +load_dotenv(dotenv_path="backend/.env") +EPC_AUTH_TOKEN = os.getenv("EPC_AUTH_TOKEN") + +# This is for 6 Laura Close, Tintagel, PL34 0EB (same property that Cotswolrd energy used) +uprn = 100040099104 + +cleaning_data = read_dataframe_from_s3_parquet( + bucket_name="retrofit-data-dev", file_key="sap_change_model/cleaning_dataset.parquet", +) + +searcher = SearchEpc(address1="6 Laura Close", postcode="PL34 0EB", uprn=uprn, auth_token=EPC_AUTH_TOKEN, os_api_key="") + +searcher.find_property(skip_os=True) + +epc_records = { + 'original_epc': searcher.newest_epc.copy(), + 'full_sap_epc': searcher.full_sap_epc.copy(), + 'old_data': searcher.older_epcs.copy(), +} + +epc = EPCRecord( + epc_records=epc_records, + run_mode="newdata", + cleaning_data=cleaning_data +) + +uprn_filenames = read_dataframe_from_s3_parquet( + bucket_name="retrofit-data-dev", file_key="spatial/filename_meta.parquet" +) + +p = Property( + id=0, + address=searcher.address_clean, + postcode=searcher.postcode_clean, + epc_record=epc, + already_installed={}, + non_invasive_recommendations={}, +) + +p.get_spatial_data(uprn_filenames) + +longitude = p.spatial["longitude"] +latitude = p.spatial["latitude"] + +api_key = "AIzaSyCIz8Psu5h-1txuDX0rQpUTgkvdj8yohqU" +url = 'https://solar.googleapis.com/v1/solarPotential' +params = { + 'location.latitude': f'{latitude:.5f}', + 'location.longitude': f'{longitude:.5f}', + 'requiredQuality': "MEDIUM", + 'key': api_key +} + +insights_url = 'https://solar.googleapis.com/v1/buildingInsights:findClosest' + +# Make the GET request to the Solar API +insights_response = requests.get(insights_url, params=params) +insights_data = insights_response.json() + +solar_potential = insights_data["solarPotential"] + +from pprint import pprint + +pprint(solar_potential) + +# This is the size of the panels used in the calculation - 400 watt +solar_potential["panelCapacityWatts"] +# Height of the panels used +solar_potential["panelHeightMeters"] +# Width of the panels used +solar_potential["panelWidthMeters"] + +solar_potential["wholeRoofStats"] + +# Copy of response for testing: +# {'name': 'buildings/ChIJ2yC6t4KEa0gRh2TIssogI7k', 'center': {'latitude': 50.667375, 'longitude': -4.7416833}, +# 'imageryDate': {'year': 2021, 'month': 7, 'day': 19}, 'regionCode': 'GB', 'solarPotential': {'maxArrayPanelsCount': +# 39, 'maxArrayAreaMeters2': 76.578636, 'maxSunshineHoursPerYear': 1172.0627, 'carbonOffsetFactorKgPerMwh': +# 478.99942, 'wholeRoofStats': {'areaMeters2': 129.65686, 'sunshineQuantiles': [537, 738.3836, 805.62445, 842.6802, +# 909.8431, 972.15234, 1036.1013, 1092.051, 1135.8192, 1163.1444, 1193.6012], 'groundAreaMeters2': 112.33}, +# 'roofSegmentStats': [{'pitchDegrees': 31.443022, 'azimuthDegrees': 218.25331, 'stats': {'areaMeters2': 44.08321, +# 'sunshineQuantiles': [614, 940.86975, 982.39124, 1057.0664, 1109.6869, 1137.5837, 1152.9211, 1163.1106, 1168.2212, +# 1170.8883, 1193.6012], 'groundAreaMeters2': 37.61}, 'center': {'latitude': 50.6673664, 'longitude': +# -4.741714099999999}, 'boundingBox': {'sw': {'latitude': 50.6673354, 'longitude': -4.741777}, 'ne': {'latitude': +# 50.6674029, 'longitude': -4.7416472}}, 'planeHeightAtCenterMeters': 93.0221}, {'pitchDegrees': 34.39779, +# 'azimuthDegrees': 31.74401, 'stats': {'areaMeters2': 44.622986, 'sunshineQuantiles': [537, 671.49774, 733.84985, +# 780.82733, 801.4026, 814.0189, 824.0077, 847.77484, 895.08295, 950.1469, 1123.3503], 'groundAreaMeters2': 36.82}, +# 'center': {'latitude': 50.6673966, 'longitude': -4.7416813}, 'boundingBox': {'sw': {'latitude': 50.667361, +# 'longitude': -4.7417497}, 'ne': {'latitude': 50.6674303, 'longitude': -4.741615599999999}}, +# 'planeHeightAtCenterMeters': 92.87593}, {'pitchDegrees': 3.0681775, 'azimuthDegrees': 301.1099, 'stats': { +# 'areaMeters2': 17.074476, 'sunshineQuantiles': [644.71136, 731.0546, 782.89813, 842.7107, 908.55585, 966.6212, +# 1010.6367, 1038.2543, 1053.2788, 1090.6831, 1128.0178], 'groundAreaMeters2': 17.050001}, 'center': {'latitude': +# 50.66740850000001, 'longitude': -4.7416025}, 'boundingBox': {'sw': {'latitude': 50.6673895, 'longitude': +# -4.7416436}, 'ne': {'latitude': 50.667431199999996, 'longitude': -4.7415572}}, 'planeHeightAtCenterMeters': +# 90.630356}, {'pitchDegrees': 27.093596, 'azimuthDegrees': 132.60162, 'stats': {'areaMeters2': 13.501617, +# 'sunshineQuantiles': [749, 976.85345, 1059.0062, 1081.6173, 1097.4441, 1110.3171, 1128.2186, 1133.9421, 1142.068, +# 1148.2168, 1157.632], 'groundAreaMeters2': 12.02}, 'center': {'latitude': 50.667315699999996, 'longitude': +# -4.741675400000001}, 'boundingBox': {'sw': {'latitude': 50.667291399999996, 'longitude': -4.7417066}, +# 'ne': {'latitude': 50.6673372, 'longitude': -4.741648400000001}}, 'planeHeightAtCenterMeters': 92.36334}, +# {'pitchDegrees': 31.666294, 'azimuthDegrees': 308.42334, 'stats': {'areaMeters2': 10.374564, 'sunshineQuantiles': [ +# 617.9507, 752.2504, 847.66315, 872.0505, 881.26227, 900.9639, 933.3188, 967.4747, 1000.8129, 1038.3002, 1105.545], +# 'groundAreaMeters2': 8.83}, 'center': {'latitude': 50.6673295, 'longitude': -4.7417128}, 'boundingBox': {'sw': { +# 'latitude': 50.6673134, 'longitude': -4.7417422}, 'ne': {'latitude': 50.6673413, 'longitude': -4.7416775}}, +# 'planeHeightAtCenterMeters': 92.31146}], 'solarPanelConfigs': [{'panelsCount': 4, 'yearlyEnergyDcKwh': 1867.1516, +# 'roofSegmentSummaries': [{'pitchDegrees': 31.443022, 'azimuthDegrees': 218.25331, 'panelsCount': 4, +# 'yearlyEnergyDcKwh': 1867.1515, 'segmentIndex': 0}]}, {'panelsCount': 5, 'yearlyEnergyDcKwh': 2335.0068, +# 'roofSegmentSummaries': [{'pitchDegrees': 31.443022, 'azimuthDegrees': 218.25331, 'panelsCount': 5, +# 'yearlyEnergyDcKwh': 2335.0068, 'segmentIndex': 0}]}, {'panelsCount': 6, 'yearlyEnergyDcKwh': 2799.8508, +# 'roofSegmentSummaries': [{'pitchDegrees': 31.443022, 'azimuthDegrees': 218.25331, 'panelsCount': 6, +# 'yearlyEnergyDcKwh': 2799.8508, 'segmentIndex': 0}]}, {'panelsCount': 7, 'yearlyEnergyDcKwh': 3264.6506, +# 'roofSegmentSummaries': [{'pitchDegrees': 31.443022, 'azimuthDegrees': 218.25331, 'panelsCount': 7, +# 'yearlyEnergyDcKwh': 3264.6506, 'segmentIndex': 0}]}, {'panelsCount': 8, 'yearlyEnergyDcKwh': 3726.2405, +# 'roofSegmentSummaries': [{'pitchDegrees': 31.443022, 'azimuthDegrees': 218.25331, 'panelsCount': 8, +# 'yearlyEnergyDcKwh': 3726.2405, 'segmentIndex': 0}]}, {'panelsCount': 9, 'yearlyEnergyDcKwh': 4187.721, +# 'roofSegmentSummaries': [{'pitchDegrees': 31.443022, 'azimuthDegrees': 218.25331, 'panelsCount': 9, +# 'yearlyEnergyDcKwh': 4187.721, 'segmentIndex': 0}]}, {'panelsCount': 10, 'yearlyEnergyDcKwh': 4646.094, +# 'roofSegmentSummaries': [{'pitchDegrees': 31.443022, 'azimuthDegrees': 218.25331, 'panelsCount': 10, +# 'yearlyEnergyDcKwh': 4646.094, 'segmentIndex': 0}]}, {'panelsCount': 11, 'yearlyEnergyDcKwh': 5103.777, +# 'roofSegmentSummaries': [{'pitchDegrees': 31.443022, 'azimuthDegrees': 218.25331, 'panelsCount': 10, +# 'yearlyEnergyDcKwh': 4646.094, 'segmentIndex': 0}, {'pitchDegrees': 27.093596, 'azimuthDegrees': 132.60162, +# 'panelsCount': 1, 'yearlyEnergyDcKwh': 457.68268, 'segmentIndex': 3}]}, {'panelsCount': 12, 'yearlyEnergyDcKwh': +# 5559.845, 'roofSegmentSummaries': [{'pitchDegrees': 31.443022, 'azimuthDegrees': 218.25331, 'panelsCount': 10, +# 'yearlyEnergyDcKwh': 4646.094, 'segmentIndex': 0}, {'pitchDegrees': 27.093596, 'azimuthDegrees': 132.60162, +# 'panelsCount': 2, 'yearlyEnergyDcKwh': 913.7509, 'segmentIndex': 3}]}, {'panelsCount': 13, 'yearlyEnergyDcKwh': +# 6013.053, 'roofSegmentSummaries': [{'pitchDegrees': 31.443022, 'azimuthDegrees': 218.25331, 'panelsCount': 11, +# 'yearlyEnergyDcKwh': 5099.302, 'segmentIndex': 0}, {'pitchDegrees': 27.093596, 'azimuthDegrees': 132.60162, +# 'panelsCount': 2, 'yearlyEnergyDcKwh': 913.7509, 'segmentIndex': 3}]}, {'panelsCount': 14, 'yearlyEnergyDcKwh': +# 6461.664, 'roofSegmentSummaries': [{'pitchDegrees': 31.443022, 'azimuthDegrees': 218.25331, 'panelsCount': 12, +# 'yearlyEnergyDcKwh': 5547.9126, 'segmentIndex': 0}, {'pitchDegrees': 27.093596, 'azimuthDegrees': 132.60162, +# 'panelsCount': 2, 'yearlyEnergyDcKwh': 913.7509, 'segmentIndex': 3}]}, {'panelsCount': 15, 'yearlyEnergyDcKwh': +# 6902.33, 'roofSegmentSummaries': [{'pitchDegrees': 31.443022, 'azimuthDegrees': 218.25331, 'panelsCount': 12, +# 'yearlyEnergyDcKwh': 5547.9126, 'segmentIndex': 0}, {'pitchDegrees': 27.093596, 'azimuthDegrees': 132.60162, +# 'panelsCount': 3, 'yearlyEnergyDcKwh': 1354.4171, 'segmentIndex': 3}]}, {'panelsCount': 16, 'yearlyEnergyDcKwh': +# 7321.6436, 'roofSegmentSummaries': [{'pitchDegrees': 31.443022, 'azimuthDegrees': 218.25331, 'panelsCount': 12, +# 'yearlyEnergyDcKwh': 5547.9126, 'segmentIndex': 0}, {'pitchDegrees': 3.0681775, 'azimuthDegrees': 301.1099, +# 'panelsCount': 1, 'yearlyEnergyDcKwh': 419.31348, 'segmentIndex': 2}, {'pitchDegrees': 27.093596, 'azimuthDegrees': +# 132.60162, 'panelsCount': 3, 'yearlyEnergyDcKwh': 1354.4171, 'segmentIndex': 3}]}, {'panelsCount': 17, +# 'yearlyEnergyDcKwh': 7740.388, 'roofSegmentSummaries': [{'pitchDegrees': 31.443022, 'azimuthDegrees': 218.25331, +# 'panelsCount': 12, 'yearlyEnergyDcKwh': 5547.9126, 'segmentIndex': 0}, {'pitchDegrees': 3.0681775, +# 'azimuthDegrees': 301.1099, 'panelsCount': 2, 'yearlyEnergyDcKwh': 838.0579, 'segmentIndex': 2}, {'pitchDegrees': +# 27.093596, 'azimuthDegrees': 132.60162, 'panelsCount': 3, 'yearlyEnergyDcKwh': 1354.4171, 'segmentIndex': 3}]}, +# {'panelsCount': 18, 'yearlyEnergyDcKwh': 8154.265, 'roofSegmentSummaries': [{'pitchDegrees': 31.443022, +# 'azimuthDegrees': 218.25331, 'panelsCount': 13, 'yearlyEnergyDcKwh': 5961.7896, 'segmentIndex': 0}, +# {'pitchDegrees': 3.0681775, 'azimuthDegrees': 301.1099, 'panelsCount': 2, 'yearlyEnergyDcKwh': 838.0579, +# 'segmentIndex': 2}, {'pitchDegrees': 27.093596, 'azimuthDegrees': 132.60162, 'panelsCount': 3, 'yearlyEnergyDcKwh': +# 1354.4171, 'segmentIndex': 3}]}, {'panelsCount': 19, 'yearlyEnergyDcKwh': 8566.032, 'roofSegmentSummaries': [{ +# 'pitchDegrees': 31.443022, 'azimuthDegrees': 218.25331, 'panelsCount': 14, 'yearlyEnergyDcKwh': 6373.556, +# 'segmentIndex': 0}, {'pitchDegrees': 3.0681775, 'azimuthDegrees': 301.1099, 'panelsCount': 2, 'yearlyEnergyDcKwh': +# 838.0579, 'segmentIndex': 2}, {'pitchDegrees': 27.093596, 'azimuthDegrees': 132.60162, 'panelsCount': 3, +# 'yearlyEnergyDcKwh': 1354.4171, 'segmentIndex': 3}]}, {'panelsCount': 20, 'yearlyEnergyDcKwh': 8976.624, +# 'roofSegmentSummaries': [{'pitchDegrees': 31.443022, 'azimuthDegrees': 218.25331, 'panelsCount': 15, +# 'yearlyEnergyDcKwh': 6784.1484, 'segmentIndex': 0}, {'pitchDegrees': 3.0681775, 'azimuthDegrees': 301.1099, +# 'panelsCount': 2, 'yearlyEnergyDcKwh': 838.0579, 'segmentIndex': 2}, {'pitchDegrees': 27.093596, 'azimuthDegrees': +# 132.60162, 'panelsCount': 3, 'yearlyEnergyDcKwh': 1354.4171, 'segmentIndex': 3}]}, {'panelsCount': 21, +# 'yearlyEnergyDcKwh': 9380.78, 'roofSegmentSummaries': [{'pitchDegrees': 31.443022, 'azimuthDegrees': 218.25331, +# 'panelsCount': 15, 'yearlyEnergyDcKwh': 6784.1484, 'segmentIndex': 0}, {'pitchDegrees': 3.0681775, +# 'azimuthDegrees': 301.1099, 'panelsCount': 3, 'yearlyEnergyDcKwh': 1242.214, 'segmentIndex': 2}, {'pitchDegrees': +# 27.093596, 'azimuthDegrees': 132.60162, 'panelsCount': 3, 'yearlyEnergyDcKwh': 1354.4171, 'segmentIndex': 3}]}, +# {'panelsCount': 22, 'yearlyEnergyDcKwh': 9784.078, 'roofSegmentSummaries': [{'pitchDegrees': 31.443022, +# 'azimuthDegrees': 218.25331, 'panelsCount': 15, 'yearlyEnergyDcKwh': 6784.1484, 'segmentIndex': 0}, +# {'pitchDegrees': 3.0681775, 'azimuthDegrees': 301.1099, 'panelsCount': 4, 'yearlyEnergyDcKwh': 1645.5122, +# 'segmentIndex': 2}, {'pitchDegrees': 27.093596, 'azimuthDegrees': 132.60162, 'panelsCount': 3, 'yearlyEnergyDcKwh': +# 1354.4171, 'segmentIndex': 3}]}, {'panelsCount': 23, 'yearlyEnergyDcKwh': 10162.354, 'roofSegmentSummaries': [{ +# 'pitchDegrees': 31.443022, 'azimuthDegrees': 218.25331, 'panelsCount': 15, 'yearlyEnergyDcKwh': 6784.1484, +# 'segmentIndex': 0}, {'pitchDegrees': 3.0681775, 'azimuthDegrees': 301.1099, 'panelsCount': 4, 'yearlyEnergyDcKwh': +# 1645.5122, 'segmentIndex': 2}, {'pitchDegrees': 27.093596, 'azimuthDegrees': 132.60162, 'panelsCount': 3, +# 'yearlyEnergyDcKwh': 1354.4171, 'segmentIndex': 3}, {'pitchDegrees': 31.666294, 'azimuthDegrees': 308.42334, +# 'panelsCount': 1, 'yearlyEnergyDcKwh': 378.2754, 'segmentIndex': 4}]}, {'panelsCount': 24, 'yearlyEnergyDcKwh': +# 10535.894, 'roofSegmentSummaries': [{'pitchDegrees': 31.443022, 'azimuthDegrees': 218.25331, 'panelsCount': 15, +# 'yearlyEnergyDcKwh': 6784.1484, 'segmentIndex': 0}, {'pitchDegrees': 3.0681775, 'azimuthDegrees': 301.1099, +# 'panelsCount': 5, 'yearlyEnergyDcKwh': 2019.0519, 'segmentIndex': 2}, {'pitchDegrees': 27.093596, 'azimuthDegrees': +# 132.60162, 'panelsCount': 3, 'yearlyEnergyDcKwh': 1354.4171, 'segmentIndex': 3}, {'pitchDegrees': 31.666294, +# 'azimuthDegrees': 308.42334, 'panelsCount': 1, 'yearlyEnergyDcKwh': 378.2754, 'segmentIndex': 4}]}, {'panelsCount': +# 25, 'yearlyEnergyDcKwh': 10901.273, 'roofSegmentSummaries': [{'pitchDegrees': 31.443022, 'azimuthDegrees': +# 218.25331, 'panelsCount': 15, 'yearlyEnergyDcKwh': 6784.1484, 'segmentIndex': 0}, {'pitchDegrees': 3.0681775, +# 'azimuthDegrees': 301.1099, 'panelsCount': 5, 'yearlyEnergyDcKwh': 2019.0519, 'segmentIndex': 2}, {'pitchDegrees': +# 27.093596, 'azimuthDegrees': 132.60162, 'panelsCount': 3, 'yearlyEnergyDcKwh': 1354.4171, 'segmentIndex': 3}, +# {'pitchDegrees': 31.666294, 'azimuthDegrees': 308.42334, 'panelsCount': 2, 'yearlyEnergyDcKwh': 743.65497, +# 'segmentIndex': 4}]}, {'panelsCount': 26, 'yearlyEnergyDcKwh': 11242.756, 'roofSegmentSummaries': [{'pitchDegrees': +# 31.443022, 'azimuthDegrees': 218.25331, 'panelsCount': 15, 'yearlyEnergyDcKwh': 6784.1484, 'segmentIndex': 0}, +# {'pitchDegrees': 34.39779, 'azimuthDegrees': 31.74401, 'panelsCount': 1, 'yearlyEnergyDcKwh': 341.4827, +# 'segmentIndex': 1}, {'pitchDegrees': 3.0681775, 'azimuthDegrees': 301.1099, 'panelsCount': 5, 'yearlyEnergyDcKwh': +# 2019.0519, 'segmentIndex': 2}, {'pitchDegrees': 27.093596, 'azimuthDegrees': 132.60162, 'panelsCount': 3, +# 'yearlyEnergyDcKwh': 1354.4171, 'segmentIndex': 3}, {'pitchDegrees': 31.666294, 'azimuthDegrees': 308.42334, +# 'panelsCount': 2, 'yearlyEnergyDcKwh': 743.65497, 'segmentIndex': 4}]}, {'panelsCount': 27, 'yearlyEnergyDcKwh': +# 11579.401, 'roofSegmentSummaries': [{'pitchDegrees': 31.443022, 'azimuthDegrees': 218.25331, 'panelsCount': 15, +# 'yearlyEnergyDcKwh': 6784.1484, 'segmentIndex': 0}, {'pitchDegrees': 34.39779, 'azimuthDegrees': 31.74401, +# 'panelsCount': 2, 'yearlyEnergyDcKwh': 678.1277, 'segmentIndex': 1}, {'pitchDegrees': 3.0681775, 'azimuthDegrees': +# 301.1099, 'panelsCount': 5, 'yearlyEnergyDcKwh': 2019.0519, 'segmentIndex': 2}, {'pitchDegrees': 27.093596, +# 'azimuthDegrees': 132.60162, 'panelsCount': 3, 'yearlyEnergyDcKwh': 1354.4171, 'segmentIndex': 3}, {'pitchDegrees': +# 31.666294, 'azimuthDegrees': 308.42334, 'panelsCount': 2, 'yearlyEnergyDcKwh': 743.65497, 'segmentIndex': 4}]}, +# {'panelsCount': 28, 'yearlyEnergyDcKwh': 11919.106, 'roofSegmentSummaries': [{'pitchDegrees': 31.443022, +# 'azimuthDegrees': 218.25331, 'panelsCount': 15, 'yearlyEnergyDcKwh': 6784.1484, 'segmentIndex': 0}, +# {'pitchDegrees': 34.39779, 'azimuthDegrees': 31.74401, 'panelsCount': 3, 'yearlyEnergyDcKwh': 1017.83356, +# 'segmentIndex': 1}, {'pitchDegrees': 3.0681775, 'azimuthDegrees': 301.1099, 'panelsCount': 5, 'yearlyEnergyDcKwh': +# 2019.0519, 'segmentIndex': 2}, {'pitchDegrees': 27.093596, 'azimuthDegrees': 132.60162, 'panelsCount': 3, +# 'yearlyEnergyDcKwh': 1354.4171, 'segmentIndex': 3}, {'pitchDegrees': 31.666294, 'azimuthDegrees': 308.42334, +# 'panelsCount': 2, 'yearlyEnergyDcKwh': 743.65497, 'segmentIndex': 4}]}, {'panelsCount': 29, 'yearlyEnergyDcKwh': +# 12255.358, 'roofSegmentSummaries': [{'pitchDegrees': 31.443022, 'azimuthDegrees': 218.25331, 'panelsCount': 15, +# 'yearlyEnergyDcKwh': 6784.1484, 'segmentIndex': 0}, {'pitchDegrees': 34.39779, 'azimuthDegrees': 31.74401, +# 'panelsCount': 4, 'yearlyEnergyDcKwh': 1354.0854, 'segmentIndex': 1}, {'pitchDegrees': 3.0681775, 'azimuthDegrees': +# 301.1099, 'panelsCount': 5, 'yearlyEnergyDcKwh': 2019.0519, 'segmentIndex': 2}, {'pitchDegrees': 27.093596, +# 'azimuthDegrees': 132.60162, 'panelsCount': 3, 'yearlyEnergyDcKwh': 1354.4171, 'segmentIndex': 3}, {'pitchDegrees': +# 31.666294, 'azimuthDegrees': 308.42334, 'panelsCount': 2, 'yearlyEnergyDcKwh': 743.65497, 'segmentIndex': 4}]}, +# {'panelsCount': 30, 'yearlyEnergyDcKwh': 12586.448, 'roofSegmentSummaries': [{'pitchDegrees': 31.443022, +# 'azimuthDegrees': 218.25331, 'panelsCount': 15, 'yearlyEnergyDcKwh': 6784.1484, 'segmentIndex': 0}, +# {'pitchDegrees': 34.39779, 'azimuthDegrees': 31.74401, 'panelsCount': 5, 'yearlyEnergyDcKwh': 1685.1748, +# 'segmentIndex': 1}, {'pitchDegrees': 3.0681775, 'azimuthDegrees': 301.1099, 'panelsCount': 5, 'yearlyEnergyDcKwh': +# 2019.0519, 'segmentIndex': 2}, {'pitchDegrees': 27.093596, 'azimuthDegrees': 132.60162, 'panelsCount': 3, +# 'yearlyEnergyDcKwh': 1354.4171, 'segmentIndex': 3}, {'pitchDegrees': 31.666294, 'azimuthDegrees': 308.42334, +# 'panelsCount': 2, 'yearlyEnergyDcKwh': 743.65497, 'segmentIndex': 4}]}, {'panelsCount': 31, 'yearlyEnergyDcKwh': +# 12911.502, 'roofSegmentSummaries': [{'pitchDegrees': 31.443022, 'azimuthDegrees': 218.25331, 'panelsCount': 15, +# 'yearlyEnergyDcKwh': 6784.1484, 'segmentIndex': 0}, {'pitchDegrees': 34.39779, 'azimuthDegrees': 31.74401, +# 'panelsCount': 6, 'yearlyEnergyDcKwh': 2010.2289, 'segmentIndex': 1}, {'pitchDegrees': 3.0681775, 'azimuthDegrees': +# 301.1099, 'panelsCount': 5, 'yearlyEnergyDcKwh': 2019.0519, 'segmentIndex': 2}, {'pitchDegrees': 27.093596, +# 'azimuthDegrees': 132.60162, 'panelsCount': 3, 'yearlyEnergyDcKwh': 1354.4171, 'segmentIndex': 3}, {'pitchDegrees': +# 31.666294, 'azimuthDegrees': 308.42334, 'panelsCount': 2, 'yearlyEnergyDcKwh': 743.65497, 'segmentIndex': 4}]}, +# {'panelsCount': 32, 'yearlyEnergyDcKwh': 13233.139, 'roofSegmentSummaries': [{'pitchDegrees': 31.443022, +# 'azimuthDegrees': 218.25331, 'panelsCount': 15, 'yearlyEnergyDcKwh': 6784.1484, 'segmentIndex': 0}, +# {'pitchDegrees': 34.39779, 'azimuthDegrees': 31.74401, 'panelsCount': 7, 'yearlyEnergyDcKwh': 2331.8652, +# 'segmentIndex': 1}, {'pitchDegrees': 3.0681775, 'azimuthDegrees': 301.1099, 'panelsCount': 5, 'yearlyEnergyDcKwh': +# 2019.0519, 'segmentIndex': 2}, {'pitchDegrees': 27.093596, 'azimuthDegrees': 132.60162, 'panelsCount': 3, +# 'yearlyEnergyDcKwh': 1354.4171, 'segmentIndex': 3}, {'pitchDegrees': 31.666294, 'azimuthDegrees': 308.42334, +# 'panelsCount': 2, 'yearlyEnergyDcKwh': 743.65497, 'segmentIndex': 4}]}, {'panelsCount': 33, 'yearlyEnergyDcKwh': +# 13554.602, 'roofSegmentSummaries': [{'pitchDegrees': 31.443022, 'azimuthDegrees': 218.25331, 'panelsCount': 15, +# 'yearlyEnergyDcKwh': 6784.1484, 'segmentIndex': 0}, {'pitchDegrees': 34.39779, 'azimuthDegrees': 31.74401, +# 'panelsCount': 8, 'yearlyEnergyDcKwh': 2653.3286, 'segmentIndex': 1}, {'pitchDegrees': 3.0681775, 'azimuthDegrees': +# 301.1099, 'panelsCount': 5, 'yearlyEnergyDcKwh': 2019.0519, 'segmentIndex': 2}, {'pitchDegrees': 27.093596, +# 'azimuthDegrees': 132.60162, 'panelsCount': 3, 'yearlyEnergyDcKwh': 1354.4171, 'segmentIndex': 3}, {'pitchDegrees': +# 31.666294, 'azimuthDegrees': 308.42334, 'panelsCount': 2, 'yearlyEnergyDcKwh': 743.65497, 'segmentIndex': 4}]}, +# {'panelsCount': 34, 'yearlyEnergyDcKwh': 13893.903, 'roofSegmentSummaries': [{'pitchDegrees': 31.443022, +# 'azimuthDegrees': 218.25331, 'panelsCount': 15, 'yearlyEnergyDcKwh': 6784.1484, 'segmentIndex': 0}, +# {'pitchDegrees': 34.39779, 'azimuthDegrees': 31.74401, 'panelsCount': 9, 'yearlyEnergyDcKwh': 2992.6301, +# 'segmentIndex': 1}, {'pitchDegrees': 3.0681775, 'azimuthDegrees': 301.1099, 'panelsCount': 5, 'yearlyEnergyDcKwh': +# 2019.0519, 'segmentIndex': 2}, {'pitchDegrees': 27.093596, 'azimuthDegrees': 132.60162, 'panelsCount': 3, +# 'yearlyEnergyDcKwh': 1354.4171, 'segmentIndex': 3}, {'pitchDegrees': 31.666294, 'azimuthDegrees': 308.42334, +# 'panelsCount': 2, 'yearlyEnergyDcKwh': 743.65497, 'segmentIndex': 4}]}, {'panelsCount': 35, 'yearlyEnergyDcKwh': +# 14221.166, 'roofSegmentSummaries': [{'pitchDegrees': 31.443022, 'azimuthDegrees': 218.25331, 'panelsCount': 15, +# 'yearlyEnergyDcKwh': 6784.1484, 'segmentIndex': 0}, {'pitchDegrees': 34.39779, 'azimuthDegrees': 31.74401, +# 'panelsCount': 10, 'yearlyEnergyDcKwh': 3319.893, 'segmentIndex': 1}, {'pitchDegrees': 3.0681775, 'azimuthDegrees': +# 301.1099, 'panelsCount': 5, 'yearlyEnergyDcKwh': 2019.0519, 'segmentIndex': 2}, {'pitchDegrees': 27.093596, +# 'azimuthDegrees': 132.60162, 'panelsCount': 3, 'yearlyEnergyDcKwh': 1354.4171, 'segmentIndex': 3}, {'pitchDegrees': +# 31.666294, 'azimuthDegrees': 308.42334, 'panelsCount': 2, 'yearlyEnergyDcKwh': 743.65497, 'segmentIndex': 4}]}, +# {'panelsCount': 36, 'yearlyEnergyDcKwh': 14536.154, 'roofSegmentSummaries': [{'pitchDegrees': 31.443022, +# 'azimuthDegrees': 218.25331, 'panelsCount': 15, 'yearlyEnergyDcKwh': 6784.1484, 'segmentIndex': 0}, +# {'pitchDegrees': 34.39779, 'azimuthDegrees': 31.74401, 'panelsCount': 11, 'yearlyEnergyDcKwh': 3634.8809, +# 'segmentIndex': 1}, {'pitchDegrees': 3.0681775, 'azimuthDegrees': 301.1099, 'panelsCount': 5, 'yearlyEnergyDcKwh': +# 2019.0519, 'segmentIndex': 2}, {'pitchDegrees': 27.093596, 'azimuthDegrees': 132.60162, 'panelsCount': 3, +# 'yearlyEnergyDcKwh': 1354.4171, 'segmentIndex': 3}, {'pitchDegrees': 31.666294, 'azimuthDegrees': 308.42334, +# 'panelsCount': 2, 'yearlyEnergyDcKwh': 743.65497, 'segmentIndex': 4}]}, {'panelsCount': 37, 'yearlyEnergyDcKwh': +# 14850.317, 'roofSegmentSummaries': [{'pitchDegrees': 31.443022, 'azimuthDegrees': 218.25331, 'panelsCount': 15, +# 'yearlyEnergyDcKwh': 6784.1484, 'segmentIndex': 0}, {'pitchDegrees': 34.39779, 'azimuthDegrees': 31.74401, +# 'panelsCount': 12, 'yearlyEnergyDcKwh': 3949.0444, 'segmentIndex': 1}, {'pitchDegrees': 3.0681775, +# 'azimuthDegrees': 301.1099, 'panelsCount': 5, 'yearlyEnergyDcKwh': 2019.0519, 'segmentIndex': 2}, {'pitchDegrees': +# 27.093596, 'azimuthDegrees': 132.60162, 'panelsCount': 3, 'yearlyEnergyDcKwh': 1354.4171, 'segmentIndex': 3}, +# {'pitchDegrees': 31.666294, 'azimuthDegrees': 308.42334, 'panelsCount': 2, 'yearlyEnergyDcKwh': 743.65497, +# 'segmentIndex': 4}]}, {'panelsCount': 38, 'yearlyEnergyDcKwh': 15160.658, 'roofSegmentSummaries': [{'pitchDegrees': +# 31.443022, 'azimuthDegrees': 218.25331, 'panelsCount': 15, 'yearlyEnergyDcKwh': 6784.1484, 'segmentIndex': 0}, +# {'pitchDegrees': 34.39779, 'azimuthDegrees': 31.74401, 'panelsCount': 13, 'yearlyEnergyDcKwh': 4259.385, +# 'segmentIndex': 1}, {'pitchDegrees': 3.0681775, 'azimuthDegrees': 301.1099, 'panelsCount': 5, 'yearlyEnergyDcKwh': +# 2019.0519, 'segmentIndex': 2}, {'pitchDegrees': 27.093596, 'azimuthDegrees': 132.60162, 'panelsCount': 3, +# 'yearlyEnergyDcKwh': 1354.4171, 'segmentIndex': 3}, {'pitchDegrees': 31.666294, 'azimuthDegrees': 308.42334, +# 'panelsCount': 2, 'yearlyEnergyDcKwh': 743.65497, 'segmentIndex': 4}]}, {'panelsCount': 39, 'yearlyEnergyDcKwh': +# 15438.986, 'roofSegmentSummaries': [{'pitchDegrees': 31.443022, 'azimuthDegrees': 218.25331, 'panelsCount': 15, +# 'yearlyEnergyDcKwh': 6784.1484, 'segmentIndex': 0}, {'pitchDegrees': 34.39779, 'azimuthDegrees': 31.74401, +# 'panelsCount': 14, 'yearlyEnergyDcKwh': 4537.713, 'segmentIndex': 1}, {'pitchDegrees': 3.0681775, 'azimuthDegrees': +# 301.1099, 'panelsCount': 5, 'yearlyEnergyDcKwh': 2019.0519, 'segmentIndex': 2}, {'pitchDegrees': 27.093596, +# 'azimuthDegrees': 132.60162, 'panelsCount': 3, 'yearlyEnergyDcKwh': 1354.4171, 'segmentIndex': 3}, {'pitchDegrees': +# 31.666294, 'azimuthDegrees': 308.42334, 'panelsCount': 2, 'yearlyEnergyDcKwh': 743.65497, 'segmentIndex': 4}]}], +# 'panelCapacityWatts': 400, 'panelHeightMeters': 1.879, 'panelWidthMeters': 1.045, 'panelLifetimeYears': 20, +# 'buildingStats': {'areaMeters2': 138.38115, 'sunshineQuantiles': [537, 728.5604, 799.23975, 833.99713, 900.88086, +# 959.65875, 1024.2743, 1086.1285, 1132.8774, 1162.1904, 1193.6012], 'groundAreaMeters2': 117.16}, 'solarPanels': [{ +# 'center': {'latitude': 50.667371499999994, 'longitude': -4.7417235}, 'orientation': 'LANDSCAPE', +# 'yearlyEnergyDcKwh': 468.5037, 'segmentIndex': 0}, {'center': {'latitude': 50.6673614, 'longitude': -4.7417023}, +# 'orientation': 'LANDSCAPE', 'yearlyEnergyDcKwh': 467.61072, 'segmentIndex': 0}, {'center': {'latitude': +# 50.667365100000005, 'longitude': -4.7417311}, 'orientation': 'LANDSCAPE', 'yearlyEnergyDcKwh': 465.55005, +# 'segmentIndex': 0}, {'center': {'latitude': 50.6673512, 'longitude': -4.741681000000001}, 'orientation': +# 'LANDSCAPE', 'yearlyEnergyDcKwh': 465.48712, 'segmentIndex': 0}, {'center': {'latitude': 50.667357599999995, +# 'longitude': -4.7416734}, 'orientation': 'LANDSCAPE', 'yearlyEnergyDcKwh': 467.8553, 'segmentIndex': 0}, +# {'center': {'latitude': 50.6673779, 'longitude': -4.741715999999999}, 'orientation': 'LANDSCAPE', +# 'yearlyEnergyDcKwh': 464.84396, 'segmentIndex': 0}, {'center': {'latitude': 50.6673678, 'longitude': -4.7416947}, +# 'orientation': 'LANDSCAPE', 'yearlyEnergyDcKwh': 464.79984, 'segmentIndex': 0}, {'center': {'latitude': 50.6673549, +# 'longitude': -4.7417098}, 'orientation': 'LANDSCAPE', 'yearlyEnergyDcKwh': 461.58975, 'segmentIndex': 0}, +# {'center': {'latitude': 50.6673816, 'longitude': -4.7417448}, 'orientation': 'LANDSCAPE', 'yearlyEnergyDcKwh': +# 461.48065, 'segmentIndex': 0}, {'center': {'latitude': 50.6673881, 'longitude': -4.7417372}, 'orientation': +# 'LANDSCAPE', 'yearlyEnergyDcKwh': 458.3733, 'segmentIndex': 0}, {'center': {'latitude': 50.6673149, 'longitude': +# -4.7416768}, 'orientation': 'LANDSCAPE', 'yearlyEnergyDcKwh': 457.68268, 'segmentIndex': 3}, {'center': { +# 'latitude': 50.6673204, 'longitude': -4.7416867}, 'orientation': 'LANDSCAPE', 'yearlyEnergyDcKwh': 456.06827, +# 'segmentIndex': 3}, {'center': {'latitude': 50.667375199999995, 'longitude': -4.7417524}, 'orientation': +# 'LANDSCAPE', 'yearlyEnergyDcKwh': 453.20776, 'segmentIndex': 0}, {'center': {'latitude': 50.667364, 'longitude': +# -4.7416659}, 'orientation': 'LANDSCAPE', 'yearlyEnergyDcKwh': 448.61087, 'segmentIndex': 0}, {'center': { +# 'latitude': 50.6673094, 'longitude': -4.741666899999999}, 'orientation': 'LANDSCAPE', 'yearlyEnergyDcKwh': +# 440.66626, 'segmentIndex': 3}, {'center': {'latitude': 50.667403799999995, 'longitude': -4.741588900000001}, +# 'orientation': 'LANDSCAPE', 'yearlyEnergyDcKwh': 419.31348, 'segmentIndex': 2}, {'center': {'latitude': +# 50.66740850000001, 'longitude': -4.7416016999999995}, 'orientation': 'LANDSCAPE', 'yearlyEnergyDcKwh': 418.74448, +# 'segmentIndex': 2}, {'center': {'latitude': 50.6673688, 'longitude': -4.7417599}, 'orientation': 'LANDSCAPE', +# 'yearlyEnergyDcKwh': 413.877, 'segmentIndex': 0}, {'center': {'latitude': 50.667348499999996, 'longitude': +# -4.7417174}, 'orientation': 'LANDSCAPE', 'yearlyEnergyDcKwh': 411.76657, 'segmentIndex': 0}, {'center': { +# 'latitude': 50.6673587, 'longitude': -4.7417387}, 'orientation': 'LANDSCAPE', 'yearlyEnergyDcKwh': 410.5925, +# 'segmentIndex': 0}, {'center': {'latitude': 50.6673992, 'longitude': -4.7415761}, 'orientation': 'LANDSCAPE', +# 'yearlyEnergyDcKwh': 404.15607, 'segmentIndex': 2}, {'center': {'latitude': 50.6674132, 'longitude': -4.7416145}, +# 'orientation': 'LANDSCAPE', 'yearlyEnergyDcKwh': 403.29822, 'segmentIndex': 2}, {'center': {'latitude': 50.6673324, +# 'longitude': -4.7417015}, 'orientation': 'PORTRAIT', 'yearlyEnergyDcKwh': 378.2754, 'segmentIndex': 4}, {'center': +# {'latitude': 50.667417799999996, 'longitude': -4.7416273}, 'orientation': 'LANDSCAPE', 'yearlyEnergyDcKwh': +# 373.53967, 'segmentIndex': 2}, {'center': {'latitude': 50.667324900000004, 'longitude': -4.7417104}, 'orientation': +# 'PORTRAIT', 'yearlyEnergyDcKwh': 365.37958, 'segmentIndex': 4}, {'center': {'latitude': 50.6674043, 'longitude': +# -4.741680800000001}, 'orientation': 'PORTRAIT', 'yearlyEnergyDcKwh': 341.4827, 'segmentIndex': 1}, {'center': { +# 'latitude': 50.667392299999996, 'longitude': -4.7416919}, 'orientation': 'PORTRAIT', 'yearlyEnergyDcKwh': +# 336.64502, 'segmentIndex': 1}, {'center': {'latitude': 50.667397, 'longitude': -4.741704599999999}, 'orientation': +# 'PORTRAIT', 'yearlyEnergyDcKwh': 339.7059, 'segmentIndex': 1}, {'center': {'latitude': 50.6674018, 'longitude': +# -4.7417174}, 'orientation': 'PORTRAIT', 'yearlyEnergyDcKwh': 336.25195, 'segmentIndex': 1}, {'center': {'latitude': +# 50.6673875, 'longitude': -4.7416791}, 'orientation': 'PORTRAIT', 'yearlyEnergyDcKwh': 331.08936, 'segmentIndex': +# 1}, {'center': {'latitude': 50.6674065, 'longitude': -4.7417301}, 'orientation': 'PORTRAIT', 'yearlyEnergyDcKwh': +# 325.05405, 'segmentIndex': 1}, {'center': {'latitude': 50.6673828, 'longitude': -4.7416664}, 'orientation': +# 'PORTRAIT', 'yearlyEnergyDcKwh': 321.63647, 'segmentIndex': 1}, {'center': {'latitude': 50.667378, 'longitude': +# -4.741653599999999}, 'orientation': 'PORTRAIT', 'yearlyEnergyDcKwh': 321.46332, 'segmentIndex': 1}, {'center': { +# 'latitude': 50.667373299999994, 'longitude': -4.7416409}, 'orientation': 'PORTRAIT', 'yearlyEnergyDcKwh': 339.3016, +# 'segmentIndex': 1}, {'center': {'latitude': 50.6673853, 'longitude': -4.7416298}, 'orientation': 'PORTRAIT', +# 'yearlyEnergyDcKwh': 327.26282, 'segmentIndex': 1}, {'center': {'latitude': 50.667399499999995, 'longitude': +# -4.741668}, 'orientation': 'PORTRAIT', 'yearlyEnergyDcKwh': 314.9878, 'segmentIndex': 1}, {'center': {'latitude': +# 50.6673948, 'longitude': -4.7416553}, 'orientation': 'PORTRAIT', 'yearlyEnergyDcKwh': 314.16364, 'segmentIndex': +# 1}, {'center': {'latitude': 50.667390000000005, 'longitude': -4.7416425}, 'orientation': 'PORTRAIT', +# 'yearlyEnergyDcKwh': 310.3404, 'segmentIndex': 1}, {'center': {'latitude': 50.6674186, 'longitude': -4.7417191}, +# 'orientation': 'PORTRAIT', 'yearlyEnergyDcKwh': 278.3281, 'segmentIndex': 1}]}, 'boundingBox': {'sw': {'latitude': +# 50.6672904, 'longitude': -4.741778}, 'ne': {'latitude': 50.667431199999996, 'longitude': -4.7415536}}, +# 'imageryQuality': 'MEDIUM', 'imageryProcessedDate': {'year': 2024, 'month': 4, 'day': 18}} diff --git a/etl/customers/places_for_people/route_march.py b/etl/customers/places_for_people/route_march.py index c38c71d3..5da1c2f7 100644 --- a/etl/customers/places_for_people/route_march.py +++ b/etl/customers/places_for_people/route_march.py @@ -1,4 +1,5 @@ import os +import time import pandas as pd from tqdm import tqdm @@ -33,7 +34,7 @@ def app(): lst = [ pfp_property["ADDRESS"], pfp_property["ADDRESS.1"], - pfp_property["ADDRESS.2"], + # pfp_property["ADDRESS.2"], pfp_property["POSTCODE"] ] lst = [str(x).strip() for x in lst if not pd.isnull(x)] @@ -135,3 +136,165 @@ def app(): # Store as an excel filename = "Places For People EPC data.xlsx" asset_list.to_excel(filename, index=False) + + +# TODO: TEMP +# This script takes in a a list of properties +# Will be postcode and address + +import requests +import numpy as np +import pandas as pd +from bs4 import BeautifulSoup +from tqdm import tqdm + +SEARCH_POSTCODE_URL = ("https://find-energy-certificate.service.gov.uk/find-a-certificate/search-by-postcode?postcode" + "={postcode_input}") +BASE_ENERGY_URL = "https://find-energy-certificate.service.gov.uk" + + +def retrieve_find_my_epc_data(postcode: str, address: str): + """ + For a post code and address, we pull out all the required data from the find my epc website + """ + + headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) ' + 'Chrome/111.0.0.0 Safari/537.36'} + postcode_input = postcode.replace(" ", "+") + postcode_search = SEARCH_POSTCODE_URL.format(postcode_input=postcode_input) + postcode_response = requests.get(postcode_search, headers=headers) + + postcode_res = BeautifulSoup(postcode_response.text) + address_links_full = postcode_res.findAll('a', {'class': 'govuk-link', 'rel': 'nofollow'}) + address_links = {element.text.lstrip().rstrip(): BASE_ENERGY_URL + element['href'] for element in + address_links_full} + + # TODO: to check the logic works for all cases but seems to be good + index_of_address = [key.lower().startswith(address) for key in list(address_links.keys())] + chosen_epc = address_links[list(address_links.keys())[np.where(index_of_address)[0][0]]] + + epc_certificate = chosen_epc.split('/')[-1] + + address_response = requests.get(chosen_epc, headers=headers) + address_res = BeautifulSoup(address_response.text) + + # print("## Energy rating - current and potential") + ratings = address_res.find('desc', {'id': 'svg-desc'}).text + + # print('### Current EPC rating') + current_rating = ratings.split(".")[0] + # print("##### " + current_rating) + + # print('### Potential EPC rating') + potential_rating = ratings.split(".")[1] + # print("##### " + potential_rating) + + new_property_df = pd.DataFrame( + {'address': [address], + 'epc_certificate': [epc_certificate], + 'current_epc_rating': [current_rating.split(' ')[-6]], + 'current_epc_efficiency': [current_rating.split(' ')[-1]], + 'potential_epc_rating': [potential_rating.split(' ')[-6]], + "potential_epc_efficiency": [potential_rating.split(' ')[-1]]} + ) + + # print("Find assessor") + assessor_block = address_res.find('div', {'class': 'epc-contact-assessor'}) + assessor_fields = assessor_block.find_all('dd', {"class": 'govuk-summary-list__value govuk-!-width-one-half'}) + assessor_name = assessor_fields[0].text.strip() + assessor_number = assessor_fields[1].text.strip() + assessor_email = assessor_fields[2].text.strip() + + new_property_df['assessor_name'] = assessor_name + new_property_df['assessor_number'] = assessor_number + new_property_df['assessor_email'] = assessor_email + + return new_property_df + + # print('### Changes that can be made:') + # improvements = address_res.find('div', {"class": "govuk-body printable-area epb-recommended-improvements"}) + + # if improvements is None: + # print("No changes suggested") + # else: + # changes = improvements.find_all('h3') + # changes_impact = improvements.find_all('dl', {"class": 'govuk-summary-list'}) + + # for element in zip(changes, changes_impact): + # improvement_header = element[0].text + # print("#### " + improvement_header) + + # improvement_text = element[1].text + # print(improvement_text) + + # col_name = improvement_header.split(":")[1] + # cost = element[1].find('dd', {"class": "govuk-summary-list__value"}).text.lstrip().rstrip() + + # impact = element[1].find('text', {"class": "govuk-!-font-weight-bold"}).text.split(" ") + # impact_num = impact[0] + # impact_cat = impact[1] + # print(cost) + # new_property_df[col_name] = True + # # cost_column = col_name + '-cost' + # # new_property_df.assign(cost_column=cost) + # new_property_df[col_name + '-cost'] = cost + # new_property_df[col_name + '-impact_num'] = impact_num + # new_property_df[col_name + '-impact_cat'] = impact_cat + + # data = pd.concat([data, new_property_df]) + # data.to_csv('./portfolio.csv') + + +def main(): + """ + Main pipeline function to take in a predefined list of properties and extract names of contractors + """ + + # Load in list of properties + addresses_df = pd.read_excel("/Users/khalimconn-kowlessar/Downloads/Places For People EPC data.xlsx") + addresses_df["uprn"] = addresses_df["uprn"].astype("Int64").astype(str) + # 1256 + + find_my_epc_data_list = [] + for i, row in tqdm(addresses_df.iterrows(), total=addresses_df.shape[0]): + + if pd.isnull(row['Matched EPC Address']): + continue + # 10 second break every 50 iterations + if (i % 50 == 0) and (i != 0): + time.sleep(10) + time.sleep(1) + if row['Matched EPC Address'] == "6 CHURCHWOOD, CHURCH STREET, CRAMLINGTON": + address_data = retrieve_find_my_epc_data( + postcode=row['POSTCODE'], + address=" ".join([str(row["ADDRESS"]), row["ADDRESS.1"]]).lower() + ) + else: + address_data = retrieve_find_my_epc_data( + postcode=row['POSTCODE'], + address=", ".join(row['Matched EPC Address'].split(", ")[:-1]).lower() + ) + + address_data.insert(0, "uprn", row["uprn"]) + + find_my_epc_data_list.append(address_data) + + find_my_epc_data = pd.concat(find_my_epc_data_list) + + find_my_epc_data.to_csv('find_my_epc_data.csv') + + find_my_epc_data = find_my_epc_data.drop_duplicates("uprn") + + # Match back to addresses + addresses_df2 = addresses_df.merge( + find_my_epc_data, + how="left", + on="uprn" + ) + + addresses_df2.to_excel("Places For People EPC data with surveyor.xlsx", index=False) + + +if __name__ == "__main__": + main() diff --git a/etl/eligibility/ha_15_32/ha_analysis_batch_3.py b/etl/eligibility/ha_15_32/ha_analysis_batch_3.py index f99c7b1a..aca36584 100644 --- a/etl/eligibility/ha_15_32/ha_analysis_batch_3.py +++ b/etl/eligibility/ha_15_32/ha_analysis_batch_3.py @@ -24,9 +24,13 @@ from etl.epc_clean.epc_attributes.RoofAttributes import RoofAttributes from etl.epc.DataProcessor import EPCDataProcessor from datetime import datetime +import inspect + +src_file_path = inspect.getfile(lambda: None) + EPC_AUTH_TOKEN = os.getenv("EPC_AUTH_TOKEN") -ENV_FILE = Path(__file__).parent / "etl" / "eligibility" / "ha_15_32" / ".env" -DATA_FOLDER = Path(__file__).parent / "local_data" / "ha_data" +ENV_FILE = Path(src_file_path).parent / "etl" / "eligibility" / "ha_15_32" / ".env" +DATA_FOLDER = Path(src_file_path).parent / "local_data" / "ha_data" logger = setup_logger() load_dotenv(ENV_FILE) @@ -6127,6 +6131,9 @@ def classify_loft(x): def fml_analysis(loader): + # In the case of the optimistic scenario, we assume that the at-risk pipeline is still viable, just at a lower rate + optimistic_scenario_rate = 1500 + assumed_ciga_pass_rate = 0.731 has_bruh = [ "HA7", "HA14", "HA25", "HA39", "HA16", "HA28", "HA13", @@ -6224,6 +6231,7 @@ def fml_analysis(loader): if fuck_this.shape[0] != before_merge_shape: raise Exception("SOMETHING WENT WRONG") + # Automated archetype check if any(fuck_this["ECO Eligibility"].str.contains("subject to archetype")): # We perform the archetype test. If the property is a house, we it needs to be detached, semi-detached # or end terrace. If it's a bungalow, it must be attached @@ -6319,6 +6327,7 @@ def fml_analysis(loader): ] # Characterise no CIGA check needed + # !!!!!!!!!!!! AT RISK !!!!!!!!!!!! ciga_check_passed = had_survey[had_survey["ECO Eligibility"] == "eco4 - passed ciga"] # These should be treated the same as one that have passed their ciga checks, from a detection perspective ciga_check_passed_eligible = ciga_check_passed[ @@ -6392,6 +6401,12 @@ def fml_analysis(loader): identified_as_gbis_looks_like_eco4 ) + # This is the work that is at risk + eco4_work_at_risk = ( + passed_ciga_expectation + + ciga_check_expectation + ) + no_ciga_check_needed_actually_gbis = no_ciga_check_needed_eligible_gbis.shape[0] gbis_qualified = gbis_qualified.shape[0] @@ -6490,11 +6505,13 @@ def fml_analysis(loader): # "Of which sold": sales_since_nov, "EPC verified ECO4 Eligible - Remaining": int(total_eco4_expectation), "EPC verified GBIS Eligibile - Remaining": int(total_gbis_expectation), + # At risk work + "Work at risk due to audits": eco4_work_at_risk } ) results_df = pd.DataFrame(results) - results_df.to_csv("analysis - revised.csv") + results_df.to_csv("analysis - revised - audit update.csv") # results_df["Delta vs November"] = 100 * ( # results_df["Of which ECO4 Eligible - Remaining"] - results_df["Original ECO4 Estimate - Remaining"] @@ -6509,7 +6526,7 @@ def create_final_report(): This function will produce the final output for the HA analysis :return: """ - epc_validated_results = pd.read_csv("analysis - revised.csv") + epc_validated_results = pd.read_csv("analysis - revised - audit update.csv") pipeline_results = pd.read_csv("pipeline_remaining_raw.csv") #################################### @@ -6593,12 +6610,14 @@ def create_final_report(): [ "HA Name", "EPC verified ECO4 Eligible - Remaining", - "EPC verified GBIS Eligibile - Remaining" + "EPC verified GBIS Eligibile - Remaining", + "Work at risk due to audits" ] ].copy().rename( columns={ "EPC verified ECO4 Eligible - Remaining": "# ECO4 remaining - From EPC Database (post CIGA)", "EPC verified GBIS Eligibile - Remaining": "# GBIS remaining - From EPC Database (post CIGA)", + "Work at risk due to audits": "ECO4 remaining work at risk due to Audits", } ) @@ -6623,7 +6642,8 @@ def create_final_report(): '# ECO4 remaining - All HA Summary', '# ECO4 remaining - Postcode list (pre CIGA)', '# ECO4 remaining - Postcode list (post CIGA)', - '# ECO4 remaining - From EPC Database (post CIGA)' + '# ECO4 remaining - From EPC Database (post CIGA)', + 'ECO4 remaining work at risk due to Audits' ]: revenue[col] = revenue[col] * 1710 @@ -6688,8 +6708,8 @@ def create_final_report(): # "# GBIS remaining - Postcode list (post CIGA)"]] # Store final outputs - volumes.to_csv("HA Analysis Final - volumes.csv") - revenue.to_csv("HA Analysis Final - revenue.csv") + volumes.to_csv("HA Analysis - Audit Update - volumes.csv") + revenue.to_csv("HA Analysis - Audit Update - revenue.csv") def identify_eco_works(loader): @@ -7203,84 +7223,96 @@ def app(): loader.load() loader.ha_facts_and_figures() + # import pickle + # with open("ha_analysis_data_temp.pkl", "wb") as f: + # pickle.dump(loader, f) + # import pickle + # with open("ha_analysis_data_temp.pkl", "rb") as f: + # loader = pickle.load(f) + forecast_remaining_sales(loader) + # Functions to produce the final output lol... + # fml_data_pull(loader) # If we need to pull EPC data + fml_analysis(loader) + create_final_report() + # Adhoc - for HA16, get the properties that still need a CIGA check - asset_list_ha16 = loader.data["HA16"]["asset_list"].copy() - ha_16_need_ciga = asset_list_ha16[ - asset_list_ha16["ECO Eligibility"].str.contains("subject to ciga") - ] - completed_cigas = loader.data["HA16"]["ciga_list"].copy() - # Store the results - ha_16_need_ciga.to_csv("ha16_need_ciga.csv") - completed_cigas.to_csv("ha16_completed_cigas.csv") - - # Adhoc - look at the current pipeline and identify how many dormant, CIGA dependent properties there are for - # live projects - - # Read excel - orderbook_filepath = "local_data/ha_data/Warmfront HA client order book overview_20240129.xlsx" - orderbook_workbook = openpyxl.load_workbook(orderbook_filepath) - orderbook_sheet = orderbook_workbook["Contractual Info"] - orderbook_colnames = [cell.value for cell in orderbook_sheet[1]] - - rows = [] - for row in orderbook_sheet.iter_rows(min_row=2, values_only=False): - row_data = [cell.value for cell in row] # This will get you the cell values - rows.append(row_data) - - orderbook = pd.DataFrame(rows, columns=orderbook_colnames) - live_orderbook = orderbook[orderbook["Live, New, or Historic?"] == "LIVE"].copy() - live_orderbook['Redacted HA'] = live_orderbook['Redacted HA'].str.replace(" ", "") - - dormant_properties = [] - missed_has = [] - for _, customer in live_orderbook.iterrows(): - if customer['Redacted HA'] not in loader.data.keys(): - missed_has.append(customer['Redacted HA']) - continue - asset_list = loader.data[customer['Redacted HA']]["asset_list"].copy() - survey_list = loader.data[customer['Redacted HA']]["survey_list"].copy() - # Remove sold - if not survey_list.empty: - survey_list = survey_list[~pd.isnull(survey_list["asset_list_row_id"])] - asset_list = asset_list.merge( - survey_list[["asset_list_row_id", "installation_status"]], - how="left", - on="asset_list_row_id" - ) - # Anything that has an installation has gone to installation, and therefore is not remaining - asset_list = asset_list[pd.isnull(asset_list["installation_status"])] - asset_list = asset_list.drop(columns=["installation_status"]) - - # We pull out the properties that need a CIGA check - need_ciga = asset_list[asset_list["ECO Eligibility"] == "eco4 (subject to ciga)"] - need_archetype = asset_list[asset_list["ECO Eligibility"] == "eco4 (subject to archetype)"] - need_ciga_and_archetype = asset_list[ - asset_list["ECO Eligibility"] == "eco4 (subject to ciga) (subject to archetype)" - ] - - dormant_properties.append( - { - "HA Name": customer['Redacted HA'], - "Need CIGA": need_ciga.shape[0], - "Need Archetype": need_archetype.shape[0], - "Need CIGA and Archetype": need_ciga_and_archetype.shape[0] - } - ) - - dormant_properties = pd.DataFrame(dormant_properties) - totals = dormant_properties.sum() - totals["HA Name"] = "Total" - - dormant_properties = pd.concat([dormant_properties, totals.to_frame().T]) - dormant_properties.to_csv("dormant_properties.csv") - - loader.december_figures["ECO4 remaining"].sum() - december_figures = loader.december_figures.copy() - december_figures["ECO4 remaining"] = np.where( - december_figures["ECO4 remaining"] < 0, - 0, - december_figures["ECO4 remaining"] - ) - december_figures["ECO4 remaining"].sum() + # asset_list_ha16 = loader.data["HA16"]["asset_list"].copy() + # ha_16_need_ciga = asset_list_ha16[ + # asset_list_ha16["ECO Eligibility"].str.contains("subject to ciga") + # ] + # completed_cigas = loader.data["HA16"]["ciga_list"].copy() + # # Store the results + # ha_16_need_ciga.to_csv("ha16_need_ciga.csv") + # completed_cigas.to_csv("ha16_completed_cigas.csv") + # + # # Adhoc - look at the current pipeline and identify how many dormant, CIGA dependent properties there are for + # # live projects + # + # # Read excel + # orderbook_filepath = "local_data/ha_data/Warmfront HA client order book overview_20240129.xlsx" + # orderbook_workbook = openpyxl.load_workbook(orderbook_filepath) + # orderbook_sheet = orderbook_workbook["Contractual Info"] + # orderbook_colnames = [cell.value for cell in orderbook_sheet[1]] + # + # rows = [] + # for row in orderbook_sheet.iter_rows(min_row=2, values_only=False): + # row_data = [cell.value for cell in row] # This will get you the cell values + # rows.append(row_data) + # + # orderbook = pd.DataFrame(rows, columns=orderbook_colnames) + # live_orderbook = orderbook[orderbook["Live, New, or Historic?"] == "LIVE"].copy() + # live_orderbook['Redacted HA'] = live_orderbook['Redacted HA'].str.replace(" ", "") + # + # dormant_properties = [] + # missed_has = [] + # for _, customer in live_orderbook.iterrows(): + # if customer['Redacted HA'] not in loader.data.keys(): + # missed_has.append(customer['Redacted HA']) + # continue + # asset_list = loader.data[customer['Redacted HA']]["asset_list"].copy() + # survey_list = loader.data[customer['Redacted HA']]["survey_list"].copy() + # # Remove sold + # if not survey_list.empty: + # survey_list = survey_list[~pd.isnull(survey_list["asset_list_row_id"])] + # asset_list = asset_list.merge( + # survey_list[["asset_list_row_id", "installation_status"]], + # how="left", + # on="asset_list_row_id" + # ) + # # Anything that has an installation has gone to installation, and therefore is not remaining + # asset_list = asset_list[pd.isnull(asset_list["installation_status"])] + # asset_list = asset_list.drop(columns=["installation_status"]) + # + # # We pull out the properties that need a CIGA check + # need_ciga = asset_list[asset_list["ECO Eligibility"] == "eco4 (subject to ciga)"] + # need_archetype = asset_list[asset_list["ECO Eligibility"] == "eco4 (subject to archetype)"] + # need_ciga_and_archetype = asset_list[ + # asset_list["ECO Eligibility"] == "eco4 (subject to ciga) (subject to archetype)" + # ] + # + # dormant_properties.append( + # { + # "HA Name": customer['Redacted HA'], + # "Need CIGA": need_ciga.shape[0], + # "Need Archetype": need_archetype.shape[0], + # "Need CIGA and Archetype": need_ciga_and_archetype.shape[0] + # } + # ) + # + # dormant_properties = pd.DataFrame(dormant_properties) + # totals = dormant_properties.sum() + # totals["HA Name"] = "Total" + # + # dormant_properties = pd.concat([dormant_properties, totals.to_frame().T]) + # dormant_properties.to_csv("dormant_properties.csv") + # + # loader.december_figures["ECO4 remaining"].sum() + # december_figures = loader.december_figures.copy() + # december_figures["ECO4 remaining"] = np.where( + # december_figures["ECO4 remaining"] < 0, + # 0, + # december_figures["ECO4 remaining"] + # ) + # december_figures["ECO4 remaining"].sum()