nakas commited on
Commit
8aa4e70
·
verified ·
1 Parent(s): feab9ab

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +258 -371
app.py CHANGED
@@ -1,24 +1,19 @@
1
- #!/usr/bin/env python3
2
-
3
  import gradio as gr
4
  import pandas as pd
5
  import numpy as np
6
- import matplotlib.pyplot as plt
7
- from matplotlib.gridspec import GridSpec
8
- from windrose import WindroseAxes
9
- from datetime import datetime, timedelta
10
  from playwright.sync_api import sync_playwright
11
  import time
12
  import os
13
  import subprocess
14
  import sys
15
- from PIL import Image
16
- import io
17
- from zoneinfo import ZoneInfo
18
- import re
19
 
 
20
  def install_playwright_browsers():
21
- """Install required Playwright browsers"""
22
  try:
23
  if not os.path.exists('/home/user/.cache/ms-playwright'):
24
  print("Installing Playwright browsers...")
@@ -32,429 +27,322 @@ def install_playwright_browsers():
32
  except Exception as e:
33
  print(f"Error installing browsers: {e}")
34
 
35
- def calculate_daily_snow(df):
36
- """Calculate daily new snow based on maximum value before reset time"""
37
- df = df.copy()
38
- # Create a reporting period identifier (4PM to 4PM)
39
- df['report_date'] = df['datetime'].apply(lambda x:
40
- (x - timedelta(hours=16)).date() if x.hour >= 16
41
- else (x - timedelta(days=1, hours=16)).date()
42
- )
43
- # Group by reporting period and get the maximum new snow value
44
- daily_snow = df.groupby('report_date')['new_snow'].max()
45
- return daily_snow
46
-
47
- def navigate_to_previous_day(page):
48
- """Navigate to the previous day using specific selector IDs"""
49
- try:
50
- current_values = page.evaluate('''() => {
51
- const monthSelect = document.getElementById('50');
52
- const daySelect = document.getElementById('51');
53
- const yearSelect = document.getElementById('52');
54
-
55
- return {
56
- month: parseInt(monthSelect.value),
57
- day: parseInt(daySelect.value),
58
- year: parseInt(yearSelect.value)
59
- };
60
- }''')
61
-
62
- current_date = datetime(
63
- current_values['year'],
64
- current_values['month'],
65
- current_values['day']
66
- )
67
- previous_date = current_date - timedelta(days=1)
68
-
69
- print(f"Navigating from {current_date.date()} to {previous_date.date()}")
70
-
71
- success = page.evaluate('''(prevDate) => {
72
- try {
73
- const monthSelect = document.getElementById('50');
74
- const daySelect = document.getElementById('51');
75
- const yearSelect = document.getElementById('52');
76
-
77
- yearSelect.value = prevDate.year.toString();
78
- yearSelect.dispatchEvent(new Event('change', { bubbles: true }));
79
-
80
- monthSelect.value = prevDate.month.toString();
81
- monthSelect.dispatchEvent(new Event('change', { bubbles: true }));
82
-
83
- daySelect.value = prevDate.day.toString();
84
- daySelect.dispatchEvent(new Event('change', { bubbles: true }));
85
-
86
- return true;
87
- } catch (e) {
88
- console.error('Error setting date:', e);
89
- return false;
90
- }
91
- }''', {
92
- 'month': previous_date.month,
93
- 'day': previous_date.day,
94
- 'year': previous_date.year
95
- })
96
-
97
- if success:
98
- print(f"Successfully navigated to {previous_date.date()}")
99
-
100
- time.sleep(3)
101
- return success
102
- except Exception as e:
103
- print(f"Error navigating to previous day: {str(e)}")
104
- return False
105
 
106
- def extract_day_data(page):
107
- """Extract all data from the current day's table"""
 
 
108
  try:
