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.*
|