Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -65,6 +65,13 @@ taiwan_standard = {
|
|
| 65 |
'Tropical Depression': {'wind_speed': 0, 'color': 'Gray', 'hex': '#808080'}
|
| 66 |
}
|
| 67 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
# Data loading and preprocessing functions
|
| 69 |
def download_oni_file(url, filename):
|
| 70 |
response = requests.get(url)
|
|
@@ -362,9 +369,9 @@ def generate_track_video(year, typhoon, standard):
|
|
| 362 |
point, = ax.plot([], [], 'o', markersize=8, transform=ccrs.PlateCarree())
|
| 363 |
date_text = ax.text(0.02, 0.02, '', transform=ax.transAxes, fontsize=10, bbox=dict(facecolor='white', alpha=0.8))
|
| 364 |
|
| 365 |
-
# Add sidebar on the right
|
| 366 |
-
details_title = fig.text(0.
|
| 367 |
-
details_text = fig.text(0.
|
| 368 |
bbox=dict(facecolor='white', alpha=0.8, boxstyle='round,pad=0.5'))
|
| 369 |
|
| 370 |
# Add color legend
|
|
@@ -372,8 +379,8 @@ def generate_track_video(year, typhoon, standard):
|
|
| 372 |
legend_elements = [plt.Line2D([0], [0], marker='o', color='w', label=f"{cat}",
|
| 373 |
markerfacecolor=details['hex'], markersize=10)
|
| 374 |
for cat, details in standard_dict.items()]
|
| 375 |
-
fig.legend(handles=legend_elements, title="Color Legend", loc='
|
| 376 |
-
bbox_to_anchor=(0.95, 0.
|
| 377 |
|
| 378 |
def init():
|
| 379 |
line.set_data([], [])
|
|
@@ -445,6 +452,29 @@ def filter_west_pacific_coordinates(lons, lats):
|
|
| 445 |
mask = (lons >= 100) & (lons <= 180) & (lats >= 0) & (lats <= 50)
|
| 446 |
return lons[mask], lats[mask]
|
| 447 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 448 |
def update_route_clusters(start_year, start_month, end_year, end_month, enso_value, season):
|
| 449 |
start_date = datetime(int(start_year), int(start_month), 1)
|
| 450 |
end_date = datetime(int(end_year), int(end_month), 28)
|
|
@@ -457,7 +487,18 @@ def update_route_clusters(start_year, start_month, end_year, end_month, enso_val
|
|
| 457 |
if storm.time[0] >= start_date and storm.time[-1] <= end_date:
|
| 458 |
lons, lats = filter_west_pacific_coordinates(np.array(storm.lon), np.array(storm.lat))
|
| 459 |
if len(lons) > 1:
|
| 460 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 461 |
|
| 462 |
if not all_storms_data:
|
| 463 |
return go.Figure(), go.Figure(), go.Figure(), "No storms found in the selected period."
|
|
@@ -465,7 +506,7 @@ def update_route_clusters(start_year, start_month, end_year, end_month, enso_val
|
|
| 465 |
# Prepare route vectors for t-SNE
|
| 466 |
max_length = max(len(st[0]) for st in all_storms_data)
|
| 467 |
route_vectors = []
|
| 468 |
-
for lons, lats, _, _, _, _ in all_storms_data:
|
| 469 |
interp_lons = np.interp(np.linspace(0, 1, max_length), np.linspace(0, 1, len(lons)), lons)
|
| 470 |
interp_lats = np.interp(np.linspace(0, 1, max_length), np.linspace(0, 1, len(lats)), lats)
|
| 471 |
route_vectors.append(np.column_stack((interp_lons, interp_lats)).flatten())
|
|
@@ -475,32 +516,7 @@ def update_route_clusters(start_year, start_month, end_year, end_month, enso_val
|
|
| 475 |
tsne_results = TSNE(n_components=2, random_state=42, perplexity=min(30, len(route_vectors)-1)).fit_transform(route_vectors)
|
| 476 |
|
| 477 |
# Dynamic DBSCAN clustering
|
| 478 |
-
|
| 479 |
-
eps_range = np.arange(5.0, 50.0, 5.0)
|
| 480 |
-
min_samples = max(3, len(all_storms_data) // 20)
|
| 481 |
-
best_labels = None
|
| 482 |
-
best_eps = None
|
| 483 |
-
best_n_clusters = 0
|
| 484 |
-
best_noise_ratio = 1.0
|
| 485 |
-
|
| 486 |
-
for eps in eps_range:
|
| 487 |
-
dbscan = DBSCAN(eps=eps, min_samples=min_samples)
|
| 488 |
-
labels = dbscan.fit_predict(tsne_results)
|
| 489 |
-
n_clusters = len(set(labels)) - (1 if -1 in labels else 0)
|
| 490 |
-
noise_points = np.sum(labels == -1)
|
| 491 |
-
noise_ratio = noise_points / len(labels)
|
| 492 |
-
|
| 493 |
-
if n_clusters >= target_clusters and noise_ratio < 0.3 and (n_clusters > best_n_clusters or (n_clusters == best_n_clusters and noise_ratio < best_noise_ratio)):
|
| 494 |
-
best_labels = labels
|
| 495 |
-
best_eps = eps
|
| 496 |
-
best_n_clusters = n_clusters
|
| 497 |
-
best_noise_ratio = noise_ratio
|
| 498 |
-
|
| 499 |
-
if best_labels is None:
|
| 500 |
-
dbscan = DBSCAN(eps=5.0, min_samples=min_samples)
|
| 501 |
-
best_labels = dbscan.fit_predict(tsne_results)
|
| 502 |
-
best_eps = 5.0
|
| 503 |
-
best_n_clusters = len(set(best_labels)) - (1 if -1 in best_labels else 0)
|
| 504 |
|
| 505 |
# t-SNE Scatter Plot
|
| 506 |
fig_tsne = go.Figure()
|
|
@@ -516,7 +532,7 @@ def update_route_clusters(start_year, start_month, end_year, end_month, enso_val
|
|
| 516 |
|
| 517 |
# Typhoon Routes Plot
|
| 518 |
fig_routes = go.Figure()
|
| 519 |
-
for i, (lons, lats, _, _, _, name) in enumerate(all_storms_data):
|
| 520 |
cluster = best_labels[i]
|
| 521 |
color = 'gray' if cluster == -1 else px.colors.qualitative.Plotly[cluster % len(px.colors.qualitative.Plotly)]
|
| 522 |
fig_routes.add_trace(go.Scattergeo(
|
|
@@ -541,13 +557,17 @@ def update_route_clusters(start_year, start_month, end_year, end_month, enso_val
|
|
| 541 |
'Mean Pressure': np.mean(pressures)
|
| 542 |
})
|
| 543 |
stats_df = pd.DataFrame(cluster_stats)
|
| 544 |
-
fig_stats =
|
| 545 |
-
|
|
|
|
|
|
|
|
|
|
| 546 |
|
| 547 |
# Cluster Information
|
| 548 |
-
cluster_info = f"
|
|
|
|
| 549 |
for stat in cluster_stats:
|
| 550 |
-
cluster_info += f"
|
| 551 |
|
| 552 |
return fig_tsne, fig_routes, fig_stats, cluster_info
|
| 553 |
|
|
@@ -737,7 +757,7 @@ with gr.Blocks(title="Typhoon Analysis Dashboard") as demo:
|
|
| 737 |
5. The animation shows the typhoon track growing over a world map, with:
|
| 738 |
- Date on the bottom left
|
| 739 |
- Sidebar on the right showing typhoon details (name, date, wind speed, category) as it moves
|
| 740 |
-
- Color legend with colored markers on the
|
| 741 |
""")
|
| 742 |
|
| 743 |
def update_typhoon_options(year):
|
|
|
|
| 65 |
'Tropical Depression': {'wind_speed': 0, 'color': 'Gray', 'hex': '#808080'}
|
| 66 |
}
|
| 67 |
|
| 68 |
+
# Season months mapping
|
| 69 |
+
season_months = {
|
| 70 |
+
'all': list(range(1, 13)),
|
| 71 |
+
'summer': [6, 7, 8],
|
| 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)
|
|
|
|
| 369 |
point, = ax.plot([], [], 'o', markersize=8, transform=ccrs.PlateCarree())
|
| 370 |
date_text = ax.text(0.02, 0.02, '', transform=ax.transAxes, fontsize=10, bbox=dict(facecolor='white', alpha=0.8))
|
| 371 |
|
| 372 |
+
# Add sidebar on the right with adjusted positions
|
| 373 |
+
details_title = fig.text(0.7, 0.95, "Typhoon Details", fontsize=12, fontweight='bold', verticalalignment='top')
|
| 374 |
+
details_text = fig.text(0.7, 0.85, '', fontsize=12, verticalalignment='top',
|
| 375 |
bbox=dict(facecolor='white', alpha=0.8, boxstyle='round,pad=0.5'))
|
| 376 |
|
| 377 |
# Add color legend
|
|
|
|
| 379 |
legend_elements = [plt.Line2D([0], [0], marker='o', color='w', label=f"{cat}",
|
| 380 |
markerfacecolor=details['hex'], markersize=10)
|
| 381 |
for cat, details in standard_dict.items()]
|
| 382 |
+
fig.legend(handles=legend_elements, title="Color Legend", loc='center right',
|
| 383 |
+
bbox_to_anchor=(0.95, 0.5), fontsize=10)
|
| 384 |
|
| 385 |
def init():
|
| 386 |
line.set_data([], [])
|
|
|
|
| 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)
|
| 459 |
+
best_eps = None
|
| 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
|
| 467 |
+
best_n_clusters = n_clusters
|
| 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):
|
| 479 |
start_date = datetime(int(start_year), int(start_month), 1)
|
| 480 |
end_date = datetime(int(end_year), int(end_month), 28)
|
|
|
|
| 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]
|
| 491 |
+
start_year_storm = start_time.year
|
| 492 |
+
start_month_storm = start_time.month
|
| 493 |
+
oni_row = oni_long[(oni_long['Year'] == start_year_storm) & (oni_long['Month'] == f'{start_month_storm:02d}')]
|
| 494 |
+
if not oni_row.empty:
|
| 495 |
+
oni_value_storm = oni_row['ONI'].iloc[0]
|
| 496 |
+
enso_phase_storm = classify_enso_phases(oni_value_storm)
|
| 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."
|
|
|
|
| 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())
|
|
|
|
| 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()
|
|
|
|
| 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(
|
|
|
|
| 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 |
|
|
|
|
| 757 |
5. The animation shows the typhoon track growing over a world map, with:
|
| 758 |
- Date on the bottom left
|
| 759 |
- Sidebar on the right showing typhoon details (name, date, wind speed, category) as it moves
|
| 760 |
+
- Color legend with colored markers centered on the right
|
| 761 |
""")
|
| 762 |
|
| 763 |
def update_typhoon_options(year):
|