109
- page.evaluate('''() => {
110
- const buttons = Array.from(document.querySelectorAll('button'));
111
- const showAllBtn = buttons.find(b => b.textContent.trim().toLowerCase() === 'show all');
112
- if (showAllBtn) {
113
- showAllBtn.click();
114
- return true;
115
- }
116
- return false;
117
- }''')
118
- time.sleep(2)
119
-
120
- current_date = page.evaluate('''() => {
121
- return {
122
- month: document.getElementById('50').value,
123
- day: document.getElementById('51').value,
124
- year: document.getElementById('52').value
125
- };
126
- }''')
127
-
128
- table_data = page.evaluate('''() => {
129
- const table = document.querySelector('table');
130
- if (!table) return null;
131
 
132
- const headers = Array.from(table.querySelectorAll('th'))
133
- .map(th => th.textContent.trim());
 
134
 
135
- const rows = Array.from(table.querySelectorAll('tbody tr'))
136
- .map(row => Array.from(row.querySelectorAll('td'))
137
- .map(td => td.textContent.trim()));
138
 
139
- return { headers, rows };
140
- }''')
141
-
142
- return current_date, table_data
143
-
144
- except Exception as e:
145
- print(f"Error extracting day data: {str(e)}")
146
- return None, None
147
-
148
- def convert_to_dataframe(all_data):
149
- """Convert collected data to pandas DataFrame format"""
150
- rows = []
151
- for data in all_data:
152
- try:
153
- date_str = data['date']
154
- row_data = data['data']
155
 
156
- if len(row_data) != 9:
157
- continue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
 
159
- # Parse date and time
160
- parsed_date = datetime.strptime(f"{date_str}", "%m/%d/%Y")
161
- time_str = row_data[0] if row_data[0] else "12:00AM"
162
- full_datetime = datetime.strptime(f"{date_str} {time_str}", "%m/%d/%Y %I:%M%p")
163
 
164
- def clean_numeric(value):
165
- try:
166
- if isinstance(value, str):
167
- cleaned = re.sub(r'[^\d.-]', '', value)
168
- return float(cleaned) if cleaned else 0.0
169
- return float(value) if value else 0.0
170
- except:
171
- return 0.0
172
 
173
- row = {
174
- 'datetime': full_datetime,
175
- 'temp': clean_numeric(row_data[1]),
176
- 'new_snow': clean_numeric(row_data[2]),
177
- 'snow_depth': clean_numeric(row_data[3]),
178
- 'h2o': clean_numeric(row_data[4]),
179
- 'humidity': clean_numeric(row_data[5]),
180
- 'wind_speed': clean_numeric(row_data[6]),
181
- 'wind_gust': clean_numeric(row_data[7]),
182
- 'wind_dir': row_data[8],
183
- 'location': data.get('location', 'alpine')
184
- }
185
- rows.append(row)
186
-
187
- except Exception as e:
188
- print(f"Error processing row: {str(e)}")
189
- continue
190
 
191
- if not rows:
192
- raise ValueError("No valid data rows to create DataFrame")
 
193
 
194
- df = pd.DataFrame(rows)
 
195
 
196
- direction_map = {
197
- 'N': 0, 'NNE': 22.5, 'NE': 45, 'ENE': 67.5,
198
- 'E': 90, 'ESE': 112.5, 'SE': 135, 'SSE': 157.5,
199
- 'S': 180, 'SSW': 202.5, 'SW': 225, 'WSW': 247.5,
200
- 'W': 270, 'WNW': 292.5, 'NW': 315, 'NNW': 337.5
201
- }
202
- df['wind_dir_deg'] = df['wind_dir'].map(direction_map)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
  df['date'] = df['datetime'].dt.date
204
- return df.sort_values('datetime')
 
205
 
206
- def scrape_location_data(page, location_id, num_days):
207
- """Scrape data for a specific location"""
208
- print(f"\nSwitching to location: {location_id}")
209
- page.evaluate(f'''() => {{
210
- const locationSelect = document.getElementById('48');
211
- locationSelect.value = "{location_id}";
212
- locationSelect.dispatchEvent(new Event('change', {{ bubbles: true }}));
213
- }}''')
214
- time.sleep(3) # Wait for location change to take effect
215
 
