euler314 commited on
Commit
7fa25f7
·
verified ·
1 Parent(s): 37cb4a7

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +431 -1351
app.py CHANGED
@@ -1,6 +1,7 @@
1
- import dash
2
  import plotly.graph_objects as go
3
  import plotly.express as px
 
4
  import pickle
5
  import tropycal.tracks as tracks
6
  import pandas as pd
@@ -9,23 +10,15 @@ import cachetools
9
  import functools
10
  import hashlib
11
  import os
12
- import argparse
13
- from dash import dcc, html
14
- from dash.dependencies import Input, Output, State
15
- from dash.exceptions import PreventUpdate
16
- from plotly.subplots import make_subplots
17
  from datetime import datetime, timedelta
18
- from datetime import date, datetime
19
  from scipy import stats
20
  from scipy.optimize import minimize, curve_fit
21
  from sklearn.linear_model import LinearRegression
22
  from sklearn.cluster import KMeans
23
  from scipy.interpolate import interp1d
24
  from fractions import Fraction
25
- from concurrent.futures import ThreadPoolExecutor
26
- from sklearn.metrics import mean_squared_error
27
  import statsmodels.api as sm
28
- import schedule
29
  import time
30
  import threading
31
  import requests
@@ -35,1410 +28,497 @@ import csv
35
  from collections import defaultdict
36
  import shutil
37
  import filecmp
 
 
38
 
39
- # Add command-line argument parsing
40
- parser = argparse.ArgumentParser(description='Typhoon Analysis Dashboard')
41
- parser.add_argument('--data_path', type=str, default=os.getcwd(), help='Path to the data directory')
42
- args = parser.parse_args()
43
-
44
- # Use the command-line argument for data path
45
- DATA_PATH = args.data_path
46
-
47
  ONI_DATA_PATH = os.path.join(DATA_PATH, 'oni_data.csv')
48
  TYPHOON_DATA_PATH = os.path.join(DATA_PATH, 'processed_typhoon_data.csv')
49
- LOCAL_iBtrace_PATH = os.path.join(DATA_PATH, 'ibtracs.WP.list.v04r01.csv')
50
- iBtrace_uri = 'https://www.ncei.noaa.gov/data/international-best-track-archive-for-climate-stewardship-ibtracs/v04r01/access/csv/ibtracs.WP.list.v04r01.csv'
51
-
52
  CACHE_FILE = 'ibtracs_cache.pkl'
53
  CACHE_EXPIRY_DAYS = 1
54
- last_oni_update = None
55
-
56
 
57
- def should_update_oni():
58
- today = datetime.now()
59
- # Beginning of the month: 1st day
60
- if today.day == 1:
61
- return True
62
- # Middle of the month: 15th day
63
- if today.day == 15:
64
- return True
65
- # End of the month: last day
66
- if today.day == (today.replace(day=1, month=today.month%12+1) - timedelta(days=1)).day:
67
- return True
68
- return False
69
-
70
- color_map = {
71
- 'C5 Super Typhoon': 'rgb(255, 0, 0)', # Red
72
- 'C4 Very Strong Typhoon': 'rgb(255, 63, 0)', # Red-Orange
73
- 'C3 Strong Typhoon': 'rgb(255, 127, 0)', # Orange
74
- 'C2 Typhoon': 'rgb(255, 191, 0)', # Orange-Yellow
75
- 'C1 Typhoon': 'rgb(255, 255, 0)', # Yellow
76
- 'Tropical Storm': 'rgb(0, 255, 255)', # Cyan
77
- 'Tropical Depression': 'rgb(173, 216, 230)' # Light Blue
78
  }
79
 
80
- def convert_typhoondata(input_file, output_file):
81
- with open(input_file, 'r') as infile:
82
- # Skip the title and the unit line.
83
- next(infile)
84
- next(infile)
85
-
86
- reader = csv.reader(infile)
87
-
88
- # Used for storing data for each SID
89
- sid_data = defaultdict(list)
90
 
91
- for row in reader:
92
- if not row: # Skip the blank lines
93
- continue
94
-
95
- sid = row[0]
96
- iso_time = row[6]
97
- sid_data[sid].append((row, iso_time))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
 
99
- with open(output_file, 'w', newline='') as outfile:
100
- fieldnames = ['SID', 'ISO_TIME', 'LAT', 'LON', 'SEASON', 'NAME', 'WMO_WIND', 'WMO_PRES', 'USA_WIND', 'USA_PRES', 'START_DATE', 'END_DATE']
101
- writer = csv.DictWriter(outfile, fieldnames=fieldnames)
102
 
103
- writer.writeheader()
104
-
105
- for sid, data in sid_data.items():
106
- start_date = min(data, key=lambda x: x[1])[1]
107
- end_date = max(data, key=lambda x: x[1])[1]
108
-
109
- for row, iso_time in data:
110
- writer.writerow({
111
- 'SID': row[0],
112
- 'ISO_TIME': iso_time,
113
- 'LAT': row[8],
114
- 'LON': row[9],
115
- 'SEASON': row[1],
116
- 'NAME': row[5],
117
- 'WMO_WIND': row[10].strip() or ' ',
118
- 'WMO_PRES': row[11].strip() or ' ',
119
- 'USA_WIND': row[23].strip() or ' ',
120
- 'USA_PRES': row[24].strip() or ' ',
121
- 'START_DATE': start_date,
122
- 'END_DATE': end_date
123
- })
124
-
125
-
126
- def download_oni_file(url, filename):
127
- print(f"Downloading file from {url}...")
128
- try:
129
- response = requests.get(url)
130
- response.raise_for_status() # Raises an exception for non-200 status codes
131
- with open(filename, 'wb') as f:
132
- f.write(response.content)
133
- print(f"File successfully downloaded and saved as {filename}")
134
- return True
135
- except requests.RequestException as e:
136
- print(f"Download failed. Error: {e}")
137
- return False
138
-
139
 
140
- def convert_oni_ascii_to_csv(input_file, output_file):
141
- data = defaultdict(lambda: [''] * 12)
142
- season_to_month = {
143
- 'DJF': 12, 'JFM': 1, 'FMA': 2, 'MAM': 3, 'AMJ': 4, 'MJJ': 5,
144
- 'JJA': 6, 'JAS': 7, 'ASO': 8, 'SON': 9, 'OND': 10, 'NDJ': 11
145
- }
146
-
147
- print(f"Attempting to read file: {input_file}")
148
- try:
149
  with open(input_file, 'r') as f:
150
- lines = f.readlines()
151
- print(f"Successfully read {len(lines)} lines")
152
-
153
- if len(lines) <= 1:
154
- print("Error: File is empty or contains only header")
155
- return
156
-
157
- for line in lines[1:]: # Skip header
158
  parts = line.split()
159
  if len(parts) >= 4:
160
- season, year = parts[0], parts[1]
161
- anom = parts[-1]
162
-
163
  if season in season_to_month:
164
  month = season_to_month[season]
165
-
166
  if season == 'DJF':
167
  year = str(int(year) - 1)
168
-
169
  data[year][month-1] = anom
170
- else:
171
- print(f"Warning: Unknown season: {season}")
172
- else:
173
- print(f"Warning: Skipping invalid line: {line.strip()}")
174
-
175
- print(f"Processed data for {len(data)} years")
176
- except Exception as e:
177
- print(f"Error reading file: {e}")
178
- return
179
 
180
- print(f"Attempting to write file: {output_file}")
181
- try:
182
  with open(output_file, 'w', newline='') as f:
183
  writer = csv.writer(f)
