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