euler314 commited on
Commit
84e165d
·
verified ·
1 Parent(s): 611f47d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +297 -74
app.py CHANGED
@@ -8,6 +8,7 @@ import cartopy.crs as ccrs
8
  import cartopy.feature as cfeature
9
  import plotly.graph_objects as go
10
  import plotly.express as px
 
11
  import tropycal.tracks as tracks
12
  import pickle
13
  import requests
@@ -22,6 +23,7 @@ from collections import defaultdict
22
  import filecmp
23
  from sklearn.manifold import TSNE
24
  from sklearn.cluster import DBSCAN
 
25
 
26
  # Command-line argument parsing
27
  parser = argparse.ArgumentParser(description='Typhoon Analysis Dashboard')
@@ -72,6 +74,16 @@ season_months = {
72
  'winter': [12, 1, 2]
73
  }
74
 
 
 
 
 
 
 
 
 
 
 
75
  # Data loading and preprocessing functions
76
  def download_oni_file(url, filename):
77
  response = requests.get(url)
@@ -449,10 +461,70 @@ def perform_longitude_regression(start_year, start_month, end_year, end_month):
449
 
450
  # t-SNE clustering functions
451
  def filter_west_pacific_coordinates(lons, lats):
452
- mask = (lons >= 100) & (lons <= 180) & (lats >= 0) & (lats <= 50)
453
  return lons[mask], lats[mask]
454
 
455
- def dynamic_dbscan(tsne_results, min_clusters=10, max_clusters=20, eps_values=np.arange(0.1, 5.0, 0.1)):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
456
  best_labels = None
457
  best_n_clusters = 0
458
  best_n_noise = len(tsne_results)
