AlexKurian commited on
Commit
9b06d18
·
1 Parent(s): a2f12e1

Changed to XGBoost

Browse files
app.py CHANGED
@@ -40,31 +40,60 @@ register_aqi_history_routes(app)
40
  from aqi_history import get_aqi_history
41
  print("Initializing AQI History & Model...")
42
  get_aqi_history()
43
- from aqi_map import generate_aqi_map_html, render_map_to_png
44
  import threading
45
 
46
  # Ensure map directory exists
47
  STATIC_DIR = os.path.join(os.getcwd(), 'static')
48
  os.makedirs(STATIC_DIR, exist_ok=True)
49
- MAP_IMAGE_PATH = os.path.join(STATIC_DIR, 'aqi_map.png')
 
 
 
 
50
 
51
  @app.route('/api/aqi-map', methods=['GET'])
52
  def get_aqi_map_html():
53
- """Returns the Interactive Folium Map HTML"""
54
- return generate_aqi_map_html()
 
 
 
 
 
 
 
 
 
 
55
 
56
  @app.route('/api/aqi-map.png', methods=['GET'])
57
  def get_aqi_map_png():
58
- """Returns the static PNG image of the map, regenerating if needed"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  from flask import send_file
60
 
61
- # Check if we need to regenerate (e.g. if file is old or missing)
62
- # For now, we'll generate if missing, or maybe background refresh
63
- if not os.path.exists(MAP_IMAGE_PATH):
64
- html = generate_aqi_map_html()
65
- render_map_to_png(html, MAP_IMAGE_PATH)
66
 
67
- return send_file(MAP_IMAGE_PATH, mimetype='image/png')
68
 
69
  # Background task to refresh map periodically (optional)
70
  def refresh_map_periodically():
 
40
  from aqi_history import get_aqi_history
41
  print("Initializing AQI History & Model...")
42
  get_aqi_history()
43
+ from aqi_map import generate_heatmap_html, generate_hotspots_html, render_map_to_png
44
  import threading
45
 
46
  # Ensure map directory exists
47
  STATIC_DIR = os.path.join(os.getcwd(), 'static')
48
  os.makedirs(STATIC_DIR, exist_ok=True)
49
+ HEATMAP_PATH = os.path.join(STATIC_DIR, 'aqi_map_heatmap.png')
50
+ HOTSPOTS_PATH = os.path.join(STATIC_DIR, 'aqi_map_hotspots.png')
51
+
52
+ # Default map path (alias to heatmap for backward compat)
53
+ MAP_IMAGE_PATH = HEATMAP_PATH
54
 
55
  @app.route('/api/aqi-map', methods=['GET'])
56
  def get_aqi_map_html():
57
+ """Returns the Interactive Heatmap HTML (Default)"""
58
+ return generate_heatmap_html()
59
+
60
+ @app.route('/api/aqi-map/heatmap', methods=['GET'])
61
+ def get_heatmap_html():
62
+ """Explicit endpoint for Heatmap HTML"""
63
+ return generate_heatmap_html()
64
+
65
+ @app.route('/api/aqi-map/hotspots', methods=['GET'])
66
+ def get_hotspots_html():
67
+ """Returns the Interactive Hotspots HTML"""
68
+ return generate_hotspots_html()
69
 
70
  @app.route('/api/aqi-map.png', methods=['GET'])
71
  def get_aqi_map_png():
72
+ """Default map image (Heatmap)"""
73
+ return get_heatmap_png()
74
+
75
+ @app.route('/api/aqi-map/heatmap.png', methods=['GET'])
76
+ def get_heatmap_png():
77
+ """Returns the Heatmap Grid PNG"""
78
+ from flask import send_file
79
+
80
+ # Simple caching/regeneration logic
81
+ if not os.path.exists(HEATMAP_PATH) or request.args.get('refresh'):
82
+ html = generate_heatmap_html()
83
+ render_map_to_png(html, HEATMAP_PATH)
84
+
85
+ return send_file(HEATMAP_PATH, mimetype='image/png')
86
+
87
+ @app.route('/api/aqi-map/hotspots.png', methods=['GET'])
88
+ def get_hotspots_png():
89
+ """Returns the Original Hotspots PNG"""
90
  from flask import send_file
91
 
92
+ if not os.path.exists(HOTSPOTS_PATH) or request.args.get('refresh'):
93
+ html = generate_hotspots_html()
94
+ render_map_to_png(html, HOTSPOTS_PATH)
 
 
95
 
96
+ return send_file(HOTSPOTS_PATH, mimetype='image/png')
97
 
98
  # Background task to refresh map periodically (optional)
99
  def refresh_map_periodically():
aqi_history.py CHANGED
@@ -4,9 +4,9 @@ import os
4
  from flask import jsonify, request
5
  from datetime import datetime, timedelta
6
 
7
- # Try importing Random Forest (Standard sklearn)
8
  try:
9
- from sklearn.ensemble import RandomForestRegressor
10
  from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
11
  HAS_MODEL = True
12
  except ImportError:
@@ -91,7 +91,7 @@ class AQIHistory:
91
  return df_feat.dropna()
92
 
93
  def _train_model(self):
94
- print("Training AQI Forecast Model (Random Forest)...")
95
  try:
96
  df_feat = self._create_features(self.df)
97
 
@@ -105,9 +105,10 @@ class AQIHistory:
105
  X_train, X_test = X[:split_idx], X[split_idx:]
106
  y_train, y_test = y[:split_idx], y[split_idx:]
107
 
108
- self.model = RandomForestRegressor(
109
  n_estimators=1000,
110
- max_depth=20,
 
111
  random_state=42,
112
  n_jobs=-1
113
  )
@@ -127,7 +128,7 @@ class AQIHistory:
127
  'rmse': float(rmse),
128
  'r2': float(r2)
129
  }
130
- print(f"AQI RF Model trained. MAE: {mae:.2f}, R2: {r2:.2f}")
131
 
132
  except Exception as e:
133
  print(f"Error training AQI model: {e}")
 
4
  from flask import jsonify, request
5
  from datetime import datetime, timedelta
6
 
7
+ # Try importing XGBoost
8
  try:
9
+ from xgboost import XGBRegressor
10
  from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
11
  HAS_MODEL = True
12
  except ImportError:
 
91
  return df_feat.dropna()
92
 
93
  def _train_model(self):
94
+ print("Training AQI Forecast Model (XGBoost)...")
95
  try:
96
  df_feat = self._create_features(self.df)
97
 
 
105
  X_train, X_test = X[:split_idx], X[split_idx:]
106
  y_train, y_test = y[:split_idx], y[split_idx:]
107
 
108
+ self.model = XGBRegressor(
109
  n_estimators=1000,
110
+ learning_rate=0.05,
111
+ max_depth=6,
112
  random_state=42,
113
  n_jobs=-1
114
  )
 
128
  'rmse': float(rmse),
129
  'r2': float(r2)
130
  }
131
+ print(f"AQI XGB Model trained. MAE: {mae:.2f}, R2: {r2:.2f}")
132
 
133
  except Exception as e:
134
  print(f"Error training AQI model: {e}")
aqi_map.py CHANGED
@@ -12,266 +12,168 @@ from webdriver_manager.chrome import ChromeDriverManager
12
  DELHI_LAT = 28.7041
13
  DELHI_LON = 77.1025
14
 
15
- def generate_aqi_map_html():
16
- """Generates a Folium map with AQI hotspots and returns the HTML string."""
17
-
18
- # Create base map
19
- m = folium.Map(location=[DELHI_LAT, DELHI_LON], zoom_start=11)
20
-
21
- # Add Delhi Boundary
22
  try:
23
- # Using DataMeet's Delhi Boundary GeoJSON
24
  geojson_url = "https://raw.githubusercontent.com/datameet/Municipal_Spatial_Data/master/Delhi/Delhi_Boundary.geojson"
 
 
25
  folium.GeoJson(
26
  geojson_url,
27
  name="Delhi Boundary",
28
  style_function=lambda x: {
29
  'fillColor': 'none',
30
- 'color': 'black',
31
- 'weight': 2,
32
- 'dashArray': '5, 5'
 
33
  }
34
  ).add_to(m)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  except Exception as e:
36
- print(f"Error loading Delhi GeoJSON: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
 
38
- # Randomly generate some "hotspot" locations around Delhi for demo
39
- # In a real app, this would come from the AQI data source
40
  hotspots = [
41
- {
42
- "name": "Chandni Chowk",
43
- "lat": 28.656,
44
- "lon": 77.231,
45
- "aqi": 384
46
- },
47
- {
48
- "name": "Nehru Nagar",
49
- "lat": 28.5697,
50
- "lon": 77.253,
51
- "aqi": 384
52
- },
53
- {
54
- "name": "New Moti Bagh",
55
- "lat": 28.582,
56
- "lon": 77.1717,
57
- "aqi": 377
58
- },
59
- {
60
- "name": "Shadipur",
61
- "lat": 28.6517,
62
- "lon": 77.1582,
63
- "aqi": 375
64
- },
65
- {
66
- "name": "Wazirpur",
67
- "lat": 28.6967,
68
- "lon": 77.1658,
69
- "aqi": 354
70
- },
71
- {
72
- "name": "Ashok Vihar",
73
- "lat": 28.6856,
74
- "lon": 77.178,
75
- "aqi": 231
76
- },
77
- {
78
- "name": "Mundka",
79
- "lat": 28.6794,
80
- "lon": 77.0284,
81
- "aqi": 353
82
- },
83
- {
84
- "name": "Punjabi Bagh",
85
- "lat": 28.673,
86
- "lon": 77.1374,
87
- "aqi": 349
88
- },
89
- {
90
- "name": "RK Puram",
91
- "lat": 28.5505,
92
- "lon": 77.1849,
93
- "aqi": 342
94
- },
95
- {
96
- "name": "Jahangirpuri",
97
- "lat": 28.716,
98
- "lon": 77.1829,
99
- "aqi": 338
100
- },
101
- {
102
- "name": "Okhla Phase-2",
103
- "lat": 28.5365,
104
- "lon": 77.2803,
105
- "aqi": 337
106
- },
107
- {
108
- "name": "Dwarka Sector-8",
109
- "lat": 28.5656,
110
- "lon": 77.067,
111
- "aqi": 336
112
- },
113
- {
114
- "name": "Patparganj",
115
- "lat": 28.612,
116
- "lon": 77.292,
117
- "aqi": 334
118
- },
119
- {
120
- "name": "Bawana",
121
- "lat": 28.8,
122
- "lon": 77.03,
123
- "aqi": 328
124
- },
125
- {
126
- "name": "Sonia Vihar",
127
- "lat": 28.7074,
128
- "lon": 77.2599,
129
- "aqi": 317
130
- },
131
- {
132
- "name": "Rohini",
133
- "lat": 28.7019,
134
- "lon": 77.0984,
135
- "aqi": 312
136
- },
137
- {
138
- "name": "Narela",
139
- "lat": 28.85,
140
- "lon": 77.1,
141
- "aqi": 311
142
- },
143
- {
144
- "name": "Mandir Marg",
145
- "lat": 28.6325,
146
- "lon": 77.1994,
147
- "aqi": 311
148
- },
149
- {
150
- "name": "Vivek Vihar",
151
- "lat": 28.6635,
152
- "lon": 77.3152,
153
- "aqi": 303
154
- },
155
- {
156
- "name": "Anand Vihar",
157
- "lat": 28.6508,
158
- "lon": 77.3152,
159
- "aqi": 335
160
- },
161
- {
162
- "name": "Dhyanchand Stadium",
163
- "lat": 28.6125,
164
- "lon": 77.2372,
165
- "aqi": 335
166
- },
167
- {
168
- "name": "Sirifort",
169
- "lat": 28.5521,
170
- "lon": 77.2193,
171
- "aqi": 326
172
- },
173
- {
174
- "name": "Karni Singh Shooting Range",
175
- "lat": 28.4998,
176
- "lon": 77.2668,
177
- "aqi": 323
178
- },
179
- {
180
- "name": "ITO",
181
- "lat": 28.6294,
182
- "lon": 77.241,
183
- "aqi": 324
184
- },
185
- {
186
- "name": "JLN Stadium",
187
- "lat": 28.5828,
188
- "lon": 77.2344,
189
- "aqi": 314
190
- },
191
- {
192
- "name": "IGI Airport T3",
193
- "lat": 28.5562,
194
- "lon": 77.1,
195
- "aqi": 305
196
- },
197
- {
198
- "name": "Pusa IMD",
199
- "lat": 28.6335,
200
- "lon": 77.1651,
201
- "aqi": 298
202
- },
203
- {
204
- "name": "Burari Crossing",
205
- "lat": 28.7592,
206
- "lon": 77.1938,
207
- "aqi": 294
208
- },
209
- {
210
- "name": "Aurobindo Marg",
211
- "lat": 28.545,
212
- "lon": 77.205,
213
- "aqi": 287
214
- },
215
- {
216
- "name": "Dilshad Garden (IHBAS)",
217
- "lat": 28.681,
218
- "lon": 77.305,
219
- "aqi": 283
220
- },
221
- {
222
- "name": "Lodhi Road",
223
- "lat": 28.5921,
224
- "lon": 77.2284,
225
- "aqi": 282
226
- },
227
- {
228
- "name": "NSIT-Dwarka",
229
- "lat": 28.6081,
230
- "lon": 77.0193,
231
- "aqi": 275
232
- },
233
- {
234
- "name": "Alipur",
235
- "lat": 28.8,
236
- "lon": 77.15,
237
- "aqi": 273
238
- },
239
- {
240
- "name": "Najafgarh",
241
- "lat": 28.6125,
242
- "lon": 76.983,
243
- "aqi": 262
244
- },
245
- {
246
- "name": "CRRI-Mathura Road",
247
- "lat": 28.5518,
248
- "lon": 77.2752,
249
- "aqi": 252
250
- },
251
- {
252
- "name": "DTU",
253
- "lat": 28.7501,
254
- "lon": 77.1177,
255
- "aqi": 219
256
- },
257
- {
258
- "name": "DU North Campus",
259
- "lat": 28.69,
260
- "lon": 77.21,
261
- "aqi": 298
262
- },
263
- {
264
- "name": "Ayanagar",
265
- "lat": 28.4809,
266
- "lon": 77.1255,
267
- "aqi": 342
268
- }
269
  ]
270
-
271
  for spot in hotspots:
272
- color = "gray" # Default for NA or invalid
273
  aqi = spot['aqi']
274
-
275
  if isinstance(aqi, (int, float)):
276
  color = "green"
277
  if aqi > 100: color = "yellow"
@@ -279,10 +181,9 @@ def generate_aqi_map_html():
279
  if aqi > 300: color = "red"
280
  if aqi > 400: color = "purple"
281
 
282
- # Add marker
283
  folium.CircleMarker(
284
  location=[spot['lat'], spot['lon']],
285
- radius=20,
286
  popup=f"{spot['name']}: AQI {spot['aqi']}",
287
  color=color,
288
  fill=True,
@@ -292,7 +193,7 @@ def generate_aqi_map_html():
292
 
293
  folium.Marker(
294
  location=[spot['lat'], spot['lon']],
295
- icon=folium.DivIcon(html=f'<div style="font-weight: bold; color: black;">{spot["aqi"]}</div>')
296
  ).add_to(m)
297
 
298
  return m.get_root().render()
@@ -301,7 +202,9 @@ def render_map_to_png(html_content, output_path):
301
  """