216
- all_data = []
217
- for day in range(num_days):
218
- print(f"\nProcessing {location_id} - day {day + 1} of {num_days}")
219
-
220
- # Get current date
221
- current_date = page.evaluate('''() => {
222
- return {
223
- month: document.getElementById('50').value,
224
- day: document.getElementById('51').value,
225
- year: document.getElementById('52').value
226
- };
227
- }''')
228
 
229
- date_str = f"{current_date['month']}/{current_date['day']}/{current_date['year']}"
230
- print(f"Processing date: {date_str}")
 
 
 
231
 
232
- # Extract data
233
- _, table_data = extract_day_data(page)
 
234
 
235
- if table_data and table_data['rows']:
236
- rows_found = len(table_data['rows'])
237
- print(f"Found {rows_found} rows of data")
238
-
239
- for row in table_data['rows']:
240
- row_data = {
241
- 'date': date_str,
242
- 'headers': table_data['headers'],
243
- 'data': row,
244
- 'location': location_id
245
- }
246
- all_data.append(row_data)
247
-
248
- # Navigate to previous day if not the last iteration
249
- if day < num_days - 1:
250
- success = navigate_to_previous_day(page)
251
- if not success:
252
- print("Failed to navigate to previous day!")
253
- break
254
- time.sleep(3)
255
- else:
256
- print(f"No data found for {date_str}")
257
 
258
- return all_data
 
 
 
 
 
 
 
 
 
259
 
260
- def create_comparison_plots(df_alpine, df_ridge=None):
261
- """Create weather plots with optional ridge data overlay"""
 
262
  fig = plt.figure(figsize=(20, 24))
263
- height_ratios = [1, 1, 1, 1, 1]
 
 
264
  gs = GridSpec(5, 1, figure=fig, height_ratios=height_ratios)
265
- gs.update(hspace=0.4)
266
 
267
  # Temperature plot
268
  ax1 = fig.add_subplot(gs[0])
269
- ax1.plot(df_alpine['datetime'], df_alpine['temp'], label='Alpine Temperature', color='red', linewidth=2)
270
- if df_ridge is not None:
271
- ax1.plot(df_ridge['datetime'], df_ridge['temp'], label='Ridge Temperature', color='darkred', linewidth=2, linestyle='--')
272
- ax1.set_title('Temperature Over Time', pad=20, fontsize=14)
273
- ax1.set_xlabel('Date', fontsize=12)
274
- ax1.set_ylabel('Temperature (°F)', fontsize=12)
275
- ax1.legend(fontsize=12)
276
- ax1.grid(True, alpha=0.3)
277
  ax1.tick_params(axis='x', rotation=45)
278
 
279
  # Wind speed plot
280
  ax2 = fig.add_subplot(gs[1])
281
- ax2.plot(df_alpine['datetime'], df_alpine['wind_speed'], label='Alpine Wind Speed', color='blue', linewidth=2)
282
- ax2.plot(df_alpine['datetime'], df_alpine['wind_gust'], label='Alpine Wind Gust', color='orange', linewidth=2)
283
- if df_ridge is not None:
284
- ax2.plot(df_ridge['datetime'], df_ridge['wind_speed'], label='Ridge Wind Speed', color='darkblue', linewidth=2, linestyle='--')
285
- ax2.plot(df_ridge['datetime'], df_ridge['wind_gust'], label='Ridge Wind Gust', color='darkorange', linewidth=2, linestyle='--')
286
- ax2.set_title('Wind Speed and Gusts Over Time', pad=20, fontsize=14)
287
- ax2.set_xlabel('Date', fontsize=12)
288
- ax2.set_ylabel('Wind Speed (mph)', fontsize=12)
289
- ax2.legend(fontsize=12)
290
- ax2.grid(True, alpha=0.3)
291
  ax2.tick_params(axis='x', rotation=45)
292
 
293
  # Snow depth plot
294
  ax3 = fig.add_subplot(gs[2])