@@ -460,7 +532,10 @@ def dynamic_dbscan(tsne_results, min_clusters=10, max_clusters=20, eps_values=np
460
  for eps in eps_values:
461
  dbscan = DBSCAN(eps=eps, min_samples=3)
462
  labels = dbscan.fit_predict(tsne_results)
463
- n_clusters = len(set(labels)) - (1 if -1 in labels else 0)
 
 
 
464
  n_noise = np.sum(labels == -1)
465
  if min_clusters <= n_clusters <= max_clusters and n_noise < best_n_noise:
466
  best_labels = labels
@@ -468,11 +543,19 @@ def dynamic_dbscan(tsne_results, min_clusters=10, max_clusters=20, eps_values=np
468
  best_n_noise = n_noise
469
  best_eps = eps
470
  if best_labels is None:
471
- dbscan = DBSCAN(eps=eps_values[0], min_samples=3)
472
- best_labels = dbscan.fit_predict(tsne_results)
473
- best_n_clusters = len(set(best_labels)) - (1 if -1 in best_labels else 0)
474
- best_n_noise = np.sum(best_labels == -1)
475
- best_eps = eps_values[0]
 
 
 
 
 
 
 
 
476
  return best_labels, best_n_clusters, best_n_noise, best_eps
477
 
478
  def update_route_clusters(start_year, start_month, end_year, end_month, enso_value, season):
@@ -484,7 +567,7 @@ def update_route_clusters(start_year, start_month, end_year, end_month, enso_val
484
  season_data = ibtracs.get_season(year)
485
  for storm_id in season_data.summary()['id']:
486
  storm = ibtracs.get_storm(storm_id)
487
- if storm.time[0] >= start_date and storm.time[-1] <= end_date:
488
  lons, lats = filter_west_pacific_coordinates(np.array(storm.lon), np.array(storm.lat))
489
  if len(lons) > 1:
490
  start_time = storm.time[0]
@@ -497,79 +580,235 @@ def update_route_clusters(start_year, start_month, end_year, end_month, enso_val
497
  if enso_value == 'all' or enso_phase_storm == enso_value.capitalize():
498
  all_storms_data.append((lons, lats, np.array(storm.vmax), np.array(storm.mslp), np.array(storm.time), storm.name, enso_phase_storm))
499
 
500
- if season != 'all':
501
- all_storms_data = [storm for storm in all_storms_data if storm[4][0].month in season_months[season]]
502
-
503
  if not all_storms_data:
504
- return go.Figure(), go.Figure(), go.Figure(), "No storms found in the selected period."
505
 
506
  # Prepare route vectors for t-SNE
507
  max_length = max(len(st[0]) for st in all_storms_data)
508
  route_vectors = []
509
- for lons, lats, _, _, _, _, _ in all_storms_data:
510
- interp_lons = np.interp(np.linspace(0, 1, max_length), np.linspace(0, 1, len(lons)), lons)
511
- interp_lats = np.interp(np.linspace(0, 1, max_length), np.linspace(0, 1, len(lats)), lats)
512
- route_vectors.append(np.column_stack((interp_lons, interp_lats)).flatten())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
513
  route_vectors = np.array(route_vectors)
 
 
514
 
515
  # Perform t-SNE
516
- tsne_results = TSNE(n_components=2, random_state=42, perplexity=min(30, len(route_vectors)-1)).fit_transform(route_vectors)
 
517
 
518
  # Dynamic DBSCAN clustering
519
  best_labels, best_n_clusters, best_n_noise, best_eps = dynamic_dbscan(tsne_results)
520
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
521
  # t-SNE Scatter Plot
522
  fig_tsne = go.Figure()
523
- for cluster in set(best_labels):
524
- mask = best_labels == cluster
525
- name = "Noise" if cluster == -1 else f"Cluster {cluster}"
 
 
 
 
526
  fig_tsne.add_trace(go.Scatter(
527
- x=tsne_results[mask, 0], y=tsne_results[mask, 1], mode='markers',
528
- name=name, text=[all_storms_data[i][5] for i in range(len(all_storms_data)) if mask[i]],
529
- hoverinfo='text'
 
 
530
  ))
531
- fig_tsne.update_layout(title="t-SNE Clustering of Typhoon Routes", xaxis_title="t-SNE 1", yaxis_title="t-SNE 2")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
532
 
533
- # Typhoon Routes Plot
534
  fig_routes = go.Figure()
535
- for i, (lons, lats, _, _, _, name, _) in enumerate(all_storms_data):
536
- cluster = best_labels[i]
537
- color = 'gray' if cluster == -1 else px.colors.qualitative.Plotly[cluster % len(px.colors.qualitative.Plotly)]
538
- fig_routes.add_trace(go.Scattergeo(
539
- lon=lons, lat=lats, mode='lines+markers', name=name,
540
- line=dict(color=color), marker=dict(size=4), hoverinfo='text', text=name
541
- ))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
542
  fig_routes.update_layout(
543
- title="Typhoon Routes by Cluster",
544
  geo=dict(scope='asia', projection_type='mercator', showland=True, landcolor='lightgray')
545
  )
546
 
547
  # Cluster Statistics Plot
548
- cluster_stats = []
549
- for cluster in set(best_labels) - {-1}:
550
- mask = best_labels == cluster
551
- winds = [all_storms_data[i][2].max() for i in range(len(all_storms_data)) if mask[i]]
552
- pressures = [all_storms_data[i][3].min() for i in range(len(all_storms_data)) if mask[i]]
553
- cluster_stats.append({
554
- 'Cluster': cluster,
555
- 'Count': np.sum(mask),
556
- 'Mean Wind': np.mean(winds),
557
- 'Mean Pressure': np.mean(pressures)
558
- })
559
- stats_df = pd.DataFrame(cluster_stats)
560
- fig_stats = go.Figure()
561
- fig_stats.add_trace(go.Bar(x=stats_df['Cluster'], y=stats_df['Count'], name='Storm Count'))
562
- fig_stats.add_trace(go.Bar(x=stats_df['Cluster'], y=stats_df['Mean Wind'], name='Mean Max Wind Speed'))
563
- fig_stats.add_trace(go.Bar(x=stats_df['Cluster'], y=stats_df['Mean Pressure'], name='Mean Min Pressure'))
564
- fig_stats.update_layout(barmode='group', title="Cluster Statistics")
 
 
 
 
 
565
 
566
  # Cluster Information
567
- cluster_info = f"Date Range: {start_year}-{start_month} to {end_year}-{end_month}\nENSO Phase: {enso_value}\nSeason: {season}\n\n"
568
- cluster_info += f"Selected EPS: {best_eps}\nNumber of Clusters: {best_n_clusters}\nNoise Points: {best_n_noise} ({(best_n_noise / len(best_labels))*100:.1f}%)\n"
569
- for stat in cluster_stats:
570
- cluster_info += f"Cluster {stat['Cluster']}: {stat['Count']} storms, Mean Max Wind: {stat['Mean Wind']:.1f} kt, Mean Min Pressure: {stat['Mean Pressure']:.1f} hPa\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
571
 
572
- return fig_tsne, fig_routes, fig_stats, cluster_info
 
573
 
574
  # Gradio Interface
575
  with gr.Blocks(title="Typhoon Analysis Dashboard") as demo:
@@ -587,7 +826,7 @@ with gr.Blocks(title="Typhoon Analysis Dashboard") as demo:
587
  - **Pressure Analysis**: Analyze pressure vs ONI relationships
588
  - **Longitude Analysis**: Study typhoon generation longitude vs ONI
589
  - **Path Animation**: Watch animated typhoon paths with a sidebar
590
- - **TSNE Cluster**: Perform t-SNE clustering on typhoon routes
591
 
592
  Select a tab above to begin your analysis.
593
  """)
@@ -783,7 +1022,7 @@ with gr.Blocks(title="Typhoon Analysis Dashboard") as demo:
783
  tsne_season = gr.Dropdown(label="Season", choices=['all', 'summer', 'winter'], value='all')
784
  tsne_analyze_btn = gr.Button("Analyze")
785
  tsne_plot = gr.Plot(label="t-SNE Clusters")
786
- routes_plot = gr.Plot(label="Typhoon Routes")
787
  stats_plot = gr.Plot(label="Cluster Statistics")
788
  cluster_info = gr.Textbox(label="Cluster Information", lines=10)
789
 
@@ -793,20 +1032,4 @@ with gr.Blocks(title="Typhoon Analysis Dashboard") as demo:
793
  outputs=[tsne_plot, routes_plot, stats_plot, cluster_info]
794
  )
795
 
796
- # Custom CSS for better visibility
797
- gr.HTML("""
798
- <style>
799
- #tracks_plot, #path_video {
800
- height: 700px !important;
801
- width: 100%;
802
- }
803
- .plot-container {
804
- min-height: 600px;
805
- }
806
- .gr-plotly {
807
- width: 100% !important;
808
- }
809
- </style>
810
- """)
811
-
812
  demo.launch(share=True)
 
8
  import cartopy.feature as cfeature
9
  import plotly.graph_objects as go
10
  import plotly.express as px
11
+ from plotly.subplots import make_subplots
12
  import tropycal.tracks as tracks
13
  import pickle
14
  import requests
 
23
  import filecmp
24
  from sklearn.manifold import TSNE
25
  from sklearn.cluster import DBSCAN
26
+ from scipy.interpolate import interp1d
27
 
28
  # Command-line argument parsing
29
  parser = argparse.ArgumentParser(description='Typhoon Analysis Dashboard')
 
74
  'winter': [12, 1, 2]
75
  }
76
 
77
+ # Regions for duration calculations
78
+ regions = {
79
+ "Taiwan Land": {"lat_min": 21.8, "lat_max": 25.3, "lon_min": 119.5, "lon_max": 122.1},
80
+ "Taiwan Sea": {"lat_min": 19, "lat_max": 28, "lon_min": 117, "lon_max": 125},
81
+ "Japan": {"lat_min": 20, "lat_max": 45, "lon_min": 120, "lon_max": 150},
82
+ "China": {"lat_min": 18, "lat_max": 53, "lon_min": 73, "lon_max": 135},
83
+ "Hong Kong": {"lat_min": 21.5, "lat_max": 23, "lon_min": 113, "lon_max": 115},
84
+ "Philippines": {"lat_min": 5, "lat_max": 21, "lon_min": 115, "lon_max": 130}
85
+ }
86
+
87
  # Data loading and preprocessing functions
88
  def download_oni_file(url, filename):
89
  response = requests.get(url)
 
461
 
462
  # t-SNE clustering functions
463
  def filter_west_pacific_coordinates(lons, lats):
464
+ mask = (lons >= 100) & (lons <= 180) & (lats >= 0) & (lats <= 40)
465
  return lons[mask], lats[mask]
466
 
467
+ def filter_storm_by_season(storm, season):
468
+ start_month = storm.time[0].month
469
+ if season == 'all':
470
+ return True
471
+ elif season == 'summer':
472
+ return 4 <= start_month <= 8
473
+ elif season == 'winter':
474
+ return 9 <= start_month <= 12
475
+ return False
476
+
477
+ def point_region(lat, lon):
478
+ twl = regions["Taiwan Land"]
479
+ if twl["lat_min"] <= lat <= twl["lat_max"] and twl["lon_min"] <= lon <= twl["lon_max"]:
480
+ return "Taiwan Land"
481
+ tws = regions["Taiwan Sea"]
482
+ if tws["lat_min"] <= lat <= tws["lat_max"] and tws["lon_min"] <= lon <= tws["lon_max"]:
483
+ if not (twl["lat_min"] <= lat <= twl["lat_max"] and twl["lon_min"] <= lon <= twl["lon_max"]):
484
+ return "Taiwan Sea"
485
+ for rg in ["Japan", "China", "Hong Kong", "Philippines"]:
486
+ box = regions[rg]
487
+ if box["lat_min"] <= lat <= box["lat_max"] and box["lon_min"] <= lon <= box["lon_max"]:
488
+ return rg
489
+ return None
490
+
491
+ def calculate_region_durations(lons, lats, times):
492
+ region_times = defaultdict(float)
493
+ point_regions_list = [point_region(lats[i], lons[i]) for i in range(len(lons))]
494
+ for i in range(len(lons) - 1):
495
+ dt = (times[i + 1] - times[i]).total_seconds() / 3600.0
496
+ r1 = point_regions_list[i]
497
+ r2 = point_regions_list[i + 1]
498
+ if r1 and r2:
499
+ if r1 == r2:
500
+ region_times[r1] += dt
501
+ else:
502
+ region_times[r1] += dt / 2
503
+ region_times[r2] += dt / 2
504
+ elif r1 and not r2:
505
+ region_times[r1] += dt / 2
506
+ elif r2 and not r1:
507
+ region_times[r2] += dt / 2
508
+ return dict(region_times)
509
+
510
+ def endpoint_region_label(cluster_label, cluster_labels, filtered_storms):
511
+ indices = np.where(cluster_labels == cluster_label)[0]
512
+ if len(indices) == 0:
513
+ return ""
514
+ end_count = defaultdict(int)
515
+ for idx in indices:
516
+ lons, lats, vmax_, mslp_, times = filtered_storms[idx]
517
+ reg = point_region(lats[-1], lons[-1])
518
+ if reg:
519
+ end_count[reg] += 1
520
+ if end_count:
521
+ max_reg = max(end_count, key=end_count.get)
522
+ ratio = end_count[max_reg] / len(indices)
523
+ if ratio > 0.5:
524
+ return max_reg
525
+ return ""
526
+
527
+ def dynamic_dbscan(tsne_results, min_clusters=10, max_clusters=20, eps_values=np.linspace(1.0, 10.0, 91)):
528
  best_labels = None
529
  best_n_clusters = 0
530
  best_n_noise = len(tsne_results)
 
532
  for eps in eps_values:
533
  dbscan = DBSCAN(eps=eps, min_samples=3)
534
  labels = dbscan.fit_predict(tsne_results)
535
+ unique_labels = set(labels)
536
+ if -1 in unique_labels:
537
+ unique_labels.remove(-1)
538
+ n_clusters = len(unique_labels)
539
  n_noise = np.sum(labels == -1)
540
  if min_clusters <= n_clusters <= max_clusters and n_noise < best_n_noise:
541
  best_labels = labels
 
543
  best_n_noise = n_noise
544
  best_eps = eps
545
  if best_labels is None:
546
+ for eps in eps_values[::-1]:
547
+ dbscan = DBSCAN(eps=eps, min_samples=3)
548
+ labels = dbscan.fit_predict(tsne_results)
549
+ unique_labels = set(labels)
550
+ if -1 in unique_labels:
551
+ unique_labels.remove(-1)
552
+ n_clusters = len(unique_labels)
553
+ if n_clusters == max_clusters:
554
+ best_labels = labels
555
+ best_n_clusters = n_clusters
556
+ best_n_noise = np.sum(labels == -1)
557
+ best_eps = eps
558
+ break
559
  return best_labels, best_n_clusters, best_n_noise, best_eps
560
 
561
  def update_route_clusters(start_year, start_month, end_year, end_month, enso_value, season):
 
567
  season_data = ibtracs.get_season(year)
568
  for storm_id in season_data.summary()['id']:
569
  storm = ibtracs.get_storm(storm_id)
570
+ if storm.time[0] >= start_date and storm.time[-1] <= end_date and filter_storm_by_season(storm, season):
571
  lons, lats = filter_west_pacific_coordinates(np.array(storm.lon), np.array(storm.lat))
572
  if len(lons) > 1:
573
  start_time = storm.time[0]
 
580
  if enso_value == 'all' or enso_phase_storm == enso_value.capitalize():
581
  all_storms_data.append((lons, lats, np.array(storm.vmax), np.array(storm.mslp), np.array(storm.time), storm.name, enso_phase_storm))
582
 
 
 
 
583
  if not all_storms_data:
584
+ return go.Figure(), go.Figure(), make_subplots(rows=2, cols=1), "No storms found in the selected period."
585
 
586
  # Prepare route vectors for t-SNE
587
  max_length = max(len(st[0]) for st in all_storms_data)
588
  route_vectors = []
589
+ filtered_storms = []
590
+ storms_vmax_list = []
591
+ storms_mslp_list = []
592
+ for idx, (lons, lats, vmax, mslp, times, name, enso_phase) in enumerate(all_storms_data):
593
+ t = np.linspace(0, 1, len(lons))
594
+ t_new = np.linspace(0, 1, max_length)
595
+ try:
596
+ lon_i = interp1d(t, lons, kind='linear', fill_value='extrapolate')(t_new)
597
+ lat_i = interp1d(t, lats, kind='linear', fill_value='extrapolate')(t_new)
598
+ vmax_i = interp1d(t, vmax, kind='linear', fill_value='extrapolate')(t_new)
599
+ mslp_i = interp1d(t, mslp, kind='linear', fill_value='extrapolate')(t_new)
600
+ except Exception as e:
601
+ continue
602
+ route_vector = np.column_stack((lon_i, lat_i)).flatten()
603
+ if np.isnan(route_vector).any():
604
+ continue
605
+ route_vectors.append(route_vector)
606
+ filtered_storms.append((lons, lats, vmax_i, mslp_i, times))
607
+ storms_vmax_list.append(vmax_i)
608
+ storms_mslp_list.append(mslp_i)
609
+
610
  route_vectors = np.array(route_vectors)
611
+ if len(route_vectors) == 0:
612
+ return go.Figure(), go.Figure(), make_subplots(rows=2, cols=1), "No valid storms after interpolation."
613
 
614
  # Perform t-SNE
615
+ tsne = TSNE(n_components=2, random_state=42, verbose=1)
616
+ tsne_results = tsne.fit_transform(route_vectors)
617
 
618
  # Dynamic DBSCAN clustering
619
  best_labels, best_n_clusters, best_n_noise, best_eps = dynamic_dbscan(tsne_results)
620
 
621
+ # Calculate region durations and mean routes
622
+ unique_labels = sorted(set(best_labels) - {-1})
623
+ label_to_idx = {label: i for i, label in enumerate(unique_labels)}
624
+ cluster_region_durations = [defaultdict(float) for _ in range(len(unique_labels))]
625
+ cluster_mean_routes = []
626
+ cluster_mean_vmax = []
627
+ cluster_mean_mslp = []
628
+
629
+ for i, (lons, lats, vmax, mslp, times) in enumerate(filtered_storms):
630
+ c = best_labels[i]
631
+ if c == -1:
632
+ continue
633
+ durations = calculate_region_durations(lons, lats, times)
634
+ idx = label_to_idx[c]
635
+ for r, val in durations.items():
636
+ cluster_region_durations[idx][r] += val
637
+
638
+ for c in unique_labels:
639
+ indices = np.where(best_labels == c)[0]
640
+ if len(indices) == 0:
641
+ cluster_mean_routes.append(([], []))
642
+ cluster_mean_vmax.append([])
643
+ cluster_mean_mslp.append([])
644
+ continue
645
+ cluster_lons = []
646
+ cluster_lats = []
647
+ cluster_v = []
648
+ cluster_p = []
649
+ for idx in indices:
650
+ lons, lats, vmax_, mslp_, times = filtered_storms[idx]
651
+ t = np.linspace(0, 1, len(lons))
652
+ t_new = np.linspace(0, 1, max_length)
653
+ lon_i = interp1d(t, lons, kind='linear', fill_value='extrapolate')(t_new)
654
+ lat_i = interp1d(t, lats, kind='linear', fill_value='extrapolate')(t_new)
655
+ cluster_lons.append(lon_i)
656
+ cluster_lats.append(lat_i)
657
+ cluster_v.append(storms_vmax_list[idx])
658
+ cluster_p.append(storms_mslp_list[idx])
659
+ if cluster_lons and cluster_lats:
660
+ mean_lon = np.mean(cluster_lons, axis=0)
661
+ mean_lat = np.mean(cluster_lats, axis=0)
662
+ mean_v = np.mean(cluster_v, axis=0)
663
+ mean_p = np.mean(cluster_p, axis=0)
664
+ cluster_mean_routes.append((mean_lon, mean_lat))
665
+ cluster_mean_vmax.append(mean_v)
666
+ cluster_mean_mslp.append(mean_p)
667
+ else:
668
+ cluster_mean_routes.append(([], []))
669
+ cluster_mean_vmax.append([])
670
+ cluster_mean_mslp.append([])
671
+
672
  # t-SNE Scatter Plot
673
  fig_tsne = go.Figure()
674
+ cluster_colors = px.colors.qualitative.Safe
675
+ if len(cluster_colors) < len(unique_labels):
676
+ cluster_colors = px.colors.qualitative.Dark24
677
+ for i, c in enumerate(unique_labels):
678
+ indices = np.where(best_labels == c)[0]
679
+ end_reg = endpoint_region_label(c, best_labels, filtered_storms)
680
+ name = f"Cluster {i+1}" + (f" (towards {end_reg})" if end_reg else "")
681
  fig_tsne.add_trace(go.Scatter(
682
+ x=tsne_results[indices, 0],
683
+ y=tsne_results[indices, 1],
684
+ mode='markers',
685
+ marker=dict(size=5, color=cluster_colors[i % len(cluster_colors)]),
686
+ name=name
687
  ))
688
+ noise_indices = np.where(best_labels == -1)[0]
689
+ if len(noise_indices) > 0:
690
+ fig_tsne.add_trace(go.Scatter(
691
+ x=tsne_results[noise_indices, 0],
692
+ y=tsne_results[noise_indices, 1],
693
+ mode='markers',
694
+ marker=dict(size=5, color='grey'),
695
+ name='Noise'
696
+ ))
697
+ fig_tsne.update_layout(
698
+ title="TSNE of Typhoon Routes",
699
+ xaxis_title="TSNE Dim 1",
700
+ yaxis_title="TSNE Dim 2",
701
+ legend_title="Clusters"
702
+ )
703
 
704
+ # Typhoon Routes Plot with Mean Routes
705
  fig_routes = go.Figure()
706
+ for i, (lons, lats, _, _, _) in enumerate(filtered_storms):
707
+ c = best_labels[i]
708
+ if c == -1:
709
+ continue
710
+ color_idx = label_to_idx[c]
711
+ fig_routes.add_trace(
712
+ go.Scattergeo(
713
+ lon=lons,
714
+ lat=lats,
715
+ mode='lines',
716
+ opacity=0.3,
717
+ line=dict(width=1, color=cluster_colors[color_idx % len(cluster_colors)]),
718
+ showlegend=False
719
+ )
720
+ )
721
+ for i, c in enumerate(unique_labels):
722
+ mean_lon, mean_lat = cluster_mean_routes[i]
723
+ if len(mean_lon) == 0:
724
+ continue
725
+ end_reg = endpoint_region_label(c, best_labels, filtered_storms)
726
+ name = f"Cluster {i+1}" + (f" (towards {end_reg})" if end_reg else "")
727
+ fig_routes.add_trace(
728
+ go.Scattergeo(
729
+ lon=mean_lon,
730
+ lat=mean_lat,
731
+ mode='lines',
732
+ line=dict(width=4, color=cluster_colors[i % len(cluster_colors)]),
733
+ name=name
734
+ )
735
+ )
736
+ fig_routes.add_trace(
737
+ go.Scattergeo(
738
+ lon=[mean_lon[0]],
739
+ lat=[mean_lat[0]],
740
+ mode='markers',
741
+ marker=dict(size=10, color='green', symbol='triangle-up'),
742
+ name=f"Cluster {i+1} Start"
743
+ )
744
+ )
745
+ fig_routes.add_trace(
746
+ go.Scattergeo(
747
+ lon=[mean_lon[-1]],
748
+ lat=[mean_lat[-1]],
749
+ mode='markers',
750
+ marker=dict(size=10, color='red', symbol='x'),
751
+ name=f"Cluster {i+1} End"
752
+ )
753
+ )
754
+ enso_phase_text = {'all': 'All Years', 'El Nino': 'El Niño', 'La Nina': 'La Niña', 'Neutral': 'Neutral Years'}
755
  fig_routes.update_layout(
756
+ title=f"West Pacific Typhoon Routes ({start_year}-{end_year}, {season.capitalize()}, {enso_phase_text.get(enso_value, 'All Years')})",
757
  geo=dict(scope='asia', projection_type='mercator', showland=True, landcolor='lightgray')
758
  )
759
 
760
  # Cluster Statistics Plot
761
+ fig_stats = make_subplots(rows=2, cols=1, shared_xaxes=True, subplot_titles=("Average Wind Speed", "Average Pressure"))
762
+ for i, c in enumerate(unique_labels):
763
+ if len(cluster_mean_vmax[i]) > 0:
764
+ end_reg = endpoint_region_label(c, best_labels, filtered_storms)
765
+ name = f"Cluster {i+1}" + (f" ({end_reg})" if end_reg else "")
766
+ fig_stats.add_trace(
767
+ go.Scatter(y=cluster_mean_vmax[i], mode='lines', line=dict(width=2, color=cluster_colors[i % len(cluster_colors)]), name=name),
768
+ row=1, col=1
769
+ )
770
+ fig_stats.add_trace(
771
+ go.Scatter(y=cluster_mean_mslp[i], mode='lines', line=dict(width=2, color=cluster_colors[i % len(cluster_colors)]), name=name),
772
+ row=2, col=1
773
+ )
774
+ fig_stats.update_layout(
775
+ title="Cluster Average Wind & Pressure Profiles",
776
+ xaxis_title="Route Normalized Index",
777
+ yaxis_title="Wind Speed (knots)",
778
+ xaxis2_title="Route Normalized Index",
779
+ yaxis2_title="Pressure (hPa)",
780
+ showlegend=True,
781
+ legend_tracegroupgap=300
782
+ )
783
 
784
  # Cluster Information
785
+ cluster_info_lines = [f"Selected DBSCAN eps: {best_eps:.2f}", f"Number of noise points: {best_n_noise}"]
786
+ for i, c in enumerate(unique_labels):
787
+ indices = np.where(best_labels == c)[0]
788
+ count = len(indices)
789
+ if count == 0:
790
+ continue
791
+ avg_durations = {r: (cluster_region_durations[i][r] / count) for r in cluster_region_durations[i]}
792
+ end_reg = endpoint_region_label(c, best_labels, filtered_storms)
793
+ name = f"Cluster {i+1}" + (f" (towards {end_reg})" if end_reg else "")
794
+ cluster_info_lines.append(f"\n{name}")
795
+ if avg_durations:
796
+ for reg, hrs in avg_durations.items():
797
+ cluster_info_lines.append(f"{reg}: {hrs:.2f} hours")
798
+ else:
799
+ cluster_info_lines.append("No significant region durations.")
800
+ if end_reg in ["Taiwan Land", "Taiwan Sea"] and len(cluster_mean_vmax[i]) > 0:
801
+ final_wind = cluster_mean_vmax[i][-1]
802
+ if final_wind >= 34:
803
+ cluster_info_lines.append(
804
+ "CWA would issue a land warning ~18 hours before arrival." if end_reg == "Taiwan Land"
805
+ else "CWA would issue a sea warning ~24 hours before arrival."
806
+ )
807
+ if len(noise_indices) > 0:
808
+ cluster_info_lines.append(f"\nNoise Cluster\nNumber of storms classified as noise: {len(noise_indices)}")
809
 
810
+ cluster_info_text = "\n".join(cluster_info_lines)
811
+ return fig_tsne, fig_routes, fig_stats, cluster_info_text
812
 
813
  # Gradio Interface
814
  with gr.Blocks(title="Typhoon Analysis Dashboard") as demo:
 
826
  - **Pressure Analysis**: Analyze pressure vs ONI relationships
827
  - **Longitude Analysis**: Study typhoon generation longitude vs ONI
828
  - **Path Animation**: Watch animated typhoon paths with a sidebar
829
+ - **TSNE Cluster**: Perform t-SNE clustering on typhoon routes with mean routes and region analysis
830
 
831
  Select a tab above to begin your analysis.
832
  """)
 
1022
  tsne_season = gr.Dropdown(label="Season", choices=['all', 'summer', 'winter'], value='all')
1023
  tsne_analyze_btn = gr.Button("Analyze")
1024
  tsne_plot = gr.Plot(label="t-SNE Clusters")
1025
+ routes_plot = gr.Plot(label="Typhoon Routes with Mean Routes")
1026
  stats_plot = gr.Plot(label="Cluster Statistics")
1027
  cluster_info = gr.Textbox(label="Cluster Information", lines=10)
1028
 
 
1032
  outputs=[tsne_plot, routes_plot, stats_plot, cluster_info]
1033
  )
1034
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1035
  demo.launch(share=True)