Jomaric commited on
Commit
bf747e9
Β·
0 Parent(s):

feat: initial release of GIS space

Browse files
Files changed (3) hide show
  1. README.md +19 -0
  2. app.py +268 -0
  3. requirements.txt +5 -0
README.md ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Proximity Buffer Analyzer
3
+ emoji: πŸ“
4
+ colorFrom: blue
5
+ colorTo: purple
6
+ sdk: gradio
7
+ app_file: app.py
8
+ pinned: false
9
+ ---
10
+
11
+ # Proximity & Buffer Analyzer
12
+
13
+ An interactive computational tool designed for urban studies, contemporary sociology, environmental justice, and public policy to audit spatial equity, food deserts, or environmental exposure.
14
+
15
+ ### Features
16
+ 1. **Haversine Distance Calculations**: Computes exact geodesic great-circle distances between census tracts and targeted features on the earth's surface.
17
+ 2. **Equity Auditing Engine**: Automatically parses all numerical demographic attributes and compares their average values inside vs. outside radial buffer zones.
18
+ 3. **Interactive Buffers Map**: Renders an interactive Leaflet map featuring transparent circular buffer overlays, asset/hazard markers, and color-coded census centroids.
19
+ 4. **Disparity Calculations**: Identifies relative percentage differences for income, race, education, or other attributes to detect structural spatial inequalities.
app.py ADDED
@@ -0,0 +1,268 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 the great-circle distance between two points on the Earth in kilometers."""
10
+ # Convert decimal degrees to radians
11
+ lat1, lon1, lat2, lon2 = map(np.radians, [lat1, lon1, lat2, lon2])
12
+
13
+ # Haversine formula
14
+ dlat = lat2 - lat1
15
+ dlon = lon2 - lon1
16
+ a = np.sin(dlat/2.0)**2 + np.cos(lat1) * np.cos(lat2) * np.sin(dlon/2.0)**2
17
+ c = 2.0 * np.arcsin(np.sqrt(a))
18
+ r = 6371.0 # Radius of Earth in kilometers
19
+ return c * r
20
+
21
+ def run_proximity_audit(df_dem, df_poi, dem_lat, dem_lon, poi_lat, poi_lon, radius_km):
22
+ """Audits demographic attributes inside vs outside the point-of-interest buffers."""
23
+ try:
24
+ n_dem = len(df_dem)
25
+ n_poi = len(df_poi)
26
+
27
+ if n_dem == 0 or n_poi == 0:
28
+ return None, "Error: Both datasets must contain at least 1 record."
29
+
30
+ # Extract coordinates
31
+ dem_lats = df_dem[dem_lat].values
32
+ dem_lons = df_dem[dem_lon].values
33
+
34
+ poi_lats = df_poi[poi_lat].values
35
+ poi_lons = df_poi[poi_lon].values
36
+
37
+ # Calculate minimum distance from each demographic point to any POI
38
+ min_distances = []
39
+ for i in range(n_dem):
40
+ # Compute distance to all POIs
41
+ dists = haversine_distance(dem_lats[i], dem_lons[i], poi_lats, poi_lons)
42
+ min_distances.append(np.min(dists))
43
+
44
+ min_distances = np.array(min_distances)
45
+
46
+ # Categorize
47
+ inside_buffer = min_distances <= radius_km
48
+
49
+ df_audit = df_dem.copy()
50
+ df_audit["Distance_to_POI_km"] = min_distances
51
+ df_audit["Location_Class"] = np.where(inside_buffer, "Inside Buffer", "Outside Buffer")
52
+
53
+ # Compile demographic comparisons
54
+ # Identify all numerical columns excluding lat, lon
55
+ exclude_cols = [dem_lat, dem_lon, "Distance_to_POI_km", "Location_Class"]
56
+ num_cols = [c for c in df_dem.columns if pd.api.types.is_numeric_dtype(df_dem[c]) and c not in exclude_cols]
57
+
58
+ comparisons = []
59
+ for col in num_cols:
60
+ mean_inside = df_audit[df_audit["Location_Class"] == "Inside Buffer"][col].mean()
61
+ mean_outside = df_audit[df_audit["Location_Class"] == "Outside Buffer"][col].mean()
62
+
63
+ # Handle empty divisions or NaNs
64
+ mean_inside = 0.0 if np.isnan(mean_inside) else mean_inside
65
+ mean_outside = 0.0 if np.isnan(mean_outside) else mean_outside
66
+
67
+ # Disparity percentage
68
+ if mean_outside != 0:
69
+ disparity_pct = ((mean_inside - mean_outside) / mean_outside) * 100.0
70
+ else:
71
+ disparity_pct = 0.0
72
+
73
+ comparisons.append({
74
+ "Demographic Attribute": col,
75
+ "Average (Inside Buffer)": mean_inside,
76
+ "Average (Outside Buffer)": mean_outside,
77
+ "Relative Disparity (%)": disparity_pct
78
+ })
79
+
80
+ df_compare = pd.DataFrame(comparisons)
81
+ return df_compare, df_audit
82
+ except Exception as e:
83
+ print(f"Audit error: {e}")
84
+ return None, f"Audit processing failed: {e}"
85
+
86
+ def generate_proximity_map(df_audit, df_poi, dem_lat, dem_lon, poi_lat, poi_lon, poi_label, radius_km):
87
+ """Draws a beautiful Folium map with transparent circular buffers around POIs."""
88
+ mean_lat = df_poi[poi_lat].mean()
89
+ mean_lon = df_poi[poi_lon].mean()
90
+
91
+ m = folium.Map(location=[mean_lat, mean_lon], zoom_start=12, tiles="CartoDB positron")
92
+
93
+ # 1. Plot buffers and POI markers
94
+ for i, row in df_poi.iterrows():
95
+ lat = row[poi_lat]
96
+ lon = row[poi_lon]
97
+ label = row[poi_label] if poi_label in df_poi.columns else "Point of Interest"
98
+
99
+ # Add transparent buffer overlay (radius in meters)
100
+ folium.Circle(
101
+ location=[lat, lon],
102
+ radius=radius_km * 1000.0,
103
+ color="#ef4444",
104
+ fill=True,
105
+ fill_color="#fca5a5",
106
+ fill_opacity=0.2,
107
+ weight=1
108
+ ).add_to(m)
109
+
110
+ # Add POI marker
111
+ folium.Marker(
112
+ location=[lat, lon],
113
+ popup=f"<b>{label}</b>",
114
+ icon=folium.Icon(color="red", icon="info-sign")
115
+ ).add_to(m)
116
+
117
+ # 2. Plot demographic centroids
118
+ for i, row in df_audit.iterrows():
119
+ lat = row[dem_lat]
120
+ lon = row[dem_lon]
121
+ loc_class = row["Location_Class"]
122
+ dist = row["Distance_to_POI_km"]
123
+
124
+ color = "#10b981" if loc_class == "Inside Buffer" else "#6b7280" # Green inside, Gray outside
125
+
126
+ popup_html = f"""
127
+ <div style="font-family: 'Inter', sans-serif; color: #111827; min-width: 150px;">
128
+ <h4 style="margin:0 0 5px 0;">Centroid Area</h4>
129
+ <b>Status</b>: {loc_class}<br>
130
+ <b>Distance</b>: {dist:.2f} km
131
+ </div>
132
+ """
133
+
134
+ folium.CircleMarker(
135
+ location=[lat, lon],
136
+ radius=6,
137
+ color=color,
138
+ fill=True,
139
+ fill_color=color,
140
+ fill_opacity=0.6,
141
+ weight=1,
142
+ popup=folium.Popup(popup_html, max_width=200)
143
+ ).add_to(m)
144
+
145
+ return m
146
+
147
+ def full_proximity_pipeline(file_dem, file_poi, dem_lat, dem_lon, poi_lat, poi_lon, poi_label, radius_km):
148
+ """Executes the full loading, auditing, mapping, and download setup."""
149
+ if file_dem is None or file_poi is None:
150
+ return None, "Please upload both the Demographic CSV and Point of Interest CSV files.", pd.DataFrame(), None
151
+
152
+ try:
153
+ df_dem = pd.read_csv(file_dem.name)
154
+ df_poi = pd.read_csv(file_poi.name)
155
+
156
+ # Column checks
157
+ for c in [dem_lat, dem_lon]:
158
+ if c not in df_dem.columns:
159
+ return None, f"ERROR: Demographic column '{c}' not found! Check columns.", pd.DataFrame(), None
160
+ for c in [poi_lat, poi_lon]:
161
+ if c not in df_poi.columns:
162
+ return None, f"ERROR: POI column '{c}' not found! Check columns.", pd.DataFrame(), None
163
+
164
+ df_dem_clean = df_dem.dropna(subset=[dem_lat, dem_lon]).copy()
165
+ df_poi_clean = df_poi.dropna(subset=[poi_lat, poi_lon]).copy()
166
+
167
+ df_compare, df_audit = run_proximity_audit(
168
+ df_dem_clean, df_poi_clean, dem_lat, dem_lon, poi_lat, poi_lon, radius_km
169
+ )
170
+
171
+ if df_compare is None:
172
+ # df_compare holds the error string
173
+ return None, df_audit, pd.DataFrame(), None
174
+
175
+ # Draw map
176
+ map_obj = generate_proximity_map(
177
+ df_audit, df_poi_clean, dem_lat, dem_lon, poi_lat, poi_lon, poi_label, radius_km
178
+ )
179
+
180
+ # Save HTML map
181
+ temp_map = tempfile.NamedTemporaryFile(delete=False, suffix=".html")
182
+ map_obj.save(temp_map.name)
183
+
184
+ inside_count = len(df_audit[df_audit["Location_Class"] == "Inside Buffer"])
185
+ total_count = len(df_audit)
186
+
187
+ status_md = f"""
188
+ ### πŸ“Š Proximity Audit Metrics:
189
+ * **Target POI Radius**: `{radius_km:.2f} km`
190
+ * **Total Census Tracts/Centroids**: `{total_count}`
191
+ * **Tracts Inside Buffer Zone**: `{inside_count} ({inside_count/total_count:.1%})`
192
+ * **Tracts Outside Buffer Zone**: `{total_count - inside_count} ({(total_count - inside_count)/total_count:.1%})`
193
+
194
+ *Interpretation*: The table on the right displays the average demographics of neighborhoods located within vs outside this buffer. Look at **Relative Disparity (%)** to detect unequal exposure or access gaps!
195
+ """
196
+
197
+ # Create CSV download path
198
+ temp_csv = tempfile.NamedTemporaryFile(delete=False, suffix="_equity_audit.csv")
199
+ df_audit.to_csv(temp_csv.name, index=False)
200
+
201
+ return temp_map.name, status_md, df_compare, temp_csv.name
202
+ except Exception as e:
203
+ return None, f"Audit processing failed: {e}", pd.DataFrame(), None
204
+
205
+ # Premium Monochrome / custom Green styling
206
+ custom_css = """
207
+ body { background-color: #0d0f12; color: #e3e6eb; font-family: 'Inter', sans-serif; }
208
+ .gradio-container { max-width: 1200px !important; margin: 0 auto !important; }
209
+ h1, h2, h3 { color: #ffffff !important; font-weight: 700 !important; }
210
+ .btn-primary { background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%) !important; border: none !important; color: white !important; font-weight: 600 !important; }
211
+ .btn-primary:hover { filter: brightness(1.1); }
212
+ """
213
+
214
+ with gr.Blocks(theme=gr.themes.Monochrome(), css=custom_css) as demo:
215
+ gr.Markdown(
216
+ """
217
+ # πŸ“ Proximity & Buffer Analyzer
218
+ ### Audit spatial equity, food deserts, or environmental exposure. Draw radial distance buffers around assets or hazards, and automatically compare demographic attributes inside vs. outside the zones.
219
+ """
220
+ )
221
+
222
+ with gr.Row():
223
+ with gr.Column(scale=4):
224
+ with gr.Card():
225
+ gr.Markdown("### 1. Upload Socio-Demographic Regions (Tracts)")
226
+ file_dem_input = gr.File(label="Upload CSV with Latitude, Longitude, and demographics", file_types=[".csv"])
227
+ with gr.Row():
228
+ dem_lat_name = gr.Textbox(label="Demographic Lat Column", value="Latitude")
229
+ dem_lon_name = gr.Textbox(label="Demographic Lon Column", value="Longitude")
230
+
231
+ with gr.Card():
232
+ gr.Markdown("### 2. Upload Points of Interest (Assets/Hazards)")
233
+ file_poi_input = gr.File(label="Upload POI CSV", file_types=[".csv"])
234
+ with gr.Row():
235
+ poi_lat_name = gr.Textbox(label="POI Lat Column", value="Latitude")
236
+ poi_lon_name = gr.Textbox(label="POI Lon Column", value="Longitude")
237
+ poi_lbl_name = gr.Textbox(label="POI Label/Name Column", value="Name")
238
+
239
+ with gr.Card():
240
+ gr.Markdown("### 3. Buffer Parameters")
241
+ radius_slider = gr.Slider(
242
+ minimum=0.1, maximum=20.0, value=2.0, step=0.1,
243
+ label="Radial Proximity Buffer (km)"
244
+ )
245
+ analyze_btn = gr.Button("Calculate Proximity Disparities", variant="primary", elem_classes="btn-primary")
246
+
247
+ with gr.Column(scale=6):
248
+ with gr.Tabs():
249
+ with gr.TabItem("πŸ—ΊοΈ Proximity Leaflet Map"):
250
+ map_output = gr.HTML(label="Leaflet Map Grid", value="<div style='text-align: center; padding: 50px; color: gray;'>Map will load here...</div>")
251
+ summary_output = gr.Markdown("Please upload data and run proximity audit.")
252
+
253
+ with gr.TabItem("πŸ“Š Comparative Equity Report"):
254
+ table_output = gr.Dataframe(
255
+ label="Calculated Disparities Table (Inside vs. Outside Buffer)",
256
+ interactive=False,
257
+ wrap=True
258
+ )
259
+ download_btn = gr.File(label="Download Labeled CSV Database", interactive=False)
260
+
261
+ analyze_btn.click(
262
+ fn=full_proximity_pipeline,
263
+ inputs=[file_dem_input, file_poi_input, dem_lat_name, dem_lon_name, poi_lat_name, poi_lon_name, poi_lbl_name, radius_slider],
264
+ outputs=[map_output, summary_output, table_output, download_btn]
265
+ )
266
+
267
+ if __name__ == "__main__":
268
+ demo.launch()
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ pandas
2
+ numpy
3
+ folium
4
+ gradio
5
+ pillow