nakas commited on
Commit
fd791ba
·
verified ·
1 Parent(s): 707ea7b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +311 -412
app.py CHANGED
@@ -1,464 +1,363 @@
1
  import gradio as gr
2
- import requests
3
  import pandas as pd
4
  import numpy as np
5
- from datetime import datetime, timedelta
 
 
 
 
 
6
  import matplotlib.pyplot as plt
7
  from matplotlib.gridspec import GridSpec
8
- import json
9
- from bs4 import BeautifulSoup
10
 
11
- def get_raw_data(station_id):
12
- """
13
- Get raw data from the NWS API.
14
- """
15
- headers = {
16
- 'User-Agent': '(Weather Data Viewer, contact@yourdomain.com)',
17
- 'Accept': 'application/json'
18
- }
19
-
20
- # Calculate correct date range for last 3 days
21
- end_time = datetime.utcnow()
22
- start_time = end_time - timedelta(hours=72) # Last 3 days
23
-
24
- params = {
25
- 'start': start_time.isoformat() + 'Z',
26
- 'end': end_time.isoformat() + 'Z'
27
- }
28
-
29
- url = f"https://api.weather.gov/stations/{station_id}/observations"
30
-
31
  try:
32
- print("\nFetching observations...")
33
- print(f"URL: {url}")
34
- print(f"Time range: {start_time} to {end_time}")
35
- response = requests.get(url, headers=headers, params=params)
36
- print(f"Response status: {response.status_code}")
37
-
38
- if response.status_code != 200:
39
- print(f"Response content: {response.text}")
40
- response.raise_for_status()
41
-
42
- data = response.json()
43
-
44
- if 'features' in data:
45
- print(f"\nNumber of observations: {len(data['features'])}")
46
- if len(data['features']) > 0:
47
- print("\nFirst observation properties:")
48
- print(json.dumps(data['features'][0]['properties'], indent=2))
49
-
50
- print("\nAll available property keys:")
51
- keys = set()
52
- for feature in data['features']:
53
- keys.update(feature['properties'].keys())
54
- print(sorted(list(keys)))
55
-
56
- return data
57
  except Exception as e:
58
- print(f"Error fetching data: {e}")
59
- import traceback
60
- traceback.print_exc()
61
- return None
62
 
63
- def parse_raw_data(data):
64
- """
65
- Parse the raw JSON data into a DataFrame with additional weather information.
66
- """
67
- if not data or 'features' not in data:
68
- return None
69
 
70
- records = []
71
- print("\n=== Detailed Weather Information ===")
 
72
 
73
- for feature in data['features']:
74
- props = feature['properties']
75
-
76
- # Print detailed information for each observation
77
- print(f"\nTimestamp: {props['timestamp']}")
78
-
79
- # Present Weather
80
- if props['presentWeather']:
81
- print("Present Weather:")
82
- for weather in props['presentWeather']:
83
- print(f" - {weather}")
84
- else:
85
- print("Present Weather: None reported")
86
 
87
- # Precipitation
88
- precip = props.get('precipitationLast3Hours', {}).get('value')
89
- print(f"Precipitation (last 3 hours): {precip if precip is not None else 'Not reported'} mm")
90
-
91
- # Raw Message
92
- raw_msg = props.get('rawMessage', '')
93
- if raw_msg:
94
- print(f"Raw Message: {raw_msg}")
95
- else:
96
- print("Raw Message: None")
97
 
