Spaces:
Sleeping
Sleeping
Add snow depth change calculation method
Browse filesNew Features:
- Calculate new snow from snow depth changes (more accurate than 3-hour reports)
- Display both methods side-by-side in grouped bar chart
- Show both totals in statistics summary
- Added explanation of both calculation methods in UI
The depth change method tracks the maximum increase in snow depth per day
(9 AM reset), which is generally more accurate than summing 3-hour reports
which can be unreliable or missing.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
app.py
CHANGED
|
@@ -164,14 +164,13 @@ def calculate_total_new_snow(df):
|
|
| 164 |
Calculate total new snow by:
|
| 165 |
1. Using ONLY the 3-hour snowfall amounts
|
| 166 |
2. Using 9 AM as the daily reset point
|
| 167 |
-
3. Filtering out obvious anomalies (>9 inches in 3 hours)
|
| 168 |
"""
|
| 169 |
# Sort by datetime to ensure correct calculation
|
| 170 |
df = df.sort_values('datetime')
|
| 171 |
-
|
| 172 |
# Create a copy of the dataframe with ONLY datetime and 3-hour snowfall
|
| 173 |
snow_df = df[['datetime', 'snowfall_3hr']].copy()
|
| 174 |
-
|
| 175 |
# Create a day group that starts at 9 AM instead of midnight
|
| 176 |
snow_df['day_group'] = snow_df['datetime'].apply(
|
| 177 |
lambda x: x.date() if x.hour >= 9 else (x - pd.Timedelta(days=1)).date()
|
|
@@ -182,6 +181,59 @@ def calculate_total_new_snow(df):
|
|
| 182 |
|
| 183 |
return daily_totals.sum()
|
| 184 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 185 |
def process_daily_snow(group):
|
| 186 |
"""Sum up ONLY the 3-hour snowfall amounts for each day period"""
|
| 187 |
# Sort by time to ensure proper sequence
|
|
@@ -234,27 +286,47 @@ def create_plots(df):
|
|
| 234 |
ax3.grid(True)
|
| 235 |
ax3.tick_params(axis='x', rotation=45)
|
| 236 |
|
| 237 |
-
# Daily new snow
|
| 238 |
ax4 = fig.add_subplot(gs[3])
|
|
|
|
|
|
|
| 239 |
snow_df = df[['datetime', 'snowfall_3hr']].copy()
|
| 240 |
snow_df['day_group'] = snow_df['datetime'].apply(
|
| 241 |
lambda x: x.date() if x.hour >= 9 else (x - pd.Timedelta(days=1)).date()
|
| 242 |
)
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
#
|
| 247 |
-
|
| 248 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 249 |
ax4.set_xlabel('Date')
|
| 250 |
ax4.set_ylabel('New Snow (inches)')
|
| 251 |
-
ax4.
|
|
|
|
|
|
|
| 252 |
ax4.grid(True, axis='y', linestyle='--', alpha=0.7)
|
| 253 |
-
|
| 254 |
-
# Add value labels on
|
| 255 |
-
for
|
| 256 |
-
|
| 257 |
-
|
|
|
|
|
|
|
|
|
|
| 258 |
|
| 259 |
# SWE bar plot
|
| 260 |
ax5 = fig.add_subplot(gs[4])
|
|
@@ -293,13 +365,14 @@ def analyze_weather_data(site_id, hours):
|
|
| 293 |
if not raw_data:
|
| 294 |
return "Error: Could not retrieve weather data.", None, None
|
| 295 |
|
| 296 |
-
print("Parsing data...")
|
| 297 |
df = parse_weather_data(raw_data)
|
| 298 |
-
|
| 299 |
-
# Calculate total new snow using
|
| 300 |
-
|
|
|
|
| 301 |
current_swe = df['swe'].iloc[0] # Get most recent SWE measurement
|
| 302 |
-
|
| 303 |
print("Calculating statistics...")
|
| 304 |
stats = {
|
| 305 |
'Temperature Range': f"{df['temp'].min():.1f}°F to {df['temp'].max():.1f}°F",
|
|
@@ -308,7 +381,8 @@ def analyze_weather_data(site_id, hours):
|
|
| 308 |
'Max Wind Gust': f"{df['wind_gust'].max():.1f} mph",
|
| 309 |
'Average Humidity': f"{df['humidity'].mean():.1f}%",
|
| 310 |
'Current Snow Depth': f"{df['snow_depth'].iloc[0]:.1f} inches",
|
| 311 |
-
'Total New Snow': f"{
|
|
|
|
| 312 |
'Current Snow/Water Equivalent': f"{current_swe:.2f} inches"
|
| 313 |
}
|
| 314 |
|
|
@@ -333,6 +407,10 @@ with gr.Blocks(title="Weather Station Data Analyzer") as demo:
|
|
| 333 |
gr.Markdown("# Weather Station Data Analyzer")
|
| 334 |
gr.Markdown("""
|
| 335 |
Select a weather station and number of hours to analyze.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 336 |
""")
|
| 337 |
|
| 338 |
with gr.Row():
|
|
|
|
| 164 |
Calculate total new snow by:
|
| 165 |
1. Using ONLY the 3-hour snowfall amounts
|
| 166 |
2. Using 9 AM as the daily reset point
|
|
|
|
| 167 |
"""
|
| 168 |
# Sort by datetime to ensure correct calculation
|
| 169 |
df = df.sort_values('datetime')
|
| 170 |
+
|
| 171 |
# Create a copy of the dataframe with ONLY datetime and 3-hour snowfall
|
| 172 |
snow_df = df[['datetime', 'snowfall_3hr']].copy()
|
| 173 |
+
|
| 174 |
# Create a day group that starts at 9 AM instead of midnight
|
| 175 |
snow_df['day_group'] = snow_df['datetime'].apply(
|
| 176 |
lambda x: x.date() if x.hour >= 9 else (x - pd.Timedelta(days=1)).date()
|
|
|
|
| 181 |
|
| 182 |
return daily_totals.sum()
|
| 183 |
|
| 184 |
+
def calculate_new_snow_from_depth(df):
|
| 185 |
+
"""
|
| 186 |
+
Calculate new snow from changes in snow depth.
|
| 187 |
+
More accurate than 3-hour readings which can be unreliable.
|
| 188 |
+
Uses 9 AM as the daily reset point.
|
| 189 |
+
"""
|
| 190 |
+
# Sort by datetime
|
| 191 |
+
df = df.sort_values('datetime').copy()
|
| 192 |
+
|
| 193 |
+
# Create day groups starting at 9 AM
|
| 194 |
+
df['day_group'] = df['datetime'].apply(
|
| 195 |
+
lambda x: x.date() if x.hour >= 9 else (x - pd.Timedelta(days=1)).date()
|
| 196 |
+
)
|
| 197 |
+
|
| 198 |
+
daily_snow_change = []
|
| 199 |
+
|
| 200 |
+
for day, group in df.groupby('day_group'):
|
| 201 |
+
group = group.sort_values('datetime')
|
| 202 |
+
valid_depths = group['snow_depth'].dropna()
|
| 203 |
+
|
| 204 |
+
if len(valid_depths) > 0:
|
| 205 |
+
# Get max depth increase for the day
|
| 206 |
+
max_depth = valid_depths.max()
|
| 207 |
+
min_depth = valid_depths.min()
|
| 208 |
+
|
| 209 |
+
# New snow is the increase in depth (ignore decreases from settling/melting)
|
| 210 |
+
new_snow = max(0, max_depth - min_depth)
|
| 211 |
+
daily_snow_change.append(new_snow)
|
| 212 |
+
|
| 213 |
+
return sum(daily_snow_change)
|
| 214 |
+
|
| 215 |
+
def calculate_daily_snow_from_depth(df):
|
| 216 |
+
"""Calculate daily new snow from depth changes, returns a dataframe"""
|
| 217 |
+
df = df.sort_values('datetime').copy()
|
| 218 |
+
|
| 219 |
+
df['day_group'] = df['datetime'].apply(
|
| 220 |
+
lambda x: x.date() if x.hour >= 9 else (x - pd.Timedelta(days=1)).date()
|
| 221 |
+
)
|
| 222 |
+
|
| 223 |
+
daily_data = []
|
| 224 |
+
|
| 225 |
+
for day, group in df.groupby('day_group'):
|
| 226 |
+
group = group.sort_values('datetime')
|
| 227 |
+
valid_depths = group['snow_depth'].dropna()
|
| 228 |
+
|
| 229 |
+
if len(valid_depths) > 0:
|
| 230 |
+
max_depth = valid_depths.max()
|
| 231 |
+
min_depth = valid_depths.min()
|
| 232 |
+
new_snow = max(0, max_depth - min_depth)
|
| 233 |
+
daily_data.append({'date': day, 'new_snow_from_depth': new_snow})
|
| 234 |
+
|
| 235 |
+
return pd.DataFrame(daily_data)
|
| 236 |
+
|
| 237 |
def process_daily_snow(group):
|
| 238 |
"""Sum up ONLY the 3-hour snowfall amounts for each day period"""
|
| 239 |
# Sort by time to ensure proper sequence
|
|
|
|
| 286 |
ax3.grid(True)
|
| 287 |
ax3.tick_params(axis='x', rotation=45)
|
| 288 |
|
| 289 |
+
# Daily new snow comparison plot - showing both methods
|
| 290 |
ax4 = fig.add_subplot(gs[3])
|
| 291 |
+
|
| 292 |
+
# Calculate from 3-hour reports
|
| 293 |
snow_df = df[['datetime', 'snowfall_3hr']].copy()
|
| 294 |
snow_df['day_group'] = snow_df['datetime'].apply(
|
| 295 |
lambda x: x.date() if x.hour >= 9 else (x - pd.Timedelta(days=1)).date()
|
| 296 |
)
|
| 297 |
+
daily_snow_3hr = snow_df.groupby('day_group').apply(process_daily_snow).reset_index()
|
| 298 |
+
daily_snow_3hr.columns = ['date', 'new_snow_3hr']
|
| 299 |
+
|
| 300 |
+
# Calculate from depth changes
|
| 301 |
+
daily_snow_depth = calculate_daily_snow_from_depth(df)
|
| 302 |
+
|
| 303 |
+
# Merge the two calculations
|
| 304 |
+
daily_combined = pd.merge(daily_snow_3hr, daily_snow_depth, on='date', how='outer').fillna(0)
|
| 305 |
+
|
| 306 |
+
# Create grouped bar chart
|
| 307 |
+
x = range(len(daily_combined))
|
| 308 |
+
width = 0.35
|
| 309 |
+
|
| 310 |
+
bars1 = ax4.bar([i - width/2 for i in x], daily_combined['new_snow_3hr'],
|
| 311 |
+
width, label='3-Hour Reports', color='skyblue', alpha=0.8)
|
| 312 |
+
bars2 = ax4.bar([i + width/2 for i in x], daily_combined['new_snow_from_depth'],
|
| 313 |
+
width, label='Snow Depth Change', color='darkblue', alpha=0.8)
|
| 314 |
+
|
| 315 |
+
ax4.set_title('Daily New Snow - Two Methods (9 AM Reset)', pad=20, fontsize=12, fontweight='bold')
|
| 316 |
ax4.set_xlabel('Date')
|
| 317 |
ax4.set_ylabel('New Snow (inches)')
|
| 318 |
+
ax4.set_xticks(x)
|
| 319 |
+
ax4.set_xticklabels([str(d) for d in daily_combined['date']], rotation=45, ha='right')
|
| 320 |
+
ax4.legend()
|
| 321 |
ax4.grid(True, axis='y', linestyle='--', alpha=0.7)
|
| 322 |
+
|
| 323 |
+
# Add value labels on bars
|
| 324 |
+
for bars in [bars1, bars2]:
|
| 325 |
+
for bar in bars:
|
| 326 |
+
height = bar.get_height()
|
| 327 |
+
if height > 0:
|
| 328 |
+
ax4.text(bar.get_x() + bar.get_width()/2., height,
|
| 329 |
+
f'{height:.1f}"', ha='center', va='bottom', fontsize=8)
|
| 330 |
|
| 331 |
# SWE bar plot
|
| 332 |
ax5 = fig.add_subplot(gs[4])
|
|
|
|
| 365 |
if not raw_data:
|
| 366 |
return "Error: Could not retrieve weather data.", None, None
|
| 367 |
|
| 368 |
+
print("Parsing data...")
|
| 369 |
df = parse_weather_data(raw_data)
|
| 370 |
+
|
| 371 |
+
# Calculate total new snow using both methods
|
| 372 |
+
total_new_snow_3hr = calculate_total_new_snow(df)
|
| 373 |
+
total_new_snow_depth = calculate_new_snow_from_depth(df)
|
| 374 |
current_swe = df['swe'].iloc[0] # Get most recent SWE measurement
|
| 375 |
+
|
| 376 |
print("Calculating statistics...")
|
| 377 |
stats = {
|
| 378 |
'Temperature Range': f"{df['temp'].min():.1f}°F to {df['temp'].max():.1f}°F",
|
|
|
|
| 381 |
'Max Wind Gust': f"{df['wind_gust'].max():.1f} mph",
|
| 382 |
'Average Humidity': f"{df['humidity'].mean():.1f}%",
|
| 383 |
'Current Snow Depth': f"{df['snow_depth'].iloc[0]:.1f} inches",
|
| 384 |
+
'Total New Snow (3-hr reports)': f"{total_new_snow_3hr:.1f} inches",
|
| 385 |
+
'Total New Snow (depth change)': f"{total_new_snow_depth:.1f} inches",
|
| 386 |
'Current Snow/Water Equivalent': f"{current_swe:.2f} inches"
|
| 387 |
}
|
| 388 |
|
|
|
|
| 407 |
gr.Markdown("# Weather Station Data Analyzer")
|
| 408 |
gr.Markdown("""
|
| 409 |
Select a weather station and number of hours to analyze.
|
| 410 |
+
|
| 411 |
+
**New Snow Calculation Methods:**
|
| 412 |
+
- **3-Hour Reports**: Sum of reported 3-hour snowfall amounts
|
| 413 |
+
- **Depth Change**: Calculated from snow depth increases (generally more accurate)
|
| 414 |
""")
|
| 415 |
|
| 416 |
with gr.Row():
|