295
- ax3.plot(df_alpine['datetime'], df_alpine['snow_depth'], color='blue', label='Alpine Snow Depth', linewidth=2)
296
- if df_ridge is not None:
297
- ax3.plot(df_ridge['datetime'], df_ridge['snow_depth'], color='darkblue', label='Ridge Snow Depth', linewidth=2, linestyle='--')
298
- ax3.set_title('Snow Depth Over Time', pad=20, fontsize=14)
299
- ax3.set_xlabel('Date', fontsize=12)
300
- ax3.set_ylabel('Snow Depth (inches)', fontsize=12)
301
- ax3.legend(fontsize=12)
302
- ax3.grid(True, alpha=0.3)
303
  ax3.tick_params(axis='x', rotation=45)
304
 
305
  # Daily new snow bar plot
306
  ax4 = fig.add_subplot(gs[3])
307
- daily_snow_alpine = calculate_daily_snow(df_alpine)
308
- bar_width = 0.35
 
 
 
 
309
 
310
- if df_ridge is not None:
311
- daily_snow_ridge = calculate_daily_snow(df_ridge)
312
- # Plot bars side by side
313
- ax4.bar(daily_snow_alpine.index - bar_width/2, daily_snow_alpine.values,
314
- bar_width, color='blue', alpha=0.7, label='Alpine')
315
- ax4.bar(daily_snow_ridge.index + bar_width/2, daily_snow_ridge.values,
316
- bar_width, color='darkblue', alpha=0.7, label='Ridge')
317
- else:
318
- ax4.bar(daily_snow_alpine.index, daily_snow_alpine.values, color='blue', alpha=0.7)
319
-
320
- ax4.set_title('Daily New Snow (4PM to 4PM)', pad=20, fontsize=14)
321
- ax4.set_xlabel('Date', fontsize=12)
322
- ax4.set_ylabel('New Snow (inches)', fontsize=12)
323
  ax4.tick_params(axis='x', rotation=45)
324
- ax4.grid(True, alpha=0.3)
325
- if df_ridge is not None:
326
- ax4.legend()
327
 
328
- # H2O (SWE) plot
329
- ax5 = fig.add_subplot(gs[4])
330
- daily_swe_alpine = df_alpine.groupby('date')['h2o'].mean()
331
- if df_ridge is not None:
332
- daily_swe_ridge = df_ridge.groupby('date')['h2o'].mean()
333
- ax5.bar(daily_swe_alpine.index - bar_width/2, daily_swe_alpine.values,
334
- bar_width, color='lightblue', alpha=0.7, label='Alpine')
335
- ax5.bar(daily_swe_ridge.index + bar_width/2, daily_swe_ridge.values,
336
- bar_width, color='steelblue', alpha=0.7, label='Ridge')
337
- else:
338
- ax5.bar(daily_swe_alpine.index, daily_swe_alpine.values, color='lightblue', alpha=0.7)
339
 
340
- ax5.set_title('Snow/Water Equivalent', pad=20, fontsize=14)
341
- ax5.set_xlabel('Date', fontsize=12)
342
- ax5.set_ylabel('SWE (inches)', fontsize=12)
 
 
 
 
343
  ax5.tick_params(axis='x', rotation=45)
344
- ax5.grid(True, alpha=0.3)
345
- if df_ridge is not None:
346
- ax5.legend()
347
 
 
348
  plt.subplots_adjust(top=0.95, bottom=0.05, left=0.1, right=0.95)
349
 
350
- # Create wind rose (alpine only)
351
  fig_rose = plt.figure(figsize=(10, 10))
352
  ax_rose = WindroseAxes.from_ax(fig=fig_rose)
353
- ax_rose.bar(df_alpine['wind_dir_deg'].dropna(), df_alpine['wind_speed'].dropna(),
354
- bins=np.arange(0, 40, 5), normed=True, opening=0.8, edgecolor='white')
355
- ax_rose.set_legend(title='Wind Speed (mph)', fontsize=10)
356
- ax_rose.set_title('Wind Rose (Alpine)', fontsize=14, pad=20)
357
  fig_rose.subplots_adjust(top=0.95, bottom=0.05, left=0.1, right=0.95)
358
 
