euler314 commited on
Commit
87de8af
·
verified ·
1 Parent(s): 30bb628

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +63 -58
app.py CHANGED
@@ -6,7 +6,7 @@ import threading
6
  import time
7
  from datetime import datetime, timedelta
8
  from collections import defaultdict
9
- import csv
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, # Use DEBUG for more details
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') # used in other tabs
57
 
58
- # IBTrACS settings (only used for updating typhoon options)
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
- # ------------- Regression Functions -------------
 
 
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
- # ------------- IBTrACS Data Loading -------------
 
 
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
- # ------------- Load & Process Data -------------
 
 
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
- # ------------- Visualization Functions -------------
 
 
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} ({storm_data['SEASON'].iloc[0]})",
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]} ({storm_data['SEASON'].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 Niña, Green: Neutral",
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
- # ------------- Updated TSNE Cluster Function with Mean Curves -------------
 
 
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 that each storm has multiple points.
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
- # Apply regional filter for Western Pacific (adjust boundaries as needed)
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 (SID); each group must have at least 2 observations
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 store wind and pressure for interpolation
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 (and wind/pressure) to a common length
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; otherwise, fill with NaN
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 so that we have roughly 5 to 20 clusters if possible
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
- num_clusters = len(clusters)
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) - {-1})} clusters.")
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 curve, and mean pressure curve.
623
  fig_routes = go.Figure()
624
- cluster_stats = [] # To hold mean curves for wind and pressure
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
- # Get storms in this cluster from wp_data by SID
641
- cluster_raw = wp_data[wp_data['SID'].isin(cluster_ids)]
642
- # For each storm in the cluster, we already interpolated wind_curves and pres_curves.
643
- cluster_winds = wind_curves[indices, :] # shape: (#storms, max_length)
644
- cluster_pres = pres_curves[indices, :] # shape: (#storms, max_length)
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
- # ------------- Animation Functions Using Processed CSV & Stock Map -------------
 
 
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}\n"
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
- # ------------- Typhoon Options Update Functions -------------
 
 
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
- # ------------- Gradio Interface -------------
 
 
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 persistent legend.
854
- - **TSNE Cluster**: Perform t-SNE clustering on WP storm routes using raw merged typhoon+ONI data.
855
- For each cluster, a mean route is computed and, importantly, mean wind and MSLP curves (plotted versus normalized route index)
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
- basin_dropdown = gr.Dropdown(label="Basin", choices=["NA - North Atlantic","EP - Eastern North Pacific","WP - Western North Pacific","All Basins"], value="NA - North Atlantic")
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 and basin (data is from your processed CSV).
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 and persistent legend.
 
936
  """)
937
- year_dropdown.change(fn=update_typhoon_options_anim, inputs=[year_dropdown, basin_dropdown], outputs=typhoon_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, basin_dropdown, typhoon_dropdown, standard_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"):