Commit ·
c902913
0
Parent(s):
feat: initial release of GIS space
Browse files- README.md +19 -0
- app.py +277 -0
- requirements.txt +5 -0
README.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Spatial Network Analyzer
|
| 3 |
+
emoji: 🕸️
|
| 4 |
+
colorFrom: indigo
|
| 5 |
+
colorTo: purple
|
| 6 |
+
sdk: gradio
|
| 7 |
+
app_file: app.py
|
| 8 |
+
pinned: false
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
# Route Equity & Spatial Network Analyzer
|
| 12 |
+
|
| 13 |
+
An interactive computational tool designed for urban sociology, transportation equity, public policy, and digital humanities to evaluate spatial access, map route networks, and analyze transit deserts.
|
| 14 |
+
|
| 15 |
+
### Features
|
| 16 |
+
1. **Circuity-Factor Road Multipliers**: Calculates travel distances using coordinates and applies a standard winding multiplier (circuity coefficient) to simulate realistic street routing.
|
| 17 |
+
2. **Transit Equity Auditor**: Correlates travel distances from starting tracts to destinations with demographic attributes (e.g. poverty, minority share) to calculate inequality ratios.
|
| 18 |
+
3. **Interactive Visual Connectivity Maps**: Displays starting neighborhoods, destination vital hubs, and draws connecting routes color-coded by accessibility status.
|
| 19 |
+
4. **Pedagogical Indicators**: Automatically categorizes neighborhoods (High Accessibility vs. Isolated Transit Deserts) and provides clear socio-spatial insights.
|
app.py
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import tempfile
|
| 3 |
+
import pandas as pd
|
| 4 |
+
import numpy as np
|
| 5 |
+
import folium
|
| 6 |
+
import gradio as gr
|
| 7 |
+
|
| 8 |
+
def haversine_distance(lat1, lon1, lat2, lon2):
|
| 9 |
+
"""Calculates great-circle distance in kilometers."""
|
| 10 |
+
lat1, lon1, lat2, lon2 = map(np.radians, [lat1, lon1, lat2, lon2])
|
| 11 |
+
dlat = lat2 - lat1
|
| 12 |
+
dlon = lon2 - lon1
|
| 13 |
+
a = np.sin(dlat/2.0)**2 + np.cos(lat1) * np.cos(lat2) * np.sin(dlon/2.0)**2
|
| 14 |
+
c = 2.0 * np.arcsin(np.sqrt(a))
|
| 15 |
+
return c * 6371.0
|
| 16 |
+
|
| 17 |
+
def run_network_equity_audit(df_start, df_dest, start_lat, start_lon, dest_lat, dest_lon, dest_label, circuity_factor=1.3):
|
| 18 |
+
"""Calculates route distance to closest destinations and correlates with demographics."""
|
| 19 |
+
try:
|
| 20 |
+
n_start = len(df_start)
|
| 21 |
+
n_dest = len(df_dest)
|
| 22 |
+
|
| 23 |
+
if n_start == 0 or n_dest == 0:
|
| 24 |
+
return None, "Error: Both datasets must contain at least 1 record."
|
| 25 |
+
|
| 26 |
+
start_lats = df_start[start_lat].values
|
| 27 |
+
start_lons = df_start[start_lon].values
|
| 28 |
+
|
| 29 |
+
dest_lats = df_dest[dest_lat].values
|
| 30 |
+
dest_lons = df_dest[dest_lon].values
|
| 31 |
+
|
| 32 |
+
closest_dest_idx = []
|
| 33 |
+
simulated_distances = []
|
| 34 |
+
|
| 35 |
+
for i in range(n_start):
|
| 36 |
+
# Calculate geodesic distance to all destinations
|
| 37 |
+
dists = haversine_distance(start_lats[i], start_lons[i], dest_lats, dest_lons)
|
| 38 |
+
|
| 39 |
+
# Apply urban circuity factor (standard road network winding multiplier)
|
| 40 |
+
dists_adjusted = dists * circuity_factor
|
| 41 |
+
|
| 42 |
+
min_idx = np.argmin(dists_adjusted)
|
| 43 |
+
closest_dest_idx.append(min_idx)
|
| 44 |
+
simulated_distances.append(dists_adjusted[min_idx])
|
| 45 |
+
|
| 46 |
+
df_audit = df_start.copy()
|
| 47 |
+
df_audit["Nearest_Destination_Index"] = closest_dest_idx
|
| 48 |
+
# Destination names
|
| 49 |
+
dest_names = df_dest[dest_label].values if dest_label in df_dest.columns else [f"Hub_{j}" for j in range(n_dest)]
|
| 50 |
+
df_audit["Nearest_Destination"] = [dest_names[idx] for idx in closest_dest_idx]
|
| 51 |
+
df_audit["Nearest_Dest_Lat"] = [dest_lats[idx] for idx in closest_dest_idx]
|
| 52 |
+
df_audit["Nearest_Dest_Lon"] = [dest_lons[idx] for idx in closest_dest_idx]
|
| 53 |
+
df_audit["Estimated_Travel_Distance_km"] = simulated_distances
|
| 54 |
+
|
| 55 |
+
# Categorize accessibility
|
| 56 |
+
# Green / High: <= 3 km. Orange / Moderate: 3 to 7 km. Red / Isolated: > 7 km.
|
| 57 |
+
conditions = [
|
| 58 |
+
df_audit["Estimated_Travel_Distance_km"] <= 3.0,
|
| 59 |
+
(df_audit["Estimated_Travel_Distance_km"] > 3.0) & (df_audit["Estimated_Travel_Distance_km"] <= 7.0),
|
| 60 |
+
df_audit["Estimated_Travel_Distance_km"] > 7.0
|
| 61 |
+
]
|
| 62 |
+
choices = ["High Accessibility", "Moderate Accessibility", "Isolated (Transit Desert)"]
|
| 63 |
+
df_audit["Accessibility_Status"] = np.select(conditions, choices, default="Moderate")
|
| 64 |
+
|
| 65 |
+
# Correlate demographics with access
|
| 66 |
+
# Find all numerical columns excluding lat, lon, calculations
|
| 67 |
+
exclude = [start_lat, start_lon, "Nearest_Destination_Index", "Nearest_Dest_Lat", "Nearest_Dest_Lon", "Estimated_Travel_Distance_km"]
|
| 68 |
+
num_cols = [c for c in df_start.columns if pd.api.types.is_numeric_dtype(df_start[c]) and c not in exclude]
|
| 69 |
+
|
| 70 |
+
correlations = []
|
| 71 |
+
for col in num_cols:
|
| 72 |
+
# Split demographic average for Highly Accessible vs Isolated neighborhoods
|
| 73 |
+
avg_accessible = df_audit[df_audit["Accessibility_Status"] == "High Accessibility"][col].mean()
|
| 74 |
+
avg_isolated = df_audit[df_audit["Accessibility_Status"] == "Isolated (Transit Desert)"][col].mean()
|
| 75 |
+
|
| 76 |
+
avg_accessible = 0.0 if np.isnan(avg_accessible) else avg_accessible
|
| 77 |
+
avg_isolated = 0.0 if np.isnan(avg_isolated) else avg_isolated
|
| 78 |
+
|
| 79 |
+
# Simple inequality quotient: Isolated Avg / Accessible Avg
|
| 80 |
+
inequality_ratio = avg_isolated / avg_accessible if avg_accessible != 0 else 0.0
|
| 81 |
+
|
| 82 |
+
correlations.append({
|
| 83 |
+
"Socio-Demographic Factor": col,
|
| 84 |
+
"Average in High-Access Areas": avg_accessible,
|
| 85 |
+
"Average in Isolated Areas": avg_isolated,
|
| 86 |
+
"Inequality Ratio (Isolated/Accessible)": inequality_ratio
|
| 87 |
+
})
|
| 88 |
+
|
| 89 |
+
df_compare = pd.DataFrame(correlations)
|
| 90 |
+
return df_compare, df_audit
|
| 91 |
+
except Exception as e:
|
| 92 |
+
print(f"Network audit failed: {e}")
|
| 93 |
+
return None, f"Network mapping failed: {e}"
|
| 94 |
+
|
| 95 |
+
def generate_network_map(df_audit, df_dest, start_lat, start_lon, dest_lat, dest_lon, dest_label):
|
| 96 |
+
"""Draws a beautiful Folium map showing connecting travel paths color-coded by accessibility."""
|
| 97 |
+
mean_lat = df_dest[dest_lat].mean()
|
| 98 |
+
mean_lon = df_dest[dest_lon].mean()
|
| 99 |
+
|
| 100 |
+
m = folium.Map(location=[mean_lat, mean_lon], zoom_start=12, tiles="CartoDB dark_matter")
|
| 101 |
+
|
| 102 |
+
# Color map for routes
|
| 103 |
+
status_colors = {
|
| 104 |
+
"High Accessibility": "#10b981", # Green
|
| 105 |
+
"Moderate Accessibility": "#f97316", # Orange
|
| 106 |
+
"Isolated (Transit Desert)": "#ef4444" # Red
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
# 1. Plot starting tract points and routes to closest destinations
|
| 110 |
+
for i, row in df_audit.iterrows():
|
| 111 |
+
s_lat = row[start_lat]
|
| 112 |
+
s_lon = row[start_lon]
|
| 113 |
+
d_lat = row["Nearest_Dest_Lat"]
|
| 114 |
+
d_lon = row["Nearest_Dest_Lon"]
|
| 115 |
+
d_name = row["Nearest_Destination"]
|
| 116 |
+
dist = row["Estimated_Travel_Distance_km"]
|
| 117 |
+
status = row["Accessibility_Status"]
|
| 118 |
+
|
| 119 |
+
color = status_colors.get(status, "#6b7280")
|
| 120 |
+
|
| 121 |
+
# Add travel connection path (direct line representing route)
|
| 122 |
+
folium.PolyLine(
|
| 123 |
+
locations=[[s_lat, s_lon], [d_lat, d_lon]],
|
| 124 |
+
color=color,
|
| 125 |
+
weight=2,
|
| 126 |
+
opacity=0.6,
|
| 127 |
+
dash_array="5, 5" if status == "Isolated (Transit Desert)" else None
|
| 128 |
+
).add_to(m)
|
| 129 |
+
|
| 130 |
+
# Add neighborhood centroid circle
|
| 131 |
+
folium.CircleMarker(
|
| 132 |
+
location=[s_lat, s_lon],
|
| 133 |
+
radius=6,
|
| 134 |
+
color=color,
|
| 135 |
+
fill=True,
|
| 136 |
+
fill_color=color,
|
| 137 |
+
fill_opacity=0.8,
|
| 138 |
+
weight=1.5,
|
| 139 |
+
popup=f"Nearest Hub: {d_name}<br>Simulated Distance: {dist:.2f} km<br>Status: {status}"
|
| 140 |
+
).add_to(m)
|
| 141 |
+
|
| 142 |
+
# 2. Plot vital Destination Hub markers (high contrast white/gold icon)
|
| 143 |
+
for i, row in df_dest.iterrows():
|
| 144 |
+
lat = row[dest_lat]
|
| 145 |
+
lon = row[dest_lon]
|
| 146 |
+
name = row[dest_label] if dest_label in df_dest.columns else f"Hub {i}"
|
| 147 |
+
|
| 148 |
+
folium.Marker(
|
| 149 |
+
location=[lat, lon],
|
| 150 |
+
popup=f"<b>Destination Hub: {name}</b>",
|
| 151 |
+
icon=folium.Icon(color="orange", icon="home")
|
| 152 |
+
).add_to(m)
|
| 153 |
+
|
| 154 |
+
return m
|
| 155 |
+
|
| 156 |
+
def full_network_pipeline(file_start, file_dest, start_lat, start_lon, dest_lat, dest_lon, dest_label, circuity):
|
| 157 |
+
"""Executes loading, network distance mapping, comparative audits, and downloads."""
|
| 158 |
+
if file_start is None or file_dest is None:
|
| 159 |
+
return None, "Please upload both the Demographic Starting Points CSV and Destination Hubs CSV files.", pd.DataFrame(), None
|
| 160 |
+
|
| 161 |
+
try:
|
| 162 |
+
df_start = pd.read_csv(file_start.name)
|
| 163 |
+
df_dest = pd.read_csv(file_dest.name)
|
| 164 |
+
|
| 165 |
+
# Column checks
|
| 166 |
+
for c in [start_lat, start_lon]:
|
| 167 |
+
if c not in df_start.columns:
|
| 168 |
+
return None, f"ERROR: Demographic column '{c}' not found! Check columns.", pd.DataFrame(), None
|
| 169 |
+
for c in [dest_lat, dest_lon]:
|
| 170 |
+
if c not in df_dest.columns:
|
| 171 |
+
return None, f"ERROR: Destination column '{c}' not found! Check columns.", pd.DataFrame(), None
|
| 172 |
+
|
| 173 |
+
df_start_clean = df_start.dropna(subset=[start_lat, start_lon]).copy()
|
| 174 |
+
df_dest_clean = df_dest.dropna(subset=[dest_lat, dest_lon]).copy()
|
| 175 |
+
|
| 176 |
+
df_compare, df_audit = run_network_equity_audit(
|
| 177 |
+
df_start_clean, df_dest_clean, start_lat, start_lon, dest_lat, dest_lon, dest_label, circuity
|
| 178 |
+
)
|
| 179 |
+
|
| 180 |
+
if df_compare is None:
|
| 181 |
+
# df_compare holds the error string
|
| 182 |
+
return None, df_audit, pd.DataFrame(), None
|
| 183 |
+
|
| 184 |
+
# Draw map
|
| 185 |
+
map_obj = generate_network_map(
|
| 186 |
+
df_audit, df_dest_clean, start_lat, start_lon, dest_lat, dest_lon, dest_label
|
| 187 |
+
)
|
| 188 |
+
|
| 189 |
+
# Save HTML map
|
| 190 |
+
temp_map = tempfile.NamedTemporaryFile(delete=False, suffix=".html")
|
| 191 |
+
map_obj.save(temp_map.name)
|
| 192 |
+
|
| 193 |
+
isolated_count = len(df_audit[df_audit["Accessibility_Status"] == "Isolated (Transit Desert)"])
|
| 194 |
+
total_count = len(df_audit)
|
| 195 |
+
|
| 196 |
+
status_md = f"""
|
| 197 |
+
### 📊 Network Transit Equity Metrics:
|
| 198 |
+
* **Total Starting Neighborhoods**: `{total_count}`
|
| 199 |
+
* **Isolated Neighborhoods (Transit Deserts)**: `{isolated_count} ({isolated_count/total_count:.1%})`
|
| 200 |
+
* **Highly Accessible Areas (Short Travel)**: `{len(df_audit[df_audit["Accessibility_Status"] == "High Accessibility"])}`
|
| 201 |
+
* **Applied Road Winding Circuity Factor**: `{circuity:.2f}x`
|
| 202 |
+
|
| 203 |
+
*Interpretation*: If the **Inequality Ratio** on the right is greater than 1.0, it indicates that isolated neighborhoods have a higher concentration of that demographic attribute than accessible zones, proving spatial polarization!
|
| 204 |
+
"""
|
| 205 |
+
|
| 206 |
+
# CSV download path
|
| 207 |
+
temp_csv = tempfile.NamedTemporaryFile(delete=False, suffix="_transit_audit.csv")
|
| 208 |
+
df_audit.to_csv(temp_csv.name, index=False)
|
| 209 |
+
|
| 210 |
+
return temp_map.name, status_md, df_compare, temp_csv.name
|
| 211 |
+
except Exception as e:
|
| 212 |
+
return None, f"Transit audit processing failed: {e}", pd.DataFrame(), None
|
| 213 |
+
|
| 214 |
+
# Custom styling (Monochrome / Indigo theme)
|
| 215 |
+
custom_css = """
|
| 216 |
+
body { background-color: #0d0f12; color: #e3e6eb; font-family: 'Inter', sans-serif; }
|
| 217 |
+
.gradio-container { max-width: 1200px !important; margin: 0 auto !important; }
|
| 218 |
+
h1, h2, h3 { color: #ffffff !important; font-weight: 700 !important; }
|
| 219 |
+
.btn-primary { background: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%) !important; border: none !important; color: white !important; font-weight: 600 !important; }
|
| 220 |
+
.btn-primary:hover { filter: brightness(1.1); }
|
| 221 |
+
"""
|
| 222 |
+
|
| 223 |
+
with gr.Blocks(theme=gr.themes.Monochrome(), css=custom_css) as demo:
|
| 224 |
+
gr.Markdown(
|
| 225 |
+
"""
|
| 226 |
+
# 🕸️ Route Equity & Spatial Network Analyzer
|
| 227 |
+
### Analyze spatial access by calculating simulated road-network routing (using circuity multipliers) from neighborhoods to vital resources. Identify geographic exclusion and audit transit deserts.
|
| 228 |
+
"""
|
| 229 |
+
)
|
| 230 |
+
|
| 231 |
+
with gr.Row():
|
| 232 |
+
with gr.Column(scale=4):
|
| 233 |
+
with gr.Card():
|
| 234 |
+
gr.Markdown("### 1. Upload Starting Tract Coordinates")
|
| 235 |
+
file_start_input = gr.File(label="Upload Neighborhood Centroids CSV", file_types=[".csv"])
|
| 236 |
+
with gr.Row():
|
| 237 |
+
start_lat_name = gr.Textbox(label="Neighborhood Lat Column", value="Latitude")
|
| 238 |
+
start_lon_name = gr.Textbox(label="Neighborhood Lon Column", value="Longitude")
|
| 239 |
+
|
| 240 |
+
with gr.Card():
|
| 241 |
+
gr.Markdown("### 2. Upload Destination Hubs (Hospitals/Services)")
|
| 242 |
+
file_dest_input = gr.File(label="Upload Vital Hub POIs CSV", file_types=[".csv"])
|
| 243 |
+
with gr.Row():
|
| 244 |
+
dest_lat_name = gr.Textbox(label="Vital Hub Lat Column", value="Latitude")
|
| 245 |
+
dest_lon_name = gr.Textbox(label="Vital Hub Lon Column", value="Longitude")
|
| 246 |
+
dest_lbl_name = gr.Textbox(label="Hub Name/Label Column", value="Name")
|
| 247 |
+
|
| 248 |
+
with gr.Card():
|
| 249 |
+
gr.Markdown("### 3. Route Settings")
|
| 250 |
+
circuity_slider = gr.Slider(
|
| 251 |
+
minimum=1.0, maximum=2.0, value=1.3, step=0.05,
|
| 252 |
+
label="Urban Circuity Winding Multiplier (Simulate roads vs. crow flies)"
|
| 253 |
+
)
|
| 254 |
+
analyze_btn = gr.Button("Analyze Route Network Equity", variant="primary", elem_classes="btn-primary")
|
| 255 |
+
|
| 256 |
+
with gr.Column(scale=6):
|
| 257 |
+
with gr.Tabs():
|
| 258 |
+
with gr.TabItem("🗺️ Dynamic Transit Network Map"):
|
| 259 |
+
map_output = gr.HTML(label="Leaflet Route Map Grid", value="<div style='text-align: center; padding: 50px; color: gray;'>Map will load here...</div>")
|
| 260 |
+
summary_output = gr.Markdown("Please load coordinates and calculate routing.")
|
| 261 |
+
|
| 262 |
+
with gr.TabItem("📊 Socio-Spatial Inequality Report"):
|
| 263 |
+
table_output = gr.Dataframe(
|
| 264 |
+
label="Calculated Access Disparities Table (Isolated vs. High-Access)",
|
| 265 |
+
interactive=False,
|
| 266 |
+
wrap=True
|
| 267 |
+
)
|
| 268 |
+
download_btn = gr.File(label="Download Labeled CSV Database", interactive=False)
|
| 269 |
+
|
| 270 |
+
analyze_btn.click(
|
| 271 |
+
fn=full_network_pipeline,
|
| 272 |
+
inputs=[file_start_input, file_dest_input, start_lat_name, start_lon_name, dest_lat_name, dest_lon_name, dest_lbl_name, circuity_slider],
|
| 273 |
+
outputs=[map_output, summary_output, table_output, download_btn]
|
| 274 |
+
)
|
| 275 |
+
|
| 276 |
+
if __name__ == "__main__":
|
| 277 |
+
demo.launch()
|
requirements.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
pandas
|
| 2 |
+
numpy
|
| 3 |
+
folium
|
| 4 |
+
gradio
|
| 5 |
+
pillow
|