euler314 commited on
Commit
d84b524
·
verified ·
1 Parent(s): 7138d01

Upload 2 files

Browse files
Files changed (2) hide show
  1. requirements.txt +22 -0
  2. typhoon_analysis.py +1445 -0
requirements.txt ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ dash==2.17.1
2
+ plotly==5.22.0
3
+ pandas==2.2.2
4
+ numpy==1.26.4
5
+ scipy==1.13.1
6
+ scikit-learn==1.5.1
7
+ cachetools==5.3.3
8
+ tropycal==1.3
9
+ pyshp==2.3.1
10
+ gitpython==3.1.30
11
+ requests==2.32.3
12
+ matplotlib==3.8.4
13
+ networkx==3.3
14
+ xarray==2024.6.0
15
+ shapely==2.0.4
16
+ pyproj==3.6.1
17
+ dash-core-components==2.0.0
18
+ dash-html-components==2.0.0
19
+ dash-table==5.0.0
20
+ Cartopy==0.23.0
21
+ statsmodels==0.14.1
22
+ schedule==1.2.0
typhoon_analysis.py ADDED
@@ -0,0 +1,1445 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
7
+ import numpy as np
8
+ 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
32
+ from io import StringIO
33
+ import tempfile
34
+ 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
+
1434
+ # Schedule IBTrACS data update daily
1435
+ schedule.every().day.at("01:00").do(update_ibtracs_data)
1436
+
1437
+ # Schedule ONI data check daily, but only update on specified dates
1438
+ schedule.every().day.at("00:00").do(lambda: update_oni_data() if should_update_oni() else None)
1439
+
1440
+ # Run the scheduler in a separate thread
1441
+ scheduler_thread = threading.Thread(target=run_schedule)
1442
+ scheduler_thread.start()
1443
+
1444
+
1445
+ app.run_server(debug=True, host='127.0.0.1', port=8050)