359
  return fig, fig_rose
360
 
361
- def analyze_weather_data(days=3, include_ridge=False):
362
  """Analyze weather data and create visualizations"""
363
  try:
364
- print("Launching browser...")
365
- with sync_playwright() as p:
366
- browser = p.chromium.launch(
367
- headless=True,
368
- args=['--no-sandbox', '--disable-dev-shm-usage']
369
- )
370
- context = browser.new_context(
371
- 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',
372
- timezone_id='America/Denver',
373
- locale='en-US'
374
- )
375
-
376
- print("Opening page...")
377
- page = context.new_page()
378
- page.goto("https://bridgerbowl.com/weather/history-tables/alpine")
379
- page.wait_for_load_state('networkidle')
380
- time.sleep(5)
381
-
382
- # Scrape Alpine data
383
- print("\nScraping Alpine data...")
384
- alpine_data = scrape_location_data(page, "alpine", days)
385
- df_alpine = convert_to_dataframe(alpine_data)
386
-
387
- # Scrape Ridge data if requested
388
- df_ridge = None
389
- if include_ridge:
390
- print("\nScraping Ridge data...")
391
- ridge_data = scrape_location_data(page, "ridge", days)
392
- df_ridge = convert_to_dataframe(ridge_data)
393
-
394
- # Create plots and statistics
395
- print("\nCreating plots...")
396
- main_plots, wind_rose = create_comparison_plots(df_alpine, df_ridge)
397
-
398
- # Calculate statistics
399
- alpine_snow = calculate_daily_snow(df_alpine)
400
- stats = {
401
- 'Alpine Temperature Range': f"{df_alpine['temp'].min():.1f}°F to {df_alpine['temp'].max():.1f}°F",
402
- 'Alpine Average Temperature': f"{df_alpine['temp'].mean():.1f}°F",
403
- 'Alpine Max Wind Speed': f"{df_alpine['wind_speed'].max():.1f} mph",
404
- 'Alpine Max Wind Gust': f"{df_alpine['wind_gust'].max():.1f} mph",
405
- 'Alpine Current Snow Depth': f"{df_alpine['snow_depth'].iloc[0]:.1f} inches",
406
- 'Alpine Total New Snow': f"{alpine_snow.sum():.1f} inches",
407
- 'Alpine Current SWE': f"{df_alpine['h2o'].iloc[0]:.2f} inches"
408
- }
409
-
410
- if include_ridge and df_ridge is not None:
411
- ridge_snow = calculate_daily_snow(df_ridge)
412
- stats.update({
413
- 'Ridge Temperature Range': f"{df_ridge['temp'].min():.1f}°F to {df_ridge['temp'].max():.1f}°F",
414
- 'Ridge Average Temperature': f"{df_ridge['temp'].mean():.1f}°F",
415
- 'Ridge Max Wind Speed': f"{df_ridge['wind_speed'].max():.1f} mph",
416
- 'Ridge Max Wind Gust': f"{df_ridge['wind_gust'].max():.1f} mph",
417
- 'Ridge Current Snow Depth': f"{df_ridge['snow_depth'].iloc[0]:.1f} inches",
418
- 'Ridge Total New Snow': f"{ridge_snow.sum():.1f} inches",
419
- 'Ridge Current SWE': f"{df_ridge['h2o'].iloc[0]:.2f} inches"
420
- })
421
-
422
- # Create HTML report
423
- html_report = "<h3>Weather Statistics:</h3>"
424
- for key, value in stats.items():
425
- html_report += f"<p><strong>{key}:</strong> {value}</p>"
426
-
427
- browser.close()
428
- return html_report, main_plots, wind_rose
429
-
430
  except Exception as e:
431
- print(f"Error during analysis: {str(e)}")
432
- return f"Error during analysis: {str(e)}", None, None
433
 
434
  # Create Gradio interface
435
- with gr.Blocks(title="Bridger Bowl Weather Analyzer") as demo:
436
- gr.Markdown("# Bridger Bowl Weather Analyzer")
437
  gr.Markdown("""
438
- Analyze weather data from Bridger Bowl's weather stations.
439
- Specify how many days of historical data to analyze and whether to include Ridge data.
 
 
 
440
  """)
