Create app.py
Browse files
app.py
ADDED
@@ -0,0 +1,338 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import gradio as gr
|
2 |
+
import requests
|
3 |
+
import pandas as pd
|
4 |
+
import folium
|
5 |
+
from folium import plugins
|
6 |
+
import json
|
7 |
+
from datetime import datetime, timedelta
|
8 |
+
import plotly.express as px
|
9 |
+
import plotly.graph_objects as go
|
10 |
+
from plotly.subplots import make_subplots
|
11 |
+
|
12 |
+
class AirQualityMonitor:
|
13 |
+
def __init__(self):
|
14 |
+
self.base_url = "https://www.airnowapi.org/aq"
|
15 |
+
# You'll need to get an API key from https://docs.airnowapi.org/
|
16 |
+
self.api_key = "YOUR_API_KEY_HERE" # Replace with actual API key
|
17 |
+
|
18 |
+
def get_current_observations(self, state_code=None, bbox=None):
|
19 |
+
"""Get current air quality observations"""
|
20 |
+
if not self.api_key or self.api_key == "YOUR_API_KEY_HERE":
|
21 |
+
# Return sample data for demo purposes
|
22 |
+
return self._get_sample_data()
|
23 |
+
|
24 |
+
url = f"{self.base_url}/observation/zipCode/current/"
|
25 |
+
params = {
|
26 |
+
'format': 'application/json',
|
27 |
+
'API_KEY': self.api_key,
|
28 |
+
'distance': 100
|
29 |
+
}
|
30 |
+
|
31 |
+
if state_code:
|
32 |
+
# Get observations for a specific state
|
33 |
+
params['zipCode'] = self._get_state_zip(state_code)
|
34 |
+
|
35 |
+
try:
|
36 |
+
response = requests.get(url, params=params)
|
37 |
+
response.raise_for_status()
|
38 |
+
return response.json()
|
39 |
+
except Exception as e:
|
40 |
+
print(f"API Error: {e}")
|
41 |
+
return self._get_sample_data()
|
42 |
+
|
43 |
+
def _get_state_zip(self, state_code):
|
44 |
+
"""Get a representative zip code for a state"""
|
45 |
+
state_zips = {
|
46 |
+
'CA': '90210', 'NY': '10001', 'TX': '73301', 'FL': '33101',
|
47 |
+
'IL': '60601', 'PA': '19101', 'OH': '43215', 'GA': '30301',
|
48 |
+
'NC': '27601', 'MI': '48201', 'NJ': '07001', 'VA': '23219',
|
49 |
+
'WA': '98101', 'AZ': '85001', 'MA': '02101', 'TN': '37201',
|
50 |
+
'IN': '46201', 'MO': '63101', 'MD': '21201', 'WI': '53201'
|
51 |
+
}
|
52 |
+
return state_zips.get(state_code, '90210')
|
53 |
+
|
54 |
+
def _get_sample_data(self):
|
55 |
+
"""Return sample data for demonstration"""
|
56 |
+
return [
|
57 |
+
{
|
58 |
+
"DateObserved": "2025-06-29",
|
59 |
+
"HourObserved": 14,
|
60 |
+
"LocalTimeZone": "PST",
|
61 |
+
"ReportingArea": "Los Angeles-South Coast Air Basin",
|
62 |
+
"StateCode": "CA",
|
63 |
+
"Latitude": 34.0522,
|
64 |
+
"Longitude": -118.2437,
|
65 |
+
"ParameterName": "PM2.5",
|
66 |
+
"AQI": 65,
|
67 |
+
"Category": {"Number": 2, "Name": "Moderate"}
|
68 |
+
},
|
69 |
+
{
|
70 |
+
"DateObserved": "2025-06-29",
|
71 |
+
"HourObserved": 14,
|
72 |
+
"LocalTimeZone": "EST",
|
73 |
+
"ReportingArea": "New York",
|
74 |
+
"StateCode": "NY",
|
75 |
+
"Latitude": 40.7128,
|
76 |
+
"Longitude": -74.0060,
|
77 |
+
"ParameterName": "Ozone",
|
78 |
+
"AQI": 45,
|
79 |
+
"Category": {"Number": 1, "Name": "Good"}
|
80 |
+
},
|
81 |
+
{
|
82 |
+
"DateObserved": "2025-06-29",
|
83 |
+
"HourObserved": 14,
|
84 |
+
"LocalTimeZone": "CST",
|
85 |
+
"ReportingArea": "Chicago",
|
86 |
+
"StateCode": "IL",
|
87 |
+
"Latitude": 41.8781,
|
88 |
+
"Longitude": -87.6298,
|
89 |
+
"ParameterName": "PM2.5",
|
90 |
+
"AQI": 85,
|
91 |
+
"Category": {"Number": 3, "Name": "Unhealthy for Sensitive Groups"}
|
92 |
+
},
|
93 |
+
{
|
94 |
+
"DateObserved": "2025-06-29",
|
95 |
+
"HourObserved": 14,
|
96 |
+
"LocalTimeZone": "MST",
|
97 |
+
"ReportingArea": "Phoenix",
|
98 |
+
"StateCode": "AZ",
|
99 |
+
"Latitude": 33.4484,
|
100 |
+
"Longitude": -112.0740,
|
101 |
+
"ParameterName": "Ozone",
|
102 |
+
"AQI": 95,
|
103 |
+
"Category": {"Number": 3, "Name": "Unhealthy for Sensitive Groups"}
|
104 |
+
},
|
105 |
+
{
|
106 |
+
"DateObserved": "2025-06-29",
|
107 |
+
"HourObserved": 14,
|
108 |
+
"LocalTimeZone": "PST",
|
109 |
+
"ReportingArea": "Seattle",
|
110 |
+
"StateCode": "WA",
|
111 |
+
"Latitude": 47.6062,
|
112 |
+
"Longitude": -122.3321,
|
113 |
+
"ParameterName": "PM2.5",
|
114 |
+
"AQI": 25,
|
115 |
+
"Category": {"Number": 1, "Name": "Good"}
|
116 |
+
},
|
117 |
+
{
|
118 |
+
"DateObserved": "2025-06-29",
|
119 |
+
"HourObserved": 14,
|
120 |
+
"LocalTimeZone": "EST",
|
121 |
+
"ReportingArea": "Miami",
|
122 |
+
"StateCode": "FL",
|
123 |
+
"Latitude": 25.7617,
|
124 |
+
"Longitude": -80.1918,
|
125 |
+
"ParameterName": "Ozone",
|
126 |
+
"AQI": 55,
|
127 |
+
"Category": {"Number": 2, "Name": "Moderate"}
|
128 |
+
}
|
129 |
+
]
|
130 |
+
|
131 |
+
def get_aqi_color(aqi):
|
132 |
+
"""Return color based on AQI value"""
|
133 |
+
if aqi <= 50:
|
134 |
+
return "#00E400" # Green
|
135 |
+
elif aqi <= 100:
|
136 |
+
return "#FFFF00" # Yellow
|
137 |
+
elif aqi <= 150:
|
138 |
+
return "#FF7E00" # Orange
|
139 |
+
elif aqi <= 200:
|
140 |
+
return "#FF0000" # Red
|
141 |
+
elif aqi <= 300:
|
142 |
+
return "#8F3F97" # Purple
|
143 |
+
else:
|
144 |
+
return "#7E0023" # Maroon
|
145 |
+
|
146 |
+
def create_air_quality_map(data):
|
147 |
+
"""Create an interactive map with air quality data"""
|
148 |
+
if not data:
|
149 |
+
# Create empty map centered on US
|
150 |
+
m = folium.Map(location=[39.8283, -98.5795], zoom_start=4)
|
151 |
+
folium.Marker(
|
152 |
+
[39.8283, -98.5795],
|
153 |
+
popup="No data available. Please add your AirNow API key.",
|
154 |
+
icon=folium.Icon(color='red')
|
155 |
+
).add_to(m)
|
156 |
+
return m._repr_html_()
|
157 |
+
|
158 |
+
# Calculate map center
|
159 |
+
lats = [float(station['Latitude']) for station in data]
|
160 |
+
lons = [float(station['Longitude']) for station in data]
|
161 |
+
center_lat = sum(lats) / len(lats)
|
162 |
+
center_lon = sum(lons) / len(lons)
|
163 |
+
|
164 |
+
# Create map
|
165 |
+
m = folium.Map(location=[center_lat, center_lon], zoom_start=4)
|
166 |
+
|
167 |
+
# Add markers for each monitoring station
|
168 |
+
for station in data:
|
169 |
+
lat = float(station['Latitude'])
|
170 |
+
lon = float(station['Longitude'])
|
171 |
+
aqi = station.get('AQI', 0)
|
172 |
+
parameter = station.get('ParameterName', 'Unknown')
|
173 |
+
area = station.get('ReportingArea', 'Unknown Area')
|
174 |
+
category = station.get('Category', {}).get('Name', 'Unknown')
|
175 |
+
|
176 |
+
color = get_aqi_color(aqi)
|
177 |
+
|
178 |
+
popup_html = f"""
|
179 |
+
<div style="width: 200px;">
|
180 |
+
<h4>{area}</h4>
|
181 |
+
<p><strong>AQI:</strong> {aqi}</p>
|
182 |
+
<p><strong>Parameter:</strong> {parameter}</p>
|
183 |
+
<p><strong>Category:</strong> {category}</p>
|
184 |
+
<p><strong>Date:</strong> {station.get('DateObserved', 'N/A')}</p>
|
185 |
+
<p><strong>Time:</strong> {station.get('HourObserved', 'N/A')}:00 {station.get('LocalTimeZone', '')}</p>
|
186 |
+
</div>
|
187 |
+
"""
|
188 |
+
|
189 |
+
folium.CircleMarker(
|
190 |
+
location=[lat, lon],
|
191 |
+
radius=8,
|
192 |
+
popup=folium.Popup(popup_html, max_width=250),
|
193 |
+
color='black',
|
194 |
+
weight=1,
|
195 |
+
fill=True,
|
196 |
+
fillColor=color,
|
197 |
+
fillOpacity=0.8,
|
198 |
+
tooltip=f"{area}: AQI {aqi}"
|
199 |
+
).add_to(m)
|
200 |
+
|
201 |
+
# Add legend
|
202 |
+
legend_html = '''
|
203 |
+
<div style="position: fixed;
|
204 |
+
top: 10px; right: 10px; width: 160px; height: 140px;
|
205 |
+
background-color: white; border:2px solid grey; z-index:9999;
|
206 |
+
font-size:14px; padding: 10px">
|
207 |
+
<h4>AQI Legend</h4>
|
208 |
+
<p><i class="fa fa-circle" style="color:#00E400"></i> Good (0-50)</p>
|
209 |
+
<p><i class="fa fa-circle" style="color:#FFFF00"></i> Moderate (51-100)</p>
|
210 |
+
<p><i class="fa fa-circle" style="color:#FF7E00"></i> Unhealthy for Sensitive (101-150)</p>
|
211 |
+
<p><i class="fa fa-circle" style="color:#FF0000"></i> Unhealthy (151-200)</p>
|
212 |
+
<p><i class="fa fa-circle" style="color:#8F3F97"></i> Very Unhealthy (201-300)</p>
|
213 |
+
</div>
|
214 |
+
'''
|
215 |
+
m.get_root().html.add_child(folium.Element(legend_html))
|
216 |
+
|
217 |
+
return m._repr_html_()
|
218 |
+
|
219 |
+
def create_aqi_chart(data):
|
220 |
+
"""Create a chart showing AQI levels by location"""
|
221 |
+
if not data:
|
222 |
+
return None
|
223 |
+
|
224 |
+
df = pd.DataFrame(data)
|
225 |
+
df['AQI'] = pd.to_numeric(df['AQI'], errors='coerce')
|
226 |
+
|
227 |
+
# Create bar chart
|
228 |
+
fig = px.bar(df,
|
229 |
+
x='ReportingArea',
|
230 |
+
y='AQI',
|
231 |
+
color='AQI',
|
232 |
+
color_continuous_scale='RdYlGn_r',
|
233 |
+
title='Air Quality Index by Location',
|
234 |
+
labels={'AQI': 'Air Quality Index', 'ReportingArea': 'Location'})
|
235 |
+
|
236 |
+
fig.update_layout(xaxis_tickangle=-45, height=500)
|
237 |
+
fig.update_traces(hovertemplate='<b>%{x}</b><br>AQI: %{y}<extra></extra>')
|
238 |
+
|
239 |
+
return fig
|
240 |
+
|
241 |
+
def get_air_quality_summary(data):
|
242 |
+
"""Generate a summary of air quality data"""
|
243 |
+
if not data:
|
244 |
+
return "No air quality data available."
|
245 |
+
|
246 |
+
df = pd.DataFrame(data)
|
247 |
+
df['AQI'] = pd.to_numeric(df['AQI'], errors='coerce')
|
248 |
+
|
249 |
+
total_stations = len(df)
|
250 |
+
avg_aqi = df['AQI'].mean()
|
251 |
+
max_aqi = df['AQI'].max()
|
252 |
+
min_aqi = df['AQI'].min()
|
253 |
+
|
254 |
+
# Count by category
|
255 |
+
categories = df['Category'].apply(lambda x: x.get('Name', 'Unknown') if isinstance(x, dict) else 'Unknown')
|
256 |
+
category_counts = categories.value_counts()
|
257 |
+
|
258 |
+
summary = f"""
|
259 |
+
## Air Quality Summary
|
260 |
+
|
261 |
+
**Total Monitoring Stations:** {total_stations}
|
262 |
+
**Average AQI:** {avg_aqi:.1f}
|
263 |
+
**Highest AQI:** {max_aqi}
|
264 |
+
**Lowest AQI:** {min_aqi}
|
265 |
+
|
266 |
+
### Air Quality Categories:
|
267 |
+
"""
|
268 |
+
|
269 |
+
for category, count in category_counts.items():
|
270 |
+
percentage = (count / total_stations) * 100
|
271 |
+
summary += f"- **{category}:** {count} stations ({percentage:.1f}%)\n"
|
272 |
+
|
273 |
+
return summary
|
274 |
+
|
275 |
+
def update_data():
|
276 |
+
"""Main function to fetch and display air quality data"""
|
277 |
+
monitor = AirQualityMonitor()
|
278 |
+
data = monitor.get_current_observations()
|
279 |
+
|
280 |
+
map_html = create_air_quality_map(data)
|
281 |
+
chart = create_aqi_chart(data)
|
282 |
+
summary = get_air_quality_summary(data)
|
283 |
+
|
284 |
+
return map_html, chart, summary
|
285 |
+
|
286 |
+
# Create Gradio interface
|
287 |
+
with gr.Blocks(title="Air Quality Monitor", theme=gr.themes.Soft()) as demo:
|
288 |
+
gr.Markdown("# π¬οΈ Real-Time Air Quality Monitor")
|
289 |
+
gr.Markdown("Monitor air quality data from NOAA/EPA AirNow stations across the United States")
|
290 |
+
|
291 |
+
with gr.Row():
|
292 |
+
gr.Markdown("""
|
293 |
+
This application displays real-time air quality data from over 2,000 monitoring stations
|
294 |
+
across the United States. The data includes Air Quality Index (AQI) values for various
|
295 |
+
pollutants like PM2.5, PM10, Ozone, and more.
|
296 |
+
|
297 |
+
**Note:** To access live data, you need to:
|
298 |
+
1. Get a free API key from [AirNow API](https://docs.airnowapi.org/)
|
299 |
+
2. Replace `YOUR_API_KEY_HERE` in the code with your actual API key
|
300 |
+
|
301 |
+
Currently showing sample data for demonstration.
|
302 |
+
""")
|
303 |
+
|
304 |
+
with gr.Row():
|
305 |
+
update_btn = gr.Button("π Update Air Quality Data", variant="primary", size="lg")
|
306 |
+
|
307 |
+
with gr.Row():
|
308 |
+
with gr.Column(scale=2):
|
309 |
+
map_output = gr.HTML(label="Air Quality Map")
|
310 |
+
with gr.Column(scale=1):
|
311 |
+
summary_output = gr.Markdown(label="Summary")
|
312 |
+
|
313 |
+
with gr.Row():
|
314 |
+
chart_output = gr.Plot(label="AQI Chart")
|
315 |
+
|
316 |
+
# Add information section
|
317 |
+
with gr.Row():
|
318 |
+
gr.Markdown("""
|
319 |
+
## About Air Quality Index (AQI)
|
320 |
+
|
321 |
+
The AQI is an index for reporting daily air quality. It tells you how clean or polluted your air is:
|
322 |
+
|
323 |
+
- **Good (0-50)**: π’ Air quality is satisfactory
|
324 |
+
- **Moderate (51-100)**: π‘ Acceptable for most people
|
325 |
+
- **Unhealthy for Sensitive Groups (101-150)**: π May cause problems for sensitive individuals
|
326 |
+
- **Unhealthy (151-200)**: π΄ May cause health problems for everyone
|
327 |
+
- **Very Unhealthy (201-300)**: π£ Health alert for everyone
|
328 |
+
- **Hazardous (301+)**: π€ Health emergency
|
329 |
+
|
330 |
+
**Data Source:** EPA AirNow - Real-time air quality observations from government monitoring stations
|
331 |
+
""")
|
332 |
+
|
333 |
+
# Load initial data
|
334 |
+
demo.load(update_data, outputs=[map_output, chart_output, summary_output])
|
335 |
+
update_btn.click(update_data, outputs=[map_output, chart_output, summary_output])
|
336 |
+
|
337 |
+
if __name__ == "__main__":
|
338 |
+
demo.launch()
|