Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -20,8 +20,9 @@ import plotly.express as px
|
|
20 |
from plotly.subplots import make_subplots
|
21 |
|
22 |
from sklearn.manifold import TSNE
|
23 |
-
from sklearn.cluster import DBSCAN
|
24 |
from sklearn.preprocessing import StandardScaler
|
|
|
25 |
from scipy.interpolate import interp1d
|
26 |
import statsmodels.api as sm
|
27 |
import requests
|
@@ -29,6 +30,23 @@ import tempfile
|
|
29 |
import shutil
|
30 |
import xarray as xr
|
31 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
32 |
try:
|
33 |
import cdsapi
|
34 |
CDSAPI_AVAILABLE = True
|
@@ -79,8 +97,20 @@ CACHE_FILE = os.path.join(DATA_PATH, 'ibtracs_cache.pkl')
|
|
79 |
CACHE_EXPIRY_DAYS = 1
|
80 |
|
81 |
# -----------------------------
|
82 |
-
# Color Maps and Standards
|
83 |
# -----------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
84 |
color_map = {
|
85 |
'C5 Super Typhoon': 'rgb(255, 0, 0)',
|
86 |
'C4 Very Strong Typhoon': 'rgb(255, 165, 0)',
|
@@ -90,6 +120,7 @@ color_map = {
|
|
90 |
'Tropical Storm': 'rgb(0, 0, 255)',
|
91 |
'Tropical Depression': 'rgb(128, 128, 128)'
|
92 |
}
|
|
|
93 |
atlantic_standard = {
|
94 |
'C5 Super Typhoon': {'wind_speed': 137, 'color': 'Red', 'hex': '#FF0000'},
|
95 |
'C4 Very Strong Typhoon': {'wind_speed': 113, 'color': 'Orange', 'hex': '#FFA500'},
|
@@ -99,6 +130,7 @@ atlantic_standard = {
|
|
99 |
'Tropical Storm': {'wind_speed': 34, 'color': 'Blue', 'hex': '#0000FF'},
|
100 |
'Tropical Depression': {'wind_speed': 0, 'color': 'Gray', 'hex': '#808080'}
|
101 |
}
|
|
|
102 |
taiwan_standard = {
|
103 |
'Strong Typhoon': {'wind_speed': 51.0, 'color': 'Red', 'hex': '#FF0000'},
|
104 |
'Medium Typhoon': {'wind_speed': 33.7, 'color': 'Orange', 'hex': '#FFA500'},
|
@@ -248,7 +280,7 @@ def update_oni_data():
|
|
248 |
|
249 |
def create_fallback_oni_data(output_file):
|
250 |
"""Create minimal ONI data for testing"""
|
251 |
-
years = range(2000, 2025
|
252 |
months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']
|
253 |
|
254 |
# Create synthetic ONI data
|
@@ -527,7 +559,7 @@ def load_data_fixed(oni_path, typhoon_path):
|
|
527 |
def create_fallback_typhoon_data():
|
528 |
"""Create minimal fallback typhoon data - FIXED VERSION"""
|
529 |
# Use proper pandas date_range instead of numpy
|
530 |
-
dates = pd.date_range(start='2000-01-01', end='
|
531 |
storm_dates = dates[np.random.choice(len(dates), size=100, replace=False)]
|
532 |
|
533 |
data = []
|
@@ -595,31 +627,46 @@ def process_typhoon_data(typhoon_data):
|
|
595 |
typhoon_max['Month'] = '01'
|
596 |
typhoon_max['Year'] = typhoon_max['SEASON']
|
597 |
|
598 |
-
typhoon_max['Category'] = typhoon_max['USA_WIND'].apply(
|
599 |
return typhoon_max
|
600 |
|
601 |
def merge_data(oni_long, typhoon_max):
|
602 |
"""Merge ONI and typhoon data"""
|
603 |
return pd.merge(typhoon_max, oni_long, on=['Year','Month'])
|
604 |
|
605 |
-
|
606 |
-
|
|
|
|
|
|
|
|
|
607 |
if pd.isna(wind_speed):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
608 |
return 'Tropical Depression'
|
609 |
-
|
610 |
-
return 'C5 Super Typhoon'
|
611 |
-
elif wind_speed >= 113:
|
612 |
-
return 'C4 Very Strong Typhoon'
|
613 |
-
elif wind_speed >= 96:
|
614 |
-
return 'C3 Strong Typhoon'
|
615 |
-
elif wind_speed >= 83:
|
616 |
-
return 'C2 Typhoon'
|
617 |
-
elif wind_speed >= 64:
|
618 |
-
return 'C1 Typhoon'
|
619 |
-
elif wind_speed >= 34:
|
620 |
return 'Tropical Storm'
|
621 |
-
|
622 |
-
return '
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
623 |
|
624 |
def classify_enso_phases(oni_value):
|
625 |
"""Classify ENSO phases based on ONI value"""
|
@@ -635,7 +682,196 @@ def classify_enso_phases(oni_value):
|
|
635 |
return 'Neutral'
|
636 |
|
637 |
# -----------------------------
|
638 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
639 |
# -----------------------------
|
640 |
|
641 |
def perform_wind_regression(start_year, start_month, end_year, end_month):
|
@@ -690,7 +926,7 @@ def perform_longitude_regression(start_year, start_month, end_year, end_month):
|
|
690 |
return f"Longitude Regression Error: {e}"
|
691 |
|
692 |
# -----------------------------
|
693 |
-
# Visualization Functions
|
694 |
# -----------------------------
|
695 |
|
696 |
def get_full_tracks(start_year, start_month, end_year, end_month, enso_phase, typhoon_search):
|
@@ -753,7 +989,7 @@ def get_full_tracks(start_year, start_month, end_year, end_month, enso_phase, ty
|
|
753 |
return fig, f"Total typhoons displayed: {count}"
|
754 |
|
755 |
def get_wind_analysis(start_year, start_month, end_year, end_month, enso_phase, typhoon_search):
|
756 |
-
"""Get wind analysis"""
|
757 |
start_date = datetime(start_year, start_month, 1)
|
758 |
end_date = datetime(end_year, end_month, 28)
|
759 |
filtered_data = merged_data[(merged_data['ISO_TIME']>=start_date) & (merged_data['ISO_TIME']<=end_date)].copy()
|
@@ -765,7 +1001,7 @@ def get_wind_analysis(start_year, start_month, end_year, end_month, enso_phase,
|
|
765 |
hover_data=['NAME','Year','Category'],
|
766 |
title='Wind Speed vs ONI',
|
767 |
labels={'ONI':'ONI Value','USA_WIND':'Max Wind Speed (knots)'},
|
768 |
-
color_discrete_map=
|
769 |
|
770 |
if typhoon_search:
|
771 |
mask = filtered_data['NAME'].str.contains(typhoon_search, case=False, na=False)
|
@@ -781,7 +1017,7 @@ def get_wind_analysis(start_year, start_month, end_year, end_month, enso_phase,
|
|
781 |
return fig, regression
|
782 |
|
783 |
def get_pressure_analysis(start_year, start_month, end_year, end_month, enso_phase, typhoon_search):
|
784 |
-
"""Get pressure analysis"""
|
785 |
start_date = datetime(start_year, start_month, 1)
|
786 |
end_date = datetime(end_year, end_month, 28)
|
787 |
filtered_data = merged_data[(merged_data['ISO_TIME']>=start_date) & (merged_data['ISO_TIME']<=end_date)].copy()
|
@@ -793,7 +1029,7 @@ def get_pressure_analysis(start_year, start_month, end_year, end_month, enso_pha
|
|
793 |
hover_data=['NAME','Year','Category'],
|
794 |
title='Pressure vs ONI',
|
795 |
labels={'ONI':'ONI Value','USA_PRES':'Min Pressure (hPa)'},
|
796 |
-
color_discrete_map=
|
797 |
|
798 |
if typhoon_search:
|
799 |
mask = filtered_data['NAME'].str.contains(typhoon_search, case=False, na=False)
|
@@ -838,7 +1074,7 @@ def get_longitude_analysis(start_year, start_month, end_year, end_month, enso_ph
|
|
838 |
return fig, slopes_text, regression
|
839 |
|
840 |
def categorize_typhoon_by_standard(wind_speed, standard='atlantic'):
|
841 |
-
"""Categorize typhoon by standard"""
|
842 |
if pd.isna(wind_speed):
|
843 |
return 'Tropical Depression', '#808080'
|
844 |
|
@@ -867,155 +1103,214 @@ def categorize_typhoon_by_standard(wind_speed, standard='atlantic'):
|
|
867 |
return 'Tropical Depression', atlantic_standard['Tropical Depression']['hex']
|
868 |
|
869 |
# -----------------------------
|
870 |
-
# Animation Functions
|
871 |
# -----------------------------
|
872 |
|
873 |
-
def
|
874 |
-
"""
|
875 |
-
|
876 |
-
|
877 |
-
|
878 |
-
|
879 |
-
|
880 |
-
storm_df = storm_df.sort_values('ISO_TIME')
|
881 |
-
lats = storm_df['LAT'].astype(float).values
|
882 |
-
lons = storm_df['LON'].astype(float).values
|
883 |
-
times = pd.to_datetime(storm_df['ISO_TIME']).values
|
884 |
-
|
885 |
-
if 'USA_WIND' in storm_df.columns:
|
886 |
-
winds = pd.to_numeric(storm_df['USA_WIND'], errors='coerce').values
|
887 |
else:
|
888 |
-
|
889 |
-
|
890 |
-
storm_name = storm_df['NAME'].iloc[0] if pd.notnull(storm_df['NAME'].iloc[0]) else "Unnamed"
|
891 |
-
basin = storm_df['SID'].iloc[0][:2]
|
892 |
-
season = storm_df['SEASON'].iloc[0] if 'SEASON' in storm_df.columns else year
|
893 |
-
|
894 |
-
min_lat, max_lat = np.min(lats), np.max(lats)
|
895 |
-
min_lon, max_lon = np.min(lons), np.max(lons)
|
896 |
-
lat_padding = max((max_lat - min_lat)*0.3, 5)
|
897 |
-
lon_padding = max((max_lon - min_lon)*0.3, 5)
|
898 |
-
|
899 |
-
fig = plt.figure(figsize=(12,6), dpi=100)
|
900 |
-
ax = plt.axes([0.05, 0.05, 0.60, 0.85],
|
901 |
-
projection=ccrs.PlateCarree(central_longitude=180))
|
902 |
-
ax.stock_img()
|
903 |
-
ax.set_extent([min_lon - lon_padding, max_lon + lon_padding, min_lat - lat_padding, max_lat + lat_padding],
|
904 |
-
crs=ccrs.PlateCarree())
|
905 |
-
ax.coastlines(resolution='50m', color='black', linewidth=1)
|
906 |
-
gl = ax.gridlines(draw_labels=True, color='gray', alpha=0.4, linestyle='--')
|
907 |
-
gl.top_labels = gl.right_labels = False
|
908 |
-
ax.set_title(f"{year} {storm_name} ({basin}) - {season}", fontsize=14)
|
909 |
-
|
910 |
-
line, = ax.plot([], [], transform=ccrs.PlateCarree(), color='blue', linewidth=2)
|
911 |
-
point, = ax.plot([], [], 'o', markersize=8, transform=ccrs.PlateCarree())
|
912 |
-
date_text = ax.text(0.02, 0.02, '', transform=ax.transAxes, fontsize=10,
|
913 |
-
bbox=dict(facecolor='white', alpha=0.8))
|
914 |
-
storm_info_text = fig.text(0.70, 0.60, '', fontsize=10,
|
915 |
-
bbox=dict(facecolor='white', alpha=0.8, boxstyle='round,pad=0.5'))
|
916 |
-
|
917 |
-
from matplotlib.lines import Line2D
|
918 |
-
standard_dict = atlantic_standard if standard=='atlantic' else taiwan_standard
|
919 |
-
legend_elements = [Line2D([0],[0], marker='o', color='w', label=cat,
|
920 |
-
markerfacecolor=details['hex'], markersize=8)
|
921 |
-
for cat, details in standard_dict.items()]
|
922 |
-
ax.legend(handles=legend_elements, title="Storm Categories",
|
923 |
-
loc='upper right', fontsize=9)
|
924 |
-
|
925 |
-
def init():
|
926 |
-
line.set_data([], [])
|
927 |
-
point.set_data([], [])
|
928 |
-
date_text.set_text('')
|
929 |
-
storm_info_text.set_text('')
|
930 |
-
return line, point, date_text, storm_info_text
|
931 |
-
|
932 |
-
def update(frame):
|
933 |
-
line.set_data(lons[:frame+1], lats[:frame+1])
|
934 |
-
point.set_data([lons[frame]], [lats[frame]])
|
935 |
-
wind_speed = winds[frame] if frame < len(winds) and not pd.isna(winds[frame]) else 0
|
936 |
-
category, color = categorize_typhoon_by_standard(wind_speed, standard)
|
937 |
-
point.set_color(color)
|
938 |
-
dt_str = pd.to_datetime(times[frame]).strftime('%Y-%m-%d %H:%M')
|
939 |
-
date_text.set_text(dt_str)
|
940 |
-
info_str = (f"Name: {storm_name}\nBasin: {basin}\nDate: {dt_str}\nWind: {wind_speed:.1f} kt\nCategory: {category}")
|
941 |
-
storm_info_text.set_text(info_str)
|
942 |
-
return line, point, date_text, storm_info_text
|
943 |
-
|
944 |
-
ani = animation.FuncAnimation(fig, update, init_func=init, frames=len(times),
|
945 |
-
interval=200, blit=True, repeat=True)
|
946 |
|
947 |
-
|
948 |
-
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.mp4', dir=DATA_PATH)
|
949 |
-
try:
|
950 |
-
writer = animation.FFMpegWriter(fps=5, bitrate=1800)
|
951 |
-
ani.save(temp_file.name, writer=writer)
|
952 |
-
plt.close(fig)
|
953 |
-
return temp_file.name
|
954 |
-
except Exception as e:
|
955 |
-
logging.error(f"Error creating animation: {e}")
|
956 |
-
plt.close(fig)
|
957 |
-
return None
|
958 |
|
959 |
-
def
|
960 |
-
"""
|
961 |
-
if not typhoon:
|
962 |
-
return None
|
963 |
-
storm_id = typhoon.split('(')[-1].strip(')')
|
964 |
-
return generate_track_video_from_csv(year, storm_id, standard)
|
965 |
-
|
966 |
-
# -----------------------------
|
967 |
-
# Update Typhoon Options Function
|
968 |
-
# -----------------------------
|
969 |
-
|
970 |
-
def update_typhoon_options_fixed(year, basin):
|
971 |
-
"""Fixed version of update_typhoon_options"""
|
972 |
try:
|
973 |
-
|
974 |
-
if typhoon_data is None or typhoon_data.empty:
|
975 |
-
logging.error("No typhoon data available")
|
976 |
-
return gr.update(choices=[], value=None)
|
977 |
|
978 |
-
# Filter by year
|
979 |
if 'ISO_TIME' in typhoon_data.columns:
|
980 |
-
|
981 |
elif 'SEASON' in typhoon_data.columns:
|
982 |
-
|
983 |
else:
|
984 |
-
# Fallback
|
985 |
-
|
|
|
|
|
986 |
|
|
|
987 |
if basin != "All Basins":
|
988 |
-
# Extract basin code
|
989 |
basin_code = basin.split(' - ')[0] if ' - ' in basin else basin[:2]
|
990 |
-
# Filter by basin
|
991 |
if 'SID' in year_data.columns:
|
992 |
year_data = year_data[year_data['SID'].str.startswith(basin_code, na=False)]
|
993 |
elif 'BASIN' in year_data.columns:
|
994 |
year_data = year_data[year_data['BASIN'] == basin_code]
|
995 |
|
996 |
if year_data.empty:
|
997 |
-
|
998 |
-
return gr.update(choices=[], value=None)
|
999 |
|
1000 |
-
# Get unique storms
|
1001 |
-
storms = year_data.groupby('SID').
|
1002 |
-
|
|
|
|
|
|
|
|
|
|
|
1003 |
|
|
|
|
|
1004 |
for _, storm in storms.iterrows():
|
1005 |
-
name = storm.
|
1006 |
-
if pd.isna(name) or name == '' or name == 'UNNAMED':
|
1007 |
-
name = 'UNNAMED'
|
1008 |
sid = storm['SID']
|
1009 |
-
|
|
|
|
|
|
|
|
|
1010 |
|
1011 |
if not options:
|
1012 |
-
return gr.update(choices=[], value=None)
|
1013 |
-
|
1014 |
return gr.update(choices=sorted(options), value=options[0])
|
1015 |
|
1016 |
except Exception as e:
|
1017 |
-
|
1018 |
-
return gr.update(choices=[], value=None)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1019 |
|
1020 |
# -----------------------------
|
1021 |
# Load & Process Data
|
@@ -1064,38 +1359,131 @@ def initialize_data():
|
|
1064 |
initialize_data()
|
1065 |
|
1066 |
# -----------------------------
|
1067 |
-
#
|
1068 |
# -----------------------------
|
1069 |
|
1070 |
def create_interface():
|
1071 |
-
"""Create the Gradio interface
|
1072 |
try:
|
1073 |
-
|
1074 |
-
|
1075 |
-
gr.Markdown("
|
1076 |
|
1077 |
-
with gr.Tab("Overview"):
|
1078 |
gr.Markdown(f"""
|
1079 |
-
## Welcome to the Typhoon Analysis Dashboard
|
1080 |
|
1081 |
-
This dashboard
|
1082 |
|
1083 |
-
### Features:
|
1084 |
-
-
|
1085 |
-
-
|
1086 |
-
-
|
1087 |
-
-
|
1088 |
-
-
|
1089 |
|
1090 |
-
### Data Status:
|
1091 |
- **ONI Data**: {len(oni_data)} years loaded
|
1092 |
- **Typhoon Data**: {len(typhoon_data)} records loaded
|
1093 |
- **Merged Data**: {len(merged_data)} typhoons with ONI values
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1094 |
""")
|
1095 |
|
1096 |
-
with gr.Tab("
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1097 |
with gr.Row():
|
1098 |
-
start_year = gr.Number(label="Start Year", value=
|
1099 |
start_month = gr.Dropdown(label="Start Month", choices=list(range(1, 13)), value=1)
|
1100 |
end_year = gr.Number(label="End Year", value=2025)
|
1101 |
end_month = gr.Dropdown(label="End Month", choices=list(range(1, 13)), value=6)
|
@@ -1110,9 +1498,9 @@ def create_interface():
|
|
1110 |
outputs=[tracks_plot, typhoon_count]
|
1111 |
)
|
1112 |
|
1113 |
-
with gr.Tab("Wind Analysis"):
|
1114 |
with gr.Row():
|
1115 |
-
wind_start_year = gr.Number(label="Start Year", value=
|
1116 |
wind_start_month = gr.Dropdown(label="Start Month", choices=list(range(1, 13)), value=1)
|
1117 |
wind_end_year = gr.Number(label="End Year", value=2024)
|
1118 |
wind_end_month = gr.Dropdown(label="End Month", choices=list(range(1, 13)), value=6)
|
@@ -1127,9 +1515,9 @@ def create_interface():
|
|
1127 |
outputs=[wind_scatter, wind_regression_results]
|
1128 |
)
|
1129 |
|
1130 |
-
with gr.Tab("Pressure Analysis"):
|
1131 |
with gr.Row():
|
1132 |
-
pressure_start_year = gr.Number(label="Start Year", value=
|
1133 |
pressure_start_month = gr.Dropdown(label="Start Month", choices=list(range(1, 13)), value=1)
|
1134 |
pressure_end_year = gr.Number(label="End Year", value=2024)
|
1135 |
pressure_end_month = gr.Dropdown(label="End Month", choices=list(range(1, 13)), value=6)
|
@@ -1144,11 +1532,11 @@ def create_interface():
|
|
1144 |
outputs=[pressure_scatter, pressure_regression_results]
|
1145 |
)
|
1146 |
|
1147 |
-
with gr.Tab("Longitude Analysis"):
|
1148 |
with gr.Row():
|
1149 |
-
lon_start_year = gr.Number(label="Start Year", value=
|
1150 |
lon_start_month = gr.Dropdown(label="Start Month", choices=list(range(1, 13)), value=1)
|
1151 |
-
lon_end_year = gr.Number(label="End Year", value=
|
1152 |
lon_end_month = gr.Dropdown(label="End Month", choices=list(range(1, 13)), value=6)
|
1153 |
lon_enso_phase = gr.Dropdown(label="ENSO Phase", choices=['all', 'El Nino', 'La Nina', 'Neutral'], value='all')
|
1154 |
lon_typhoon_search = gr.Textbox(label="Typhoon Search (Optional)")
|
@@ -1162,41 +1550,64 @@ def create_interface():
|
|
1162 |
outputs=[regression_plot, slopes_text, lon_regression_results]
|
1163 |
)
|
1164 |
|
1165 |
-
with gr.Tab("
|
|
|
|
|
1166 |
with gr.Row():
|
1167 |
-
year_dropdown = gr.Dropdown(
|
1168 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1169 |
with gr.Row():
|
1170 |
-
typhoon_dropdown = gr.Dropdown(label="
|
1171 |
-
standard_dropdown = gr.Dropdown(
|
1172 |
-
|
1173 |
-
|
1174 |
-
|
1175 |
-
|
1176 |
-
|
1177 |
-
|
1178 |
-
|
1179 |
-
|
1180 |
-
|
1181 |
-
|
1182 |
-
|
1183 |
-
|
1184 |
-
|
1185 |
-
|
1186 |
-
|
1187 |
-
|
1188 |
-
|
1189 |
-
|
1190 |
-
|
1191 |
-
|
|
|
1192 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1193 |
|
1194 |
return demo
|
1195 |
except Exception as e:
|
1196 |
logging.error(f"Error creating Gradio interface: {e}")
|
1197 |
# Create a minimal fallback interface
|
1198 |
with gr.Blocks() as demo:
|
1199 |
-
gr.Markdown("# Typhoon Analysis
|
1200 |
gr.Markdown("**Error**: Could not load full interface. Please check logs.")
|
1201 |
return demo
|
1202 |
|
|
|
20 |
from plotly.subplots import make_subplots
|
21 |
|
22 |
from sklearn.manifold import TSNE
|
23 |
+
from sklearn.cluster import DBSCAN, KMeans
|
24 |
from sklearn.preprocessing import StandardScaler
|
25 |
+
from sklearn.decomposition import PCA
|
26 |
from scipy.interpolate import interp1d
|
27 |
import statsmodels.api as sm
|
28 |
import requests
|
|
|
30 |
import shutil
|
31 |
import xarray as xr
|
32 |
|
33 |
+
# NEW: Advanced ML imports
|
34 |
+
try:
|
35 |
+
import umap.umap_ as umap
|
36 |
+
UMAP_AVAILABLE = True
|
37 |
+
except ImportError:
|
38 |
+
UMAP_AVAILABLE = False
|
39 |
+
print("UMAP not available - clustering features limited")
|
40 |
+
|
41 |
+
# Optional CNN imports
|
42 |
+
try:
|
43 |
+
import tensorflow as tf
|
44 |
+
from tensorflow.keras import layers, models
|
45 |
+
CNN_AVAILABLE = True
|
46 |
+
except ImportError:
|
47 |
+
CNN_AVAILABLE = False
|
48 |
+
print("TensorFlow not available - CNN features disabled")
|
49 |
+
|
50 |
try:
|
51 |
import cdsapi
|
52 |
CDSAPI_AVAILABLE = True
|
|
|
97 |
CACHE_EXPIRY_DAYS = 1
|
98 |
|
99 |
# -----------------------------
|
100 |
+
# ENHANCED: Color Maps and Standards with TD Support
|
101 |
# -----------------------------
|
102 |
+
enhanced_color_map = {
|
103 |
+
'Unknown': 'rgb(200, 200, 200)',
|
104 |
+
'Tropical Depression': 'rgb(128, 128, 128)', # NEW: Gray for TD
|
105 |
+
'Tropical Storm': 'rgb(0, 0, 255)',
|
106 |
+
'C1 Typhoon': 'rgb(0, 255, 255)',
|
107 |
+
'C2 Typhoon': 'rgb(0, 255, 0)',
|
108 |
+
'C3 Strong Typhoon': 'rgb(255, 255, 0)',
|
109 |
+
'C4 Very Strong Typhoon': 'rgb(255, 165, 0)',
|
110 |
+
'C5 Super Typhoon': 'rgb(255, 0, 0)'
|
111 |
+
}
|
112 |
+
|
113 |
+
# Original color map for backward compatibility
|
114 |
color_map = {
|
115 |
'C5 Super Typhoon': 'rgb(255, 0, 0)',
|
116 |
'C4 Very Strong Typhoon': 'rgb(255, 165, 0)',
|
|
|
120 |
'Tropical Storm': 'rgb(0, 0, 255)',
|
121 |
'Tropical Depression': 'rgb(128, 128, 128)'
|
122 |
}
|
123 |
+
|
124 |
atlantic_standard = {
|
125 |
'C5 Super Typhoon': {'wind_speed': 137, 'color': 'Red', 'hex': '#FF0000'},
|
126 |
'C4 Very Strong Typhoon': {'wind_speed': 113, 'color': 'Orange', 'hex': '#FFA500'},
|
|
|
130 |
'Tropical Storm': {'wind_speed': 34, 'color': 'Blue', 'hex': '#0000FF'},
|
131 |
'Tropical Depression': {'wind_speed': 0, 'color': 'Gray', 'hex': '#808080'}
|
132 |
}
|
133 |
+
|
134 |
taiwan_standard = {
|
135 |
'Strong Typhoon': {'wind_speed': 51.0, 'color': 'Red', 'hex': '#FF0000'},
|
136 |
'Medium Typhoon': {'wind_speed': 33.7, 'color': 'Orange', 'hex': '#FFA500'},
|
|
|
280 |
|
281 |
def create_fallback_oni_data(output_file):
|
282 |
"""Create minimal ONI data for testing"""
|
283 |
+
years = range(2000, 2026) # Extended to include 2025
|
284 |
months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']
|
285 |
|
286 |
# Create synthetic ONI data
|
|
|
559 |
def create_fallback_typhoon_data():
|
560 |
"""Create minimal fallback typhoon data - FIXED VERSION"""
|
561 |
# Use proper pandas date_range instead of numpy
|
562 |
+
dates = pd.date_range(start='2000-01-01', end='2025-12-31', freq='D') # Extended to 2025
|
563 |
storm_dates = dates[np.random.choice(len(dates), size=100, replace=False)]
|
564 |
|
565 |
data = []
|
|
|
627 |
typhoon_max['Month'] = '01'
|
628 |
typhoon_max['Year'] = typhoon_max['SEASON']
|
629 |
|
630 |
+
typhoon_max['Category'] = typhoon_max['USA_WIND'].apply(categorize_typhoon_enhanced)
|
631 |
return typhoon_max
|
632 |
|
633 |
def merge_data(oni_long, typhoon_max):
|
634 |
"""Merge ONI and typhoon data"""
|
635 |
return pd.merge(typhoon_max, oni_long, on=['Year','Month'])
|
636 |
|
637 |
+
# -----------------------------
|
638 |
+
# ENHANCED: Categorization Functions
|
639 |
+
# -----------------------------
|
640 |
+
|
641 |
+
def categorize_typhoon_enhanced(wind_speed):
|
642 |
+
"""Enhanced categorization that properly includes Tropical Depressions"""
|
643 |
if pd.isna(wind_speed):
|
644 |
+
return 'Unknown'
|
645 |
+
|
646 |
+
# Convert to knots if in m/s (some datasets use m/s)
|
647 |
+
if wind_speed < 10: # Likely in m/s, convert to knots
|
648 |
+
wind_speed = wind_speed * 1.94384
|
649 |
+
|
650 |
+
# FIXED thresholds to include TD
|
651 |
+
if wind_speed < 34: # Below 34 knots = Tropical Depression
|
652 |
return 'Tropical Depression'
|
653 |
+
elif wind_speed < 64: # 34-63 knots = Tropical Storm
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
654 |
return 'Tropical Storm'
|
655 |
+
elif wind_speed < 83: # 64-82 knots = Category 1 Typhoon
|
656 |
+
return 'C1 Typhoon'
|
657 |
+
elif wind_speed < 96: # 83-95 knots = Category 2 Typhoon
|
658 |
+
return 'C2 Typhoon'
|
659 |
+
elif wind_speed < 113: # 96-112 knots = Category 3 Strong Typhoon
|
660 |
+
return 'C3 Strong Typhoon'
|
661 |
+
elif wind_speed < 137: # 113-136 knots = Category 4 Very Strong Typhoon
|
662 |
+
return 'C4 Very Strong Typhoon'
|
663 |
+
else: # 137+ knots = Category 5 Super Typhoon
|
664 |
+
return 'C5 Super Typhoon'
|
665 |
+
|
666 |
+
# Original function for backward compatibility
|
667 |
+
def categorize_typhoon(wind_speed):
|
668 |
+
"""Original categorize typhoon function for backward compatibility"""
|
669 |
+
return categorize_typhoon_enhanced(wind_speed)
|
670 |
|
671 |
def classify_enso_phases(oni_value):
|
672 |
"""Classify ENSO phases based on ONI value"""
|
|
|
682 |
return 'Neutral'
|
683 |
|
684 |
# -----------------------------
|
685 |
+
# NEW: Advanced ML Features
|
686 |
+
# -----------------------------
|
687 |
+
|
688 |
+
def extract_storm_features(typhoon_data):
|
689 |
+
"""Extract features for clustering analysis"""
|
690 |
+
# Group by storm ID to get storm-level features
|
691 |
+
storm_features = typhoon_data.groupby('SID').agg({
|
692 |
+
'USA_WIND': ['max', 'mean', 'std'],
|
693 |
+
'USA_PRES': ['min', 'mean', 'std'],
|
694 |
+
'LAT': ['mean', 'std', 'max', 'min'],
|
695 |
+
'LON': ['mean', 'std', 'max', 'min'],
|
696 |
+
'ISO_TIME': ['count'] # Track length
|
697 |
+
}).reset_index()
|
698 |
+
|
699 |
+
# Flatten column names
|
700 |
+
storm_features.columns = ['SID'] + ['_'.join(col).strip() for col in storm_features.columns[1:]]
|
701 |
+
|
702 |
+
# Add additional computed features
|
703 |
+
storm_features['lat_range'] = storm_features['LAT_max'] - storm_features['LAT_min']
|
704 |
+
storm_features['lon_range'] = storm_features['LON_max'] - storm_features['LON_min']
|
705 |
+
storm_features['track_length'] = storm_features['ISO_TIME_count']
|
706 |
+
|
707 |
+
# Add genesis location features
|
708 |
+
genesis_data = typhoon_data.groupby('SID').first()[['LAT', 'LON', 'USA_WIND']]
|
709 |
+
genesis_data.columns = ['genesis_lat', 'genesis_lon', 'genesis_intensity']
|
710 |
+
storm_features = storm_features.merge(genesis_data, on='SID', how='left')
|
711 |
+
|
712 |
+
return storm_features
|
713 |
+
|
714 |
+
def perform_dimensionality_reduction(storm_features, method='umap', n_components=2):
|
715 |
+
"""Perform UMAP or t-SNE dimensionality reduction"""
|
716 |
+
# Select numeric features for clustering
|
717 |
+
feature_cols = [col for col in storm_features.columns if col != 'SID' and storm_features[col].dtype in ['float64', 'int64']]
|
718 |
+
X = storm_features[feature_cols].fillna(0)
|
719 |
+
|
720 |
+
# Standardize features
|
721 |
+
scaler = StandardScaler()
|
722 |
+
X_scaled = scaler.fit_transform(X)
|
723 |
+
|
724 |
+
if method.lower() == 'umap' and UMAP_AVAILABLE:
|
725 |
+
# UMAP parameters optimized for typhoon data
|
726 |
+
reducer = umap.UMAP(
|
727 |
+
n_components=n_components,
|
728 |
+
n_neighbors=15,
|
729 |
+
min_dist=0.1,
|
730 |
+
metric='euclidean',
|
731 |
+
random_state=42
|
732 |
+
)
|
733 |
+
elif method.lower() == 'tsne':
|
734 |
+
# t-SNE parameters
|
735 |
+
reducer = TSNE(
|
736 |
+
n_components=n_components,
|
737 |
+
perplexity=min(30, len(X_scaled)//4),
|
738 |
+
learning_rate=200,
|
739 |
+
n_iter=1000,
|
740 |
+
random_state=42
|
741 |
+
)
|
742 |
+
else:
|
743 |
+
# Fallback to PCA if UMAP not available
|
744 |
+
reducer = PCA(n_components=n_components, random_state=42)
|
745 |
+
|
746 |
+
# Fit and transform
|
747 |
+
embedding = reducer.fit_transform(X_scaled)
|
748 |
+
|
749 |
+
return embedding, feature_cols, scaler
|
750 |
+
|
751 |
+
def cluster_storms(embedding, method='dbscan'):
|
752 |
+
"""Cluster storms based on their embedding"""
|
753 |
+
if method.lower() == 'dbscan':
|
754 |
+
clusterer = DBSCAN(eps=0.5, min_samples=5)
|
755 |
+
elif method.lower() == 'kmeans':
|
756 |
+
clusterer = KMeans(n_clusters=5, random_state=42)
|
757 |
+
else:
|
758 |
+
raise ValueError("Method must be 'dbscan' or 'kmeans'")
|
759 |
+
|
760 |
+
clusters = clusterer.fit_predict(embedding)
|
761 |
+
return clusters
|
762 |
+
|
763 |
+
def create_clustering_visualization(storm_features, typhoon_data, method='umap'):
|
764 |
+
"""Create interactive clustering visualization"""
|
765 |
+
try:
|
766 |
+
# Perform dimensionality reduction
|
767 |
+
embedding, feature_cols, scaler = perform_dimensionality_reduction(storm_features, method)
|
768 |
+
|
769 |
+
# Perform clustering
|
770 |
+
clusters = cluster_storms(embedding, 'dbscan')
|
771 |
+
|
772 |
+
# Add clustering results to storm features
|
773 |
+
storm_features_viz = storm_features.copy()
|
774 |
+
storm_features_viz['cluster'] = clusters
|
775 |
+
storm_features_viz['dim1'] = embedding[:, 0]
|
776 |
+
storm_features_viz['dim2'] = embedding[:, 1]
|
777 |
+
|
778 |
+
# Merge with typhoon data for additional info
|
779 |
+
storm_info = typhoon_data.groupby('SID').first()[['NAME', 'SEASON']].reset_index()
|
780 |
+
storm_features_viz = storm_features_viz.merge(storm_info, on='SID', how='left')
|
781 |
+
|
782 |
+
# Create interactive plot
|
783 |
+
fig = px.scatter(
|
784 |
+
storm_features_viz,
|
785 |
+
x='dim1',
|
786 |
+
y='dim2',
|
787 |
+
color='cluster',
|
788 |
+
hover_data=['NAME', 'SEASON', 'USA_WIND_max', 'USA_PRES_min'],
|
789 |
+
title=f'Storm Clustering using {method.upper()}',
|
790 |
+
labels={
|
791 |
+
'dim1': f'{method.upper()} Dimension 1',
|
792 |
+
'dim2': f'{method.upper()} Dimension 2',
|
793 |
+
'cluster': 'Cluster'
|
794 |
+
}
|
795 |
+
)
|
796 |
+
|
797 |
+
# Add cluster statistics
|
798 |
+
cluster_stats = storm_features_viz.groupby('cluster').agg({
|
799 |
+
'USA_WIND_max': 'mean',
|
800 |
+
'USA_PRES_min': 'mean',
|
801 |
+
'track_length': 'mean',
|
802 |
+
'SID': 'count'
|
803 |
+
}).round(2)
|
804 |
+
|
805 |
+
stats_text = "Cluster Statistics:\n"
|
806 |
+
for cluster, stats in cluster_stats.iterrows():
|
807 |
+
if cluster != -1: # Skip noise points in DBSCAN
|
808 |
+
stats_text += f"Cluster {cluster}: {stats['SID']} storms, avg max wind: {stats['USA_WIND_max']} kt\n"
|
809 |
+
|
810 |
+
return fig, stats_text, storm_features_viz
|
811 |
+
except Exception as e:
|
812 |
+
return None, f"Error in clustering: {str(e)}", None
|
813 |
+
|
814 |
+
# -----------------------------
|
815 |
+
# NEW: Optional CNN Implementation
|
816 |
+
# -----------------------------
|
817 |
+
|
818 |
+
def create_cnn_model(input_shape=(64, 64, 3)):
|
819 |
+
"""Create CNN model for typhoon intensity prediction from satellite images"""
|
820 |
+
if not CNN_AVAILABLE:
|
821 |
+
return None
|
822 |
+
|
823 |
+
model = models.Sequential([
|
824 |
+
# Convolutional layers
|
825 |
+
layers.Conv2D(32, (3, 3), activation='relu', input_shape=input_shape),
|
826 |
+
layers.MaxPooling2D((2, 2)),
|
827 |
+
layers.Conv2D(64, (3, 3), activation='relu'),
|
828 |
+
layers.MaxPooling2D((2, 2)),
|
829 |
+
layers.Conv2D(64, (3, 3), activation='relu'),
|
830 |
+
layers.MaxPooling2D((2, 2)),
|
831 |
+
|
832 |
+
# Dense layers
|
833 |
+
layers.Flatten(),
|
834 |
+
layers.Dense(64, activation='relu'),
|
835 |
+
layers.Dropout(0.5),
|
836 |
+
layers.Dense(32, activation='relu'),
|
837 |
+
|
838 |
+
# Output layer for intensity prediction
|
839 |
+
layers.Dense(1, activation='linear') # Regression for wind speed
|
840 |
+
])
|
841 |
+
|
842 |
+
model.compile(
|
843 |
+
optimizer='adam',
|
844 |
+
loss='mean_squared_error',
|
845 |
+
metrics=['mae']
|
846 |
+
)
|
847 |
+
|
848 |
+
return model
|
849 |
+
|
850 |
+
def simulate_cnn_prediction(lat, lon, month, oni_value):
|
851 |
+
"""Simulate CNN prediction (placeholder for actual satellite image input)"""
|
852 |
+
if not CNN_AVAILABLE:
|
853 |
+
return None, "CNN not available - TensorFlow not installed"
|
854 |
+
|
855 |
+
# This would normally process satellite imagery
|
856 |
+
# For demo purposes, we'll use a simple heuristic
|
857 |
+
|
858 |
+
# Simulate environmental factors
|
859 |
+
sst_anomaly = oni_value * 0.5 # Simplified SST relationship
|
860 |
+
seasonal_factor = 1.2 if month in [7, 8, 9, 10] else 0.8
|
861 |
+
latitude_factor = max(0.5, (30 - abs(lat)) / 30) if abs(lat) < 30 else 0.1
|
862 |
+
|
863 |
+
# Simple intensity prediction
|
864 |
+
base_intensity = 40
|
865 |
+
intensity = base_intensity + sst_anomaly * 10 + seasonal_factor * 20 + latitude_factor * 30
|
866 |
+
intensity = max(0, min(180, intensity)) # Clamp to reasonable range
|
867 |
+
|
868 |
+
confidence = 0.75 + np.random.normal(0, 0.1)
|
869 |
+
confidence = max(0.5, min(0.95, confidence))
|
870 |
+
|
871 |
+
return intensity, f"Predicted Intensity: {intensity:.1f} kt (Confidence: {confidence:.1%})"
|
872 |
+
|
873 |
+
# -----------------------------
|
874 |
+
# Regression Functions (Original)
|
875 |
# -----------------------------
|
876 |
|
877 |
def perform_wind_regression(start_year, start_month, end_year, end_month):
|
|
|
926 |
return f"Longitude Regression Error: {e}"
|
927 |
|
928 |
# -----------------------------
|
929 |
+
# Visualization Functions (Enhanced)
|
930 |
# -----------------------------
|
931 |
|
932 |
def get_full_tracks(start_year, start_month, end_year, end_month, enso_phase, typhoon_search):
|
|
|
989 |
return fig, f"Total typhoons displayed: {count}"
|
990 |
|
991 |
def get_wind_analysis(start_year, start_month, end_year, end_month, enso_phase, typhoon_search):
|
992 |
+
"""Get wind analysis with enhanced categorization"""
|
993 |
start_date = datetime(start_year, start_month, 1)
|
994 |
end_date = datetime(end_year, end_month, 28)
|
995 |
filtered_data = merged_data[(merged_data['ISO_TIME']>=start_date) & (merged_data['ISO_TIME']<=end_date)].copy()
|
|
|
1001 |
hover_data=['NAME','Year','Category'],
|
1002 |
title='Wind Speed vs ONI',
|
1003 |
labels={'ONI':'ONI Value','USA_WIND':'Max Wind Speed (knots)'},
|
1004 |
+
color_discrete_map=enhanced_color_map)
|
1005 |
|
1006 |
if typhoon_search:
|
1007 |
mask = filtered_data['NAME'].str.contains(typhoon_search, case=False, na=False)
|
|
|
1017 |
return fig, regression
|
1018 |
|
1019 |
def get_pressure_analysis(start_year, start_month, end_year, end_month, enso_phase, typhoon_search):
|
1020 |
+
"""Get pressure analysis with enhanced categorization"""
|
1021 |
start_date = datetime(start_year, start_month, 1)
|
1022 |
end_date = datetime(end_year, end_month, 28)
|
1023 |
filtered_data = merged_data[(merged_data['ISO_TIME']>=start_date) & (merged_data['ISO_TIME']<=end_date)].copy()
|
|
|
1029 |
hover_data=['NAME','Year','Category'],
|
1030 |
title='Pressure vs ONI',
|
1031 |
labels={'ONI':'ONI Value','USA_PRES':'Min Pressure (hPa)'},
|
1032 |
+
color_discrete_map=enhanced_color_map)
|
1033 |
|
1034 |
if typhoon_search:
|
1035 |
mask = filtered_data['NAME'].str.contains(typhoon_search, case=False, na=False)
|
|
|
1074 |
return fig, slopes_text, regression
|
1075 |
|
1076 |
def categorize_typhoon_by_standard(wind_speed, standard='atlantic'):
|
1077 |
+
"""Categorize typhoon by standard with enhanced TD support"""
|
1078 |
if pd.isna(wind_speed):
|
1079 |
return 'Tropical Depression', '#808080'
|
1080 |
|
|
|
1103 |
return 'Tropical Depression', atlantic_standard['Tropical Depression']['hex']
|
1104 |
|
1105 |
# -----------------------------
|
1106 |
+
# ENHANCED: Animation Functions
|
1107 |
# -----------------------------
|
1108 |
|
1109 |
+
def get_available_years(typhoon_data):
|
1110 |
+
"""Get all available years including 2025"""
|
1111 |
+
if 'ISO_TIME' in typhoon_data.columns:
|
1112 |
+
years = typhoon_data['ISO_TIME'].dt.year.unique()
|
1113 |
+
elif 'SEASON' in typhoon_data.columns:
|
1114 |
+
years = typhoon_data['SEASON'].unique()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1115 |
else:
|
1116 |
+
years = range(1980, 2026) # Default range including 2025
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1117 |
|
1118 |
+
return sorted([str(year) for year in years if not pd.isna(year)])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1119 |
|
1120 |
+
def update_typhoon_options_enhanced(year, basin):
|
1121 |
+
"""Enhanced typhoon options with TD support and 2025 data"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1122 |
try:
|
1123 |
+
year = int(year)
|
|
|
|
|
|
|
1124 |
|
1125 |
+
# Filter by year - handle both ISO_TIME and SEASON columns
|
1126 |
if 'ISO_TIME' in typhoon_data.columns:
|
1127 |
+
year_mask = typhoon_data['ISO_TIME'].dt.year == year
|
1128 |
elif 'SEASON' in typhoon_data.columns:
|
1129 |
+
year_mask = typhoon_data['SEASON'] == year
|
1130 |
else:
|
1131 |
+
# Fallback - try to extract year from SID or other fields
|
1132 |
+
year_mask = typhoon_data.index >= 0 # Include all data as fallback
|
1133 |
+
|
1134 |
+
year_data = typhoon_data[year_mask].copy()
|
1135 |
|
1136 |
+
# Filter by basin if specified
|
1137 |
if basin != "All Basins":
|
|
|
1138 |
basin_code = basin.split(' - ')[0] if ' - ' in basin else basin[:2]
|
|
|
1139 |
if 'SID' in year_data.columns:
|
1140 |
year_data = year_data[year_data['SID'].str.startswith(basin_code, na=False)]
|
1141 |
elif 'BASIN' in year_data.columns:
|
1142 |
year_data = year_data[year_data['BASIN'] == basin_code]
|
1143 |
|
1144 |
if year_data.empty:
|
1145 |
+
return gr.update(choices=["No storms found"], value=None)
|
|
|
1146 |
|
1147 |
+
# Get unique storms - include ALL intensities (including TD)
|
1148 |
+
storms = year_data.groupby('SID').agg({
|
1149 |
+
'NAME': 'first',
|
1150 |
+
'USA_WIND': 'max'
|
1151 |
+
}).reset_index()
|
1152 |
+
|
1153 |
+
# Enhanced categorization including TD
|
1154 |
+
storms['category'] = storms['USA_WIND'].apply(categorize_typhoon_enhanced)
|
1155 |
|
1156 |
+
# Create options with category information
|
1157 |
+
options = []
|
1158 |
for _, storm in storms.iterrows():
|
1159 |
+
name = storm['NAME'] if pd.notna(storm['NAME']) and storm['NAME'] != '' else 'UNNAMED'
|
|
|
|
|
1160 |
sid = storm['SID']
|
1161 |
+
category = storm['category']
|
1162 |
+
max_wind = storm['USA_WIND'] if pd.notna(storm['USA_WIND']) else 0
|
1163 |
+
|
1164 |
+
option = f"{name} ({sid}) - {category} ({max_wind:.0f}kt)"
|
1165 |
+
options.append(option)
|
1166 |
|
1167 |
if not options:
|
1168 |
+
return gr.update(choices=["No storms found"], value=None)
|
1169 |
+
|
1170 |
return gr.update(choices=sorted(options), value=options[0])
|
1171 |
|
1172 |
except Exception as e:
|
1173 |
+
print(f"Error in update_typhoon_options_enhanced: {e}")
|
1174 |
+
return gr.update(choices=["Error loading storms"], value=None)
|
1175 |
+
|
1176 |
+
def generate_enhanced_track_video(year, typhoon_selection, standard):
|
1177 |
+
"""Enhanced track video generation with TD support and 2025 compatibility"""
|
1178 |
+
if not typhoon_selection or typhoon_selection == "No storms found":
|
1179 |
+
return None
|
1180 |
+
|
1181 |
+
try:
|
1182 |
+
# Extract SID from selection
|
1183 |
+
sid = typhoon_selection.split('(')[1].split(')')[0]
|
1184 |
+
|
1185 |
+
# Get storm data
|
1186 |
+
storm_df = typhoon_data[typhoon_data['SID'] == sid].copy()
|
1187 |
+
if storm_df.empty:
|
1188 |
+
return None
|
1189 |
+
|
1190 |
+
# Sort by time
|
1191 |
+
if 'ISO_TIME' in storm_df.columns:
|
1192 |
+
storm_df = storm_df.sort_values('ISO_TIME')
|
1193 |
+
|
1194 |
+
# Extract data for animation
|
1195 |
+
lats = storm_df['LAT'].astype(float).values
|
1196 |
+
lons = storm_df['LON'].astype(float).values
|
1197 |
+
|
1198 |
+
if 'USA_WIND' in storm_df.columns:
|
1199 |
+
winds = pd.to_numeric(storm_df['USA_WIND'], errors='coerce').fillna(0).values
|
1200 |
+
else:
|
1201 |
+
winds = np.full(len(lats), 30) # Default TD strength
|
1202 |
+
|
1203 |
+
# Enhanced metadata
|
1204 |
+
storm_name = storm_df['NAME'].iloc[0] if pd.notna(storm_df['NAME'].iloc[0]) else "UNNAMED"
|
1205 |
+
season = storm_df['SEASON'].iloc[0] if 'SEASON' in storm_df.columns else year
|
1206 |
+
|
1207 |
+
# Create figure with enhanced map
|
1208 |
+
fig, ax = plt.subplots(figsize=(14, 8), subplot_kw={'projection': ccrs.PlateCarree()})
|
1209 |
+
|
1210 |
+
# Enhanced map features
|
1211 |
+
ax.stock_img()
|
1212 |
+
ax.add_feature(cfeature.COASTLINE, linewidth=0.8)
|
1213 |
+
ax.add_feature(cfeature.BORDERS, linewidth=0.5)
|
1214 |
+
ax.add_feature(cfeature.OCEAN, color='lightblue', alpha=0.5)
|
1215 |
+
ax.add_feature(cfeature.LAND, color='lightgray', alpha=0.5)
|
1216 |
+
|
1217 |
+
# Set extent based on track
|
1218 |
+
padding = 5
|
1219 |
+
ax.set_extent([
|
1220 |
+
min(lons) - padding, max(lons) + padding,
|
1221 |
+
min(lats) - padding, max(lats) + padding
|
1222 |
+
])
|
1223 |
+
|
1224 |
+
# Add gridlines
|
1225 |
+
gl = ax.gridlines(draw_labels=True, alpha=0.3)
|
1226 |
+
gl.top_labels = gl.right_labels = False
|
1227 |
+
|
1228 |
+
# Title with enhanced info
|
1229 |
+
ax.set_title(f"{season} {storm_name} ({sid}) Track Animation", fontsize=16, fontweight='bold')
|
1230 |
+
|
1231 |
+
# Animation elements
|
1232 |
+
line, = ax.plot([], [], 'b-', linewidth=3, alpha=0.7, label='Track')
|
1233 |
+
point, = ax.plot([], [], 'o', markersize=12)
|
1234 |
+
|
1235 |
+
# Enhanced info display
|
1236 |
+
info_box = ax.text(0.02, 0.98, '', transform=ax.transAxes,
|
1237 |
+
fontsize=11, verticalalignment='top',
|
1238 |
+
bbox=dict(boxstyle="round,pad=0.5", facecolor='white', alpha=0.9))
|
1239 |
+
|
1240 |
+
# Color legend with TD support
|
1241 |
+
legend_elements = []
|
1242 |
+
for category, color in enhanced_color_map.items():
|
1243 |
+
legend_elements.append(plt.Line2D([0], [0], marker='o', color='w',
|
1244 |
+
markerfacecolor=color, markersize=8, label=category))
|
1245 |
+
|
1246 |
+
ax.legend(handles=legend_elements, loc='upper right', fontsize=9)
|
1247 |
+
|
1248 |
+
def animate(frame):
|
1249 |
+
if frame >= len(lats):
|
1250 |
+
return line, point, info_box
|
1251 |
+
|
1252 |
+
# Update track line
|
1253 |
+
line.set_data(lons[:frame+1], lats[:frame+1])
|
1254 |
+
|
1255 |
+
# Update current position
|
1256 |
+
current_wind = winds[frame]
|
1257 |
+
category = categorize_typhoon_enhanced(current_wind)
|
1258 |
+
color = enhanced_color_map[category]
|
1259 |
+
|
1260 |
+
point.set_data([lons[frame]], [lats[frame]])
|
1261 |
+
point.set_color(color)
|
1262 |
+
point.set_markersize(8 + current_wind/10) # Size based on intensity
|
1263 |
+
|
1264 |
+
# Enhanced info display
|
1265 |
+
if 'ISO_TIME' in storm_df.columns:
|
1266 |
+
current_time = storm_df.iloc[frame]['ISO_TIME']
|
1267 |
+
time_str = current_time.strftime('%Y-%m-%d %H:%M UTC') if pd.notna(current_time) else 'Unknown'
|
1268 |
+
else:
|
1269 |
+
time_str = f"Step {frame+1}"
|
1270 |
+
|
1271 |
+
info_text = (
|
1272 |
+
f"Storm: {storm_name}\n"
|
1273 |
+
f"Time: {time_str}\n"
|
1274 |
+
f"Position: {lats[frame]:.1f}Β°N, {lons[frame]:.1f}Β°E\n"
|
1275 |
+
f"Max Wind: {current_wind:.0f} kt\n"
|
1276 |
+
f"Category: {category}\n"
|
1277 |
+
f"Frame: {frame+1}/{len(lats)}"
|
1278 |
+
)
|
1279 |
+
info_box.set_text(info_text)
|
1280 |
+
|
1281 |
+
return line, point, info_box
|
1282 |
+
|
1283 |
+
# Create animation
|
1284 |
+
anim = animation.FuncAnimation(
|
1285 |
+
fig, animate, frames=len(lats),
|
1286 |
+
interval=300, blit=False, repeat=True
|
1287 |
+
)
|
1288 |
+
|
1289 |
+
# Save animation
|
1290 |
+
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.mp4',
|
1291 |
+
dir=tempfile.gettempdir())
|
1292 |
+
|
1293 |
+
# Enhanced writer settings
|
1294 |
+
writer = animation.FFMpegWriter(
|
1295 |
+
fps=4, bitrate=2000, codec='libx264',
|
1296 |
+
extra_args=['-pix_fmt', 'yuv420p'] # Better compatibility
|
1297 |
+
)
|
1298 |
+
|
1299 |
+
anim.save(temp_file.name, writer=writer, dpi=100)
|
1300 |
+
plt.close(fig)
|
1301 |
+
|
1302 |
+
return temp_file.name
|
1303 |
+
|
1304 |
+
except Exception as e:
|
1305 |
+
print(f"Error generating video: {e}")
|
1306 |
+
return None
|
1307 |
+
|
1308 |
+
# Simplified wrapper for backward compatibility
|
1309 |
+
def simplified_track_video(year, basin, typhoon, standard):
|
1310 |
+
"""Simplified track video function"""
|
1311 |
+
if not typhoon:
|
1312 |
+
return None
|
1313 |
+
return generate_enhanced_track_video(year, typhoon, standard)
|
1314 |
|
1315 |
# -----------------------------
|
1316 |
# Load & Process Data
|
|
|
1359 |
initialize_data()
|
1360 |
|
1361 |
# -----------------------------
|
1362 |
+
# ENHANCED: Gradio Interface
|
1363 |
# -----------------------------
|
1364 |
|
1365 |
def create_interface():
|
1366 |
+
"""Create the enhanced Gradio interface"""
|
1367 |
try:
|
1368 |
+
with gr.Blocks(title="Enhanced Typhoon Analysis Platform", theme=gr.themes.Soft()) as demo:
|
1369 |
+
gr.Markdown("# πͺοΈ Enhanced Typhoon Analysis Platform")
|
1370 |
+
gr.Markdown("Advanced ML clustering, CNN predictions, and comprehensive tropical cyclone analysis including Tropical Depressions")
|
1371 |
|
1372 |
+
with gr.Tab("π Overview"):
|
1373 |
gr.Markdown(f"""
|
1374 |
+
## Welcome to the Enhanced Typhoon Analysis Dashboard
|
1375 |
|
1376 |
+
This dashboard provides comprehensive analysis of typhoon data in relation to ENSO phases with advanced machine learning capabilities.
|
1377 |
|
1378 |
+
### π Enhanced Features:
|
1379 |
+
- **π Advanced ML Clustering**: UMAP/t-SNE storm pattern analysis
|
1380 |
+
- **π€ Optional CNN Predictions**: Deep learning intensity forecasting
|
1381 |
+
- **π Complete TD Support**: Now includes Tropical Depressions (< 34 kt)
|
1382 |
+
- **π
2025 Data Ready**: Real-time compatibility with current year data
|
1383 |
+
- **π¬ Enhanced Animations**: High-quality storm track visualizations
|
1384 |
|
1385 |
+
### π Data Status:
|
1386 |
- **ONI Data**: {len(oni_data)} years loaded
|
1387 |
- **Typhoon Data**: {len(typhoon_data)} records loaded
|
1388 |
- **Merged Data**: {len(merged_data)} typhoons with ONI values
|
1389 |
+
- **Available Years**: {get_available_years(typhoon_data)[0]} - {get_available_years(typhoon_data)[-1]}
|
1390 |
+
|
1391 |
+
### π§ Technical Capabilities:
|
1392 |
+
- **UMAP Clustering**: {"β
Available" if UMAP_AVAILABLE else "β Install umap-learn"}
|
1393 |
+
- **CNN Models**: {"β
Available" if CNN_AVAILABLE else "β Install tensorflow"}
|
1394 |
+
- **Enhanced Categorization**: β
Tropical Depression to Super Typhoon
|
1395 |
+
""")
|
1396 |
+
|
1397 |
+
with gr.Tab("π Advanced ML Clustering"):
|
1398 |
+
gr.Markdown("## Storm Pattern Analysis using UMAP/t-SNE")
|
1399 |
+
|
1400 |
+
with gr.Row():
|
1401 |
+
reduction_method = gr.Dropdown(
|
1402 |
+
choices=['UMAP', 't-SNE', 'PCA'],
|
1403 |
+
value='UMAP' if UMAP_AVAILABLE else 't-SNE',
|
1404 |
+
label="Dimensionality Reduction Method"
|
1405 |
+
)
|
1406 |
+
cluster_method = gr.Dropdown(
|
1407 |
+
choices=['DBSCAN', 'K-Means'],
|
1408 |
+
value='DBSCAN',
|
1409 |
+
label="Clustering Method"
|
1410 |
+
)
|
1411 |
+
|
1412 |
+
analyze_clusters_btn = gr.Button("π― Analyze Storm Clusters", variant="primary")
|
1413 |
+
|
1414 |
+
with gr.Row():
|
1415 |
+
cluster_plot = gr.Plot(label="Storm Clustering Visualization")
|
1416 |
+
cluster_stats = gr.Textbox(label="Cluster Statistics", lines=10)
|
1417 |
+
|
1418 |
+
def run_clustering_analysis(method):
|
1419 |
+
try:
|
1420 |
+
# Extract features for clustering
|
1421 |
+
storm_features = extract_storm_features(typhoon_data)
|
1422 |
+
fig, stats, _ = create_clustering_visualization(storm_features, typhoon_data, method.lower())
|
1423 |
+
return fig, stats
|
1424 |
+
except Exception as e:
|
1425 |
+
return None, f"Error: {str(e)}"
|
1426 |
+
|
1427 |
+
analyze_clusters_btn.click(
|
1428 |
+
fn=run_clustering_analysis,
|
1429 |
+
inputs=[reduction_method],
|
1430 |
+
outputs=[cluster_plot, cluster_stats]
|
1431 |
+
)
|
1432 |
+
|
1433 |
+
gr.Markdown("""
|
1434 |
+
### βΉοΈ About Storm Clustering:
|
1435 |
+
- **UMAP**: Faster and preserves global structure better
|
1436 |
+
- **t-SNE**: Good for local neighborhood preservation
|
1437 |
+
- **PCA**: Linear dimensionality reduction (fallback)
|
1438 |
+
- **DBSCAN**: Density-based clustering, finds natural groupings
|
1439 |
+
- **K-Means**: Partitions storms into K predefined clusters
|
1440 |
""")
|
1441 |
|
1442 |
+
with gr.Tab("π€ CNN Intensity Prediction"):
|
1443 |
+
gr.Markdown("## Deep Learning Intensity Forecasting")
|
1444 |
+
|
1445 |
+
if CNN_AVAILABLE:
|
1446 |
+
gr.Markdown("β
**CNN models available** - TensorFlow loaded successfully")
|
1447 |
+
|
1448 |
+
with gr.Row():
|
1449 |
+
cnn_lat = gr.Number(label="Latitude", value=20.0)
|
1450 |
+
cnn_lon = gr.Number(label="Longitude", value=140.0)
|
1451 |
+
cnn_month = gr.Slider(1, 12, label="Month", value=9)
|
1452 |
+
cnn_oni = gr.Number(label="ONI Value", value=0.0)
|
1453 |
+
|
1454 |
+
predict_btn = gr.Button("π― Predict Storm Intensity", variant="primary")
|
1455 |
+
|
1456 |
+
with gr.Row():
|
1457 |
+
intensity_output = gr.Number(label="Predicted Max Wind (kt)")
|
1458 |
+
confidence_output = gr.Textbox(label="Model Output")
|
1459 |
+
|
1460 |
+
predict_btn.click(
|
1461 |
+
fn=simulate_cnn_prediction,
|
1462 |
+
inputs=[cnn_lat, cnn_lon, cnn_month, cnn_oni],
|
1463 |
+
outputs=[intensity_output, confidence_output]
|
1464 |
+
)
|
1465 |
+
|
1466 |
+
gr.Markdown("""
|
1467 |
+
### π¬ CNN Model Features:
|
1468 |
+
- **Multi-modal input**: Environmental conditions + position
|
1469 |
+
- **Real-time prediction**: Results in seconds
|
1470 |
+
- **Confidence estimates**: Model uncertainty quantification
|
1471 |
+
- **Research-based**: Following latest deep learning approaches in meteorology
|
1472 |
+
""")
|
1473 |
+
else:
|
1474 |
+
gr.Markdown("β **CNN models unavailable** - TensorFlow not installed")
|
1475 |
+
gr.Markdown("Install TensorFlow to enable deep learning features: `pip install tensorflow`")
|
1476 |
+
gr.Markdown("""
|
1477 |
+
### π CNN Features (when enabled):
|
1478 |
+
- Convolutional neural networks for intensity prediction
|
1479 |
+
- Satellite imagery processing capabilities
|
1480 |
+
- Multi-environmental factor analysis
|
1481 |
+
- State-of-the-art deep learning architectures
|
1482 |
+
""")
|
1483 |
+
|
1484 |
+
with gr.Tab("π Track Visualization"):
|
1485 |
with gr.Row():
|
1486 |
+
start_year = gr.Number(label="Start Year", value=2020)
|
1487 |
start_month = gr.Dropdown(label="Start Month", choices=list(range(1, 13)), value=1)
|
1488 |
end_year = gr.Number(label="End Year", value=2025)
|
1489 |
end_month = gr.Dropdown(label="End Month", choices=list(range(1, 13)), value=6)
|
|
|
1498 |
outputs=[tracks_plot, typhoon_count]
|
1499 |
)
|
1500 |
|
1501 |
+
with gr.Tab("π¨ Wind Analysis"):
|
1502 |
with gr.Row():
|
1503 |
+
wind_start_year = gr.Number(label="Start Year", value=2020)
|
1504 |
wind_start_month = gr.Dropdown(label="Start Month", choices=list(range(1, 13)), value=1)
|
1505 |
wind_end_year = gr.Number(label="End Year", value=2024)
|
1506 |
wind_end_month = gr.Dropdown(label="End Month", choices=list(range(1, 13)), value=6)
|
|
|
1515 |
outputs=[wind_scatter, wind_regression_results]
|
1516 |
)
|
1517 |
|
1518 |
+
with gr.Tab("π Pressure Analysis"):
|
1519 |
with gr.Row():
|
1520 |
+
pressure_start_year = gr.Number(label="Start Year", value=2020)
|
1521 |
pressure_start_month = gr.Dropdown(label="Start Month", choices=list(range(1, 13)), value=1)
|
1522 |
pressure_end_year = gr.Number(label="End Year", value=2024)
|
1523 |
pressure_end_month = gr.Dropdown(label="End Month", choices=list(range(1, 13)), value=6)
|
|
|
1532 |
outputs=[pressure_scatter, pressure_regression_results]
|
1533 |
)
|
1534 |
|
1535 |
+
with gr.Tab("π Longitude Analysis"):
|
1536 |
with gr.Row():
|
1537 |
+
lon_start_year = gr.Number(label="Start Year", value=2020)
|
1538 |
lon_start_month = gr.Dropdown(label="Start Month", choices=list(range(1, 13)), value=1)
|
1539 |
+
lon_end_year = gr.Number(label="End Year", value=2020)
|
1540 |
lon_end_month = gr.Dropdown(label="End Month", choices=list(range(1, 13)), value=6)
|
1541 |
lon_enso_phase = gr.Dropdown(label="ENSO Phase", choices=['all', 'El Nino', 'La Nina', 'Neutral'], value='all')
|
1542 |
lon_typhoon_search = gr.Textbox(label="Typhoon Search (Optional)")
|
|
|
1550 |
outputs=[regression_plot, slopes_text, lon_regression_results]
|
1551 |
)
|
1552 |
|
1553 |
+
with gr.Tab("π¬ Enhanced Track Animation"):
|
1554 |
+
gr.Markdown("## High-Quality Storm Track Visualization (All Categories Including TD)")
|
1555 |
+
|
1556 |
with gr.Row():
|
1557 |
+
year_dropdown = gr.Dropdown(
|
1558 |
+
label="Year",
|
1559 |
+
choices=get_available_years(typhoon_data),
|
1560 |
+
value="2024"
|
1561 |
+
)
|
1562 |
+
basin_dropdown = gr.Dropdown(
|
1563 |
+
label="Basin",
|
1564 |
+
choices=["All Basins", "WP - Western Pacific", "EP - Eastern Pacific", "NA - North Atlantic"],
|
1565 |
+
value="All Basins"
|
1566 |
+
)
|
1567 |
+
|
1568 |
with gr.Row():
|
1569 |
+
typhoon_dropdown = gr.Dropdown(label="Storm Selection (All Categories Including TD)")
|
1570 |
+
standard_dropdown = gr.Dropdown(
|
1571 |
+
label="Classification Standard",
|
1572 |
+
choices=['atlantic', 'taiwan'],
|
1573 |
+
value='atlantic'
|
1574 |
+
)
|
1575 |
+
|
1576 |
+
generate_video_btn = gr.Button("π¬ Generate Enhanced Animation", variant="primary")
|
1577 |
+
video_output = gr.Video(label="Storm Track Animation")
|
1578 |
+
|
1579 |
+
# Update storm options when year or basin changes
|
1580 |
+
for input_comp in [year_dropdown, basin_dropdown]:
|
1581 |
+
input_comp.change(
|
1582 |
+
fn=update_typhoon_options_enhanced,
|
1583 |
+
inputs=[year_dropdown, basin_dropdown],
|
1584 |
+
outputs=[typhoon_dropdown]
|
1585 |
+
)
|
1586 |
+
|
1587 |
+
# Generate video
|
1588 |
+
generate_video_btn.click(
|
1589 |
+
fn=generate_enhanced_track_video,
|
1590 |
+
inputs=[year_dropdown, typhoon_dropdown, standard_dropdown],
|
1591 |
+
outputs=[video_output]
|
1592 |
)
|
1593 |
+
|
1594 |
+
gr.Markdown("""
|
1595 |
+
### π Enhanced Animation Features:
|
1596 |
+
- **π Full TD Support**: Now displays Tropical Depressions (< 34 kt) in gray
|
1597 |
+
- **π
2025 Compatibility**: Complete support for current year data
|
1598 |
+
- **πΊοΈ Enhanced Maps**: Better cartographic projections with terrain features
|
1599 |
+
- **π Smart Scaling**: Storm symbols scale dynamically with intensity
|
1600 |
+
- **π Real-time Info**: Live position, time, and meteorological data display
|
1601 |
+
- **π¨ Professional Styling**: Publication-quality animations with proper legends
|
1602 |
+
- **β‘ Optimized Export**: Fast rendering with web-compatible video formats
|
1603 |
+
""")
|
1604 |
|
1605 |
return demo
|
1606 |
except Exception as e:
|
1607 |
logging.error(f"Error creating Gradio interface: {e}")
|
1608 |
# Create a minimal fallback interface
|
1609 |
with gr.Blocks() as demo:
|
1610 |
+
gr.Markdown("# πͺοΈ Enhanced Typhoon Analysis Platform")
|
1611 |
gr.Markdown("**Error**: Could not load full interface. Please check logs.")
|
1612 |
return demo
|
1613 |
|