302
  Renders the HTML content to a PNG image using Selenium headless Chrome.
303
  """
304
- tmp_html_path = os.path.abspath("temp_map.html")
 
 
305
 
306
  # Write HTML to temp file
307
  with open(tmp_html_path, "w", encoding="utf-8") as f:
 
12
  DELHI_LAT = 28.7041
13
  DELHI_LON = 77.1025
14
 
15
+ def add_delhi_boundary_to_map(m):
16
+ """Helper to fetch and add Delhi boundary to a map."""
17
+ import requests
 
 
 
 
18
  try:
 
19
  geojson_url = "https://raw.githubusercontent.com/datameet/Municipal_Spatial_Data/master/Delhi/Delhi_Boundary.geojson"
20
+
21
+ # Add Boundary Outline
22
  folium.GeoJson(
23
  geojson_url,
24
  name="Delhi Boundary",
25
  style_function=lambda x: {
26
  'fillColor': 'none',
27
+ 'color': 'black', # Black for visibility on light tiles
28
+ 'weight': 3,
29
+ 'dashArray': '5, 5',
30
+ 'opacity': 0.6
31
  }
32
  ).add_to(m)
33
+ return True
34
+ except Exception as e:
35
+ print(f"Error adding boundary: {e}")
36
+ return False
37
+
38
+ def generate_heatmap_html():
39
+ """Generates a Folium map with Grid Heatmap (Clipped to Delhi)."""
40
+
41
+ # Create base map
42
+ m = folium.Map(
43
+ location=[DELHI_LAT, DELHI_LON],
44
+ zoom_start=10,
45
+ control_scale=True
46
+ )
47
+
48
+ # Heatmap Grid Generation
49
+ import requests
50
+ from shapely.geometry import shape, Point
51
+ from shapely.ops import unary_union
52
+
53
+ # Fetch Delhi Boundary for layout clipping & display
54
+ # We fetch manually here for the shapely polygon, AND add the visual layer
55
+ has_boundary = False
56
+ delhi_polygon = None
57
+
58
+ # Add visual boundary
59
+ add_delhi_boundary_to_map(m)
60
+
61
+ try:
62
+ geojson_url = "https://raw.githubusercontent.com/datameet/Municipal_Spatial_Data/master/Delhi/Delhi_Boundary.geojson"
63
+ resp = requests.get(geojson_url)
64
+ data = resp.json()
65
+
66
+ # Create a unified polygon for checking
67
+ features = data.get('features', [])
68
+ shapes = [shape(f['geometry']) for f in features]
69
+ delhi_polygon = unary_union(shapes)
70
+ has_boundary = True
71
  except Exception as e:
72
+ print(f"Error processing boundary for clipping: {e}")
73
+
74
+ # Grid config
75
+ lat_min, lat_max = 28.40, 28.88
76
+ lon_min, lon_max = 76.85, 77.35
77
+ step = 0.015 # Approx 1.5km grid size
78
+
79
+ def get_color(value):
80
+ """Red-scale heatmap colors like the reference image."""
81
+ if value < 100: return "#fee5d9" # Very Light Pink
82
+ if value < 200: return "#fcae91" # Light Pink/Orange
83
+ if value < 300: return "#fb6a4a" # Salmon
84
+ if value < 400: return "#de2d26" # Red
85
+ return "#a50f15" # Dark Red/Brown
86
+
87
+ lat = lat_min
88
+ while lat < lat_max:
89
+ lon = lon_min
90
+ while lon < lon_max:
91
+ # Check if center of grid is inside Delhi
92
+ center_point = Point(lon + step/2, lat + step/2)
93
+
94
+ if has_boundary and not delhi_polygon.contains(center_point):
95
+ # Skip if outside
96
+ lon += step
97
+ continue
98
+
99
+ # Generate simulated AQI/CO2 data with spatial coherence
100
+ # Higher near center (Delhi)
101
+ dist_center = ((lat - DELHI_LAT)**2 + (lon - DELHI_LON)**2)**0.5
102
+
103
+ # Base value + random noise - distance decay
104
+ base_value = 450 - (dist_center * 800)
105
+ val = base_value + random.randint(-50, 50)
106
+ val = max(50, min(500, val)) # Clamp 50-500
107
+
108
+ color = get_color(val)
109
+
110
+ # Draw grid cell
111
+ folium.Rectangle(
112
+ bounds=[[lat, lon], [lat + step, lon + step]],
113
+ color=None, # No border
114
+ fill=True,
115
+ fill_color=color,
116
+ fill_opacity=0.6, # Slightly transparent
117
+ tooltip=f"Zone AQI: {int(val)}"
118
+ ).add_to(m)
119
+
120
+ lon += step
121
+ lat += step
122
+
123
+ return m.get_root().render()
124
+
125
+ def generate_hotspots_html():
126
+ """Generates the original Hotspot Map with markers."""
127
+ m = folium.Map(location=[DELHI_LAT, DELHI_LON], zoom_start=11)
128
+
129
+ # Add Visual Boundary
130
+ add_delhi_boundary_to_map(m)
131
 
132
+ # Original Hotspots Data
 
133
  hotspots = [
134
+ {"name": "Chandni Chowk", "lat": 28.656, "lon": 77.231, "aqi": 384},
135
+ {"name": "Nehru Nagar", "lat": 28.5697, "lon": 77.253, "aqi": 384},
136
+ {"name": "New Moti Bagh", "lat": 28.582, "lon": 77.1717, "aqi": 377},
137
+ {"name": "Shadipur", "lat": 28.6517, "lon": 77.1582, "aqi": 375},
138
+ {"name": "Wazirpur", "lat": 28.6967, "lon": 77.1658, "aqi": 354},
139
+ {"name": "Ashok Vihar", "lat": 28.6856, "lon": 77.178, "aqi": 231},
140
+ {"name": "Mundka", "lat": 28.6794, "lon": 77.0284, "aqi": 353},
141
+ {"name": "Punjabi Bagh", "lat": 28.673, "lon": 77.1374, "aqi": 349},
142
+ {"name": "RK Puram", "lat": 28.5505, "lon": 77.1849, "aqi": 342},
143
+ {"name": "Jahangirpuri", "lat": 28.716, "lon": 77.1829, "aqi": 338},
144
+ {"name": "Okhla Phase-2", "lat": 28.5365, "lon": 77.2803, "aqi": 337},
145
+ {"name": "Dwarka Sector-8", "lat": 28.5656, "lon": 77.067, "aqi": 336},
146
+ {"name": "Patparganj", "lat": 28.612, "lon": 77.292, "aqi": 334},
147
+ {"name": "Bawana", "lat": 28.8, "lon": 77.03, "aqi": 328},
148
+ {"name": "Sonia Vihar", "lat": 28.7074, "lon": 77.2599, "aqi": 317},
149
+ {"name": "Rohini", "lat": 28.7019, "lon": 77.0984, "aqi": 312},
150
+ {"name": "Narela", "lat": 28.85, "lon": 77.1, "aqi": 311},
151
+ {"name": "Mandir Marg", "lat": 28.6325, "lon": 77.1994, "aqi": 311},
152
+ {"name": "Vivek Vihar", "lat": 28.6635, "lon": 77.3152, "aqi": 303},
153
+ {"name": "Anand Vihar", "lat": 28.6508, "lon": 77.3152, "aqi": 335},
154
+ {"name": "Dhyanchand Stadium", "lat": 28.6125, "lon": 77.2372, "aqi": 335},
155
+ {"name": "Sirifort", "lat": 28.5521, "lon": 77.2193, "aqi": 326},
156
+ {"name": "Karni Singh Shooting Range", "lat": 28.4998, "lon": 77.2668, "aqi": 323},
157
+ {"name": "ITO", "lat": 28.6294, "lon": 77.241, "aqi": 324},
158
+ {"name": "JLN Stadium", "lat": 28.5828, "lon": 77.2344, "aqi": 314},
159
+ {"name": "IGI Airport T3", "lat": 28.5562, "lon": 77.1, "aqi": 305},
160
+ {"name": "Pusa IMD", "lat": 28.6335, "lon": 77.1651, "aqi": 298},
161
+ {"name": "Burari Crossing", "lat": 28.7592, "lon": 77.1938, "aqi": 294},
162
+ {"name": "Aurobindo Marg", "lat": 28.545, "lon": 77.205, "aqi": 287},
163
+ {"name": "Dilshad Garden (IHBAS)", "lat": 28.681, "lon": 77.305, "aqi": 283},
164
+ {"name": "Lodhi Road", "lat": 28.5921, "lon": 77.2284, "aqi": 282},
165
+ {"name": "NSIT-Dwarka", "lat": 28.6081, "lon": 77.0193, "aqi": 275},
166
+ {"name": "Alipur", "lat": 28.8, "lon": 77.15, "aqi": 273},
167
+ {"name": "Najafgarh", "lat": 28.6125, "lon": 76.983, "aqi": 262},
168
+ {"name": "CRRI-Mathura Road", "lat": 28.5518, "lon": 77.2752, "aqi": 252},
169
+ {"name": "DTU", "lat": 28.7501, "lon": 77.1177, "aqi": 219},
170
+ {"name": "DU North Campus", "lat": 28.69, "lon": 77.21, "aqi": 298},
171
+ {"name": "Ayanagar", "lat": 28.4809, "lon": 77.1255, "aqi": 342}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
  ]
173
+
174
  for spot in hotspots:
175
+ color = "gray"
176
  aqi = spot['aqi']
 
177
  if isinstance(aqi, (int, float)):
178
  color = "green"
179
  if aqi > 100: color = "yellow"
 
181
  if aqi > 300: color = "red"
182
  if aqi > 400: color = "purple"
183
 
 
184
  folium.CircleMarker(
185
  location=[spot['lat'], spot['lon']],
186
+ radius=15,
187
  popup=f"{spot['name']}: AQI {spot['aqi']}",
188
  color=color,
189
  fill=True,
 
193
 
194
  folium.Marker(
195
  location=[spot['lat'], spot['lon']],
196
+ icon=folium.DivIcon(html=f'<div style="font-weight: bold; color: white; text-shadow: 0 0 3px black;">{spot["aqi"]}</div>')
197
  ).add_to(m)
198
 
199
  return m.get_root().render()
 
202
  """
