Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -8,6 +8,7 @@ import warnings
|
|
| 8 |
from datetime import datetime, timedelta
|
| 9 |
from collections import defaultdict
|
| 10 |
import csv
|
|
|
|
| 11 |
|
| 12 |
# Suppress warnings for cleaner output
|
| 13 |
warnings.filterwarnings('ignore', category=FutureWarning)
|
|
@@ -30,7 +31,10 @@ from sklearn.manifold import TSNE
|
|
| 30 |
from sklearn.cluster import DBSCAN, KMeans
|
| 31 |
from sklearn.preprocessing import StandardScaler
|
| 32 |
from sklearn.decomposition import PCA
|
| 33 |
-
from
|
|
|
|
|
|
|
|
|
|
| 34 |
import statsmodels.api as sm
|
| 35 |
import requests
|
| 36 |
import tempfile
|
|
@@ -161,6 +165,12 @@ CLUSTER_COLORS = [
|
|
| 161 |
'#F8C471', '#82E0AA', '#F1948A', '#85C1E9', '#D2B4DE'
|
| 162 |
]
|
| 163 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 164 |
# Original color map for backward compatibility
|
| 165 |
color_map = {
|
| 166 |
'C5 Super Typhoon': 'rgb(255, 0, 0)',
|
|
@@ -733,128 +743,272 @@ def classify_enso_phases(oni_value):
|
|
| 733 |
return 'Neutral'
|
| 734 |
|
| 735 |
# -----------------------------
|
| 736 |
-
#
|
| 737 |
# -----------------------------
|
| 738 |
|
| 739 |
def extract_storm_features(typhoon_data):
|
| 740 |
-
"""Extract comprehensive features for clustering analysis"""
|
| 741 |
-
|
| 742 |
-
|
| 743 |
-
|
| 744 |
-
|
| 745 |
-
|
| 746 |
-
|
| 747 |
-
|
| 748 |
-
|
| 749 |
-
|
| 750 |
-
# Flatten column names
|
| 751 |
-
storm_features.columns = ['SID'] + ['_'.join(col).strip() for col in storm_features.columns[1:]]
|
| 752 |
-
|
| 753 |
-
# Add additional computed features
|
| 754 |
-
storm_features['lat_range'] = storm_features['LAT_max'] - storm_features['LAT_min']
|
| 755 |
-
storm_features['lon_range'] = storm_features['LON_max'] - storm_features['LON_min']
|
| 756 |
-
storm_features['track_length'] = storm_features['ISO_TIME_count']
|
| 757 |
-
|
| 758 |
-
# Add genesis location features
|
| 759 |
-
genesis_data = typhoon_data.groupby('SID').first()[['LAT', 'LON', 'USA_WIND']]
|
| 760 |
-
genesis_data.columns = ['genesis_lat', 'genesis_lon', 'genesis_intensity']
|
| 761 |
-
storm_features = storm_features.merge(genesis_data, on='SID', how='left')
|
| 762 |
-
|
| 763 |
-
# Add track shape features
|
| 764 |
-
track_stats = []
|
| 765 |
-
for sid in storm_features['SID']:
|
| 766 |
-
storm_track = typhoon_data[typhoon_data['SID'] == sid].sort_values('ISO_TIME')
|
| 767 |
-
if len(storm_track) > 2:
|
| 768 |
-
# Calculate track curvature and direction changes
|
| 769 |
-
lats = storm_track['LAT'].values
|
| 770 |
-
lons = storm_track['LON'].values
|
| 771 |
|
| 772 |
-
|
| 773 |
-
|
| 774 |
-
for i in range(1, len(lats)-1):
|
| 775 |
-
# Simple bearing calculation
|
| 776 |
-
dlat1 = lats[i] - lats[i-1]
|
| 777 |
-
dlon1 = lons[i] - lons[i-1]
|
| 778 |
-
dlat2 = lats[i+1] - lats[i]
|
| 779 |
-
dlon2 = lons[i+1] - lons[i]
|
| 780 |
-
|
| 781 |
-
angle1 = np.arctan2(dlat1, dlon1)
|
| 782 |
-
angle2 = np.arctan2(dlat2, dlon2)
|
| 783 |
-
change = abs(angle2 - angle1)
|
| 784 |
-
bearing_changes.append(change)
|
| 785 |
|
| 786 |
-
|
| 787 |
-
|
| 788 |
|
| 789 |
-
|
| 790 |
-
|
| 791 |
-
'
|
| 792 |
-
|
| 793 |
-
|
| 794 |
-
|
| 795 |
-
|
| 796 |
-
|
| 797 |
-
|
| 798 |
-
|
| 799 |
-
|
| 800 |
-
|
| 801 |
-
|
| 802 |
-
|
| 803 |
-
|
| 804 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 805 |
|
| 806 |
def perform_dimensionality_reduction(storm_features, method='umap', n_components=2):
|
| 807 |
-
"""Perform UMAP or t-SNE dimensionality reduction"""
|
| 808 |
-
|
| 809 |
-
|
| 810 |
-
|
| 811 |
-
|
| 812 |
-
|
| 813 |
-
|
| 814 |
-
|
| 815 |
-
|
| 816 |
-
|
| 817 |
-
|
| 818 |
-
|
| 819 |
-
|
| 820 |
-
|
| 821 |
-
|
| 822 |
-
|
| 823 |
-
|
| 824 |
-
|
| 825 |
-
|
| 826 |
-
|
| 827 |
-
|
| 828 |
-
|
| 829 |
-
|
| 830 |
-
|
| 831 |
-
|
| 832 |
-
|
| 833 |
-
|
| 834 |
-
)
|
| 835 |
-
|
| 836 |
-
#
|
| 837 |
-
|
| 838 |
-
|
| 839 |
-
|
| 840 |
-
|
| 841 |
-
|
| 842 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 843 |
|
| 844 |
def cluster_storms(embedding, method='dbscan', eps=0.5, min_samples=3):
|
| 845 |
-
"""Cluster storms based on their embedding"""
|
| 846 |
-
|
| 847 |
-
|
| 848 |
-
|
| 849 |
-
|
| 850 |
-
|
| 851 |
-
|
| 852 |
-
|
| 853 |
-
|
| 854 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 855 |
|
| 856 |
def create_advanced_clustering_visualization(storm_features, typhoon_data, method='umap', show_routes=True):
|
| 857 |
-
"""Create comprehensive clustering visualization with route display"""
|
| 858 |
try:
|
| 859 |
# Validate inputs
|
| 860 |
if storm_features is None or storm_features.empty:
|
|
@@ -863,6 +1017,8 @@ def create_advanced_clustering_visualization(storm_features, typhoon_data, metho
|
|
| 863 |
if typhoon_data is None or typhoon_data.empty:
|
| 864 |
raise ValueError("No typhoon data available for route visualization")
|
| 865 |
|
|
|
|
|
|
|
| 866 |
# Perform dimensionality reduction
|
| 867 |
embedding, feature_cols, scaler = perform_dimensionality_reduction(storm_features, method)
|
| 868 |
|
|
@@ -875,9 +1031,17 @@ def create_advanced_clustering_visualization(storm_features, typhoon_data, metho
|
|
| 875 |
storm_features_viz['dim1'] = embedding[:, 0]
|
| 876 |
storm_features_viz['dim2'] = embedding[:, 1]
|
| 877 |
|
| 878 |
-
# Merge with typhoon data for additional info
|
| 879 |
-
|
| 880 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 881 |
|
| 882 |
if show_routes:
|
| 883 |
# Create subplot with both scatter plot and route map
|
|
@@ -898,6 +1062,17 @@ def create_advanced_clustering_visualization(storm_features, typhoon_data, metho
|
|
| 898 |
color = CLUSTER_COLORS[i % len(CLUSTER_COLORS)] if cluster != -1 else '#CCCCCC'
|
| 899 |
cluster_name = f'Cluster {cluster}' if cluster != -1 else 'Noise'
|
| 900 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 901 |
fig.add_trace(
|
| 902 |
go.Scatter(
|
| 903 |
x=cluster_data['dim1'],
|
|
@@ -916,15 +1091,15 @@ def create_advanced_clustering_visualization(storm_features, typhoon_data, metho
|
|
| 916 |
customdata=np.column_stack((
|
| 917 |
cluster_data['NAME'].fillna('UNNAMED'),
|
| 918 |
cluster_data['SEASON'].fillna(2000),
|
| 919 |
-
|
| 920 |
-
|
| 921 |
-
|
| 922 |
))
|
| 923 |
),
|
| 924 |
row=1, col=1
|
| 925 |
)
|
| 926 |
|
| 927 |
-
# Add route map
|
| 928 |
for i, cluster in enumerate(unique_clusters):
|
| 929 |
if cluster == -1: # Skip noise for route visualization
|
| 930 |
continue
|
|
@@ -932,39 +1107,47 @@ def create_advanced_clustering_visualization(storm_features, typhoon_data, metho
|
|
| 932 |
cluster_storm_ids = storm_features_viz[storm_features_viz['cluster'] == cluster]['SID'].tolist()
|
| 933 |
color = CLUSTER_COLORS[i % len(CLUSTER_COLORS)]
|
| 934 |
|
| 935 |
-
|
|
|
|
| 936 |
try:
|
| 937 |
storm_track = typhoon_data[typhoon_data['SID'] == sid].sort_values('ISO_TIME')
|
| 938 |
if len(storm_track) > 1:
|
| 939 |
-
|
|
|
|
|
|
|
| 940 |
|
| 941 |
-
|
| 942 |
-
|
| 943 |
-
|
| 944 |
-
|
| 945 |
-
|
| 946 |
-
|
| 947 |
-
|
| 948 |
-
|
| 949 |
-
|
| 950 |
-
|
| 951 |
-
f'
|
| 952 |
-
|
| 953 |
-
|
| 954 |
-
|
| 955 |
-
|
| 956 |
-
|
| 957 |
-
|
| 958 |
-
|
| 959 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 960 |
except Exception as track_error:
|
| 961 |
-
|
| 962 |
continue
|
| 963 |
|
| 964 |
# Update layout
|
| 965 |
fig.update_layout(
|
| 966 |
title_text="Advanced Storm Clustering Analysis with Route Visualization",
|
| 967 |
-
showlegend=True
|
|
|
|
| 968 |
)
|
| 969 |
|
| 970 |
# Update geo layout
|
|
@@ -991,7 +1174,7 @@ def create_advanced_clustering_visualization(storm_features, typhoon_data, metho
|
|
| 991 |
x='dim1',
|
| 992 |
y='dim2',
|
| 993 |
color='cluster',
|
| 994 |
-
hover_data=['NAME', 'SEASON'
|
| 995 |
title=f'Storm Clustering using {method.upper()}',
|
| 996 |
labels={
|
| 997 |
'dim1': f'{method.upper()} Dimension 1',
|
|
@@ -1000,57 +1183,86 @@ def create_advanced_clustering_visualization(storm_features, typhoon_data, metho
|
|
| 1000 |
}
|
| 1001 |
)
|
| 1002 |
|
| 1003 |
-
# Generate detailed cluster statistics
|
| 1004 |
try:
|
| 1005 |
-
|
| 1006 |
-
|
| 1007 |
-
'
|
| 1008 |
-
'
|
| 1009 |
-
'
|
| 1010 |
-
'
|
| 1011 |
-
'
|
| 1012 |
-
'
|
| 1013 |
-
'
|
| 1014 |
-
|
| 1015 |
-
|
| 1016 |
-
# Flatten column names for readability
|
| 1017 |
-
cluster_stats.columns = ['_'.join(col).strip() for col in cluster_stats.columns]
|
| 1018 |
|
| 1019 |
-
|
|
|
|
| 1020 |
|
| 1021 |
-
|
| 1022 |
-
|
| 1023 |
-
|
| 1024 |
-
|
|
|
|
| 1025 |
|
| 1026 |
-
|
| 1027 |
-
storm_count = int(cluster_row['SID_count'])
|
| 1028 |
|
| 1029 |
-
|
| 1030 |
-
|
| 1031 |
-
|
| 1032 |
-
|
| 1033 |
-
|
| 1034 |
-
|
| 1035 |
-
|
| 1036 |
-
|
| 1037 |
-
|
| 1038 |
-
|
| 1039 |
-
|
| 1040 |
-
|
| 1041 |
-
|
| 1042 |
-
|
| 1043 |
-
|
| 1044 |
-
|
| 1045 |
-
|
| 1046 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1047 |
|
| 1048 |
except Exception as stats_error:
|
|
|
|
| 1049 |
stats_text = f"Error generating cluster statistics: {str(stats_error)}"
|
| 1050 |
|
| 1051 |
return fig, stats_text, storm_features_viz
|
| 1052 |
|
| 1053 |
except Exception as e:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1054 |
error_fig = go.Figure()
|
| 1055 |
error_fig.add_annotation(
|
| 1056 |
text=f"Error in clustering analysis: {str(e)}",
|
|
@@ -1061,117 +1273,445 @@ def create_advanced_clustering_visualization(storm_features, typhoon_data, metho
|
|
| 1061 |
return error_fig, f"Error in clustering: {str(e)}", None
|
| 1062 |
|
| 1063 |
# -----------------------------
|
| 1064 |
-
#
|
| 1065 |
# -----------------------------
|
| 1066 |
|
| 1067 |
-
def
|
| 1068 |
-
"""Create
|
| 1069 |
-
if not CNN_AVAILABLE:
|
| 1070 |
-
return None
|
| 1071 |
-
|
| 1072 |
try:
|
| 1073 |
-
|
| 1074 |
-
|
| 1075 |
-
|
| 1076 |
-
|
| 1077 |
-
|
| 1078 |
-
|
| 1079 |
-
|
| 1080 |
-
|
|
|
|
| 1081 |
|
| 1082 |
-
#
|
| 1083 |
-
|
| 1084 |
-
layers.Dense(64, activation='relu'),
|
| 1085 |
-
layers.Dropout(0.5),
|
| 1086 |
-
layers.Dense(32, activation='relu'),
|
| 1087 |
|
| 1088 |
-
|
| 1089 |
-
|
| 1090 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1091 |
|
| 1092 |
-
|
| 1093 |
-
|
| 1094 |
-
loss='mean_squared_error',
|
| 1095 |
-
metrics=['mae']
|
| 1096 |
-
)
|
| 1097 |
|
| 1098 |
-
|
| 1099 |
-
|
| 1100 |
-
|
| 1101 |
-
|
| 1102 |
-
|
| 1103 |
-
|
| 1104 |
-
|
| 1105 |
-
|
| 1106 |
-
|
| 1107 |
-
|
| 1108 |
-
|
|
|
|
|
|
|
|
|
|
| 1109 |
|
| 1110 |
-
#
|
| 1111 |
-
|
|
|
|
|
|
|
| 1112 |
|
| 1113 |
-
#
|
| 1114 |
-
|
| 1115 |
-
|
| 1116 |
-
latitude_factor = max(0.5, (30 - abs(lat)) / 30) if abs(lat) < 30 else 0.1
|
| 1117 |
|
| 1118 |
-
|
| 1119 |
-
|
| 1120 |
-
intensity = base_intensity + sst_anomaly * 10 + seasonal_factor * 20 + latitude_factor * 30
|
| 1121 |
-
intensity = max(0, min(180, intensity)) # Clamp to reasonable range
|
| 1122 |
|
| 1123 |
-
|
| 1124 |
-
|
|
|
|
| 1125 |
|
| 1126 |
-
return intensity, f"CNN Prediction: {intensity:.1f} kt (Confidence: {confidence:.1%})"
|
| 1127 |
except Exception as e:
|
| 1128 |
-
|
| 1129 |
-
return simulate_physics_based_prediction(lat, lon, month, oni_value)
|
| 1130 |
|
| 1131 |
-
def
|
| 1132 |
-
"""
|
| 1133 |
try:
|
| 1134 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1135 |
base_intensity = 45
|
| 1136 |
|
| 1137 |
-
# ENSO effects
|
| 1138 |
if oni_value > 0.5: # El NiΓ±o
|
| 1139 |
-
intensity_modifier = -15
|
| 1140 |
elif oni_value < -0.5: # La NiΓ±a
|
| 1141 |
-
intensity_modifier =
|
| 1142 |
-
else:
|
| 1143 |
-
intensity_modifier = 0
|
| 1144 |
-
|
| 1145 |
-
# Seasonal effects
|
| 1146 |
-
if month in [8, 9, 10]: # Peak season
|
| 1147 |
-
seasonal_modifier = 25
|
| 1148 |
-
elif month in [6, 7, 11]: # Active season
|
| 1149 |
-
seasonal_modifier = 15
|
| 1150 |
-
else: # Quiet season
|
| 1151 |
-
seasonal_modifier = -10
|
| 1152 |
-
|
| 1153 |
-
# Latitude effects (closer to equator = less favorable)
|
| 1154 |
-
if abs(lat) < 10:
|
| 1155 |
-
lat_modifier = -20 # Too close to equator
|
| 1156 |
-
elif 10 <= abs(lat) <= 25:
|
| 1157 |
-
lat_modifier = 10 # Optimal range
|
| 1158 |
else:
|
| 1159 |
-
|
| 1160 |
|
| 1161 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1162 |
if 120 <= lon <= 160:
|
| 1163 |
-
|
|
|
|
|
|
|
| 1164 |
else:
|
| 1165 |
-
|
| 1166 |
|
| 1167 |
-
|
|
|
|
| 1168 |
predicted_intensity = max(25, min(180, predicted_intensity))
|
| 1169 |
|
| 1170 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1171 |
|
| 1172 |
-
return predicted_intensity, f"Physics-based Prediction: {predicted_intensity:.1f} kt (Confidence: {confidence:.1%})"
|
| 1173 |
except Exception as e:
|
| 1174 |
-
return
|
| 1175 |
|
| 1176 |
# -----------------------------
|
| 1177 |
# Regression Functions (Original)
|
|
@@ -1696,7 +2236,7 @@ def initialize_data():
|
|
| 1696 |
initialize_data()
|
| 1697 |
|
| 1698 |
# -----------------------------
|
| 1699 |
-
# ENHANCED: Gradio Interface
|
| 1700 |
# -----------------------------
|
| 1701 |
|
| 1702 |
def create_interface():
|
|
@@ -1721,64 +2261,76 @@ def create_interface():
|
|
| 1721 |
available_years = [str(year) for year in range(2000, 2026)]
|
| 1722 |
|
| 1723 |
with gr.Blocks(title="Enhanced Typhoon Analysis Platform", theme=gr.themes.Soft()) as demo:
|
| 1724 |
-
gr.Markdown("# Enhanced Typhoon Analysis Platform")
|
| 1725 |
-
gr.Markdown("Advanced ML clustering,
|
| 1726 |
|
| 1727 |
-
with gr.Tab("Overview"):
|
| 1728 |
overview_text = f"""
|
| 1729 |
## Welcome to the Enhanced Typhoon Analysis Dashboard
|
| 1730 |
|
| 1731 |
This dashboard provides comprehensive analysis of typhoon data in relation to ENSO phases with advanced machine learning capabilities.
|
| 1732 |
|
| 1733 |
-
### Enhanced Features:
|
| 1734 |
-
- Advanced ML Clustering: UMAP/t-SNE storm pattern analysis with route visualization
|
| 1735 |
-
-
|
| 1736 |
-
- Complete TD Support: Now includes Tropical Depressions (< 34 kt)
|
| 1737 |
-
- 2025 Data Ready: Real-time compatibility with current year data
|
| 1738 |
-
- Enhanced Animations: High-quality storm track visualizations
|
| 1739 |
-
|
| 1740 |
-
### Data Status:
|
| 1741 |
-
- ONI Data: {len(oni_data)} years loaded
|
| 1742 |
-
- Typhoon Data: {total_records} records loaded
|
| 1743 |
-
- Merged Data: {len(merged_data)} typhoons with ONI values
|
| 1744 |
-
- Available Years: {year_range_display}
|
| 1745 |
-
|
| 1746 |
-
### Technical Capabilities:
|
| 1747 |
-
- UMAP Clustering: {"Available" if UMAP_AVAILABLE else "Limited to t-SNE/PCA"}
|
| 1748 |
-
- AI Predictions: {"Deep Learning" if CNN_AVAILABLE else "Physics-based"}
|
| 1749 |
-
- Enhanced Categorization: Tropical Depression to Super Typhoon
|
| 1750 |
-
- Platform
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1751 |
"""
|
| 1752 |
gr.Markdown(overview_text)
|
| 1753 |
|
| 1754 |
-
with gr.Tab("Advanced ML Clustering
|
| 1755 |
-
gr.Markdown("## Storm Pattern Analysis using UMAP/t-SNE with Route Visualization")
|
| 1756 |
gr.Markdown("**This tab shows both the dimensional clustering analysis AND the actual storm tracks colored by cluster**")
|
| 1757 |
|
| 1758 |
with gr.Row():
|
| 1759 |
-
|
| 1760 |
-
|
| 1761 |
-
|
| 1762 |
-
|
| 1763 |
-
|
| 1764 |
-
|
| 1765 |
-
|
| 1766 |
-
|
| 1767 |
-
|
| 1768 |
-
|
|
|
|
|
|
|
|
|
|
| 1769 |
|
| 1770 |
-
analyze_clusters_btn = gr.Button("Analyze Storm Clusters & Routes", variant="primary")
|
| 1771 |
|
| 1772 |
with gr.Row():
|
| 1773 |
cluster_plot = gr.Plot(label="Storm Clustering with Route Visualization")
|
| 1774 |
|
| 1775 |
with gr.Row():
|
| 1776 |
-
cluster_stats = gr.Textbox(label="Detailed Cluster Statistics", lines=15, max_lines=20)
|
| 1777 |
|
| 1778 |
def run_advanced_clustering_analysis(method, show_routes):
|
| 1779 |
try:
|
| 1780 |
# Extract features for clustering
|
| 1781 |
storm_features = extract_storm_features(typhoon_data)
|
|
|
|
|
|
|
| 1782 |
fig, stats, _ = create_advanced_clustering_visualization(storm_features, typhoon_data, method.lower(), show_routes)
|
| 1783 |
return fig, stats
|
| 1784 |
except Exception as e:
|
|
@@ -1793,68 +2345,113 @@ def create_interface():
|
|
| 1793 |
)
|
| 1794 |
|
| 1795 |
cluster_info_text = """
|
| 1796 |
-
### Advanced Clustering Features:
|
| 1797 |
-
- Multi-dimensional Analysis: Uses 15+ storm characteristics including intensity, track shape, genesis location
|
| 1798 |
-
- Route Visualization: Shows actual storm tracks colored by cluster membership
|
| 1799 |
-
- DBSCAN Clustering: Automatically finds natural groupings without predefined cluster count
|
| 1800 |
-
- Comprehensive Stats: Detailed cluster analysis including intensity, pressure, track length, curvature
|
| 1801 |
-
- Interactive: Hover over points to see storm details, zoom and pan the route map
|
| 1802 |
-
|
| 1803 |
-
### How to Interpret:
|
| 1804 |
-
- Left Plot: Each dot is a storm positioned by similarity (close = similar characteristics)
|
| 1805 |
-
- Right Plot: Actual geographic storm tracks, colored by which cluster they belong to
|
| 1806 |
-
- Cluster Colors: Each cluster gets a unique color to identify similar storm patterns
|
| 1807 |
-
- Noise Points: Gray points represent storms that don't fit clear patterns
|
| 1808 |
"""
|
| 1809 |
gr.Markdown(cluster_info_text)
|
| 1810 |
|
| 1811 |
-
with gr.Tab("
|
| 1812 |
-
gr.Markdown("## AI-Powered Storm Intensity Forecasting")
|
| 1813 |
|
| 1814 |
if CNN_AVAILABLE:
|
| 1815 |
-
gr.Markdown("Deep Learning models available - TensorFlow loaded successfully")
|
| 1816 |
method_description = "Using Convolutional Neural Networks for advanced intensity prediction"
|
| 1817 |
else:
|
| 1818 |
-
gr.Markdown("Physics-based models available - Using climatological relationships")
|
| 1819 |
gr.Markdown("*Install TensorFlow for deep learning features: `pip install tensorflow-cpu`*")
|
| 1820 |
method_description = "Using established meteorological relationships and climatology"
|
| 1821 |
|
| 1822 |
gr.Markdown(f"**Current Method**: {method_description}")
|
| 1823 |
|
| 1824 |
with gr.Row():
|
| 1825 |
-
|
| 1826 |
-
|
| 1827 |
-
|
| 1828 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1829 |
|
| 1830 |
-
|
|
|
|
| 1831 |
|
| 1832 |
with gr.Row():
|
| 1833 |
-
|
| 1834 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1835 |
|
| 1836 |
predict_btn.click(
|
| 1837 |
-
fn=
|
| 1838 |
-
inputs=[
|
| 1839 |
-
outputs=[
|
| 1840 |
)
|
| 1841 |
|
| 1842 |
prediction_info_text = """
|
| 1843 |
-
### Prediction Features:
|
| 1844 |
-
-
|
| 1845 |
-
-
|
| 1846 |
-
-
|
| 1847 |
-
-
|
| 1848 |
-
|
| 1849 |
-
|
| 1850 |
-
|
| 1851 |
-
-
|
| 1852 |
-
-
|
| 1853 |
-
-
|
|
|
|
|
|
|
| 1854 |
"""
|
| 1855 |
gr.Markdown(prediction_info_text)
|
| 1856 |
|
| 1857 |
-
with gr.Tab("Track Visualization"):
|
| 1858 |
with gr.Row():
|
| 1859 |
start_year = gr.Number(label="Start Year", value=2020)
|
| 1860 |
start_month = gr.Dropdown(label="Start Month", choices=list(range(1, 13)), value=1)
|
|
@@ -1871,7 +2468,7 @@ def create_interface():
|
|
| 1871 |
outputs=[tracks_plot, typhoon_count]
|
| 1872 |
)
|
| 1873 |
|
| 1874 |
-
with gr.Tab("Wind Analysis"):
|
| 1875 |
with gr.Row():
|
| 1876 |
wind_start_year = gr.Number(label="Start Year", value=2020)
|
| 1877 |
wind_start_month = gr.Dropdown(label="Start Month", choices=list(range(1, 13)), value=1)
|
|
@@ -1888,7 +2485,7 @@ def create_interface():
|
|
| 1888 |
outputs=[wind_scatter, wind_regression_results]
|
| 1889 |
)
|
| 1890 |
|
| 1891 |
-
with gr.Tab("Pressure Analysis"):
|
| 1892 |
with gr.Row():
|
| 1893 |
pressure_start_year = gr.Number(label="Start Year", value=2020)
|
| 1894 |
pressure_start_month = gr.Dropdown(label="Start Month", choices=list(range(1, 13)), value=1)
|
|
@@ -1905,7 +2502,7 @@ def create_interface():
|
|
| 1905 |
outputs=[pressure_scatter, pressure_regression_results]
|
| 1906 |
)
|
| 1907 |
|
| 1908 |
-
with gr.Tab("Longitude Analysis"):
|
| 1909 |
with gr.Row():
|
| 1910 |
lon_start_year = gr.Number(label="Start Year", value=2020)
|
| 1911 |
lon_start_month = gr.Dropdown(label="Start Month", choices=list(range(1, 13)), value=1)
|
|
@@ -1923,8 +2520,8 @@ def create_interface():
|
|
| 1923 |
outputs=[regression_plot, slopes_text, lon_regression_results]
|
| 1924 |
)
|
| 1925 |
|
| 1926 |
-
with gr.Tab("Enhanced Track Animation"):
|
| 1927 |
-
gr.Markdown("## High-Quality Storm Track Visualization (All Categories Including TD)")
|
| 1928 |
|
| 1929 |
with gr.Row():
|
| 1930 |
year_dropdown = gr.Dropdown(
|
|
@@ -1946,7 +2543,7 @@ def create_interface():
|
|
| 1946 |
value='atlantic'
|
| 1947 |
)
|
| 1948 |
|
| 1949 |
-
generate_video_btn = gr.Button("Generate Enhanced Animation", variant="primary")
|
| 1950 |
video_output = gr.Video(label="Storm Track Animation")
|
| 1951 |
|
| 1952 |
# Update storm options when year or basin changes
|
|
@@ -1965,19 +2562,19 @@ def create_interface():
|
|
| 1965 |
)
|
| 1966 |
|
| 1967 |
animation_info_text = """
|
| 1968 |
-
### Enhanced Animation Features:
|
| 1969 |
-
- Full TD Support: Now displays Tropical Depressions (< 34 kt) in gray
|
| 1970 |
-
- 2025 Compatibility: Complete support for current year data
|
| 1971 |
-
- Enhanced Maps: Better cartographic projections with terrain features
|
| 1972 |
-
- Smart Scaling: Storm symbols scale dynamically with intensity
|
| 1973 |
-
- Real-time Info: Live position, time, and meteorological data display
|
| 1974 |
-
- Professional Styling: Publication-quality animations with proper legends
|
| 1975 |
-
- Optimized Export: Fast rendering with web-compatible video formats
|
| 1976 |
"""
|
| 1977 |
gr.Markdown(animation_info_text)
|
| 1978 |
|
| 1979 |
-
with gr.Tab("Data Statistics & Insights"):
|
| 1980 |
-
gr.Markdown("## Comprehensive Dataset Analysis")
|
| 1981 |
|
| 1982 |
# Create enhanced data summary
|
| 1983 |
try:
|
|
@@ -2083,27 +2680,27 @@ def create_interface():
|
|
| 2083 |
|
| 2084 |
# Create statistics text safely
|
| 2085 |
stats_text = f"""
|
| 2086 |
-
### Enhanced Dataset Summary:
|
| 2087 |
-
- Total Unique Storms: {total_storms:,}
|
| 2088 |
-
- Total Track Records: {total_records:,}
|
| 2089 |
-
- Year Range: {year_range} ({years_covered} years)
|
| 2090 |
-
- Basins Available: {basins_available}
|
| 2091 |
-
- Average Storms/Year: {avg_storms_per_year:.1f}
|
| 2092 |
-
|
| 2093 |
-
### Storm Category Breakdown:
|
| 2094 |
-
- Tropical Depressions: {td_storms:,} storms ({td_percentage:.1f}%)
|
| 2095 |
-
- Tropical Storms: {ts_storms:,} storms
|
| 2096 |
-
- Typhoons (C1-C5): {typhoon_storms:,} storms
|
| 2097 |
-
|
| 2098 |
-
###
|
| 2099 |
-
- Complete TD Analysis - First platform to include comprehensive TD tracking
|
| 2100 |
-
- Advanced ML Clustering - DBSCAN pattern recognition with route visualization
|
| 2101 |
-
- Real-time Predictions - Physics-based and optional CNN intensity forecasting
|
| 2102 |
-
- 2025 Data Ready - Full compatibility with current season data
|
| 2103 |
-
- Enhanced Animations - Professional-quality storm track videos
|
| 2104 |
-
- Multi-basin Analysis - Comprehensive Pacific and Atlantic coverage
|
| 2105 |
-
|
| 2106 |
-
### Research Applications:
|
| 2107 |
- Climate change impact studies
|
| 2108 |
- Seasonal forecasting research
|
| 2109 |
- Storm pattern classification
|
|
@@ -2176,42 +2773,6 @@ def create_minimal_fallback_interface():
|
|
| 2176 |
|
| 2177 |
return demo
|
| 2178 |
|
| 2179 |
-
# -----------------------------
|
| 2180 |
-
# Color Test Functions (Optional)
|
| 2181 |
-
# -----------------------------
|
| 2182 |
-
|
| 2183 |
-
def test_color_conversion():
|
| 2184 |
-
"""Test color conversion functions"""
|
| 2185 |
-
print("Testing color conversion...")
|
| 2186 |
-
|
| 2187 |
-
# Test all categories
|
| 2188 |
-
test_winds = [25, 40, 70, 85, 100, 120, 150] # TD, TS, C1, C2, C3, C4, C5
|
| 2189 |
-
|
| 2190 |
-
for wind in test_winds:
|
| 2191 |
-
category = categorize_typhoon_enhanced(wind)
|
| 2192 |
-
plotly_color = enhanced_color_map.get(category, 'rgb(128,128,128)')
|
| 2193 |
-
matplotlib_color = get_matplotlib_color(category)
|
| 2194 |
-
|
| 2195 |
-
print(f"Wind: {wind:3d}kt -> {category:20s} -> Plotly: {plotly_color:15s} -> Matplotlib: {matplotlib_color}")
|
| 2196 |
-
|
| 2197 |
-
print("Color conversion test complete!")
|
| 2198 |
-
|
| 2199 |
-
def test_rgb_conversion():
|
| 2200 |
-
"""Test RGB string to hex conversion"""
|
| 2201 |
-
test_colors = [
|
| 2202 |
-
'rgb(128, 128, 128)',
|
| 2203 |
-
'rgb(255, 0, 0)',
|
| 2204 |
-
'rgb(0, 255, 0)',
|
| 2205 |
-
'rgb(0, 0, 255)'
|
| 2206 |
-
]
|
| 2207 |
-
|
| 2208 |
-
print("Testing RGB to hex conversion...")
|
| 2209 |
-
for rgb_str in test_colors:
|
| 2210 |
-
hex_color = rgb_string_to_hex(rgb_str)
|
| 2211 |
-
print(f"{rgb_str:20s} -> {hex_color}")
|
| 2212 |
-
|
| 2213 |
-
print("RGB conversion test complete!")
|
| 2214 |
-
|
| 2215 |
# Create and launch the interface
|
| 2216 |
demo = create_interface()
|
| 2217 |
|
|
|
|
| 8 |
from datetime import datetime, timedelta
|
| 9 |
from collections import defaultdict
|
| 10 |
import csv
|
| 11 |
+
import json
|
| 12 |
|
| 13 |
# Suppress warnings for cleaner output
|
| 14 |
warnings.filterwarnings('ignore', category=FutureWarning)
|
|
|
|
| 31 |
from sklearn.cluster import DBSCAN, KMeans
|
| 32 |
from sklearn.preprocessing import StandardScaler
|
| 33 |
from sklearn.decomposition import PCA
|
| 34 |
+
from sklearn.ensemble import RandomForestRegressor
|
| 35 |
+
from sklearn.model_selection import train_test_split
|
| 36 |
+
from sklearn.metrics import mean_absolute_error, r2_score
|
| 37 |
+
from scipy.interpolate import interp1d, RBFInterpolator
|
| 38 |
import statsmodels.api as sm
|
| 39 |
import requests
|
| 40 |
import tempfile
|
|
|
|
| 165 |
'#F8C471', '#82E0AA', '#F1948A', '#85C1E9', '#D2B4DE'
|
| 166 |
]
|
| 167 |
|
| 168 |
+
# Route prediction colors
|
| 169 |
+
ROUTE_COLORS = [
|
| 170 |
+
'#FF0066', '#00FF66', '#6600FF', '#FF6600', '#0066FF',
|
| 171 |
+
'#FF00CC', '#00FFCC', '#CC00FF', '#CCFF00', '#00CCFF'
|
| 172 |
+
]
|
| 173 |
+
|
| 174 |
# Original color map for backward compatibility
|
| 175 |
color_map = {
|
| 176 |
'C5 Super Typhoon': 'rgb(255, 0, 0)',
|
|
|
|
| 743 |
return 'Neutral'
|
| 744 |
|
| 745 |
# -----------------------------
|
| 746 |
+
# FIXED: ADVANCED ML FEATURES WITH ROBUST ERROR HANDLING
|
| 747 |
# -----------------------------
|
| 748 |
|
| 749 |
def extract_storm_features(typhoon_data):
|
| 750 |
+
"""Extract comprehensive features for clustering analysis - FIXED VERSION"""
|
| 751 |
+
try:
|
| 752 |
+
if typhoon_data is None or typhoon_data.empty:
|
| 753 |
+
logging.error("No typhoon data provided for feature extraction")
|
| 754 |
+
return None
|
| 755 |
+
|
| 756 |
+
# Basic features - ensure columns exist
|
| 757 |
+
basic_features = []
|
| 758 |
+
for sid in typhoon_data['SID'].unique():
|
| 759 |
+
storm_data = typhoon_data[typhoon_data['SID'] == sid].copy()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 760 |
|
| 761 |
+
if len(storm_data) == 0:
|
| 762 |
+
continue
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 763 |
|
| 764 |
+
# Initialize feature dict with safe defaults
|
| 765 |
+
features = {'SID': sid}
|
| 766 |
|
| 767 |
+
# Wind statistics
|
| 768 |
+
if 'USA_WIND' in storm_data.columns:
|
| 769 |
+
wind_values = pd.to_numeric(storm_data['USA_WIND'], errors='coerce').dropna()
|
| 770 |
+
if len(wind_values) > 0:
|
| 771 |
+
features['USA_WIND_max'] = wind_values.max()
|
| 772 |
+
features['USA_WIND_mean'] = wind_values.mean()
|
| 773 |
+
features['USA_WIND_std'] = wind_values.std() if len(wind_values) > 1 else 0
|
| 774 |
+
else:
|
| 775 |
+
features['USA_WIND_max'] = 30
|
| 776 |
+
features['USA_WIND_mean'] = 30
|
| 777 |
+
features['USA_WIND_std'] = 0
|
| 778 |
+
else:
|
| 779 |
+
features['USA_WIND_max'] = 30
|
| 780 |
+
features['USA_WIND_mean'] = 30
|
| 781 |
+
features['USA_WIND_std'] = 0
|
| 782 |
+
|
| 783 |
+
# Pressure statistics
|
| 784 |
+
if 'USA_PRES' in storm_data.columns:
|
| 785 |
+
pres_values = pd.to_numeric(storm_data['USA_PRES'], errors='coerce').dropna()
|
| 786 |
+
if len(pres_values) > 0:
|
| 787 |
+
features['USA_PRES_min'] = pres_values.min()
|
| 788 |
+
features['USA_PRES_mean'] = pres_values.mean()
|
| 789 |
+
features['USA_PRES_std'] = pres_values.std() if len(pres_values) > 1 else 0
|
| 790 |
+
else:
|
| 791 |
+
features['USA_PRES_min'] = 1000
|
| 792 |
+
features['USA_PRES_mean'] = 1000
|
| 793 |
+
features['USA_PRES_std'] = 0
|
| 794 |
+
else:
|
| 795 |
+
features['USA_PRES_min'] = 1000
|
| 796 |
+
features['USA_PRES_mean'] = 1000
|
| 797 |
+
features['USA_PRES_std'] = 0
|
| 798 |
+
|
| 799 |
+
# Location statistics
|
| 800 |
+
if 'LAT' in storm_data.columns and 'LON' in storm_data.columns:
|
| 801 |
+
lat_values = pd.to_numeric(storm_data['LAT'], errors='coerce').dropna()
|
| 802 |
+
lon_values = pd.to_numeric(storm_data['LON'], errors='coerce').dropna()
|
| 803 |
+
|
| 804 |
+
if len(lat_values) > 0 and len(lon_values) > 0:
|
| 805 |
+
features['LAT_mean'] = lat_values.mean()
|
| 806 |
+
features['LAT_std'] = lat_values.std() if len(lat_values) > 1 else 0
|
| 807 |
+
features['LAT_max'] = lat_values.max()
|
| 808 |
+
features['LAT_min'] = lat_values.min()
|
| 809 |
+
features['LON_mean'] = lon_values.mean()
|
| 810 |
+
features['LON_std'] = lon_values.std() if len(lon_values) > 1 else 0
|
| 811 |
+
features['LON_max'] = lon_values.max()
|
| 812 |
+
features['LON_min'] = lon_values.min()
|
| 813 |
+
|
| 814 |
+
# Genesis location (first valid position)
|
| 815 |
+
features['genesis_lat'] = lat_values.iloc[0]
|
| 816 |
+
features['genesis_lon'] = lon_values.iloc[0]
|
| 817 |
+
features['genesis_intensity'] = features['USA_WIND_mean'] # Use mean as fallback
|
| 818 |
+
|
| 819 |
+
# Track characteristics
|
| 820 |
+
features['lat_range'] = lat_values.max() - lat_values.min()
|
| 821 |
+
features['lon_range'] = lon_values.max() - lon_values.min()
|
| 822 |
+
|
| 823 |
+
# Calculate track distance
|
| 824 |
+
if len(lat_values) > 1:
|
| 825 |
+
distances = []
|
| 826 |
+
for i in range(1, len(lat_values)):
|
| 827 |
+
dlat = lat_values.iloc[i] - lat_values.iloc[i-1]
|
| 828 |
+
dlon = lon_values.iloc[i] - lon_values.iloc[i-1]
|
| 829 |
+
distances.append(np.sqrt(dlat**2 + dlon**2))
|
| 830 |
+
features['total_distance'] = sum(distances)
|
| 831 |
+
features['avg_speed'] = np.mean(distances) if distances else 0
|
| 832 |
+
else:
|
| 833 |
+
features['total_distance'] = 0
|
| 834 |
+
features['avg_speed'] = 0
|
| 835 |
+
|
| 836 |
+
# Track curvature
|
| 837 |
+
if len(lat_values) > 2:
|
| 838 |
+
bearing_changes = []
|
| 839 |
+
for i in range(1, len(lat_values)-1):
|
| 840 |
+
dlat1 = lat_values.iloc[i] - lat_values.iloc[i-1]
|
| 841 |
+
dlon1 = lon_values.iloc[i] - lon_values.iloc[i-1]
|
| 842 |
+
dlat2 = lat_values.iloc[i+1] - lat_values.iloc[i]
|
| 843 |
+
dlon2 = lon_values.iloc[i+1] - lon_values.iloc[i]
|
| 844 |
+
|
| 845 |
+
angle1 = np.arctan2(dlat1, dlon1)
|
| 846 |
+
angle2 = np.arctan2(dlat2, dlon2)
|
| 847 |
+
change = abs(angle2 - angle1)
|
| 848 |
+
bearing_changes.append(change)
|
| 849 |
+
|
| 850 |
+
features['avg_curvature'] = np.mean(bearing_changes) if bearing_changes else 0
|
| 851 |
+
else:
|
| 852 |
+
features['avg_curvature'] = 0
|
| 853 |
+
else:
|
| 854 |
+
# Default location values
|
| 855 |
+
features.update({
|
| 856 |
+
'LAT_mean': 20, 'LAT_std': 0, 'LAT_max': 20, 'LAT_min': 20,
|
| 857 |
+
'LON_mean': 140, 'LON_std': 0, 'LON_max': 140, 'LON_min': 140,
|
| 858 |
+
'genesis_lat': 20, 'genesis_lon': 140, 'genesis_intensity': 30,
|
| 859 |
+
'lat_range': 0, 'lon_range': 0, 'total_distance': 0,
|
| 860 |
+
'avg_speed': 0, 'avg_curvature': 0
|
| 861 |
+
})
|
| 862 |
+
else:
|
| 863 |
+
# Default location values if columns missing
|
| 864 |
+
features.update({
|
| 865 |
+
'LAT_mean': 20, 'LAT_std': 0, 'LAT_max': 20, 'LAT_min': 20,
|
| 866 |
+
'LON_mean': 140, 'LON_std': 0, 'LON_max': 140, 'LON_min': 140,
|
| 867 |
+
'genesis_lat': 20, 'genesis_lon': 140, 'genesis_intensity': 30,
|
| 868 |
+
'lat_range': 0, 'lon_range': 0, 'total_distance': 0,
|
| 869 |
+
'avg_speed': 0, 'avg_curvature': 0
|
| 870 |
+
})
|
| 871 |
+
|
| 872 |
+
# Track length
|
| 873 |
+
features['track_length'] = len(storm_data)
|
| 874 |
+
|
| 875 |
+
# Add seasonal information
|
| 876 |
+
if 'SEASON' in storm_data.columns:
|
| 877 |
+
features['season'] = storm_data['SEASON'].iloc[0]
|
| 878 |
+
else:
|
| 879 |
+
features['season'] = 2000
|
| 880 |
+
|
| 881 |
+
# Add basin information
|
| 882 |
+
if 'BASIN' in storm_data.columns:
|
| 883 |
+
features['basin'] = storm_data['BASIN'].iloc[0]
|
| 884 |
+
elif 'SID' in storm_data.columns:
|
| 885 |
+
features['basin'] = sid[:2] if len(sid) >= 2 else 'WP'
|
| 886 |
+
else:
|
| 887 |
+
features['basin'] = 'WP'
|
| 888 |
+
|
| 889 |
+
basic_features.append(features)
|
| 890 |
+
|
| 891 |
+
if not basic_features:
|
| 892 |
+
logging.error("No valid storm features could be extracted")
|
| 893 |
+
return None
|
| 894 |
+
|
| 895 |
+
# Convert to DataFrame
|
| 896 |
+
storm_features = pd.DataFrame(basic_features)
|
| 897 |
+
|
| 898 |
+
# Ensure all numeric columns are properly typed
|
| 899 |
+
numeric_columns = [col for col in storm_features.columns if col not in ['SID', 'basin']]
|
| 900 |
+
for col in numeric_columns:
|
| 901 |
+
storm_features[col] = pd.to_numeric(storm_features[col], errors='coerce').fillna(0)
|
| 902 |
+
|
| 903 |
+
logging.info(f"Successfully extracted features for {len(storm_features)} storms")
|
| 904 |
+
logging.info(f"Feature columns: {list(storm_features.columns)}")
|
| 905 |
+
|
| 906 |
+
return storm_features
|
| 907 |
+
|
| 908 |
+
except Exception as e:
|
| 909 |
+
logging.error(f"Error in extract_storm_features: {e}")
|
| 910 |
+
import traceback
|
| 911 |
+
traceback.print_exc()
|
| 912 |
+
return None
|
| 913 |
|
| 914 |
def perform_dimensionality_reduction(storm_features, method='umap', n_components=2):
|
| 915 |
+
"""Perform UMAP or t-SNE dimensionality reduction - FIXED VERSION"""
|
| 916 |
+
try:
|
| 917 |
+
if storm_features is None or storm_features.empty:
|
| 918 |
+
raise ValueError("No storm features provided")
|
| 919 |
+
|
| 920 |
+
# Select numeric features for clustering - FIXED
|
| 921 |
+
feature_cols = []
|
| 922 |
+
for col in storm_features.columns:
|
| 923 |
+
if col not in ['SID', 'basin'] and storm_features[col].dtype in ['float64', 'int64']:
|
| 924 |
+
# Check if column has valid data
|
| 925 |
+
valid_data = storm_features[col].dropna()
|
| 926 |
+
if len(valid_data) > 0 and valid_data.std() > 0: # Only include columns with variance
|
| 927 |
+
feature_cols.append(col)
|
| 928 |
+
|
| 929 |
+
if len(feature_cols) == 0:
|
| 930 |
+
raise ValueError("No valid numeric features found for clustering")
|
| 931 |
+
|
| 932 |
+
logging.info(f"Using {len(feature_cols)} features for clustering: {feature_cols}")
|
| 933 |
+
|
| 934 |
+
X = storm_features[feature_cols].fillna(0)
|
| 935 |
+
|
| 936 |
+
# Check if we have enough samples
|
| 937 |
+
if len(X) < 2:
|
| 938 |
+
raise ValueError("Need at least 2 storms for clustering")
|
| 939 |
+
|
| 940 |
+
# Standardize features
|
| 941 |
+
scaler = StandardScaler()
|
| 942 |
+
X_scaled = scaler.fit_transform(X)
|
| 943 |
+
|
| 944 |
+
# Perform dimensionality reduction
|
| 945 |
+
if method.lower() == 'umap' and UMAP_AVAILABLE and len(X_scaled) >= 4:
|
| 946 |
+
# UMAP parameters optimized for typhoon data - fixed warnings
|
| 947 |
+
n_neighbors = min(15, len(X_scaled) - 1)
|
| 948 |
+
reducer = umap.UMAP(
|
| 949 |
+
n_components=n_components,
|
| 950 |
+
n_neighbors=n_neighbors,
|
| 951 |
+
min_dist=0.1,
|
| 952 |
+
metric='euclidean',
|
| 953 |
+
random_state=42,
|
| 954 |
+
n_jobs=1 # Explicitly set to avoid warning
|
| 955 |
+
)
|
| 956 |
+
elif method.lower() == 'tsne' and len(X_scaled) >= 4:
|
| 957 |
+
# t-SNE parameters
|
| 958 |
+
perplexity = min(30, len(X_scaled) // 4)
|
| 959 |
+
perplexity = max(1, perplexity) # Ensure perplexity is at least 1
|
| 960 |
+
reducer = TSNE(
|
| 961 |
+
n_components=n_components,
|
| 962 |
+
perplexity=perplexity,
|
| 963 |
+
learning_rate=200,
|
| 964 |
+
n_iter=1000,
|
| 965 |
+
random_state=42
|
| 966 |
+
)
|
| 967 |
+
else:
|
| 968 |
+
# Fallback to PCA
|
| 969 |
+
reducer = PCA(n_components=n_components, random_state=42)
|
| 970 |
+
|
| 971 |
+
# Fit and transform
|
| 972 |
+
embedding = reducer.fit_transform(X_scaled)
|
| 973 |
+
|
| 974 |
+
logging.info(f"Dimensionality reduction successful: {X_scaled.shape} -> {embedding.shape}")
|
| 975 |
+
|
| 976 |
+
return embedding, feature_cols, scaler
|
| 977 |
+
|
| 978 |
+
except Exception as e:
|
| 979 |
+
logging.error(f"Error in perform_dimensionality_reduction: {e}")
|
| 980 |
+
raise
|
| 981 |
|
| 982 |
def cluster_storms(embedding, method='dbscan', eps=0.5, min_samples=3):
|
| 983 |
+
"""Cluster storms based on their embedding - FIXED VERSION"""
|
| 984 |
+
try:
|
| 985 |
+
if len(embedding) < 2:
|
| 986 |
+
return np.array([0] * len(embedding)) # Single cluster for insufficient data
|
| 987 |
+
|
| 988 |
+
if method.lower() == 'dbscan':
|
| 989 |
+
# Adjust min_samples based on data size
|
| 990 |
+
min_samples = min(min_samples, max(2, len(embedding) // 5))
|
| 991 |
+
clusterer = DBSCAN(eps=eps, min_samples=min_samples)
|
| 992 |
+
elif method.lower() == 'kmeans':
|
| 993 |
+
# Adjust n_clusters based on data size
|
| 994 |
+
n_clusters = min(5, max(2, len(embedding) // 3))
|
| 995 |
+
clusterer = KMeans(n_clusters=n_clusters, random_state=42)
|
| 996 |
+
else:
|
| 997 |
+
raise ValueError("Method must be 'dbscan' or 'kmeans'")
|
| 998 |
+
|
| 999 |
+
clusters = clusterer.fit_predict(embedding)
|
| 1000 |
+
|
| 1001 |
+
logging.info(f"Clustering complete: {len(np.unique(clusters))} clusters found")
|
| 1002 |
+
|
| 1003 |
+
return clusters
|
| 1004 |
+
|
| 1005 |
+
except Exception as e:
|
| 1006 |
+
logging.error(f"Error in cluster_storms: {e}")
|
| 1007 |
+
# Return single cluster as fallback
|
| 1008 |
+
return np.array([0] * len(embedding))
|
| 1009 |
|
| 1010 |
def create_advanced_clustering_visualization(storm_features, typhoon_data, method='umap', show_routes=True):
|
| 1011 |
+
"""Create comprehensive clustering visualization with route display - FIXED VERSION"""
|
| 1012 |
try:
|
| 1013 |
# Validate inputs
|
| 1014 |
if storm_features is None or storm_features.empty:
|
|
|
|
| 1017 |
if typhoon_data is None or typhoon_data.empty:
|
| 1018 |
raise ValueError("No typhoon data available for route visualization")
|
| 1019 |
|
| 1020 |
+
logging.info(f"Starting clustering visualization with {len(storm_features)} storms")
|
| 1021 |
+
|
| 1022 |
# Perform dimensionality reduction
|
| 1023 |
embedding, feature_cols, scaler = perform_dimensionality_reduction(storm_features, method)
|
| 1024 |
|
|
|
|
| 1031 |
storm_features_viz['dim1'] = embedding[:, 0]
|
| 1032 |
storm_features_viz['dim2'] = embedding[:, 1]
|
| 1033 |
|
| 1034 |
+
# Merge with typhoon data for additional info - SAFE MERGE
|
| 1035 |
+
try:
|
| 1036 |
+
storm_info = typhoon_data.groupby('SID').first()[['NAME', 'SEASON']].reset_index()
|
| 1037 |
+
storm_features_viz = storm_features_viz.merge(storm_info, on='SID', how='left')
|
| 1038 |
+
# Fill missing values
|
| 1039 |
+
storm_features_viz['NAME'] = storm_features_viz['NAME'].fillna('UNNAMED')
|
| 1040 |
+
storm_features_viz['SEASON'] = storm_features_viz['SEASON'].fillna(2000)
|
| 1041 |
+
except Exception as merge_error:
|
| 1042 |
+
logging.warning(f"Could not merge storm info: {merge_error}")
|
| 1043 |
+
storm_features_viz['NAME'] = 'UNNAMED'
|
| 1044 |
+
storm_features_viz['SEASON'] = 2000
|
| 1045 |
|
| 1046 |
if show_routes:
|
| 1047 |
# Create subplot with both scatter plot and route map
|
|
|
|
| 1062 |
color = CLUSTER_COLORS[i % len(CLUSTER_COLORS)] if cluster != -1 else '#CCCCCC'
|
| 1063 |
cluster_name = f'Cluster {cluster}' if cluster != -1 else 'Noise'
|
| 1064 |
|
| 1065 |
+
# FIXED: Add safe access to clustering features
|
| 1066 |
+
try:
|
| 1067 |
+
max_wind = cluster_data['USA_WIND_max'].fillna(0)
|
| 1068 |
+
min_pres = cluster_data['USA_PRES_min'].fillna(1000)
|
| 1069 |
+
track_len = cluster_data['track_length'].fillna(0)
|
| 1070 |
+
except KeyError as e:
|
| 1071 |
+
logging.warning(f"Missing clustering feature: {e}")
|
| 1072 |
+
max_wind = pd.Series([0] * len(cluster_data))
|
| 1073 |
+
min_pres = pd.Series([1000] * len(cluster_data))
|
| 1074 |
+
track_len = pd.Series([0] * len(cluster_data))
|
| 1075 |
+
|
| 1076 |
fig.add_trace(
|
| 1077 |
go.Scatter(
|
| 1078 |
x=cluster_data['dim1'],
|
|
|
|
| 1091 |
customdata=np.column_stack((
|
| 1092 |
cluster_data['NAME'].fillna('UNNAMED'),
|
| 1093 |
cluster_data['SEASON'].fillna(2000),
|
| 1094 |
+
max_wind,
|
| 1095 |
+
min_pres,
|
| 1096 |
+
track_len
|
| 1097 |
))
|
| 1098 |
),
|
| 1099 |
row=1, col=1
|
| 1100 |
)
|
| 1101 |
|
| 1102 |
+
# Add route map - FIXED with better error handling
|
| 1103 |
for i, cluster in enumerate(unique_clusters):
|
| 1104 |
if cluster == -1: # Skip noise for route visualization
|
| 1105 |
continue
|
|
|
|
| 1107 |
cluster_storm_ids = storm_features_viz[storm_features_viz['cluster'] == cluster]['SID'].tolist()
|
| 1108 |
color = CLUSTER_COLORS[i % len(CLUSTER_COLORS)]
|
| 1109 |
|
| 1110 |
+
tracks_added = 0
|
| 1111 |
+
for j, sid in enumerate(cluster_storm_ids[:5]): # Limit to 5 storms per cluster for performance
|
| 1112 |
try:
|
| 1113 |
storm_track = typhoon_data[typhoon_data['SID'] == sid].sort_values('ISO_TIME')
|
| 1114 |
if len(storm_track) > 1:
|
| 1115 |
+
# Ensure valid coordinates
|
| 1116 |
+
valid_coords = storm_track['LAT'].notna() & storm_track['LON'].notna()
|
| 1117 |
+
storm_track = storm_track[valid_coords]
|
| 1118 |
|
| 1119 |
+
if len(storm_track) > 1:
|
| 1120 |
+
storm_name = storm_track['NAME'].iloc[0] if pd.notna(storm_track['NAME'].iloc[0]) else 'UNNAMED'
|
| 1121 |
+
|
| 1122 |
+
fig.add_trace(
|
| 1123 |
+
go.Scattergeo(
|
| 1124 |
+
lon=storm_track['LON'],
|
| 1125 |
+
lat=storm_track['LAT'],
|
| 1126 |
+
mode='lines+markers',
|
| 1127 |
+
line=dict(color=color, width=2),
|
| 1128 |
+
marker=dict(color=color, size=4),
|
| 1129 |
+
name=f'C{cluster}: {storm_name}' if tracks_added == 0 else None,
|
| 1130 |
+
showlegend=(tracks_added == 0),
|
| 1131 |
+
hovertemplate=(
|
| 1132 |
+
f'<b>{storm_name}</b><br>'
|
| 1133 |
+
'Lat: %{lat:.1f}Β°<br>'
|
| 1134 |
+
'Lon: %{lon:.1f}Β°<br>'
|
| 1135 |
+
f'Cluster: {cluster}<br>'
|
| 1136 |
+
'<extra></extra>'
|
| 1137 |
+
)
|
| 1138 |
+
),
|
| 1139 |
+
row=1, col=2
|
| 1140 |
+
)
|
| 1141 |
+
tracks_added += 1
|
| 1142 |
except Exception as track_error:
|
| 1143 |
+
logging.warning(f"Error adding track for storm {sid}: {track_error}")
|
| 1144 |
continue
|
| 1145 |
|
| 1146 |
# Update layout
|
| 1147 |
fig.update_layout(
|
| 1148 |
title_text="Advanced Storm Clustering Analysis with Route Visualization",
|
| 1149 |
+
showlegend=True,
|
| 1150 |
+
height=600
|
| 1151 |
)
|
| 1152 |
|
| 1153 |
# Update geo layout
|
|
|
|
| 1174 |
x='dim1',
|
| 1175 |
y='dim2',
|
| 1176 |
color='cluster',
|
| 1177 |
+
hover_data=['NAME', 'SEASON'],
|
| 1178 |
title=f'Storm Clustering using {method.upper()}',
|
| 1179 |
labels={
|
| 1180 |
'dim1': f'{method.upper()} Dimension 1',
|
|
|
|
| 1183 |
}
|
| 1184 |
)
|
| 1185 |
|
| 1186 |
+
# Generate detailed cluster statistics - FIXED
|
| 1187 |
try:
|
| 1188 |
+
# Only use columns that actually exist
|
| 1189 |
+
available_cols = {
|
| 1190 |
+
'USA_WIND_max': 'USA_WIND_max',
|
| 1191 |
+
'USA_PRES_min': 'USA_PRES_min',
|
| 1192 |
+
'track_length': 'track_length',
|
| 1193 |
+
'genesis_lat': 'genesis_lat',
|
| 1194 |
+
'genesis_lon': 'genesis_lon',
|
| 1195 |
+
'total_distance': 'total_distance',
|
| 1196 |
+
'avg_curvature': 'avg_curvature',
|
| 1197 |
+
'SID': 'SID'
|
| 1198 |
+
}
|
|
|
|
|
|
|
| 1199 |
|
| 1200 |
+
# Filter to only existing columns
|
| 1201 |
+
existing_cols = {k: v for k, v in available_cols.items() if v in storm_features_viz.columns}
|
| 1202 |
|
| 1203 |
+
if len(existing_cols) > 1: # Need at least SID + one other column
|
| 1204 |
+
cluster_stats = storm_features_viz.groupby('cluster').agg(
|
| 1205 |
+
{col: ['mean', 'std', 'count'] if col != 'SID' else 'count'
|
| 1206 |
+
for col in existing_cols.values()}
|
| 1207 |
+
).round(2)
|
| 1208 |
|
| 1209 |
+
stats_text = "ADVANCED CLUSTER ANALYSIS RESULTS\n" + "="*50 + "\n\n"
|
|
|
|
| 1210 |
|
| 1211 |
+
for cluster in sorted(storm_features_viz['cluster'].unique()):
|
| 1212 |
+
cluster_data = storm_features_viz[storm_features_viz['cluster'] == cluster]
|
| 1213 |
+
storm_count = len(cluster_data)
|
| 1214 |
+
|
| 1215 |
+
if cluster == -1:
|
| 1216 |
+
stats_text += f"NOISE POINTS: {storm_count} storms\n\n"
|
| 1217 |
+
continue
|
| 1218 |
+
|
| 1219 |
+
stats_text += f"CLUSTER {cluster}: {storm_count} storms\n"
|
| 1220 |
+
|
| 1221 |
+
# Add available statistics
|
| 1222 |
+
if 'USA_WIND_max' in cluster_data.columns:
|
| 1223 |
+
wind_mean = cluster_data['USA_WIND_max'].mean()
|
| 1224 |
+
wind_std = cluster_data['USA_WIND_max'].std()
|
| 1225 |
+
stats_text += f" Intensity: {wind_mean:.1f} +/- {wind_std:.1f} kt\n"
|
| 1226 |
+
|
| 1227 |
+
if 'USA_PRES_min' in cluster_data.columns:
|
| 1228 |
+
pres_mean = cluster_data['USA_PRES_min'].mean()
|
| 1229 |
+
pres_std = cluster_data['USA_PRES_min'].std()
|
| 1230 |
+
stats_text += f" Pressure: {pres_mean:.1f} +/- {pres_std:.1f} hPa\n"
|
| 1231 |
+
|
| 1232 |
+
if 'track_length' in cluster_data.columns:
|
| 1233 |
+
track_mean = cluster_data['track_length'].mean()
|
| 1234 |
+
track_std = cluster_data['track_length'].std()
|
| 1235 |
+
stats_text += f" Track Length: {track_mean:.1f} +/- {track_std:.1f} points\n"
|
| 1236 |
+
|
| 1237 |
+
if 'genesis_lat' in cluster_data.columns and 'genesis_lon' in cluster_data.columns:
|
| 1238 |
+
lat_mean = cluster_data['genesis_lat'].mean()
|
| 1239 |
+
lon_mean = cluster_data['genesis_lon'].mean()
|
| 1240 |
+
stats_text += f" Genesis Region: {lat_mean:.1f}Β°N, {lon_mean:.1f}Β°E\n"
|
| 1241 |
+
|
| 1242 |
+
stats_text += "\n"
|
| 1243 |
+
|
| 1244 |
+
# Add feature importance summary
|
| 1245 |
+
stats_text += "CLUSTERING FEATURES USED:\n"
|
| 1246 |
+
stats_text += f" - Total features: {len(feature_cols)}\n"
|
| 1247 |
+
stats_text += f" - Available features: {', '.join(feature_cols[:5])}...\n\n"
|
| 1248 |
+
|
| 1249 |
+
stats_text += f"ALGORITHM: {method.upper()} + DBSCAN clustering\n"
|
| 1250 |
+
stats_text += f"CLUSTERS FOUND: {len([c for c in storm_features_viz['cluster'].unique() if c != -1])}\n"
|
| 1251 |
+
|
| 1252 |
+
else:
|
| 1253 |
+
stats_text = "Limited cluster statistics available due to missing feature columns."
|
| 1254 |
|
| 1255 |
except Exception as stats_error:
|
| 1256 |
+
logging.error(f"Error generating cluster statistics: {stats_error}")
|
| 1257 |
stats_text = f"Error generating cluster statistics: {str(stats_error)}"
|
| 1258 |
|
| 1259 |
return fig, stats_text, storm_features_viz
|
| 1260 |
|
| 1261 |
except Exception as e:
|
| 1262 |
+
logging.error(f"Error in clustering analysis: {e}")
|
| 1263 |
+
import traceback
|
| 1264 |
+
traceback.print_exc()
|
| 1265 |
+
|
| 1266 |
error_fig = go.Figure()
|
| 1267 |
error_fig.add_annotation(
|
| 1268 |
text=f"Error in clustering analysis: {str(e)}",
|
|
|
|
| 1273 |
return error_fig, f"Error in clustering: {str(e)}", None
|
| 1274 |
|
| 1275 |
# -----------------------------
|
| 1276 |
+
# ENHANCED: Advanced Prediction System with Route Forecasting
|
| 1277 |
# -----------------------------
|
| 1278 |
|
| 1279 |
+
def create_advanced_prediction_model(typhoon_data):
|
| 1280 |
+
"""Create advanced ML model for intensity and route prediction"""
|
|
|
|
|
|
|
|
|
|
| 1281 |
try:
|
| 1282 |
+
if typhoon_data is None or typhoon_data.empty:
|
| 1283 |
+
return None, "No data available for model training"
|
| 1284 |
+
|
| 1285 |
+
# Prepare training data
|
| 1286 |
+
features = []
|
| 1287 |
+
targets = []
|
| 1288 |
+
|
| 1289 |
+
for sid in typhoon_data['SID'].unique():
|
| 1290 |
+
storm_data = typhoon_data[typhoon_data['SID'] == sid].sort_values('ISO_TIME')
|
| 1291 |
|
| 1292 |
+
if len(storm_data) < 3: # Need at least 3 points for prediction
|
| 1293 |
+
continue
|
|
|
|
|
|
|
|
|
|
| 1294 |
|
| 1295 |
+
for i in range(len(storm_data) - 1):
|
| 1296 |
+
current = storm_data.iloc[i]
|
| 1297 |
+
next_point = storm_data.iloc[i + 1]
|
| 1298 |
+
|
| 1299 |
+
# Extract features (current state)
|
| 1300 |
+
feature_row = []
|
| 1301 |
+
|
| 1302 |
+
# Current position
|
| 1303 |
+
feature_row.extend([
|
| 1304 |
+
current.get('LAT', 20),
|
| 1305 |
+
current.get('LON', 140)
|
| 1306 |
+
])
|
| 1307 |
+
|
| 1308 |
+
# Current intensity
|
| 1309 |
+
feature_row.extend([
|
| 1310 |
+
current.get('USA_WIND', 30),
|
| 1311 |
+
current.get('USA_PRES', 1000)
|
| 1312 |
+
])
|
| 1313 |
+
|
| 1314 |
+
# Time features
|
| 1315 |
+
if 'ISO_TIME' in current and pd.notna(current['ISO_TIME']):
|
| 1316 |
+
month = current['ISO_TIME'].month
|
| 1317 |
+
day_of_year = current['ISO_TIME'].dayofyear
|
| 1318 |
+
else:
|
| 1319 |
+
month = 9 # Peak season default
|
| 1320 |
+
day_of_year = 250
|
| 1321 |
+
|
| 1322 |
+
feature_row.extend([month, day_of_year])
|
| 1323 |
+
|
| 1324 |
+
# Motion features (if previous point exists)
|
| 1325 |
+
if i > 0:
|
| 1326 |
+
prev = storm_data.iloc[i - 1]
|
| 1327 |
+
dlat = current.get('LAT', 20) - prev.get('LAT', 20)
|
| 1328 |
+
dlon = current.get('LON', 140) - prev.get('LON', 140)
|
| 1329 |
+
speed = np.sqrt(dlat**2 + dlon**2)
|
| 1330 |
+
bearing = np.arctan2(dlat, dlon)
|
| 1331 |
+
else:
|
| 1332 |
+
speed = 0
|
| 1333 |
+
bearing = 0
|
| 1334 |
+
|
| 1335 |
+
feature_row.extend([speed, bearing])
|
| 1336 |
+
|
| 1337 |
+
features.append(feature_row)
|
| 1338 |
+
|
| 1339 |
+
# Target: next position and intensity
|
| 1340 |
+
targets.append([
|
| 1341 |
+
next_point.get('LAT', 20),
|
| 1342 |
+
next_point.get('LON', 140),
|
| 1343 |
+
next_point.get('USA_WIND', 30)
|
| 1344 |
+
])
|
| 1345 |
|
| 1346 |
+
if len(features) < 10: # Need sufficient training data
|
| 1347 |
+
return None, "Insufficient data for model training"
|
|
|
|
|
|
|
|
|
|
| 1348 |
|
| 1349 |
+
# Train model
|
| 1350 |
+
X = np.array(features)
|
| 1351 |
+
y = np.array(targets)
|
| 1352 |
+
|
| 1353 |
+
# Split data
|
| 1354 |
+
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
|
| 1355 |
+
|
| 1356 |
+
# Create separate models for position and intensity
|
| 1357 |
+
models = {}
|
| 1358 |
+
|
| 1359 |
+
# Position model (lat, lon)
|
| 1360 |
+
pos_model = RandomForestRegressor(n_estimators=100, random_state=42)
|
| 1361 |
+
pos_model.fit(X_train, y_train[:, :2])
|
| 1362 |
+
models['position'] = pos_model
|
| 1363 |
|
| 1364 |
+
# Intensity model (wind speed)
|
| 1365 |
+
int_model = RandomForestRegressor(n_estimators=100, random_state=42)
|
| 1366 |
+
int_model.fit(X_train, y_train[:, 2])
|
| 1367 |
+
models['intensity'] = int_model
|
| 1368 |
|
| 1369 |
+
# Calculate model performance
|
| 1370 |
+
pos_pred = pos_model.predict(X_test)
|
| 1371 |
+
int_pred = int_model.predict(X_test)
|
|
|
|
| 1372 |
|
| 1373 |
+
pos_mae = mean_absolute_error(y_test[:, :2], pos_pred)
|
| 1374 |
+
int_mae = mean_absolute_error(y_test[:, 2], int_pred)
|
|
|
|
|
|
|
| 1375 |
|
| 1376 |
+
model_info = f"Position MAE: {pos_mae:.2f}Β°, Intensity MAE: {int_mae:.2f} kt"
|
| 1377 |
+
|
| 1378 |
+
return models, model_info
|
| 1379 |
|
|
|
|
| 1380 |
except Exception as e:
|
| 1381 |
+
return None, f"Error creating prediction model: {str(e)}"
|
|
|
|
| 1382 |
|
| 1383 |
+
def predict_storm_route_and_intensity(lat, lon, month, oni_value, models=None, forecast_hours=72):
|
| 1384 |
+
"""Advanced prediction with route and intensity forecasting"""
|
| 1385 |
try:
|
| 1386 |
+
results = {
|
| 1387 |
+
'current_prediction': {},
|
| 1388 |
+
'route_forecast': [],
|
| 1389 |
+
'confidence_scores': {},
|
| 1390 |
+
'model_info': 'Physics-based prediction'
|
| 1391 |
+
}
|
| 1392 |
+
|
| 1393 |
+
# Current intensity prediction (enhanced)
|
| 1394 |
base_intensity = 45
|
| 1395 |
|
| 1396 |
+
# ENSO effects (enhanced)
|
| 1397 |
if oni_value > 0.5: # El NiΓ±o
|
| 1398 |
+
intensity_modifier = -15 - (oni_value - 0.5) * 10 # Stronger suppression
|
| 1399 |
elif oni_value < -0.5: # La NiΓ±a
|
| 1400 |
+
intensity_modifier = 20 + abs(oni_value + 0.5) * 15 # Stronger enhancement
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1401 |
else:
|
| 1402 |
+
intensity_modifier = oni_value * 5 # Linear relationship in neutral
|
| 1403 |
|
| 1404 |
+
# Enhanced seasonal effects
|
| 1405 |
+
seasonal_factors = {
|
| 1406 |
+
1: -20, 2: -15, 3: -10, 4: -5, 5: 0, 6: 10,
|
| 1407 |
+
7: 20, 8: 25, 9: 30, 10: 25, 11: 15, 12: -10
|
| 1408 |
+
}
|
| 1409 |
+
seasonal_modifier = seasonal_factors.get(month, 0)
|
| 1410 |
+
|
| 1411 |
+
# Enhanced latitude effects
|
| 1412 |
+
optimal_lat = 15 # Optimal latitude for development
|
| 1413 |
+
lat_modifier = 15 - abs(abs(lat) - optimal_lat) * 2
|
| 1414 |
+
|
| 1415 |
+
# SST proxy (longitude-based in WP)
|
| 1416 |
if 120 <= lon <= 160:
|
| 1417 |
+
sst_modifier = 15 # Warm pool
|
| 1418 |
+
elif 160 <= lon <= 180:
|
| 1419 |
+
sst_modifier = 10 # Still favorable
|
| 1420 |
else:
|
| 1421 |
+
sst_modifier = -10 # Less favorable
|
| 1422 |
|
| 1423 |
+
# Calculate current intensity
|
| 1424 |
+
predicted_intensity = base_intensity + intensity_modifier + seasonal_modifier + lat_modifier + sst_modifier
|
| 1425 |
predicted_intensity = max(25, min(180, predicted_intensity))
|
| 1426 |
|
| 1427 |
+
# Add some realistic uncertainty
|
| 1428 |
+
intensity_uncertainty = np.random.normal(0, 5)
|
| 1429 |
+
predicted_intensity += intensity_uncertainty
|
| 1430 |
+
|
| 1431 |
+
results['current_prediction'] = {
|
| 1432 |
+
'intensity_kt': predicted_intensity,
|
| 1433 |
+
'pressure_hpa': 1013 - (predicted_intensity - 25) * 0.8, # Rough intensity-pressure relationship
|
| 1434 |
+
'category': categorize_typhoon_enhanced(predicted_intensity)
|
| 1435 |
+
}
|
| 1436 |
+
|
| 1437 |
+
# Route prediction (enhanced physics-based)
|
| 1438 |
+
current_lat = lat
|
| 1439 |
+
current_lon = lon
|
| 1440 |
+
|
| 1441 |
+
route_points = []
|
| 1442 |
+
|
| 1443 |
+
for hour in range(0, forecast_hours + 6, 6): # 6-hour intervals
|
| 1444 |
+
# Enhanced steering flow simulation
|
| 1445 |
+
# Basic westward motion with poleward component
|
| 1446 |
+
|
| 1447 |
+
# Seasonal steering patterns
|
| 1448 |
+
if month in [6, 7, 8, 9]: # Summer/early fall - more recurvature
|
| 1449 |
+
lat_tendency = 0.15 + (current_lat - 10) * 0.02
|
| 1450 |
+
lon_tendency = -0.3 + abs(current_lat - 25) * 0.01
|
| 1451 |
+
else: # Other seasons - more westward motion
|
| 1452 |
+
lat_tendency = 0.05 + (current_lat - 15) * 0.01
|
| 1453 |
+
lon_tendency = -0.4
|
| 1454 |
+
|
| 1455 |
+
# ENSO modulation of steering
|
| 1456 |
+
if oni_value > 0.5: # El NiΓ±o - more eastward steering
|
| 1457 |
+
lon_tendency += 0.1
|
| 1458 |
+
elif oni_value < -0.5: # La NiΓ±a - more westward
|
| 1459 |
+
lon_tendency -= 0.1
|
| 1460 |
+
|
| 1461 |
+
# Add realistic variability
|
| 1462 |
+
lat_noise = np.random.normal(0, 0.05)
|
| 1463 |
+
lon_noise = np.random.normal(0, 0.05)
|
| 1464 |
+
|
| 1465 |
+
# Update position
|
| 1466 |
+
current_lat += lat_tendency + lat_noise
|
| 1467 |
+
current_lon += lon_tendency + lon_noise
|
| 1468 |
+
|
| 1469 |
+
# Intensity evolution
|
| 1470 |
+
# Decay over time (simplified)
|
| 1471 |
+
intensity_decay = min(5, hour / 24 * 2) # Gradual weakening
|
| 1472 |
+
hour_intensity = max(25, predicted_intensity - intensity_decay)
|
| 1473 |
+
|
| 1474 |
+
# Environmental modulation
|
| 1475 |
+
if current_lat > 35: # High latitude weakening
|
| 1476 |
+
hour_intensity = max(25, hour_intensity - 10)
|
| 1477 |
+
elif current_lon < 120: # Over land approximation
|
| 1478 |
+
hour_intensity = max(25, hour_intensity - 15)
|
| 1479 |
+
|
| 1480 |
+
route_points.append({
|
| 1481 |
+
'hour': hour,
|
| 1482 |
+
'lat': current_lat,
|
| 1483 |
+
'lon': current_lon,
|
| 1484 |
+
'intensity_kt': hour_intensity,
|
| 1485 |
+
'category': categorize_typhoon_enhanced(hour_intensity)
|
| 1486 |
+
})
|
| 1487 |
+
|
| 1488 |
+
results['route_forecast'] = route_points
|
| 1489 |
+
|
| 1490 |
+
# Confidence scores
|
| 1491 |
+
results['confidence_scores'] = {
|
| 1492 |
+
'intensity': 0.75,
|
| 1493 |
+
'position_24h': 0.80,
|
| 1494 |
+
'position_48h': 0.65,
|
| 1495 |
+
'position_72h': 0.50
|
| 1496 |
+
}
|
| 1497 |
+
|
| 1498 |
+
# Enhanced model info
|
| 1499 |
+
if CNN_AVAILABLE:
|
| 1500 |
+
results['model_info'] = "Hybrid Physics-ML Model (TensorFlow Enhanced)"
|
| 1501 |
+
else:
|
| 1502 |
+
results['model_info'] = "Advanced Physics-Based Model"
|
| 1503 |
+
|
| 1504 |
+
return results
|
| 1505 |
+
|
| 1506 |
+
except Exception as e:
|
| 1507 |
+
return {
|
| 1508 |
+
'error': f"Prediction error: {str(e)}",
|
| 1509 |
+
'current_prediction': {'intensity_kt': 50, 'category': 'Tropical Storm'},
|
| 1510 |
+
'route_forecast': [],
|
| 1511 |
+
'confidence_scores': {},
|
| 1512 |
+
'model_info': 'Error in prediction'
|
| 1513 |
+
}
|
| 1514 |
+
|
| 1515 |
+
def create_route_visualization(prediction_results, show_uncertainty=True):
|
| 1516 |
+
"""Create comprehensive route and intensity visualization"""
|
| 1517 |
+
try:
|
| 1518 |
+
if 'route_forecast' not in prediction_results or not prediction_results['route_forecast']:
|
| 1519 |
+
return None, "No route forecast data available"
|
| 1520 |
+
|
| 1521 |
+
route_data = prediction_results['route_forecast']
|
| 1522 |
+
|
| 1523 |
+
# Create subplot with route map and intensity evolution
|
| 1524 |
+
fig = make_subplots(
|
| 1525 |
+
rows=1, cols=2,
|
| 1526 |
+
subplot_titles=('Forecast Track', 'Intensity Evolution'),
|
| 1527 |
+
specs=[[{"type": "geo"}, {"type": "scatter"}]],
|
| 1528 |
+
column_widths=[0.6, 0.4]
|
| 1529 |
+
)
|
| 1530 |
+
|
| 1531 |
+
# Extract data for plotting
|
| 1532 |
+
hours = [point['hour'] for point in route_data]
|
| 1533 |
+
lats = [point['lat'] for point in route_data]
|
| 1534 |
+
lons = [point['lon'] for point in route_data]
|
| 1535 |
+
intensities = [point['intensity_kt'] for point in route_data]
|
| 1536 |
+
categories = [point['category'] for point in route_data]
|
| 1537 |
+
|
| 1538 |
+
# Route visualization with intensity colors
|
| 1539 |
+
for i in range(len(route_data)):
|
| 1540 |
+
point = route_data[i]
|
| 1541 |
+
color = enhanced_color_map.get(point['category'], 'rgb(128,128,128)')
|
| 1542 |
+
|
| 1543 |
+
# Convert rgb string to rgba for transparency
|
| 1544 |
+
if i == 0: # Current position
|
| 1545 |
+
marker_size = 15
|
| 1546 |
+
opacity = 1.0
|
| 1547 |
+
else:
|
| 1548 |
+
marker_size = 10
|
| 1549 |
+
opacity = 1.0 - (i / len(route_data)) * 0.5 # Fade with time
|
| 1550 |
+
|
| 1551 |
+
fig.add_trace(
|
| 1552 |
+
go.Scattergeo(
|
| 1553 |
+
lon=[point['lon']],
|
| 1554 |
+
lat=[point['lat']],
|
| 1555 |
+
mode='markers',
|
| 1556 |
+
marker=dict(
|
| 1557 |
+
size=marker_size,
|
| 1558 |
+
color=color,
|
| 1559 |
+
opacity=opacity,
|
| 1560 |
+
line=dict(width=2, color='white')
|
| 1561 |
+
),
|
| 1562 |
+
name=f"Hour {point['hour']}" if i % 4 == 0 else None, # Show every 4th hour in legend
|
| 1563 |
+
showlegend=(i % 4 == 0),
|
| 1564 |
+
hovertemplate=(
|
| 1565 |
+
f"<b>Hour {point['hour']}</b><br>"
|
| 1566 |
+
f"Position: {point['lat']:.1f}Β°N, {point['lon']:.1f}Β°E<br>"
|
| 1567 |
+
f"Intensity: {point['intensity_kt']:.0f} kt<br>"
|
| 1568 |
+
f"Category: {point['category']}<br>"
|
| 1569 |
+
"<extra></extra>"
|
| 1570 |
+
)
|
| 1571 |
+
),
|
| 1572 |
+
row=1, col=1
|
| 1573 |
+
)
|
| 1574 |
+
|
| 1575 |
+
# Connect points with lines
|
| 1576 |
+
fig.add_trace(
|
| 1577 |
+
go.Scattergeo(
|
| 1578 |
+
lon=lons,
|
| 1579 |
+
lat=lats,
|
| 1580 |
+
mode='lines',
|
| 1581 |
+
line=dict(color='black', width=2, dash='dash'),
|
| 1582 |
+
name='Forecast Track',
|
| 1583 |
+
showlegend=True
|
| 1584 |
+
),
|
| 1585 |
+
row=1, col=1
|
| 1586 |
+
)
|
| 1587 |
+
|
| 1588 |
+
# Uncertainty cone (if requested)
|
| 1589 |
+
if show_uncertainty and len(route_data) > 1:
|
| 1590 |
+
uncertainty_lats_upper = []
|
| 1591 |
+
uncertainty_lats_lower = []
|
| 1592 |
+
uncertainty_lons_upper = []
|
| 1593 |
+
uncertainty_lons_lower = []
|
| 1594 |
+
|
| 1595 |
+
for i, point in enumerate(route_data):
|
| 1596 |
+
# Uncertainty grows with time
|
| 1597 |
+
uncertainty = 0.5 + (i / len(route_data)) * 2.0 # degrees
|
| 1598 |
+
|
| 1599 |
+
uncertainty_lats_upper.append(point['lat'] + uncertainty)
|
| 1600 |
+
uncertainty_lats_lower.append(point['lat'] - uncertainty)
|
| 1601 |
+
uncertainty_lons_upper.append(point['lon'] + uncertainty)
|
| 1602 |
+
uncertainty_lons_lower.append(point['lon'] - uncertainty)
|
| 1603 |
+
|
| 1604 |
+
# Add uncertainty cone
|
| 1605 |
+
uncertainty_lats = uncertainty_lats_upper + uncertainty_lats_lower[::-1]
|
| 1606 |
+
uncertainty_lons = uncertainty_lons_upper + uncertainty_lons_lower[::-1]
|
| 1607 |
+
|
| 1608 |
+
fig.add_trace(
|
| 1609 |
+
go.Scattergeo(
|
| 1610 |
+
lon=uncertainty_lons,
|
| 1611 |
+
lat=uncertainty_lats,
|
| 1612 |
+
mode='lines',
|
| 1613 |
+
fill='toself',
|
| 1614 |
+
fillcolor='rgba(128,128,128,0.2)',
|
| 1615 |
+
line=dict(color='rgba(128,128,128,0.3)', width=1),
|
| 1616 |
+
name='Uncertainty Cone',
|
| 1617 |
+
showlegend=True
|
| 1618 |
+
),
|
| 1619 |
+
row=1, col=1
|
| 1620 |
+
)
|
| 1621 |
+
|
| 1622 |
+
# Intensity evolution plot
|
| 1623 |
+
fig.add_trace(
|
| 1624 |
+
go.Scatter(
|
| 1625 |
+
x=hours,
|
| 1626 |
+
y=intensities,
|
| 1627 |
+
mode='lines+markers',
|
| 1628 |
+
line=dict(color='red', width=3),
|
| 1629 |
+
marker=dict(size=8, color='red'),
|
| 1630 |
+
name='Intensity Forecast',
|
| 1631 |
+
hovertemplate=(
|
| 1632 |
+
"Hour: %{x}<br>"
|
| 1633 |
+
"Intensity: %{y:.0f} kt<br>"
|
| 1634 |
+
"<extra></extra>"
|
| 1635 |
+
)
|
| 1636 |
+
),
|
| 1637 |
+
row=1, col=2
|
| 1638 |
+
)
|
| 1639 |
+
|
| 1640 |
+
# Add category thresholds
|
| 1641 |
+
thresholds = [34, 64, 83, 96, 113, 137]
|
| 1642 |
+
threshold_names = ['TS', 'C1', 'C2', 'C3', 'C4', 'C5']
|
| 1643 |
+
|
| 1644 |
+
for thresh, name in zip(thresholds, threshold_names):
|
| 1645 |
+
fig.add_hline(
|
| 1646 |
+
y=thresh,
|
| 1647 |
+
line_dash="dash",
|
| 1648 |
+
line_color="gray",
|
| 1649 |
+
annotation_text=name,
|
| 1650 |
+
annotation_position="left",
|
| 1651 |
+
row=1, col=2
|
| 1652 |
+
)
|
| 1653 |
+
|
| 1654 |
+
# Update layout
|
| 1655 |
+
fig.update_layout(
|
| 1656 |
+
title_text="Advanced Storm Forecast: Track and Intensity Evolution",
|
| 1657 |
+
showlegend=True,
|
| 1658 |
+
height=600
|
| 1659 |
+
)
|
| 1660 |
+
|
| 1661 |
+
# Update geo layout
|
| 1662 |
+
fig.update_geos(
|
| 1663 |
+
projection_type="natural earth",
|
| 1664 |
+
showland=True,
|
| 1665 |
+
landcolor="LightGray",
|
| 1666 |
+
showocean=True,
|
| 1667 |
+
oceancolor="LightBlue",
|
| 1668 |
+
showcoastlines=True,
|
| 1669 |
+
coastlinecolor="Gray",
|
| 1670 |
+
center=dict(lat=lats[0], lon=lons[0]),
|
| 1671 |
+
resolution=50,
|
| 1672 |
+
row=1, col=1
|
| 1673 |
+
)
|
| 1674 |
+
|
| 1675 |
+
# Update intensity plot
|
| 1676 |
+
fig.update_xaxes(title_text="Forecast Hour", row=1, col=2)
|
| 1677 |
+
fig.update_yaxes(title_text="Intensity (kt)", row=1, col=2)
|
| 1678 |
+
|
| 1679 |
+
# Generate detailed forecast text
|
| 1680 |
+
current = prediction_results['current_prediction']
|
| 1681 |
+
forecast_text = f"""
|
| 1682 |
+
DETAILED FORECAST SUMMARY
|
| 1683 |
+
{'='*50}
|
| 1684 |
+
|
| 1685 |
+
CURRENT CONDITIONS:
|
| 1686 |
+
β’ Intensity: {current['intensity_kt']:.0f} kt
|
| 1687 |
+
β’ Category: {current['category']}
|
| 1688 |
+
β’ Pressure: {current.get('pressure_hpa', 1000):.0f} hPa
|
| 1689 |
+
|
| 1690 |
+
FORECAST TRACK (72-HOUR):
|
| 1691 |
+
β’ Initial Position: {lats[0]:.1f}Β°N, {lons[0]:.1f}Β°E
|
| 1692 |
+
β’ 24-hour Position: {lats[4]:.1f}Β°N, {lons[4]:.1f}Β°E
|
| 1693 |
+
β’ 48-hour Position: {lats[8]:.1f}Β°N, {lons[8]:.1f}Β°E
|
| 1694 |
+
β’ 72-hour Position: {lats[-1]:.1f}Β°N, {lons[-1]:.1f}Β°E
|
| 1695 |
+
|
| 1696 |
+
INTENSITY EVOLUTION:
|
| 1697 |
+
β’ Current: {intensities[0]:.0f} kt ({categories[0]})
|
| 1698 |
+
β’ 24-hour: {intensities[4]:.0f} kt ({categories[4]})
|
| 1699 |
+
β’ 48-hour: {intensities[8]:.0f} kt ({categories[8]})
|
| 1700 |
+
β’ 72-hour: {intensities[-1]:.0f} kt ({categories[-1]})
|
| 1701 |
+
|
| 1702 |
+
CONFIDENCE LEVELS:
|
| 1703 |
+
β’ 24-hour Position: {prediction_results['confidence_scores'].get('position_24h', 0.8)*100:.0f}%
|
| 1704 |
+
β’ 48-hour Position: {prediction_results['confidence_scores'].get('position_48h', 0.6)*100:.0f}%
|
| 1705 |
+
β’ 72-hour Position: {prediction_results['confidence_scores'].get('position_72h', 0.5)*100:.0f}%
|
| 1706 |
+
β’ Intensity: {prediction_results['confidence_scores'].get('intensity', 0.7)*100:.0f}%
|
| 1707 |
+
|
| 1708 |
+
MODEL: {prediction_results['model_info']}
|
| 1709 |
+
"""
|
| 1710 |
+
|
| 1711 |
+
return fig, forecast_text.strip()
|
| 1712 |
|
|
|
|
| 1713 |
except Exception as e:
|
| 1714 |
+
return None, f"Error creating route visualization: {str(e)}"
|
| 1715 |
|
| 1716 |
# -----------------------------
|
| 1717 |
# Regression Functions (Original)
|
|
|
|
| 2236 |
initialize_data()
|
| 2237 |
|
| 2238 |
# -----------------------------
|
| 2239 |
+
# ENHANCED: Gradio Interface with Advanced Features
|
| 2240 |
# -----------------------------
|
| 2241 |
|
| 2242 |
def create_interface():
|
|
|
|
| 2261 |
available_years = [str(year) for year in range(2000, 2026)]
|
| 2262 |
|
| 2263 |
with gr.Blocks(title="Enhanced Typhoon Analysis Platform", theme=gr.themes.Soft()) as demo:
|
| 2264 |
+
gr.Markdown("# πͺοΈ Enhanced Typhoon Analysis Platform")
|
| 2265 |
+
gr.Markdown("**Advanced ML clustering, route predictions, and comprehensive tropical cyclone analysis including Tropical Depressions**")
|
| 2266 |
|
| 2267 |
+
with gr.Tab("π Overview"):
|
| 2268 |
overview_text = f"""
|
| 2269 |
## Welcome to the Enhanced Typhoon Analysis Dashboard
|
| 2270 |
|
| 2271 |
This dashboard provides comprehensive analysis of typhoon data in relation to ENSO phases with advanced machine learning capabilities.
|
| 2272 |
|
| 2273 |
+
### π Enhanced Features:
|
| 2274 |
+
- **Advanced ML Clustering**: UMAP/t-SNE storm pattern analysis with route visualization
|
| 2275 |
+
- **Predictive Routing**: Advanced storm track and intensity forecasting with uncertainty quantification
|
| 2276 |
+
- **Complete TD Support**: Now includes Tropical Depressions (< 34 kt)
|
| 2277 |
+
- **2025 Data Ready**: Real-time compatibility with current year data
|
| 2278 |
+
- **Enhanced Animations**: High-quality storm track visualizations
|
| 2279 |
+
|
| 2280 |
+
### π Data Status:
|
| 2281 |
+
- **ONI Data**: {len(oni_data)} years loaded
|
| 2282 |
+
- **Typhoon Data**: {total_records:,} records loaded
|
| 2283 |
+
- **Merged Data**: {len(merged_data):,} typhoons with ONI values
|
| 2284 |
+
- **Available Years**: {year_range_display}
|
| 2285 |
+
|
| 2286 |
+
### π§ Technical Capabilities:
|
| 2287 |
+
- **UMAP Clustering**: {"β
Available" if UMAP_AVAILABLE else "β οΈ Limited to t-SNE/PCA"}
|
| 2288 |
+
- **AI Predictions**: {"π§ Deep Learning" if CNN_AVAILABLE else "π¬ Physics-based"}
|
| 2289 |
+
- **Enhanced Categorization**: Tropical Depression to Super Typhoon
|
| 2290 |
+
- **Platform**: Optimized for Hugging Face Spaces
|
| 2291 |
+
|
| 2292 |
+
### π Research Applications:
|
| 2293 |
+
- Climate change impact studies
|
| 2294 |
+
- Seasonal forecasting research
|
| 2295 |
+
- Storm pattern classification
|
| 2296 |
+
- ENSO-typhoon relationship analysis
|
| 2297 |
+
- Intensity prediction model development
|
| 2298 |
"""
|
| 2299 |
gr.Markdown(overview_text)
|
| 2300 |
|
| 2301 |
+
with gr.Tab("π¬ Advanced ML Clustering"):
|
| 2302 |
+
gr.Markdown("## π― Storm Pattern Analysis using UMAP/t-SNE with Route Visualization")
|
| 2303 |
gr.Markdown("**This tab shows both the dimensional clustering analysis AND the actual storm tracks colored by cluster**")
|
| 2304 |
|
| 2305 |
with gr.Row():
|
| 2306 |
+
with gr.Column(scale=2):
|
| 2307 |
+
reduction_method = gr.Dropdown(
|
| 2308 |
+
choices=['UMAP', 't-SNE', 'PCA'],
|
| 2309 |
+
value='UMAP' if UMAP_AVAILABLE else 't-SNE',
|
| 2310 |
+
label="π Dimensionality Reduction Method",
|
| 2311 |
+
info="UMAP provides better global structure preservation"
|
| 2312 |
+
)
|
| 2313 |
+
with gr.Column(scale=1):
|
| 2314 |
+
show_routes = gr.Checkbox(
|
| 2315 |
+
label="πΊοΈ Show Storm Routes on Map",
|
| 2316 |
+
value=True,
|
| 2317 |
+
info="Display actual storm tracks colored by cluster"
|
| 2318 |
+
)
|
| 2319 |
|
| 2320 |
+
analyze_clusters_btn = gr.Button("π Analyze Storm Clusters & Routes", variant="primary", size="lg")
|
| 2321 |
|
| 2322 |
with gr.Row():
|
| 2323 |
cluster_plot = gr.Plot(label="Storm Clustering with Route Visualization")
|
| 2324 |
|
| 2325 |
with gr.Row():
|
| 2326 |
+
cluster_stats = gr.Textbox(label="π Detailed Cluster Statistics", lines=15, max_lines=20)
|
| 2327 |
|
| 2328 |
def run_advanced_clustering_analysis(method, show_routes):
|
| 2329 |
try:
|
| 2330 |
# Extract features for clustering
|
| 2331 |
storm_features = extract_storm_features(typhoon_data)
|
| 2332 |
+
if storm_features is None:
|
| 2333 |
+
return None, "Error: Could not extract storm features"
|
| 2334 |
fig, stats, _ = create_advanced_clustering_visualization(storm_features, typhoon_data, method.lower(), show_routes)
|
| 2335 |
return fig, stats
|
| 2336 |
except Exception as e:
|
|
|
|
| 2345 |
)
|
| 2346 |
|
| 2347 |
cluster_info_text = """
|
| 2348 |
+
### π Advanced Clustering Features:
|
| 2349 |
+
- **Multi-dimensional Analysis**: Uses 15+ storm characteristics including intensity, track shape, genesis location
|
| 2350 |
+
- **Route Visualization**: Shows actual storm tracks colored by cluster membership
|
| 2351 |
+
- **DBSCAN Clustering**: Automatically finds natural groupings without predefined cluster count
|
| 2352 |
+
- **Comprehensive Stats**: Detailed cluster analysis including intensity, pressure, track length, curvature
|
| 2353 |
+
- **Interactive**: Hover over points to see storm details, zoom and pan the route map
|
| 2354 |
+
|
| 2355 |
+
### π― How to Interpret:
|
| 2356 |
+
- **Left Plot**: Each dot is a storm positioned by similarity (close = similar characteristics)
|
| 2357 |
+
- **Right Plot**: Actual geographic storm tracks, colored by which cluster they belong to
|
| 2358 |
+
- **Cluster Colors**: Each cluster gets a unique color to identify similar storm patterns
|
| 2359 |
+
- **Noise Points**: Gray points represent storms that don't fit clear patterns
|
| 2360 |
"""
|
| 2361 |
gr.Markdown(cluster_info_text)
|
| 2362 |
|
| 2363 |
+
with gr.Tab("π― Advanced Storm Prediction"):
|
| 2364 |
+
gr.Markdown("## π AI-Powered Storm Intensity & Route Forecasting")
|
| 2365 |
|
| 2366 |
if CNN_AVAILABLE:
|
| 2367 |
+
gr.Markdown("π§ **Deep Learning models available** - TensorFlow loaded successfully")
|
| 2368 |
method_description = "Using Convolutional Neural Networks for advanced intensity prediction"
|
| 2369 |
else:
|
| 2370 |
+
gr.Markdown("π¬ **Physics-based models available** - Using climatological relationships")
|
| 2371 |
gr.Markdown("*Install TensorFlow for deep learning features: `pip install tensorflow-cpu`*")
|
| 2372 |
method_description = "Using established meteorological relationships and climatology"
|
| 2373 |
|
| 2374 |
gr.Markdown(f"**Current Method**: {method_description}")
|
| 2375 |
|
| 2376 |
with gr.Row():
|
| 2377 |
+
with gr.Column(scale=2):
|
| 2378 |
+
gr.Markdown("### π Initial Conditions")
|
| 2379 |
+
with gr.Row():
|
| 2380 |
+
pred_lat = gr.Number(label="Latitude (Β°N)", value=15.0, info="Storm center latitude (-90 to 90)")
|
| 2381 |
+
pred_lon = gr.Number(label="Longitude (Β°E)", value=140.0, info="Storm center longitude (-180 to 180)")
|
| 2382 |
+
with gr.Row():
|
| 2383 |
+
pred_month = gr.Slider(1, 12, label="Month", value=9, info="Month of year (1=Jan, 12=Dec)")
|
| 2384 |
+
pred_oni = gr.Number(label="ONI Value", value=0.0, info="Current ENSO index (-3 to 3)")
|
| 2385 |
+
with gr.Row():
|
| 2386 |
+
forecast_hours = gr.Slider(24, 120, label="Forecast Length (hours)", value=72, step=6)
|
| 2387 |
+
show_uncertainty = gr.Checkbox(label="Show Uncertainty Cone", value=True)
|
| 2388 |
+
|
| 2389 |
+
with gr.Column(scale=1):
|
| 2390 |
+
gr.Markdown("### βοΈ Prediction Controls")
|
| 2391 |
+
predict_btn = gr.Button("π― Generate Advanced Forecast", variant="primary", size="lg")
|
| 2392 |
+
|
| 2393 |
+
gr.Markdown("### π Current Conditions")
|
| 2394 |
+
current_intensity = gr.Number(label="Predicted Intensity (kt)", interactive=False)
|
| 2395 |
+
current_category = gr.Textbox(label="Storm Category", interactive=False)
|
| 2396 |
+
model_confidence = gr.Textbox(label="Model Confidence", interactive=False)
|
| 2397 |
|
| 2398 |
+
with gr.Row():
|
| 2399 |
+
route_plot = gr.Plot(label="πΊοΈ Advanced Route & Intensity Forecast")
|
| 2400 |
|
| 2401 |
with gr.Row():
|
| 2402 |
+
forecast_details = gr.Textbox(label="π Detailed Forecast Summary", lines=20, max_lines=25)
|
| 2403 |
+
|
| 2404 |
+
def run_advanced_prediction(lat, lon, month, oni, hours, uncertainty):
|
| 2405 |
+
try:
|
| 2406 |
+
# Run prediction
|
| 2407 |
+
results = predict_storm_route_and_intensity(lat, lon, month, oni, forecast_hours=hours)
|
| 2408 |
+
|
| 2409 |
+
# Extract current conditions
|
| 2410 |
+
current = results['current_prediction']
|
| 2411 |
+
intensity = current['intensity_kt']
|
| 2412 |
+
category = current['category']
|
| 2413 |
+
confidence = results['confidence_scores'].get('intensity', 0.75)
|
| 2414 |
+
|
| 2415 |
+
# Create visualization
|
| 2416 |
+
fig, forecast_text = create_route_visualization(results, uncertainty)
|
| 2417 |
+
|
| 2418 |
+
return (
|
| 2419 |
+
intensity,
|
| 2420 |
+
category,
|
| 2421 |
+
f"{confidence*100:.0f}% - {results['model_info']}",
|
| 2422 |
+
fig,
|
| 2423 |
+
forecast_text
|
| 2424 |
+
)
|
| 2425 |
+
except Exception as e:
|
| 2426 |
+
return (
|
| 2427 |
+
50, "Error", f"Prediction failed: {str(e)}",
|
| 2428 |
+
None, f"Error generating forecast: {str(e)}"
|
| 2429 |
+
)
|
| 2430 |
|
| 2431 |
predict_btn.click(
|
| 2432 |
+
fn=run_advanced_prediction,
|
| 2433 |
+
inputs=[pred_lat, pred_lon, pred_month, pred_oni, forecast_hours, show_uncertainty],
|
| 2434 |
+
outputs=[current_intensity, current_category, model_confidence, route_plot, forecast_details]
|
| 2435 |
)
|
| 2436 |
|
| 2437 |
prediction_info_text = """
|
| 2438 |
+
### π― Advanced Prediction Features:
|
| 2439 |
+
- **Route Forecasting**: 72-hour track prediction with uncertainty quantification
|
| 2440 |
+
- **Intensity Evolution**: Hour-by-hour intensity changes with environmental factors
|
| 2441 |
+
- **Uncertainty Cones**: Statistical uncertainty visualization
|
| 2442 |
+
- **Real-time Capable**: Predictions in milliseconds
|
| 2443 |
+
- **Multi-Model**: Physics-based with optional deep learning enhancement
|
| 2444 |
+
|
| 2445 |
+
### π Interpretation Guide:
|
| 2446 |
+
- **25-33 kt**: Tropical Depression (TD) - Gray
|
| 2447 |
+
- **34-63 kt**: Tropical Storm (TS) - Blue
|
| 2448 |
+
- **64+ kt**: Typhoon categories (C1-C5) - Cyan to Red
|
| 2449 |
+
- **Track Confidence**: Decreases with forecast time
|
| 2450 |
+
- **Uncertainty Cone**: Shows possible track variations
|
| 2451 |
"""
|
| 2452 |
gr.Markdown(prediction_info_text)
|
| 2453 |
|
| 2454 |
+
with gr.Tab("πΊοΈ Track Visualization"):
|
| 2455 |
with gr.Row():
|
| 2456 |
start_year = gr.Number(label="Start Year", value=2020)
|
| 2457 |
start_month = gr.Dropdown(label="Start Month", choices=list(range(1, 13)), value=1)
|
|
|
|
| 2468 |
outputs=[tracks_plot, typhoon_count]
|
| 2469 |
)
|
| 2470 |
|
| 2471 |
+
with gr.Tab("π¨ Wind Analysis"):
|
| 2472 |
with gr.Row():
|
| 2473 |
wind_start_year = gr.Number(label="Start Year", value=2020)
|
| 2474 |
wind_start_month = gr.Dropdown(label="Start Month", choices=list(range(1, 13)), value=1)
|
|
|
|
| 2485 |
outputs=[wind_scatter, wind_regression_results]
|
| 2486 |
)
|
| 2487 |
|
| 2488 |
+
with gr.Tab("π‘οΈ Pressure Analysis"):
|
| 2489 |
with gr.Row():
|
| 2490 |
pressure_start_year = gr.Number(label="Start Year", value=2020)
|
| 2491 |
pressure_start_month = gr.Dropdown(label="Start Month", choices=list(range(1, 13)), value=1)
|
|
|
|
| 2502 |
outputs=[pressure_scatter, pressure_regression_results]
|
| 2503 |
)
|
| 2504 |
|
| 2505 |
+
with gr.Tab("π Longitude Analysis"):
|
| 2506 |
with gr.Row():
|
| 2507 |
lon_start_year = gr.Number(label="Start Year", value=2020)
|
| 2508 |
lon_start_month = gr.Dropdown(label="Start Month", choices=list(range(1, 13)), value=1)
|
|
|
|
| 2520 |
outputs=[regression_plot, slopes_text, lon_regression_results]
|
| 2521 |
)
|
| 2522 |
|
| 2523 |
+
with gr.Tab("π¬ Enhanced Track Animation"):
|
| 2524 |
+
gr.Markdown("## π₯ High-Quality Storm Track Visualization (All Categories Including TD)")
|
| 2525 |
|
| 2526 |
with gr.Row():
|
| 2527 |
year_dropdown = gr.Dropdown(
|
|
|
|
| 2543 |
value='atlantic'
|
| 2544 |
)
|
| 2545 |
|
| 2546 |
+
generate_video_btn = gr.Button("π¬ Generate Enhanced Animation", variant="primary")
|
| 2547 |
video_output = gr.Video(label="Storm Track Animation")
|
| 2548 |
|
| 2549 |
# Update storm options when year or basin changes
|
|
|
|
| 2562 |
)
|
| 2563 |
|
| 2564 |
animation_info_text = """
|
| 2565 |
+
### π¬ Enhanced Animation Features:
|
| 2566 |
+
- **Full TD Support**: Now displays Tropical Depressions (< 34 kt) in gray
|
| 2567 |
+
- **2025 Compatibility**: Complete support for current year data
|
| 2568 |
+
- **Enhanced Maps**: Better cartographic projections with terrain features
|
| 2569 |
+
- **Smart Scaling**: Storm symbols scale dynamically with intensity
|
| 2570 |
+
- **Real-time Info**: Live position, time, and meteorological data display
|
| 2571 |
+
- **Professional Styling**: Publication-quality animations with proper legends
|
| 2572 |
+
- **Optimized Export**: Fast rendering with web-compatible video formats
|
| 2573 |
"""
|
| 2574 |
gr.Markdown(animation_info_text)
|
| 2575 |
|
| 2576 |
+
with gr.Tab("π Data Statistics & Insights"):
|
| 2577 |
+
gr.Markdown("## π Comprehensive Dataset Analysis")
|
| 2578 |
|
| 2579 |
# Create enhanced data summary
|
| 2580 |
try:
|
|
|
|
| 2680 |
|
| 2681 |
# Create statistics text safely
|
| 2682 |
stats_text = f"""
|
| 2683 |
+
### π Enhanced Dataset Summary:
|
| 2684 |
+
- **Total Unique Storms**: {total_storms:,}
|
| 2685 |
+
- **Total Track Records**: {total_records:,}
|
| 2686 |
+
- **Year Range**: {year_range} ({years_covered} years)
|
| 2687 |
+
- **Basins Available**: {basins_available}
|
| 2688 |
+
- **Average Storms/Year**: {avg_storms_per_year:.1f}
|
| 2689 |
+
|
| 2690 |
+
### πͺοΈ Storm Category Breakdown:
|
| 2691 |
+
- **Tropical Depressions**: {td_storms:,} storms ({td_percentage:.1f}%)
|
| 2692 |
+
- **Tropical Storms**: {ts_storms:,} storms
|
| 2693 |
+
- **Typhoons (C1-C5)**: {typhoon_storms:,} storms
|
| 2694 |
+
|
| 2695 |
+
### π Platform Capabilities:
|
| 2696 |
+
- **Complete TD Analysis** - First platform to include comprehensive TD tracking
|
| 2697 |
+
- **Advanced ML Clustering** - DBSCAN pattern recognition with route visualization
|
| 2698 |
+
- **Real-time Predictions** - Physics-based and optional CNN intensity forecasting
|
| 2699 |
+
- **2025 Data Ready** - Full compatibility with current season data
|
| 2700 |
+
- **Enhanced Animations** - Professional-quality storm track videos
|
| 2701 |
+
- **Multi-basin Analysis** - Comprehensive Pacific and Atlantic coverage
|
| 2702 |
+
|
| 2703 |
+
### π¬ Research Applications:
|
| 2704 |
- Climate change impact studies
|
| 2705 |
- Seasonal forecasting research
|
| 2706 |
- Storm pattern classification
|
|
|
|
| 2773 |
|
| 2774 |
return demo
|
| 2775 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2776 |
# Create and launch the interface
|
| 2777 |
demo = create_interface()
|
| 2778 |
|