441
 
442
  with gr.Row():
443
- days_input = gr.Number(
444
- label="Number of Days to Analyze",
445
- value=3,
446
- minimum=1,
447
- maximum=31
448
  )
449
- include_ridge = gr.Checkbox(
450
- label="Include Ridge Data",
451
- value=False
 
 
452
  )
453
 
454
- analyze_btn = gr.Button("Collect and Analyze Weather Data")
455
 
456
  with gr.Row():
457
- stats_output = gr.HTML(label="Statistics and Data Collection Info")
458
 
459
  with gr.Row():
460
  weather_plots = gr.Plot(label="Weather Plots")
@@ -462,10 +350,9 @@ with gr.Blocks(title="Bridger Bowl Weather Analyzer") as demo:
462
 
463
  analyze_btn.click(
464
  fn=analyze_weather_data,
465
- inputs=[days_input, include_ridge],
466
  outputs=[stats_output, weather_plots, wind_rose]
467
  )
468
 
469
  if __name__ == "__main__":
470
- install_playwright_browsers()
471
  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...")
 
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 calculate_total_new_snow(df):
144
+ """
145
+ Calculate total new snow by:
146
+ 1. Using ONLY the 3-hour snowfall amounts
147
+ 2. Using 9 AM as the daily reset point
148
+ 3. Filtering out obvious anomalies (>9 inches in 3 hours)
149
+ """
150
+ # Sort by datetime to ensure correct calculation
151
+ df = df.sort_values('datetime')
152
 
153
+ # Create a copy of the dataframe with ONLY datetime and 3-hour snowfall
154
+ snow_df = df[['datetime', 'snowfall_3hr']].copy()
155
+
156
+ # Create a day group that starts at 9 AM instead of midnight
157
+ snow_df['day_group'] = snow_df['datetime'].apply(
158
+ lambda x: x.date() if x.hour >= 9 else (x - pd.Timedelta(days=1)).date()
159
+ )
160
+
161
+ def process_daily_snow(group):
162
+ """Sum up ONLY the 3-hour snowfall amounts for each day period"""
163
+ # Sort by time to ensure proper sequence
164
+ group = group.sort_values('datetime')
165
 
166
+ # Print debugging information
167
+ print(f"\nSnowfall amounts for {group['day_group'].iloc[0]}:")
168
+ for _, row in group.iterrows():
169
+ if pd.notna(row['snowfall_3hr']):
170
+ print(f"{row['datetime'].strftime('%Y-%m-%d %H:%M')}: {row['snowfall_3hr']} inches")
171
 
172
+ # Sum only the valid 3-hour amounts, treating NaN as 0
173
+ valid_amounts = group['snowfall_3hr'].fillna(0)
174
+ daily_total = valid_amounts.sum()
175
 
176
+ print(f"Daily total: {daily_total} inches")
177
+ return daily_total
178
+
179
+ # Calculate daily snow totals
180
+ daily_totals = snow_df.groupby('day_group').apply(process_daily_snow)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
181
 
182
+ return daily_totals.sum()
183
+
184
+ def create_wind_rose(df, ax):
185
+ """Create a wind rose plot"""
186
+ if not isinstance(ax, WindroseAxes):
187
+ ax = WindroseAxes.from_ax(ax=ax)
188
+ ax.bar(df['wind_dir_deg'].dropna(), df['wind_speed'].dropna(),
189
+ bins=np.arange(0, 40, 5), normed=True, opening=0.8, edgecolor='white')
190
+ ax.set_legend(title='Wind Speed (mph)')
191
+ ax.set_title('Wind Rose')
192
 
193
+ def create_plots(df):
194
+ """Create all weather plots including SWE estimates"""
195
+ # Create figure with adjusted height and spacing
196
  fig = plt.figure(figsize=(20, 24))
