euler314 commited on
Commit
3380de9
·
verified ·
1 Parent(s): ed262cf

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +353 -173
app.py CHANGED
@@ -1038,7 +1038,7 @@ def cluster_storms(embedding, method='dbscan', eps=0.5, min_samples=3):
1038
  return np.array([0] * len(embedding))
1039
 
1040
  def create_separate_clustering_plots(storm_features, typhoon_data, method='umap'):
1041
- """Create separate plots for clustering analysis - FIXED VERSION"""
1042
  try:
1043
  # Validate inputs
1044
  if storm_features is None or storm_features.empty:
@@ -1073,34 +1073,102 @@ def create_separate_clustering_plots(storm_features, typhoon_data, method='umap'
1073
  storm_features_viz['NAME'] = 'UNNAMED'
1074
  storm_features_viz['SEASON'] = 2000
1075
 
1076
- # 1. Clustering scatter plot
1077
- fig_cluster = px.scatter(
1078
- storm_features_viz,
1079
- x='dim1',
1080
- y='dim2',
1081
- color='cluster',
1082
- hover_data=['NAME', 'SEASON'],
1083
- title=f'Storm Clustering using {method.upper()}',
1084
- labels={
1085
- 'dim1': f'{method.upper()} Dimension 1',
1086
- 'dim2': f'{method.upper()} Dimension 2',
1087
- 'cluster': 'Cluster'
1088
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1089
  )
1090
 
1091
- # 2. Route map
1092
  fig_routes = go.Figure()
1093
 
1094
- unique_clusters = sorted(storm_features_viz['cluster'].unique())
 
 
1095
  for i, cluster in enumerate(unique_clusters):
1096
- if cluster == -1: # Skip noise for route visualization
1097
- continue
1098
-
1099
  cluster_storm_ids = storm_features_viz[storm_features_viz['cluster'] == cluster]['SID'].tolist()
1100
  color = CLUSTER_COLORS[i % len(CLUSTER_COLORS)]
1101
 
1102
- tracks_added = 0
1103
- for j, sid in enumerate(cluster_storm_ids[:5]): # Limit to 5 storms per cluster for performance
 
 
 
 
 
 
 
 
 
 
 
1104
  try:
1105
  storm_track = typhoon_data[typhoon_data['SID'] == sid].sort_values('ISO_TIME')
1106
  if len(storm_track) > 1:
@@ -1110,33 +1178,73 @@ def create_separate_clustering_plots(storm_features, typhoon_data, method='umap'
1110
 
1111
  if len(storm_track) > 1:
1112
  storm_name = storm_track['NAME'].iloc[0] if pd.notna(storm_track['NAME'].iloc[0]) else 'UNNAMED'
 
 
 
 
 
 
1113
 
1114
  fig_routes.add_trace(
1115
  go.Scattergeo(
1116
  lon=storm_track['LON'],
1117
  lat=storm_track['LAT'],
1118
  mode='lines+markers',
1119
- line=dict(color=color, width=2),
1120
- marker=dict(color=color, size=4),
1121
- name=f'C{cluster}: {storm_name}' if tracks_added == 0 else None,
1122
- showlegend=(tracks_added == 0),
 
1123
  hovertemplate=(
1124
- f'<b>{storm_name}</b><br>'
1125
  'Lat: %{lat:.1f}°<br>'
1126
  'Lon: %{lon:.1f}°<br>'
1127
- f'Cluster: {cluster}<br>'
 
1128
  '<extra></extra>'
1129
  )
1130
  )
1131
  )
1132
- tracks_added += 1
1133
  except Exception as track_error:
1134
  logging.warning(f"Error adding track for storm {sid}: {track_error}")
1135
  continue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1136
 
1137
- # Update route map layout
1138
  fig_routes.update_layout(
1139
- title="Clustered Storm Routes",
1140
  geo=dict(
1141
  projection_type="natural earth",
1142
  showland=True,
@@ -1146,20 +1254,33 @@ def create_separate_clustering_plots(storm_features, typhoon_data, method='umap'
1146
  showcoastlines=True,
1147
  coastlinecolor="Gray",
1148
  center=dict(lat=20, lon=140)
1149
- )
 
 
1150
  )
1151
 
1152
- # 3. Pressure evolution plot
 
 
 
 
 
 
 
 
 
 
 
 
 
1153
  fig_pressure = go.Figure()
1154
 
1155
  for i, cluster in enumerate(unique_clusters):
1156
- if cluster == -1:
1157
- continue
1158
-
1159
  cluster_storm_ids = storm_features_viz[storm_features_viz['cluster'] == cluster]['SID'].tolist()
1160
  color = CLUSTER_COLORS[i % len(CLUSTER_COLORS)]
1161
 
1162
- for j, sid in enumerate(cluster_storm_ids[:3]): # Limit to 3 storms per cluster
 
1163
  try:
1164
  storm_track = typhoon_data[typhoon_data['SID'] == sid].sort_values('ISO_TIME')
1165
  if len(storm_track) > 1 and 'USA_PRES' in storm_track.columns:
@@ -1168,147 +1289,182 @@ def create_separate_clustering_plots(storm_features, typhoon_data, method='umap'
1168
  storm_name = storm_track['NAME'].iloc[0] if pd.notna(storm_track['NAME'].iloc[0]) else 'UNNAMED'
1169
  time_hours = range(len(pressure_values))
1170
 
 
 
 
1171
  fig_pressure.add_trace(
1172
  go.Scatter(
1173
- x=time_hours,
1174
  y=pressure_values,
1175
  mode='lines',
1176
- line=dict(color=color, width=2),
1177
  name=f'C{cluster}: {storm_name}' if j == 0 else None,
1178
  showlegend=(j == 0),
 
1179
  hovertemplate=(
1180
- f'<b>{storm_name}</b><br>'
1181
- 'Hour: %{x}<br>'
1182
  'Pressure: %{y:.0f} hPa<br>'
1183
  '<extra></extra>'
1184
- )
 
1185
  )
1186
  )
 
1187
  except Exception as e:
1188
  continue
 
 
 
 
 
 
 
 
 
 
 
1189
 
1190
  fig_pressure.update_layout(
1191
- title="Pressure Evolution by Cluster",
1192
- xaxis_title="Time (hours)",
1193
- yaxis_title="Pressure (hPa)"
 
1194
  )
1195
 
1196
- # 4. Wind evolution plot
1197
  fig_wind = go.Figure()
1198
 
1199
  for i, cluster in enumerate(unique_clusters):
1200
- if cluster == -1:
1201
- continue
1202
-
1203
  cluster_storm_ids = storm_features_viz[storm_features_viz['cluster'] == cluster]['SID'].tolist()
1204
  color = CLUSTER_COLORS[i % len(CLUSTER_COLORS)]
1205
 
1206
- for j, sid in enumerate(cluster_storm_ids[:3]): # Limit to 3 storms per cluster
 
1207
  try:
1208
  storm_track = typhoon_data[typhoon_data['SID'] == sid].sort_values('ISO_TIME')
1209
  if len(storm_track) > 1 and 'USA_WIND' in storm_track.columns:
1210
  wind_values = pd.to_numeric(storm_track['USA_WIND'], errors='coerce').dropna()
1211
  if len(wind_values) > 0:
1212
  storm_name = storm_track['NAME'].iloc[0] if pd.notna(storm_track['NAME'].iloc[0]) else 'UNNAMED'
1213
- time_hours = range(len(wind_values))
 
 
1214
 
1215
  fig_wind.add_trace(
1216
  go.Scatter(
1217
- x=time_hours,
1218
  y=wind_values,
1219
  mode='lines',
1220
- line=dict(color=color, width=2),
1221
  name=f'C{cluster}: {storm_name}' if j == 0 else None,
1222
  showlegend=(j == 0),
 
1223
  hovertemplate=(
1224
- f'<b>{storm_name}</b><br>'
1225
- 'Hour: %{x}<br>'
1226
  'Wind: %{y:.0f} kt<br>'
1227
  '<extra></extra>'
1228
- )
 
1229
  )
1230
  )
 
1231
  except Exception as e:
1232
  continue
 
 
 
 
 
 
 
 
 
 
 
1233
 
1234
  fig_wind.update_layout(
1235
- title="Wind Speed Evolution by Cluster",
1236
- xaxis_title="Time (hours)",
1237
- yaxis_title="Wind Speed (kt)"
 
1238
  )
1239
 
1240
- # Generate detailed cluster statistics - FIXED
1241
  try:
1242
- # Only use columns that actually exist
1243
- available_cols = {
1244
- 'USA_WIND_max': 'USA_WIND_max',
1245
- 'USA_PRES_min': 'USA_PRES_min',
1246
- 'track_length': 'track_length',
1247
- 'genesis_lat': 'genesis_lat',
1248
- 'genesis_lon': 'genesis_lon',
1249
- 'total_distance': 'total_distance',
1250
- 'avg_curvature': 'avg_curvature',
1251
- 'SID': 'SID'
1252
- }
1253
-
1254
- # Filter to only existing columns
1255
- existing_cols = {k: v for k, v in available_cols.items() if v in storm_features_viz.columns}
1256
 
1257
- if len(existing_cols) > 1: # Need at least SID + one other column
1258
- stats_text = "ADVANCED CLUSTER ANALYSIS RESULTS\n" + "="*50 + "\n\n"
1259
-
1260
- for cluster in sorted(storm_features_viz['cluster'].unique()):
1261
- cluster_data = storm_features_viz[storm_features_viz['cluster'] == cluster]
1262
- storm_count = len(cluster_data)
1263
-
1264
- if cluster == -1:
1265
- stats_text += f"NOISE POINTS: {storm_count} storms\n\n"
1266
- continue
1267
-
1268
- stats_text += f"CLUSTER {cluster}: {storm_count} storms\n"
1269
-
1270
- # Add available statistics
1271
- if 'USA_WIND_max' in cluster_data.columns:
1272
- wind_mean = cluster_data['USA_WIND_max'].mean()
1273
- wind_std = cluster_data['USA_WIND_max'].std()
1274
- stats_text += f" Intensity: {wind_mean:.1f} +/- {wind_std:.1f} kt\n"
1275
-
1276
- if 'USA_PRES_min' in cluster_data.columns:
1277
- pres_mean = cluster_data['USA_PRES_min'].mean()
1278
- pres_std = cluster_data['USA_PRES_min'].std()
1279
- stats_text += f" Pressure: {pres_mean:.1f} +/- {pres_std:.1f} hPa\n"
1280
-
1281
- if 'track_length' in cluster_data.columns:
1282
- track_mean = cluster_data['track_length'].mean()
1283
- track_std = cluster_data['track_length'].std()
1284
- stats_text += f" Track Length: {track_mean:.1f} +/- {track_std:.1f} points\n"
1285
-
1286
- if 'genesis_lat' in cluster_data.columns and 'genesis_lon' in cluster_data.columns:
1287
- lat_mean = cluster_data['genesis_lat'].mean()
1288
- lon_mean = cluster_data['genesis_lon'].mean()
1289
- stats_text += f" Genesis Region: {lat_mean:.1f}°N, {lon_mean:.1f}°E\n"
1290
-
1291
- stats_text += "\n"
1292
-
1293
- # Add feature importance summary
1294
- stats_text += "CLUSTERING FEATURES USED:\n"
1295
- stats_text += f" - Total features: {len(feature_cols)}\n"
1296
- stats_text += f" - Available features: {', '.join(feature_cols[:5])}...\n\n"
1297
 
1298
- stats_text += f"ALGORITHM: {method.upper()} + DBSCAN clustering\n"
1299
- stats_text += f"CLUSTERS FOUND: {len([c for c in storm_features_viz['cluster'].unique() if c != -1])}\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1300
 
1301
- else:
1302
- stats_text = "Limited cluster statistics available due to missing feature columns."
 
 
 
 
 
 
 
 
 
 
 
 
1303
 
1304
  except Exception as stats_error:
1305
- logging.error(f"Error generating cluster statistics: {stats_error}")
1306
- stats_text = f"Error generating cluster statistics: {str(stats_error)}"
1307
 
1308
  return fig_cluster, fig_routes, fig_pressure, fig_wind, stats_text
1309
 
1310
  except Exception as e:
1311
- logging.error(f"Error in clustering analysis: {e}")
1312
  import traceback
1313
  traceback.print_exc()
1314
 
@@ -1562,21 +1718,13 @@ def predict_storm_route_and_intensity(lat, lon, month, oni_value, models=None, f
1562
  }
1563
 
1564
  def create_route_visualization(prediction_results, show_uncertainty=True):
1565
- """Create comprehensive route and intensity visualization - FIXED"""
1566
  try:
1567
  if 'route_forecast' not in prediction_results or not prediction_results['route_forecast']:
1568
  return None, "No route forecast data available"
1569
 
1570
  route_data = prediction_results['route_forecast']
1571
 
1572
- # Create subplot with route map and intensity evolution - FIXED
1573
- fig = make_subplots(
1574
- rows=1, cols=2,
1575
- subplot_titles=('Forecast Track', 'Intensity Evolution'),
1576
- specs=[[{"type": "geo"}, {"type": "xy"}]], # Changed to xy for regular plot
1577
- column_widths=[0.6, 0.4]
1578
- )
1579
-
1580
  # Extract data for plotting
1581
  hours = [point['hour'] for point in route_data]
1582
  lats = [point['lat'] for point in route_data]
@@ -1584,18 +1732,31 @@ def create_route_visualization(prediction_results, show_uncertainty=True):
1584
  intensities = [point['intensity_kt'] for point in route_data]
1585
  categories = [point['category'] for point in route_data]
1586
 
1587
- # Route visualization with intensity colors
 
 
 
 
 
 
 
 
 
1588
  for i in range(len(route_data)):
1589
  point = route_data[i]
1590
  color = enhanced_color_map.get(point['category'], 'rgb(128,128,128)')
1591
 
1592
- # Convert rgb string to rgba for transparency
 
 
1593
  if i == 0: # Current position
1594
  marker_size = 15
1595
  opacity = 1.0
 
1596
  else:
1597
- marker_size = 10
1598
- opacity = 1.0 - (i / len(route_data)) * 0.5 # Fade with time
 
1599
 
1600
  fig.add_trace(
1601
  go.Scattergeo(
@@ -1604,12 +1765,13 @@ def create_route_visualization(prediction_results, show_uncertainty=True):
1604
  mode='markers',
1605
  marker=dict(
1606
  size=marker_size,
1607
- color=color,
1608
  opacity=opacity,
1609
- line=dict(width=2, color='white')
 
1610
  ),
1611
- name=f"Hour {point['hour']}" if i % 4 == 0 else None, # Show every 4th hour in legend
1612
- showlegend=(i % 4 == 0),
1613
  hovertemplate=(
1614
  f"<b>Hour {point['hour']}</b><br>"
1615
  f"Position: {point['lat']:.1f}°N, {point['lon']:.1f}°E<br>"
@@ -1627,7 +1789,7 @@ def create_route_visualization(prediction_results, show_uncertainty=True):
1627
  lon=lons,
1628
  lat=lats,
1629
  mode='lines',
1630
- line=dict(color='black', width=2, dash='dash'),
1631
  name='Forecast Track',
1632
  showlegend=True
1633
  ),
@@ -1642,15 +1804,12 @@ def create_route_visualization(prediction_results, show_uncertainty=True):
1642
  uncertainty_lons_lower = []
1643
 
1644
  for i, point in enumerate(route_data):
1645
- # Uncertainty grows with time
1646
- uncertainty = 0.5 + (i / len(route_data)) * 2.0 # degrees
1647
-
1648
  uncertainty_lats_upper.append(point['lat'] + uncertainty)
1649
  uncertainty_lats_lower.append(point['lat'] - uncertainty)
1650
  uncertainty_lons_upper.append(point['lon'] + uncertainty)
1651
  uncertainty_lons_lower.append(point['lon'] - uncertainty)
1652
 
1653
- # Add uncertainty cone
1654
  uncertainty_lats = uncertainty_lats_upper + uncertainty_lats_lower[::-1]
1655
  uncertainty_lons = uncertainty_lons_upper + uncertainty_lons_lower[::-1]
1656
 
@@ -1660,54 +1819,69 @@ def create_route_visualization(prediction_results, show_uncertainty=True):
1660
  lat=uncertainty_lats,
1661
  mode='lines',
1662
  fill='toself',
1663
- fillcolor='rgba(128,128,128,0.2)',
1664
- line=dict(color='rgba(128,128,128,0.3)', width=1),
1665
  name='Uncertainty Cone',
1666
  showlegend=True
1667
  ),
1668
  row=1, col=1
1669
  )
1670
 
1671
- # Intensity evolution plot - FIXED to use regular scatter plot
1672
  fig.add_trace(
1673
  go.Scatter(
1674
  x=hours,
1675
  y=intensities,
1676
  mode='lines+markers',
1677
  line=dict(color='red', width=3),
1678
- marker=dict(size=8, color='red'),
1679
- name='Intensity Forecast',
1680
- hovertemplate=(
1681
- "Hour: %{x}<br>"
1682
- "Intensity: %{y:.0f} kt<br>"
1683
- "<extra></extra>"
1684
- )
1685
  ),
1686
- row=1, col=2
1687
  )
1688
 
1689
- # Add category thresholds
1690
  thresholds = [34, 64, 83, 96, 113, 137]
1691
  threshold_names = ['TS', 'C1', 'C2', 'C3', 'C4', 'C5']
1692
 
1693
  for thresh, name in zip(thresholds, threshold_names):
1694
- fig.add_hline(
1695
- y=thresh,
1696
- line_dash="dash",
1697
- line_color="gray",
1698
- annotation_text=name,
1699
- annotation_position="left",
1700
- row=1, col=2
 
 
 
 
1701
  )
1702
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1703
  # Update layout
1704
  fig.update_layout(
1705
- title_text="Advanced Storm Forecast: Track and Intensity Evolution",
1706
  showlegend=True,
1707
- height=600
1708
  )
1709
 
1710
- # Update geo layout - FIXED: Only update geo subplot
1711
  fig.update_geos(
1712
  projection_type="natural earth",
1713
  showland=True,
@@ -1716,14 +1890,16 @@ def create_route_visualization(prediction_results, show_uncertainty=True):
1716
  oceancolor="LightBlue",
1717
  showcoastlines=True,
1718
  coastlinecolor="Gray",
1719
- center=dict(lat=lats[0], lon=lons[0]),
1720
  resolution=50,
1721
  row=1, col=1
1722
  )
1723
 
1724
- # Update intensity plot - FIXED: Use correct method for regular subplot
1725
- fig.update_xaxes(title_text="Forecast Hour", row=1, col=2)
1726
- fig.update_yaxes(title_text="Intensity (kt)", row=1, col=2)
 
 
1727
 
1728
  # Generate detailed forecast text
1729
  current = prediction_results['current_prediction']
@@ -1738,14 +1914,14 @@ CURRENT CONDITIONS:
1738
 
1739
  FORECAST TRACK (72-HOUR):
1740
  • Initial Position: {lats[0]:.1f}°N, {lons[0]:.1f}°E
1741
- • 24-hour Position: {lats[4]:.1f}°N, {lons[4]:.1f}°E
1742
- • 48-hour Position: {lats[8]:.1f}°N, {lons[8]:.1f}°E
1743
  • 72-hour Position: {lats[-1]:.1f}°N, {lons[-1]:.1f}°E
1744
 
1745
  INTENSITY EVOLUTION:
1746
  • Current: {intensities[0]:.0f} kt ({categories[0]})
1747
- • 24-hour: {intensities[4]:.0f} kt ({categories[4]})
1748
- • 48-hour: {intensities[8]:.0f} kt ({categories[8]})
1749
  • 72-hour: {intensities[-1]:.0f} kt ({categories[-1]})
1750
 
1751
  CONFIDENCE LEVELS:
@@ -1760,7 +1936,11 @@ MODEL: {prediction_results['model_info']}
1760
  return fig, forecast_text.strip()
1761
 
1762
  except Exception as e:
1763
- return None, f"Error creating route visualization: {str(e)}"
 
 
 
 
1764
 
1765
  # -----------------------------
1766
  # Regression Functions (Original)
 
1038
  return np.array([0] * len(embedding))
1039
 
1040
  def create_separate_clustering_plots(storm_features, typhoon_data, method='umap'):
1041
+ """Create separate plots for clustering analysis - ENHANCED CLARITY VERSION"""
1042
  try:
1043
  # Validate inputs
1044
  if storm_features is None or storm_features.empty:
 
1073
  storm_features_viz['NAME'] = 'UNNAMED'
1074
  storm_features_viz['SEASON'] = 2000
1075
 
1076
+ # Get unique clusters and assign distinct colors
1077
+ unique_clusters = sorted([c for c in storm_features_viz['cluster'].unique() if c != -1])
1078
+ noise_count = len(storm_features_viz[storm_features_viz['cluster'] == -1])
1079
+
1080
+ # 1. Enhanced clustering scatter plot with clear cluster identification
1081
+ fig_cluster = go.Figure()
1082
+
1083
+ # Add noise points first
1084
+ if noise_count > 0:
1085
+ noise_data = storm_features_viz[storm_features_viz['cluster'] == -1]
1086
+ fig_cluster.add_trace(
1087
+ go.Scatter(
1088
+ x=noise_data['dim1'],
1089
+ y=noise_data['dim2'],
1090
+ mode='markers',
1091
+ marker=dict(color='lightgray', size=8, opacity=0.5, symbol='x'),
1092
+ name=f'Noise ({noise_count} storms)',
1093
+ hovertemplate=(
1094
+ '<b>%{customdata[0]}</b><br>'
1095
+ 'Season: %{customdata[1]}<br>'
1096
+ 'Cluster: Noise<br>'
1097
+ f'{method.upper()} Dim 1: %{{x:.2f}}<br>'
1098
+ f'{method.upper()} Dim 2: %{{y:.2f}}<br>'
1099
+ '<extra></extra>'
1100
+ ),
1101
+ customdata=np.column_stack((
1102
+ noise_data['NAME'].fillna('UNNAMED'),
1103
+ noise_data['SEASON'].fillna(2000)
1104
+ ))
1105
+ )
1106
+ )
1107
+
1108
+ # Add clusters with distinct colors and shapes
1109
+ cluster_symbols = ['circle', 'square', 'diamond', 'triangle-up', 'triangle-down',
1110
+ 'pentagon', 'hexagon', 'star', 'cross', 'circle-open']
1111
+
1112
+ for i, cluster in enumerate(unique_clusters):
1113
+ cluster_data = storm_features_viz[storm_features_viz['cluster'] == cluster]
1114
+ color = CLUSTER_COLORS[i % len(CLUSTER_COLORS)]
1115
+ symbol = cluster_symbols[i % len(cluster_symbols)]
1116
+
1117
+ fig_cluster.add_trace(
1118
+ go.Scatter(
1119
+ x=cluster_data['dim1'],
1120
+ y=cluster_data['dim2'],
1121
+ mode='markers',
1122
+ marker=dict(color=color, size=10, symbol=symbol, line=dict(width=1, color='white')),
1123
+ name=f'Cluster {cluster} ({len(cluster_data)} storms)',
1124
+ hovertemplate=(
1125
+ '<b>%{customdata[0]}</b><br>'
1126
+ 'Season: %{customdata[1]}<br>'
1127
+ f'Cluster: {cluster}<br>'
1128
+ f'{method.upper()} Dim 1: %{{x:.2f}}<br>'
1129
+ f'{method.upper()} Dim 2: %{{y:.2f}}<br>'
1130
+ 'Intensity: %{customdata[2]:.0f} kt<br>'
1131
+ '<extra></extra>'
1132
+ ),
1133
+ customdata=np.column_stack((
1134
+ cluster_data['NAME'].fillna('UNNAMED'),
1135
+ cluster_data['SEASON'].fillna(2000),
1136
+ cluster_data['USA_WIND_max'].fillna(0)
1137
+ ))
1138
+ )
1139
+ )
1140
+
1141
+ fig_cluster.update_layout(
1142
+ title=f'Storm Clustering Analysis using {method.upper()}<br><sub>Each symbol/color represents a distinct storm pattern group</sub>',
1143
+ xaxis_title=f'{method.upper()} Dimension 1',
1144
+ yaxis_title=f'{method.upper()} Dimension 2',
1145
+ height=600,
1146
+ showlegend=True
1147
  )
1148
 
1149
+ # 2. ENHANCED route map with cluster legends and clearer representation
1150
  fig_routes = go.Figure()
1151
 
1152
+ # Create a comprehensive legend showing cluster characteristics
1153
+ cluster_info_text = []
1154
+
1155
  for i, cluster in enumerate(unique_clusters):
 
 
 
1156
  cluster_storm_ids = storm_features_viz[storm_features_viz['cluster'] == cluster]['SID'].tolist()
1157
  color = CLUSTER_COLORS[i % len(CLUSTER_COLORS)]
1158
 
1159
+ # Get cluster statistics for legend
1160
+ cluster_data = storm_features_viz[storm_features_viz['cluster'] == cluster]
1161
+ avg_intensity = cluster_data['USA_WIND_max'].mean() if 'USA_WIND_max' in cluster_data.columns else 0
1162
+ avg_pressure = cluster_data['USA_PRES_min'].mean() if 'USA_PRES_min' in cluster_data.columns else 1000
1163
+
1164
+ cluster_info_text.append(
1165
+ f"Cluster {cluster}: {len(cluster_storm_ids)} storms, "
1166
+ f"Avg: {avg_intensity:.0f}kt/{avg_pressure:.0f}hPa"
1167
+ )
1168
+
1169
+ # Add multiple storms per cluster with clear identification
1170
+ storms_added = 0
1171
+ for j, sid in enumerate(cluster_storm_ids[:8]): # Show up to 8 storms per cluster
1172
  try:
1173
  storm_track = typhoon_data[typhoon_data['SID'] == sid].sort_values('ISO_TIME')
1174
  if len(storm_track) > 1:
 
1178
 
1179
  if len(storm_track) > 1:
1180
  storm_name = storm_track['NAME'].iloc[0] if pd.notna(storm_track['NAME'].iloc[0]) else 'UNNAMED'
1181
+ storm_season = storm_track['SEASON'].iloc[0] if 'SEASON' in storm_track.columns else 'Unknown'
1182
+
1183
+ # Vary line style for different storms in same cluster
1184
+ line_styles = ['solid', 'dash', 'dot', 'dashdot']
1185
+ line_style = line_styles[j % len(line_styles)]
1186
+ line_width = 3 if j == 0 else 2 # First storm thicker
1187
 
1188
  fig_routes.add_trace(
1189
  go.Scattergeo(
1190
  lon=storm_track['LON'],
1191
  lat=storm_track['LAT'],
1192
  mode='lines+markers',
1193
+ line=dict(color=color, width=line_width, dash=line_style),
1194
+ marker=dict(color=color, size=3),
1195
+ name=f'C{cluster}: {storm_name} ({storm_season})',
1196
+ showlegend=True,
1197
+ legendgroup=f'cluster_{cluster}',
1198
  hovertemplate=(
1199
+ f'<b>Cluster {cluster}: {storm_name}</b><br>'
1200
  'Lat: %{lat:.1f}°<br>'
1201
  'Lon: %{lon:.1f}°<br>'
1202
+ f'Season: {storm_season}<br>'
1203
+ f'Pattern Group: {cluster}<br>'
1204
  '<extra></extra>'
1205
  )
1206
  )
1207
  )
1208
+ storms_added += 1
1209
  except Exception as track_error:
1210
  logging.warning(f"Error adding track for storm {sid}: {track_error}")
1211
  continue
1212
+
1213
+ # Add cluster centroid marker
1214
+ if len(cluster_storm_ids) > 0:
1215
+ # Calculate average genesis location for cluster
1216
+ cluster_storms = storm_features_viz[storm_features_viz['cluster'] == cluster]
1217
+ if 'genesis_lat' in cluster_storms.columns and 'genesis_lon' in cluster_storms.columns:
1218
+ avg_lat = cluster_storms['genesis_lat'].mean()
1219
+ avg_lon = cluster_storms['genesis_lon'].mean()
1220
+
1221
+ fig_routes.add_trace(
1222
+ go.Scattergeo(
1223
+ lon=[avg_lon],
1224
+ lat=[avg_lat],
1225
+ mode='markers',
1226
+ marker=dict(
1227
+ color=color,
1228
+ size=20,
1229
+ symbol='star',
1230
+ line=dict(width=2, color='white')
1231
+ ),
1232
+ name=f'C{cluster} Center',
1233
+ showlegend=True,
1234
+ legendgroup=f'cluster_{cluster}',
1235
+ hovertemplate=(
1236
+ f'<b>Cluster {cluster} Genesis Center</b><br>'
1237
+ f'Avg Position: {avg_lat:.1f}°N, {avg_lon:.1f}°E<br>'
1238
+ f'Storms: {len(cluster_storm_ids)}<br>'
1239
+ f'Avg Intensity: {avg_intensity:.0f} kt<br>'
1240
+ '<extra></extra>'
1241
+ )
1242
+ )
1243
+ )
1244
 
1245
+ # Update route map layout with enhanced information
1246
  fig_routes.update_layout(
1247
+ title=f"Storm Routes by {method.upper()} Clusters<br><sub>Different line styles = different storms in same cluster | Stars = cluster centers</sub>",
1248
  geo=dict(
1249
  projection_type="natural earth",
1250
  showland=True,
 
1254
  showcoastlines=True,
1255
  coastlinecolor="Gray",
1256
  center=dict(lat=20, lon=140)
1257
+ ),
1258
+ height=600,
1259
+ showlegend=True
1260
  )
1261
 
1262
+ # Add cluster info annotation
1263
+ cluster_summary = "<br>".join(cluster_info_text)
1264
+ fig_routes.add_annotation(
1265
+ text=f"<b>Cluster Summary:</b><br>{cluster_summary}",
1266
+ xref="paper", yref="paper",
1267
+ x=0.02, y=0.98,
1268
+ showarrow=False,
1269
+ align="left",
1270
+ bgcolor="rgba(255,255,255,0.8)",
1271
+ bordercolor="gray",
1272
+ borderwidth=1
1273
+ )
1274
+
1275
+ # 3. Enhanced pressure evolution plot with cluster identification
1276
  fig_pressure = go.Figure()
1277
 
1278
  for i, cluster in enumerate(unique_clusters):
 
 
 
1279
  cluster_storm_ids = storm_features_viz[storm_features_viz['cluster'] == cluster]['SID'].tolist()
1280
  color = CLUSTER_COLORS[i % len(CLUSTER_COLORS)]
1281
 
1282
+ cluster_pressures = []
1283
+ for j, sid in enumerate(cluster_storm_ids[:5]): # Limit to 5 storms per cluster
1284
  try:
1285
  storm_track = typhoon_data[typhoon_data['SID'] == sid].sort_values('ISO_TIME')
1286
  if len(storm_track) > 1 and 'USA_PRES' in storm_track.columns:
 
1289
  storm_name = storm_track['NAME'].iloc[0] if pd.notna(storm_track['NAME'].iloc[0]) else 'UNNAMED'
1290
  time_hours = range(len(pressure_values))
1291
 
1292
+ # Normalize time to show relative progression
1293
+ normalized_time = np.linspace(0, 100, len(pressure_values))
1294
+
1295
  fig_pressure.add_trace(
1296
  go.Scatter(
1297
+ x=normalized_time,
1298
  y=pressure_values,
1299
  mode='lines',
1300
+ line=dict(color=color, width=2, dash='solid' if j == 0 else 'dash'),
1301
  name=f'C{cluster}: {storm_name}' if j == 0 else None,
1302
  showlegend=(j == 0),
1303
+ legendgroup=f'pressure_cluster_{cluster}',
1304
  hovertemplate=(
1305
+ f'<b>Cluster {cluster}: {storm_name}</b><br>'
1306
+ 'Progress: %{x:.0f}%<br>'
1307
  'Pressure: %{y:.0f} hPa<br>'
1308
  '<extra></extra>'
1309
+ ),
1310
+ opacity=0.8 if j == 0 else 0.5
1311
  )
1312
  )
1313
+ cluster_pressures.extend(pressure_values)
1314
  except Exception as e:
1315
  continue
1316
+
1317
+ # Add cluster average line
1318
+ if cluster_pressures:
1319
+ avg_pressure = np.mean(cluster_pressures)
1320
+ fig_pressure.add_hline(
1321
+ y=avg_pressure,
1322
+ line_dash="dot",
1323
+ line_color=color,
1324
+ annotation_text=f"C{cluster} Avg: {avg_pressure:.0f}",
1325
+ annotation_position="right"
1326
+ )
1327
 
1328
  fig_pressure.update_layout(
1329
+ title=f"Pressure Evolution by {method.upper()} Clusters<br><sub>Normalized timeline (0-100%) | Dotted lines = cluster averages</sub>",
1330
+ xaxis_title="Storm Progress (%)",
1331
+ yaxis_title="Pressure (hPa)",
1332
+ height=500
1333
  )
1334
 
1335
+ # 4. Enhanced wind evolution plot
1336
  fig_wind = go.Figure()
1337
 
1338
  for i, cluster in enumerate(unique_clusters):
 
 
 
1339
  cluster_storm_ids = storm_features_viz[storm_features_viz['cluster'] == cluster]['SID'].tolist()
1340
  color = CLUSTER_COLORS[i % len(CLUSTER_COLORS)]
1341
 
1342
+ cluster_winds = []
1343
+ for j, sid in enumerate(cluster_storm_ids[:5]): # Limit to 5 storms per cluster
1344
  try:
1345
  storm_track = typhoon_data[typhoon_data['SID'] == sid].sort_values('ISO_TIME')
1346
  if len(storm_track) > 1 and 'USA_WIND' in storm_track.columns:
1347
  wind_values = pd.to_numeric(storm_track['USA_WIND'], errors='coerce').dropna()
1348
  if len(wind_values) > 0:
1349
  storm_name = storm_track['NAME'].iloc[0] if pd.notna(storm_track['NAME'].iloc[0]) else 'UNNAMED'
1350
+
1351
+ # Normalize time to show relative progression
1352
+ normalized_time = np.linspace(0, 100, len(wind_values))
1353
 
1354
  fig_wind.add_trace(
1355
  go.Scatter(
1356
+ x=normalized_time,
1357
  y=wind_values,
1358
  mode='lines',
1359
+ line=dict(color=color, width=2, dash='solid' if j == 0 else 'dash'),
1360
  name=f'C{cluster}: {storm_name}' if j == 0 else None,
1361
  showlegend=(j == 0),
1362
+ legendgroup=f'wind_cluster_{cluster}',
1363
  hovertemplate=(
1364
+ f'<b>Cluster {cluster}: {storm_name}</b><br>'
1365
+ 'Progress: %{x:.0f}%<br>'
1366
  'Wind: %{y:.0f} kt<br>'
1367
  '<extra></extra>'
1368
+ ),
1369
+ opacity=0.8 if j == 0 else 0.5
1370
  )
1371
  )
1372
+ cluster_winds.extend(wind_values)
1373
  except Exception as e:
1374
  continue
1375
+
1376
+ # Add cluster average line
1377
+ if cluster_winds:
1378
+ avg_wind = np.mean(cluster_winds)
1379
+ fig_wind.add_hline(
1380
+ y=avg_wind,
1381
+ line_dash="dot",
1382
+ line_color=color,
1383
+ annotation_text=f"C{cluster} Avg: {avg_wind:.0f}",
1384
+ annotation_position="right"
1385
+ )
1386
 
1387
  fig_wind.update_layout(
1388
+ title=f"Wind Speed Evolution by {method.upper()} Clusters<br><sub>Normalized timeline (0-100%) | Dotted lines = cluster averages</sub>",
1389
+ xaxis_title="Storm Progress (%)",
1390
+ yaxis_title="Wind Speed (kt)",
1391
+ height=500
1392
  )
1393
 
1394
+ # Generate enhanced cluster statistics with clear explanations
1395
  try:
1396
+ stats_text = f"ENHANCED {method.upper()} CLUSTER ANALYSIS RESULTS\n" + "="*60 + "\n\n"
1397
+ stats_text += f"🔍 DIMENSIONALITY REDUCTION: {method.upper()}\n"
1398
+ stats_text += f"🎯 CLUSTERING ALGORITHM: DBSCAN (automatic pattern discovery)\n"
1399
+ stats_text += f"📊 TOTAL STORMS ANALYZED: {len(storm_features_viz)}\n"
1400
+ stats_text += f"🎨 CLUSTERS DISCOVERED: {len(unique_clusters)}\n"
1401
+ if noise_count > 0:
1402
+ stats_text += f"❌ NOISE POINTS: {noise_count} storms (don't fit clear patterns)\n"
1403
+ stats_text += "\n"
 
 
 
 
 
 
1404
 
1405
+ for cluster in sorted(storm_features_viz['cluster'].unique()):
1406
+ cluster_data = storm_features_viz[storm_features_viz['cluster'] == cluster]
1407
+ storm_count = len(cluster_data)
1408
+
1409
+ if cluster == -1:
1410
+ stats_text += f"❌ NOISE GROUP: {storm_count} storms\n"
1411
+ stats_text += " → These storms don't follow the main patterns\n"
1412
+ stats_text += " → May represent unique or rare storm behaviors\n\n"
1413
+ continue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1414
 
1415
+ stats_text += f"🎯 CLUSTER {cluster}: {storm_count} storms\n"
1416
+ stats_text += f" 🎨 Color: {CLUSTER_COLORS[cluster % len(CLUSTER_COLORS)]}\n"
1417
+
1418
+ # Add detailed statistics if available
1419
+ if 'USA_WIND_max' in cluster_data.columns:
1420
+ wind_mean = cluster_data['USA_WIND_max'].mean()
1421
+ wind_std = cluster_data['USA_WIND_max'].std()
1422
+ stats_text += f" 💨 Intensity: {wind_mean:.1f} ± {wind_std:.1f} kt\n"
1423
+
1424
+ if 'USA_PRES_min' in cluster_data.columns:
1425
+ pres_mean = cluster_data['USA_PRES_min'].mean()
1426
+ pres_std = cluster_data['USA_PRES_min'].std()
1427
+ stats_text += f" 🌡️ Pressure: {pres_mean:.1f} ± {pres_std:.1f} hPa\n"
1428
+
1429
+ if 'track_length' in cluster_data.columns:
1430
+ track_mean = cluster_data['track_length'].mean()
1431
+ stats_text += f" 📏 Avg Track Length: {track_mean:.1f} points\n"
1432
+
1433
+ if 'genesis_lat' in cluster_data.columns and 'genesis_lon' in cluster_data.columns:
1434
+ lat_mean = cluster_data['genesis_lat'].mean()
1435
+ lon_mean = cluster_data['genesis_lon'].mean()
1436
+ stats_text += f" 🎯 Genesis Region: {lat_mean:.1f}°N, {lon_mean:.1f}°E\n"
1437
+
1438
+ # Add interpretation
1439
+ if wind_mean < 50:
1440
+ stats_text += " 💡 Pattern: Weaker storm group\n"
1441
+ elif wind_mean > 100:
1442
+ stats_text += " 💡 Pattern: Intense storm group\n"
1443
+ else:
1444
+ stats_text += " 💡 Pattern: Moderate intensity group\n"
1445
 
1446
+ stats_text += "\n"
1447
+
1448
+ # Add explanation of the analysis
1449
+ stats_text += "📖 INTERPRETATION GUIDE:\n"
1450
+ stats_text += f"• {method.upper()} reduces storm characteristics to 2D for visualization\n"
1451
+ stats_text += "• DBSCAN finds natural groupings without preset number of clusters\n"
1452
+ stats_text += "• Each cluster represents storms with similar behavior patterns\n"
1453
+ stats_text += "• Route colors match cluster colors from the similarity plot\n"
1454
+ stats_text += "• Stars on map show average genesis locations for each cluster\n"
1455
+ stats_text += "• Temporal plots show how each cluster behaves over time\n\n"
1456
+
1457
+ stats_text += f"🔧 FEATURES USED FOR CLUSTERING:\n"
1458
+ stats_text += f" Total: {len(feature_cols)} storm characteristics\n"
1459
+ stats_text += f" Including: intensity, pressure, track shape, genesis location\n"
1460
 
1461
  except Exception as stats_error:
1462
+ stats_text = f"Error generating enhanced statistics: {str(stats_error)}"
 
1463
 
1464
  return fig_cluster, fig_routes, fig_pressure, fig_wind, stats_text
1465
 
1466
  except Exception as e:
1467
+ logging.error(f"Error in enhanced clustering analysis: {e}")
1468
  import traceback
1469
  traceback.print_exc()
1470
 
 
1718
  }
1719
 
1720
  def create_route_visualization(prediction_results, show_uncertainty=True):
1721
+ """Create comprehensive route and intensity visualization - COMPLETELY FIXED"""
1722
  try:
1723
  if 'route_forecast' not in prediction_results or not prediction_results['route_forecast']:
1724
  return None, "No route forecast data available"
1725
 
1726
  route_data = prediction_results['route_forecast']
1727
 
 
 
 
 
 
 
 
 
1728
  # Extract data for plotting
1729
  hours = [point['hour'] for point in route_data]
1730
  lats = [point['lat'] for point in route_data]
 
1732
  intensities = [point['intensity_kt'] for point in route_data]
1733
  categories = [point['category'] for point in route_data]
1734
 
1735
+ # Create separate figures to avoid geo/regular plot conflicts
1736
+ fig = make_subplots(
1737
+ rows=2, cols=2,
1738
+ subplot_titles=('Forecast Track', 'Intensity Evolution', 'Position Uncertainty', 'Category Timeline'),
1739
+ specs=[[{"type": "geo", "colspan": 2}, None],
1740
+ [{"type": "xy"}, {"type": "xy"}]],
1741
+ vertical_spacing=0.1
1742
+ )
1743
+
1744
+ # 1. Route visualization on geographic plot
1745
  for i in range(len(route_data)):
1746
  point = route_data[i]
1747
  color = enhanced_color_map.get(point['category'], 'rgb(128,128,128)')
1748
 
1749
+ # Convert rgb to regular color format
1750
+ color_hex = rgb_string_to_hex(color)
1751
+
1752
  if i == 0: # Current position
1753
  marker_size = 15
1754
  opacity = 1.0
1755
+ symbol = 'star'
1756
  else:
1757
+ marker_size = 8 + (point['intensity_kt'] / 20) # Size based on intensity
1758
+ opacity = max(0.3, 1.0 - (i / len(route_data)) * 0.7)
1759
+ symbol = 'circle'
1760
 
1761
  fig.add_trace(
1762
  go.Scattergeo(
 
1765
  mode='markers',
1766
  marker=dict(
1767
  size=marker_size,
1768
+ color=color_hex,
1769
  opacity=opacity,
1770
+ symbol=symbol,
1771
+ line=dict(width=1, color='white')
1772
  ),
1773
+ name=f"Hour {point['hour']}" if i % 6 == 0 else None,
1774
+ showlegend=(i % 6 == 0),
1775
  hovertemplate=(
1776
  f"<b>Hour {point['hour']}</b><br>"
1777
  f"Position: {point['lat']:.1f}°N, {point['lon']:.1f}°E<br>"
 
1789
  lon=lons,
1790
  lat=lats,
1791
  mode='lines',
1792
+ line=dict(color='black', width=3, dash='solid'),
1793
  name='Forecast Track',
1794
  showlegend=True
1795
  ),
 
1804
  uncertainty_lons_lower = []
1805
 
1806
  for i, point in enumerate(route_data):
1807
+ uncertainty = 0.3 + (i / len(route_data)) * 1.5
 
 
1808
  uncertainty_lats_upper.append(point['lat'] + uncertainty)
1809
  uncertainty_lats_lower.append(point['lat'] - uncertainty)
1810
  uncertainty_lons_upper.append(point['lon'] + uncertainty)
1811
  uncertainty_lons_lower.append(point['lon'] - uncertainty)
1812
 
 
1813
  uncertainty_lats = uncertainty_lats_upper + uncertainty_lats_lower[::-1]
1814
  uncertainty_lons = uncertainty_lons_upper + uncertainty_lons_lower[::-1]
1815
 
 
1819
  lat=uncertainty_lats,
1820
  mode='lines',
1821
  fill='toself',
1822
+ fillcolor='rgba(128,128,128,0.15)',
1823
+ line=dict(color='rgba(128,128,128,0.4)', width=1),
1824
  name='Uncertainty Cone',
1825
  showlegend=True
1826
  ),
1827
  row=1, col=1
1828
  )
1829
 
1830
+ # 2. Intensity evolution plot (regular subplot - no geo conflicts)
1831
  fig.add_trace(
1832
  go.Scatter(
1833
  x=hours,
1834
  y=intensities,
1835
  mode='lines+markers',
1836
  line=dict(color='red', width=3),
1837
+ marker=dict(size=6, color='red'),
1838
+ name='Intensity',
1839
+ showlegend=False
 
 
 
 
1840
  ),
1841
+ row=2, col=1
1842
  )
1843
 
1844
+ # Add category threshold lines (NOT using add_hline to avoid geo conflicts)
1845
  thresholds = [34, 64, 83, 96, 113, 137]
1846
  threshold_names = ['TS', 'C1', 'C2', 'C3', 'C4', 'C5']
1847
 
1848
  for thresh, name in zip(thresholds, threshold_names):
1849
+ fig.add_trace(
1850
+ go.Scatter(
1851
+ x=[min(hours), max(hours)],
1852
+ y=[thresh, thresh],
1853
+ mode='lines',
1854
+ line=dict(color='gray', width=1, dash='dash'),
1855
+ name=name,
1856
+ showlegend=False,
1857
+ hovertemplate=f"{name} Threshold: {thresh} kt<extra></extra>"
1858
+ ),
1859
+ row=2, col=1
1860
  )
1861
 
1862
+ # 3. Position uncertainty plot
1863
+ uncertainties = [0.3 + (i / len(route_data)) * 1.5 for i in range(len(route_data))]
1864
+ fig.add_trace(
1865
+ go.Scatter(
1866
+ x=hours,
1867
+ y=uncertainties,
1868
+ mode='lines+markers',
1869
+ line=dict(color='orange', width=2),
1870
+ marker=dict(size=4, color='orange'),
1871
+ name='Position Error',
1872
+ showlegend=False
1873
+ ),
1874
+ row=2, col=2
1875
+ )
1876
+
1877
  # Update layout
1878
  fig.update_layout(
1879
+ title_text="Comprehensive Storm Forecast Analysis",
1880
  showlegend=True,
1881
+ height=800
1882
  )
1883
 
1884
+ # Update geo layout (only for geo subplot)
1885
  fig.update_geos(
1886
  projection_type="natural earth",
1887
  showland=True,
 
1890
  oceancolor="LightBlue",
1891
  showcoastlines=True,
1892
  coastlinecolor="Gray",
1893
+ center=dict(lat=np.mean(lats), lon=np.mean(lons)),
1894
  resolution=50,
1895
  row=1, col=1
1896
  )
1897
 
1898
+ # Update regular subplot axes (NOT geo)
1899
+ fig.update_xaxes(title_text="Forecast Hour", row=2, col=1)
1900
+ fig.update_yaxes(title_text="Intensity (kt)", row=2, col=1)
1901
+ fig.update_xaxes(title_text="Forecast Hour", row=2, col=2)
1902
+ fig.update_yaxes(title_text="Position Error (°)", row=2, col=2)
1903
 
1904
  # Generate detailed forecast text
1905
  current = prediction_results['current_prediction']
 
1914
 
1915
  FORECAST TRACK (72-HOUR):
1916
  • Initial Position: {lats[0]:.1f}°N, {lons[0]:.1f}°E
1917
+ • 24-hour Position: {lats[min(4, len(lats)-1)]:.1f}°N, {lons[min(4, len(lons)-1)]:.1f}°E
1918
+ • 48-hour Position: {lats[min(8, len(lats)-1)]:.1f}°N, {lons[min(8, len(lons)-1)]:.1f}°E
1919
  • 72-hour Position: {lats[-1]:.1f}°N, {lons[-1]:.1f}°E
1920
 
1921
  INTENSITY EVOLUTION:
1922
  • Current: {intensities[0]:.0f} kt ({categories[0]})
1923
+ • 24-hour: {intensities[min(4, len(intensities)-1)]:.0f} kt ({categories[min(4, len(categories)-1)]})
1924
+ • 48-hour: {intensities[min(8, len(intensities)-1)]:.0f} kt ({categories[min(8, len(categories)-1)]})
1925
  • 72-hour: {intensities[-1]:.0f} kt ({categories[-1]})
1926
 
1927
  CONFIDENCE LEVELS:
 
1936
  return fig, forecast_text.strip()
1937
 
1938
  except Exception as e:
1939
+ error_msg = f"Error creating route visualization: {str(e)}"
1940
+ print(error_msg)
1941
+ import traceback
1942
+ traceback.print_exc()
1943
+ return None, error_msg
1944
 
1945
  # -----------------------------
1946
  # Regression Functions (Original)