98
- # Extract text description if available
99
- text_desc = props.get('textDescription', '')
100
- if text_desc:
101
- print(f"Text Description: {text_desc}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
 
103
- print("-" * 50)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
 
105
- # Create record for DataFrame
106
- record = {
107
- 'timestamp': props['timestamp'],
108
- 'temperature': props.get('temperature', {}).get('value'),
109
- 'wind_speed': props.get('windSpeed', {}).get('value'),
110
- 'wind_direction': props.get('windDirection', {}).get('value'),
111
- 'precipitation_3h': props.get('precipitationLast3Hours', {}).get('value'),
112
- 'present_weather': '; '.join(str(w) for w in props.get('presentWeather', [])) if props.get('presentWeather') else None,
113
- 'raw_message': props.get('rawMessage', ''),
114
- 'text_description': props.get('textDescription', ''),
115
- 'dewpoint': props.get('dewpoint', {}).get('value'),
116
- 'relative_humidity': props.get('relativeHumidity', {}).get('value'),
117
- 'wind_chill': props.get('windChill', {}).get('value')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
  }
119
-
120
- records.append(record)
121
-
122
- df = pd.DataFrame(records)
123
 
124
- print("\nDataFrame columns from API:")
125
- print(df.columns.tolist())
126
- print("\nDetailed sample of raw API data:")
127
- pd.set_option('display.max_columns', None)
128
- print(df.head().to_string())
129
 
130
  return df
131
 
132
- def process_weather_data(df):
133
- """
134
- Process the weather DataFrame including precipitation and additional fields.
135
- """
136
- if df is None or df.empty:
137
- return None
138
-
139
- # Convert timestamp
140
- df['timestamp'] = pd.to_datetime(df['timestamp'])
141
- df['date'] = df['timestamp'].dt.date
142
-
143
- # Convert temperature from Celsius to Fahrenheit if not null
144
- if df['temperature'].notna().any():
145
- df['temperature'] = (df['temperature'] * 9/5) + 32
146
-
147
- # Convert wind speed from km/h to mph if not null
148
- if df['wind_speed'].notna().any():
149
- df['wind_speed'] = df['wind_speed'] * 0.621371
150
-
151
- # Convert precipitation from mm to inches if not null
152
- if 'precipitation_3h' in df.columns and df['precipitation_3h'].notna().any():
153
- df['precipitation_3h'] = df['precipitation_3h'] * 0.0393701 # mm to inches
154
-
155
- # Convert dewpoint from Celsius to Fahrenheit if not null
156
- if 'dewpoint' in df.columns and df['dewpoint'].notna().any():
157
- df['dewpoint'] = (df['dewpoint'] * 9/5) + 32
158
-
159
- # Convert wind chill from Celsius to Fahrenheit if not null
160
- if 'wind_chill' in df.columns and df['wind_chill'].notna().any():
161
- df['wind_chill'] = (df['wind_chill'] * 9/5) + 32
162
-
163
- # Print summary of weather conditions
164
- print("\n=== Weather Summary ===")
165
- print(f"Time range: {df['timestamp'].min()} to {df['timestamp'].max()}")
166
-
167
- print("\nPrecipitation Summary:")
168
- precip_data = df[df['precipitation_3h'].notna()]
169
- if not precip_data.empty:
170
- print(f"Total precipitation events: {len(precip_data)}")
171
- print(f"Maximum 3-hour precipitation: {precip_data['precipitation_3h'].max():.2f} inches")
172
- else:
173
- print("No precipitation data available")
174
-
175
- print("\nPresent Weather Conditions Summary:")
176
- weather_data = df[df['present_weather'].notna()]
177
- if not weather_data.empty:
178
- unique_conditions = weather_data['present_weather'].unique()
179
- print("Observed weather conditions:")
180
- for condition in unique_conditions:
181
- print(f" - {condition}")
182
- else:
183
- print("No present weather conditions reported")
184
-
185
- return df
186
-
187
- def scrape_snow_depth():
188
- """
189
- Scrapes snow depth data from the weather.gov timeseries page.
190
- """
191
- url = "https://www.weather.gov/wrh/timeseries?site=YCTIM&hours=720&units=english&chart=on&headers=on&obs=tabular&hourly=false&pview=standard&font=12&plot="
192
- try:
193
- response = requests.get(url)
194
- if response.status_code != 200:
195
- print(f"Failed to fetch HTML page: {response.status_code}")
196
- return pd.DataFrame()
197
- soup = BeautifulSoup(response.text, 'html.parser')
198
-
199
- tables = soup.find_all("table")
200
- target_table = None
201
- for table in tables:
202
- header_row = table.find("tr")
203
- if not header_row:
204
- continue
205
- headers = [th.get_text(strip=True) for th in header_row.find_all("th")]
206
- print("Found table headers:", headers)
207
- if any("time" in h.lower() for h in headers) and any("snow" in h.lower() for h in headers):
208
- target_table = table
209
- break
210
-
211
- if target_table is None:
212
- print("No table with required headers found.")
213
- return pd.DataFrame()
214
-
215
- header_row = target_table.find("tr")
216
- headers = [th.get_text(strip=True) for th in header_row.find_all("th")]
217
- time_index = None
218
- snow_index = None
219
- for i, header in enumerate(headers):
220
- if "time" in header.lower():
221
- time_index = i
222
- if "snow" in header.lower():
223
- snow_index = i
224
 
225
- if time_index is None or snow_index is None:
226
- print("Required columns not found in the table headers.")
227
- return pd.DataFrame()
228
-
229
- data = []
230
- rows = target_table.find_all("tr")[1:]
231
- for row in rows:
232
- cells = row.find_all("td")
233
- if len(cells) <= max(time_index, snow_index):
234
- continue
235
- time_text = cells[time_index].get_text(strip=True)
236
- snow_text = cells[snow_index].get_text(strip=True)
237
- data.append((time_text, snow_text))
238
-
239
- df = pd.DataFrame(data, columns=["Time", "Snow Depth"])
240
- df["Time"] = pd.to_datetime(df["Time"], errors="coerce")
241
- df["Snow Depth"] = pd.to_numeric(df["Snow Depth"], errors="coerce")
242
- print("Scraped snow depth data:")
243
- print(df.head())
244
- return df.rename(columns={"Time": "timestamp", "Snow Depth": "snowDepth"})
245
- except Exception as e:
246
- print(f"Error scraping snow depth: {e}")
247
- return pd.DataFrame()
248
 
249
- def create_wind_rose(ax, data, title):
250
- """
251
- Create a wind rose subplot.
252
- """
253
- if data.empty or data['wind_direction'].isna().all() or data['wind_speed'].isna().all():
254
- ax.text(0.5, 0.5, 'No wind data available',
255
- horizontalalignment='center',
256
- verticalalignment='center',
257
- transform=ax.transAxes)
258
- ax.set_title(title)
259
- return
 
 
 
 
 
 
260
 
261
- plot_data = data.copy()
262
-
263
- direction_bins = np.arange(0, 361, 45)
264
- directions = ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW']
265
-
266
- mask = plot_data['wind_direction'].notna() & plot_data['wind_speed'].notna()
267
- plot_data = plot_data[mask]
268
-
269
- if plot_data.empty:
270
- ax.text(0.5, 0.5, 'No valid wind data',
271
- horizontalalignment='center',
272
- verticalalignment='center',
273
- transform=ax.transAxes)
274
- ax.set_title(title)
275
- return
276
-
277
- plot_data.loc[:, 'direction_bin'] = pd.cut(plot_data['wind_direction'],
278
- bins=direction_bins,
279
- labels=directions,
280
- include_lowest=True)
281
-
282
- wind_stats = plot_data.groupby('direction_bin', observed=True)['wind_speed'].mean()
283
-
284
- all_directions = pd.Series(0.0, index=directions)
285
- wind_stats = wind_stats.combine_first(all_directions)
286
-
287
- angles = np.linspace(0, 2*np.pi, len(directions), endpoint=False)
288
- values = [wind_stats[d] for d in directions]
289
-
290
- if any(v > 0 for v in values):
291
- ax.bar(angles, values, width=0.5, alpha=0.6)
292
- ax.set_xticks(angles)
293
- ax.set_xticklabels(directions)
294
- else:
295
- ax.text(0.5, 0.5, 'No significant wind',
296
- horizontalalignment='center',
297
- verticalalignment='center',
298
- transform=ax.transAxes)
299
-
300
- ax.set_title(title)
301
 
302
- def create_visualizations(df):
303
- """
304
- Create static visualizations using matplotlib.
305
- """
306
- fig = plt.figure(figsize=(20, 30)) # Increased height to accommodate new plots
307
- gs = GridSpec(7, 2, figure=fig) # Increased number of rows
 
 
 
308
 
309
  # Temperature plot
310
- ax1 = fig.add_subplot(gs[0, :])
311
- if not df['temperature'].isna().all():
312
- ax1.plot(df['timestamp'], df['temperature'], linewidth=2, label='Temperature')
313
- if 'wind_chill' in df.columns and not df['wind_chill'].isna().all():
314
- ax1.plot(df['timestamp'], df['wind_chill'], linewidth=2, label='Wind Chill', linestyle='--')
315
- if 'dewpoint' in df.columns and not df['dewpoint'].isna().all():
316
- ax1.plot(df['timestamp'], df['dewpoint'], linewidth=2, label='Dewpoint', linestyle=':')
317
- ax1.legend()
318
- ax1.set_title('Temperature Measurements Over Time')
319
  ax1.set_ylabel('Temperature (°F)')
320
- ax1.set_xlabel('')
321
  ax1.grid(True)
 
322
 
323
  # Wind speed plot
324
- ax2 = fig.add_subplot(gs[1, :])
325
- if not df['wind_speed'].isna().all():
326
- ax2.plot(df['timestamp'], df['wind_speed'], linewidth=2)
327
- ax2.set_title('Wind Speed Over Time')
 
328
  ax2.set_ylabel('Wind Speed (mph)')
329
- ax2.set_xlabel('')
330
  ax2.grid(True)
 
331
 
332
- # Precipitation plot (new)
333
- ax3 = fig.add_subplot(gs[2, :])
334
- if 'precipitation_3h' in df.columns and not df['precipitation_3h'].isna().all():
335
- ax3.bar(df['timestamp'], df['precipitation_3h'], width=0.02)
336
- ax3.set_title('Precipitation (Last 3 Hours)')
337
- ax3.set_ylabel('Precipitation (inches)')
338
- else:
339
- ax3.text(0.5, 0.5, 'No precipitation data available',
340
- horizontalalignment='center',
341
- verticalalignment='center',
342
- transform=ax3.transAxes)
343
  ax3.grid(True)
 
344
 
345
- # Relative Humidity plot (new)
346
- ax4 = fig.add_subplot(gs[3, :])
347
- if 'relative_humidity' in df.columns and not df['relative_humidity'].isna().all():
348
- ax4.plot(df['timestamp'], df['relative_humidity'], linewidth=2)
349
- ax4.set_title('Relative Humidity Over Time')
350
- ax4.set_ylabel('Relative Humidity (%)')
351
- ax4.set_ylim(0, 100)
352
- else:
353
- ax4.text(0.5, 0.5, 'No humidity data available',
354
- horizontalalignment='center',
355
- verticalalignment='center',
356
- transform=ax4.transAxes)
357
- ax4.grid(True)
358
-
359
- # Snow depth plot
360
- ax5 = fig.add_subplot(gs[4, :])
361
- if 'snowDepth' in df.columns and not df['snowDepth'].isna().all():
362
- ax5.plot(df['timestamp'], df['snowDepth'], linewidth=2)
363
- ax5.set_ylim(0, 80) # Fixed y-axis limit to 80 inches
364
- else:
365
- ax5.text(0.5, 0.5, 'No snow depth data available',
366
- horizontalalignment='center',
367
- verticalalignment='center',
368
- transform=ax5.transAxes)
369
- ax5.set_title('Snow Depth')
370
- ax5.set_ylabel('Snow Depth (inches)')
371
- ax5.set_xlabel('')
372
- ax5.grid(True)
373
-
374
- # Format x-axis labels for all plots
375
- for ax in [ax1, ax2, ax3, ax4, ax5]:
376
- ax.tick_params(axis='x', rotation=45)
377
- ax.xaxis.set_major_formatter(plt.matplotlib.dates.DateFormatter('%Y-%m-%d %H:%M'))
378
-
379
- # Wind rose plots
380
- dates = sorted(df['date'].unique())
381
- for i, date in enumerate(dates):
382
- if i < 2: # Only show last two days
383
- ax = fig.add_subplot(gs[6, i], projection='polar')
384
- day_data = df[df['date'] == date].copy()
385
- create_wind_rose(ax, day_data, pd.to_datetime(date).strftime('%Y-%m-%d'))
386
-
387
- plt.tight_layout()
388
- return fig
389
 
390
- def get_weather_data(station_id, hours):
391
- """
392
- Main function to get and process weather data.
393
- Combines API data and scraped snow depth data.
394
- """
395
  try:
396
- # Get raw data from API
397
- raw_data = get_raw_data(station_id)
398
- if raw_data is None:
399
- return None, "Failed to fetch data from API"
400
-
401
- # Parse raw API data
402
- df = parse_raw_data(raw_data)
403
- if df is None:
404
- return None, "Failed to parse API data"
405
-
406
- # Process API data
407
- df = process_weather_data(df)
408
- if df is None:
409
- return None, "Failed to process API data"
410
 
411
- # Scrape snow depth data and merge with API data
412
- snow_df = scrape_snow_depth()
413
- if not snow_df.empty:
414
- df = df.sort_values('timestamp')
415
- snow_df = snow_df.sort_values('timestamp')
416
- # Merge using nearest timestamp within a 30-minute tolerance
417
- df = pd.merge_asof(df, snow_df, on='timestamp', tolerance=pd.Timedelta('30min'), direction='nearest')
418
 
419
- print("\nProcessed combined data sample:")
420
- print(df.head())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
421
 
422
- return df, None
423
-
424
  except Exception as e:
425
- return None, f"Error: {str(e)}"
426
-
427
- def fetch_and_display(station_id, hours):
428
- """
429
- Fetch data and create visualization.
430
- """
431
- df, error = get_weather_data(station_id, hours)
432
-
433
- if error:
434
- return None, error
435
-
436
- if df is not None and not df.empty:
437
- fig = create_visualizations(df)
438
- return fig, "Data fetched successfully!"
439
-
440
- return None, "No data available for the specified parameters."
441
 
442
  # Create Gradio interface
443
- with gr.Blocks() as demo:
444
- gr.Markdown("# Weather Data Viewer")
445
- gr.Markdown("Displays comprehensive weather data from NWS stations including temperature, wind, precipitation, and snow depth.")
 
 
 
 
 
 
446
 
447
  with gr.Row():
448
- station_id = gr.Textbox(label="Station ID", value="YCTIM")
449
- hours = gr.Slider(minimum=24, maximum=168, value=72, label="Hours of Data", step=24)
 
 
 
 
 
 
 
 
 
 
 
450
 
451
- fetch_btn = gr.Button("Fetch Data")
 
452
 
453
- plot_output = gr.Plot()
454
- message = gr.Textbox(label="Status")
 
455
 
456
- fetch_btn.click(
457
- fn=fetch_and_display,
458
- inputs=[station_id, hours],
459
- outputs=[plot_output, message]
460
  )
461
 
462
- # Launch the app
463
  if __name__ == "__main__":
464
  demo.launch()
 
1
  import gradio as gr
 
2
  import pandas as pd
3
  import numpy as np
4
+ import re
5
+ from playwright.sync_api import sync_playwright
6
+ import time
7
+ import os
8
+ import subprocess
9
+ import sys
10
  import matplotlib.pyplot as plt
11
  from matplotlib.gridspec import GridSpec
12
+ from windrose import WindroseAxes
13
+ from datetime import datetime
14
 
15
+ # Install Playwright browsers on startup
16
+ def install_playwright_browsers():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  try:
18
+ if not os.path.exists('/home/user/.cache/ms-playwright'):
19
+ print("Installing Playwright browsers...")
20
+ subprocess.run(
21
+ [sys.executable, "-m", "playwright", "install", "chromium"],
22
+ check=True,
23
+ capture_output=True,
24
+ text=True
25
+ )
26
+ print("Playwright browsers installed successfully")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  except Exception as e:
28
+ print(f"Error installing browsers: {e}")
 
 
 
29
 
30
+ # Install browsers when the module loads
31
+ install_playwright_browsers()
 
 
 
 
32
 
33
+ def scrape_weather_data(site_id, hours=720):
34
+ """Scrape weather data from weather.gov timeseries"""
35
+ url = f"https://www.weather.gov/wrh/timeseries?site={site_id}&hours={hours}&units=english&chart=on&headers=on&obs=tabular&hourly=false&pview=full&font=12&plot="
36
 
37
+ try:
38
+ with sync_playwright() as p:
39
+ browser = p.chromium.launch(
40
+ headless=True,
41
+ args=['--no-sandbox', '--disable-dev-shm-usage']
42
+ )
 
 
 
 
 
 
 
43
 
44
+ context = browser.new_context(
45
+ user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
46
+ )
47
+
48
+ page = context.new_page()
49
+ response = page.goto(url)
50
+ print(f"Response status: {response.status}")
51
+
52
+ page.wait_for_selector('table', timeout=30000)
53
+ time.sleep(5)
54
 
55
+ print("Extracting data...")
56
+ content = page.evaluate('''() => {
57
+ const getTextContent = () => {
58
+ const rows = [];
59
+ const tables = document.getElementsByTagName('table');
60
+ for (const table of tables) {
61
+ if (table.textContent.includes('Date/Time')) {
62
+ const headerRow = Array.from(table.querySelectorAll('th'))
63
+ .map(th => th.textContent.trim());
64
+
65
+ const dataRows = Array.from(table.querySelectorAll('tbody tr'))
66
+ .map(row => Array.from(row.querySelectorAll('td'))
67
+ .map(td => td.textContent.trim()));
68
+
69
+ return {headers: headerRow, rows: dataRows};
70
+ }
71
+ }
72
+ return null;
73
+ };
74
+
75
+ return getTextContent();
76
+ }''')
77
+
78
+ print(f"Found {len(content['rows'] if content else [])} rows of data")
79
+ browser.close()
80
+ return content
81
 
82
+ except Exception as e:
83
+ print(f"Error scraping data: {str(e)}")
84
+ raise e
85
+
86
+ def parse_date(date_str):
87
+ """Parse date string to datetime"""
88
+ try:
89
+ current_year = datetime.now().year
90
+ return pd.to_datetime(f"{date_str}, {current_year}", format="%b %d, %I:%M %p, %Y")
91
+ except:
92
+ return pd.NaT
93
+
94
+ def parse_weather_data(data):
95
+ """Parse the weather data into a pandas DataFrame"""
96
+ if not data or 'rows' not in data:
97
+ raise ValueError("No valid weather data found")
98
 
99
+ df = pd.DataFrame(data['rows'])
100
+
101
+ columns = ['datetime', 'temp', 'dew_point', 'humidity', 'wind_chill',
102
+ 'wind_dir', 'wind_speed', 'snow_depth', 'snowfall_3hr',
103
+ 'snowfall_6hr', 'snowfall_24hr', 'swe']
104
+
105
+ df = df.iloc[:, :12]
106
+ df.columns = columns
107
+
108
+ numeric_cols = ['temp', 'dew_point', 'humidity', 'wind_chill', 'snow_depth',
109
+ 'snowfall_3hr', 'snowfall_6hr', 'snowfall_24hr', 'swe']
110
+ for col in numeric_cols:
111
+ df[col] = pd.to_numeric(df[col], errors='coerce')
112
+
113
+ def parse_wind(x):
114
+ if pd.isna(x): return np.nan, np.nan
115
+ match = re.search(r'(\d+)G(\d+)', str(x))
116
+ if match:
117
+ return float(match.group(1)), float(match.group(2))
118
+ try:
119
+ return float(x), np.nan
120
+ except:
121
+ return np.nan, np.nan
122
+
123
+ wind_data = df['wind_speed'].apply(parse_wind)
124
+ df['wind_speed'] = wind_data.apply(lambda x: x[0])
125
+ df['wind_gust'] = wind_data.apply(lambda x: x[1])
126
+
127
+ def parse_direction(direction):
128
+ direction_map = {
129
+ 'N': 0, 'NNE': 22.5, 'NE': 45, 'ENE': 67.5,
130
+ 'E': 90, 'ESE': 112.5, 'SE': 135, 'SSE': 157.5,
131
+ 'S': 180, 'SSW': 202.5, 'SW': 225, 'WSW': 247.5,
132
+ 'W': 270, 'WNW': 292.5, 'NW': 315, 'NNW': 337.5
133
  }
134
+ return direction_map.get(direction, np.nan)
 
 
 
135
 
136
+ df['wind_dir_deg'] = df['wind_dir'].apply(parse_direction)
137
+
138
+ df['datetime'] = df['datetime'].apply(parse_date)
139
+ df['date'] = df['datetime'].dt.date
 
140
 
141
  return df
142
 
143
+ def process_daily_snow(group):
144
+ """Sum up ONLY the 3-hour snowfall amounts for each day period"""
145
+ # Sort by time to ensure proper sequence
146
+ group = group.sort_values('datetime')
147
+
148
+ # Initialize variables for tracking snow accumulation
149
+ daily_total = 0
150
+ last_valid_time = None
151
+ last_amount = 0
152
+
153
+ for _, row in group.iterrows():
154
+ current_amount = row['snowfall_3hr'] if pd.notna(row['snowfall_3hr']) else 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
 
156
+ # If this is a new reading (not overlapping with previous)
157
+ if current_amount > 0:
158
+ if last_valid_time is None or (row['datetime'] - last_valid_time).total_seconds() > 3600:
159
+ daily_total += current_amount
160
+ last_valid_time = row['datetime']
161
+ last_amount = current_amount
162
+ else:
163
+ # For overlapping periods, only count the difference if it's higher
164
+ if current_amount > last_amount:
165
+ daily_total += (current_amount - last_amount)
166
+ last_amount = current_amount
167
+
168
+ return daily_total
 
 
 
 
 
 
 
 
 
 
169
 
170
+ def calculate_total_new_snow(df):
171
+ """Calculate total new snow accumulation"""
172
+ # Sort by datetime to ensure correct calculation
173
+ df = df.sort_values('datetime')
174
+
175
+ # Create a copy of the dataframe with ONLY datetime and 3-hour snowfall
176
+ snow_df = df[['datetime', 'snowfall_3hr']].copy()
177
+
178
+ # Create a day group that starts at 9 AM instead of midnight
179
+ snow_df['day_group'] = snow_df['datetime'].apply(
180
+ lambda x: x.date() if x.hour >= 9 else (x - pd.Timedelta(days=1)).date()
181
+ )
182
+
183
+ # Calculate daily snow totals
184
+ daily_totals = snow_df.groupby('day_group').apply(process_daily_snow)
185
+
186
+ return daily_totals.sum()
187
 
188
+ def create_wind_rose(df, ax):
189
+ """Create a wind rose plot"""
190
+ if not isinstance(ax, WindroseAxes):
191
+ ax = WindroseAxes.from_ax(ax=ax)
192
+ ax.bar(df['wind_dir_deg'].dropna(), df['wind_speed'].dropna(),
193
+ bins=np.arange(0, 40, 5), normed=True, opening=0.8, edgecolor='white')
194
+ ax.set_legend(title='Wind Speed (mph)')
195
+ ax.set_title('Wind Rose')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
196
 
197
+ def create_plots(df):
198
+ """Create all weather plots including SWE estimates"""
199
+ # Create figure with adjusted height and spacing
200
+ fig = plt.figure(figsize=(20, 24))
201
+
202
+ # Calculate height ratios for different plots
203
+ height_ratios = [1, 1, 1, 1, 1] # Equal height for all plots
204
+ gs = GridSpec(5, 1, figure=fig, height_ratios=height_ratios)
205
+ gs.update(hspace=0.4) # Increase vertical spacing between plots
206
 
207
  # Temperature plot
208
+ ax1 = fig.add_subplot(gs[0])
209
+ ax1.plot(df['datetime'], df['temp'], label='Temperature', color='red')
210
+ ax1.plot(df['datetime'], df['wind_chill'], label='Wind Chill', color='blue')
211
+ ax1.set_title('Temperature and Wind Chill Over Time', pad=20)
212
+ ax1.set_xlabel('Date')
 
 
 
 
213
  ax1.set_ylabel('Temperature (°F)')
214
+ ax1.legend()
215
  ax1.grid(True)
216
+ ax1.tick_params(axis='x', rotation=45)
217
 
218
  # Wind speed plot
219
+ ax2 = fig.add_subplot(gs[1])
220
+ ax2.plot(df['datetime'], df['wind_speed'], label='Wind Speed', color='blue')
221
+ ax2.plot(df['datetime'], df['wind_gust'], label='Wind Gust', color='orange')
222
+ ax2.set_title('Wind Speed and Gusts Over Time', pad=20)
223
+ ax2.set_xlabel('Date')
224
  ax2.set_ylabel('Wind Speed (mph)')
225
+ ax2.legend()
226
  ax2.grid(True)
227
+ ax2.tick_params(axis='x', rotation=45)
228
 
229
+ # Snow depth plot
230
+ ax3 = fig.add_subplot(gs[2])
231
+ ax3.plot(df['datetime'], df['snow_depth'], color='blue', label='Snow Depth')
232
+ ax3.set_title('Snow Depth Over Time', pad=20)
233
+ ax3.set_xlabel('Date')
234
+ ax3.set_ylabel('Snow Depth (inches)')
 
 
 
 
 
235
  ax3.grid(True)
236
+ ax3.tick_params(axis='x', rotation=45)
237
 
238
+ # Daily new snow bar plot
239
+ ax4 = fig.add_subplot(gs[3])
240
+ snow_df = df[['datetime', 'snowfall_3hr']].copy()
241
+ snow_df['day_group'] = snow_df['datetime'].apply(
242
+ lambda x: x.date() if x.hour >= 9 else (x - pd.Timedelta(days=1)).date()
243
+ )
244
+ daily_snow = snow_df.groupby('day_group').apply(process_daily_snow).reset_index()
245
+ daily_snow.columns = ['date', 'new_snow']
246
+
247
+ # Create the bar plot
248
+ ax4.bar(daily_snow['date'], daily_snow['new_snow'], color='blue')
249
+ ax4.set_title('Daily New Snow (Sum of 3-hour amounts, 9 AM Reset)', pad=20)
250
+ ax4.set_xlabel('Date')
251
+ ax4.set_ylabel('New Snow (inches)')
252
+ ax4.tick_params(axis='x', rotation=45)
253
+ ax4.grid(True, axis='y', linestyle='--', alpha=0.7)
254
+
255
+ # Add value labels on top of each bar
256
+ for i, v in enumerate(daily_snow['new_snow']):
257
+ if v > 0: # Only label bars with snow
258
+ ax4.text(i, v, f'{v:.1f}"', ha='center', va='bottom')
259
+
260
+ # SWE bar plot
261
+ ax5 = fig.add_subplot(gs[4])
262
+ daily_swe = df.groupby('date')['swe'].mean()
263
+ ax5.bar(daily_swe.index, daily_swe.values, color='lightblue')
264
+ ax5.set_title('Snow/Water Equivalent', pad=20)
265
+ ax5.set_xlabel('Date')
266
+ ax5.set_ylabel('SWE (inches)')
267
+ ax5.tick_params(axis='x', rotation=45)
268
+
269
+ # Adjust layout
270
+ plt.subplots_adjust(top=0.95, bottom=0.05, left=0.1, right=0.95)
271
+
272
+ # Create separate wind rose figure
273
+ fig_rose = plt.figure(figsize=(10, 10))
274
+ ax_rose = WindroseAxes.from_ax(fig=fig_rose)
275
+ create_wind_rose(df, ax_rose)
276
+ fig_rose.subplots_adjust(top=0.95, bottom=0.05, left=0.1, right=0.95)
277
+
278
+ return fig, fig_rose
 
 
 
279
 
280
+ def analyze_weather_data(site_id, hours):
281
+ """Analyze weather data and create visualizations"""
 
 
 
282
  try:
283
+ print(f"Scraping data for {site_id}...")
284
+ raw_data = scrape_weather_data(site_id, hours)
285
+ if not raw_data:
286
+ return "Error: Could not retrieve weather data.", None, None
 
 
 
 
 
 
 
 
 
 
287
 
288
+ print("Parsing data...")
289
+ df = parse_weather_data(raw_data)
 
 
 
 
 
290
 
291
+ # Calculate total new snow using the new method
292
+ total_new_snow = calculate_total_new_snow(df)
293
+ current_swe = df['swe'].iloc[0] # Get most recent SWE measurement
294
+
295
+ print("Calculating statistics...")
296
+ stats = {
297
+ 'Temperature Range': f"{df['temp'].min():.1f}°F to {df['temp'].max():.1f}°F",
298
+ 'Average Temperature': f"{df['temp'].mean():.1f}°F",
299
+ 'Max Wind Speed': f"{df['wind_speed'].max():.1f} mph",
300
+ 'Max Wind Gust': f"{df['wind_gust'].max():.1f} mph",
301
+ 'Average Humidity': f"{df['humidity'].mean():.1f}%",
302
+ 'Current Snow Depth': f"{df['snow_depth'].iloc[0]:.1f} inches",
303
+ 'Total New Snow': f"{total_new_snow:.1f} inches",
304
+ 'Current Snow/Water Equivalent': f"{current_swe:.2f} inches"
305
+ }
306
+
307
+ html_output = "<div style='font-size: 16px; line-height: 1.5;'>"
308
+ html_output += f"<p><strong>Weather Station:</strong> {site_id}</p>"
309
+ html_output += f"<p><strong>Data Range:</strong> {df['datetime'].min().strftime('%Y-%m-%d %H:%M')} to {df['datetime'].max().strftime('%Y-%m-%d %H:%M')}</p>"
310
+ for key, value in stats.items():
311
+ html_output += f"<p><strong>{key}:</strong> {value}</p>"
312
+ html_output += "</div>"
313
+
314
+ print("Creating plots...")
315
+ main_plots, wind_rose = create_plots(df)
316
+
317
+ return html_output, main_plots, wind_rose
318
 
 
 
319
  except Exception as e:
320
+ print(f"Error in analysis: {str(e)}")
321
+ return f"Error analyzing data: {str(e)}", None, None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
322
 
323
  # Create Gradio interface
324
+ with gr.Blocks(title="Weather Station Data Analyzer") as demo:
325
+ gr.Markdown("# Weather Station Data Analyzer")
326
+ gr.Markdown("""
327
+ Enter a weather station ID and number of hours to analyze.
328
+ Example station IDs:
329
+ - YCTIM (Yellowstone Club - Timber)
330
+ - KBZN (Bozeman Airport)
331
+ - KSLC (Salt Lake City)
332
+ """)
333
 
334
  with gr.Row():
335
+ site_id = gr.Textbox(
336
+ label="Weather Station ID",
337
+ value="YCTIM",
338
+ placeholder="Enter station ID (e.g., YCTIM)"
339
+ )
340
+ hours = gr.Number(
341
+ label="Hours of Data",
342
+ value=720,
343
+ minimum=1,
344
+ maximum=1440
345
+ )
346
+
347
+ analyze_btn = gr.Button("Fetch and Analyze Weather Data")
348
 
349
+ with gr.Row():
350
+ stats_output = gr.HTML(label="Statistics")
351
 
352
+ with gr.Row():
353
+ weather_plots = gr.Plot(label="Weather Plots")
354
+ wind_rose = gr.Plot(label="Wind Rose")
355
 
356
+ analyze_btn.click(
357
+ fn=analyze_weather_data,
358
+ inputs=[site_id, hours],
359
+ outputs=[stats_output, weather_plots, wind_rose]
360
  )
361
 
 
362
  if __name__ == "__main__":
363
  demo.launch()