197
+
198
+ # Calculate height ratios for different plots
199
+ height_ratios = [1, 1, 1, 1, 1] # Equal height for all plots
200
  gs = GridSpec(5, 1, figure=fig, height_ratios=height_ratios)
201
+ gs.update(hspace=0.4) # Increase vertical spacing between plots
202
 
203
  # Temperature plot
204
  ax1 = fig.add_subplot(gs[0])
205
+ ax1.plot(df['datetime'], df['temp'], label='Temperature', color='red')
206
+ ax1.plot(df['datetime'], df['wind_chill'], label='Wind Chill', color='blue')
207
+ ax1.set_title('Temperature and Wind Chill Over Time', pad=20)
208
+ ax1.set_xlabel('Date')
209
+ ax1.set_ylabel('Temperature (°F)')
210
+ ax1.legend()
211
+ ax1.grid(True)
 
212
  ax1.tick_params(axis='x', rotation=45)
213
 
214
  # Wind speed plot
215
  ax2 = fig.add_subplot(gs[1])
216
+ ax2.plot(df['datetime'], df['wind_speed'], label='Wind Speed', color='blue')
217
+ ax2.plot(df['datetime'], df['wind_gust'], label='Wind Gust', color='orange')
218
+ ax2.set_title('Wind Speed and Gusts Over Time', pad=20)
219
+ ax2.set_xlabel('Date')
220
+ ax2.set_ylabel('Wind Speed (mph)')
221
+ ax2.legend()
222
+ ax2.grid(True)
 
 
 
223
  ax2.tick_params(axis='x', rotation=45)
224
 
225
  # Snow depth plot
226
  ax3 = fig.add_subplot(gs[2])
227
+ ax3.plot(df['datetime'], df['snow_depth'], color='blue', label='Snow Depth')
228
+ ax3.set_title('Snow Depth Over Time', pad=20)
229
+ ax3.set_xlabel('Date')
230
+ ax3.set_ylabel('Snow Depth (inches)')
231
+ ax3.grid(True)
 
 
 
232
  ax3.tick_params(axis='x', rotation=45)
233
 
234
  # Daily new snow bar plot
235
  ax4 = fig.add_subplot(gs[3])
236
+ snow_df = df[['datetime', 'snowfall_3hr']].copy()
237
+ snow_df['day_group'] = snow_df['datetime'].apply(
238
+ lambda x: x.date() if x.hour >= 9 else (x - pd.Timedelta(days=1)).date()
239
+ )
240
+ daily_snow = snow_df.groupby('day_group').apply(process_daily_snow).reset_index()
241
+ daily_snow.columns = ['date', 'new_snow']
242
 
243
+ ax4.bar(daily_snow['date'], daily_snow['new_snow'], color='blue')
244
+ ax4.set_title('Daily New Snow (Sum of 3-hour amounts, 9 AM Reset)', pad=20)
245
+ ax4.set_xlabel('Date')
246
+ ax4.set_ylabel('New Snow (inches)')
 
 
 
 
 
 
 
 
 
247
  ax4.tick_params(axis='x', rotation=45)
248
+ ax4.grid(True, axis='y', linestyle='--', alpha=0.7)
 
 
249
 
250
+ # Add value labels on top of each bar
251
+ for i, v in enumerate(daily_snow['new_snow']):
252
+ if v > 0: # Only label bars with snow
253
+ ax4.text(i, v, f'{v:.1f}"', ha='center', va='bottom')
 
 
 
 
 
 
 
254
 
255
+ # SWE bar plot
256
+ ax5 = fig.add_subplot(gs[4])
257
+ daily_swe = df.groupby('date')['swe'].mean()
258
+ ax5.bar(daily_swe.index, daily_swe.values, color='lightblue')
259
+ ax5.set_title('Snow/Water Equivalent', pad=20)
260
+ ax5.set_xlabel('Date')
261
+ ax5.set_ylabel('SWE (inches)')
262
  ax5.tick_params(axis='x', rotation=45)
 
 
 
263
 
264
+ # Adjust layout
265
  plt.subplots_adjust(top=0.95, bottom=0.05, left=0.1, right=0.95)
266
 
