euler314's picture
Update app.py
dd02776 verified
raw
history blame
41.6 kB
import gradio as gr
import plotly.graph_objects as go
import plotly.express as px
from plotly.subplots import make_subplots
import pickle
import tropycal.tracks as tracks
import pandas as pd
import numpy as np
import cachetools
import functools
import hashlib
import os
from datetime import datetime, timedelta
from datetime import date
from scipy import stats
from scipy.optimize import minimize, curve_fit
from sklearn.linear_model import LinearRegression
from sklearn.cluster import KMeans
from scipy.interpolate import interp1d
from fractions import Fraction
import statsmodels.api as sm
import time
import threading
import requests
from io import StringIO
import tempfile
import csv
from collections import defaultdict
import shutil
import filecmp
import warnings
warnings.filterwarnings('ignore')
# Constants
DATA_PATH = os.getcwd()
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.v04r00.csv')
iBtrace_uri = 'https://www.ncei.noaa.gov/data/international-best-track-archive-for-climate-stewardship-ibtracs/v04r00/access/csv/ibtracs.WP.list.v04r00.csv'
CACHE_FILE = 'ibtracs_cache.pkl'
CACHE_EXPIRY_DAYS = 1
# Color mappings
COLOR_MAP = {
'C5 Super Typhoon': 'rgb(255, 0, 0)',
'C4 Very Strong Typhoon': 'rgb(255, 63, 0)',
'C3 Strong Typhoon': 'rgb(255, 127, 0)',
'C2 Typhoon': 'rgb(255, 191, 0)',
'C1 Typhoon': 'rgb(255, 255, 0)',
'Tropical Storm': 'rgb(0, 255, 255)',
'Tropical Depression': 'rgb(173, 216, 230)'
}
class TyphoonAnalyzer:
def __init__(self):
self.last_oni_update = None
self.ensure_data_files_exist()
self.load_initial_data()
def ensure_data_files_exist(self):
"""Ensure all required data files exist before loading"""
print("Checking and downloading required data files...")
# Create data directory if it doesn't exist
os.makedirs(DATA_PATH, exist_ok=True)
# Download ONI data if it doesn't exist
if not os.path.exists(ONI_DATA_PATH):
print("Downloading 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")
try:
response = requests.get(url)
response.raise_for_status()
with open(temp_file, 'wb') as f:
f.write(response.content)
self.convert_oni_ascii_to_csv(temp_file, ONI_DATA_PATH)
print("ONI data downloaded and converted successfully")
except Exception as e:
print(f"Error downloading ONI data: {e}")
raise
finally:
if os.path.exists(temp_file):
os.remove(temp_file)
# Download IBTrACS data if it doesn't exist
if not os.path.exists(LOCAL_iBtrace_PATH):
print("Downloading IBTrACS data...")
try:
response = requests.get(iBtrace_uri)
response.raise_for_status()
with open(LOCAL_iBtrace_PATH, 'w') as f:
f.write(response.text)
print("IBTrACS data downloaded successfully")
except Exception as e:
print(f"Error downloading IBTrACS data: {e}")
raise
# Create processed typhoon data if it doesn't exist
if not os.path.exists(TYPHOON_DATA_PATH):
print("Processing typhoon data...")
try:
self.convert_typhoondata(LOCAL_iBtrace_PATH, TYPHOON_DATA_PATH)
print("Typhoon data processed successfully")
except Exception as e:
print(f"Error processing typhoon data: {e}")
raise
print("All required data files are ready")
def load_initial_data(self):
print("Loading initial data...")
self.update_oni_data()
self.oni_df = self.fetch_oni_data_from_csv()
self.ibtracs = self.load_ibtracs_data()
self.update_typhoon_data()
self.oni_data, self.typhoon_data = self.load_data()
self.oni_long = self.process_oni_data(self.oni_data)
self.typhoon_max = self.process_typhoon_data(self.typhoon_data)
self.merged_data = self.merge_data()
print("Initial data loading complete")
def fetch_oni_data_from_csv(self):
"""Load ONI data from CSV"""
df = pd.read_csv(ONI_DATA_PATH)
df = df.melt(id_vars=['Year'], var_name='Month', value_name='ONI')
# Convert month numbers to month names
month_map = {
'01': 'Jan', '02': 'Feb', '03': 'Mar', '04': 'Apr',
'05': 'May', '06': 'Jun', '07': 'Jul', '08': 'Aug',
'09': 'Sep', '10': 'Oct', '11': 'Nov', '12': 'Dec'
}
df['Month'] = df['Month'].map(month_map)
# Now create the date
df['Date'] = pd.to_datetime(df['Year'].astype(str) + df['Month'], format='%Y%b')
return df.set_index('Date')
def should_update_oni(self):
today = datetime.now()
return (today.day == 1 or today.day == 15 or
today.day == (today.replace(day=1, month=today.month%12+1) - timedelta(days=1)).day)
def convert_typhoondata(self, input_file, output_file):
"""Convert IBTrACS data to processed format"""
print(f"Converting typhoon data from {input_file} to {output_file}")
with open(input_file, 'r') as infile:
# Skip the header lines
next(infile)
next(infile)
reader = csv.reader(infile)
sid_data = defaultdict(list)
for row in reader:
if not row: # Skip blank lines
continue
sid = row[0]
iso_time = row[6]
sid_data[sid].append((row, iso_time))
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 update_oni_data(self):
if not self.should_update_oni():
return
url = "https://www.cpc.ncep.noaa.gov/data/indices/oni.ascii.txt"
temp_file = os.path.join(DATA_PATH, "temp_oni.ascii.txt")
try:
response = requests.get(url)
response.raise_for_status()
with open(temp_file, 'wb') as f:
f.write(response.content)
self.convert_oni_ascii_to_csv(temp_file, ONI_DATA_PATH)
self.last_oni_update = date.today()
except Exception as e:
print(f"Error updating ONI data: {e}")
finally:
if os.path.exists(temp_file):
os.remove(temp_file)
def create_wind_analysis(self, data):
"""Create wind speed analysis plot"""
fig = px.scatter(data,
x='ONI',
y='USA_WIND',
color='Category',
color_discrete_map=COLOR_MAP,
title='Wind Speed vs ONI Index',
labels={
'ONI': 'Oceanic Niño Index',
'USA_WIND': 'Maximum Wind Speed (kt)'
},
hover_data=['NAME', 'ISO_TIME', 'Category']
)
# Add regression line
x = data['ONI']
y = data['USA_WIND']
slope, intercept = np.polyfit(x, y, 1)
fig.add_trace(
go.Scatter(
x=x,
y=slope * x + intercept,
mode='lines',
name=f'Regression (slope={slope:.2f})',
line=dict(color='black', dash='dash')
)
)
return fig
def create_typhoon_animation(self, year, typhoon_id):
"""Create animated visualization of typhoon path"""
if not typhoon_id:
return go.Figure(), "Please select a typhoon"
storm_data = self.typhoon_data[self.typhoon_data['SID'] == typhoon_id]
if storm_data.empty:
return go.Figure(), "No data available for selected typhoon"
storm_data = storm_data.sort_values('ISO_TIME')
fig = go.Figure()
# Base map settings
fig.update_layout(
title=f"Typhoon Path Animation - {storm_data['NAME'].iloc[0]}",
showlegend=True,
geo=dict(
projection_type='mercator',
showland=True,
showcoastlines=True,
landcolor='rgb(243, 243, 243)',
countrycolor='rgb(204, 204, 204)',
coastlinecolor='rgb(214, 214, 214)',
showocean=True,
oceancolor='rgb(230, 250, 255)',
lataxis=dict(range=[0, 50]),
lonaxis=dict(range=[100, 180]),
center=dict(lat=20, lon=140)
)
)
# Create animation frames
frames = []
for i in range(len(storm_data)):
frame = go.Frame(
data=[
go.Scattergeo(
lon=storm_data['LON'].iloc[:i+1],
lat=storm_data['LAT'].iloc[:i+1],
mode='lines+markers',
line=dict(width=2, color='red'),
marker=dict(size=8, color='red'),
name='Path',
hovertemplate=(
f"Time: {storm_data['ISO_TIME'].iloc[i]:%Y-%m-%d %H:%M}<br>" +
f"Wind: {storm_data['USA_WIND'].iloc[i]:.1f} kt<br>" +
f"Pressure: {storm_data['WMO_PRES'].iloc[i]:.1f} hPa<br>" +
f"Lat: {storm_data['LAT'].iloc[i]:.2f}°N<br>" +
f"Lon: {storm_data['LON'].iloc[i]:.2f}°E"
)
)
],
name=f'frame{i}'
)
frames.append(frame)
fig.frames = frames
# Add animation controls
fig.update_layout(
updatemenus=[{
'buttons': [
{
'args': [None, {'frame': {'duration': 100, 'redraw': True},
'fromcurrent': True}],
'label': 'Play',
'method': 'animate'
},
{
'args': [[None], {'frame': {'duration': 0, 'redraw': True},
'mode': 'immediate',
'transition': {'duration': 0}}],
'label': 'Pause',
'method': 'animate'
}
],
'type': 'buttons',
'showactive': False,
'x': 0.1,
'y': 0,
'xanchor': 'right',
'yanchor': 'top'
}]
)
info_text = f"""
### Typhoon Information
- Name: {storm_data['NAME'].iloc[0]}
- Start Date: {storm_data['ISO_TIME'].iloc[0]:%Y-%m-%d %H:%M}
- End Date: {storm_data['ISO_TIME'].iloc[-1]:%Y-%m-%d %H:%M}
- Maximum Wind Speed: {storm_data['USA_WIND'].max():.1f} kt
- Minimum Pressure: {storm_data['WMO_PRES'].min():.1f} hPa
"""
return fig, info_text
def convert_oni_ascii_to_csv(self, 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:
next(f) # Skip header
for line in f:
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'] + [f"{m:02d}" for m in range(1, 13)])
for year in sorted(data.keys()):
writer.writerow([year] + data[year])
def load_ibtracs_data(self):
if os.path.exists(CACHE_FILE):
cache_time = datetime.fromtimestamp(os.path.getmtime(CACHE_FILE))
if datetime.now() - cache_time < timedelta(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 open(LOCAL_iBtrace_PATH, 'w') as f:
f.write(response.text)
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 update_typhoon_data(self):
try:
response = requests.head(iBtrace_uri)
remote_modified = datetime.strptime(response.headers['Last-Modified'],
'%a, %d %b %Y %H:%M:%S GMT')
local_modified = (datetime.fromtimestamp(os.path.getmtime(LOCAL_iBtrace_PATH))
if os.path.exists(LOCAL_iBtrace_PATH) else datetime.min)
if remote_modified > local_modified:
response = requests.get(iBtrace_uri)
response.raise_for_status()
with open(LOCAL_iBtrace_PATH, 'w') as f:
f.write(response.text)
except Exception as e:
print(f"Error updating typhoon data: {e}")
def load_data(self):
oni_data = pd.read_csv(ONI_DATA_PATH)
typhoon_data = pd.read_csv(TYPHOON_DATA_PATH, low_memory=False)
typhoon_data['ISO_TIME'] = pd.to_datetime(typhoon_data['ISO_TIME'])
return oni_data, typhoon_data
def process_oni_data(self, oni_data):
"""Process ONI data"""
oni_long = pd.melt(oni_data, id_vars=['Year'], var_name='Month', value_name='ONI')
# Create a mapping for month numbers
month_map = {
'01': 1, '02': 2, '03': 3, '04': 4,
'05': 5, '06': 6, '07': 7, '08': 8,
'09': 9, '10': 10, '11': 11, '12': 12
}
# Convert month strings to numbers directly
oni_long['Month'] = oni_long['Month'].map(month_map)
return oni_long
def process_typhoon_data(self, typhoon_data):
typhoon_data['USA_WIND'] = pd.to_numeric(typhoon_data['USA_WIND'], errors='coerce')
typhoon_data['WMO_PRES'] = pd.to_numeric(typhoon_data['WMO_PRES'], errors='coerce')
typhoon_data['ISO_TIME'] = pd.to_datetime(typhoon_data['ISO_TIME'])
typhoon_data['Year'] = typhoon_data['ISO_TIME'].dt.year
typhoon_data['Month'] = typhoon_data['ISO_TIME'].dt.month
typhoon_max = typhoon_data.groupby(['SID', 'Year', 'Month']).agg({
'USA_WIND': 'max',
'WMO_PRES': 'min',
'NAME': 'first',
'LAT': 'first',
'LON': 'first',
'ISO_TIME': 'first'
}).reset_index()
typhoon_max['Category'] = typhoon_max['USA_WIND'].apply(self.categorize_typhoon)
return typhoon_max
def merge_data(self):
return pd.merge(self.typhoon_max, self.oni_long, on=['Year', 'Month'])
def categorize_typhoon(self, wind_speed):
if wind_speed >= 137:
return 'C5 Super Typhoon'
elif wind_speed >= 113:
return 'C4 Very Strong Typhoon'
elif wind_speed >= 96:
return 'C3 Strong Typhoon'
elif wind_speed >= 83:
return 'C2 Typhoon'
elif wind_speed >= 64:
return 'C1 Typhoon'
elif wind_speed >= 34:
return 'Tropical Storm'
else:
return 'Tropical Depression'
def analyze_typhoon(self, start_year, start_month, end_year, end_month, enso_value='all'):
start_date = datetime(start_year, start_month, 1)
end_date = datetime(end_year, end_month, 28)
filtered_data = self.merged_data[
(self.merged_data['ISO_TIME'] >= start_date) &
(self.merged_data['ISO_TIME'] <= end_date)
]
if enso_value != 'all':
filtered_data = filtered_data[
(filtered_data['ONI'] >= 0.5 if enso_value == 'el_nino' else
filtered_data['ONI'] <= -0.5 if enso_value == 'la_nina' else
(filtered_data['ONI'] > -0.5) & (filtered_data['ONI'] < 0.5))
]
return {
'tracks': self.create_tracks_plot(filtered_data),
'wind': self.create_wind_analysis(filtered_data),
'pressure': self.create_pressure_analysis(filtered_data),
'clusters': self.create_cluster_analysis(filtered_data, 5),
'stats': self.generate_statistics(filtered_data)
}
def create_tracks_plot(self, data):
"""Create typhoon tracks visualization"""
fig = go.Figure()
fig.update_layout(
title={
'text': 'Typhoon Tracks',
'y':0.95,
'x':0.5,
'xanchor': 'center',
'yanchor': 'top'
},
showlegend=True,
legend=dict(
yanchor="top",
y=0.99,
xanchor="left",
x=0.01,
bgcolor='rgba(255, 255, 255, 0.8)'
),
geo=dict(
projection_type='mercator',
showland=True,
showcoastlines=True,
landcolor='rgb(243, 243, 243)',
countrycolor='rgb(204, 204, 204)',
coastlinecolor='rgb(214, 214, 214)',
showocean=True,
oceancolor='rgb(230, 250, 255)',
showlakes=True,
lakecolor='rgb(230, 250, 255)',
lataxis=dict(range=[0, 50]),
lonaxis=dict(range=[100, 180]),
center=dict(lat=20, lon=140),
bgcolor='rgba(255, 255, 255, 0.5)'
),
paper_bgcolor='rgba(255, 255, 255, 0.5)',
plot_bgcolor='rgba(255, 255, 255, 0.5)'
)
for category in COLOR_MAP.keys():
category_data = data[data['Category'] == category]
for _, storm in category_data.groupby('SID'):
track_data = self.typhoon_data[self.typhoon_data['SID'] == storm['SID'].iloc[0]]
track_data = track_data.sort_values('ISO_TIME')
fig.add_trace(go.Scattergeo(
lon=track_data['LON'],
lat=track_data['LAT'],
mode='lines',
line=dict(
width=2,
color=COLOR_MAP[category]
),
name=category,
legendgroup=category,
showlegend=True if storm.iloc[0]['SID'] == category_data.iloc[0]['SID'] else False,
hovertemplate=(
f"Name: {storm['NAME'].iloc[0]}<br>" +
f"Category: {category}<br>" +
f"Wind Speed: {storm['USA_WIND'].iloc[0]:.1f} kt<br>" +
f"Pressure: {storm['WMO_PRES'].iloc[0]:.1f} hPa<br>" +
f"Date: {track_data['ISO_TIME'].dt.strftime('%Y-%m-%d %H:%M').iloc[0]}<br>" +
f"Lat: {track_data['LAT'].iloc[0]:.2f}°N<br>" +
f"Lon: {track_data['LON'].iloc[0]:.2f}°E<br>" +
"<extra></extra>"
)
))
return fig
def analyze_clusters(self, year, n_clusters):
"""Analyze typhoon clusters for a specific year"""
year_data = self.typhoon_data[self.typhoon_data['SEASON'] == year]
if year_data.empty:
return go.Figure(), "No data available for selected year"
# Prepare data for clustering
routes = []
for _, storm in year_data.groupby('SID'):
if len(storm) > 1:
# Standardize route length
t = np.linspace(0, 1, len(storm))
t_new = np.linspace(0, 1, 100)
lon_interp = interp1d(t, storm['LON'], kind='linear')(t_new)
lat_interp = interp1d(t, storm['LAT'], kind='linear')(t_new)
routes.append(np.column_stack((lon_interp, lat_interp)))
if len(routes) < n_clusters:
return go.Figure(), f"Not enough typhoons ({len(routes)}) for {n_clusters} clusters"
# Perform clustering
routes_array = np.array(routes)
routes_reshaped = routes_array.reshape(routes_array.shape[0], -1)
kmeans = KMeans(n_clusters=n_clusters, random_state=42)
clusters = kmeans.fit_predict(routes_reshaped)
# Create visualization
fig = go.Figure()
# Set layout
fig.update_layout(
title=f'Typhoon Route Clusters ({year})',
showlegend=True,
geo=dict(
projection_type='mercator',
showland=True,
showcoastlines=True,
landcolor='rgb(243, 243, 243)',
countrycolor='rgb(204, 204, 204)',
coastlinecolor='rgb(214, 214, 214)',
showocean=True,
oceancolor='rgb(230, 250, 255)',
lataxis=dict(range=[0, 50]),
lonaxis=dict(range=[100, 180]),
center=dict(lat=20, lon=140)
)
)
# Plot routes colored by cluster
for route, cluster_id in zip(routes, clusters):
fig.add_trace(go.Scattergeo(
lon=route[:, 0],
lat=route[:, 1],
mode='lines',
line=dict(
width=1,
color=f'hsl({cluster_id * 360/n_clusters}, 50%, 50%)'
),
name=f'Cluster {cluster_id + 1}',
showlegend=False
))
# Plot cluster centers
for i in range(n_clusters):
center = kmeans.cluster_centers_[i].reshape(-1, 2)
fig.add_trace(go.Scattergeo(
lon=center[:, 0],
lat=center[:, 1],
mode='lines',
name=f'Cluster {i+1} Center',
line=dict(
width=3,
color=f'hsl({i * 360/n_clusters}, 100%, 50%)'
)
))
# Generate statistics text
stats_text = "### Clustering Results\n\n"
cluster_counts = np.bincount(clusters)
for i in range(n_clusters):
stats_text += f"- Cluster {i+1}: {cluster_counts[i]} typhoons\n"
return fig, stats_text
def get_typhoons_for_year(self, year):
"""Get list of typhoons for a specific year"""
year_data = self.typhoon_data[self.typhoon_data['ISO_TIME'].dt.year == year]
typhoons = year_data.groupby('SID').first()
return [{'label': f"{row['NAME']} ({row.name})", 'value': row.name}
for _, row in typhoons.iterrows()]
def create_typhoon_animation(self, year, typhoon_id):
"""Create animated visualization of typhoon path"""
storm_data = self.typhoon_data[self.typhoon_data['SID'] == typhoon_id]
storm_data = storm_data.sort_values('ISO_TIME')
fig = go.Figure()
# Base map settings
fig.update_layout(
title=f"Typhoon Path Animation - {storm_data['NAME'].iloc[0]} ({year})",
showlegend=True,
geo=dict(
projection_type='mercator',
showland=True,
showcoastlines=True,
landcolor='rgb(243, 243, 243)',
countrycolor='rgb(204, 204, 204)',
coastlinecolor='rgb(214, 214, 214)',
showocean=True,
oceancolor='rgb(230, 250, 255)',
lataxis=dict(range=[0, 50]),
lonaxis=dict(range=[100, 180]),
center=dict(lat=20, lon=140)
)
)
# Create animation frames
frames = []
for i in range(len(storm_data)):
frame = go.Frame(
data=[
go.Scattergeo(
lon=storm_data['LON'].iloc[:i+1],
lat=storm_data['LAT'].iloc[:i+1],
mode='lines+markers',
line=dict(width=2, color='red'),
marker=dict(size=8, color='red'),
name='Path'
)
],
name=f'frame{i}'
)
frames.append(frame)
fig.frames = frames
# Add animation controls
fig.update_layout(
updatemenus=[{
'buttons': [
{
'args': [None, {'frame': {'duration': 100, 'redraw': True},
'fromcurrent': True}],
'label': 'Play',
'method': 'animate'
},
{
'args': [[None], {'frame': {'duration': 0, 'redraw': True},
'mode': 'immediate',
'transition': {'duration': 0}}],
'label': 'Pause',
'method': 'animate'
}
],
'type': 'buttons',
'showactive': False,
'x': 0.1,
'y': 0,
'xanchor': 'right',
'yanchor': 'top'
}]
)
info_text = f"""
### Typhoon Information
- Name: {storm_data['NAME'].iloc[0]}
- Start Date: {storm_data['ISO_TIME'].iloc[0]:%Y-%m-%d %H:%M}
- End Date: {storm_data['ISO_TIME'].iloc[-1]:%Y-%m-%d %H:%M}
- Maximum Wind Speed: {storm_data['USA_WIND'].max():.1f} kt
- Minimum Pressure: {storm_data['WMO_PRES'].min():.1f} hPa
"""
return fig, info_text
def create_pressure_analysis(self, data):
fig = px.scatter(data,
x='ONI',
y='WMO_PRES',
color='Category',
color_discrete_map=COLOR_MAP,
title='Pressure vs ONI Index',
labels={
'ONI': 'Oceanic Niño Index',
'WMO_PRES': 'Minimum Pressure (hPa)'
},
hover_data=['NAME', 'ISO_TIME']
)
# Add regression line
x = data['ONI']
y = data['WMO_PRES']
slope, intercept = np.polyfit(x, y, 1)
fig.add_trace(
go.Scatter(
x=x,
y=slope * x + intercept,
mode='lines',
name=f'Regression (slope={slope:.2f})',
line=dict(color='black', dash='dash')
)
)
return fig
def create_cluster_analysis(self, data, n_clusters=5):
# Prepare data for clustering
routes = []
for _, storm in data.groupby('SID'):
if len(storm) > 1:
# Standardize route length
t = np.linspace(0, 1, len(storm))
t_new = np.linspace(0, 1, 100)
lon_interp = interp1d(t, storm['LON'], kind='linear')(t_new)
lat_interp = interp1d(t, storm['LAT'], kind='linear')(t_new)
routes.append(np.column_stack((lon_interp, lat_interp)))
if not routes:
return go.Figure()
# Perform clustering
routes_array = np.array(routes)
routes_reshaped = routes_array.reshape(routes_array.shape[0], -1)
kmeans = KMeans(n_clusters=n_clusters, random_state=42)
clusters = kmeans.fit_predict(routes_reshaped)
# Create visualization
fig = go.Figure()
# Plot original routes colored by cluster
for route, cluster_id in zip(routes, clusters):
fig.add_trace(go.Scattergeo(
lon=route[:, 0],
lat=route[:, 1],
mode='lines',
line=dict(width=1, color=f'hsl({cluster_id * 360/n_clusters}, 50%, 50%)'),
showlegend=False
))
# Plot cluster centers
for i in range(n_clusters):
center = kmeans.cluster_centers_[i].reshape(-1, 2)
fig.add_trace(go.Scattergeo(
lon=center[:, 0],
lat=center[:, 1],
mode='lines',
name=f'Cluster {i+1} Center',
line=dict(width=3, color=f'hsl({i * 360/n_clusters}, 100%, 50%)')
))
fig.update_layout(
title='Typhoon Route Clusters',
showlegend=True,
geo=dict(
projection_type='mercator',
showland=True,
showcoastlines=True,
landcolor='rgb(243, 243, 243)',
countrycolor='rgb(204, 204, 204)',
coastlinecolor='rgb(214, 214, 214)',
lataxis=dict(range=[0, 50]),
lonaxis=dict(range=[100, 180]),
)
)
return fig
def get_typhoons_for_year(self, year):
"""Get list of typhoons for a specific year"""
year_data = self.typhoon_data[self.typhoon_data['SEASON'] == year]
unique_typhoons = year_data.groupby('SID').first().reset_index()
return [
{'label': f"{row['NAME']} ({row['ISO_TIME'].strftime('%Y-%m-%d')})",
'value': row['SID']}
for _, row in unique_typhoons.iterrows()
]
def search_typhoon_details(self, year, typhoon_id):
"""Get detailed information for a specific typhoon"""
if not typhoon_id:
return None, "Please select a typhoon"
storm_data = self.typhoon_data[self.typhoon_data['SID'] == typhoon_id]
storm_data = storm_data.sort_values('ISO_TIME')
# Create track plot
fig = self.create_single_typhoon_plot(storm_data)
# Create detailed information text
info = self.create_typhoon_info_text(storm_data)
return fig, info
def create_single_typhoon_plot(self, storm_data):
"""Create a detailed plot for a single typhoon"""
fig = go.Figure()
fig.update_layout(
title=f"Typhoon Track - {storm_data['NAME'].iloc[0]} ({storm_data['SEASON'].iloc[0]})",
showlegend=True,
geo=dict(
projection_type='mercator',
showland=True,
showcoastlines=True,
landcolor='rgb(243, 243, 243)',
countrycolor='rgb(204, 204, 204)',
coastlinecolor='rgb(214, 214, 214)',
showocean=True,
oceancolor='rgb(230, 250, 255)',
lataxis=dict(range=[0, 50]),
lonaxis=dict(range=[100, 180]),
center=dict(lat=20, lon=140)
)
)
# Add main track
fig.add_trace(go.Scattergeo(
lon=storm_data['LON'],
lat=storm_data['LAT'],
mode='lines+markers',
line=dict(width=2, color='red'),
marker=dict(
size=8,
color=storm_data['USA_WIND'],
colorscale='Viridis',
showscale=True,
colorbar=dict(title='Wind Speed (kt)')
),
text=[f"Time: {time:%Y-%m-%d %H:%M}<br>Wind: {wind:.1f} kt<br>Pressure: {pres:.1f} hPa"
for time, wind, pres in zip(storm_data['ISO_TIME'],
storm_data['USA_WIND'],
storm_data['WMO_PRES'])],
hoverinfo='text'
))
return fig
def create_typhoon_info_text(self, storm_data):
"""Create detailed information text for a typhoon"""
max_wind = storm_data['USA_WIND'].max()
min_pressure = storm_data['WMO_PRES'].min()
duration = (storm_data['ISO_TIME'].max() - storm_data['ISO_TIME'].min()).total_seconds() / 3600 # hours
return f"""
### Typhoon Details: {storm_data['NAME'].iloc[0]}
**Timing Information:**
- Start: {storm_data['ISO_TIME'].min():%Y-%m-%d %H:%M}
- End: {storm_data['ISO_TIME'].max():%Y-%m-%d %H:%M}
- Duration: {duration:.1f} hours
**Intensity Metrics:**
- Maximum Wind Speed: {max_wind:.1f} kt
- Minimum Pressure: {min_pressure:.1f} hPa
- Maximum Category: {self.categorize_typhoon(max_wind)}
**Track Information:**
- Starting Position: {storm_data['LAT'].iloc[0]:.1f}°N, {storm_data['LON'].iloc[0]:.1f}°E
- Ending Position: {storm_data['LAT'].iloc[-1]:.1f}°N, {storm_data['LON'].iloc[-1]:.1f}°E
- Total Track Points: {len(storm_data)}
"""
def generate_statistics(self, data):
stats = {
'total_typhoons': len(data['SID'].unique()),
'avg_wind': data['USA_WIND'].mean(),
'max_wind': data['USA_WIND'].max(),
'avg_pressure': data['WMO_PRES'].mean(),
'min_pressure': data['WMO_PRES'].min(),
'oni_correlation_wind': data['ONI'].corr(data['USA_WIND']),
'oni_correlation_pressure': data['ONI'].corr(data['WMO_PRES']),
'category_counts': data['Category'].value_counts().to_dict()
}
return f"""
### Statistical Summary
- Total Typhoons: {stats['total_typhoons']}
- Average Wind Speed: {stats['avg_wind']:.2f} kt
- Maximum Wind Speed: {stats['max_wind']:.2f} kt
- Average Pressure: {stats['avg_pressure']:.2f} hPa
- Minimum Pressure: {stats['min_pressure']:.2f} hPa
- ONI-Wind Speed Correlation: {stats['oni_correlation_wind']:.3f}
- ONI-Pressure Correlation: {stats['oni_correlation_pressure']:.3f}
### Category Distribution
{chr(10).join(f'- {cat}: {count}' for cat, count in stats['category_counts'].items())}
"""
def create_interface():
analyzer = TyphoonAnalyzer()
with gr.Blocks(title="Typhoon Analysis Dashboard", theme=gr.themes.Base()) as demo:
gr.Markdown("# Typhoon Analysis Dashboard")
with gr.Tabs():
# Main Analysis Tab
with gr.Tab("Main Analysis"):
with gr.Row():
with gr.Column():
start_year = gr.Slider(1900, 2024, 2000, label="Start Year")
start_month = gr.Slider(1, 12, 1, label="Start Month")
with gr.Column():
end_year = gr.Slider(1900, 2024, 2024, label="End Year")
end_month = gr.Slider(1, 12, 12, label="End Month")
enso_dropdown = gr.Dropdown(
choices=["all", "el_nino", "la_nina", "neutral"],
value="all",
label="ENSO Phase"
)
analyze_btn = gr.Button("Analyze")
with gr.Row():
tracks_plot = gr.Plot()
with gr.Row():
wind_plot = gr.Plot()
pressure_plot = gr.Plot()
stats_text = gr.Markdown()
# Clustering Analysis Tab
with gr.Tab("Clustering Analysis"):
with gr.Row():
cluster_year = gr.Slider(1900, 2024, 2000, label="Year")
n_clusters = gr.Slider(2, 20, 5, label="Number of Clusters")
cluster_btn = gr.Button("Analyze Clusters")
cluster_plot = gr.Plot()
cluster_stats = gr.Markdown()
# Animation Tab
with gr.Tab("Typhoon Animation"):
with gr.Row():
animation_year = gr.Slider(
minimum=1900,
maximum=2024,
value=2024,
step=1,
label="Select Year"
)
with gr.Row():
animation_typhoon = gr.Dropdown(
choices=[],
label="Select Typhoon",
interactive=True
)
animation_btn = gr.Button("Animate Typhoon Path", variant="primary")
animation_plot = gr.Plot()
animation_info = gr.Markdown()
# Search Tab
with gr.Tab("Typhoon Search"):
with gr.Row():
search_year = gr.Slider(
minimum=1900,
maximum=2024,
value=2024,
step=1,
label="Select Year"
)
with gr.Row():
search_typhoon = gr.Dropdown(
choices=[],
label="Select Typhoon",
interactive=True
)
search_btn = gr.Button("Show Typhoon Details", variant="primary")
search_plot = gr.Plot()
search_info = gr.Markdown()
# Event handlers
def analyze_callback(start_y, start_m, end_y, end_m, enso):
results = analyzer.analyze_typhoon(start_y, start_m, end_y, end_m, enso)
return [
results['tracks'],
results['wind'],
results['pressure'],
results['stats']
]
def cluster_callback(year, n_clusters):
return analyzer.analyze_clusters(year, n_clusters)
def update_typhoon_choices(year):
typhoons = analyzer.get_typhoons_for_year(year)
return gr.update(choices=typhoons, value=None)
# Connect events for main analysis
analyze_btn.click(
analyze_callback,
inputs=[start_year, start_month, end_year, end_month, enso_dropdown],
outputs=[tracks_plot, wind_plot, pressure_plot, stats_text]
)
# Connect events for clustering
cluster_btn.click(
cluster_callback,
inputs=[cluster_year, n_clusters],
outputs=[cluster_plot, cluster_stats]
)
# Connect events for Animation tab
animation_year.change(
update_typhoon_choices,
inputs=[animation_year],
outputs=[animation_typhoon]
)
animation_btn.click(
analyzer.create_typhoon_animation,
inputs=[animation_year, animation_typhoon],
outputs=[animation_plot, animation_info]
)
# Connect events for Search tab
search_year.change(
update_typhoon_choices,
inputs=[search_year],
outputs=[search_typhoon]
)
search_btn.click(
analyzer.search_typhoon_details,
inputs=[search_year, search_typhoon],
outputs=[search_plot, search_info]
)
return demo
if __name__ == "__main__":
demo = create_interface()
demo.launch(
server_name="0.0.0.0",
server_port=7860,
share=True
)