euler314's picture
Update app.py
16ed767 verified
raw
history blame
39.6 kB
import gradio as gr
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas
import cartopy.crs as ccrs
import cartopy.feature as cfeature
import plotly.graph_objects as go
import plotly.express as px
import tropycal.tracks as tracks
import pickle
import requests
import os
import argparse
from datetime import datetime
import statsmodels.api as sm
import shutil
import tempfile
import csv
from collections import defaultdict
import filecmp
from sklearn.manifold import TSNE
from sklearn.cluster import DBSCAN
# Command-line argument parsing
parser = argparse.ArgumentParser(description='Typhoon Analysis Dashboard')
parser.add_argument('--data_path', type=str, default=os.getcwd(), help='Path to the data directory')
args = parser.parse_args()
DATA_PATH = args.data_path
ONI_DATA_PATH = os.path.join(DATA_PATH, 'oni_data.csv')
TYPHOON_DATA_PATH = os.path.join(DATA_PATH, 'processed_typhoon_data.csv')
LOCAL_iBtrace_PATH = os.path.join(DATA_PATH, 'ibtracs.WP.list.v04r01.csv')
iBtrace_uri = 'https://www.ncei.noaa.gov/data/international-best-track-archive-for-climate-stewardship-ibtracs/v04r01/access/csv/ibtracs.WP.list.v04r01.csv'
CACHE_FILE = 'ibtracs_cache.pkl'
CACHE_EXPIRY_DAYS = 1
# Color maps for Plotly (RGB)
color_map = {
'C5 Super Typhoon': 'rgb(255, 0, 0)',
'C4 Very Strong Typhoon': 'rgb(255, 165, 0)',
'C3 Strong Typhoon': 'rgb(255, 255, 0)',
'C2 Typhoon': 'rgb(0, 255, 0)',
'C1 Typhoon': 'rgb(0, 255, 255)',
'Tropical Storm': 'rgb(0, 0, 255)',
'Tropical Depression': 'rgb(128, 128, 128)'
}
# Classification standards with distinct colors for Matplotlib
atlantic_standard = {
'C5 Super Typhoon': {'wind_speed': 137, 'color': 'Red', 'hex': '#FF0000'},
'C4 Very Strong Typhoon': {'wind_speed': 113, 'color': 'Orange', 'hex': '#FFA500'},
'C3 Strong Typhoon': {'wind_speed': 96, 'color': 'Yellow', 'hex': '#FFFF00'},
'C2 Typhoon': {'wind_speed': 83, 'color': 'Green', 'hex': '#00FF00'},
'C1 Typhoon': {'wind_speed': 64, 'color': 'Cyan', 'hex': '#00FFFF'},
'Tropical Storm': {'wind_speed': 34, 'color': 'Blue', 'hex': '#0000FF'},
'Tropical Depression': {'wind_speed': 0, 'color': 'Gray', 'hex': '#808080'}
}
taiwan_standard = {
'Strong Typhoon': {'wind_speed': 51.0, 'color': 'Red', 'hex': '#FF0000'},
'Medium Typhoon': {'wind_speed': 33.7, 'color': 'Orange', 'hex': '#FFA500'},
'Mild Typhoon': {'wind_speed': 17.2, 'color': 'Yellow', 'hex': '#FFFF00'},
'Tropical Depression': {'wind_speed': 0, 'color': 'Gray', 'hex': '#808080'}
}
# Data loading and preprocessing functions
def download_oni_file(url, filename):
response = requests.get(url)
response.raise_for_status()
with open(filename, 'wb') as f:
f.write(response.content)
return True
def convert_oni_ascii_to_csv(input_file, output_file):
data = defaultdict(lambda: [''] * 12)
season_to_month = {'DJF': 12, 'JFM': 1, 'FMA': 2, 'MAM': 3, 'AMJ': 4, 'MJJ': 5,
'JJA': 6, 'JAS': 7, 'ASO': 8, 'SON': 9, 'OND': 10, 'NDJ': 11}
with open(input_file, 'r') as f:
lines = f.readlines()[1:]
for line in lines:
parts = line.split()
if len(parts) >= 4:
season, year, anom = parts[0], parts[1], parts[-1]
if season in season_to_month:
month = season_to_month[season]
if season == 'DJF':
year = str(int(year) - 1)
data[year][month-1] = anom
with open(output_file, 'w', newline='') as f:
writer = csv.writer(f)
writer.writerow(['Year', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'])
for year in sorted(data.keys()):
writer.writerow([year] + data[year])
def update_oni_data():
url = "https://www.cpc.ncep.noaa.gov/data/indices/oni.ascii.txt"
temp_file = os.path.join(DATA_PATH, "temp_oni.ascii.txt")
input_file = os.path.join(DATA_PATH, "oni.ascii.txt")
output_file = ONI_DATA_PATH
if download_oni_file(url, temp_file):
if not os.path.exists(input_file) or not filecmp.cmp(temp_file, input_file):
os.replace(temp_file, input_file)
convert_oni_ascii_to_csv(input_file, output_file)
else:
os.remove(temp_file)
def load_ibtracs_data():
if os.path.exists(CACHE_FILE) and (datetime.now() - datetime.fromtimestamp(os.path.getmtime(CACHE_FILE))).days < CACHE_EXPIRY_DAYS:
with open(CACHE_FILE, 'rb') as f:
return pickle.load(f)
if os.path.exists(LOCAL_iBtrace_PATH):
ibtracs = tracks.TrackDataset(basin='west_pacific', source='ibtracs', ibtracs_url=LOCAL_iBtrace_PATH)
else:
response = requests.get(iBtrace_uri)
response.raise_for_status()
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.csv') as temp_file:
temp_file.write(response.text)
shutil.move(temp_file.name, LOCAL_iBtrace_PATH)
ibtracs = tracks.TrackDataset(basin='west_pacific', source='ibtracs', ibtracs_url=LOCAL_iBtrace_PATH)
with open(CACHE_FILE, 'wb') as f:
pickle.dump(ibtracs, f)
return ibtracs
def convert_typhoondata(input_file, output_file):
with open(input_file, 'r') as infile:
next(infile); next(infile)
reader = csv.reader(infile)
sid_data = defaultdict(list)
for row in reader:
if row:
sid = row[0]
sid_data[sid].append((row, row[6]))
with open(output_file, 'w', newline='') as outfile:
fieldnames = ['SID', 'ISO_TIME', 'LAT', 'LON', 'SEASON', 'NAME', 'WMO_WIND', 'WMO_PRES', 'USA_WIND', 'USA_PRES', 'START_DATE', 'END_DATE']
writer = csv.DictWriter(outfile, fieldnames=fieldnames)
writer.writeheader()
for sid, data in sid_data.items():
start_date = min(data, key=lambda x: x[1])[1]
end_date = max(data, key=lambda x: x[1])[1]
for row, iso_time in data:
writer.writerow({
'SID': row[0], 'ISO_TIME': iso_time, 'LAT': row[8], 'LON': row[9], 'SEASON': row[1], 'NAME': row[5],
'WMO_WIND': row[10].strip() or ' ', 'WMO_PRES': row[11].strip() or ' ',
'USA_WIND': row[23].strip() or ' ', 'USA_PRES': row[24].strip() or ' ',
'START_DATE': start_date, 'END_DATE': end_date
})
def load_data(oni_path, typhoon_path):
oni_data = pd.read_csv(oni_path)
typhoon_data = pd.read_csv(typhoon_path, low_memory=False)
typhoon_data['ISO_TIME'] = pd.to_datetime(typhoon_data['ISO_TIME'], errors='coerce')
typhoon_data = typhoon_data.dropna(subset=['ISO_TIME'])
return oni_data, typhoon_data
def process_oni_data(oni_data):
oni_long = oni_data.melt(id_vars=['Year'], var_name='Month', value_name='ONI')
month_map = {'Jan': '01', 'Feb': '02', 'Mar': '03', 'Apr': '04', 'May': '05', 'Jun': '06',
'Jul': '07', 'Aug': '08', 'Sep': '09', 'Oct': '10', 'Nov': '11', 'Dec': '12'}
oni_long['Month'] = oni_long['Month'].map(month_map)
oni_long['Date'] = pd.to_datetime(oni_long['Year'].astype(str) + '-' + oni_long['Month'] + '-01')
oni_long['ONI'] = pd.to_numeric(oni_long['ONI'], errors='coerce')
return oni_long
def process_typhoon_data(typhoon_data):
typhoon_data['ISO_TIME'] = pd.to_datetime(typhoon_data['ISO_TIME'], errors='coerce')
typhoon_data['USA_WIND'] = pd.to_numeric(typhoon_data['USA_WIND'], errors='coerce')
typhoon_data['USA_PRES'] = pd.to_numeric(typhoon_data['USA_PRES'], errors='coerce')
typhoon_data['LON'] = pd.to_numeric(typhoon_data['LON'], errors='coerce')
typhoon_max = typhoon_data.groupby('SID').agg({
'USA_WIND': 'max', 'USA_PRES': 'min', 'ISO_TIME': 'first', 'SEASON': 'first', 'NAME': 'first',
'LAT': 'first', 'LON': 'first'
}).reset_index()
typhoon_max['Month'] = typhoon_max['ISO_TIME'].dt.strftime('%m')
typhoon_max['Year'] = typhoon_max['ISO_TIME'].dt.year
typhoon_max['Category'] = typhoon_max['USA_WIND'].apply(categorize_typhoon)
return typhoon_max
def merge_data(oni_long, typhoon_max):
return pd.merge(typhoon_max, oni_long, on=['Year', 'Month'])
def categorize_typhoon(wind_speed):
wind_speed_kt = wind_speed
if wind_speed_kt >= 137:
return 'C5 Super Typhoon'
elif wind_speed_kt >= 113:
return 'C4 Very Strong Typhoon'
elif wind_speed_kt >= 96:
return 'C3 Strong Typhoon'
elif wind_speed_kt >= 83:
return 'C2 Typhoon'
elif wind_speed_kt >= 64:
return 'C1 Typhoon'
elif wind_speed_kt >= 34:
return 'Tropical Storm'
else:
return 'Tropical Depression'
def classify_enso_phases(oni_value):
if isinstance(oni_value, pd.Series):
oni_value = oni_value.iloc[0]
if oni_value >= 0.5:
return 'El Nino'
elif oni_value <= -0.5:
return 'La Nina'
else:
return 'Neutral'
# Load data globally
update_oni_data()
ibtracs = load_ibtracs_data()
convert_typhoondata(LOCAL_iBtrace_PATH, TYPHOON_DATA_PATH)
oni_data, typhoon_data = load_data(ONI_DATA_PATH, TYPHOON_DATA_PATH)
oni_long = process_oni_data(oni_data)
typhoon_max = process_typhoon_data(typhoon_data)
merged_data = merge_data(oni_long, typhoon_max)
# Main analysis functions (using Plotly)
def generate_typhoon_tracks(filtered_data, typhoon_search):
fig = go.Figure()
for sid in filtered_data['SID'].unique():
storm_data = filtered_data[filtered_data['SID'] == sid]
color = {'El Nino': 'red', 'La Nina': 'blue', 'Neutral': 'green'}[storm_data['ENSO_Phase'].iloc[0]]
fig.add_trace(go.Scattergeo(
lon=storm_data['LON'], lat=storm_data['LAT'], mode='lines',
name=storm_data['NAME'].iloc[0], line=dict(width=2, color=color)
))
if typhoon_search:
mask = filtered_data['NAME'].str.contains(typhoon_search, case=False, na=False)
if mask.any():
storm_data = filtered_data[mask]
fig.add_trace(go.Scattergeo(
lon=storm_data['LON'], lat=storm_data['LAT'], mode='lines',
name=f'Matched: {typhoon_search}', line=dict(width=5, color='yellow')
))
fig.update_layout(
title='Typhoon Tracks',
geo=dict(projection_type='natural earth', showland=True),
height=700
)
return fig
def generate_wind_oni_scatter(filtered_data, typhoon_search):
fig = px.scatter(filtered_data, x='ONI', y='USA_WIND', color='Category', hover_data=['NAME', 'Year', 'Category'],
title='Wind Speed vs ONI', labels={'ONI': 'ONI Value', 'USA_WIND': 'Max Wind Speed (knots)'},
color_discrete_map=color_map)
if typhoon_search:
mask = filtered_data['NAME'].str.contains(typhoon_search, case=False, na=False)
if mask.any():
fig.add_trace(go.Scatter(
x=filtered_data.loc[mask, 'ONI'], y=filtered_data.loc[mask, 'USA_WIND'],
mode='markers', marker=dict(size=10, color='red', symbol='star'),
name=f'Matched: {typhoon_search}',
text=filtered_data.loc[mask, 'NAME'] + ' (' + filtered_data.loc[mask, 'Year'].astype(str) + ')'
))
return fig
def generate_pressure_oni_scatter(filtered_data, typhoon_search):
fig = px.scatter(filtered_data, x='ONI', y='USA_PRES', color='Category', hover_data=['NAME', 'Year', 'Category'],
title='Pressure vs ONI', labels={'ONI': 'ONI Value', 'USA_PRES': 'Min Pressure (hPa)'},
color_discrete_map=color_map)
if typhoon_search:
mask = filtered_data['NAME'].str.contains(typhoon_search, case=False, na=False)
if mask.any():
fig.add_trace(go.Scatter(
x=filtered_data.loc[mask, 'ONI'], y=filtered_data.loc[mask, 'USA_PRES'],
mode='markers', marker=dict(size=10, color='red', symbol='star'),
name=f'Matched: {typhoon_search}',
text=filtered_data.loc[mask, 'NAME'] + ' (' + filtered_data.loc[mask, 'Year'].astype(str) + ')'
))
return fig
def generate_regression_analysis(filtered_data):
fig = px.scatter(filtered_data, x='LON', y='ONI', hover_data=['NAME'],
title='Typhoon Generation Longitude vs ONI (All Years)')
if len(filtered_data) > 1:
X = np.array(filtered_data['LON']).reshape(-1, 1)
y = filtered_data['ONI']
model = sm.OLS(y, sm.add_constant(X)).fit()
y_pred = model.predict(sm.add_constant(X))
fig.add_trace(go.Scatter(x=filtered_data['LON'], y=y_pred, mode='lines', name='Regression Line'))
slope = model.params[1]
slopes_text = f"All Years Slope: {slope:.4f}"
else:
slopes_text = "Insufficient data for regression"
return fig, slopes_text
def generate_main_analysis(start_year, start_month, end_year, end_month, enso_phase, typhoon_search):
start_date = datetime(start_year, start_month, 1)
end_date = datetime(end_year, end_month, 28)
filtered_data = merged_data[
(merged_data['ISO_TIME'] >= start_date) &
(merged_data['ISO_TIME'] <= end_date)
]
filtered_data['ENSO_Phase'] = filtered_data['ONI'].apply(classify_enso_phases)
if enso_phase != 'all':
filtered_data = filtered_data[filtered_data['ENSO_Phase'] == enso_phase.capitalize()]
tracks_fig = generate_typhoon_tracks(filtered_data, typhoon_search)
wind_scatter = generate_wind_oni_scatter(filtered_data, typhoon_search)
pressure_scatter = generate_pressure_oni_scatter(filtered_data, typhoon_search)
regression_fig, slopes_text = generate_regression_analysis(filtered_data)
return tracks_fig, wind_scatter, pressure_scatter, regression_fig, slopes_text
# Video animation function with fixed sidebar
def categorize_typhoon_by_standard(wind_speed, standard):
if standard == 'taiwan':
wind_speed_ms = wind_speed * 0.514444
if wind_speed_ms >= 51.0:
return 'Strong Typhoon', taiwan_standard['Strong Typhoon']['hex']
elif wind_speed_ms >= 33.7:
return 'Medium Typhoon', taiwan_standard['Medium Typhoon']['hex']
elif wind_speed_ms >= 17.2:
return 'Mild Typhoon', taiwan_standard['Mild Typhoon']['hex']
return 'Tropical Depression', taiwan_standard['Tropical Depression']['hex']
else:
if wind_speed >= 137:
return 'C5 Super Typhoon', atlantic_standard['C5 Super Typhoon']['hex']
elif wind_speed >= 113:
return 'C4 Very Strong Typhoon', atlantic_standard['C4 Very Strong Typhoon']['hex']
elif wind_speed >= 96:
return 'C3 Strong Typhoon', atlantic_standard['C3 Strong Typhoon']['hex']
elif wind_speed >= 83:
return 'C2 Typhoon', atlantic_standard['C2 Typhoon']['hex']
elif wind_speed >= 64:
return 'C1 Typhoon', atlantic_standard['C1 Typhoon']['hex']
elif wind_speed >= 34:
return 'Tropical Storm', atlantic_standard['Tropical Storm']['hex']
return 'Tropical Depression', atlantic_standard['Tropical Depression']['hex']
def generate_track_video(year, typhoon, standard):
if not typhoon:
return None
typhoon_id = typhoon.split('(')[-1].strip(')')
storm = ibtracs.get_storm(typhoon_id)
# Map focus
min_lat, max_lat = min(storm.lat), max(storm.lat)
min_lon, max_lon = min(storm.lon), max(storm.lon)
lat_padding = max((max_lat - min_lat) * 0.3, 5)
lon_padding = max((max_lon - min_lon) * 0.3, 5)
# Set up the figure (900x700 pixels at 100 DPI)
fig = plt.figure(figsize=(9, 7), dpi=100)
ax = plt.axes([0.05, 0.05, 0.65, 0.90], projection=ccrs.PlateCarree()) # Adjusted to leave space for sidebar
ax.set_extent([min_lon - lon_padding, max_lon + lon_padding, min_lat - lat_padding, max_lat + lat_padding], crs=ccrs.PlateCarree())
# Add world map features
ax.add_feature(cfeature.LAND, facecolor='lightgray')
ax.add_feature(cfeature.OCEAN, facecolor='lightblue')
ax.add_feature(cfeature.COASTLINE, edgecolor='black')
ax.add_feature(cfeature.BORDERS, linestyle=':', edgecolor='gray')
ax.gridlines(draw_labels=True, linestyle='--', color='gray', alpha=0.5)
ax.set_title(f"{year} {storm.name} Typhoon Path")
# Initialize the line and point
line, = ax.plot([], [], 'b-', linewidth=2, transform=ccrs.PlateCarree())
point, = ax.plot([], [], 'o', markersize=8, transform=ccrs.PlateCarree())
date_text = ax.text(0.02, 0.02, '', transform=ax.transAxes, fontsize=10, bbox=dict(facecolor='white', alpha=0.8))
# Add sidebar on the right
details_title = fig.text(0.75, 0.95, "Typhoon Details", fontsize=12, fontweight='bold', verticalalignment='top')
details_text = fig.text(0.75, 0.85, '', fontsize=10, verticalalignment='top',
bbox=dict(facecolor='white', alpha=0.8, boxstyle='round,pad=0.5'))
# Add color legend
standard_dict = atlantic_standard if standard == 'atlantic' else taiwan_standard
legend_elements = [plt.Line2D([0], [0], marker='o', color='w', label=f"{cat}",
markerfacecolor=details['hex'], markersize=10)
for cat, details in standard_dict.items()]
fig.legend(handles=legend_elements, title="Color Legend", loc='lower right',
bbox_to_anchor=(0.95, 0.05), fontsize=10)
def init():
line.set_data([], [])
point.set_data([], [])
date_text.set_text('')
details_text.set_text('')
return line, point, date_text, details_text
def update(frame):
line.set_data(storm.lon[:frame+1], storm.lat[:frame+1])
category, color = categorize_typhoon_by_standard(storm.vmax[frame], standard)
point.set_data([storm.lon[frame]], [storm.lat[frame]])
point.set_color(color)
date_text.set_text(storm.time[frame].strftime('%Y-%m-%d %H:%M'))
details = f"Name: {storm.name}\n" \
f"Date: {storm.time[frame].strftime('%Y-%m-%d %H:%M')}\n" \
f"Wind Speed: {storm.vmax[frame]:.1f} kt\n" \
f"Category: {category}"
details_text.set_text(details)
return line, point, date_text, details_text
ani = animation.FuncAnimation(fig, update, init_func=init, frames=len(storm.time),
interval=200, blit=True, repeat=True)
# Save as video
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.mp4')
writer = animation.FFMpegWriter(fps=5, bitrate=1800)
ani.save(temp_file.name, writer=writer)
plt.close(fig)
return temp_file.name
# Logistic regression functions
def perform_wind_regression(start_year, start_month, end_year, end_month):
start_date = datetime(start_year, start_month, 1)
end_date = datetime(end_year, end_month, 28)
data = merged_data[(merged_data['ISO_TIME'] >= start_date) & (merged_data['ISO_TIME'] <= end_date)].dropna(subset=['USA_WIND', 'ONI'])
data['severe_typhoon'] = (data['USA_WIND'] >= 64).astype(int)
X = sm.add_constant(data['ONI'])
y = data['severe_typhoon']
model = sm.Logit(y, X).fit()
beta_1, exp_beta_1, p_value = model.params['ONI'], np.exp(model.params['ONI']), model.pvalues['ONI']
return f"Wind Regression: β1={beta_1:.4f}, Odds Ratio={exp_beta_1:.4f}, P-value={p_value:.4f}"
def perform_pressure_regression(start_year, start_month, end_year, end_month):
start_date = datetime(start_year, start_month, 1)
end_date = datetime(end_year, end_month, 28)
data = merged_data[(merged_data['ISO_TIME'] >= start_date) & (merged_data['ISO_TIME'] <= end_date)].dropna(subset=['USA_PRES', 'ONI'])
data['intense_typhoon'] = (data['USA_PRES'] <= 950).astype(int)
X = sm.add_constant(data['ONI'])
y = data['intense_typhoon']
model = sm.Logit(y, X).fit()
beta_1, exp_beta_1, p_value = model.params['ONI'], np.exp(model.params['ONI']), model.pvalues['ONI']
return f"Pressure Regression: β1={beta_1:.4f}, Odds Ratio={exp_beta_1:.4f}, P-value={p_value:.4f}"
def perform_longitude_regression(start_year, start_month, end_year, end_month):
start_date = datetime(start_year, start_month, 1)
end_date = datetime(end_year, end_month, 28)
data = merged_data[(merged_data['ISO_TIME'] >= start_date) & (merged_data['ISO_TIME'] <= end_date)].dropna(subset=['LON', 'ONI'])
data['western_typhoon'] = (data['LON'] <= 140).astype(int)
X = sm.add_constant(data['ONI'])
y = data['western_typhoon']
model = sm.Logit(y, X).fit()
beta_1, exp_beta_1, p_value = model.params['ONI'], np.exp(model.params['ONI']), model.pvalues['ONI']
return f"Longitude Regression: β1={beta_1:.4f}, Odds Ratio={exp_beta_1:.4f}, P-value={p_value:.4f}"
# t-SNE clustering functions
def filter_west_pacific_coordinates(lons, lats):
mask = (lons >= 100) & (lons <= 180) & (lats >= 0) & (lats <= 50)
return lons[mask], lats[mask]
def update_route_clusters(start_year, start_month, end_year, end_month, enso_value, season):
start_date = datetime(int(start_year), int(start_month), 1)
end_date = datetime(int(end_year), int(end_month), 28)
all_storms_data = []
for year in range(int(start_year), int(end_year) + 1):
season_data = ibtracs.get_season(year)
for storm_id in season_data.summary()['id']:
storm = ibtracs.get_storm(storm_id)
if storm.time[0] >= start_date and storm.time[-1] <= end_date:
lons, lats = filter_west_pacific_coordinates(np.array(storm.lon), np.array(storm.lat))
if len(lons) > 1:
all_storms_data.append((lons, lats, np.array(storm.vmax), np.array(storm.mslp), np.array(storm.time), storm.name))
if not all_storms_data:
return go.Figure(), go.Figure(), go.Figure(), "No storms found in the selected period."
# Prepare route vectors for t-SNE
max_length = max(len(st[0]) for st in all_storms_data)
route_vectors = []
for lons, lats, _, _, _, _ in all_storms_data:
interp_lons = np.interp(np.linspace(0, 1, max_length), np.linspace(0, 1, len(lons)), lons)
interp_lats = np.interp(np.linspace(0, 1, max_length), np.linspace(0, 1, len(lats)), lats)
route_vectors.append(np.column_stack((interp_lons, interp_lats)).flatten())
route_vectors = np.array(route_vectors)
# Perform t-SNE
tsne_results = TSNE(n_components=2, random_state=42, perplexity=min(30, len(route_vectors)-1)).fit_transform(route_vectors)
# Dynamic DBSCAN clustering
target_clusters = min(5, len(all_storms_data) // 3)
eps_range = np.arange(5.0, 50.0, 5.0)
min_samples = max(3, len(all_storms_data) // 20)
best_labels = None
best_eps = None
best_n_clusters = 0
best_noise_ratio = 1.0
for eps in eps_range:
dbscan = DBSCAN(eps=eps, min_samples=min_samples)
labels = dbscan.fit_predict(tsne_results)
n_clusters = len(set(labels)) - (1 if -1 in labels else 0)
noise_points = np.sum(labels == -1)
noise_ratio = noise_points / len(labels)
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)):
best_labels = labels
best_eps = eps
best_n_clusters = n_clusters
best_noise_ratio = noise_ratio
if best_labels is None:
dbscan = DBSCAN(eps=5.0, min_samples=min_samples)
best_labels = dbscan.fit_predict(tsne_results)
best_eps = 5.0
best_n_clusters = len(set(best_labels)) - (1 if -1 in best_labels else 0)
# t-SNE Scatter Plot
fig_tsne = go.Figure()
for cluster in set(best_labels):
mask = best_labels == cluster
name = "Noise" if cluster == -1 else f"Cluster {cluster}"
fig_tsne.add_trace(go.Scatter(
x=tsne_results[mask, 0], y=tsne_results[mask, 1], mode='markers',
name=name, text=[all_storms_data[i][5] for i in range(len(all_storms_data)) if mask[i]],
hoverinfo='text'
))
fig_tsne.update_layout(title="t-SNE Clustering of Typhoon Routes", xaxis_title="t-SNE 1", yaxis_title="t-SNE 2")
# Typhoon Routes Plot
fig_routes = go.Figure()
for i, (lons, lats, _, _, _, name) in enumerate(all_storms_data):
cluster = best_labels[i]
color = 'gray' if cluster == -1 else px.colors.qualitative.Plotly[cluster % len(px.colors.qualitative.Plotly)]
fig_routes.add_trace(go.Scattergeo(
lon=lons, lat=lats, mode='lines+markers', name=name,
line=dict(color=color), marker=dict(size=4), hoverinfo='text', text=name
))
fig_routes.update_layout(
title="Typhoon Routes by Cluster",
geo=dict(scope='asia', projection_type='mercator', showland=True, landcolor='lightgray')
)
# Cluster Statistics Plot
cluster_stats = []
for cluster in set(best_labels) - {-1}:
mask = best_labels == cluster
winds = [all_storms_data[i][2].max() for i in range(len(all_storms_data)) if mask[i]]
pressures = [all_storms_data[i][3].min() for i in range(len(all_storms_data)) if mask[i]]
cluster_stats.append({
'Cluster': cluster,
'Count': np.sum(mask),
'Mean Wind': np.mean(winds),
'Mean Pressure': np.mean(pressures)
})
stats_df = pd.DataFrame(cluster_stats)
fig_stats = px.bar(stats_df, x='Cluster', y=['Mean Wind', 'Mean Pressure'], barmode='group',
title="Cluster Statistics (Mean Wind Speed and Pressure)")
# Cluster Information
cluster_info = f"Number of Clusters: {best_n_clusters}\nBest EPS: {best_eps}\nNoise Points: {best_noise_ratio*100:.1f}%"
for stat in cluster_stats:
cluster_info += f"\nCluster {stat['Cluster']}: {stat['Count']} storms, Mean Wind: {stat['Mean Wind']:.1f} kt, Mean Pressure: {stat['Mean Pressure']:.1f} hPa"
return fig_tsne, fig_routes, fig_stats, cluster_info
# Gradio Interface
with gr.Blocks(title="Typhoon Analysis Dashboard") as demo:
gr.Markdown("# Typhoon Analysis Dashboard")
with gr.Tab("Overview"):
gr.Markdown("""
## Welcome to the Typhoon Analysis Dashboard
This dashboard allows you to analyze typhoon data in relation to ENSO phases.
### Features:
- **Track Visualization**: View typhoon tracks by time period and ENSO phase
- **Wind Analysis**: Examine wind speed vs ONI relationships
- **Pressure Analysis**: Analyze pressure vs ONI relationships
- **Longitude Analysis**: Study typhoon generation longitude vs ONI
- **Path Animation**: Watch animated typhoon paths with a sidebar
- **TSNE Cluster**: Perform t-SNE clustering on typhoon routes
Select a tab above to begin your analysis.
""")
with gr.Tab("Track Visualization"):
with gr.Row():
start_year = gr.Number(label="Start Year", value=2000, minimum=1900, maximum=2024, step=1)
start_month = gr.Dropdown(label="Start Month", choices=list(range(1, 13)), value=1)
end_year = gr.Number(label="End Year", value=2024, minimum=1900, maximum=2024, step=1)
end_month = gr.Dropdown(label="End Month", choices=list(range(1, 13)), value=6)
enso_phase = gr.Dropdown(label="ENSO Phase", choices=['all', 'El Nino', 'La Nina', 'Neutral'], value='all')
typhoon_search = gr.Textbox(label="Typhoon Search")
analyze_btn = gr.Button("Generate Tracks")
tracks_plot = gr.Plot(label="Typhoon Tracks", elem_id="tracks_plot")
typhoon_count = gr.Textbox(label="Number of Typhoons Displayed")
def get_full_tracks(start_year, start_month, end_year, end_month, enso_phase, typhoon_search):
start_date = datetime(start_year, start_month, 1)
end_date = datetime(end_year, end_month, 28)
filtered_data = merged_data[
(merged_data['ISO_TIME'] >= start_date) &
(merged_data['ISO_TIME'] <= end_date)
]
filtered_data['ENSO_Phase'] = filtered_data['ONI'].apply(classify_enso_phases)
if enso_phase != 'all':
filtered_data = filtered_data[filtered_data['ENSO_Phase'] == enso_phase.capitalize()]
unique_storms = filtered_data['SID'].unique()
count = len(unique_storms)
fig = go.Figure()
for sid in unique_storms:
storm_data = typhoon_data[typhoon_data['SID'] == sid]
name = storm_data['NAME'].iloc[0] if not pd.isna(storm_data['NAME'].iloc[0]) else "Unnamed"
storm_oni = filtered_data[filtered_data['SID'] == sid]['ONI'].iloc[0]
color = 'red' if storm_oni >= 0.5 else ('blue' if storm_oni <= -0.5 else 'green')
fig.add_trace(go.Scattergeo(
lon=storm_data['LON'], lat=storm_data['LAT'], mode='lines',
name=f"{name} ({storm_data['SEASON'].iloc[0]})",
line=dict(width=1.5, color=color),
hoverinfo="name"
))
if typhoon_search:
search_mask = typhoon_data['NAME'].str.contains(typhoon_search, case=False, na=False)
if search_mask.any():
for sid in typhoon_data[search_mask]['SID'].unique():
storm_data = typhoon_data[typhoon_data['SID'] == sid]
fig.add_trace(go.Scattergeo(
lon=storm_data['LON'], lat=storm_data['LAT'], mode='lines+markers',
name=f"MATCHED: {storm_data['NAME'].iloc[0]} ({storm_data['SEASON'].iloc[0]})",
line=dict(width=3, color='yellow'),
marker=dict(size=5),
hoverinfo="name"
))
fig.update_layout(
title=f"Typhoon Tracks ({start_year}-{start_month} to {end_year}-{end_month})",
geo=dict(
projection_type='natural earth',
showland=True,
showcoastlines=True,
landcolor='rgb(243, 243, 243)',
countrycolor='rgb(204, 204, 204)',
coastlinecolor='rgb(204, 204, 204)',
center=dict(lon=140, lat=20),
projection_scale=3
),
legend_title="Typhoons by ENSO Phase",
showlegend=True,
height=700
)
fig.add_annotation(
x=0.02, y=0.98, xref="paper", yref="paper",
text="Red: El Niño, Blue: La Niña, Green: Neutral",
showarrow=False, align="left",
bgcolor="rgba(255,255,255,0.8)"
)
return fig, f"Total typhoons displayed: {count}"
analyze_btn.click(
fn=get_full_tracks,
inputs=[start_year, start_month, end_year, end_month, enso_phase, typhoon_search],
outputs=[tracks_plot, typhoon_count]
)
with gr.Tab("Wind Analysis"):
with gr.Row():
wind_start_year = gr.Number(label="Start Year", value=2000, minimum=1900, maximum=2024, step=1)
wind_start_month = gr.Dropdown(label="Start Month", choices=list(range(1, 13)), value=1)
wind_end_year = gr.Number(label="End Year", value=2024, minimum=1900, maximum=2024, step=1)
wind_end_month = gr.Dropdown(label="End Month", choices=list(range(1, 13)), value=6)
wind_enso_phase = gr.Dropdown(label="ENSO Phase", choices=['all', 'El Nino', 'La Nina', 'Neutral'], value='all')
wind_typhoon_search = gr.Textbox(label="Typhoon Search")
wind_analyze_btn = gr.Button("Generate Wind Analysis")
wind_scatter = gr.Plot(label="Wind Speed vs ONI")
wind_regression_results = gr.Textbox(label="Wind Regression Results")
def get_wind_analysis(start_year, start_month, end_year, end_month, enso_phase, typhoon_search):
results = generate_main_analysis(start_year, start_month, end_year, end_month, enso_phase, typhoon_search)
regression = perform_wind_regression(start_year, start_month, end_year, end_month)
return results[1], regression
wind_analyze_btn.click(
fn=get_wind_analysis,
inputs=[wind_start_year, wind_start_month, wind_end_year, wind_end_month, wind_enso_phase, wind_typhoon_search],
outputs=[wind_scatter, wind_regression_results]
)
with gr.Tab("Pressure Analysis"):
with gr.Row():
pressure_start_year = gr.Number(label="Start Year", value=2000, minimum=1900, maximum=2024, step=1)
pressure_start_month = gr.Dropdown(label="Start Month", choices=list(range(1, 13)), value=1)
pressure_end_year = gr.Number(label="End Year", value=2024, minimum=1900, maximum=2024, step=1)
pressure_end_month = gr.Dropdown(label="End Month", choices=list(range(1, 13)), value=6)
pressure_enso_phase = gr.Dropdown(label="ENSO Phase", choices=['all', 'El Nino', 'La Nina', 'Neutral'], value='all')
pressure_typhoon_search = gr.Textbox(label="Typhoon Search")
pressure_analyze_btn = gr.Button("Generate Pressure Analysis")
pressure_scatter = gr.Plot(label="Pressure vs ONI")
pressure_regression_results = gr.Textbox(label="Pressure Regression Results")
def get_pressure_analysis(start_year, start_month, end_year, end_month, enso_phase, typhoon_search):
results = generate_main_analysis(start_year, start_month, end_year, end_month, enso_phase, typhoon_search)
regression = perform_pressure_regression(start_year, start_month, end_year, end_month)
return results[2], regression
pressure_analyze_btn.click(
fn=get_pressure_analysis,
inputs=[pressure_start_year, pressure_start_month, pressure_end_year, pressure_end_month, pressure_enso_phase, pressure_typhoon_search],
outputs=[pressure_scatter, pressure_regression_results]
)
with gr.Tab("Longitude Analysis"):
with gr.Row():
lon_start_year = gr.Number(label="Start Year", value=2000, minimum=1900, maximum=2024, step=1)
lon_start_month = gr.Dropdown(label="Start Month", choices=list(range(1, 13)), value=1)
lon_end_year = gr.Number(label="End Year", value=2024, minimum=1900, maximum=2024, step=1)
lon_end_month = gr.Dropdown(label="End Month", choices=list(range(1, 13)), value=6)
lon_enso_phase = gr.Dropdown(label="ENSO Phase", choices=['all', 'El Nino', 'La Nina', 'Neutral'], value='all')
lon_typhoon_search = gr.Textbox(label="Typhoon Search (Optional)")
lon_analyze_btn = gr.Button("Generate Longitude Analysis")
regression_plot = gr.Plot(label="Longitude vs ONI")
slopes_text = gr.Textbox(label="Regression Slopes")
lon_regression_results = gr.Textbox(label="Longitude Regression Results")
def get_longitude_analysis(start_year, start_month, end_year, end_month, enso_phase, typhoon_search):
results = generate_main_analysis(start_year, start_month, end_year, end_month, enso_phase, typhoon_search)
regression = perform_longitude_regression(start_year, start_month, end_year, end_month)
return results[3], results[4], regression
lon_analyze_btn.click(
fn=get_longitude_analysis,
inputs=[lon_start_year, lon_start_month, lon_end_year, lon_end_month, lon_enso_phase, lon_typhoon_search],
outputs=[regression_plot, slopes_text, lon_regression_results]
)
with gr.Tab("Typhoon Path Animation"):
with gr.Row():
year_dropdown = gr.Dropdown(label="Year", choices=[str(y) for y in range(1950, 2025)], value="2024")
typhoon_dropdown = gr.Dropdown(label="Typhoon")
standard_dropdown = gr.Dropdown(label="Classification Standard", choices=['atlantic', 'taiwan'], value='atlantic')
animate_btn = gr.Button("Generate Animation")
path_video = gr.Video(label="Typhoon Path Animation", elem_id="path_video")
animation_info = gr.Markdown("""
### Animation Instructions
1. Select a year and typhoon from the dropdowns
2. Choose a classification standard (Atlantic or Taiwan)
3. Click "Generate Animation"
4. Use the video player's built-in controls to play, pause, or scrub through the animation
5. The animation shows the typhoon track growing over a world map, with:
- Date on the bottom left
- Sidebar on the right showing typhoon details (name, date, wind speed, category) as it moves
- Color legend with colored markers on the bottom right
""")
def update_typhoon_options(year):
season = ibtracs.get_season(int(year))
storm_summary = season.summary()
options = [f"{storm_summary['name'][i]} ({storm_summary['id'][i]})" for i in range(storm_summary['season_storms'])]
return gr.update(choices=options, value=options[0] if options else None)
year_dropdown.change(fn=update_typhoon_options, inputs=year_dropdown, outputs=typhoon_dropdown)
animate_btn.click(
fn=generate_track_video,
inputs=[year_dropdown, typhoon_dropdown, standard_dropdown],
outputs=path_video
)
with gr.Tab("TSNE Cluster"):
with gr.Row():
tsne_start_year = gr.Number(label="Start Year", value=2000, minimum=1900, maximum=2024, step=1)
tsne_start_month = gr.Dropdown(label="Start Month", choices=list(range(1, 13)), value=1)
tsne_end_year = gr.Number(label="End Year", value=2024, minimum=1900, maximum=2024, step=1)
tsne_end_month = gr.Dropdown(label="End Month", choices=list(range(1, 13)), value=12)
tsne_enso_phase = gr.Dropdown(label="ENSO Phase", choices=['all', 'El Nino', 'La Nina', 'Neutral'], value='all')
tsne_season = gr.Dropdown(label="Season", choices=['all', 'summer', 'winter'], value='all')
tsne_analyze_btn = gr.Button("Analyze")
tsne_plot = gr.Plot(label="t-SNE Clusters")
routes_plot = gr.Plot(label="Typhoon Routes")
stats_plot = gr.Plot(label="Cluster Statistics")
cluster_info = gr.Textbox(label="Cluster Information", lines=10)
tsne_analyze_btn.click(
fn=update_route_clusters,
inputs=[tsne_start_year, tsne_start_month, tsne_end_year, tsne_end_month, tsne_enso_phase, tsne_season],
outputs=[tsne_plot, routes_plot, stats_plot, cluster_info]
)
# Custom CSS for better visibility
gr.HTML("""
<style>
#tracks_plot, #path_video {
height: 700px !important;
width: 100%;
}
.plot-container {
min-height: 600px;
}
.gr-plotly {
width: 100% !important;
}
</style>
""")
demo.launch(share=True)