267
+ # Create separate wind rose figure
268
  fig_rose = plt.figure(figsize=(10, 10))
269
  ax_rose = WindroseAxes.from_ax(fig=fig_rose)
270
+ create_wind_rose(df, ax_rose)
 
 
 
271
  fig_rose.subplots_adjust(top=0.95, bottom=0.05, left=0.1, right=0.95)
272
 
273
  return fig, fig_rose
274
 
275
+ def analyze_weather_data(site_id, hours):
276
  """Analyze weather data and create visualizations"""
277
  try:
278
+ print(f"Scraping data for {site_id}...")
279
+ raw_data = scrape_weather_data(site_id, hours)
280
+ if not raw_data:
281
+ return "Error: Could not retrieve weather data.", None, None
282
+
283
+ print("Parsing data...")
284
+ df = parse_weather_data(raw_data)
285
+
286
+ # Calculate total new snow using the new method
287
+ total_new_snow = calculate_total_new_snow(df)
288
+ current_swe = df['swe'].iloc[0] # Get most recent SWE measurement
289
+
290
+ print("Calculating statistics...")
291
+ stats = {
292
+ 'Temperature Range': f"{df['temp'].min():.1f}°F to {df['temp'].max():.1f}°F",
293
+ 'Average Temperature': f"{df['temp'].mean():.1f}°F",
294
+ 'Max Wind Speed': f"{df['wind_speed'].max():.1f} mph",
295
+ 'Max Wind Gust': f"{df['wind_gust'].max():.1f} mph",
296
+ 'Average Humidity': f"{df['humidity'].mean():.1f}%",
297
+ 'Current Snow Depth': f"{df['snow_depth'].iloc[0]:.1f} inches",
298
+ 'Total New Snow': f"{total_new_snow:.1f} inches",
299
+ 'Current Snow/Water Equivalent': f"{current_swe:.2f} inches"
300
+ }
301
+
302
+ html_output = "<div style='font-size: 16px; line-height: 1.5;'>"
303
+ html_output += f"<p><strong>Weather Station:</strong> {site_id}</p>"
304
+ 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>"
305
+ for key, value in stats.items():
306
+ html_output += f"<p><strong>{key}:</strong> {value}</p>"
307
+ html_output += "</div>"
308
+
309
+ print("Creating plots...")
310
+ main_plots, wind_rose = create_plots(df)
311
+
312
+ return html_output, main_plots, wind_rose
313
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
314
  except Exception as e:
315
+ print(f"Error in analysis: {str(e)}")
316
+ return f"Error analyzing data: {str(e)}", None, None
317
 
318
  # Create Gradio interface
319
+ with gr.Blocks(title="Weather Station Data Analyzer") as demo:
320
+ gr.Markdown("# Weather Station Data Analyzer")
321
  gr.Markdown("""
322
+ Enter a weather station ID and number of hours to analyze.
323
+ Example station IDs:
324
+ - YCTIM (Yellowstone Club - Timber)
325
+ - KBZN (Bozeman Airport)
326
+ - KSLC (Salt Lake City)
327
  """)
328
 
329
  with gr.Row():
330
+ site_id = gr.Textbox(
331
+ label="Weather Station ID",
332
+ value="YCTIM",
333
+ placeholder="Enter station ID (e.g., YCTIM)"
 
334
  )
335
+ hours = gr.Number(
336
+ label="Hours of Data",
337
+ value=720,
338
+ minimum=1,
339
+ maximum=1440
340
  )
341
 
342
+ analyze_btn = gr.Button("Fetch and Analyze Weather Data")
343
 
344
  with gr.Row():
345
+ stats_output = gr.HTML(label="Statistics")
346
 
347
  with gr.Row():
348
  weather_plots = gr.Plot(label="Weather Plots")
 
350
 
351
  analyze_btn.click(
352
  fn=analyze_weather_data,
353
+ inputs=[site_id, hours],
354
  outputs=[stats_output, weather_plots, wind_rose]
355
  )
356
 
357
  if __name__ == "__main__":
 
358
  demo.launch()