203
  Renders the HTML content to a PNG image using Selenium headless Chrome.
204
  """
205
+ import uuid
206
+ unique_id = uuid.uuid4()
207
+ tmp_html_path = os.path.abspath(f"temp_map_{unique_id}.html")
208
 
209
  # Write HTML to temp file
210
  with open(tmp_html_path, "w", encoding="utf-8") as f:
static/aqi_map.png → aqi_metrics.pkl RENAMED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:687c30365da87046c8524385e8098a2db608a45d42a368e9f4788ba97d4b601e
3
- size 433996
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f32f773d93a81034f6c95c72aca695424d5540bc1cf93800b28c80e122160db5
3
+ size 76
aqi_model.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6cc68866cbf743a194980b11eaa30b172647edb1fff701f4d0660c7109178298
3
+ size 191992993
graph_state.json ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "nodes": [
3
+ {"id": "industries", "type": "causal", "position": {"x": 600, "y": 0}, "data": {"label": "Industries", "value": 100, "enabled": true, "type": "sector"}},
4
+ {"id": "transport", "type": "causal", "position": {"x": 600, "y": 150}, "data": {"label": "Transport", "value": 35, "enabled": true, "type": "sector"}},
5
+ {"id": "energy", "type": "causal", "position": {"x": 900, "y": 320}, "data": {"label": "Energy", "value": 0, "enabled": true, "type": "sector"}},
6
+ {"id": "infrastructure", "type": "causal", "position": {"x": 200, "y": 420}, "data": {"label": "Infrastructure", "value": 0, "enabled": true, "type": "sector"}},
7
+ {"id": "moves-goods", "type": "causal", "position": {"x": 600, "y": 80}, "data": {"label": "Moves goods → ↑ CO₂ & particulates", "value": 0, "enabled": true, "type": "intermediate"}},
8
+ {"id": "uses-power", "type": "causal", "position": {"x": 850, "y": 120}, "data": {"label": "Uses power → ↑ CO₂ & pollutants", "value": 0, "enabled": true, "type": "intermediate"}},
9
+ {"id": "powers-industry", "type": "causal", "position": {"x": 1050, "y": 120}, "data": {"label": "Powers industry → ↑ CO₂", "value": 0, "enabled": true, "type": "intermediate"}},
10
+ {"id": "industrial-pollutants", "type": "causal", "position": {"x": 1150, "y": 320}, "data": {"label": "Industrial pollutants (PM, NOx, SO₂)", "value": 0, "enabled": true, "type": "intermediate"}},
11
+ {"id": "fuels-transport", "type": "causal", "position": {"x": 750, "y": 240}, "data": {"label": "Fuels transport → ↑ CO₂", "value": 0, "enabled": true, "type": "intermediate"}},
12
+ {"id": "fuel-refining", "type": "causal", "position": {"x": 950, "y": 240}, "data": {"label": "Fuel refining → ↑ CO₂", "value": 0, "enabled": true, "type": "intermediate"}},
13
+ {"id": "vehicle-emissions", "type": "causal", "position": {"x": 1150, "y": 450}, "data": {"label": "Vehicle emissions (PM, NOx)", "value": 0, "enabled": true, "type": "intermediate"}},
14
+ {"id": "urban-sprawl", "type": "causal", "position": {"x": 600, "y": 280}, "data": {"label": "Urban sprawl → ↑ CO₂ & emissions", "value": 0, "enabled": true, "type": "intermediate"}},
15
+ {"id": "drives-construction", "type": "causal", "position": {"x": 100, "y": 240}, "data": {"label": "Drives construction → ↑ CO₂", "value": 0, "enabled": true, "type": "intermediate"}},
16
+ {"id": "enables-industry", "type": "causal", "position": {"x": 300, "y": 240}, "data": {"label": "Enables industry → ↑ CO₂", "value": 0, "enabled": true, "type": "intermediate"}},
17
+ {"id": "needs-roads", "type": "causal", "position": {"x": 400, "y": 300}, "data": {"label": "Needs roads/airports → ↑ CO₂", "value": 0, "enabled": true, "type": "intermediate"}},
18
+ {"id": "embodied-use", "type": "causal", "position": {"x": 200, "y": 540}, "data": {"label": "Embodied + use", "value": 0, "enabled": true, "type": "intermediate"}},
19
+ {"id": "energy-demand", "type": "causal", "position": {"x": 700, "y": 360}, "data": {"label": "Energy demand → ↑ CO₂", "value": 0, "enabled": true, "type": "intermediate"}},
20
+ {"id": "generation", "type": "causal", "position": {"x": 900, "y": 420}, "data": {"label": "Generation", "value": 0, "enabled": true, "type": "intermediate"}},
21
+ {"id": "power-generation", "type": "causal", "position": {"x": 900, "y": 540}, "data": {"label": "Power generation (SO₂, NOx)", "value": 0, "enabled": true, "type": "intermediate"}},
22
+ {"id": "direct-indirect", "type": "causal", "position": {"x": 50, "y": 340}, "data": {"label": "Direct + indirect", "value": 0, "enabled": true, "type": "intermediate"}},
23
+ {"id": "fuel-travel", "type": "causal", "position": {"x": 50, "y": 420}, "data": {"label": "Fuel & travel", "value": 0, "enabled": true, "type": "intermediate"}},
24
+ {"id": "co2", "type": "causal", "position": {"x": 300, "y": 650}, "data": {"label": "CO₂ Emissions", "value": 0, "enabled": true, "type": "output"}},
25
+ {"id": "contributes", "type": "causal", "position": {"x": 300, "y": 720}, "data": {"label": "Contributes to", "value": 0, "enabled": true, "type": "intermediate"}},
26
+ {"id": "aqi", "type": "causal", "position": {"x": 300, "y": 800}, "data": {"label": "Air Quality Index (AQI)", "value": 0, "enabled": true, "type": "output"}},
27
+ {"id": "poor-aqi", "type": "causal", "position": {"x": 700, "y": 650}, "data": {"label": "Poor AQI → Urban health/stability", "value": 0, "enabled": true, "type": "intermediate"}}
28
+ ],
29
+ "edges": [
30
+ {"id": "i-mg", "source": "industries", "target": "moves-goods", "data": {"weight": 0.6}, "type": "smoothstep"},
31
+ {"id": "mg-t", "source": "moves-goods", "target": "transport", "data": {"weight": 0.6}, "type": "smoothstep"},
32
+ {"id": "i-up", "source": "industries", "target": "uses-power", "data": {"weight": 0.5}, "type": "smoothstep"},
33
+ {"id": "i-pi", "source": "industries", "target": "powers-industry", "data": {"weight": 0.4}, "type": "smoothstep"},
34
+ {"id": "pi-ip", "source": "powers-industry", "target": "industrial-pollutants", "data": {"weight": 0.7}, "type": "smoothstep"},
35
+ {"id": "up-ip", "source": "uses-power", "target": "industrial-pollutants", "data": {"weight": 0.6}, "type": "smoothstep"},
36
+ {"id": "pi-e", "source": "powers-industry", "target": "energy", "data": {"weight": 0.6}, "type": "smoothstep"},
37
+ {"id": "t-ft", "source": "transport", "target": "fuels-transport", "data": {"weight": 0.5}, "type": "smoothstep"},
38
+ {"id": "t-fr", "source": "transport", "target": "fuel-refining", "data": {"weight": 0.5}, "type": "smoothstep"},
39
+ {"id": "ft-e", "source": "fuels-transport", "target": "energy", "data": {"weight": 0.6}, "type": "smoothstep"},
40
+ {"id": "fr-e", "source": "fuel-refining", "target": "energy", "data": {"weight": 0.6}, "type": "smoothstep"},
41
+ {"id": "t-us", "source": "transport", "target": "urban-sprawl", "data": {"weight": 0.4}, "type": "smoothstep"},
42
+ {"id": "t-ve", "source": "transport", "target": "vehicle-emissions", "data": {"weight": 0.7}, "type": "smoothstep"},
43
+ {"id": "t-dc", "source": "transport", "target": "drives-construction", "data": {"weight": 0.4}, "type": "smoothstep"},
44
+ {"id": "t-ei", "source": "transport", "target": "enables-industry", "data": {"weight": 0.4}, "type": "smoothstep"},
45
+ {"id": "t-nr", "source": "transport", "target": "needs-roads", "data": {"weight": 0.4}, "type": "smoothstep"},
46
+ {"id": "dc-inf", "source": "drives-construction", "target": "infrastructure", "data": {"weight": 0.6}, "type": "smoothstep"},
47
+ {"id": "ei-inf", "source": "enables-industry", "target": "infrastructure", "data": {"weight": 0.6}, "type": "smoothstep"},
48
+ {"id": "nr-inf", "source": "needs-roads", "target": "infrastructure", "data": {"weight": 0.6}, "type": "smoothstep"},
49
+ {"id": "us-inf", "source": "urban-sprawl", "target": "infrastructure", "data": {"weight": 0.6}, "type": "smoothstep"},
50
+ {"id": "inf-eu", "source": "infrastructure", "target": "embodied-use", "data": {"weight": 0.5}, "type": "smoothstep"},
51
+ {"id": "us-ed", "source": "urban-sprawl", "target": "energy-demand", "data": {"weight": 0.5}, "type": "smoothstep"},
52
+ {"id": "ed-g", "source": "energy-demand", "target": "generation", "data": {"weight": 0.6}, "type": "smoothstep"},
53
+ {"id": "e-g", "source": "energy", "target": "generation", "data": {"weight": 0.7}, "type": "smoothstep"},
54
+ {"id": "g-pg", "source": "generation", "target": "power-generation", "data": {"weight": 0.6}, "type": "smoothstep"},
55
+ {"id": "up-di", "source": "uses-power", "target": "direct-indirect", "data": {"weight": 0.5}, "type": "smoothstep"},
56
+ {"id": "ip-di", "source": "industrial-pollutants", "target": "direct-indirect", "data": {"weight": 0.6}, "type": "smoothstep"},
57
+ {"id": "ve-ft", "source": "vehicle-emissions", "target": "fuel-travel", "data": {"weight": 0.7}, "type": "smoothstep"},
58
+ {"id": "eu-co2", "source": "embodied-use", "target": "co2", "data": {"weight": 0.6}, "type": "smoothstep"},
59
+ {"id": "pg-co2", "source": "power-generation", "target": "co2", "data": {"weight": 0.7}, "type": "smoothstep"},
60
+ {"id": "ve-co2", "source": "vehicle-emissions", "target": "co2", "data": {"weight": 0.7}, "type": "smoothstep"},
61
+ {"id": "ip-co2", "source": "industrial-pollutants", "target": "co2", "data": {"weight": 0.8}, "type": "smoothstep"},
62
+ {"id": "di-co2", "source": "direct-indirect", "target": "co2", "data": {"weight": 0.8}, "type": "smoothstep"},
63
+ {"id": "ft-co2", "source": "fuel-travel", "target": "co2", "data": {"weight": 0.7}, "type": "smoothstep"},
64
+ {"id": "co2-c", "source": "co2", "target": "contributes", "data": {"weight": 0.8}, "type": "smoothstep"},
65
+ {"id": "c-aqi", "source": "contributes", "target": "aqi", "data": {"weight": 0.8}, "type": "smoothstep"},
66
+ {"id": "aqi-pa", "source": "aqi", "target": "poor-aqi", "data": {"weight": 0.8}, "type": "smoothstep"}
67
+ ]
68
+ }
gunicorn_conf.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ # Render.com gives 512MB RAM for the free tier.
4
+ # We must limit workers to avoid OOM kills.
5
+ # 1 worker + multiple threads is best for memory-constrained Python apps with I/O (like DB/LLM calls).
6
+
7
+ port = os.environ.get("PORT", "10000")
8
+ bind = f"0.0.0.0:{port}"
9
+
10
+ workers = 1
11
+ threads = 4
12
+ timeout = 120 # LLM calls can be slow
13
+ keepalive = 5
14
+
15
+ # Logging
16
+ accesslog = "-"
17
+ errorlog = "-"
18
+ loglevel = "info"
requirements.txt CHANGED
@@ -33,3 +33,5 @@ xgboost
33
  folium
34
  selenium
35
  webdriver-manager
 
 
 
33
  folium
34
  selenium
35
  webdriver-manager
36
+ shapely
37
+ requests
static/aqi_map_heatmap.png ADDED

Git LFS Details

  • SHA256: db47a474a2f67cf0a365a3e65612498a725b852c625432f92021f0868ad4ceda
  • Pointer size: 131 Bytes
  • Size of remote file: 447 kB
static/aqi_map_hotspots.png ADDED

Git LFS Details

  • SHA256: 7bb768e2d302f6113252d8d32ce569a2752cd1a6a88514c307a3464a5b152b88
  • Pointer size: 131 Bytes
  • Size of remote file: 442 kB
train_aqi_model.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import joblib
3
+ from aqi_history import AQIHistory
4
+
5
+ MODEL_PATH = 'aqi_model.pkl'
6
+
7
+ def train_and_save():
8
+ print("Initializing AQI History...")
9
+ # This triggers training in __init__
10
+ handler = AQIHistory()
11
+
12
+ if handler.model:
13
+ print("Model trained successfully.")
14
+ print("Saving model to disk...")
15
+ joblib.dump(handler.model, MODEL_PATH)
16
+ joblib.dump(handler.metrics, 'aqi_metrics.pkl')
17
+ print(f"Model saved to {MODEL_PATH}")
18
+ else:
19
+ print("Failed to train model.")
20
+
21
+ if __name__ == "__main__":
22
+ train_and_save()