Spaces:
Sleeping
Sleeping
Update app.py
Browse files
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 <=
|
453 |
return lons[mask], lats[mask]
|
454 |
|
455 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
|
|
|
|
|
|
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 |
-
|
472 |
-
|
473 |
-
|
474 |
-
|
475 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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(),
|
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 |
-
|
510 |
-
|
511 |
-
|
512 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
513 |
route_vectors = np.array(route_vectors)
|
|
|
|
|
514 |
|
515 |
# Perform t-SNE
|
516 |
-
|
|
|
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 |
-
|
524 |
-
|
525 |
-
|
|
|
|
|
|
|
|
|
526 |
fig_tsne.add_trace(go.Scatter(
|
527 |
-
x=tsne_results[
|
528 |
-
|
529 |
-
|
|
|
|
|
530 |
))
|
531 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
532 |
|
533 |
-
# Typhoon Routes Plot
|
534 |
fig_routes = go.Figure()
|
535 |
-
for i, (lons, lats, _, _, _
|
536 |
-
|
537 |
-
|
538 |
-
|
539 |
-
|
540 |
-
|
541 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
542 |
fig_routes.update_layout(
|
543 |
-
title="Typhoon Routes
|
544 |
geo=dict(scope='asia', projection_type='mercator', showland=True, landcolor='lightgray')
|
545 |
)
|
546 |
|
547 |
# Cluster Statistics Plot
|
548 |
-
|
549 |
-
for
|
550 |
-
|
551 |
-
|
552 |
-
|
553 |
-
|
554 |
-
|
555 |
-
|
556 |
-
|
557 |
-
|
558 |
-
|
559 |
-
|
560 |
-
|
561 |
-
fig_stats.
|
562 |
-
|
563 |
-
|
564 |
-
|
|
|
|
|
|
|
|
|
|
|
565 |
|
566 |
# Cluster Information
|
567 |
-
|
568 |
-
|
569 |
-
|
570 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
571 |
|
572 |
-
|
|
|
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)
|