Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -41,7 +41,7 @@ import tropycal.tracks as tracks
|
|
41 |
# Configuration and Setup
|
42 |
# -----------------------------
|
43 |
logging.basicConfig(
|
44 |
-
level=logging.INFO, #
|
45 |
format='%(asctime)s - %(levelname)s - %(message)s'
|
46 |
)
|
47 |
|
@@ -53,18 +53,18 @@ DATA_PATH = args.data_path
|
|
53 |
# Data paths
|
54 |
ONI_DATA_PATH = os.path.join(DATA_PATH, 'oni_data.csv')
|
55 |
TYPHOON_DATA_PATH = os.path.join(DATA_PATH, 'processed_typhoon_data.csv')
|
56 |
-
MERGED_DATA_CSV = os.path.join(DATA_PATH, 'merged_typhoon_era5_data.csv') #
|
57 |
|
58 |
-
# IBTrACS (used only for typhoon option updates)
|
59 |
-
LOCAL_IBTRACS_PATH = os.path.join(DATA_PATH, 'ibtracs.WP.list.v04r01.csv')
|
60 |
-
IBTRACS_URI = 'https://www.ncei.noaa.gov/data/international-best-track-archive-for-climate-stewardship-ibtracs/v04r01/access/csv/ibtracs.WP.list.v04r01.csv'
|
61 |
-
CACHE_FILE = os.path.join(DATA_PATH, 'ibtracs_cache.pkl')
|
62 |
-
CACHE_EXPIRY_DAYS = 1
|
63 |
BASIN_FILES = {
|
64 |
'EP': 'ibtracs.EP.list.v04r01.csv',
|
65 |
'NA': 'ibtracs.NA.list.v04r01.csv',
|
66 |
'WP': 'ibtracs.WP.list.v04r01.csv'
|
67 |
}
|
|
|
|
|
|
|
|
|
68 |
|
69 |
# -----------------------------
|
70 |
# Color Maps and Standards
|
@@ -261,7 +261,7 @@ def load_ibtracs_data():
|
|
261 |
local_path = os.path.join(DATA_PATH, filename)
|
262 |
if not os.path.exists(local_path):
|
263 |
logging.info(f"Downloading {basin} basin file...")
|
264 |
-
response = requests.get(
|
265 |
response.raise_for_status()
|
266 |
with open(local_path, 'wb') as f:
|
267 |
f.write(response.content)
|
@@ -366,7 +366,7 @@ def generate_main_analysis(start_year, start_month, end_year, end_month, enso_ph
|
|
366 |
filtered_data = merged_data[(merged_data['ISO_TIME']>=start_date) & (merged_data['ISO_TIME']<=end_date)].copy()
|
367 |
filtered_data['ENSO_Phase'] = filtered_data['ONI'].apply(classify_enso_phases)
|
368 |
if enso_phase != 'all':
|
369 |
-
filtered_data = filtered_data[filtered_data['ENSO_Phase']
|
370 |
tracks_fig = generate_typhoon_tracks(filtered_data, typhoon_search)
|
371 |
wind_scatter = generate_wind_oni_scatter(filtered_data, typhoon_search)
|
372 |
pressure_scatter = generate_pressure_oni_scatter(filtered_data, typhoon_search)
|
@@ -379,7 +379,7 @@ def get_full_tracks(start_year, start_month, end_year, end_month, enso_phase, ty
|
|
379 |
filtered_data = merged_data[(merged_data['ISO_TIME']>=start_date) & (merged_data['ISO_TIME']<=end_date)].copy()
|
380 |
filtered_data['ENSO_Phase'] = filtered_data['ONI'].apply(classify_enso_phases)
|
381 |
if enso_phase != 'all':
|
382 |
-
filtered_data = filtered_data[filtered_data['ENSO_Phase']
|
383 |
unique_storms = filtered_data['SID'].unique()
|
384 |
count = len(unique_storms)
|
385 |
fig = go.Figure()
|
@@ -468,10 +468,10 @@ def categorize_typhoon_by_standard(wind_speed, standard='atlantic'):
|
|
468 |
return 'Tropical Storm', atlantic_standard['Tropical Storm']['hex']
|
469 |
return 'Tropical Depression', atlantic_standard['Tropical Depression']['hex']
|
470 |
|
471 |
-
# ------------- Updated TSNE Cluster Function -------------
|
472 |
def update_route_clusters(start_year, start_month, end_year, end_month, enso_value, season):
|
473 |
try:
|
474 |
-
# Use raw typhoon data
|
475 |
raw_data = typhoon_data.copy()
|
476 |
raw_data['Year'] = raw_data['ISO_TIME'].dt.year
|
477 |
raw_data['Month'] = raw_data['ISO_TIME'].dt.strftime('%m')
|
@@ -483,13 +483,13 @@ def update_route_clusters(start_year, start_month, end_year, end_month, enso_val
|
|
483 |
merged_raw = merged_raw[(merged_raw['ISO_TIME'] >= start_date) & (merged_raw['ISO_TIME'] <= end_date)]
|
484 |
logging.info(f"Total points after date filtering: {merged_raw.shape[0]}")
|
485 |
|
486 |
-
# Filter by ENSO phase if
|
487 |
merged_raw['ENSO_Phase'] = merged_raw['ONI'].apply(classify_enso_phases)
|
488 |
if enso_value != 'all':
|
489 |
merged_raw = merged_raw[merged_raw['ENSO_Phase'] == enso_value.capitalize()]
|
490 |
logging.info(f"Total points after ENSO filtering: {merged_raw.shape[0]}")
|
491 |
|
492 |
-
#
|
493 |
wp_data = merged_raw[(merged_raw['LON'] >= 100) & (merged_raw['LON'] <= 180) &
|
494 |
(merged_raw['LAT'] >= 0) & (merged_raw['LAT'] <= 40)]
|
495 |
logging.info(f"Total points after WP regional filtering: {wp_data.shape[0]}")
|
@@ -497,7 +497,7 @@ def update_route_clusters(start_year, start_month, end_year, end_month, enso_val
|
|
497 |
logging.info("WP regional filter returned no data; using all filtered data.")
|
498 |
wp_data = merged_raw
|
499 |
|
500 |
-
# Group by SID so each storm
|
501 |
all_storms_data = []
|
502 |
for sid, group in wp_data.groupby('SID'):
|
503 |
group = group.sort_values('ISO_TIME')
|
@@ -511,7 +511,7 @@ def update_route_clusters(start_year, start_month, end_year, end_month, enso_val
|
|
511 |
if not all_storms_data:
|
512 |
return go.Figure(), go.Figure(), make_subplots(rows=2, cols=1), "No valid storms for clustering."
|
513 |
|
514 |
-
# Interpolate each storm
|
515 |
max_length = max(len(item[1]) for item in all_storms_data)
|
516 |
route_vectors = []
|
517 |
storm_ids = []
|
@@ -567,10 +567,53 @@ def update_route_clusters(start_year, start_month, end_year, end_month, enso_val
|
|
567 |
yaxis_title="t-SNE Dim 2"
|
568 |
)
|
569 |
|
570 |
-
#
|
571 |
fig_routes = go.Figure()
|
572 |
fig_stats = make_subplots(rows=2, cols=1, shared_xaxes=True,
|
573 |
-
subplot_titles=("Average Wind Speed", "Average
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
574 |
info = "TSNE clustering complete."
|
575 |
return fig_tsne, fig_routes, fig_stats, info
|
576 |
except Exception as e:
|
@@ -741,7 +784,7 @@ with gr.Blocks(title="Typhoon Analysis Dashboard") as demo:
|
|
741 |
- **Pressure Analysis**: Analyze pressure vs ONI relationships.
|
742 |
- **Longitude Analysis**: Study typhoon generation longitude vs ONI.
|
743 |
- **Path Animation**: View animated storm tracks on a free stock world map (centered at 180°) with a dynamic sidebar and persistent legend.
|
744 |
-
- **TSNE Cluster**: Perform t-SNE clustering on WP storm routes using raw merged typhoon+ONI data with detailed error management.
|
745 |
""")
|
746 |
|
747 |
with gr.Tab("Track Visualization"):
|
@@ -820,7 +863,7 @@ with gr.Blocks(title="Typhoon Analysis Dashboard") as demo:
|
|
820 |
2. Choose a tropical cyclone from the populated list.
|
821 |
3. Select a classification standard (Atlantic or Taiwan).
|
822 |
4. Click "Generate Animation".
|
823 |
-
5. The animation displays the storm track on a free stock world map (centered at 180°) with a dynamic sidebar and
|
824 |
""")
|
825 |
year_dropdown.change(fn=update_typhoon_options_anim, inputs=[year_dropdown, basin_dropdown], outputs=typhoon_dropdown)
|
826 |
basin_dropdown.change(fn=update_typhoon_options_anim, inputs=[year_dropdown, basin_dropdown], outputs=typhoon_dropdown)
|
|
|
41 |
# Configuration and Setup
|
42 |
# -----------------------------
|
43 |
logging.basicConfig(
|
44 |
+
level=logging.INFO, # Change to DEBUG for more details
|
45 |
format='%(asctime)s - %(levelname)s - %(message)s'
|
46 |
)
|
47 |
|
|
|
53 |
# Data paths
|
54 |
ONI_DATA_PATH = os.path.join(DATA_PATH, 'oni_data.csv')
|
55 |
TYPHOON_DATA_PATH = os.path.join(DATA_PATH, 'processed_typhoon_data.csv')
|
56 |
+
MERGED_DATA_CSV = os.path.join(DATA_PATH, 'merged_typhoon_era5_data.csv') # Used by other analyses
|
57 |
|
58 |
+
# IBTrACS settings (used only for typhoon option updates)
|
|
|
|
|
|
|
|
|
59 |
BASIN_FILES = {
|
60 |
'EP': 'ibtracs.EP.list.v04r01.csv',
|
61 |
'NA': 'ibtracs.NA.list.v04r01.csv',
|
62 |
'WP': 'ibtracs.WP.list.v04r01.csv'
|
63 |
}
|
64 |
+
IBTRACS_BASE_URL = 'https://www.ncei.noaa.gov/data/international-best-track-archive-for-climate-stewardship-ibtracs/v04r01/access/csv/'
|
65 |
+
LOCAL_IBTRACS_PATH = os.path.join(DATA_PATH, 'ibtracs.WP.list.v04r01.csv')
|
66 |
+
CACHE_FILE = os.path.join(DATA_PATH, 'ibtracs_cache.pkl')
|
67 |
+
CACHE_EXPIRY_DAYS = 1
|
68 |
|
69 |
# -----------------------------
|
70 |
# Color Maps and Standards
|
|
|
261 |
local_path = os.path.join(DATA_PATH, filename)
|
262 |
if not os.path.exists(local_path):
|
263 |
logging.info(f"Downloading {basin} basin file...")
|
264 |
+
response = requests.get(IBTRACS_BASE_URL+filename)
|
265 |
response.raise_for_status()
|
266 |
with open(local_path, 'wb') as f:
|
267 |
f.write(response.content)
|
|
|
366 |
filtered_data = merged_data[(merged_data['ISO_TIME']>=start_date) & (merged_data['ISO_TIME']<=end_date)].copy()
|
367 |
filtered_data['ENSO_Phase'] = filtered_data['ONI'].apply(classify_enso_phases)
|
368 |
if enso_phase != 'all':
|
369 |
+
filtered_data = filtered_data[filtered_data['ENSO_Phase']==enso_phase.capitalize()]
|
370 |
tracks_fig = generate_typhoon_tracks(filtered_data, typhoon_search)
|
371 |
wind_scatter = generate_wind_oni_scatter(filtered_data, typhoon_search)
|
372 |
pressure_scatter = generate_pressure_oni_scatter(filtered_data, typhoon_search)
|
|
|
379 |
filtered_data = merged_data[(merged_data['ISO_TIME']>=start_date) & (merged_data['ISO_TIME']<=end_date)].copy()
|
380 |
filtered_data['ENSO_Phase'] = filtered_data['ONI'].apply(classify_enso_phases)
|
381 |
if enso_phase != 'all':
|
382 |
+
filtered_data = filtered_data[filtered_data['ENSO_Phase']==enso_phase.capitalize()]
|
383 |
unique_storms = filtered_data['SID'].unique()
|
384 |
count = len(unique_storms)
|
385 |
fig = go.Figure()
|
|
|
468 |
return 'Tropical Storm', atlantic_standard['Tropical Storm']['hex']
|
469 |
return 'Tropical Depression', atlantic_standard['Tropical Depression']['hex']
|
470 |
|
471 |
+
# ------------- Updated TSNE Cluster Function with Mean Path and Stats -------------
|
472 |
def update_route_clusters(start_year, start_month, end_year, end_month, enso_value, season):
|
473 |
try:
|
474 |
+
# Use raw typhoon data merged with ONI info so each storm has multiple points.
|
475 |
raw_data = typhoon_data.copy()
|
476 |
raw_data['Year'] = raw_data['ISO_TIME'].dt.year
|
477 |
raw_data['Month'] = raw_data['ISO_TIME'].dt.strftime('%m')
|
|
|
483 |
merged_raw = merged_raw[(merged_raw['ISO_TIME'] >= start_date) & (merged_raw['ISO_TIME'] <= end_date)]
|
484 |
logging.info(f"Total points after date filtering: {merged_raw.shape[0]}")
|
485 |
|
486 |
+
# Filter by ENSO phase if specified
|
487 |
merged_raw['ENSO_Phase'] = merged_raw['ONI'].apply(classify_enso_phases)
|
488 |
if enso_value != 'all':
|
489 |
merged_raw = merged_raw[merged_raw['ENSO_Phase'] == enso_value.capitalize()]
|
490 |
logging.info(f"Total points after ENSO filtering: {merged_raw.shape[0]}")
|
491 |
|
492 |
+
# Apply regional filter for Western Pacific (adjust as needed)
|
493 |
wp_data = merged_raw[(merged_raw['LON'] >= 100) & (merged_raw['LON'] <= 180) &
|
494 |
(merged_raw['LAT'] >= 0) & (merged_raw['LAT'] <= 40)]
|
495 |
logging.info(f"Total points after WP regional filtering: {wp_data.shape[0]}")
|
|
|
497 |
logging.info("WP regional filter returned no data; using all filtered data.")
|
498 |
wp_data = merged_raw
|
499 |
|
500 |
+
# Group by SID so that each storm has multiple points
|
501 |
all_storms_data = []
|
502 |
for sid, group in wp_data.groupby('SID'):
|
503 |
group = group.sort_values('ISO_TIME')
|
|
|
511 |
if not all_storms_data:
|
512 |
return go.Figure(), go.Figure(), make_subplots(rows=2, cols=1), "No valid storms for clustering."
|
513 |
|
514 |
+
# Interpolate each storm route to a common length
|
515 |
max_length = max(len(item[1]) for item in all_storms_data)
|
516 |
route_vectors = []
|
517 |
storm_ids = []
|
|
|
567 |
yaxis_title="t-SNE Dim 2"
|
568 |
)
|
569 |
|
570 |
+
# Compute mean route for each cluster and cluster stats (average wind and pressure)
|
571 |
fig_routes = go.Figure()
|
572 |
fig_stats = make_subplots(rows=2, cols=1, shared_xaxes=True,
|
573 |
+
subplot_titles=("Average Wind Speed (knots)", "Average MSLP (hPa)"))
|
574 |
+
for i, label in enumerate(unique_labels):
|
575 |
+
indices = np.where(labels == label)[0]
|
576 |
+
cluster_ids = [storm_ids[j] for j in indices]
|
577 |
+
cluster_vectors = route_vectors[indices, :]
|
578 |
+
mean_vector = np.mean(cluster_vectors, axis=0)
|
579 |
+
mean_route = mean_vector.reshape((max_length, 2))
|
580 |
+
mean_lon = mean_route[:, 0]
|
581 |
+
mean_lat = mean_route[:, 1]
|
582 |
+
fig_routes.add_trace(go.Scattergeo(
|
583 |
+
lon=mean_lon,
|
584 |
+
lat=mean_lat,
|
585 |
+
mode='lines',
|
586 |
+
line=dict(width=4, color=colors[i % len(colors)]),
|
587 |
+
name=f"Cluster {label} Mean Route"
|
588 |
+
))
|
589 |
+
# Get cluster data from raw filtered data (wp_data)
|
590 |
+
cluster_data = wp_data[wp_data['SID'].isin(cluster_ids)]
|
591 |
+
avg_wind = cluster_data['USA_WIND'].mean() if 'USA_WIND' in cluster_data.columns else np.nan
|
592 |
+
avg_pres = cluster_data['USA_PRES'].mean() if 'USA_PRES' in cluster_data.columns else np.nan
|
593 |
+
# Plot horizontal lines for cluster stats at index i
|
594 |
+
fig_stats.add_trace(go.Scatter(
|
595 |
+
x=[i, i],
|
596 |
+
y=[avg_wind, avg_wind],
|
597 |
+
mode='lines+markers',
|
598 |
+
line=dict(width=2, color=colors[i % len(colors)]),
|
599 |
+
name=f"Cluster {label} Avg Wind"
|
600 |
+
), row=1, col=1)
|
601 |
+
fig_stats.add_trace(go.Scatter(
|
602 |
+
x=[i, i],
|
603 |
+
y=[avg_pres, avg_pres],
|
604 |
+
mode='lines+markers',
|
605 |
+
line=dict(width=2, color=colors[i % len(colors)]),
|
606 |
+
name=f"Cluster {label} Avg Pres"
|
607 |
+
), row=2, col=1)
|
608 |
+
|
609 |
+
fig_stats.update_layout(
|
610 |
+
title="Cluster Statistics",
|
611 |
+
xaxis_title="Cluster Index",
|
612 |
+
yaxis_title="Average Wind Speed (knots)",
|
613 |
+
xaxis2_title="Cluster Index",
|
614 |
+
yaxis2_title="Average MSLP (hPa)",
|
615 |
+
showlegend=True
|
616 |
+
)
|
617 |
info = "TSNE clustering complete."
|
618 |
return fig_tsne, fig_routes, fig_stats, info
|
619 |
except Exception as e:
|
|
|
784 |
- **Pressure Analysis**: Analyze pressure vs ONI relationships.
|
785 |
- **Longitude Analysis**: Study typhoon generation longitude vs ONI.
|
786 |
- **Path Animation**: View animated storm tracks on a free stock world map (centered at 180°) with a dynamic sidebar and persistent legend.
|
787 |
+
- **TSNE Cluster**: Perform t-SNE clustering on WP storm routes using raw merged typhoon+ONI data with detailed error management. Mean storm routes and cluster-level average wind/pressure are computed.
|
788 |
""")
|
789 |
|
790 |
with gr.Tab("Track Visualization"):
|
|
|
863 |
2. Choose a tropical cyclone from the populated list.
|
864 |
3. Select a classification standard (Atlantic or Taiwan).
|
865 |
4. Click "Generate Animation".
|
866 |
+
5. The animation displays the storm track on a free stock world map (centered at 180°) with a dynamic sidebar and persistent legend.
|
867 |
""")
|
868 |
year_dropdown.change(fn=update_typhoon_options_anim, inputs=[year_dropdown, basin_dropdown], outputs=typhoon_dropdown)
|
869 |
basin_dropdown.change(fn=update_typhoon_options_anim, inputs=[year_dropdown, basin_dropdown], outputs=typhoon_dropdown)
|