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)
|