Spaces:
Sleeping
Sleeping
File size: 12,296 Bytes
27842e4 00b70ec 27842e4 0ba1155 27842e4 1470e01 e83f295 1661019 ebd99a3 0da2389 adbb525 f54358b bb0256a 27842e4 0da2389 33cad06 39663f4 33cad06 0da2389 adbb525 d961ef7 2516134 6126b25 adbb525 2516134 6126b25 dc622ef adbb525 dc622ef adbb525 2516134 6126b25 adbb525 2516134 bb0256a adbb525 6126b25 0ba1155 adbb525 0ba1155 47c89eb 0ba1155 adbb525 0ba1155 f54358b 857736e f54358b 2ff4dc9 f54358b fc423f0 2ff4dc9 f54358b 0ba1155 857736e f54358b adbb525 47c89eb f54358b 47c89eb f86d7a2 f54358b 2ff4dc9 adbb525 2ff4dc9 33cad06 47c89eb 2ff4dc9 47c89eb 33cad06 47c89eb dc622ef 47c89eb 33cad06 0ba1155 33cad06 adbb525 1504a7c adbb525 1504a7c 7a7462f 1504a7c adbb525 d2645ba 1504a7c adbb525 1504a7c adbb525 1504a7c adbb525 d2645ba adbb525 1504a7c adbb525 1504a7c adbb525 1504a7c adbb525 7a7462f adbb525 7a7462f adbb525 1504a7c 7a7462f 1504a7c d2645ba 2ff4dc9 7a7462f 2ff4dc9 dc622ef 2ff4dc9 adbb525 2ff4dc9 adbb525 005fd45 adbb525 005fd45 adbb525 005fd45 adbb525 005fd45 adbb525 2ff4dc9 adbb525 d2645ba adbb525 d2645ba adbb525 005fd45 adbb525 d2645ba adbb525 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 | import pandas as pd
import numpy as np
import gradio as gr
import matplotlib.pyplot as plt
import h3.api.basic_str as h3
import folium
from shapely.geometry import Polygon
import geopandas as gpd
import tempfile
import zipfile
import os
import glob
from shapely.ops import unary_union
# Define feature lists & descriptions
PRIMARY_FEATURES = ['OEM Critical Facilities', 'High Frequency Flooding', 'Low Frequency Flooding', 'METRO', 'HPW Critical Facilities', 'Other Critical Facilities']
SECONDARY_FEATURES = ['PCI', 'ADT', 'RPL_THEME_1', 'RPL_THEME_2', 'RPL_THEME_3', 'RPL_THEME_4', 'RPL_THEME_ALL']
ALL_FEATURES = PRIMARY_FEATURES + SECONDARY_FEATURES
EXTRA_COLUMNS = ['PCI Score', 'SURFACETYP', 'METRO_ROUTES', 'STREETNAME']
FEATURE_DESCRIPTIONS = {
'OEM Critical Facilities': 'Distance to HFD, HPD, hospitals, and OEM facilities.',
'HPW Critical Facilities': 'Distance to Houston Water facilities.',
'Other Critical Facilities': 'Distance to NGOs, private entities, MSC, CC.',
'High Frequency Flooding': 'Roadway flooding instance for 2, 5, 10 and 25 Year Storm events.',
'Low Frequency Flooding': 'Roadway flooding instance for 50, 100 and 500 Year Storm events.',
'METRO': 'Number of METRO bus routes.',
'PCI': 'Pavement Condition Index.',
'ADT': 'Annual Average Daily Traffic.',
'RPL_THEME_1': 'Socioeconomic vulnerability (SVI theme 1).',
'RPL_THEME_2': 'Household characteristics (SVI theme 2).',
'RPL_THEME_3': 'Racial & ethnic minority (SVI theme 3).',
'RPL_THEME_4': 'Housing & transportation (SVI theme 4).',
'RPL_THEME_ALL': 'Overall social vulnerability (combined).'
}
# Load data including SurfaceTyp only once
def load_data(path="./MainInstances.xlsx"):
df = pd.read_excel(path, dtype={'SURFACETYP': str, 'METRO_ROUTES': str})
df.columns = df.columns.str.strip()
# Ensure SurfaceTyp and METRO_ROUTES columns exist
if 'SURFACETYP' not in df.columns:
df['SURFACETYP'] = ''
if 'METRO_ROUTES' not in df.columns:
df['METRO_ROUTES'] = ''
# Ensure other columns exist
for col in ['GRID_ID'] + ALL_FEATURES + ['PCI Score', 'STREETNAME']:
if col not in df.columns:
df[col] = 0
# Trim to needed columns
cols = ['GRID_ID'] + ALL_FEATURES + ['PCI Score', 'SURFACETYP', 'METRO_ROUTES', 'STREETNAME']
df = df[cols]
# Convert numeric fields
num_cols = ALL_FEATURES + ['PCI Score']
df[num_cols] = df[num_cols].apply(pd.to_numeric, errors='coerce').fillna(0)
# Clean SurfaceTyp and METRO_ROUTES
df['METRO_ROUTES'] = df['METRO_ROUTES'].fillna('').astype(str).str.strip()
df['SURFACETYP'] = df['SURFACETYP'].fillna('').astype(str).str.strip()
return df
# H3 boundary to coords
def h3_poly_coords(h3_id):
return [(lat, lon) for lat, lon in h3.cell_to_boundary(h3_id)]
# Compute weighted index
def calculate_index(df, features, weights):
w = np.array(weights) / np.sum(weights)
df_idx = df.copy()
df_idx['Index'] = df_idx[features].values.dot(w)
return df_idx
# Stats markdown
def stats_text(df_idx):
s = df_idx['Index'].describe()
return (
"### Index Statistics\n"
f"- Min: {s['min']:.4f}\n"
f"- 25%: {s['25%']:.4f}\n"
f"- Median: {s['50%']:.4f}\n"
f"- 75%: {s['75%']:.4f}\n"
f"- Max: {s['max']:.4f}"
)
# Histogram
def make_histogram(df_idx):
fig, ax = plt.subplots()
ax.hist(df_idx['Index'], bins=30)
ax.set_title('Index Distribution')
ax.set_xlabel('Index')
ax.set_ylabel('Frequency')
plt.tight_layout()
plt.close(fig)
return fig
# Create index map
def create_index_map(df_idx, features):
sel = df_idx[df_idx['Index'] >= df_idx['Index'].quantile(0.5)]
center = (29.76, -95.37)
if not sel.empty:
center = h3.cell_to_latlng(sel.iloc[0]['GRID_ID'])
m = folium.Map(location=center, zoom_start=10)
for _, row in sel.iterrows():
idx = row['Index']
fill_opacity = 0.6 if 3 <= idx <= 4 else 0
norm = (idx - sel['Index'].min()) / (sel['Index'].max() - sel['Index'].min()) if sel['Index'].max() > sel['Index'].min() else 0.5
color = f"rgb({int(255*norm)},0,{int(255*(1-norm))})"
popup = "<br>".join(
[f"{feat}: {row[feat]:.2f}" for feat in features] +
[f"{col}: {row[col]}" for col in EXTRA_COLUMNS] +
[f"Index: {idx:.2f}"]
)
folium.Polygon(
locations=h3_poly_coords(row['GRID_ID']),
color='black', fill=True, fill_color=color, fill_opacity=fill_opacity,
popup=popup
).add_to(m)
return m._repr_html_()
# Clustering logic
def find_clusters(hex_ids, min_size):
clusters, visited = [], set()
for h in hex_ids:
if h in visited: continue
cluster, stack = {h}, [h]
visited.add(h)
while stack:
cur = stack.pop()
for nb in h3.grid_disk(cur, 1):
if nb in hex_ids and nb not in visited:
visited.add(nb)
cluster.add(nb)
stack.append(nb)
if len(cluster) >= min_size:
clusters.append(cluster)
return clusters
# Generate clusters map/table and shapefile
def create_cluster_outputs(df_idx, threshold, min_size, pci_path="./PCI.json"):
# 1) Filter by threshold
filt = df_idx[df_idx['Index'] >= threshold]
if filt.empty:
return "No hexes above threshold.", None, None, None
# 2) Find clusters
clusters = find_clusters(filt['GRID_ID'].tolist(), min_size)
if not clusters:
return "No clusters found.", None, None, None
# 3) Flatten all hex IDs
ids = set().union(*clusters)
# 4) Build table of cluster hexes
table = df_idx[df_idx['GRID_ID'].isin(ids)][['GRID_ID', 'Index'] + EXTRA_COLUMNS]
# 5) Create Folium map centered on first hex
center = h3.cell_to_latlng(next(iter(ids)))
m = folium.Map(location=center, zoom_start=12)
# 6) Draw each hexagon with popup, and collect Shapely polygons
hex_polys = []
for _, row in table.iterrows():
h3_id = row['GRID_ID']
coords = h3_poly_coords(h3_id)
# HTML popup
popup_html = (
f"<b>Hex:</b> {h3_id}<br>"
f"<b>Index:</b> {row['Index']:.2f}<br>"
+ "<br>".join(f"{col}: {row[col]}" for col in EXTRA_COLUMNS)
)
folium.Polygon(
locations=coords,
color='blue', fill=True, fill_opacity=0.5,
popup=folium.Popup(popup_html, max_width=300)
).add_to(m)
# Note: Shapely expects (lon, lat)
hex_polys.append(Polygon([(lon, lat) for lat, lon in coords]))
# 7) Union hex polygons for the PCI intersection
cluster_union = unary_union(hex_polys)
# 8) Load PCI GeoJSON, set & transform CRS
if os.path.exists(pci_path):
pci_gdf = gpd.read_file(pci_path)
pci_gdf = pci_gdf.set_crs("EPSG:2278", allow_override=True).to_crs(epsg=4326)
# 9) Filter line segments that intersect the cluster_union
lines = pci_gdf[pci_gdf.intersects(cluster_union)]
if not lines.empty:
# 10) Serialize any Timestamp fields to string
for col in lines.columns:
if pd.api.types.is_datetime64_any_dtype(lines[col]):
lines[col] = lines[col].astype(str)
# 11) Add PCI lines as a GeoJson layer
folium.GeoJson(
data=lines.to_json(),
name="PCI Lines",
popup=folium.GeoJsonPopup(
fields=[c for c in lines.columns if c != 'geometry'],
aliases=[c.replace('_', ' ').title() for c in lines.columns if c != 'geometry'],
localize=True
),
style_function=lambda feat: {'color': 'red', 'weight': 2}
).add_to(m)
# 12) Add layer control so overlays are visible/toggleable
folium.LayerControl().add_to(m)
# 13) Generate the Shapefile ZIP
tmp = tempfile.mkdtemp()
base = os.path.join(tmp, 'focus_area')
# Crucial Fix: Shapely needs (lon, lat) while h3 provides (lat, lon)
shp_polys = [Polygon([(lon, lat) for lat, lon in h3.cell_to_boundary(h)]) for h in table['GRID_ID']]
gdf = gpd.GeoDataFrame(table, geometry=shp_polys, crs='EPSG:4326')
gdf.to_file(base + '.shp')
zip_path = base + '.zip'
with zipfile.ZipFile(zip_path, 'w') as zf:
for f in glob.glob(base + '.*'):
if not f.endswith('.zip'):
zf.write(f, arcname=os.path.basename(f))
# Return the zip_path to the Gradio file component instead of None
return "Clusters, PCI overlay, and Shapefile ready!", m._repr_html_(), table, zip_path
# Build Gradio app
def create_app():
# If load_data fails during initial boot without excel file, we can bypass or load a dummy
try:
df = load_data()
except FileNotFoundError:
df = pd.DataFrame()
with gr.Blocks() as app:
gr.Markdown("# Focus Area Identification by creating a Risk Index")
gr.Markdown("Build a custom risk index for Houston thoroughfare hexes by assigning feature weights.")
href = "https://www.mermaidchart.com/raw/7431d57c-8e4d-47ec-bb22-ec64c34f5d03?theme=light&version=v0.1&format=svg"
gr.Markdown(f"[This is a link to view the behind the scenes flow chart]({href})")
# Feature Weights
gr.Markdown("### Step 1: Assign weights to features")
init = pd.DataFrame({
"Feature": ALL_FEATURES,
"Description": [FEATURE_DESCRIPTIONS[f] for f in ALL_FEATURES],
"Weight": [0]*len(ALL_FEATURES)
})
wf = gr.Dataframe(value=init, headers=["Feature","Description","Weight"], datatype=["str","str","number"], interactive=True)
btn1 = gr.Button("Calculate Index")
stat_msg = gr.Markdown()
out_stats = gr.Markdown()
out_hist = gr.Plot()
out_map = gr.HTML()
def calc(wt):
try:
df_wt = pd.DataFrame(wt, columns=["Feature","Description","Weight"]) if isinstance(wt, list) else wt
df_wt['Weight'] = pd.to_numeric(df_wt['Weight'], errors='coerce').fillna(0)
sel = df_wt[df_wt['Weight']>0]
if not np.isclose(sel['Weight'].sum(), 100):
raise ValueError(f"Weights sum to {sel['Weight'].sum():.2f}, must be 100.")
df_idx = calculate_index(df, sel['Feature'].tolist(), sel['Weight'].tolist())
return "Risk Index created!", stats_text(df_idx), make_histogram(df_idx), create_index_map(df_idx, sel['Feature'].tolist())
except Exception as e:
return f"⚠️ Error calculating index: {e}", None, None, None
btn1.click(calc, inputs=[wf], outputs=[stat_msg, out_stats, out_hist, out_map])
# Clustering
gr.Markdown("### Step 2: Prioritized Focus Area Creation")
thr = gr.Number(label="Index Threshold", value=3)
ms = gr.Number(label="Minimum Cluster Size", value=3)
btn2 = gr.Button("Create Clusters")
cl_msg = gr.Markdown()
cl_map = gr.HTML()
cl_tbl = gr.Dataframe(label="Focus Area Hexes Data")
cl_zip = gr.File(label="Download Focus Area Shapefile Zip")
def cluster(wt, t, msize):
try:
df_wt = pd.DataFrame(wt, columns=["Feature","Description","Weight"]) if isinstance(wt, list) else wt
df_wt['Weight'] = pd.to_numeric(df_wt['Weight'], errors='coerce').fillna(0)
sel = df_wt[df_wt['Weight']>0]
if not np.isclose(sel['Weight'].sum(), 100):
raise ValueError(f"Weights sum to {sel['Weight'].sum():.2f}, must be 100.")
df_idx = calculate_index(df, sel['Feature'].tolist(), sel['Weight'].tolist())
return create_cluster_outputs(df_idx, t, int(msize))
except Exception as e:
return f"⚠️ Error clustering: {e}", None, None, None
btn2.click(cluster, inputs=[wf, thr, ms], outputs=[cl_msg, cl_map, cl_tbl, cl_zip])
return app
if __name__ == '__main__':
app = create_app()
app.launch() |