Commit
·
e0d8ab4
1
Parent(s):
60fa23b
publish app code
Browse files- README.md +4 -4
- app.py +208 -0
- examples.csv +0 -0
- explainer.py +111 -0
- mixed_buffers_ResNet_model.keras +3 -0
- mixed_buffers_standard_scaler.pkl +3 -0
- model.py +133 -0
- requirements.txt +4 -0
README.md
CHANGED
@@ -1,8 +1,8 @@
|
|
1 |
---
|
2 |
-
title:
|
3 |
-
emoji:
|
4 |
-
colorFrom:
|
5 |
-
colorTo:
|
6 |
sdk: gradio
|
7 |
sdk_version: 5.21.0
|
8 |
app_file: app.py
|
|
|
1 |
---
|
2 |
+
title: Play with an Urban Heat Island ResNet Model
|
3 |
+
emoji: 🔥
|
4 |
+
colorFrom: gray
|
5 |
+
colorTo: red
|
6 |
sdk: gradio
|
7 |
sdk_version: 5.21.0
|
8 |
app_file: app.py
|
app.py
ADDED
@@ -0,0 +1,208 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import gradio as gr
|
2 |
+
import shap
|
3 |
+
from model import UhiModel
|
4 |
+
from explainer import UhiExplainer
|
5 |
+
import numpy as np
|
6 |
+
import pandas as pd
|
7 |
+
import plotly.graph_objects as go
|
8 |
+
|
9 |
+
MODEL = UhiModel("mixed_buffers_ResNet_model.keras","mixed_buffers_standard_scaler.pkl")
|
10 |
+
|
11 |
+
def filter_map(uhi, longitude, latitude):
|
12 |
+
'''
|
13 |
+
This function generates a map based on uhi prediction
|
14 |
+
'''
|
15 |
+
#set up custom data
|
16 |
+
data = [uhi, longitude, latitude]
|
17 |
+
|
18 |
+
# Create the plot
|
19 |
+
fig = go.Figure(go.Scattermapbox(
|
20 |
+
lat=latitude,
|
21 |
+
lon=longitude,
|
22 |
+
mode='markers',
|
23 |
+
marker=go.scattermapbox.Marker(
|
24 |
+
size=6
|
25 |
+
),
|
26 |
+
hoverinfo="text",
|
27 |
+
hovertemplate='<b>UHI Index</b>: %{customdata[0]}<br><b>long</b>: %{customdata[1]}<br><b>lat</b>: %{customdata[2]}<br>',
|
28 |
+
customdata=data
|
29 |
+
))
|
30 |
+
|
31 |
+
fig.update_layout(
|
32 |
+
mapbox_style="open-street-map",
|
33 |
+
hovermode='closest',
|
34 |
+
mapbox=dict(
|
35 |
+
bearing=0,
|
36 |
+
center=go.layout.mapbox.Center(
|
37 |
+
lat=40.7128,
|
38 |
+
lon=-74.0060 # Default to New York City for initial view
|
39 |
+
),
|
40 |
+
pitch=0,
|
41 |
+
zoom=10
|
42 |
+
),
|
43 |
+
)
|
44 |
+
|
45 |
+
return fig
|
46 |
+
|
47 |
+
def predict(
|
48 |
+
longitude, latitude, m50_NPCRI, m100_Ground_Elevation, avg_wind_speed,
|
49 |
+
wind_direction, traffic_volume, m150_Ground_Elevation,
|
50 |
+
relative_humidity, m150_NDVI, m150_NDBI,
|
51 |
+
m300_SI, m300_NPCRI, m300_Coastal_Aerosol,
|
52 |
+
m300_Total_Building_Area_m2, m300_Building_Construction_Year, m300_Ground_Elevation,
|
53 |
+
m300_Building_Height, m300_Building_Count, m300_NDVI,
|
54 |
+
m300_NDBI, m300_Building_Density, solar_flux
|
55 |
+
):
|
56 |
+
'''
|
57 |
+
Predict the UHI index for the data inputed, Longitude and Latitude are used to generate a map
|
58 |
+
and do not affect the UHI index prediction.
|
59 |
+
'''
|
60 |
+
|
61 |
+
# Create a dictionary with input data and dataset var names
|
62 |
+
input_data = {
|
63 |
+
"50m_1NPCRI": m50_NPCRI,
|
64 |
+
"100m_Ground_Elevation": m100_Ground_Elevation,
|
65 |
+
"Avg_Wind_Speed": avg_wind_speed,
|
66 |
+
"Wind_Direction": wind_direction,
|
67 |
+
"Traffic_Volume": traffic_volume,
|
68 |
+
"150m_Ground_Elevation": m150_Ground_Elevation,
|
69 |
+
"Relative_Humidity": relative_humidity,
|
70 |
+
"150m_NDVI": m150_NDVI,
|
71 |
+
"150m_NDBI": m150_NDBI,
|
72 |
+
"300m_SI": m300_SI,
|
73 |
+
"300m_NPCRI": m300_NPCRI,
|
74 |
+
"300m_Coastal_Aerosol": m300_Coastal_Aerosol,
|
75 |
+
"300m_Total_Building_Area_m2": m300_Total_Building_Area_m2,
|
76 |
+
"300m_Building_Construction_Year": m300_Building_Construction_Year,
|
77 |
+
"300m_Ground_Elevation": m300_Ground_Elevation,
|
78 |
+
"300m_Building_Height": m300_Building_Height,
|
79 |
+
"300m_Building_Count": m300_Building_Count,
|
80 |
+
"300m_NDVI": m300_NDVI,
|
81 |
+
"300m_NDBI": m300_NDBI,
|
82 |
+
"300m_Building_Density": m300_Building_Density,
|
83 |
+
"Solar_Flux": solar_flux
|
84 |
+
}
|
85 |
+
|
86 |
+
# Convert to DataFrame
|
87 |
+
input_df = pd.DataFrame(input_data, index=[0])
|
88 |
+
|
89 |
+
#predict
|
90 |
+
uhi_index = MODEL.predict(input_df)
|
91 |
+
|
92 |
+
# explain the prediction
|
93 |
+
explainer = UhiExplainer(
|
94 |
+
model=MODEL.model,
|
95 |
+
explainer_type=shap.DeepExplainer,
|
96 |
+
X=input_df,
|
97 |
+
feature_names=input_df.columns,
|
98 |
+
ref_data=input_df,
|
99 |
+
shap_values=None # Compute SHAP values on the fly
|
100 |
+
)
|
101 |
+
reason = explainer.reasoning(index=0, location=(longitude, latitude))
|
102 |
+
|
103 |
+
# generate map
|
104 |
+
plot = filter_map(uhi_index, longitude, latitude)
|
105 |
+
|
106 |
+
return uhi_index, reason["uhi_status"], reason["feature_contributions"], plot
|
107 |
+
|
108 |
+
def load_examples(csv_file):
|
109 |
+
'''
|
110 |
+
Load examples from csv file
|
111 |
+
'''
|
112 |
+
# Read examples from CSV file
|
113 |
+
df = pd.read_csv(csv_file)
|
114 |
+
|
115 |
+
# Convert DataFrame to a list of lists
|
116 |
+
examples = df.values.tolist()
|
117 |
+
|
118 |
+
return examples
|
119 |
+
|
120 |
+
def load_interface():
|
121 |
+
'''
|
122 |
+
Configure Gradio interface
|
123 |
+
'''
|
124 |
+
|
125 |
+
#set blocks
|
126 |
+
info_page = gr.Blocks()
|
127 |
+
|
128 |
+
with info_page:
|
129 |
+
# set title and description
|
130 |
+
gr.Markdown(
|
131 |
+
"""
|
132 |
+
# ResNet model for Predicting Urban Heat Island (UHI) Index
|
133 |
+
|
134 |
+
**Contributors**: Francisco Lozano, Dalton Knapp, Adam Zizi\n
|
135 |
+
**University**: Depaul University\n
|
136 |
+
|
137 |
+
## Overview
|
138 |
+
Our project focused on creating a micro-scale machine learning model that predicts the locations and severity of the UHI effect.
|
139 |
+
The model used various datasets, including near-surface air temperatures, building footprint data, weather data, and
|
140 |
+
satellite data, to identify key drivers of UHI. This model provides insights into urban areas that are most affected by UHI,
|
141 |
+
enabling urban planners and policymakers to take effective mitigation actions.
|
142 |
+
>NOTE: The longitude and latitude inputs are used to identify the location of the prediction, but they do not affect the UHI index prediction.\n
|
143 |
+
|
144 |
+
## Repository
|
145 |
+
The code for this project is available on GitHub. It includes the model training, evaluation, and prediction scripts, as well as
|
146 |
+
the datasets used for training and testing. The repository also contains Jupyter notebooks that provide detailed explanations of the model's
|
147 |
+
architecture, training process, and evaluation metrics. The notebooks include visualizations of the model's performance and feature importance analysis.\n
|
148 |
+
[Project Repo](https://github.com/FranciscoLozCoding/cooling_with_code)
|
149 |
+
"""
|
150 |
+
)
|
151 |
+
|
152 |
+
# set inputs and outputs for the model
|
153 |
+
longitude = gr.Number(label="Longitude", precision=5, info="The Longitude of the location")
|
154 |
+
latitude = gr.Number(label="Latitude", precision=5, info="The Latitude of the location")
|
155 |
+
m50_NPCRI = gr.Number(label="50m NPCRI", precision=5, info="The average Normalized Difference Vegetation Index in a 50m Buffer Zone")
|
156 |
+
m100_Ground_Elevation = gr.Number(label="100m Ground Elevation", precision=5, info="The average Ground Elevation in a 100m Buffer Zone")
|
157 |
+
avg_wind_speed = gr.Number(label="Avg Wind Speed [m/s]", precision=5, info="The average Wind Speed at the location")
|
158 |
+
wind_direction = gr.Number(label="Wind Direction [degrees]", precision=5, info="The average Wind Direction at the location")
|
159 |
+
traffic_volume = gr.Number(label="Traffic Volume", precision=5, info="The Traffic Volume at the location")
|
160 |
+
m150_Ground_Elevation = gr.Number(label="150m Ground Elevation", precision=5, info="The average Ground Elevation in a 150m Buffer Zone")
|
161 |
+
relative_humidity = gr.Number(label="Relative Humidity [percent]", precision=5, info="The average Relative Humidity at the location")
|
162 |
+
m150_NDVI = gr.Number(label="150m NDVI", precision=5, info="The average Normalized Difference Vegetation Index in a 150m Buffer Zone")
|
163 |
+
m150_NDBI = gr.Number(label="150m NDBI", precision=5, info="The average Normalized Difference Built-up Index in a 150m Buffer Zone")
|
164 |
+
m300_SI = gr.Number(label="300m SI", precision=5, info="The average Shadow Index in a 300m Buffer Zone")
|
165 |
+
m300_NPCRI = gr.Number(label="300m NPCRI", precision=5, info="The average Normalized Pigment Chlorophyll Ratio Index in a 300m Buffer Zone")
|
166 |
+
m300_Coastal_Aerosol = gr.Number(label="300m Coastal Aerosol", precision=5, info="The average Coastal Aerosol in a 300m Buffer Zone")
|
167 |
+
m300_Total_Building_Area_m2 = gr.Number(label="300m Total Building Area(m2)", precision=5, info="The Total Building Area in a 300m Buffer Zone")
|
168 |
+
m300_Building_Construction_Year = gr.Number(label="300m Building Construction Year", precision=5, info="The average Building Construction Year in a 300m Buffer Zone")
|
169 |
+
m300_Ground_Elevation = gr.Number(label="300m Ground Elevation", precision=5, info="The average Ground Elevation in a 300m Buffer Zone")
|
170 |
+
m300_Building_Height = gr.Number(label="300m Building Height", precision=5, info="The average Building Height in a 300m Buffer Zone")
|
171 |
+
m300_Building_Count = gr.Number(label="300m Building Count", precision=5, info="The average Building Count in a 300m Buffer Zone")
|
172 |
+
m300_NDVI = gr.Number(label="300m NDVI", precision=5, info="The average Normalized Difference Vegetation Index in a 300m Buffer Zone")
|
173 |
+
m300_NDBI = gr.Number(label="300m NDBI", precision=5, info="The average Normalized Difference Built-up Index in a 300m Buffer Zone")
|
174 |
+
m300_Building_Density = gr.Number(label="300m Building Density", precision=5, info="The average Building Density in a 300m Buffer Zone")
|
175 |
+
solar_flux = gr.Number(label="Solar Flux [W/m^2]", precision=5, info="The average Solar Flux at the location")
|
176 |
+
inputs = [longitude, latitude, m50_NPCRI, m100_Ground_Elevation, avg_wind_speed, wind_direction,
|
177 |
+
traffic_volume, m150_Ground_Elevation, relative_humidity, m150_NDVI,
|
178 |
+
m150_NDBI, m300_SI, m300_NPCRI, m300_Coastal_Aerosol, m300_Total_Building_Area_m2,
|
179 |
+
m300_Building_Construction_Year, m300_Ground_Elevation, m300_Building_Height, m300_Building_Count,
|
180 |
+
m300_NDVI, m300_NDBI, m300_Building_Density, solar_flux]
|
181 |
+
uhi = gr.number(label="Predicted UHI Index", precision=5)
|
182 |
+
|
183 |
+
# set model explainer outputs
|
184 |
+
uhi_label = gr.Label(label="Predicted Status based on UHI Index")
|
185 |
+
feature_contributions = gr.JSON(label="Feature Contributions", info="The contributions of each feature to the UHI index prediction")
|
186 |
+
|
187 |
+
# Urban Location
|
188 |
+
plot = gr.Plot(label="Urban Location", info="A plot showing the location of the prediction based on the longitude and latitude inputs")
|
189 |
+
|
190 |
+
model_page = gr.Interface(
|
191 |
+
predict,
|
192 |
+
inputs=inputs,
|
193 |
+
outputs=[uhi, uhi_label, feature_contributions, plot],
|
194 |
+
live=True,
|
195 |
+
examples=load_examples("examples.csv"),
|
196 |
+
title="Interact with The ResNet UHI Model",
|
197 |
+
description="This model predicts the Urban Heat Island (UHI) index based on various environmental and urban factors. Adjust the inputs to see how they affect the UHI index prediction.",
|
198 |
+
)
|
199 |
+
|
200 |
+
iface = gr.TabbedInterface(
|
201 |
+
[info_page, model_page],
|
202 |
+
["Information", "UHI Model"]
|
203 |
+
)
|
204 |
+
|
205 |
+
iface.launch(server_name="0.0.0.0", server_port=7860, allowed_paths=["/"])
|
206 |
+
|
207 |
+
if __name__ == "__main__":
|
208 |
+
load_interface()
|
examples.csv
ADDED
The diff for this file is too large to render.
See raw diff
|
|
explainer.py
ADDED
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""This module provides an explainer for the model."""
|
2 |
+
|
3 |
+
import shap
|
4 |
+
import matplotlib.pyplot as plt
|
5 |
+
import pandas as pd
|
6 |
+
import numpy as np
|
7 |
+
|
8 |
+
class UhiExplainer:
|
9 |
+
"""
|
10 |
+
A class for SHAP-based model explanation.
|
11 |
+
|
12 |
+
Attributes:
|
13 |
+
- model: Trained model (e.g., RandomForestRegressor, XGBRegressor).
|
14 |
+
- explainer_type: SHAP explainer class (e.g., shap.TreeExplainer, shap.KernelExplainer).
|
15 |
+
- X: Data (Pandas DataFrame) used to compute SHAP values.
|
16 |
+
- feature_names: List of feature names.
|
17 |
+
- explainer: SHAP explainer instance.
|
18 |
+
- shap_values: Computed SHAP values.
|
19 |
+
|
20 |
+
Methods:
|
21 |
+
- apply_shap(): Computes SHAP values.
|
22 |
+
- summary_plot(): Generates a SHAP summary plot.
|
23 |
+
- bar_plot(): Generates a bar chart of feature importance.
|
24 |
+
- dependence_plot(): Generates a dependence plot for a feature.
|
25 |
+
- force_plot(): Generates a force plot for an individual prediction.
|
26 |
+
- init_js(): Initializes SHAP for Jupyter Notebook.
|
27 |
+
- reasoning(): Provides insights on why a record received a high or low UHI index.
|
28 |
+
"""
|
29 |
+
|
30 |
+
def __init__(self, model, explainer_type, X, feature_names, ref_data=None, shap_values=None):
|
31 |
+
"""
|
32 |
+
Initializes the Explainer with a trained model, explainer type, and dataset.
|
33 |
+
|
34 |
+
Parameters:
|
35 |
+
- model: Trained model (e.g., RandomForestRegressor, XGBRegressor).
|
36 |
+
- explainer_type: SHAP explainer class (e.g., shap.TreeExplainer, shap.KernelExplainer).
|
37 |
+
- X: Data (Pandas DataFrame) used to compute SHAP values.
|
38 |
+
- feature_names: List of feature names.
|
39 |
+
- ref_data (optional): The reference dataset (background dataset) is used by SHAP to estimate the expected output of the model
|
40 |
+
- shap_values (optional): Precomputed SHAP values
|
41 |
+
"""
|
42 |
+
self.model = model
|
43 |
+
self.explainer_type = explainer_type
|
44 |
+
self.X = np.array(X) if isinstance(X, pd.DataFrame) else X # Ensure NumPy format
|
45 |
+
if ref_data is not None:
|
46 |
+
ref_data = np.array(ref_data) if isinstance(ref_data, pd.DataFrame) else ref_data # Ensure NumPy format
|
47 |
+
self.feature_names = feature_names
|
48 |
+
self.explainer = explainer_type(model, ref_data) # Initialize explainer
|
49 |
+
# Compute SHAP values
|
50 |
+
if shap_values is not None:
|
51 |
+
self.shap_values = shap_values
|
52 |
+
else:
|
53 |
+
self.shap_values = self.explainer.shap_values(self.X, check_additivity=False) if self.explainer_type == shap.DeepExplainer else self.explainer.shap_values(self.X)
|
54 |
+
# Apply squeeze only if the array has three dimensions and the last dimension is 1
|
55 |
+
if self.shap_values.ndim == 3 and self.shap_values.shape[-1] == 1:
|
56 |
+
self.shap_values = np.squeeze(self.shap_values)
|
57 |
+
|
58 |
+
def reasoning(self, index=0, location=(None, None)):
|
59 |
+
"""
|
60 |
+
Provides insights on why the record received a high or low UHI index.
|
61 |
+
|
62 |
+
Parameters:
|
63 |
+
index (int): The index of the observation of interest.
|
64 |
+
location (tuple) (optional): The location of the record (long, lat).
|
65 |
+
|
66 |
+
Returns:
|
67 |
+
dict: The insights for the selected record.
|
68 |
+
"""
|
69 |
+
|
70 |
+
# Ensure expected_value is a single value (not tensor)
|
71 |
+
if self.explainer_type == shap.DeepExplainer:
|
72 |
+
expected_value = np.array(self.explainer.expected_value)
|
73 |
+
else:
|
74 |
+
expected_value = self.explainer.expected_value
|
75 |
+
|
76 |
+
# Extract single value if expected_value is an array
|
77 |
+
if isinstance(expected_value, np.ndarray):
|
78 |
+
expected_value = expected_value[0]
|
79 |
+
|
80 |
+
# Validate record index
|
81 |
+
if index >= len(self.shap_values) or index < 0:
|
82 |
+
return {"error": "Invalid record index"}
|
83 |
+
|
84 |
+
# Extract SHAP values for the specified record
|
85 |
+
record_shap_values = self.shap_values[index]
|
86 |
+
|
87 |
+
# Compute SHAP-based final prediction
|
88 |
+
shap_final_prediction = expected_value + sum(record_shap_values)
|
89 |
+
|
90 |
+
# Structure feature contributions
|
91 |
+
feature_contributions = [
|
92 |
+
{
|
93 |
+
"feature": feature,
|
94 |
+
"shap_value": value,
|
95 |
+
"impact": "increase" if value > 0 else "decrease"
|
96 |
+
}
|
97 |
+
for feature, value in zip(self.feature_names, record_shap_values)
|
98 |
+
]
|
99 |
+
|
100 |
+
# Create JSON structure
|
101 |
+
shap_json = {
|
102 |
+
"record_index": index,
|
103 |
+
"longitude": location[0],
|
104 |
+
"latitude": location[1],
|
105 |
+
"base_value": expected_value,
|
106 |
+
"shap_final_prediction": shap_final_prediction, # SHAP-based predicted value
|
107 |
+
"uhi_status": "Urban Heat Island" if shap_final_prediction > 1 else "Cooler Region",
|
108 |
+
"feature_contributions": feature_contributions,
|
109 |
+
}
|
110 |
+
|
111 |
+
return shap_json
|
mixed_buffers_ResNet_model.keras
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:a5420fdce9a338369c6c88bb33fcdfdc5bfb0ed8e674fdd64afa52453fcddbf1
|
3 |
+
size 43663843
|
mixed_buffers_standard_scaler.pkl
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:d3961d88dc1644f33012bcab972d6e44aec2a0028502412a009bbc58905f347d
|
3 |
+
size 1605
|
model.py
ADDED
@@ -0,0 +1,133 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import numpy as np
|
2 |
+
import pandas as pd
|
3 |
+
from tensorflow.keras.models import load_model
|
4 |
+
import pickle
|
5 |
+
|
6 |
+
class UhiModel:
|
7 |
+
"""
|
8 |
+
Urban Heat Island Model Class that can predict new instances
|
9 |
+
|
10 |
+
INPUTS
|
11 |
+
---
|
12 |
+
model_path: the path to the model file
|
13 |
+
scaler_path: the path to the standard scaler file
|
14 |
+
"""
|
15 |
+
def __init__(self, model_path, scaler_path):
|
16 |
+
self.model = load_model(model_path)
|
17 |
+
with open(scaler_path, 'rb') as f:
|
18 |
+
self.scaler = pickle.load(f)
|
19 |
+
|
20 |
+
def preprocess(self, df: pd.DataFrame) -> pd.DataFrame:
|
21 |
+
"""
|
22 |
+
Preprocess the input DataFrame to create new features for the model.
|
23 |
+
|
24 |
+
INPUT
|
25 |
+
-----
|
26 |
+
df: pd.DataFrame
|
27 |
+
The input DataFrame containing the features.
|
28 |
+
|
29 |
+
OUTPUT
|
30 |
+
------
|
31 |
+
pd.DataFrame
|
32 |
+
The preprocessed DataFrame with additional features.
|
33 |
+
"""
|
34 |
+
Wind_X = np.sin(df["Wind_Direction"])
|
35 |
+
Wind_Y = np.cos(df["Wind_Direction"])
|
36 |
+
|
37 |
+
m100_Elevation_Wind_X = df["100m_Ground_Elevation"] * df["Avg_Wind_Speed"] * Wind_X
|
38 |
+
m150_Elevation_Wind_Y = df["150m_Ground_Elevation"] * df["Avg_Wind_Speed"] * Wind_Y
|
39 |
+
m150_Humidity_NDVI = df["Relative_Humidity"] * df["150m_NDVI"]
|
40 |
+
m150_Traffic_NDBI = df["Traffic_Volume"] * df["150m_NDBI"]
|
41 |
+
m300_Building_Wind_X = df["300m_Building_Height"] * df["Avg_Wind_Speed"] * Wind_X
|
42 |
+
m300_Building_Wind_Y = df["300m_Building_Height"] * df["Avg_Wind_Speed"] * Wind_Y
|
43 |
+
m300_Elevation_Wind_Y = df["300m_Ground_Elevation"] * df["Avg_Wind_Speed"] * Wind_Y
|
44 |
+
m300_BldgHeight_Count = df["300m_Building_Height"] * df["300m_Building_Count"]
|
45 |
+
m300_TotalBuildingArea_NDVI = df["300m_Total_Building_Area_m2"] * df["300m_NDVI"]
|
46 |
+
m300_Traffic_NDVI = df["Traffic_Volume"] * df["300m_NDVI"]
|
47 |
+
m300_Traffic_NDBI = df["Traffic_Volume"] * df["300m_NDBI"]
|
48 |
+
m300_Building_Aspect_Ratio = df["300m_Building_Height"] / np.sqrt(df["300m_Total_Building_Area_m2"] + 1e-6)
|
49 |
+
m300_Sky_View_Factor = 1 - df["300m_Building_Density"]
|
50 |
+
m300_Canopy_Cover_Ratio = df["300m_NDVI"] / (df["300m_Building_Density"] + 1e-6)
|
51 |
+
m300_GHG_Proxy = df["300m_Building_Count"] * df["Traffic_Volume"] * df["Solar_Flux"]
|
52 |
+
|
53 |
+
output = {
|
54 |
+
"50m_1NPCRI": df["50m_1NPCRI"],
|
55 |
+
"100m_Elevation_Wind_X": m100_Elevation_Wind_X,
|
56 |
+
"150m_Traffic_Volume": df["Traffic_Volume"],
|
57 |
+
"150m_Elevation_Wind_Y": m150_Elevation_Wind_Y,
|
58 |
+
"150m_Humidity_NDVI": m150_Humidity_NDVI,
|
59 |
+
"150m_Traffic_NDBI": m150_Traffic_NDBI,
|
60 |
+
"300m_SI": df["300m_SI"],
|
61 |
+
"300m_NPCRI": df["300m_NPCRI"],
|
62 |
+
"300m_Coastal_Aerosol": df["300m_Coastal_Aerosol"],
|
63 |
+
"300m_Total_Building_Area_m2": df["300m_Total_Building_Area_m2"],
|
64 |
+
"300m_Building_Construction_Year": df["300m_Building_Construction_Year"],
|
65 |
+
"300m_Ground_Elevation": df["300m_Ground_Elevation"],
|
66 |
+
"300m_Building_Wind_X": m300_Building_Wind_X,
|
67 |
+
"300m_Building_Wind_Y": m300_Building_Wind_Y,
|
68 |
+
"300m_Elevation_Wind_Y": m300_Elevation_Wind_Y,
|
69 |
+
"300m_BldgHeight_Count": m300_BldgHeight_Count,
|
70 |
+
"300m_TotalBuildingArea_NDVI": m300_TotalBuildingArea_NDVI,
|
71 |
+
"300m_Traffic_NDVI": m300_Traffic_NDVI,
|
72 |
+
"300m_Traffic_NDBI": m300_Traffic_NDBI,
|
73 |
+
"300m_Building_Aspect_Ratio": m300_Building_Aspect_Ratio,
|
74 |
+
"300m_Sky_View_Factor": m300_Sky_View_Factor,
|
75 |
+
"300m_Canopy_Cover_Ratio": m300_Canopy_Cover_Ratio,
|
76 |
+
"300m_GHG_Proxy": m300_GHG_Proxy
|
77 |
+
}
|
78 |
+
|
79 |
+
return output
|
80 |
+
|
81 |
+
def scale(self, X):
|
82 |
+
"""
|
83 |
+
Apply the scaler used to train the model to the new data
|
84 |
+
|
85 |
+
INPUT
|
86 |
+
-----
|
87 |
+
X: the data to be scaled
|
88 |
+
|
89 |
+
OUTPUT
|
90 |
+
------
|
91 |
+
returns the scaled data
|
92 |
+
"""
|
93 |
+
|
94 |
+
new_data_scaled = self.scaler.transform(X)
|
95 |
+
|
96 |
+
return new_data_scaled
|
97 |
+
|
98 |
+
def predict(self, X: pd.DataFrame) -> float:
|
99 |
+
"""
|
100 |
+
Make a prediction on one sample using the loaded model.
|
101 |
+
|
102 |
+
INPUT
|
103 |
+
-----
|
104 |
+
X: pd.DataFrame
|
105 |
+
The data to predict a UHI index for. Must contain only one sample.
|
106 |
+
|
107 |
+
OUTPUT
|
108 |
+
------
|
109 |
+
str:
|
110 |
+
Predicted UHI index.
|
111 |
+
"""
|
112 |
+
|
113 |
+
# Check that input contains only one sample
|
114 |
+
if X.shape[0] != 1:
|
115 |
+
raise ValueError(f"Input array must contain only one sample, but {X.shape[0]} samples were found")
|
116 |
+
|
117 |
+
# Preprocess the input data to create new features
|
118 |
+
X_processed = self.preprocess(X)
|
119 |
+
|
120 |
+
# Scale the input data
|
121 |
+
X_scaled = self.scale(X_processed)
|
122 |
+
|
123 |
+
# Ensure the scaled data is 2D
|
124 |
+
X_scaled = X_scaled.reshape(1, -1)
|
125 |
+
|
126 |
+
# Make prediction
|
127 |
+
y_pred = self.model.predict(X_scaled)
|
128 |
+
|
129 |
+
# Extract the predicted UHI index (assuming it's a single value)
|
130 |
+
uhi = y_pred[0][0] if y_pred.ndim == 2 else y_pred[0]
|
131 |
+
|
132 |
+
# Return UHI
|
133 |
+
return uhi
|
requirements.txt
ADDED
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
1 |
+
gradio==5.14.0
|
2 |
+
shap==0.46.0
|
3 |
+
tensorflow[and-cuda]==2.18.0
|
4 |
+
plotly==6.0.*
|