184
- writer.writerow(['Year', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'])
185
-
186
  for year in sorted(data.keys()):
187
- row = [year] + data[year]
188
- writer.writerow(row)
189
-
190
- print(f"Successfully wrote {len(data)} rows of data")
191
- except Exception as e:
192
- print(f"Error writing file: {e}")
193
- return
194
 
195
- print(f"Conversion complete. Data saved to {output_file}")
 
 
 
 
 
196
 
197
- def update_oni_data():
198
- global last_oni_update
199
- current_date = date.today()
200
-
201
- # Check if already updated today
202
- if last_oni_update == current_date:
203
- print("ONI data already checked today. Skipping update.")
204
- return
205
-
206
- url = "https://www.cpc.ncep.noaa.gov/data/indices/oni.ascii.txt"
207
- temp_file = os.path.join(DATA_PATH, "temp_oni.ascii.txt")
208
- input_file = os.path.join(DATA_PATH, "oni.ascii.txt")
209
- output_file = ONI_DATA_PATH
210
-
211
- if download_oni_file(url, temp_file):
212
- if not os.path.exists(input_file) or not filecmp.cmp(temp_file, input_file, shallow=False):
213
- # File doesn't exist or has been updated
214
- os.replace(temp_file, input_file)
215
- print("New ONI data detected. Converting to CSV.")
216
- convert_oni_ascii_to_csv(input_file, output_file)
217
- print("ONI data updated successfully.")
218
  else:
219
- print("ONI data is up to date. No conversion needed.")
220
- os.remove(temp_file) # Remove temporary file
221
-
222
- last_oni_update = current_date
223
- else:
224
- print("Failed to download ONI data.")
225
- if os.path.exists(temp_file):
226
- os.remove(temp_file) # Ensure cleanup of temporary file
227
-
228
- def load_ibtracs_data():
229
- if os.path.exists(CACHE_FILE):
230
- cache_time = datetime.fromtimestamp(os.path.getmtime(CACHE_FILE))
231
- if datetime.now() - cache_time < timedelta(days=CACHE_EXPIRY_DAYS):
232
- print("Loading data from cache...")
233
- with open(CACHE_FILE, 'rb') as f:
234
- return pickle.load(f)
235
-
236
- if os.path.exists(LOCAL_iBtrace_PATH):
237
- print("Using local IBTrACS file...")
238
- ibtracs = tracks.TrackDataset(basin='west_pacific', source='ibtracs', ibtracs_url=LOCAL_iBtrace_PATH)
239
- else:
240
- print("Local IBTrACS file not found. Fetching data from remote server...")
241
- try:
242
  response = requests.get(iBtrace_uri)
243
  response.raise_for_status()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
244
 
245
- with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.csv') as temp_file:
246
- temp_file.write(response.text)
247
- temp_file_path = temp_file.name
248
-
249
- # Save the downloaded data as the local file
250
- shutil.move(temp_file_path, LOCAL_iBtrace_PATH)
251
- print(f"Downloaded data saved to {LOCAL_iBtrace_PATH}")
252
-
253
- ibtracs = tracks.TrackDataset(basin='west_pacific', source='ibtracs', ibtracs_url=LOCAL_iBtrace_PATH)
254
- except requests.RequestException as e:
255
- print(f"Error downloading data: {e}")
256
- print("No local file available and download failed. Unable to load IBTrACS data.")
257
- return None
258
-
259
- with open(CACHE_FILE, 'wb') as f:
260
- pickle.dump(ibtracs, f)
261
-
262
- return ibtracs
263
-
264
- def update_ibtracs_data():
265
- global ibtracs
266
- print("Checking for IBTrACS data updates...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
 
268
- try:
269
- # Get the last-modified time of the remote file
270
- response = requests.head(iBtrace_uri)
271
- remote_last_modified = datetime.strptime(response.headers['Last-Modified'], '%a, %d %b %Y %H:%M:%S GMT')
272
 
273
- # Get the last-modified time of the local file
274
- if os.path.exists(LOCAL_iBtrace_PATH):
275
- local_last_modified = datetime.fromtimestamp(os.path.getmtime(LOCAL_iBtrace_PATH))
 
 
 
 
 
 
 
 
 
 
276
  else:
277
- local_last_modified = datetime.min
278
 
279
- # Compare the modification times
280
- if remote_last_modified <= local_last_modified:
281
- print("Local IBTrACS data is up to date. No update needed.")
282
- if os.path.exists(CACHE_FILE):
283
- # Update the cache file's timestamp to extend its validity
284
- os.utime(CACHE_FILE, None)
285
- print("Cache file timestamp updated.")
286
- return
287
-
288
- print("Remote data is newer. Updating IBTrACS data...")
289
-
290
- # Download the new data
291
- response = requests.get(iBtrace_uri)
292
- response.raise_for_status()
293
 
294
- with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.csv') as temp_file:
295
- temp_file.write(response.text)
296
- temp_file_path = temp_file.name
 
297
 
298
- # Save the downloaded data as the local file
299
- shutil.move(temp_file_path, LOCAL_iBtrace_PATH)
300
- print(f"Downloaded data saved to {LOCAL_iBtrace_PATH}")
 
 
 
301
 
302
- # Update the last modified time of the local file to match the remote file
303
- os.utime(LOCAL_iBtrace_PATH, (remote_last_modified.timestamp(), remote_last_modified.timestamp()))
 
 
 
 
 
 
 
 
304
 
305
- ibtracs = tracks.TrackDataset(basin='west_pacific', source='ibtracs', ibtracs_url=LOCAL_iBtrace_PATH)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
306
 
307
- with open(CACHE_FILE, 'wb') as f:
308
- pickle.dump(ibtracs, f)
309
- print("IBTrACS data updated and cache refreshed.")
310
-
311
- except requests.RequestException as e:
312
- print(f"Error checking or downloading data: {e}")
313
- if os.path.exists(LOCAL_iBtrace_PATH):
314
- print("Using existing local file.")
315
- ibtracs = tracks.TrackDataset(basin='west_pacific', source='ibtracs', ibtracs_url=LOCAL_iBtrace_PATH)
316
- if os.path.exists(CACHE_FILE):
317
- # Update the cache file's timestamp even when using existing local file
318
- os.utime(CACHE_FILE, None)
319
- print("Cache file timestamp updated.")
320
- else:
321
- print("No local file available. Update failed.")
322
-
323
- def run_schedule():
324
- while True:
325
- schedule.run_pending()
326
- time.sleep(1)
327
-
328
- def analyze_typhoon_generation(merged_data, start_date, end_date):
329
- filtered_data = merged_data[
330
- (merged_data['ISO_TIME'] >= start_date) &
331
- (merged_data['ISO_TIME'] <= end_date)
332
- ]
333
-
334
- filtered_data['ENSO_Phase'] = filtered_data['ONI'].apply(classify_enso_phases)
335
-
336
- typhoon_counts = filtered_data['ENSO_Phase'].value_counts().to_dict()
337
-
338
- month_counts = filtered_data.groupby(['ENSO_Phase', filtered_data['ISO_TIME'].dt.month]).size().unstack(fill_value=0)
339
- concentrated_months = month_counts.idxmax(axis=1).to_dict()
340
-
341
- return typhoon_counts, concentrated_months
342
-
343
- def cache_key_generator(*args, **kwargs):
344
- key = hashlib.md5()
345
- for arg in args:
346
- key.update(str(arg).encode())
347
- for k, v in sorted(kwargs.items()):
348
- key.update(str(k).encode())
349
- key.update(str(v).encode())
350
- return key.hexdigest()
351
-
352
- def categorize_typhoon(wind_speed):
353
- wind_speed_kt = wind_speed / 2 # Convert kt to m/s
354
-
355
- # Add category classification
356
- if wind_speed_kt >= 137/2.35:
357
- return 'C5 Super Typhoon'
358
- elif wind_speed_kt >= 113/2.35:
359
- return 'C4 Very Strong Typhoon'
360
- elif wind_speed_kt >= 96/2.35:
361
- return 'C3 Strong Typhoon'
362
- elif wind_speed_kt >= 83/2.35:
363
- return 'C2 Typhoon'
364
- elif wind_speed_kt >= 64/2.35:
365
- return 'C1 Typhoon'
366
- elif wind_speed_kt >= 34/2.35:
367
- return 'Tropical Storm'
368
- else:
369
- return 'Tropical Depression'
370
-
371
- @functools.lru_cache(maxsize=None)
372
- def process_oni_data_cached(oni_data_hash):
373
- return process_oni_data(oni_data)
374
-
375
- def process_oni_data(oni_data):
376
- oni_long = oni_data.melt(id_vars=['Year'], var_name='Month', value_name='ONI')
377
- oni_long['Month'] = oni_long['Month'].map({
378
- 'Jan': '01', 'Feb': '02', 'Mar': '03', 'Apr': '04', 'May': '05', 'Jun': '06',
379
- 'Jul': '07', 'Aug': '08', 'Sep': '09', 'Oct': '10', 'Nov': '11', 'Dec': '12'
380
- })
381
- oni_long['Date'] = pd.to_datetime(oni_long['Year'].astype(str) + '-' + oni_long['Month'] + '-01')
382
- oni_long['ONI'] = pd.to_numeric(oni_long['ONI'], errors='coerce')
383
- return oni_long
384
-
385
- def process_oni_data_with_cache(oni_data):
386
- oni_data_hash = cache_key_generator(oni_data.to_json())
387
- return process_oni_data_cached(oni_data_hash)
388
-
389
- @functools.lru_cache(maxsize=None)
390
- def process_typhoon_data_cached(typhoon_data_hash):
391
- return process_typhoon_data(typhoon_data)
392
-
393
- def process_typhoon_data(typhoon_data):
394
- typhoon_data['ISO_TIME'] = pd.to_datetime(typhoon_data['ISO_TIME'], errors='coerce')
395
- typhoon_data['USA_WIND'] = pd.to_numeric(typhoon_data['USA_WIND'], errors='coerce')
396
- typhoon_data['USA_PRES'] = pd.to_numeric(typhoon_data['USA_PRES'], errors='coerce')
397
- typhoon_data['LON'] = pd.to_numeric(typhoon_data['LON'], errors='coerce')
398
-
399
- typhoon_max = typhoon_data.groupby('SID').agg({
400
- 'USA_WIND': 'max',
401
- 'USA_PRES': 'min',
402
- 'ISO_TIME': 'first',
403
- 'SEASON': 'first',
404
- 'NAME': 'first',
405
- 'LAT': 'first',
406
- 'LON': 'first'
407
- }).reset_index()
408
-
409
- typhoon_max['Month'] = typhoon_max['ISO_TIME'].dt.strftime('%m')
410
- typhoon_max['Year'] = typhoon_max['ISO_TIME'].dt.year
411
- typhoon_max['Category'] = typhoon_max['USA_WIND'].apply(categorize_typhoon)
412
- return typhoon_max
413
-
414
- def process_typhoon_data_with_cache(typhoon_data):
415
- typhoon_data_hash = cache_key_generator(typhoon_data.to_json())
416
- return process_typhoon_data_cached(typhoon_data_hash)
417
-
418
- def merge_data(oni_long, typhoon_max):
419
- return pd.merge(typhoon_max, oni_long, on=['Year', 'Month'])
420
-
421
- def calculate_logistic_regression(merged_data):
422
- data = merged_data.dropna(subset=['USA_WIND', 'ONI'])
423
-
424
- # Create binary outcome for severe typhoons
425
- data['severe_typhoon'] = (data['USA_WIND'] >= 51).astype(int)
426
-
427
- # Create binary predictor for El Niño
428
- data['el_nino'] = (data['ONI'] >= 0.5).astype(int)
429
-
430
- X = data['el_nino']
431
- X = sm.add_constant(X) # Add constant term
432
- y = data['severe_typhoon']
433
-
434
- model = sm.Logit(y, X).fit()
435
-
436
- beta_1 = model.params['el_nino']
437
- exp_beta_1 = np.exp(beta_1)
438
- p_value = model.pvalues['el_nino']
439
-
440
- return beta_1, exp_beta_1, p_value
441
-
442
- @cachetools.cached(cache={})
443
- def fetch_oni_data_from_csv(file_path):
444
- df = pd.read_csv(file_path, sep=',', header=0, na_values='-99.90')
445
- df.columns = ['Year', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
446
- df = df.melt(id_vars=['Year'], var_name='Month', value_name='ONI')
447
- df['Date'] = pd.to_datetime(df['Year'].astype(str) + df['Month'], format='%Y%b')
448
- df = df.set_index('Date')
449
- return df
450
-
451
- def classify_enso_phases(oni_value):
452
- if isinstance(oni_value, pd.Series):
453
- oni_value = oni_value.iloc[0]
454
- if oni_value >= 0.5:
455
- return 'El Nino'
456
- elif oni_value <= -0.5:
457
- return 'La Nina'
458
- else:
459
- return 'Neutral'
460
-
461
- def load_data(oni_data_path, typhoon_data_path):
462
- oni_data = pd.read_csv(oni_data_path)
463
- typhoon_data = pd.read_csv(typhoon_data_path, low_memory=False)
464
-
465
- typhoon_data['ISO_TIME'] = pd.to_datetime(typhoon_data['ISO_TIME'], errors='coerce')
466
-
467
- typhoon_data = typhoon_data.dropna(subset=['ISO_TIME'])
468
-
469
- print(f"Typhoon data shape after cleaning: {typhoon_data.shape}")
470
- print(f"Year range: {typhoon_data['ISO_TIME'].dt.year.min()} - {typhoon_data['ISO_TIME'].dt.year.max()}")
471
-
472
- return oni_data, typhoon_data
473
-
474
- def preprocess_data(oni_data, typhoon_data):
475
- typhoon_data['USA_WIND'] = pd.to_numeric(typhoon_data['USA_WIND'], errors='coerce')
476
- typhoon_data['WMO_PRES'] = pd.to_numeric(typhoon_data['WMO_PRES'], errors='coerce')
477
- typhoon_data['ISO_TIME'] = pd.to_datetime(typhoon_data['ISO_TIME'], errors='coerce')
478
- typhoon_data['Year'] = typhoon_data['ISO_TIME'].dt.year
479
- typhoon_data['Month'] = typhoon_data['ISO_TIME'].dt.month
480
-
481
- monthly_max_wind_speed = typhoon_data.groupby(['Year', 'Month'])['USA_WIND'].max().reset_index()
482
-
483
- oni_data_long = pd.melt(oni_data, id_vars=['Year'], var_name='Month', value_name='ONI')
484
- oni_data_long['Month'] = oni_data_long['Month'].apply(lambda x: pd.to_datetime(x, format='%b').month)
485
-
486
- merged_data = pd.merge(monthly_max_wind_speed, oni_data_long, on=['Year', 'Month'])
487
-
488
- return merged_data
489
-
490
- def calculate_max_wind_min_pressure(typhoon_data):
491
- max_wind_speed = typhoon_data['USA_WIND'].max()
492
- min_pressure = typhoon_data['WMO_PRES'].min()
493
- return max_wind_speed, min_pressure
494
-
495
- @functools.lru_cache(maxsize=None)
496
- def get_storm_data(storm_id):
497
- return ibtracs.get_storm(storm_id)
498
-
499
- def filter_west_pacific_coordinates(lons, lats):
500
- mask = (100 <= lons) & (lons <= 180) & (0 <= lats) & (lats <= 40)
501
- return lons[mask], lats[mask]
502
-
503
- def polynomial_exp(x, a, b, c, d):
504
- return a * x**2 + b * x + c + d * np.exp(x)
505
-
506
- def exponential(x, a, b, c):
507
- return a * np.exp(b * x) + c
508
-
509
- def generate_cluster_equations(cluster_center):
510
- X = cluster_center[:, 0] # Longitudes
511
- y = cluster_center[:, 1] # Latitudes
512
-
513
- x_min = X.min()
514
- x_max = X.max()
515
-
516
- equations = []
517
-
518
- # Fourier Series (up to 4th order)
519
- def fourier_series(x, a0, a1, b1, a2, b2, a3, b3, a4, b4):
520
- return (a0 + a1*np.cos(x) + b1*np.sin(x) +
521
- a2*np.cos(2*x) + b2*np.sin(2*x) +
522
- a3*np.cos(3*x) + b3*np.sin(3*x) +
523
- a4*np.cos(4*x) + b4*np.sin(4*x))
524
-
525
- # Normalize X to the range [0, 2π]
526
- X_normalized = 2 * np.pi * (X - x_min) / (x_max - x_min)
527
-
528
- params, _ = curve_fit(fourier_series, X_normalized, y)
529
- a0, a1, b1, a2, b2, a3, b3, a4, b4 = params
530
-
531
- # Create the equation string
532
- fourier_eq = (f"y = {a0:.4f} + {a1:.4f}*cos(x) + {b1:.4f}*sin(x) + "
533
- f"{a2:.4f}*cos(2x) + {b2:.4f}*sin(2x) + "
534
- f"{a3:.4f}*cos(3x) + {b3:.4f}*sin(3x) + "
535
- f"{a4:.4f}*cos(4x) + {b4:.4f}*sin(4x)")
536
-
537
- equations.append(("Fourier Series", fourier_eq))
538
- equations.append(("X Range", f"x goes from 0 to {2*np.pi:.4f}"))
539
- equations.append(("Longitude Range", f"Longitude goes from {x_min:.4f}°E to {x_max:.4f}°E"))
540
-
541
- return equations, (x_min, x_max)
542
-
543
- #oni_df = fetch_oni_data_from_csv(ONI_DATA_PATH)
544
- #ibtracs = load_ibtracs_data()
545
- #oni_data, typhoon_data = load_data(ONI_DATA_PATH, TYPHOON_DATA_PATH)
546
- #oni_long = process_oni_data_with_cache(oni_data)
547
- #typhoon_max = process_typhoon_data_with_cache(typhoon_data)
548
- #merged_data = merge_data(oni_long, typhoon_max)
549
- #data = preprocess_data(oni_data, typhoon_data)
550
- #max_wind_speed, min_pressure = calculate_max_wind_min_pressure(typhoon_data)
551
- #
552
- ## Schedule the update to run daily at 1:00 AM
553
- #schedule.every().day.at("01:00").do(update_ibtracs_data)
554
- #
555
- ## Run the scheduler in a separate thread
556
- #scheduler_thread = threading.Thread(target=run_schedule)
557
- #scheduler_thread.start()
558
-
559
-
560
- app = dash.Dash(__name__)
561
-
562
- # First, add the classification standards
563
- atlantic_standard = {
564
- 'C5 Super Typhoon': {'wind_speed': 137, 'color': 'rgb(255, 0, 0)'},
565
- 'C4 Very Strong Typhoon': {'wind_speed': 113, 'color': 'rgb(255, 63, 0)'},
566
- 'C3 Strong Typhoon': {'wind_speed': 96, 'color': 'rgb(255, 127, 0)'},
567
- 'C2 Typhoon': {'wind_speed': 83, 'color': 'rgb(255, 191, 0)'},
568
- 'C1 Typhoon': {'wind_speed': 64, 'color': 'rgb(255, 255, 0)'},
569
- 'Tropical Storm': {'wind_speed': 34, 'color': 'rgb(0, 255, 255)'},
570
- 'Tropical Depression': {'wind_speed': 0, 'color': 'rgb(173, 216, 230)'}
571
- }
572
-
573
- taiwan_standard = {
574
- 'Strong Typhoon': {'wind_speed': 51.0, 'color': 'rgb(255, 0, 0)'}, # >= 51.0 m/s
575
- 'Medium Typhoon': {'wind_speed': 33.7, 'color': 'rgb(255, 127, 0)'}, # 33.7-50.9 m/s
576
- 'Mild Typhoon': {'wind_speed': 17.2, 'color': 'rgb(255, 255, 0)'}, # 17.2-33.6 m/s
577
- 'Tropical Depression': {'wind_speed': 0, 'color': 'rgb(173, 216, 230)'} # < 17.2 m/s
578
- }
579
-
580
- app.layout = html.Div([
581
- html.H1("Typhoon Analysis Dashboard"),
582
-
583
- html.Div([
584
- dcc.Input(id='start-year', type='number', placeholder='Start Year', value=2000, min=1900, max=2024, step=1),
585
- dcc.Input(id='start-month', type='number', placeholder='Start Month', value=1, min=1, max=12, step=1),
586
- dcc.Input(id='end-year', type='number', placeholder='End Year', value=2024, min=1900, max=2024, step=1),
587
- dcc.Input(id='end-month', type='number', placeholder='End Month', value=6, min=1, max=12, step=1),
588
- dcc.Dropdown(
589
- id='enso-dropdown',
590
- options=[
591
- {'label': 'All Years', 'value': 'all'},
592
- {'label': 'El Niño Years', 'value': 'el_nino'},
593
- {'label': 'La Niña Years', 'value': 'la_nina'},
594
- {'label': 'Neutral Years', 'value': 'neutral'}
595
- ],
596
- value='all'
597
- ),
598
- html.Button('Analyze', id='analyze-button', n_clicks=0),
599
- ]),
600
-
601
- html.Div([
602
- dcc.Input(id='typhoon-search', type='text', placeholder='Search Typhoon Name'),
603
- html.Button('Find Typhoon', id='find-typhoon-button', n_clicks=0),
604
- ]),
605
-
606
- html.Div([
607
- html.Div(id='correlation-coefficient'),
608
- html.Div(id='max-wind-speed'),
609
- html.Div(id='min-pressure'),
610
- ]),
611
-
612
- dcc.Graph(id='typhoon-tracks-graph'),
613
- html.Div([
614
- html.P("Number of Clusters"),
615
- dcc.Input(id='n-clusters', type='number', placeholder='Number of Clusters', value=5, min=1, max=20, step=1),
616
- html.Button('Show Clusters', id='show-clusters-button', n_clicks=0),
617
- html.Button('Show Typhoon Routes', id='show-routes-button', n_clicks=0),
618
- ]),
619
-
620
- dcc.Graph(id='typhoon-routes-graph'),
621
-
622
- html.Div([
623
- html.Button('Fourier Series', id='fourier-series-button', n_clicks=0),
624
- ]),
625
- html.Div(id='cluster-equation-results'),
626
-
627
- html.Div([
628
- html.Button('Wind Speed Logistic Regression', id='wind-regression-button', n_clicks=0),
629
- html.Button('Pressure Logistic Regression', id='pressure-regression-button', n_clicks=0),
630
- html.Button('Longitude Logistic Regression', id='longitude-regression-button', n_clicks=0),
631
- ]),
632
- html.Div(id='logistic-regression-results'),
633
-
634
- html.H2("Typhoon Path Analysis"),
635
- html.Div([
636
- dcc.Dropdown(
637
- id='year-dropdown',
638
- options=[{'label': str(year), 'value': year} for year in range(1950, 2025)],
639
- value=2024,
640
- style={'width': '200px'}
641
- ),
642
- dcc.Dropdown(
643
- id='typhoon-dropdown',
644
- style={'width': '300px'}
645
- ),
646
- dcc.Dropdown(
647
- id='classification-standard',
648
- options=[
649
- {'label': 'Atlantic Standard', 'value': 'atlantic'},
650
- {'label': 'Taiwan Standard', 'value': 'taiwan'}
651
- ],
652
- value='atlantic',
653
- style={'width': '200px'}
654
- )
655
- ], style={'display': 'flex', 'gap': '10px'}),
656
-
657
- dcc.Graph(id='typhoon-path-animation'),
658
- dcc.Graph(id='all-years-regression-graph'),
659
- dcc.Graph(id='wind-oni-scatter-plot'),
660
- dcc.Graph(id='pressure-oni-scatter'),
661
-
662
- html.Div(id='regression-graphs'),
663
- html.Div(id='slopes'),
664
- html.Div([
665
- html.H3("Correlation Analysis"),
666
- html.Div(id='wind-oni-correlation'),
667
- html.Div(id='pressure-oni-correlation'),
668
- ]),
669
- html.Div([
670
- html.H3("Typhoon Generation Analysis"),
671
- html.Div(id='typhoon-count-analysis'),
672
- html.Div(id='concentrated-months-analysis'),
673
- ]),
674
- html.Div(id='cluster-info'),
675
-
676
- html.Div([
677
- dcc.Dropdown(
678
- id='classification-standard',
679
- options=[
680
- {'label': 'Atlantic Standard', 'value': 'atlantic'},
681
- {'label': 'Taiwan Standard', 'value': 'taiwan'}
682
- ],
683
- value='atlantic',
684
- style={'width': '200px'}
685
  )
686
- ], style={'margin': '10px'}),
687
-
688
- ], style={'font-family': 'Arial, sans-serif'})
689
-
690
- @app.callback(
691
- Output('year-dropdown', 'options'),
692
- Input('typhoon-tracks-graph', 'figure')
693
- )
694
- def initialize_year_dropdown(_):
695
- try:
696
- years = typhoon_data['ISO_TIME'].dt.year.unique()
697
- years = years[~np.isnan(years)]
698
- years = sorted(years)
699
 
700
- options = [{'label': str(int(year)), 'value': int(year)} for year in years]
701
- print(f"Generated options: {options[:5]}...")
702
- return options
703
- except Exception as e:
704
- print(f"Error in initialize_year_dropdown: {str(e)}")
705
- return [{'label': 'Error', 'value': 'error'}]
706
-
707
- @app.callback(
708
- [Output('typhoon-dropdown', 'options'),
709
- Output('typhoon-dropdown', 'value')],
710
- [Input('year-dropdown', 'value')]
711
- )
712
- def update_typhoon_dropdown(selected_year):
713
- if not selected_year:
714
- raise PreventUpdate
715
-
716
- selected_year = int(selected_year)
717
-
718
- season = ibtracs.get_season(selected_year)
719
- storm_summary = season.summary()
720
-
721
- typhoon_options = []
722
- for i in range(storm_summary['season_storms']):
723
- storm_id = storm_summary['id'][i]
724
- storm_name = storm_summary['name'][i]
725
- typhoon_options.append({'label': f"{storm_name} ({storm_id})", 'value': storm_id})
726
-
727
- selected_typhoon = typhoon_options[0]['value'] if typhoon_options else None
728
- return typhoon_options, selected_typhoon
729
-
730
- @app.callback(
731
- Output('typhoon-path-animation', 'figure'),
732
- [Input('year-dropdown', 'value'),
733
- Input('typhoon-dropdown', 'value'),
734
- Input('classification-standard', 'value')]
735
- )
736
- def update_typhoon_path(selected_year, selected_sid, standard):
737
- if not selected_year or not selected_sid:
738
- raise PreventUpdate
739
-
740
- storm = ibtracs.get_storm(selected_sid)
741
- return create_typhoon_path_figure(storm, selected_year, standard)
742
-
743
- def create_typhoon_path_figure(storm, selected_year, standard='atlantic'):
744
- fig = go.Figure()
745
-
746
- fig.add_trace(
747
- go.Scattergeo(
748
- lon=storm.lon,
749
- lat=storm.lat,
750
- mode='lines',
751
- line=dict(width=2, color='gray'),
752
- name='Path',
753
- showlegend=False,
754
- )
755
- )
756
-
757
- fig.add_trace(
758
- go.Scattergeo(
759
- lon=[storm.lon[0]],
760
- lat=[storm.lat[0]],
761
- mode='markers',
762
- marker=dict(size=10, color='green', symbol='star'),
763
- name='Starting Point',
764
- text=storm.time[0].strftime('%Y-%m-%d %H:%M'),
765
- hoverinfo='text+name',
766
  )
767
- )
768
-
769
- frames = []
770
- for i in range(len(storm.time)):
771
- category, color = categorize_typhoon_by_standard(storm.vmax[i], standard)
772
 
773
- r34_ne = storm.dict['USA_R34_NE'][i] if 'USA_R34_NE' in storm.dict else None
774
- r34_se = storm.dict['USA_R34_SE'][i] if 'USA_R34_SE' in storm.dict else None
775
- r34_sw = storm.dict['USA_R34_SW'][i] if 'USA_R34_SW' in storm.dict else None
776
- r34_nw = storm.dict['USA_R34_NW'][i] if 'USA_R34_NW' in storm.dict else None
777
- rmw = storm.dict['USA_RMW'][i] if 'USA_RMW' in storm.dict else None
778
- eye_diameter = storm.dict['USA_EYE'][i] if 'USA_EYE' in storm.dict else None
779
-
780
- radius_info = f"R34: NE={r34_ne}, SE={r34_se}, SW={r34_sw}, NW={r34_nw}<br>"
781
- radius_info += f"RMW: {rmw}<br>"
782
- radius_info += f"Eye Diameter: {eye_diameter}"
783
-
784
- frame_data = [
785
- go.Scattergeo(
786
- lon=storm.lon[:i+1],
787
- lat=storm.lat[:i+1],
788
  mode='lines',
789
- line=dict(width=2, color='blue'),
790
- name='Path Traveled',
791
- showlegend=False,
792
- ),
793
- go.Scattergeo(
794
- lon=[storm.lon[i]],
795
- lat=[storm.lat[i]],
796
- mode='markers+text',
797
- marker=dict(size=10, color=color, symbol='star'),
798
- text=category,
799
- textposition="top center",
800
- textfont=dict(size=12, color=color),
801
- name='Current Location',
802
- hovertext=f"{storm.time[i].strftime('%Y-%m-%d %H:%M')}<br>"
803
- f"Category: {category}<br>"
804
- f"Wind Speed: {storm.vmax[i]:.1f} m/s<br>"
805
- f"{radius_info}",
806
- hoverinfo='text',
807
- ),
808
- ]
809
- frames.append(go.Frame(data=frame_data, name=f"frame{i}"))
810
-
811
- fig.frames = frames
812
-
813
- fig.update_layout(
814
- title=f"{selected_year} Year {storm.name} Typhoon Path",
815
- showlegend=False,
816
- geo=dict(
817
- projection_type='natural earth',
818
- showland=True,
819
- landcolor='rgb(243, 243, 243)',
820
- countrycolor='rgb(204, 204, 204)',
821
- coastlinecolor='rgb(100, 100, 100)',
822
- showocean=True,
823
- oceancolor='rgb(230, 250, 255)',
824
- ),
825
- updatemenus=[{
826
- "buttons": [
827
- {
828
- "args": [None, {"frame": {"duration": 100, "redraw": True},
829
- "fromcurrent": True,
830
- "transition": {"duration": 0}}],
831
- "label": "Play",
832
- "method": "animate"
833
- },
834
- {
835
- "args": [[None], {"frame": {"duration": 0, "redraw": True},
836
- "mode": "immediate",
837
- "transition": {"duration": 0}}],
838
- "label": "Pause",
839
- "method": "animate"
840
- }
841
- ],
842
- "direction": "left",
843
- "pad": {"r": 10, "t": 87},
844
- "showactive": False,
845
- "type": "buttons",
846
- "x": 0.1,
847
- "xanchor": "right",
848
- "y": 0,
849
- "yanchor": "top"
850
- }],
851
- sliders=[{
852
- "active": 0,
853
- "yanchor": "top",
854
- "xanchor": "left",
855
- "currentvalue": {
856
- "font": {"size": 20},
857
- "prefix": "Time: ",
858
- "visible": True,
859
- "xanchor": "right"
860
  },
861
- "transition": {"duration": 100, "easing": "cubic-in-out"},
862
- "pad": {"b": 10, "t": 50},
863
- "len": 0.9,
864
- "x": 0.1,
865
- "y": 0,
866
- "steps": [
867
- {
868
- "args": [[f"frame{k}"],
869
- {"frame": {"duration": 100, "redraw": True},
870
- "mode": "immediate",
871
- "transition": {"duration": 0}}
872
- ],
873
- "label": storm.time[k].strftime('%Y-%m-%d %H:%M'),
874
- "method": "animate"
875
- }
876
- for k in range(len(storm.time))
877
- ]
878
- }]
879
- )
880
-
881
- return fig
882
-
883
- @app.callback(
884
- [Output('typhoon-routes-graph', 'figure'),
885
- Output('cluster-equation-results', 'children')],
886
- [Input('analyze-button', 'n_clicks'),
887
- Input('show-clusters-button', 'n_clicks'),
888
- Input('show-routes-button', 'n_clicks'),
889
- Input('fourier-series-button', 'n_clicks')],
890
- [State('start-year', 'value'),
891
- State('start-month', 'value'),
892
- State('end-year', 'value'),
893
- State('end-month', 'value'),
894
- State('n-clusters', 'value'),
895
- State('enso-dropdown', 'value')]
896
- )
897
-
898
- def update_route_clusters(analyze_clicks, show_clusters_clicks, show_routes_clicks,
899
- fourier_clicks, start_year, start_month, end_year, end_month,
900
- n_clusters, enso_value):
901
- ctx = dash.callback_context
902
- button_id = ctx.triggered[0]['prop_id'].split('.')[0]
903
-
904
- start_date = datetime(start_year, start_month, 1)
905
- end_date = datetime(end_year, end_month, 28)
906
-
907
- filtered_oni_df = oni_df[(oni_df.index >= start_date) & (oni_df.index <= end_date)]
908
-
909
- fig_routes = go.Figure()
910
-
911
- clusters = np.array([]) # Initialize as empty NumPy array
912
- cluster_equations = []
913
-
914
- # Clustering analysis
915
- west_pacific_storms = []
916
- for year in range(start_year, end_year + 1):
917
- season = ibtracs.get_season(year)
918
- for storm_id in season.summary()['id']:
919
- storm = get_storm_data(storm_id)
920
- storm_date = storm.time[0]
921
- storm_oni = oni_df.loc[storm_date.strftime('%Y-%b')]['ONI']
922
- if isinstance(storm_oni, pd.Series):
923
- storm_oni = storm_oni.iloc[0]
924
- storm_phase = classify_enso_phases(storm_oni)
925
-
926
- if enso_value == 'all' or \
927
- (enso_value == 'el_nino' and storm_phase == 'El Nino') or \
928
- (enso_value == 'la_nina' and storm_phase == 'La Nina') or \
929
- (enso_value == 'neutral' and storm_phase == 'Neutral'):
930
- lons, lats = filter_west_pacific_coordinates(np.array(storm.lon), np.array(storm.lat))
931
- if len(lons) > 1: # Ensure the storm has a valid path in West Pacific
932
- west_pacific_storms.append((lons, lats))
933
-
934
- max_length = max(len(storm[0]) for storm in west_pacific_storms)
935
- standardized_routes = []
936
-
937
- for lons, lats in west_pacific_storms:
938
- if len(lons) < 2: # Skip if not enough points
939
- continue
940
- t = np.linspace(0, 1, len(lons))
941
- t_new = np.linspace(0, 1, max_length)
942
- lon_interp = interp1d(t, lons, kind='linear')(t_new)
943
- lat_interp = interp1d(t, lats, kind='linear')(t_new)
944
- route_vector = np.column_stack((lon_interp, lat_interp)).flatten()
945
- standardized_routes.append(route_vector)
946
-
947
- kmeans = KMeans(n_clusters=n_clusters, random_state=42, n_init=10)
948
- clusters = kmeans.fit_predict(standardized_routes)
949
-
950
- # Count the number of typhoons in each cluster
951
- cluster_counts = np.bincount(clusters)
952
-
953
- for lons, lats in west_pacific_storms:
954
- fig_routes.add_trace(go.Scattergeo(
955
- lon=lons, lat=lats,
956
- mode='lines',
957
- line=dict(width=1, color='lightgray'),
958
- showlegend=False,
959
- hoverinfo='none',
960
- visible=(button_id == 'show-routes-button')
961
- ))
962
-
963
- equations_output = []
964
- for i in range(n_clusters):
965
- cluster_center = kmeans.cluster_centers_[i].reshape(-1, 2)
966
- cluster_equations, (lon_min, lon_max) = generate_cluster_equations(cluster_center)
967
 
968
- #equations_output.append(html.H4(f"Cluster {i+1} (Typhoons: {cluster_counts[i]})"))
969
- equations_output.append(html.H4([
970
- f"Cluster {i+1} (Typhoons: ",
971
- html.Span(f"{cluster_counts[i]}", style={'color': 'blue'}),
972
- ")"
973
- ]))
974
- for name, eq in cluster_equations:
975
- equations_output.append(html.P(f"{name}: {eq}"))
 
 
 
 
 
976
 
977
- equations_output.append(html.P("To use in GeoGebra:"))
978
- equations_output.append(html.P(f"1. Set x-axis from 0 to {2*np.pi:.4f}"))
979
- equations_output.append(html.P(f"2. Use the equation as is"))
980
- equations_output.append(html.P(f"3. To convert x back to longitude: lon = {lon_min:.4f} + x * {(lon_max - lon_min) / (2*np.pi):.4f}"))
981
- equations_output.append(html.Hr())
 
 
 
 
 
 
 
 
982
 
983
- fig_routes.add_trace(go.Scattergeo(
984
- lon=cluster_center[:, 0],
985
- lat=cluster_center[:, 1],
986
- mode='lines',
987
- name=f'Cluster {i+1} (n={cluster_counts[i]})',
988
- line=dict(width=3),
989
- visible=(button_id == 'show-clusters-button')
990
- ))
991
-
992
- enso_phase_text = {
993
- 'all': 'All Years',
994
- 'el_nino': 'El Niño Years',
995
- 'la_nina': 'La Niña Years',
996
- 'neutral': 'Neutral Years'
997
- }
998
- fig_routes.update_layout(
999
- title=f'Typhoon Routes Clustering in West Pacific ({start_year}-{end_year}) - {enso_phase_text[enso_value]}',
1000
- geo=dict(
1001
- projection_type='mercator',
1002
- showland=True,
1003
- landcolor='rgb(243, 243, 243)',
1004
- countrycolor='rgb(204, 204, 204)',
1005
- coastlinecolor='rgb(100, 100, 100)',
1006
- showocean=True,
1007
- oceancolor='rgb(230, 250, 255)',
1008
- lataxis={'range': [0, 40]},
1009
- lonaxis={'range': [100, 180]},
1010
- center={'lat': 20, 'lon': 140},
1011
- ),
1012
- legend_title='Clusters'
1013
- )
1014
-
1015
- return fig_routes, html.Div(equations_output)
1016
-
1017
- @app.callback(
1018
- [Output('typhoon-tracks-graph', 'figure'),
1019
- Output('all-years-regression-graph', 'figure'),
1020
- Output('regression-graphs', 'children'),
1021
- Output('slopes', 'children'),
1022
- Output('wind-oni-scatter-plot', 'figure'),
1023
- Output('pressure-oni-scatter', 'figure'),
1024
- Output('correlation-coefficient', 'children'),
1025
- Output('max-wind-speed', 'children'),
1026
- Output('min-pressure', 'children'),
1027
- Output('wind-oni-correlation', 'children'),
1028
- Output('pressure-oni-correlation', 'children'),
1029
- Output('typhoon-count-analysis', 'children'),
1030
- Output('concentrated-months-analysis', 'children')],
1031
- [Input('analyze-button', 'n_clicks'),
1032
- Input('find-typhoon-button', 'n_clicks')],
1033
- [State('start-year', 'value'),
1034
- State('start-month', 'value'),
1035
- State('end-year', 'value'),
1036
- State('end-month', 'value'),
1037
- State('enso-dropdown', 'value'),
1038
- State('typhoon-search', 'value')]
1039
- )
1040
-
1041
- def update_graphs(analyze_clicks, find_typhoon_clicks,
1042
- start_year, start_month, end_year, end_month,
1043
- enso_value, typhoon_search):
1044
- ctx = dash.callback_context
1045
- button_id = ctx.triggered[0]['prop_id'].split('.')[0]
1046
-
1047
- start_date = datetime(start_year, start_month, 1)
1048
- end_date = datetime(end_year, end_month, 28)
1049
-
1050
- filtered_oni_df = oni_df[(oni_df.index >= start_date) & (oni_df.index <= end_date)]
1051
-
1052
 
1053
- regression_data = {'El Nino': {'longitudes': [], 'oni_values': [], 'names': []},
1054
- 'La Nina': {'longitudes': [], 'oni_values': [], 'names': []},
1055
- 'Neutral': {'longitudes': [], 'oni_values': [], 'names': []},
1056
- 'All': {'longitudes': [], 'oni_values': [], 'names': []}}
 
1057
 
1058
- fig_tracks = go.Figure()
1059
-
1060
- def process_storm(year, storm_id):
1061
- storm = get_storm_data(storm_id)
1062
- storm_dates = storm.time
1063
- if any(start_date <= date <= end_date for date in storm_dates):
1064
- storm_oni = filtered_oni_df.loc[storm_dates[0].strftime('%Y-%b')]['ONI']
1065
- if isinstance(storm_oni, pd.Series):
1066
- storm_oni = storm_oni.iloc[0]
1067
- phase = classify_enso_phases(storm_oni)
1068
-
1069
- regression_data[phase]['longitudes'].append(storm.lon[0])
1070
- regression_data[phase]['oni_values'].append(storm_oni)
1071
- regression_data[phase]['names'].append(f'{storm.name} ({year})')
1072
- regression_data['All']['longitudes'].append(storm.lon[0])
1073
- regression_data['All']['oni_values'].append(storm_oni)
1074
- regression_data['All']['names'].append(f'{storm.name} ({year})')
1075
-
1076
- if (enso_value == 'all' or
1077
- (enso_value == 'el_nino' and phase == 'El Nino') or
1078
- (enso_value == 'la_nina' and phase == 'La Nina') or
1079
- (enso_value == 'neutral' and phase == 'Neutral')):
1080
- color = {'El Nino': 'red', 'La Nina': 'blue', 'Neutral': 'green'}[phase]
1081
- return go.Scattergeo(
1082
- lon=storm.lon,
1083
- lat=storm.lat,
1084
- mode='lines',
1085
- name=storm.name,
1086
- text=f'{storm.name} ({year})',
1087
- hoverinfo='text',
1088
- line=dict(width=2, color=color)
1089
- )
1090
- return None
1091
-
1092
- with ThreadPoolExecutor() as executor:
1093
- futures = []
1094
- for year in range(start_year, end_year + 1):
1095
- season = ibtracs.get_season(year)
1096
- for storm_id in season.summary()['id']:
1097
- futures.append(executor.submit(process_storm, year, storm_id))
1098
 
1099
- for future in futures:
1100
- result = future.result()
1101
- if result:
1102
- fig_tracks.add_trace(result)
1103
-
1104
- fig_tracks.update_layout(
1105
- title=f'Typhoon Tracks from {start_year}-{start_month} to {end_year}-{end_month}',
1106
- geo=dict(
1107
- projection_type='natural earth',
1108
- showland=True,
1109
- )
1110
- )
1111
 
1112
- regression_figs = []
1113
- slopes = []
1114
- all_years_fig = go.Figure() # Initialize with an empty figure
 
 
 
 
 
 
 
1115
 
1116
- for phase in ['El Nino', 'La Nina', 'Neutral', 'All']:
1117
- df = pd.DataFrame({
1118
- 'Longitude': regression_data[phase]['longitudes'],
1119
- 'ONI': regression_data[phase]['oni_values'],
1120
- 'Name': regression_data[phase]['names']
1121
- })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1122
 
1123
- if not df.empty and len(df) > 1: # Ensure there's enough data for regression
1124
- try:
1125
- fig = px.scatter(df, x='Longitude', y='ONI', hover_data=['Name'],
1126
- labels={'Longitude': 'Longitude of Typhoon Generation', 'ONI': 'ONI Value'},
1127
- title=f'Typhoon Generation Location vs. ONI ({phase})')
 
 
 
 
 
1128
 
1129
- X = np.array(df['Longitude']).reshape(-1, 1)
1130
- y = df['ONI']
1131
- model = LinearRegression()
1132
- model.fit(X, y)
1133
- y_pred = model.predict(X)
1134
- slope = model.coef_[0]
1135
- intercept = model.intercept_
1136
- fraction_slope = Fraction(slope).limit_denominator()
1137
- equation = f'ONI = {fraction_slope} * Longitude + {Fraction(intercept).limit_denominator()}'
1138
 
1139
- fig.add_trace(go.Scatter(x=df['Longitude'], y=y_pred, mode='lines', name='Regression Line'))
1140
- fig.add_annotation(x=df['Longitude'].mean(), y=y_pred.mean(),
1141
- text=equation, showarrow=False, yshift=10)
1142
 
1143
- if phase == 'All':
1144
- all_years_fig = fig
1145
- else:
1146
- regression_figs.append(dcc.Graph(figure=fig))
 
 
 
 
 
 
1147
 
1148
- correlation_coef = np.corrcoef(df['Longitude'], df['ONI'])[0, 1]
1149
- slopes.append(html.P(f'{phase} Regression Slope: {slope:.4f}, Correlation Coefficient: {correlation_coef:.4f}'))
1150
- except Exception as e:
1151
- print(f"Error in regression analysis for {phase}: {str(e)}")
1152
- if phase != 'All':
1153
- regression_figs.append(html.Div(f"Error in analysis for {phase}"))
1154
- slopes.append(html.P(f'{phase} Regression: Error in analysis'))
1155
- else:
1156
- if phase != 'All':
1157
- regression_figs.append(html.Div(f"Insufficient data for {phase}"))
1158
- slopes.append(html.P(f'{phase} Regression: Insufficient data'))
1159
-
1160
- if all_years_fig.data == ():
1161
- all_years_fig = go.Figure()
1162
- all_years_fig.add_annotation(text="No data available for regression analysis",
1163
- xref="paper", yref="paper",
1164
- x=0.5, y=0.5, showarrow=False)
 
 
1165
 
1166
- if button_id == 'find-typhoon-button' and typhoon_search:
1167
- for trace in fig_tracks.data:
1168
- if typhoon_search.lower() in trace.name.lower():
1169
- trace.line.width = 5
1170
- trace.line.color = 'yellow'
1171
 
1172
- filtered_data = merged_data[
1173
- (merged_data['Year'] >= start_year) &
1174
- (merged_data['Year'] <= end_year) &
1175
- (merged_data['Month'].astype(int) >= start_month) &
1176
- (merged_data['Month'].astype(int) <= end_month)
1177
- ]
1178
-
1179
- wind_oni_scatter = px.scatter(filtered_data, x='ONI', y='USA_WIND', color='Category',
1180
- hover_data=['NAME', 'Year','Category'],
1181
- title='Wind Speed vs ONI',
1182
- labels={'ONI': 'ONI Value', 'USA_WIND': 'Maximum Wind Speed (knots)'},
1183
- color_discrete_map=color_map)
1184
- wind_oni_scatter.update_traces(hovertemplate='<b>%{customdata[0]} (%{customdata[1]})</b><br>Category: %{customdata[2]}<br>ONI: %{x}<br>Wind Speed: %{y} knots')
1185
-
1186
- pressure_oni_scatter = px.scatter(filtered_data, x='ONI', y='USA_PRES',color='Category',
1187
- hover_data=['NAME', 'Year','Category'],
1188
- title='Pressure vs ONI',
1189
- labels={'ONI': 'ONI Value', 'USA_PRES': 'Minimum Pressure (hPa)'},
1190
- color_discrete_map=color_map)
1191
- pressure_oni_scatter.update_traces(hovertemplate='<b>%{customdata[0]} (%{customdata[1]})</b><br>Category: %{customdata[2]}<br>ONI: %{x}<br>Pressure: %{y} hPa')
1192
-
1193
- if typhoon_search:
1194
- for fig in [wind_oni_scatter, pressure_oni_scatter]:
1195
- mask = filtered_data['NAME'].str.contains(typhoon_search, case=False, na=False)
1196
- fig.add_trace(go.Scatter(
1197
- x=filtered_data.loc[mask, 'ONI'],
1198
- y=filtered_data.loc[mask, 'USA_WIND' if 'Wind' in fig.layout.title.text else 'USA_PRES'],
1199
- mode='markers',
1200
- marker=dict(size=10, color='red', symbol='star'),
1201
- name=f'Matched: {typhoon_search}',
1202
- hovertemplate='<b>%{text}</b><br>Category: %{customdata}<br>ONI: %{x}<br>Value: %{y}',
1203
- text=filtered_data.loc[mask, 'NAME'] + ' (' + filtered_data.loc[mask, 'Year'].astype(str) + ')',
1204
- customdata=filtered_data.loc[mask, 'Category']
1205
- ))
1206
-
1207
 
1208
- start_date = datetime(start_year, start_month, 1)
1209
- end_date = datetime(end_year, end_month, 28)
1210
- typhoon_counts, concentrated_months = analyze_typhoon_generation(merged_data, start_date, end_date)
1211
-
1212
- month_names = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
1213
- count_analysis = [html.P(f"{phase}: {count} typhoons") for phase, count in typhoon_counts.items()]
1214
- month_analysis = [html.P(f"{phase}: Most concentrated in {month_names[month-1]}") for phase, month in concentrated_months.items()]
1215
-
1216
- max_wind_speed = filtered_data['USA_WIND'].max()
1217
- min_pressure = typhoon_data[(typhoon_data['ISO_TIME'].dt.year >= start_year) &
1218
- (typhoon_data['ISO_TIME'].dt.year <= end_year)]['WMO_PRES'].min()
1219
-
1220
- correlation_text = f"Logistic Regression Results: see below"
1221
- max_wind_speed_text = f"Maximum Wind Speed: {max_wind_speed:.2f} knots"
1222
- min_pressure_text = f"Minimum Pressure: {min_pressure:.2f} hPa"
1223
-
1224
-
1225
- return (fig_tracks, all_years_fig, regression_figs, slopes,
1226
- wind_oni_scatter, pressure_oni_scatter,
1227
- correlation_text, max_wind_speed_text, min_pressure_text,
1228
- "Wind-ONI correlation: See logistic regression results",
1229
- "Pressure-ONI correlation: See logistic regression results",
1230
- count_analysis, month_analysis)
1231
-
1232
- @app.callback(
1233
- Output('logistic-regression-results', 'children'),
1234
- [Input('wind-regression-button', 'n_clicks'),
1235
- Input('pressure-regression-button', 'n_clicks'),
1236
- Input('longitude-regression-button', 'n_clicks')],
1237
- [State('start-year', 'value'),
1238
- State('start-month', 'value'),
1239
- State('end-year', 'value'),
1240
- State('end-month', 'value')]
1241
- )
1242
- def update_logistic_regression(wind_clicks, pressure_clicks, longitude_clicks,
1243
- start_year, start_month, end_year, end_month):
1244
- ctx = dash.callback_context
1245
- if not ctx.triggered:
1246
- return "Click a button to see logistic regression results."
1247
-
1248
- button_id = ctx.triggered[0]['prop_id'].split('.')[0]
1249
-
1250
- start_date = datetime(start_year, start_month, 1)
1251
- end_date = datetime(end_year, end_month, 28)
1252
-
1253
- filtered_data = merged_data[
1254
- (merged_data['ISO_TIME'] >= start_date) &
1255
- (merged_data['ISO_TIME'] <= end_date)
1256
- ]
1257
-
1258
- if button_id == 'wind-regression-button':
1259
- return calculate_wind_logistic_regression(filtered_data)
1260
- elif button_id == 'pressure-regression-button':
1261
- return calculate_pressure_logistic_regression(filtered_data)
1262
- elif button_id == 'longitude-regression-button':
1263
- return calculate_longitude_logistic_regression(filtered_data)
1264
-
1265
- def calculate_wind_logistic_regression(data):
1266
- data['severe_typhoon'] = (data['USA_WIND'] >= 64).astype(int) # 64 knots threshold for severe typhoons
1267
- X = sm.add_constant(data['ONI'])
1268
- y = data['severe_typhoon']
1269
- model = sm.Logit(y, X).fit()
1270
-
1271
- beta_1 = model.params['ONI']
1272
- exp_beta_1 = np.exp(beta_1)
1273
- p_value = model.pvalues['ONI']
1274
-
1275
- el_nino_data = data[data['ONI'] >= 0.5]
1276
- la_nina_data = data[data['ONI'] <= -0.5]
1277
- neutral_data = data[(data['ONI'] > -0.5) & (data['ONI'] < 0.5)]
1278
-
1279
- el_nino_severe = el_nino_data['severe_typhoon'].mean()
1280
- la_nina_severe = la_nina_data['severe_typhoon'].mean()
1281
- neutral_severe = neutral_data['severe_typhoon'].mean()
1282
-
1283
- return html.Div([
1284
- html.H3("Wind Speed Logistic Regression Results"),
1285
- html.P(f"β1 (ONI coefficient): {beta_1:.4f}"),
1286
- html.P(f"exp(β1) (Odds Ratio): {exp_beta_1:.4f}"),
1287
- html.P(f"P-value: {p_value:.4f}"),
1288
- html.P("Interpretation:"),
1289
- html.Ul([
1290
- html.Li(f"For each unit increase in ONI, the odds of a severe typhoon are "
1291
- f"{'increased' if exp_beta_1 > 1 else 'decreased'} by a factor of {exp_beta_1:.2f}."),
1292
- html.Li(f"This effect is {'statistically significant' if p_value < 0.05 else 'not statistically significant'} "
1293
- f"at the 0.05 level.")
1294
- ]),
1295
- html.P("Proportion of severe typhoons:"),
1296
- html.Ul([
1297
- html.Li(f"El Niño conditions: {el_nino_severe:.2%}"),
1298
- html.Li(f"La Niña conditions: {la_nina_severe:.2%}"),
1299
- html.Li(f"Neutral conditions: {neutral_severe:.2%}")
1300
- ])
1301
- ])
1302
-
1303
- def calculate_pressure_logistic_regression(data):
1304
- data['intense_typhoon'] = (data['USA_PRES'] <= 950).astype(int) # 950 hPa threshold for intense typhoons
1305
- X = sm.add_constant(data['ONI'])
1306
- y = data['intense_typhoon']
1307
- model = sm.Logit(y, X).fit()
1308
-
1309
- beta_1 = model.params['ONI']
1310
- exp_beta_1 = np.exp(beta_1)
1311
- p_value = model.pvalues['ONI']
1312
-
1313
- el_nino_data = data[data['ONI'] >= 0.5]
1314
- la_nina_data = data[data['ONI'] <= -0.5]
1315
- neutral_data = data[(data['ONI'] > -0.5) & (data['ONI'] < 0.5)]
1316
-
1317
- el_nino_intense = el_nino_data['intense_typhoon'].mean()
1318
- la_nina_intense = la_nina_data['intense_typhoon'].mean()
1319
- neutral_intense = neutral_data['intense_typhoon'].mean()
1320
-
1321
- return html.Div([
1322
- html.H3("Pressure Logistic Regression Results"),
1323
- html.P(f"β1 (ONI coefficient): {beta_1:.4f}"),
1324
- html.P(f"exp(β1) (Odds Ratio): {exp_beta_1:.4f}"),
1325
- html.P(f"P-value: {p_value:.4f}"),
1326
- html.P("Interpretation:"),
1327
- html.Ul([
1328
- html.Li(f"For each unit increase in ONI, the odds of an intense typhoon (pressure <= 950 hPa) are "
1329
- f"{'increased' if exp_beta_1 > 1 else 'decreased'} by a factor of {exp_beta_1:.2f}."),
1330
- html.Li(f"This effect is {'statistically significant' if p_value < 0.05 else 'not statistically significant'} "
1331
- f"at the 0.05 level.")
1332
- ]),
1333
- html.P("Proportion of intense typhoons:"),
1334
- html.Ul([
1335
- html.Li(f"El Niño conditions: {el_nino_intense:.2%}"),
1336
- html.Li(f"La Niña conditions: {la_nina_intense:.2%}"),
1337
- html.Li(f"Neutral conditions: {neutral_intense:.2%}")
1338
- ])
1339
- ])
1340
 
1341
- def calculate_longitude_logistic_regression(data):
1342
- # Use only the data points where longitude is available
1343
- data = data.dropna(subset=['LON'])
1344
-
1345
- if len(data) == 0:
1346
- return html.Div("Insufficient data for longitude analysis")
1347
-
1348
- data['western_typhoon'] = (data['LON'] <= 140).astype(int) # 140°E as threshold for western typhoons
1349
- X = sm.add_constant(data['ONI'])
1350
- y = data['western_typhoon']
1351
- model = sm.Logit(y, X).fit()
1352
-
1353
- beta_1 = model.params['ONI']
1354
- exp_beta_1 = np.exp(beta_1)
1355
- p_value = model.pvalues['ONI']
1356
-
1357
- el_nino_data = data[data['ONI'] >= 0.5]
1358
- la_nina_data = data[data['ONI'] <= -0.5]
1359
- neutral_data = data[(data['ONI'] > -0.5) & (data['ONI'] < 0.5)]
1360
-
1361
- el_nino_western = el_nino_data['western_typhoon'].mean()
1362
- la_nina_western = la_nina_data['western_typhoon'].mean()
1363
- neutral_western = neutral_data['western_typhoon'].mean()
1364
-
1365
- return html.Div([
1366
- html.H3("Longitude Logistic Regression Results"),
1367
- html.P(f"β1 (ONI coefficient): {beta_1:.4f}"),
1368
- html.P(f"exp(β1) (Odds Ratio): {exp_beta_1:.4f}"),
1369
- html.P(f"P-value: {p_value:.4f}"),
1370
- html.P("Interpretation:"),
1371
- html.Ul([
1372
- html.Li(f"For each unit increase in ONI, the odds of a typhoon forming west of 140°E are "
1373
- f"{'increased' if exp_beta_1 > 1 else 'decreased'} by a factor of {exp_beta_1:.2f}."),
1374
- html.Li(f"This effect is {'statistically significant' if p_value < 0.05 else 'not statistically significant'} "
1375
- f"at the 0.05 level.")
1376
- ]),
1377
- html.P("Proportion of typhoons forming west of 140°E:"),
1378
- html.Ul([
1379
- html.Li(f"El Niño conditions: {el_nino_western:.2%}"),
1380
- html.Li(f"La Niña conditions: {la_nina_western:.2%}"),
1381
- html.Li(f"Neutral conditions: {neutral_western:.2%}")
1382
- ])
1383
- ])
1384
 
1385
- def categorize_typhoon_by_standard(wind_speed, standard='atlantic'):
1386
- """
1387
- Categorize typhoon based on wind speed and chosen standard
1388
- wind_speed is in knots
1389
- """
1390
- if standard == 'taiwan':
1391
- # Convert knots to m/s for Taiwan standard
1392
- wind_speed_ms = wind_speed * 0.514444
1393
-
1394
- if wind_speed_ms >= 51.0:
1395
- return 'Strong Typhoon', taiwan_standard['Strong Typhoon']['color']
1396
- elif wind_speed_ms >= 33.7:
1397
- return 'Medium Typhoon', taiwan_standard['Medium Typhoon']['color']
1398
- elif wind_speed_ms >= 17.2:
1399
- return 'Mild Typhoon', taiwan_standard['Mild Typhoon']['color']
1400
- else:
1401
- return 'Tropical Depression', taiwan_standard['Tropical Depression']['color']
1402
- else:
1403
- # Atlantic standard uses knots
1404
- if wind_speed >= 137:
1405
- return 'C5 Super Typhoon', atlantic_standard['C5 Super Typhoon']['color']
1406
- elif wind_speed >= 113:
1407
- return 'C4 Very Strong Typhoon', atlantic_standard['C4 Very Strong Typhoon']['color']
1408
- elif wind_speed >= 96:
1409
- return 'C3 Strong Typhoon', atlantic_standard['C3 Strong Typhoon']['color']
1410
- elif wind_speed >= 83:
1411
- return 'C2 Typhoon', atlantic_standard['C2 Typhoon']['color']
1412
- elif wind_speed >= 64:
1413
- return 'C1 Typhoon', atlantic_standard['C1 Typhoon']['color']
1414
- elif wind_speed >= 34:
1415
- return 'Tropical Storm', atlantic_standard['Tropical Storm']['color']
1416
- else:
1417
- return 'Tropical Depression', atlantic_standard['Tropical Depression']['color']
1418
 
1419
  if __name__ == "__main__":
1420
- print(f"Using data path: {DATA_PATH}")
1421
- # Update ONI data before starting the application
1422
- update_oni_data()
1423
- oni_df = fetch_oni_data_from_csv(ONI_DATA_PATH)
1424
- ibtracs = load_ibtracs_data()
1425
- convert_typhoondata(LOCAL_iBtrace_PATH, TYPHOON_DATA_PATH)
1426
- oni_data, typhoon_data = load_data(ONI_DATA_PATH, TYPHOON_DATA_PATH)
1427
- oni_long = process_oni_data_with_cache(oni_data)
1428
- typhoon_max = process_typhoon_data_with_cache(typhoon_data)
1429
- merged_data = merge_data(oni_long, typhoon_max)
1430
- data = preprocess_data(oni_data, typhoon_data)
1431
- max_wind_speed, min_pressure = calculate_max_wind_min_pressure(typhoon_data)
1432
-
1433
- # Schedule updates
1434
- scheduler_thread = threading.Thread(target=run_schedule)
1435
- scheduler_thread.daemon = True # Make the thread daemon so it doesn't block shutdown
1436
- scheduler_thread.start()
1437
-
1438
- # Run the server
1439
- app.run_server(
1440
- debug=False, # Set debug to False in production
1441
- host='0.0.0.0', # Bind to all interfaces
1442
- port=7860,
1443
- use_reloader=False # Disable reloader to prevent duplicate processes
1444
  )
 
1
+ import gradio as gr
2
  import plotly.graph_objects as go
3
  import plotly.express as px
4
+ from plotly.subplots import make_subplots
5
  import pickle
6
  import tropycal.tracks as tracks
7
  import pandas as pd
 
10
  import functools
11
  import hashlib
12
  import os
 
 
 
 
 
13
  from datetime import datetime, timedelta
14
+ from datetime import date
15
  from scipy import stats
16
  from scipy.optimize import minimize, curve_fit
17
  from sklearn.linear_model import LinearRegression
18
  from sklearn.cluster import KMeans
19
  from scipy.interpolate import interp1d
20
  from fractions import Fraction
 
 
21
  import statsmodels.api as sm
 
22
  import time
23
  import threading
24
  import requests
 
28
  from collections import defaultdict
29
  import shutil
30
  import filecmp
31
+ import warnings
32
+ warnings.filterwarnings('ignore')
33
 
34
+ # Constants
35
+ DATA_PATH = os.getcwd()
 
 
 
 
 
 
36
  ONI_DATA_PATH = os.path.join(DATA_PATH, 'oni_data.csv')
37
  TYPHOON_DATA_PATH = os.path.join(DATA_PATH, 'processed_typhoon_data.csv')
38
+ LOCAL_iBtrace_PATH = os.path.join(DATA_PATH, 'ibtracs.WP.list.v04r00.csv')
39
+ iBtrace_uri = 'https://www.ncei.noaa.gov/data/international-best-track-archive-for-climate-stewardship-ibtracs/v04r00/access/csv/ibtracs.WP.list.v04r00.csv'
 
40
  CACHE_FILE = 'ibtracs_cache.pkl'
41
  CACHE_EXPIRY_DAYS = 1
 
 
42
 
43
+ # Color mappings
44
+ COLOR_MAP = {
45
+ 'C5 Super Typhoon': 'rgb(255, 0, 0)',
46
+ 'C4 Very Strong Typhoon': 'rgb(255, 63, 0)',
47
+ 'C3 Strong Typhoon': 'rgb(255, 127, 0)',
48
+ 'C2 Typhoon': 'rgb(255, 191, 0)',
49
+ 'C1 Typhoon': 'rgb(255, 255, 0)',
50
+ 'Tropical Storm': 'rgb(0, 255, 255)',
51
+ 'Tropical Depression': 'rgb(173, 216, 230)'
 
 
 
 
 
 
 
 
 
 
 
 
52
  }
53
 
54
+ class TyphoonAnalyzer:
55
+ def __init__(self):
56
+ self.last_oni_update = None
57
+ self.load_initial_data()
 
 
 
 
 
 
58
 
59
+ def load_initial_data(self):
60
+ print("Loading initial data...")
61
+ self.update_oni_data()
62
+ self.oni_df = self.fetch_oni_data_from_csv()
63
+ self.ibtracs = self.load_ibtracs_data()
64
+ self.update_typhoon_data()
65
+ self.oni_data, self.typhoon_data = self.load_data()
66
+ self.oni_long = self.process_oni_data(self.oni_data)
67
+ self.typhoon_max = self.process_typhoon_data(self.typhoon_data)
68
+ self.merged_data = self.merge_data()
69
+ print("Initial data loading complete")
70
+
71
+ def fetch_oni_data_from_csv(self):
72
+ df = pd.read_csv(ONI_DATA_PATH)
73
+ df = df.melt(id_vars=['Year'], var_name='Month', value_name='ONI')
74
+ df['Date'] = pd.to_datetime(df['Year'].astype(str) + df['Month'], format='%Y%b')
75
+ return df.set_index('Date')
76
+
77
+ def should_update_oni(self):
78
+ today = datetime.now()
79
+ return (today.day == 1 or today.day == 15 or
80
+ today.day == (today.replace(day=1, month=today.month%12+1) - timedelta(days=1)).day)
81
+
82
+ def update_oni_data(self):
83
+ if not self.should_update_oni():
84
+ return
85
 
86
+ url = "https://www.cpc.ncep.noaa.gov/data/indices/oni.ascii.txt"
87
+ temp_file = os.path.join(DATA_PATH, "temp_oni.ascii.txt")
 
88
 
89
+ try:
90
+ response = requests.get(url)
91
+ response.raise_for_status()
92
+ with open(temp_file, 'wb') as f:
93
+ f.write(response.content)
94
+ self.convert_oni_ascii_to_csv(temp_file, ONI_DATA_PATH)
95
+ self.last_oni_update = date.today()
96
+ except Exception as e:
97
+ print(f"Error updating ONI data: {e}")
98
+ finally:
99
+ if os.path.exists(temp_file):
100
+ os.remove(temp_file)
101
+
102
+ def convert_oni_ascii_to_csv(self, input_file, output_file):
103
+ data = defaultdict(lambda: [''] * 12)
104
+ season_to_month = {
105
+ 'DJF': 12, 'JFM': 1, 'FMA': 2, 'MAM': 3, 'AMJ': 4, 'MJJ': 5,
106
+ 'JJA': 6, 'JAS': 7, 'ASO': 8, 'SON': 9, 'OND': 10, 'NDJ': 11
107
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
 
 
 
 
 
 
 
 
 
 
109
  with open(input_file, 'r') as f:
110
+ next(f) # Skip header
111
+ for line in f:
 
 
 
 
 
 
112
  parts = line.split()
113
  if len(parts) >= 4:
114
+ season, year, anom = parts[0], parts[1], parts[-1]
 
 
115
  if season in season_to_month:
116
  month = season_to_month[season]
 
117
  if season == 'DJF':
118
  year = str(int(year) - 1)
 
119
  data[year][month-1] = anom
 
 
 
 
 
 
 
 
 
120
 
 
 
121
  with open(output_file, 'w', newline='') as f:
122
  writer = csv.writer(f)
123
+ writer.writerow(['Year'] + [f"{m:02d}" for m in range(1, 13)])
 
124
  for year in sorted(data.keys()):
125
+ writer.writerow([year] + data[year])
 
 
 
 
 
 
126
 
127
+ def load_ibtracs_data(self):
128
+ if os.path.exists(CACHE_FILE):
129
+ cache_time = datetime.fromtimestamp(os.path.getmtime(CACHE_FILE))
130
+ if datetime.now() - cache_time < timedelta(days=CACHE_EXPIRY_DAYS):
131
+ with open(CACHE_FILE, 'rb') as f:
132
+ return pickle.load(f)
133
 
134
+ if os.path.exists(LOCAL_iBtrace_PATH):
135
+ ibtracs = tracks.TrackDataset(basin='west_pacific', source='ibtracs',
136
+ ibtracs_url=LOCAL_iBtrace_PATH)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
  else:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
  response = requests.get(iBtrace_uri)
139
  response.raise_for_status()
140
+ with open(LOCAL_iBtrace_PATH, 'w') as f:
141
+ f.write(response.text)
142
+ ibtracs = tracks.TrackDataset(basin='west_pacific', source='ibtracs',
143
+ ibtracs_url=LOCAL_iBtrace_PATH)
144
+
145
+ with open(CACHE_FILE, 'wb') as f:
146
+ pickle.dump(ibtracs, f)
147
+ return ibtracs
148
+
149
+ def update_typhoon_data(self):
150
+ try:
151
+ response = requests.head(iBtrace_uri)
152
+ remote_modified = datetime.strptime(response.headers['Last-Modified'],
153
+ '%a, %d %b %Y %H:%M:%S GMT')
154
+ local_modified = (datetime.fromtimestamp(os.path.getmtime(LOCAL_iBtrace_PATH))
155
+ if os.path.exists(LOCAL_iBtrace_PATH) else datetime.min)
156
 
157
+ if remote_modified > local_modified:
158
+ response = requests.get(iBtrace_uri)
159
+ response.raise_for_status()
160
+ with open(LOCAL_iBtrace_PATH, 'w') as f:
161
+ f.write(response.text)
162
+ except Exception as e:
163
+ print(f"Error updating typhoon data: {e}")
164
+
165
+ def load_data(self):
166
+ oni_data = pd.read_csv(ONI_DATA_PATH)
167
+ typhoon_data = pd.read_csv(TYPHOON_DATA_PATH, low_memory=False)
168
+ typhoon_data['ISO_TIME'] = pd.to_datetime(typhoon_data['ISO_TIME'])
169
+ return oni_data, typhoon_data
170
+
171
+ def process_oni_data(self, oni_data):
172
+ oni_long = pd.melt(oni_data, id_vars=['Year'], var_name='Month', value_name='ONI')
173
+ oni_long['Month'] = oni_long['Month'].map(lambda x: pd.to_datetime(x, format='%b').month)
174
+ return oni_long
175
+
176
+ def process_typhoon_data(self, typhoon_data):
177
+ typhoon_data['USA_WIND'] = pd.to_numeric(typhoon_data['USA_WIND'], errors='coerce')
178
+ typhoon_data['WMO_PRES'] = pd.to_numeric(typhoon_data['WMO_PRES'], errors='coerce')
179
+ typhoon_data['ISO_TIME'] = pd.to_datetime(typhoon_data['ISO_TIME'])
180
+ typhoon_data['Year'] = typhoon_data['ISO_TIME'].dt.year
181
+ typhoon_data['Month'] = typhoon_data['ISO_TIME'].dt.month
182
+
183
+ typhoon_max = typhoon_data.groupby(['SID', 'Year', 'Month']).agg({
184
+ 'USA_WIND': 'max',
185
+ 'WMO_PRES': 'min',
186
+ 'NAME': 'first',
187
+ 'LAT': 'first',
188
+ 'LON': 'first',
189
+ 'ISO_TIME': 'first'
190
+ }).reset_index()
191
+
192
+ typhoon_max['Category'] = typhoon_max['USA_WIND'].apply(self.categorize_typhoon)
193
+ return typhoon_max
194
 
195
+ def merge_data(self):
196
+ return pd.merge(self.typhoon_max, self.oni_long, on=['Year', 'Month'])
 
 
197
 
198
+ def categorize_typhoon(self, wind_speed):
199
+ if wind_speed >= 137:
200
+ return 'C5 Super Typhoon'
201
+ elif wind_speed >= 113:
202
+ return 'C4 Very Strong Typhoon'
203
+ elif wind_speed >= 96:
204
+ return 'C3 Strong Typhoon'
205
+ elif wind_speed >= 83:
206
+ return 'C2 Typhoon'
207
+ elif wind_speed >= 64:
208
+ return 'C1 Typhoon'
209
+ elif wind_speed >= 34:
210
+ return 'Tropical Storm'
211
  else:
212
+ return 'Tropical Depression'
213
 
214
+ def analyze_typhoon(self, start_year, start_month, end_year, end_month, enso_value='all'):
215
+ start_date = datetime(start_year, start_month, 1)
216
+ end_date = datetime(end_year, end_month, 28)
 
 
 
 
 
 
 
 
 
 
 
217
 
218
+ filtered_data = self.merged_data[
219
+ (self.merged_data['ISO_TIME'] >= start_date) &
220
+ (self.merged_data['ISO_TIME'] <= end_date)
221
+ ]
222
 
223
+ if enso_value != 'all':
224
+ filtered_data = filtered_data[
225
+ (filtered_data['ONI'] >= 0.5 if enso_value == 'el_nino' else
226
+ filtered_data['ONI'] <= -0.5 if enso_value == 'la_nina' else
227
+ (filtered_data['ONI'] > -0.5) & (filtered_data['ONI'] < 0.5))
228
+ ]
229
 
230
+ return {
231
+ 'tracks': self.create_tracks_plot(filtered_data),
232
+ 'wind': self.create_wind_analysis(filtered_data),
233
+ 'pressure': self.create_pressure_analysis(filtered_data),
234
+ 'clusters': self.create_cluster_analysis(filtered_data, 5),
235
+ 'stats': self.generate_statistics(filtered_data)
236
+ }
237
+
238
+ def create_tracks_plot(self, data):
239
+ fig = go.Figure()
240
 
241
+ for _, storm in data.groupby('SID'):
242
+ fig.add_trace(go.Scattergeo(
243
+ lon=storm['LON'],
244
+ lat=storm['LAT'],
245
+ mode='lines',
246
+ name=storm['NAME'].iloc[0],
247
+ line=dict(
248
+ width=2,
249
+ color=COLOR_MAP[storm['Category'].iloc[0]]
250
+ ),
251
+ hovertemplate=(
252
+ f"Name: {storm['NAME'].iloc[0]}<br>"
253
+ f"Category: {storm['Category'].iloc[0]}<br>"
254
+ f"Wind Speed: {storm['USA_WIND'].iloc[0]:.1f} kt<br>"
255
+ f"Pressure: {storm['WMO_PRES'].iloc[0]:.1f} hPa<br>"
256
+ f"Date: {storm['ISO_TIME'].iloc[0]:%Y-%m-%d}"
257
+ )
258
+ ))
259
 
260
+ fig.update_layout(
261
+ title='Typhoon Tracks',
262
+ showlegend=True,
263
+ geo=dict(
264
+ projection_type='mercator',
265
+ showland=True,
266
+ showcoastlines=True,
267
+ landcolor='rgb(243, 243, 243)',
268
+ countrycolor='rgb(204, 204, 204)',
269
+ coastlinecolor='rgb(214, 214, 214)',
270
+ lataxis=dict(range=[0, 50]),
271
+ lonaxis=dict(range=[100, 180]),
272
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
273
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
274
 
275
+ return fig
276
+
277
+ def create_wind_analysis(self, data):
278
+ fig = px.scatter(data,
279
+ x='ONI',
280
+ y='USA_WIND',
281
+ color='Category',
282
+ color_discrete_map=COLOR_MAP,
283
+ title='Wind Speed vs ONI Index',
284
+ labels={
285
+ 'ONI': 'Oceanic Niño Index',
286
+ 'USA_WIND': 'Maximum Wind Speed (kt)'
287
+ },
288
+ hover_data=['NAME', 'ISO_TIME']
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
289
  )
 
 
 
 
 
290
 
291
+ # Add regression line
292
+ x = data['ONI']
293
+ y = data['USA_WIND']
294
+ slope, intercept = np.polyfit(x, y, 1)
295
+ fig.add_trace(
296
+ go.Scatter(
297
+ x=x,
298
+ y=slope * x + intercept,
 
 
 
 
 
 
 
299
  mode='lines',
300
+ name=f'Regression (slope={slope:.2f})',
301
+ line=dict(color='black', dash='dash')
302
+ )
303
+ )
304
+
305
+ return fig
306
+
307
+ def create_pressure_analysis(self, data):
308
+ fig = px.scatter(data,
309
+ x='ONI',
310
+ y='WMO_PRES',
311
+ color='Category',
312
+ color_discrete_map=COLOR_MAP,
313
+ title='Pressure vs ONI Index',
314
+ labels={
315
+ 'ONI': 'Oceanic Niño Index',
316
+ 'WMO_PRES': 'Minimum Pressure (hPa)'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
317
  },
318
+ hover_data=['NAME', 'ISO_TIME']
319
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
320
 
321
+ # Add regression line
322
+ x = data['ONI']
323
+ y = data['WMO_PRES']
324
+ slope, intercept = np.polyfit(x, y, 1)
325
+ fig.add_trace(
326
+ go.Scatter(
327
+ x=x,
328
+ y=slope * x + intercept,
329
+ mode='lines',
330
+ name=f'Regression (slope={slope:.2f})',
331
+ line=dict(color='black', dash='dash')
332
+ )
333
+ )
334
 
335
+ return fig
336
+
337
+ def create_cluster_analysis(self, data, n_clusters=5):
338
+ # Prepare data for clustering
339
+ routes = []
340
+ for _, storm in data.groupby('SID'):
341
+ if len(storm) > 1:
342
+ # Standardize route length
343
+ t = np.linspace(0, 1, len(storm))
344
+ t_new = np.linspace(0, 1, 100)
345
+ lon_interp = interp1d(t, storm['LON'], kind='linear')(t_new)
346
+ lat_interp = interp1d(t, storm['LAT'], kind='linear')(t_new)
347
+ routes.append(np.column_stack((lon_interp, lat_interp)))
348
 
349
+ if not routes:
350
+ return go.Figure()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
351
 
352
+ # Perform clustering
353
+ routes_array = np.array(routes)
354
+ routes_reshaped = routes_array.reshape(routes_array.shape[0], -1)
355
+ kmeans = KMeans(n_clusters=n_clusters, random_state=42)
356
+ clusters = kmeans.fit_predict(routes_reshaped)
357
 
358
+ # Create visualization
359
+ fig = go.Figure()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
360
 
361
+ # Plot original routes colored by cluster
362
+ for route, cluster_id in zip(routes, clusters):
363
+ fig.add_trace(go.Scattergeo(
364
+ lon=route[:, 0],
365
+ lat=route[:, 1],
366
+ mode='lines',
367
+ line=dict(width=1, color=f'hsl({cluster_id * 360/n_clusters}, 50%, 50%)'),
368
+ showlegend=False
369
+ ))
 
 
 
370
 
371
+ # Plot cluster centers
372
+ for i in range(n_clusters):
373
+ center = kmeans.cluster_centers_[i].reshape(-1, 2)
374
+ fig.add_trace(go.Scattergeo(
375
+ lon=center[:, 0],
376
+ lat=center[:, 1],
377
+ mode='lines',
378
+ name=f'Cluster {i+1} Center',
379
+ line=dict(width=3, color=f'hsl({i * 360/n_clusters}, 100%, 50%)')
380
+ ))
381
 
382
+ fig.update_layout(
383
+ title='Typhoon Route Clusters',
384
+ showlegend=True,
385
+ geo=dict(
386
+ projection_type='mercator',
387
+ showland=True,
388
+ showcoastlines=True,
389
+ landcolor='rgb(243, 243, 243)',
390
+ countrycolor='rgb(204, 204, 204)',
391
+ coastlinecolor='rgb(214, 214, 214)',
392
+ lataxis=dict(range=[0, 50]),
393
+ lonaxis=dict(range=[100, 180]),
394
+ )
395
+ )
396
+
397
+ return fig
398
+
399
+ def generate_statistics(self, data):
400
+ stats = {
401
+ 'total_typhoons': len(data['SID'].unique()),
402
+ 'avg_wind': data['USA_WIND'].mean(),
403
+ 'max_wind': data['USA_WIND'].max(),
404
+ 'avg_pressure': data['WMO_PRES'].mean(),
405
+ 'min_pressure': data['WMO_PRES'].min(),
406
+ 'oni_correlation_wind': data['ONI'].corr(data['USA_WIND']),
407
+ 'oni_correlation_pressure': data['ONI'].corr(data['WMO_PRES']),
408
+ 'category_counts': data['Category'].value_counts().to_dict()
409
+ }
410
+
411
+ return f"""
412
+ ### Statistical Summary
413
+
414
+ - Total Typhoons: {stats['total_typhoons']}
415
+ - Average Wind Speed: {stats['avg_wind']:.2f} kt
416
+ - Maximum Wind Speed: {stats['max_wind']:.2f} kt
417
+ - Average Pressure: {stats['avg_pressure']:.2f} hPa
418
+ - Minimum Pressure: {stats['min_pressure']:.2f} hPa
419
+ - ONI-Wind Speed Correlation: {stats['oni_correlation_wind']:.3f}
420
+ - ONI-Pressure Correlation: {stats['oni_correlation_pressure']:.3f}
421
+
422
+ ### Category Distribution
423
+ {chr(10).join(f'- {cat}: {count}' for cat, count in stats['category_counts'].items())}
424
+ """
425
+
426
+ def create_interface():
427
+ analyzer = TyphoonAnalyzer()
428
+
429
+ with gr.Blocks(title="Typhoon Analysis Dashboard", theme=gr.themes.Base()) as demo:
430
+ gr.Markdown("# Typhoon Analysis Dashboard")
431
 
432
+ with gr.Tabs():
433
+ # Main Analysis Tab
434
+ with gr.Tab("Main Analysis"):
435
+ with gr.Row():
436
+ with gr.Column():
437
+ start_year = gr.Slider(1900, 2024, 2000, label="Start Year")
438
+ start_month = gr.Slider(1, 12, 1, label="Start Month")
439
+ with gr.Column():
440
+ end_year = gr.Slider(1900, 2024, 2024, label="End Year")
441
+ end_month = gr.Slider(1, 12, 12, label="End Month")
442
 
443
+ enso_dropdown = gr.Dropdown(
444
+ choices=["all", "el_nino", "la_nina", "neutral"],
445
+ value="all",
446
+ label="ENSO Phase"
447
+ )
 
 
 
 
448
 
449
+ analyze_btn = gr.Button("Analyze")
 
 
450
 
451
+ plots_tabs = gr.Tabs()
452
+ with plots_tabs:
453
+ with gr.Tab("Tracks"):
454
+ tracks_plot = gr.Plot()
455
+ with gr.Tab("Wind Analysis"):
456
+ wind_plot = gr.Plot()
457
+ with gr.Tab("Pressure Analysis"):
458
+ pressure_plot = gr.Plot()
459
+ with gr.Tab("Clusters"):
460
+ cluster_plot = gr.Plot()
461
 
462
+ stats_text = gr.Markdown()
463
+
464
+ # Search Tab
465
+ with gr.Tab("Typhoon Search"):
466
+ with gr.Row():
467
+ search_input = gr.Textbox(label="Search Typhoon Name")
468
+ search_btn = gr.Button("Search")
469
+ search_results = gr.Plot()
470
+ typhoon_info = gr.Markdown()
471
+
472
+ def analyze_callback(start_y, start_m, end_y, end_m, enso):
473
+ results = analyzer.analyze_typhoon(start_y, start_m, end_y, end_m, enso)
474
+ return [
475
+ results['tracks'],
476
+ results['wind'],
477
+ results['pressure'],
478
+ results['clusters'],
479
+ results['stats']
480
+ ]
481
 
482
+ analyze_btn.click(
483
+ analyze_callback,
484
+ inputs=[start_year, start_month, end_year, end_month, enso_dropdown],
485
+ outputs=[tracks_plot, wind_plot, pressure_plot, cluster_plot, stats_text]
486
+ )
487
 
488
+ def search_callback(query):
489
+ if not query:
490
+ return None, "Please enter a typhoon name to search."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
491
 
492
+ matches = analyzer.merged_data[
493
+ analyzer.merged_data['NAME'].str.contains(query, case=False, na=False)
494
+ ]
495
+
496
+ if matches.empty:
497
+ return None, "No typhoons found matching your search."
498
+
499
+ fig = analyzer.create_tracks_plot(matches)
500
+
501
+ info = f"### Found {len(matches['SID'].unique())} matching typhoons:\n\n"
502
+ for _, storm in matches.groupby('SID'):
503
+ info += (f"- {storm['NAME'].iloc[0]} ({storm['ISO_TIME'].iloc[0]:%Y-%m-%d})\n"
504
+ f" - Category: {storm['Category'].iloc[0]}\n"
505
+ f" - Max Wind: {storm['USA_WIND'].iloc[0]:.1f} kt\n"
506
+ f" - Min Pressure: {storm['WMO_PRES'].iloc[0]:.1f} hPa\n")
507
+
508
+ return fig, info
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
509
 
510
+ search_btn.click(
511
+ search_callback,
512
+ inputs=[search_input],
513
+ outputs=[search_results, typhoon_info]
514
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
515
 
516
+ return demo
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
517
 
518
  if __name__ == "__main__":
519
+ demo = create_interface()
520
+ demo.launch(
521
+ server_name="0.0.0.0",
522
+ server_port=7860,
523
+ share=True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
524
  )