Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
@@ -6,7 +6,7 @@ import threading
|
|
6 |
import time
|
7 |
from datetime import datetime, timedelta
|
8 |
from collections import defaultdict
|
9 |
-
|
10 |
import gradio as gr
|
11 |
import pandas as pd
|
12 |
import numpy as np
|
@@ -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,9 +53,9 @@ 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 settings (
|
59 |
BASIN_FILES = {
|
60 |
'EP': 'ibtracs.EP.list.v04r01.csv',
|
61 |
'NA': 'ibtracs.NA.list.v04r01.csv',
|
@@ -137,7 +137,6 @@ def convert_oni_ascii_to_csv(input_file, output_file):
|
|
137 |
year = str(int(year)-1)
|
138 |
data[year][month-1] = anom
|
139 |
with open(output_file, 'w', newline='') as f:
|
140 |
-
writer = pd.ExcelWriter(f)
|
141 |
writer = csv.writer(f)
|
142 |
writer.writerow(['Year','Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'])
|
143 |
for year in sorted(data.keys()):
|
@@ -222,7 +221,9 @@ def classify_enso_phases(oni_value):
|
|
222 |
else:
|
223 |
return 'Neutral'
|
224 |
|
225 |
-
#
|
|
|
|
|
226 |
def perform_wind_regression(start_year, start_month, end_year, end_month):
|
227 |
start_date = datetime(start_year, start_month, 1)
|
228 |
end_date = datetime(end_year, end_month, 28)
|
@@ -262,7 +263,9 @@ def perform_longitude_regression(start_year, start_month, end_year, end_month):
|
|
262 |
p_value = model.pvalues['ONI']
|
263 |
return f"Longitude Regression: β1={beta_1:.4f}, Odds Ratio={exp_beta_1:.4f}, P-value={p_value:.4f}"
|
264 |
|
265 |
-
#
|
|
|
|
|
266 |
def load_ibtracs_data():
|
267 |
ibtracs_data = {}
|
268 |
for basin, filename in BASIN_FILES.items():
|
@@ -286,14 +289,18 @@ def load_ibtracs_data():
|
|
286 |
|
287 |
ibtracs = load_ibtracs_data()
|
288 |
|
289 |
-
#
|
|
|
|
|
290 |
update_oni_data()
|
291 |
oni_data, typhoon_data = load_data(ONI_DATA_PATH, TYPHOON_DATA_PATH)
|
292 |
oni_long = process_oni_data(oni_data)
|
293 |
typhoon_max = process_typhoon_data(typhoon_data)
|
294 |
merged_data = merge_data(oni_long, typhoon_max)
|
295 |
|
296 |
-
#
|
|
|
|
|
297 |
def generate_typhoon_tracks(filtered_data, typhoon_search):
|
298 |
fig = go.Figure()
|
299 |
for sid in filtered_data['SID'].unique():
|
@@ -394,11 +401,12 @@ def get_full_tracks(start_year, start_month, end_year, end_month, enso_phase, ty
|
|
394 |
for sid in unique_storms:
|
395 |
storm_data = typhoon_data[typhoon_data['SID']==sid]
|
396 |
name = storm_data['NAME'].iloc[0] if pd.notnull(storm_data['NAME'].iloc[0]) else "Unnamed"
|
|
|
397 |
storm_oni = filtered_data[filtered_data['SID']==sid]['ONI'].iloc[0]
|
398 |
color = 'red' if storm_oni>=0.5 else ('blue' if storm_oni<=-0.5 else 'green')
|
399 |
fig.add_trace(go.Scattergeo(
|
400 |
lon=storm_data['LON'], lat=storm_data['LAT'], mode='lines',
|
401 |
-
name=f"{name} ({
|
402 |
line=dict(width=1.5, color=color), hoverinfo="name"
|
403 |
))
|
404 |
if typhoon_search:
|
@@ -408,7 +416,7 @@ def get_full_tracks(start_year, start_month, end_year, end_month, enso_phase, ty
|
|
408 |
storm_data = typhoon_data[typhoon_data['SID']==sid]
|
409 |
fig.add_trace(go.Scattergeo(
|
410 |
lon=storm_data['LON'], lat=storm_data['LAT'], mode='lines+markers',
|
411 |
-
name=f"MATCHED: {storm_data['NAME'].iloc[0]}
|
412 |
line=dict(width=3, color='yellow'),
|
413 |
marker=dict(size=5), hoverinfo="name"
|
414 |
))
|
@@ -430,7 +438,7 @@ def get_full_tracks(start_year, start_month, end_year, end_month, enso_phase, ty
|
|
430 |
)
|
431 |
fig.add_annotation(
|
432 |
x=0.02, y=0.98, xref="paper", yref="paper",
|
433 |
-
text="Red: El Niño, Blue: La
|
434 |
showarrow=False, align="left",
|
435 |
bgcolor="rgba(255,255,255,0.8)"
|
436 |
)
|
@@ -476,10 +484,12 @@ def categorize_typhoon_by_standard(wind_speed, standard='atlantic'):
|
|
476 |
return 'Tropical Storm', atlantic_standard['Tropical Storm']['hex']
|
477 |
return 'Tropical Depression', atlantic_standard['Tropical Depression']['hex']
|
478 |
|
479 |
-
#
|
|
|
|
|
480 |
def update_route_clusters(start_year, start_month, end_year, end_month, enso_value, season):
|
481 |
try:
|
482 |
-
# Merge raw typhoon data with ONI so
|
483 |
raw_data = typhoon_data.copy()
|
484 |
raw_data['Year'] = raw_data['ISO_TIME'].dt.year
|
485 |
raw_data['Month'] = raw_data['ISO_TIME'].dt.strftime('%m')
|
@@ -497,7 +507,7 @@ def update_route_clusters(start_year, start_month, end_year, end_month, enso_val
|
|
497 |
merged_raw = merged_raw[merged_raw['ENSO_Phase'] == enso_value.capitalize()]
|
498 |
logging.info(f"Total points after ENSO filtering: {merged_raw.shape[0]}")
|
499 |
|
500 |
-
#
|
501 |
wp_data = merged_raw[(merged_raw['LON'] >= 100) & (merged_raw['LON'] <= 180) &
|
502 |
(merged_raw['LAT'] >= 0) & (merged_raw['LAT'] <= 40)]
|
503 |
logging.info(f"Total points after WP regional filtering: {wp_data.shape[0]}")
|
@@ -505,7 +515,7 @@ def update_route_clusters(start_year, start_month, end_year, end_month, enso_val
|
|
505 |
logging.info("WP regional filter returned no data; using all filtered data.")
|
506 |
wp_data = merged_raw
|
507 |
|
508 |
-
# Group by storm ID
|
509 |
all_storms_data = []
|
510 |
for sid, group in wp_data.groupby('SID'):
|
511 |
group = group.sort_values('ISO_TIME')
|
@@ -514,7 +524,7 @@ def update_route_clusters(start_year, start_month, end_year, end_month, enso_val
|
|
514 |
lons = group['LON'].astype(float).values
|
515 |
if len(lons) < 2:
|
516 |
continue
|
517 |
-
# Also
|
518 |
wind = group['USA_WIND'].astype(float).values if 'USA_WIND' in group.columns else None
|
519 |
pres = group['USA_PRES'].astype(float).values if 'USA_PRES' in group.columns else None
|
520 |
all_storms_data.append((sid, lons, lats, times, wind, pres))
|
@@ -522,7 +532,7 @@ def update_route_clusters(start_year, start_month, end_year, end_month, enso_val
|
|
522 |
if not all_storms_data:
|
523 |
return go.Figure(), go.Figure(), make_subplots(rows=2, cols=1), "No valid storms for clustering."
|
524 |
|
525 |
-
# Interpolate each storm's route
|
526 |
max_length = max(len(item[1]) for item in all_storms_data)
|
527 |
route_vectors = []
|
528 |
wind_curves = []
|
@@ -542,7 +552,7 @@ def update_route_clusters(start_year, start_month, end_year, end_month, enso_val
|
|
542 |
continue
|
543 |
route_vectors.append(route_vector)
|
544 |
storm_ids.append(sid)
|
545 |
-
# Interpolate wind and pressure if available
|
546 |
if wind is not None and len(wind) >= 2:
|
547 |
try:
|
548 |
wind_interp = interp1d(t, wind, kind='linear', fill_value='extrapolate')(t_new)
|
@@ -573,15 +583,14 @@ def update_route_clusters(start_year, start_month, end_year, end_month, enso_val
|
|
573 |
tsne = TSNE(n_components=2, random_state=42, verbose=1)
|
574 |
tsne_results = tsne.fit_transform(route_vectors)
|
575 |
|
576 |
-
# Dynamic DBSCAN: choose eps
|
577 |
selected_labels = None
|
578 |
selected_eps = None
|
579 |
for eps in np.linspace(1.0, 10.0, 91):
|
580 |
dbscan = DBSCAN(eps=eps, min_samples=3)
|
581 |
labels = dbscan.fit_predict(tsne_results)
|
582 |
clusters = set(labels) - {-1}
|
583 |
-
|
584 |
-
if 5 <= num_clusters <= 20:
|
585 |
selected_labels = labels
|
586 |
selected_eps = eps
|
587 |
break
|
@@ -589,7 +598,7 @@ def update_route_clusters(start_year, start_month, end_year, end_month, enso_val
|
|
589 |
selected_eps = 5.0
|
590 |
dbscan = DBSCAN(eps=selected_eps, min_samples=3)
|
591 |
selected_labels = dbscan.fit_predict(tsne_results)
|
592 |
-
logging.info(f"Selected DBSCAN eps: {selected_eps:.2f} yielding {len(set(selected_labels)
|
593 |
|
594 |
# TSNE scatter plot
|
595 |
fig_tsne = go.Figure()
|
@@ -619,9 +628,9 @@ def update_route_clusters(start_year, start_month, end_year, end_month, enso_val
|
|
619 |
yaxis_title="t-SNE Dim 2"
|
620 |
)
|
621 |
|
622 |
-
# For each cluster, compute mean route, mean wind
|
623 |
fig_routes = go.Figure()
|
624 |
-
cluster_stats = [] # To hold mean curves
|
625 |
for i, label in enumerate(unique_labels):
|
626 |
indices = np.where(selected_labels == label)[0]
|
627 |
cluster_ids = [storm_ids[j] for j in indices]
|
@@ -637,23 +646,14 @@ def update_route_clusters(start_year, start_month, end_year, end_month, enso_val
|
|
637 |
line=dict(width=4, color=colors[i % len(colors)]),
|
638 |
name=f"Cluster {label} Mean Route"
|
639 |
))
|
640 |
-
#
|
641 |
-
|
642 |
-
|
643 |
-
|
644 |
-
|
645 |
-
# Compute mean curves (if available)
|
646 |
-
if cluster_winds.size > 0:
|
647 |
-
mean_wind_curve = np.nanmean(cluster_winds, axis=0)
|
648 |
-
else:
|
649 |
-
mean_wind_curve = np.full(max_length, np.nan)
|
650 |
-
if cluster_pres.size > 0:
|
651 |
-
mean_pres_curve = np.nanmean(cluster_pres, axis=0)
|
652 |
-
else:
|
653 |
-
mean_pres_curve = np.full(max_length, np.nan)
|
654 |
cluster_stats.append((label, mean_wind_curve, mean_pres_curve))
|
655 |
|
656 |
-
# Create cluster stats plot with curves vs normalized route index (0 to 1)
|
657 |
x_axis = np.linspace(0, 1, max_length)
|
658 |
fig_stats = make_subplots(rows=2, cols=1, shared_xaxes=True,
|
659 |
subplot_titles=("Mean Wind Speed (knots)", "Mean MSLP (hPa)"))
|
@@ -687,7 +687,9 @@ def update_route_clusters(start_year, start_month, end_year, end_month, enso_val
|
|
687 |
logging.error(f"Error in TSNE clustering: {e}")
|
688 |
return go.Figure(), go.Figure(), make_subplots(rows=2, cols=1), f"Error in TSNE clustering: {e}"
|
689 |
|
690 |
-
#
|
|
|
|
|
691 |
def generate_track_video_from_csv(year, storm_id, standard):
|
692 |
storm_df = typhoon_data[typhoon_data['SID'] == storm_id].copy()
|
693 |
if storm_df.empty:
|
@@ -702,6 +704,7 @@ def generate_track_video_from_csv(year, storm_id, standard):
|
|
702 |
else:
|
703 |
winds = np.full(len(lats), np.nan)
|
704 |
storm_name = storm_df['NAME'].iloc[0]
|
|
|
705 |
season = storm_df['SEASON'].iloc[0]
|
706 |
|
707 |
min_lat, max_lat = np.min(lats), np.max(lats)
|
@@ -718,12 +721,13 @@ def generate_track_video_from_csv(year, storm_id, standard):
|
|
718 |
ax.coastlines(resolution='50m', color='black', linewidth=1)
|
719 |
gl = ax.gridlines(draw_labels=True, color='gray', alpha=0.4, linestyle='--')
|
720 |
gl.top_labels = gl.right_labels = False
|
721 |
-
ax.set_title(f"{year} {storm_name} - {season}", fontsize=14)
|
722 |
|
723 |
line, = ax.plot([], [], transform=ccrs.PlateCarree(), color='blue', linewidth=2)
|
724 |
point, = ax.plot([], [], 'o', markersize=8, transform=ccrs.PlateCarree())
|
725 |
date_text = ax.text(0.02, 0.02, '', transform=ax.transAxes, fontsize=10,
|
726 |
bbox=dict(facecolor='white', alpha=0.8))
|
|
|
727 |
storm_info_text = fig.text(0.70, 0.60, '', fontsize=10,
|
728 |
bbox=dict(facecolor='white', alpha=0.8, boxstyle='round,pad=0.5'))
|
729 |
|
@@ -750,10 +754,7 @@ def generate_track_video_from_csv(year, storm_id, standard):
|
|
750 |
point.set_color(color)
|
751 |
dt_str = pd.to_datetime(times[frame]).strftime('%Y-%m-%d %H:%M')
|
752 |
date_text.set_text(dt_str)
|
753 |
-
info_str = (f"Name: {storm_name}\
|
754 |
-
f"Date: {dt_str}\n"
|
755 |
-
f"Wind: {wind_speed:.1f} kt\n"
|
756 |
-
f"Category: {category}")
|
757 |
storm_info_text.set_text(info_str)
|
758 |
return line, point, date_text, storm_info_text
|
759 |
|
@@ -771,7 +772,9 @@ def simplified_track_video(year, basin, typhoon, standard):
|
|
771 |
storm_id = typhoon.split('(')[-1].strip(')')
|
772 |
return generate_track_video_from_csv(year, storm_id, standard)
|
773 |
|
774 |
-
#
|
|
|
|
|
775 |
basin_to_prefix = {
|
776 |
"All Basins": "all",
|
777 |
"NA - North Atlantic": "NA",
|
@@ -835,7 +838,9 @@ def update_typhoon_options_anim(year, basin):
|
|
835 |
logging.error(f"Error in update_typhoon_options_anim: {e}")
|
836 |
return gr.update(choices=[], value=None)
|
837 |
|
838 |
-
#
|
|
|
|
|
839 |
with gr.Blocks(title="Typhoon Analysis Dashboard") as demo:
|
840 |
gr.Markdown("# Typhoon Analysis Dashboard")
|
841 |
|
@@ -850,10 +855,9 @@ with gr.Blocks(title="Typhoon Analysis Dashboard") as demo:
|
|
850 |
- **Wind Analysis**: Examine wind speed vs ONI relationships.
|
851 |
- **Pressure Analysis**: Analyze pressure vs ONI relationships.
|
852 |
- **Longitude Analysis**: Study typhoon generation longitude vs ONI.
|
853 |
-
- **Path Animation**: View animated storm tracks on a free stock world map (centered at 180°) with a dynamic sidebar and
|
854 |
-
- **TSNE Cluster**: Perform t-SNE clustering on WP storm routes using raw merged typhoon+ONI data.
|
855 |
-
|
856 |
-
are computed from start to end.
|
857 |
""")
|
858 |
|
859 |
with gr.Tab("Track Visualization"):
|
@@ -918,9 +922,10 @@ with gr.Blocks(title="Typhoon Analysis Dashboard") as demo:
|
|
918 |
outputs=[regression_plot, slopes_text, lon_regression_results])
|
919 |
|
920 |
with gr.Tab("Tropical Cyclone Path Animation"):
|
|
|
921 |
with gr.Row():
|
922 |
year_dropdown = gr.Dropdown(label="Year", choices=[str(y) for y in range(1950,2025)], value="2000")
|
923 |
-
|
924 |
with gr.Row():
|
925 |
typhoon_dropdown = gr.Dropdown(label="Tropical Cyclone")
|
926 |
standard_dropdown = gr.Dropdown(label="Classification Standard", choices=['atlantic','taiwan'], value='atlantic')
|
@@ -928,16 +933,16 @@ with gr.Blocks(title="Typhoon Analysis Dashboard") as demo:
|
|
928 |
path_video = gr.Video(label="Tropical Cyclone Path Animation", format="mp4", interactive=False, elem_id="path_video")
|
929 |
animation_info = gr.Markdown("""
|
930 |
### Animation Instructions
|
931 |
-
1. Select a year
|
932 |
2. Choose a tropical cyclone from the populated list.
|
933 |
3. Select a classification standard (Atlantic or Taiwan).
|
934 |
4. Click "Generate Animation".
|
935 |
-
5. The animation displays the storm track on a free stock world map (centered at 180°) with a dynamic sidebar
|
|
|
936 |
""")
|
937 |
-
year_dropdown.change(fn=update_typhoon_options_anim, inputs=[year_dropdown,
|
938 |
-
basin_dropdown.change(fn=update_typhoon_options_anim, inputs=[year_dropdown, basin_dropdown], outputs=typhoon_dropdown)
|
939 |
animate_btn.click(fn=simplified_track_video,
|
940 |
-
inputs=[year_dropdown,
|
941 |
outputs=path_video)
|
942 |
|
943 |
with gr.Tab("TSNE Cluster"):
|
|
|
6 |
import time
|
7 |
from datetime import datetime, timedelta
|
8 |
from collections import defaultdict
|
9 |
+
|
10 |
import gradio as gr
|
11 |
import pandas as pd
|
12 |
import numpy as np
|
|
|
41 |
# Configuration and Setup
|
42 |
# -----------------------------
|
43 |
logging.basicConfig(
|
44 |
+
level=logging.INFO,
|
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')
|
57 |
|
58 |
+
# IBTrACS settings (for typhoon options)
|
59 |
BASIN_FILES = {
|
60 |
'EP': 'ibtracs.EP.list.v04r01.csv',
|
61 |
'NA': 'ibtracs.NA.list.v04r01.csv',
|
|
|
137 |
year = str(int(year)-1)
|
138 |
data[year][month-1] = anom
|
139 |
with open(output_file, 'w', newline='') as f:
|
|
|
140 |
writer = csv.writer(f)
|
141 |
writer.writerow(['Year','Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'])
|
142 |
for year in sorted(data.keys()):
|
|
|
221 |
else:
|
222 |
return 'Neutral'
|
223 |
|
224 |
+
# -----------------------------
|
225 |
+
# Regression Functions
|
226 |
+
# -----------------------------
|
227 |
def perform_wind_regression(start_year, start_month, end_year, end_month):
|
228 |
start_date = datetime(start_year, start_month, 1)
|
229 |
end_date = datetime(end_year, end_month, 28)
|
|
|
263 |
p_value = model.pvalues['ONI']
|
264 |
return f"Longitude Regression: β1={beta_1:.4f}, Odds Ratio={exp_beta_1:.4f}, P-value={p_value:.4f}"
|
265 |
|
266 |
+
# -----------------------------
|
267 |
+
# IBTrACS Data Loading
|
268 |
+
# -----------------------------
|
269 |
def load_ibtracs_data():
|
270 |
ibtracs_data = {}
|
271 |
for basin, filename in BASIN_FILES.items():
|
|
|
289 |
|
290 |
ibtracs = load_ibtracs_data()
|
291 |
|
292 |
+
# -----------------------------
|
293 |
+
# Load & Process Data
|
294 |
+
# -----------------------------
|
295 |
update_oni_data()
|
296 |
oni_data, typhoon_data = load_data(ONI_DATA_PATH, TYPHOON_DATA_PATH)
|
297 |
oni_long = process_oni_data(oni_data)
|
298 |
typhoon_max = process_typhoon_data(typhoon_data)
|
299 |
merged_data = merge_data(oni_long, typhoon_max)
|
300 |
|
301 |
+
# -----------------------------
|
302 |
+
# Visualization Functions
|
303 |
+
# -----------------------------
|
304 |
def generate_typhoon_tracks(filtered_data, typhoon_search):
|
305 |
fig = go.Figure()
|
306 |
for sid in filtered_data['SID'].unique():
|
|
|
401 |
for sid in unique_storms:
|
402 |
storm_data = typhoon_data[typhoon_data['SID']==sid]
|
403 |
name = storm_data['NAME'].iloc[0] if pd.notnull(storm_data['NAME'].iloc[0]) else "Unnamed"
|
404 |
+
basin = storm_data['SID'].iloc[0][:2] # First 2 characters often denote basin
|
405 |
storm_oni = filtered_data[filtered_data['SID']==sid]['ONI'].iloc[0]
|
406 |
color = 'red' if storm_oni>=0.5 else ('blue' if storm_oni<=-0.5 else 'green')
|
407 |
fig.add_trace(go.Scattergeo(
|
408 |
lon=storm_data['LON'], lat=storm_data['LAT'], mode='lines',
|
409 |
+
name=f"{name} ({basin})",
|
410 |
line=dict(width=1.5, color=color), hoverinfo="name"
|
411 |
))
|
412 |
if typhoon_search:
|
|
|
416 |
storm_data = typhoon_data[typhoon_data['SID']==sid]
|
417 |
fig.add_trace(go.Scattergeo(
|
418 |
lon=storm_data['LON'], lat=storm_data['LAT'], mode='lines+markers',
|
419 |
+
name=f"MATCHED: {storm_data['NAME'].iloc[0]}",
|
420 |
line=dict(width=3, color='yellow'),
|
421 |
marker=dict(size=5), hoverinfo="name"
|
422 |
))
|
|
|
438 |
)
|
439 |
fig.add_annotation(
|
440 |
x=0.02, y=0.98, xref="paper", yref="paper",
|
441 |
+
text="Red: El Niño, Blue: La Nina, Green: Neutral",
|
442 |
showarrow=False, align="left",
|
443 |
bgcolor="rgba(255,255,255,0.8)"
|
444 |
)
|
|
|
484 |
return 'Tropical Storm', atlantic_standard['Tropical Storm']['hex']
|
485 |
return 'Tropical Depression', atlantic_standard['Tropical Depression']['hex']
|
486 |
|
487 |
+
# -----------------------------
|
488 |
+
# Updated TSNE Cluster Function with Mean Curves
|
489 |
+
# -----------------------------
|
490 |
def update_route_clusters(start_year, start_month, end_year, end_month, enso_value, season):
|
491 |
try:
|
492 |
+
# Merge raw typhoon data with ONI so each storm has multiple observations.
|
493 |
raw_data = typhoon_data.copy()
|
494 |
raw_data['Year'] = raw_data['ISO_TIME'].dt.year
|
495 |
raw_data['Month'] = raw_data['ISO_TIME'].dt.strftime('%m')
|
|
|
507 |
merged_raw = merged_raw[merged_raw['ENSO_Phase'] == enso_value.capitalize()]
|
508 |
logging.info(f"Total points after ENSO filtering: {merged_raw.shape[0]}")
|
509 |
|
510 |
+
# Regional filtering for Western Pacific
|
511 |
wp_data = merged_raw[(merged_raw['LON'] >= 100) & (merged_raw['LON'] <= 180) &
|
512 |
(merged_raw['LAT'] >= 0) & (merged_raw['LAT'] <= 40)]
|
513 |
logging.info(f"Total points after WP regional filtering: {wp_data.shape[0]}")
|
|
|
515 |
logging.info("WP regional filter returned no data; using all filtered data.")
|
516 |
wp_data = merged_raw
|
517 |
|
518 |
+
# Group by storm ID so each storm has multiple observations
|
519 |
all_storms_data = []
|
520 |
for sid, group in wp_data.groupby('SID'):
|
521 |
group = group.sort_values('ISO_TIME')
|
|
|
524 |
lons = group['LON'].astype(float).values
|
525 |
if len(lons) < 2:
|
526 |
continue
|
527 |
+
# Also extract wind and pressure curves
|
528 |
wind = group['USA_WIND'].astype(float).values if 'USA_WIND' in group.columns else None
|
529 |
pres = group['USA_PRES'].astype(float).values if 'USA_PRES' in group.columns else None
|
530 |
all_storms_data.append((sid, lons, lats, times, wind, pres))
|
|
|
532 |
if not all_storms_data:
|
533 |
return go.Figure(), go.Figure(), make_subplots(rows=2, cols=1), "No valid storms for clustering."
|
534 |
|
535 |
+
# Interpolate each storm's route, wind, and pressure to a common length
|
536 |
max_length = max(len(item[1]) for item in all_storms_data)
|
537 |
route_vectors = []
|
538 |
wind_curves = []
|
|
|
552 |
continue
|
553 |
route_vectors.append(route_vector)
|
554 |
storm_ids.append(sid)
|
555 |
+
# Interpolate wind and pressure if available
|
556 |
if wind is not None and len(wind) >= 2:
|
557 |
try:
|
558 |
wind_interp = interp1d(t, wind, kind='linear', fill_value='extrapolate')(t_new)
|
|
|
583 |
tsne = TSNE(n_components=2, random_state=42, verbose=1)
|
584 |
tsne_results = tsne.fit_transform(route_vectors)
|
585 |
|
586 |
+
# Dynamic DBSCAN: choose eps to yield roughly 5 to 20 clusters
|
587 |
selected_labels = None
|
588 |
selected_eps = None
|
589 |
for eps in np.linspace(1.0, 10.0, 91):
|
590 |
dbscan = DBSCAN(eps=eps, min_samples=3)
|
591 |
labels = dbscan.fit_predict(tsne_results)
|
592 |
clusters = set(labels) - {-1}
|
593 |
+
if 5 <= len(clusters) <= 20:
|
|
|
594 |
selected_labels = labels
|
595 |
selected_eps = eps
|
596 |
break
|
|
|
598 |
selected_eps = 5.0
|
599 |
dbscan = DBSCAN(eps=selected_eps, min_samples=3)
|
600 |
selected_labels = dbscan.fit_predict(tsne_results)
|
601 |
+
logging.info(f"Selected DBSCAN eps: {selected_eps:.2f} yielding {len(set(selected_labels)-{-1})} clusters.")
|
602 |
|
603 |
# TSNE scatter plot
|
604 |
fig_tsne = go.Figure()
|
|
|
628 |
yaxis_title="t-SNE Dim 2"
|
629 |
)
|
630 |
|
631 |
+
# For each cluster, compute mean route, and compute mean wind and pressure curves along normalized route index.
|
632 |
fig_routes = go.Figure()
|
633 |
+
cluster_stats = [] # To hold mean curves per cluster
|
634 |
for i, label in enumerate(unique_labels):
|
635 |
indices = np.where(selected_labels == label)[0]
|
636 |
cluster_ids = [storm_ids[j] for j in indices]
|
|
|
646 |
line=dict(width=4, color=colors[i % len(colors)]),
|
647 |
name=f"Cluster {label} Mean Route"
|
648 |
))
|
649 |
+
# Retrieve raw wind and pressure curves for storms in this cluster
|
650 |
+
cluster_winds = wind_curves[indices, :]
|
651 |
+
cluster_pres = pres_curves[indices, :]
|
652 |
+
mean_wind_curve = np.nanmean(cluster_winds, axis=0)
|
653 |
+
mean_pres_curve = np.nanmean(cluster_pres, axis=0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
654 |
cluster_stats.append((label, mean_wind_curve, mean_pres_curve))
|
655 |
|
656 |
+
# Create a cluster stats plot with curves vs normalized route index (0 to 1)
|
657 |
x_axis = np.linspace(0, 1, max_length)
|
658 |
fig_stats = make_subplots(rows=2, cols=1, shared_xaxes=True,
|
659 |
subplot_titles=("Mean Wind Speed (knots)", "Mean MSLP (hPa)"))
|
|
|
687 |
logging.error(f"Error in TSNE clustering: {e}")
|
688 |
return go.Figure(), go.Figure(), make_subplots(rows=2, cols=1), f"Error in TSNE clustering: {e}"
|
689 |
|
690 |
+
# -----------------------------
|
691 |
+
# Animation Functions Using Processed CSV & Stock Map
|
692 |
+
# -----------------------------
|
693 |
def generate_track_video_from_csv(year, storm_id, standard):
|
694 |
storm_df = typhoon_data[typhoon_data['SID'] == storm_id].copy()
|
695 |
if storm_df.empty:
|
|
|
704 |
else:
|
705 |
winds = np.full(len(lats), np.nan)
|
706 |
storm_name = storm_df['NAME'].iloc[0]
|
707 |
+
basin = storm_df['SID'].iloc[0][:2] # Use first 2 characters as basin code
|
708 |
season = storm_df['SEASON'].iloc[0]
|
709 |
|
710 |
min_lat, max_lat = np.min(lats), np.max(lats)
|
|
|
721 |
ax.coastlines(resolution='50m', color='black', linewidth=1)
|
722 |
gl = ax.gridlines(draw_labels=True, color='gray', alpha=0.4, linestyle='--')
|
723 |
gl.top_labels = gl.right_labels = False
|
724 |
+
ax.set_title(f"{year} {storm_name} ({basin}) - {season}", fontsize=14)
|
725 |
|
726 |
line, = ax.plot([], [], transform=ccrs.PlateCarree(), color='blue', linewidth=2)
|
727 |
point, = ax.plot([], [], 'o', markersize=8, transform=ccrs.PlateCarree())
|
728 |
date_text = ax.text(0.02, 0.02, '', transform=ax.transAxes, fontsize=10,
|
729 |
bbox=dict(facecolor='white', alpha=0.8))
|
730 |
+
# Display storm name and basin in a dynamic sidebar
|
731 |
storm_info_text = fig.text(0.70, 0.60, '', fontsize=10,
|
732 |
bbox=dict(facecolor='white', alpha=0.8, boxstyle='round,pad=0.5'))
|
733 |
|
|
|
754 |
point.set_color(color)
|
755 |
dt_str = pd.to_datetime(times[frame]).strftime('%Y-%m-%d %H:%M')
|
756 |
date_text.set_text(dt_str)
|
757 |
+
info_str = (f"Name: {storm_name}\nBasin: {basin}\nDate: {dt_str}\nWind: {wind_speed:.1f} kt\nCategory: {category}")
|
|
|
|
|
|
|
758 |
storm_info_text.set_text(info_str)
|
759 |
return line, point, date_text, storm_info_text
|
760 |
|
|
|
772 |
storm_id = typhoon.split('(')[-1].strip(')')
|
773 |
return generate_track_video_from_csv(year, storm_id, standard)
|
774 |
|
775 |
+
# -----------------------------
|
776 |
+
# Typhoon Options Update Functions
|
777 |
+
# -----------------------------
|
778 |
basin_to_prefix = {
|
779 |
"All Basins": "all",
|
780 |
"NA - North Atlantic": "NA",
|
|
|
838 |
logging.error(f"Error in update_typhoon_options_anim: {e}")
|
839 |
return gr.update(choices=[], value=None)
|
840 |
|
841 |
+
# -----------------------------
|
842 |
+
# Gradio Interface
|
843 |
+
# -----------------------------
|
844 |
with gr.Blocks(title="Typhoon Analysis Dashboard") as demo:
|
845 |
gr.Markdown("# Typhoon Analysis Dashboard")
|
846 |
|
|
|
855 |
- **Wind Analysis**: Examine wind speed vs ONI relationships.
|
856 |
- **Pressure Analysis**: Analyze pressure vs ONI relationships.
|
857 |
- **Longitude Analysis**: Study typhoon generation longitude vs ONI.
|
858 |
+
- **Path Animation**: View animated storm tracks on a free stock world map (centered at 180°) with a dynamic sidebar that shows the typhoon name and basin.
|
859 |
+
- **TSNE Cluster**: Perform t-SNE clustering on WP storm routes using raw merged typhoon+ONI data with detailed error management.
|
860 |
+
Mean routes and evolving curves (wind and pressure vs. normalized route index) are computed.
|
|
|
861 |
""")
|
862 |
|
863 |
with gr.Tab("Track Visualization"):
|
|
|
922 |
outputs=[regression_plot, slopes_text, lon_regression_results])
|
923 |
|
924 |
with gr.Tab("Tropical Cyclone Path Animation"):
|
925 |
+
# Basin selection removed. Always use All Basins.
|
926 |
with gr.Row():
|
927 |
year_dropdown = gr.Dropdown(label="Year", choices=[str(y) for y in range(1950,2025)], value="2000")
|
928 |
+
# Remove basin dropdown and set it internally to "all"
|
929 |
with gr.Row():
|
930 |
typhoon_dropdown = gr.Dropdown(label="Tropical Cyclone")
|
931 |
standard_dropdown = gr.Dropdown(label="Classification Standard", choices=['atlantic','taiwan'], value='atlantic')
|
|
|
933 |
path_video = gr.Video(label="Tropical Cyclone Path Animation", format="mp4", interactive=False, elem_id="path_video")
|
934 |
animation_info = gr.Markdown("""
|
935 |
### Animation Instructions
|
936 |
+
1. Select a year (data is from your processed CSV, using all basins).
|
937 |
2. Choose a tropical cyclone from the populated list.
|
938 |
3. Select a classification standard (Atlantic or Taiwan).
|
939 |
4. Click "Generate Animation".
|
940 |
+
5. The animation displays the storm track on a free stock world map (centered at 180°) with a dynamic sidebar.
|
941 |
+
The sidebar shows the storm name and basin.
|
942 |
""")
|
943 |
+
year_dropdown.change(fn=update_typhoon_options_anim, inputs=[year_dropdown, gr.State("dummy")], outputs=typhoon_dropdown)
|
|
|
944 |
animate_btn.click(fn=simplified_track_video,
|
945 |
+
inputs=[year_dropdown, gr.Textbox.update(value="All Basins"), typhoon_dropdown, standard_dropdown],
|
946 |
outputs=path_video)
|
947 |
|
948 |
with gr.Tab("TSNE Cluster"):
|