diff --git "a/app.py" "b/app.py"
--- "a/app.py"
+++ "b/app.py"
@@ -1,1445 +1,1445 @@
-import dash
-import plotly.graph_objects as go
-import plotly.express as px
-import pickle
-import tropycal.tracks as tracks
-import pandas as pd
-import numpy as np
-import cachetools
-import functools
-import hashlib
-import os
-import argparse
-from dash import dcc, html
-from dash.dependencies import Input, Output, State
-from dash.exceptions import PreventUpdate
-from plotly.subplots import make_subplots
-from datetime import datetime, timedelta
-from datetime import date, datetime
-from scipy import stats
-from scipy.optimize import minimize, curve_fit
-from sklearn.linear_model import LinearRegression
-from sklearn.cluster import KMeans
-from scipy.interpolate import interp1d
-from fractions import Fraction
-from concurrent.futures import ThreadPoolExecutor
-from sklearn.metrics import mean_squared_error
-import statsmodels.api as sm
-import schedule
-import time
-import threading
-import requests
-from io import StringIO
-import tempfile
-import csv
-from collections import defaultdict
-import shutil
-import filecmp
-
-# Add command-line argument parsing
-parser = argparse.ArgumentParser(description='Typhoon Analysis Dashboard')
-parser.add_argument('--data_path', type=str, default=os.getcwd(), help='Path to the data directory')
-args = parser.parse_args()
-
-# Use the command-line argument for data path
-DATA_PATH = args.data_path
-
-ONI_DATA_PATH = os.path.join(DATA_PATH, 'oni_data.csv')
-TYPHOON_DATA_PATH = os.path.join(DATA_PATH, 'processed_typhoon_data.csv')
-LOCAL_iBtrace_PATH = os.path.join(DATA_PATH, 'ibtracs.WP.list.v04r01.csv')
-iBtrace_uri = 'https://www.ncei.noaa.gov/data/international-best-track-archive-for-climate-stewardship-ibtracs/v04r01/access/csv/ibtracs.WP.list.v04r01.csv'
-
-CACHE_FILE = 'ibtracs_cache.pkl'
-CACHE_EXPIRY_DAYS = 1
-last_oni_update = None
-
-
-def should_update_oni():
- today = datetime.now()
- # Beginning of the month: 1st day
- if today.day == 1:
- return True
- # Middle of the month: 15th day
- if today.day == 15:
- return True
- # End of the month: last day
- if today.day == (today.replace(day=1, month=today.month%12+1) - timedelta(days=1)).day:
- return True
- return False
-
-color_map = {
- 'C5 Super Typhoon': 'rgb(255, 0, 0)', # Red
- 'C4 Very Strong Typhoon': 'rgb(255, 63, 0)', # Red-Orange
- 'C3 Strong Typhoon': 'rgb(255, 127, 0)', # Orange
- 'C2 Typhoon': 'rgb(255, 191, 0)', # Orange-Yellow
- 'C1 Typhoon': 'rgb(255, 255, 0)', # Yellow
- 'Tropical Storm': 'rgb(0, 255, 255)', # Cyan
- 'Tropical Depression': 'rgb(173, 216, 230)' # Light Blue
-}
-
-def convert_typhoondata(input_file, output_file):
- with open(input_file, 'r') as infile:
- # Skip the title and the unit line.
- next(infile)
- next(infile)
-
- reader = csv.reader(infile)
-
- # Used for storing data for each SID
- sid_data = defaultdict(list)
-
- for row in reader:
- if not row: # Skip the blank lines
- continue
-
- sid = row[0]
- iso_time = row[6]
- sid_data[sid].append((row, iso_time))
-
- with open(output_file, 'w', newline='') as outfile:
- fieldnames = ['SID', 'ISO_TIME', 'LAT', 'LON', 'SEASON', 'NAME', 'WMO_WIND', 'WMO_PRES', 'USA_WIND', 'USA_PRES', 'START_DATE', 'END_DATE']
- writer = csv.DictWriter(outfile, fieldnames=fieldnames)
-
- writer.writeheader()
-
- for sid, data in sid_data.items():
- start_date = min(data, key=lambda x: x[1])[1]
- end_date = max(data, key=lambda x: x[1])[1]
-
- for row, iso_time in data:
- writer.writerow({
- 'SID': row[0],
- 'ISO_TIME': iso_time,
- 'LAT': row[8],
- 'LON': row[9],
- 'SEASON': row[1],
- 'NAME': row[5],
- 'WMO_WIND': row[10].strip() or ' ',
- 'WMO_PRES': row[11].strip() or ' ',
- 'USA_WIND': row[23].strip() or ' ',
- 'USA_PRES': row[24].strip() or ' ',
- 'START_DATE': start_date,
- 'END_DATE': end_date
- })
-
-
-def download_oni_file(url, filename):
- print(f"Downloading file from {url}...")
- try:
- response = requests.get(url)
- response.raise_for_status() # Raises an exception for non-200 status codes
- with open(filename, 'wb') as f:
- f.write(response.content)
- print(f"File successfully downloaded and saved as {filename}")
- return True
- except requests.RequestException as e:
- print(f"Download failed. Error: {e}")
- return False
-
-
-def convert_oni_ascii_to_csv(input_file, output_file):
- data = defaultdict(lambda: [''] * 12)
- season_to_month = {
- 'DJF': 12, 'JFM': 1, 'FMA': 2, 'MAM': 3, 'AMJ': 4, 'MJJ': 5,
- 'JJA': 6, 'JAS': 7, 'ASO': 8, 'SON': 9, 'OND': 10, 'NDJ': 11
- }
-
- print(f"Attempting to read file: {input_file}")
- try:
- with open(input_file, 'r') as f:
- lines = f.readlines()
- print(f"Successfully read {len(lines)} lines")
-
- if len(lines) <= 1:
- print("Error: File is empty or contains only header")
- return
-
- for line in lines[1:]: # Skip header
- parts = line.split()
- if len(parts) >= 4:
- season, year = parts[0], parts[1]
- anom = parts[-1]
-
- if season in season_to_month:
- month = season_to_month[season]
-
- if season == 'DJF':
- year = str(int(year) - 1)
-
- data[year][month-1] = anom
- else:
- print(f"Warning: Unknown season: {season}")
- else:
- print(f"Warning: Skipping invalid line: {line.strip()}")
-
- print(f"Processed data for {len(data)} years")
- except Exception as e:
- print(f"Error reading file: {e}")
- return
-
- print(f"Attempting to write file: {output_file}")
- try:
- with open(output_file, 'w', newline='') as f:
- writer = csv.writer(f)
- writer.writerow(['Year', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'])
-
- for year in sorted(data.keys()):
- row = [year] + data[year]
- writer.writerow(row)
-
- print(f"Successfully wrote {len(data)} rows of data")
- except Exception as e:
- print(f"Error writing file: {e}")
- return
-
- print(f"Conversion complete. Data saved to {output_file}")
-
-def update_oni_data():
- global last_oni_update
- current_date = date.today()
-
- # Check if already updated today
- if last_oni_update == current_date:
- print("ONI data already checked today. Skipping update.")
- return
-
- url = "https://www.cpc.ncep.noaa.gov/data/indices/oni.ascii.txt"
- temp_file = os.path.join(DATA_PATH, "temp_oni.ascii.txt")
- input_file = os.path.join(DATA_PATH, "oni.ascii.txt")
- output_file = ONI_DATA_PATH
-
- if download_oni_file(url, temp_file):
- if not os.path.exists(input_file) or not filecmp.cmp(temp_file, input_file, shallow=False):
- # File doesn't exist or has been updated
- os.replace(temp_file, input_file)
- print("New ONI data detected. Converting to CSV.")
- convert_oni_ascii_to_csv(input_file, output_file)
- print("ONI data updated successfully.")
- else:
- print("ONI data is up to date. No conversion needed.")
- os.remove(temp_file) # Remove temporary file
-
- last_oni_update = current_date
- else:
- print("Failed to download ONI data.")
- if os.path.exists(temp_file):
- os.remove(temp_file) # Ensure cleanup of temporary file
-
-def load_ibtracs_data():
- if os.path.exists(CACHE_FILE):
- cache_time = datetime.fromtimestamp(os.path.getmtime(CACHE_FILE))
- if datetime.now() - cache_time < timedelta(days=CACHE_EXPIRY_DAYS):
- print("Loading data from cache...")
- with open(CACHE_FILE, 'rb') as f:
- return pickle.load(f)
-
- if os.path.exists(LOCAL_iBtrace_PATH):
- print("Using local IBTrACS file...")
- ibtracs = tracks.TrackDataset(basin='west_pacific', source='ibtracs', ibtracs_url=LOCAL_iBtrace_PATH)
- else:
- print("Local IBTrACS file not found. Fetching data from remote server...")
- try:
- response = requests.get(iBtrace_uri)
- response.raise_for_status()
-
- with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.csv') as temp_file:
- temp_file.write(response.text)
- temp_file_path = temp_file.name
-
- # Save the downloaded data as the local file
- shutil.move(temp_file_path, LOCAL_iBtrace_PATH)
- print(f"Downloaded data saved to {LOCAL_iBtrace_PATH}")
-
- ibtracs = tracks.TrackDataset(basin='west_pacific', source='ibtracs', ibtracs_url=LOCAL_iBtrace_PATH)
- except requests.RequestException as e:
- print(f"Error downloading data: {e}")
- print("No local file available and download failed. Unable to load IBTrACS data.")
- return None
-
- with open(CACHE_FILE, 'wb') as f:
- pickle.dump(ibtracs, f)
-
- return ibtracs
-
-def update_ibtracs_data():
- global ibtracs
- print("Checking for IBTrACS data updates...")
-
- try:
- # Get the last-modified time of the remote file
- response = requests.head(iBtrace_uri)
- remote_last_modified = datetime.strptime(response.headers['Last-Modified'], '%a, %d %b %Y %H:%M:%S GMT')
-
- # Get the last-modified time of the local file
- if os.path.exists(LOCAL_iBtrace_PATH):
- local_last_modified = datetime.fromtimestamp(os.path.getmtime(LOCAL_iBtrace_PATH))
- else:
- local_last_modified = datetime.min
-
- # Compare the modification times
- if remote_last_modified <= local_last_modified:
- print("Local IBTrACS data is up to date. No update needed.")
- if os.path.exists(CACHE_FILE):
- # Update the cache file's timestamp to extend its validity
- os.utime(CACHE_FILE, None)
- print("Cache file timestamp updated.")
- return
-
- print("Remote data is newer. Updating IBTrACS data...")
-
- # Download the new data
- response = requests.get(iBtrace_uri)
- response.raise_for_status()
-
- with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.csv') as temp_file:
- temp_file.write(response.text)
- temp_file_path = temp_file.name
-
- # Save the downloaded data as the local file
- shutil.move(temp_file_path, LOCAL_iBtrace_PATH)
- print(f"Downloaded data saved to {LOCAL_iBtrace_PATH}")
-
- # Update the last modified time of the local file to match the remote file
- os.utime(LOCAL_iBtrace_PATH, (remote_last_modified.timestamp(), remote_last_modified.timestamp()))
-
- ibtracs = tracks.TrackDataset(basin='west_pacific', source='ibtracs', ibtracs_url=LOCAL_iBtrace_PATH)
-
- with open(CACHE_FILE, 'wb') as f:
- pickle.dump(ibtracs, f)
- print("IBTrACS data updated and cache refreshed.")
-
- except requests.RequestException as e:
- print(f"Error checking or downloading data: {e}")
- if os.path.exists(LOCAL_iBtrace_PATH):
- print("Using existing local file.")
- ibtracs = tracks.TrackDataset(basin='west_pacific', source='ibtracs', ibtracs_url=LOCAL_iBtrace_PATH)
- if os.path.exists(CACHE_FILE):
- # Update the cache file's timestamp even when using existing local file
- os.utime(CACHE_FILE, None)
- print("Cache file timestamp updated.")
- else:
- print("No local file available. Update failed.")
-
-def run_schedule():
- while True:
- schedule.run_pending()
- time.sleep(1)
-
-def analyze_typhoon_generation(merged_data, start_date, end_date):
- filtered_data = merged_data[
- (merged_data['ISO_TIME'] >= start_date) &
- (merged_data['ISO_TIME'] <= end_date)
- ]
-
- filtered_data['ENSO_Phase'] = filtered_data['ONI'].apply(classify_enso_phases)
-
- typhoon_counts = filtered_data['ENSO_Phase'].value_counts().to_dict()
-
- month_counts = filtered_data.groupby(['ENSO_Phase', filtered_data['ISO_TIME'].dt.month]).size().unstack(fill_value=0)
- concentrated_months = month_counts.idxmax(axis=1).to_dict()
-
- return typhoon_counts, concentrated_months
-
-def cache_key_generator(*args, **kwargs):
- key = hashlib.md5()
- for arg in args:
- key.update(str(arg).encode())
- for k, v in sorted(kwargs.items()):
- key.update(str(k).encode())
- key.update(str(v).encode())
- return key.hexdigest()
-
-def categorize_typhoon(wind_speed):
- wind_speed_kt = wind_speed / 2 # Convert kt to m/s
-
- # Add category classification
- if wind_speed_kt >= 137/2.35:
- return 'C5 Super Typhoon'
- elif wind_speed_kt >= 113/2.35:
- return 'C4 Very Strong Typhoon'
- elif wind_speed_kt >= 96/2.35:
- return 'C3 Strong Typhoon'
- elif wind_speed_kt >= 83/2.35:
- return 'C2 Typhoon'
- elif wind_speed_kt >= 64/2.35:
- return 'C1 Typhoon'
- elif wind_speed_kt >= 34/2.35:
- return 'Tropical Storm'
- else:
- return 'Tropical Depression'
-
-@functools.lru_cache(maxsize=None)
-def process_oni_data_cached(oni_data_hash):
- return process_oni_data(oni_data)
-
-def process_oni_data(oni_data):
- oni_long = oni_data.melt(id_vars=['Year'], var_name='Month', value_name='ONI')
- oni_long['Month'] = oni_long['Month'].map({
- 'Jan': '01', 'Feb': '02', 'Mar': '03', 'Apr': '04', 'May': '05', 'Jun': '06',
- 'Jul': '07', 'Aug': '08', 'Sep': '09', 'Oct': '10', 'Nov': '11', 'Dec': '12'
- })
- oni_long['Date'] = pd.to_datetime(oni_long['Year'].astype(str) + '-' + oni_long['Month'] + '-01')
- oni_long['ONI'] = pd.to_numeric(oni_long['ONI'], errors='coerce')
- return oni_long
-
-def process_oni_data_with_cache(oni_data):
- oni_data_hash = cache_key_generator(oni_data.to_json())
- return process_oni_data_cached(oni_data_hash)
-
-@functools.lru_cache(maxsize=None)
-def process_typhoon_data_cached(typhoon_data_hash):
- return process_typhoon_data(typhoon_data)
-
-def process_typhoon_data(typhoon_data):
- typhoon_data['ISO_TIME'] = pd.to_datetime(typhoon_data['ISO_TIME'], errors='coerce')
- typhoon_data['USA_WIND'] = pd.to_numeric(typhoon_data['USA_WIND'], errors='coerce')
- typhoon_data['USA_PRES'] = pd.to_numeric(typhoon_data['USA_PRES'], errors='coerce')
- typhoon_data['LON'] = pd.to_numeric(typhoon_data['LON'], errors='coerce')
-
- typhoon_max = typhoon_data.groupby('SID').agg({
- 'USA_WIND': 'max',
- 'USA_PRES': 'min',
- 'ISO_TIME': 'first',
- 'SEASON': 'first',
- 'NAME': 'first',
- 'LAT': 'first',
- 'LON': 'first'
- }).reset_index()
-
- typhoon_max['Month'] = typhoon_max['ISO_TIME'].dt.strftime('%m')
- typhoon_max['Year'] = typhoon_max['ISO_TIME'].dt.year
- typhoon_max['Category'] = typhoon_max['USA_WIND'].apply(categorize_typhoon)
- return typhoon_max
-
-def process_typhoon_data_with_cache(typhoon_data):
- typhoon_data_hash = cache_key_generator(typhoon_data.to_json())
- return process_typhoon_data_cached(typhoon_data_hash)
-
-def merge_data(oni_long, typhoon_max):
- return pd.merge(typhoon_max, oni_long, on=['Year', 'Month'])
-
-def calculate_logistic_regression(merged_data):
- data = merged_data.dropna(subset=['USA_WIND', 'ONI'])
-
- # Create binary outcome for severe typhoons
- data['severe_typhoon'] = (data['USA_WIND'] >= 51).astype(int)
-
- # Create binary predictor for El Niño
- data['el_nino'] = (data['ONI'] >= 0.5).astype(int)
-
- X = data['el_nino']
- X = sm.add_constant(X) # Add constant term
- y = data['severe_typhoon']
-
- model = sm.Logit(y, X).fit()
-
- beta_1 = model.params['el_nino']
- exp_beta_1 = np.exp(beta_1)
- p_value = model.pvalues['el_nino']
-
- return beta_1, exp_beta_1, p_value
-
-@cachetools.cached(cache={})
-def fetch_oni_data_from_csv(file_path):
- df = pd.read_csv(file_path, sep=',', header=0, na_values='-99.90')
- df.columns = ['Year', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
- df = df.melt(id_vars=['Year'], var_name='Month', value_name='ONI')
- df['Date'] = pd.to_datetime(df['Year'].astype(str) + df['Month'], format='%Y%b')
- df = df.set_index('Date')
- return df
-
-def classify_enso_phases(oni_value):
- if isinstance(oni_value, pd.Series):
- oni_value = oni_value.iloc[0]
- if oni_value >= 0.5:
- return 'El Nino'
- elif oni_value <= -0.5:
- return 'La Nina'
- else:
- return 'Neutral'
-
-def load_data(oni_data_path, typhoon_data_path):
- oni_data = pd.read_csv(oni_data_path)
- typhoon_data = pd.read_csv(typhoon_data_path, low_memory=False)
-
- typhoon_data['ISO_TIME'] = pd.to_datetime(typhoon_data['ISO_TIME'], errors='coerce')
-
- typhoon_data = typhoon_data.dropna(subset=['ISO_TIME'])
-
- print(f"Typhoon data shape after cleaning: {typhoon_data.shape}")
- print(f"Year range: {typhoon_data['ISO_TIME'].dt.year.min()} - {typhoon_data['ISO_TIME'].dt.year.max()}")
-
- return oni_data, typhoon_data
-
-def preprocess_data(oni_data, typhoon_data):
- typhoon_data['USA_WIND'] = pd.to_numeric(typhoon_data['USA_WIND'], errors='coerce')
- typhoon_data['WMO_PRES'] = pd.to_numeric(typhoon_data['WMO_PRES'], errors='coerce')
- typhoon_data['ISO_TIME'] = pd.to_datetime(typhoon_data['ISO_TIME'], errors='coerce')
- typhoon_data['Year'] = typhoon_data['ISO_TIME'].dt.year
- typhoon_data['Month'] = typhoon_data['ISO_TIME'].dt.month
-
- monthly_max_wind_speed = typhoon_data.groupby(['Year', 'Month'])['USA_WIND'].max().reset_index()
-
- oni_data_long = pd.melt(oni_data, id_vars=['Year'], var_name='Month', value_name='ONI')
- oni_data_long['Month'] = oni_data_long['Month'].apply(lambda x: pd.to_datetime(x, format='%b').month)
-
- merged_data = pd.merge(monthly_max_wind_speed, oni_data_long, on=['Year', 'Month'])
-
- return merged_data
-
-def calculate_max_wind_min_pressure(typhoon_data):
- max_wind_speed = typhoon_data['USA_WIND'].max()
- min_pressure = typhoon_data['WMO_PRES'].min()
- return max_wind_speed, min_pressure
-
-@functools.lru_cache(maxsize=None)
-def get_storm_data(storm_id):
- return ibtracs.get_storm(storm_id)
-
-def filter_west_pacific_coordinates(lons, lats):
- mask = (100 <= lons) & (lons <= 180) & (0 <= lats) & (lats <= 40)
- return lons[mask], lats[mask]
-
-def polynomial_exp(x, a, b, c, d):
- return a * x**2 + b * x + c + d * np.exp(x)
-
-def exponential(x, a, b, c):
- return a * np.exp(b * x) + c
-
-def generate_cluster_equations(cluster_center):
- X = cluster_center[:, 0] # Longitudes
- y = cluster_center[:, 1] # Latitudes
-
- x_min = X.min()
- x_max = X.max()
-
- equations = []
-
- # Fourier Series (up to 4th order)
- def fourier_series(x, a0, a1, b1, a2, b2, a3, b3, a4, b4):
- return (a0 + a1*np.cos(x) + b1*np.sin(x) +
- a2*np.cos(2*x) + b2*np.sin(2*x) +
- a3*np.cos(3*x) + b3*np.sin(3*x) +
- a4*np.cos(4*x) + b4*np.sin(4*x))
-
- # Normalize X to the range [0, 2π]
- X_normalized = 2 * np.pi * (X - x_min) / (x_max - x_min)
-
- params, _ = curve_fit(fourier_series, X_normalized, y)
- a0, a1, b1, a2, b2, a3, b3, a4, b4 = params
-
- # Create the equation string
- fourier_eq = (f"y = {a0:.4f} + {a1:.4f}*cos(x) + {b1:.4f}*sin(x) + "
- f"{a2:.4f}*cos(2x) + {b2:.4f}*sin(2x) + "
- f"{a3:.4f}*cos(3x) + {b3:.4f}*sin(3x) + "
- f"{a4:.4f}*cos(4x) + {b4:.4f}*sin(4x)")
-
- equations.append(("Fourier Series", fourier_eq))
- equations.append(("X Range", f"x goes from 0 to {2*np.pi:.4f}"))
- equations.append(("Longitude Range", f"Longitude goes from {x_min:.4f}°E to {x_max:.4f}°E"))
-
- return equations, (x_min, x_max)
-
-#oni_df = fetch_oni_data_from_csv(ONI_DATA_PATH)
-#ibtracs = load_ibtracs_data()
-#oni_data, typhoon_data = load_data(ONI_DATA_PATH, TYPHOON_DATA_PATH)
-#oni_long = process_oni_data_with_cache(oni_data)
-#typhoon_max = process_typhoon_data_with_cache(typhoon_data)
-#merged_data = merge_data(oni_long, typhoon_max)
-#data = preprocess_data(oni_data, typhoon_data)
-#max_wind_speed, min_pressure = calculate_max_wind_min_pressure(typhoon_data)
-#
-## Schedule the update to run daily at 1:00 AM
-#schedule.every().day.at("01:00").do(update_ibtracs_data)
-#
-## Run the scheduler in a separate thread
-#scheduler_thread = threading.Thread(target=run_schedule)
-#scheduler_thread.start()
-
-
-app = dash.Dash(__name__)
-
-# First, add the classification standards
-atlantic_standard = {
- 'C5 Super Typhoon': {'wind_speed': 137, 'color': 'rgb(255, 0, 0)'},
- 'C4 Very Strong Typhoon': {'wind_speed': 113, 'color': 'rgb(255, 63, 0)'},
- 'C3 Strong Typhoon': {'wind_speed': 96, 'color': 'rgb(255, 127, 0)'},
- 'C2 Typhoon': {'wind_speed': 83, 'color': 'rgb(255, 191, 0)'},
- 'C1 Typhoon': {'wind_speed': 64, 'color': 'rgb(255, 255, 0)'},
- 'Tropical Storm': {'wind_speed': 34, 'color': 'rgb(0, 255, 255)'},
- 'Tropical Depression': {'wind_speed': 0, 'color': 'rgb(173, 216, 230)'}
-}
-
-taiwan_standard = {
- 'Strong Typhoon': {'wind_speed': 51.0, 'color': 'rgb(255, 0, 0)'}, # >= 51.0 m/s
- 'Medium Typhoon': {'wind_speed': 33.7, 'color': 'rgb(255, 127, 0)'}, # 33.7-50.9 m/s
- 'Mild Typhoon': {'wind_speed': 17.2, 'color': 'rgb(255, 255, 0)'}, # 17.2-33.6 m/s
- 'Tropical Depression': {'wind_speed': 0, 'color': 'rgb(173, 216, 230)'} # < 17.2 m/s
-}
-
-app.layout = html.Div([
- html.H1("Typhoon Analysis Dashboard"),
-
- html.Div([
- dcc.Input(id='start-year', type='number', placeholder='Start Year', value=2000, min=1900, max=2024, step=1),
- dcc.Input(id='start-month', type='number', placeholder='Start Month', value=1, min=1, max=12, step=1),
- dcc.Input(id='end-year', type='number', placeholder='End Year', value=2024, min=1900, max=2024, step=1),
- dcc.Input(id='end-month', type='number', placeholder='End Month', value=6, min=1, max=12, step=1),
- dcc.Dropdown(
- id='enso-dropdown',
- options=[
- {'label': 'All Years', 'value': 'all'},
- {'label': 'El Niño Years', 'value': 'el_nino'},
- {'label': 'La Niña Years', 'value': 'la_nina'},
- {'label': 'Neutral Years', 'value': 'neutral'}
- ],
- value='all'
- ),
- html.Button('Analyze', id='analyze-button', n_clicks=0),
- ]),
-
- html.Div([
- dcc.Input(id='typhoon-search', type='text', placeholder='Search Typhoon Name'),
- html.Button('Find Typhoon', id='find-typhoon-button', n_clicks=0),
- ]),
-
- html.Div([
- html.Div(id='correlation-coefficient'),
- html.Div(id='max-wind-speed'),
- html.Div(id='min-pressure'),
- ]),
-
- dcc.Graph(id='typhoon-tracks-graph'),
- html.Div([
- html.P("Number of Clusters"),
- dcc.Input(id='n-clusters', type='number', placeholder='Number of Clusters', value=5, min=1, max=20, step=1),
- html.Button('Show Clusters', id='show-clusters-button', n_clicks=0),
- html.Button('Show Typhoon Routes', id='show-routes-button', n_clicks=0),
- ]),
-
- dcc.Graph(id='typhoon-routes-graph'),
-
- html.Div([
- html.Button('Fourier Series', id='fourier-series-button', n_clicks=0),
- ]),
- html.Div(id='cluster-equation-results'),
-
- html.Div([
- html.Button('Wind Speed Logistic Regression', id='wind-regression-button', n_clicks=0),
- html.Button('Pressure Logistic Regression', id='pressure-regression-button', n_clicks=0),
- html.Button('Longitude Logistic Regression', id='longitude-regression-button', n_clicks=0),
- ]),
- html.Div(id='logistic-regression-results'),
-
- html.H2("Typhoon Path Analysis"),
- html.Div([
- dcc.Dropdown(
- id='year-dropdown',
- options=[{'label': str(year), 'value': year} for year in range(1950, 2025)],
- value=2024,
- style={'width': '200px'}
- ),
- dcc.Dropdown(
- id='typhoon-dropdown',
- style={'width': '300px'}
- ),
- dcc.Dropdown(
- id='classification-standard',
- options=[
- {'label': 'Atlantic Standard', 'value': 'atlantic'},
- {'label': 'Taiwan Standard', 'value': 'taiwan'}
- ],
- value='atlantic',
- style={'width': '200px'}
- )
- ], style={'display': 'flex', 'gap': '10px'}),
-
- dcc.Graph(id='typhoon-path-animation'),
- dcc.Graph(id='all-years-regression-graph'),
- dcc.Graph(id='wind-oni-scatter-plot'),
- dcc.Graph(id='pressure-oni-scatter'),
-
- html.Div(id='regression-graphs'),
- html.Div(id='slopes'),
- html.Div([
- html.H3("Correlation Analysis"),
- html.Div(id='wind-oni-correlation'),
- html.Div(id='pressure-oni-correlation'),
- ]),
- html.Div([
- html.H3("Typhoon Generation Analysis"),
- html.Div(id='typhoon-count-analysis'),
- html.Div(id='concentrated-months-analysis'),
- ]),
- html.Div(id='cluster-info'),
-
- html.Div([
- dcc.Dropdown(
- id='classification-standard',
- options=[
- {'label': 'Atlantic Standard', 'value': 'atlantic'},
- {'label': 'Taiwan Standard', 'value': 'taiwan'}
- ],
- value='atlantic',
- style={'width': '200px'}
- )
- ], style={'margin': '10px'}),
-
-], style={'font-family': 'Arial, sans-serif'})
-
-@app.callback(
- Output('year-dropdown', 'options'),
- Input('typhoon-tracks-graph', 'figure')
-)
-def initialize_year_dropdown(_):
- try:
- years = typhoon_data['ISO_TIME'].dt.year.unique()
- years = years[~np.isnan(years)]
- years = sorted(years)
-
- options = [{'label': str(int(year)), 'value': int(year)} for year in years]
- print(f"Generated options: {options[:5]}...")
- return options
- except Exception as e:
- print(f"Error in initialize_year_dropdown: {str(e)}")
- return [{'label': 'Error', 'value': 'error'}]
-
-@app.callback(
- [Output('typhoon-dropdown', 'options'),
- Output('typhoon-dropdown', 'value')],
- [Input('year-dropdown', 'value')]
-)
-def update_typhoon_dropdown(selected_year):
- if not selected_year:
- raise PreventUpdate
-
- selected_year = int(selected_year)
-
- season = ibtracs.get_season(selected_year)
- storm_summary = season.summary()
-
- typhoon_options = []
- for i in range(storm_summary['season_storms']):
- storm_id = storm_summary['id'][i]
- storm_name = storm_summary['name'][i]
- typhoon_options.append({'label': f"{storm_name} ({storm_id})", 'value': storm_id})
-
- selected_typhoon = typhoon_options[0]['value'] if typhoon_options else None
- return typhoon_options, selected_typhoon
-
-@app.callback(
- Output('typhoon-path-animation', 'figure'),
- [Input('year-dropdown', 'value'),
- Input('typhoon-dropdown', 'value'),
- Input('classification-standard', 'value')]
-)
-def update_typhoon_path(selected_year, selected_sid, standard):
- if not selected_year or not selected_sid:
- raise PreventUpdate
-
- storm = ibtracs.get_storm(selected_sid)
- return create_typhoon_path_figure(storm, selected_year, standard)
-
-def create_typhoon_path_figure(storm, selected_year, standard='atlantic'):
- fig = go.Figure()
-
- fig.add_trace(
- go.Scattergeo(
- lon=storm.lon,
- lat=storm.lat,
- mode='lines',
- line=dict(width=2, color='gray'),
- name='Path',
- showlegend=False,
- )
- )
-
- fig.add_trace(
- go.Scattergeo(
- lon=[storm.lon[0]],
- lat=[storm.lat[0]],
- mode='markers',
- marker=dict(size=10, color='green', symbol='star'),
- name='Starting Point',
- text=storm.time[0].strftime('%Y-%m-%d %H:%M'),
- hoverinfo='text+name',
- )
- )
-
- frames = []
- for i in range(len(storm.time)):
- category, color = categorize_typhoon_by_standard(storm.vmax[i], standard)
-
- r34_ne = storm.dict['USA_R34_NE'][i] if 'USA_R34_NE' in storm.dict else None
- r34_se = storm.dict['USA_R34_SE'][i] if 'USA_R34_SE' in storm.dict else None
- r34_sw = storm.dict['USA_R34_SW'][i] if 'USA_R34_SW' in storm.dict else None
- r34_nw = storm.dict['USA_R34_NW'][i] if 'USA_R34_NW' in storm.dict else None
- rmw = storm.dict['USA_RMW'][i] if 'USA_RMW' in storm.dict else None
- eye_diameter = storm.dict['USA_EYE'][i] if 'USA_EYE' in storm.dict else None
-
- radius_info = f"R34: NE={r34_ne}, SE={r34_se}, SW={r34_sw}, NW={r34_nw}
"
- radius_info += f"RMW: {rmw}
"
- radius_info += f"Eye Diameter: {eye_diameter}"
-
- frame_data = [
- go.Scattergeo(
- lon=storm.lon[:i+1],
- lat=storm.lat[:i+1],
- mode='lines',
- line=dict(width=2, color='blue'),
- name='Path Traveled',
- showlegend=False,
- ),
- go.Scattergeo(
- lon=[storm.lon[i]],
- lat=[storm.lat[i]],
- mode='markers+text',
- marker=dict(size=10, color=color, symbol='star'),
- text=category,
- textposition="top center",
- textfont=dict(size=12, color=color),
- name='Current Location',
- hovertext=f"{storm.time[i].strftime('%Y-%m-%d %H:%M')}
"
- f"Category: {category}
"
- f"Wind Speed: {storm.vmax[i]:.1f} m/s
"
- f"{radius_info}",
- hoverinfo='text',
- ),
- ]
- frames.append(go.Frame(data=frame_data, name=f"frame{i}"))
-
- fig.frames = frames
-
- fig.update_layout(
- title=f"{selected_year} Year {storm.name} Typhoon Path",
- showlegend=False,
- geo=dict(
- projection_type='natural earth',
- showland=True,
- landcolor='rgb(243, 243, 243)',
- countrycolor='rgb(204, 204, 204)',
- coastlinecolor='rgb(100, 100, 100)',
- showocean=True,
- oceancolor='rgb(230, 250, 255)',
- ),
- updatemenus=[{
- "buttons": [
- {
- "args": [None, {"frame": {"duration": 100, "redraw": True},
- "fromcurrent": True,
- "transition": {"duration": 0}}],
- "label": "Play",
- "method": "animate"
- },
- {
- "args": [[None], {"frame": {"duration": 0, "redraw": True},
- "mode": "immediate",
- "transition": {"duration": 0}}],
- "label": "Pause",
- "method": "animate"
- }
- ],
- "direction": "left",
- "pad": {"r": 10, "t": 87},
- "showactive": False,
- "type": "buttons",
- "x": 0.1,
- "xanchor": "right",
- "y": 0,
- "yanchor": "top"
- }],
- sliders=[{
- "active": 0,
- "yanchor": "top",
- "xanchor": "left",
- "currentvalue": {
- "font": {"size": 20},
- "prefix": "Time: ",
- "visible": True,
- "xanchor": "right"
- },
- "transition": {"duration": 100, "easing": "cubic-in-out"},
- "pad": {"b": 10, "t": 50},
- "len": 0.9,
- "x": 0.1,
- "y": 0,
- "steps": [
- {
- "args": [[f"frame{k}"],
- {"frame": {"duration": 100, "redraw": True},
- "mode": "immediate",
- "transition": {"duration": 0}}
- ],
- "label": storm.time[k].strftime('%Y-%m-%d %H:%M'),
- "method": "animate"
- }
- for k in range(len(storm.time))
- ]
- }]
- )
-
- return fig
-
-@app.callback(
- [Output('typhoon-routes-graph', 'figure'),
- Output('cluster-equation-results', 'children')],
- [Input('analyze-button', 'n_clicks'),
- Input('show-clusters-button', 'n_clicks'),
- Input('show-routes-button', 'n_clicks'),
- Input('fourier-series-button', 'n_clicks')],
- [State('start-year', 'value'),
- State('start-month', 'value'),
- State('end-year', 'value'),
- State('end-month', 'value'),
- State('n-clusters', 'value'),
- State('enso-dropdown', 'value')]
-)
-
-def update_route_clusters(analyze_clicks, show_clusters_clicks, show_routes_clicks,
- fourier_clicks, start_year, start_month, end_year, end_month,
- n_clusters, enso_value):
- ctx = dash.callback_context
- button_id = ctx.triggered[0]['prop_id'].split('.')[0]
-
- start_date = datetime(start_year, start_month, 1)
- end_date = datetime(end_year, end_month, 28)
-
- filtered_oni_df = oni_df[(oni_df.index >= start_date) & (oni_df.index <= end_date)]
-
- fig_routes = go.Figure()
-
- clusters = np.array([]) # Initialize as empty NumPy array
- cluster_equations = []
-
- # Clustering analysis
- west_pacific_storms = []
- for year in range(start_year, end_year + 1):
- season = ibtracs.get_season(year)
- for storm_id in season.summary()['id']:
- storm = get_storm_data(storm_id)
- storm_date = storm.time[0]
- storm_oni = oni_df.loc[storm_date.strftime('%Y-%b')]['ONI']
- if isinstance(storm_oni, pd.Series):
- storm_oni = storm_oni.iloc[0]
- storm_phase = classify_enso_phases(storm_oni)
-
- if enso_value == 'all' or \
- (enso_value == 'el_nino' and storm_phase == 'El Nino') or \
- (enso_value == 'la_nina' and storm_phase == 'La Nina') or \
- (enso_value == 'neutral' and storm_phase == 'Neutral'):
- lons, lats = filter_west_pacific_coordinates(np.array(storm.lon), np.array(storm.lat))
- if len(lons) > 1: # Ensure the storm has a valid path in West Pacific
- west_pacific_storms.append((lons, lats))
-
- max_length = max(len(storm[0]) for storm in west_pacific_storms)
- standardized_routes = []
-
- for lons, lats in west_pacific_storms:
- if len(lons) < 2: # Skip if not enough points
- continue
- t = np.linspace(0, 1, len(lons))
- t_new = np.linspace(0, 1, max_length)
- lon_interp = interp1d(t, lons, kind='linear')(t_new)
- lat_interp = interp1d(t, lats, kind='linear')(t_new)
- route_vector = np.column_stack((lon_interp, lat_interp)).flatten()
- standardized_routes.append(route_vector)
-
- kmeans = KMeans(n_clusters=n_clusters, random_state=42, n_init=10)
- clusters = kmeans.fit_predict(standardized_routes)
-
- # Count the number of typhoons in each cluster
- cluster_counts = np.bincount(clusters)
-
- for lons, lats in west_pacific_storms:
- fig_routes.add_trace(go.Scattergeo(
- lon=lons, lat=lats,
- mode='lines',
- line=dict(width=1, color='lightgray'),
- showlegend=False,
- hoverinfo='none',
- visible=(button_id == 'show-routes-button')
- ))
-
- equations_output = []
- for i in range(n_clusters):
- cluster_center = kmeans.cluster_centers_[i].reshape(-1, 2)
- cluster_equations, (lon_min, lon_max) = generate_cluster_equations(cluster_center)
-
- #equations_output.append(html.H4(f"Cluster {i+1} (Typhoons: {cluster_counts[i]})"))
- equations_output.append(html.H4([
- f"Cluster {i+1} (Typhoons: ",
- html.Span(f"{cluster_counts[i]}", style={'color': 'blue'}),
- ")"
- ]))
- for name, eq in cluster_equations:
- equations_output.append(html.P(f"{name}: {eq}"))
-
- equations_output.append(html.P("To use in GeoGebra:"))
- equations_output.append(html.P(f"1. Set x-axis from 0 to {2*np.pi:.4f}"))
- equations_output.append(html.P(f"2. Use the equation as is"))
- 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}"))
- equations_output.append(html.Hr())
-
- fig_routes.add_trace(go.Scattergeo(
- lon=cluster_center[:, 0],
- lat=cluster_center[:, 1],
- mode='lines',
- name=f'Cluster {i+1} (n={cluster_counts[i]})',
- line=dict(width=3),
- visible=(button_id == 'show-clusters-button')
- ))
-
- enso_phase_text = {
- 'all': 'All Years',
- 'el_nino': 'El Niño Years',
- 'la_nina': 'La Niña Years',
- 'neutral': 'Neutral Years'
- }
- fig_routes.update_layout(
- title=f'Typhoon Routes Clustering in West Pacific ({start_year}-{end_year}) - {enso_phase_text[enso_value]}',
- geo=dict(
- projection_type='mercator',
- showland=True,
- landcolor='rgb(243, 243, 243)',
- countrycolor='rgb(204, 204, 204)',
- coastlinecolor='rgb(100, 100, 100)',
- showocean=True,
- oceancolor='rgb(230, 250, 255)',
- lataxis={'range': [0, 40]},
- lonaxis={'range': [100, 180]},
- center={'lat': 20, 'lon': 140},
- ),
- legend_title='Clusters'
- )
-
- return fig_routes, html.Div(equations_output)
-
-@app.callback(
- [Output('typhoon-tracks-graph', 'figure'),
- Output('all-years-regression-graph', 'figure'),
- Output('regression-graphs', 'children'),
- Output('slopes', 'children'),
- Output('wind-oni-scatter-plot', 'figure'),
- Output('pressure-oni-scatter', 'figure'),
- Output('correlation-coefficient', 'children'),
- Output('max-wind-speed', 'children'),
- Output('min-pressure', 'children'),
- Output('wind-oni-correlation', 'children'),
- Output('pressure-oni-correlation', 'children'),
- Output('typhoon-count-analysis', 'children'),
- Output('concentrated-months-analysis', 'children')],
- [Input('analyze-button', 'n_clicks'),
- Input('find-typhoon-button', 'n_clicks')],
- [State('start-year', 'value'),
- State('start-month', 'value'),
- State('end-year', 'value'),
- State('end-month', 'value'),
- State('enso-dropdown', 'value'),
- State('typhoon-search', 'value')]
-)
-
-def update_graphs(analyze_clicks, find_typhoon_clicks,
- start_year, start_month, end_year, end_month,
- enso_value, typhoon_search):
- ctx = dash.callback_context
- button_id = ctx.triggered[0]['prop_id'].split('.')[0]
-
- start_date = datetime(start_year, start_month, 1)
- end_date = datetime(end_year, end_month, 28)
-
- filtered_oni_df = oni_df[(oni_df.index >= start_date) & (oni_df.index <= end_date)]
-
-
- regression_data = {'El Nino': {'longitudes': [], 'oni_values': [], 'names': []},
- 'La Nina': {'longitudes': [], 'oni_values': [], 'names': []},
- 'Neutral': {'longitudes': [], 'oni_values': [], 'names': []},
- 'All': {'longitudes': [], 'oni_values': [], 'names': []}}
-
- fig_tracks = go.Figure()
-
- def process_storm(year, storm_id):
- storm = get_storm_data(storm_id)
- storm_dates = storm.time
- if any(start_date <= date <= end_date for date in storm_dates):
- storm_oni = filtered_oni_df.loc[storm_dates[0].strftime('%Y-%b')]['ONI']
- if isinstance(storm_oni, pd.Series):
- storm_oni = storm_oni.iloc[0]
- phase = classify_enso_phases(storm_oni)
-
- regression_data[phase]['longitudes'].append(storm.lon[0])
- regression_data[phase]['oni_values'].append(storm_oni)
- regression_data[phase]['names'].append(f'{storm.name} ({year})')
- regression_data['All']['longitudes'].append(storm.lon[0])
- regression_data['All']['oni_values'].append(storm_oni)
- regression_data['All']['names'].append(f'{storm.name} ({year})')
-
- if (enso_value == 'all' or
- (enso_value == 'el_nino' and phase == 'El Nino') or
- (enso_value == 'la_nina' and phase == 'La Nina') or
- (enso_value == 'neutral' and phase == 'Neutral')):
- color = {'El Nino': 'red', 'La Nina': 'blue', 'Neutral': 'green'}[phase]
- return go.Scattergeo(
- lon=storm.lon,
- lat=storm.lat,
- mode='lines',
- name=storm.name,
- text=f'{storm.name} ({year})',
- hoverinfo='text',
- line=dict(width=2, color=color)
- )
- return None
-
- with ThreadPoolExecutor() as executor:
- futures = []
- for year in range(start_year, end_year + 1):
- season = ibtracs.get_season(year)
- for storm_id in season.summary()['id']:
- futures.append(executor.submit(process_storm, year, storm_id))
-
- for future in futures:
- result = future.result()
- if result:
- fig_tracks.add_trace(result)
-
- fig_tracks.update_layout(
- title=f'Typhoon Tracks from {start_year}-{start_month} to {end_year}-{end_month}',
- geo=dict(
- projection_type='natural earth',
- showland=True,
- )
- )
-
- regression_figs = []
- slopes = []
- all_years_fig = go.Figure() # Initialize with an empty figure
-
- for phase in ['El Nino', 'La Nina', 'Neutral', 'All']:
- df = pd.DataFrame({
- 'Longitude': regression_data[phase]['longitudes'],
- 'ONI': regression_data[phase]['oni_values'],
- 'Name': regression_data[phase]['names']
- })
-
- if not df.empty and len(df) > 1: # Ensure there's enough data for regression
- try:
- fig = px.scatter(df, x='Longitude', y='ONI', hover_data=['Name'],
- labels={'Longitude': 'Longitude of Typhoon Generation', 'ONI': 'ONI Value'},
- title=f'Typhoon Generation Location vs. ONI ({phase})')
-
- X = np.array(df['Longitude']).reshape(-1, 1)
- y = df['ONI']
- model = LinearRegression()
- model.fit(X, y)
- y_pred = model.predict(X)
- slope = model.coef_[0]
- intercept = model.intercept_
- fraction_slope = Fraction(slope).limit_denominator()
- equation = f'ONI = {fraction_slope} * Longitude + {Fraction(intercept).limit_denominator()}'
-
- fig.add_trace(go.Scatter(x=df['Longitude'], y=y_pred, mode='lines', name='Regression Line'))
- fig.add_annotation(x=df['Longitude'].mean(), y=y_pred.mean(),
- text=equation, showarrow=False, yshift=10)
-
- if phase == 'All':
- all_years_fig = fig
- else:
- regression_figs.append(dcc.Graph(figure=fig))
-
- correlation_coef = np.corrcoef(df['Longitude'], df['ONI'])[0, 1]
- slopes.append(html.P(f'{phase} Regression Slope: {slope:.4f}, Correlation Coefficient: {correlation_coef:.4f}'))
- except Exception as e:
- print(f"Error in regression analysis for {phase}: {str(e)}")
- if phase != 'All':
- regression_figs.append(html.Div(f"Error in analysis for {phase}"))
- slopes.append(html.P(f'{phase} Regression: Error in analysis'))
- else:
- if phase != 'All':
- regression_figs.append(html.Div(f"Insufficient data for {phase}"))
- slopes.append(html.P(f'{phase} Regression: Insufficient data'))
-
- if all_years_fig.data == ():
- all_years_fig = go.Figure()
- all_years_fig.add_annotation(text="No data available for regression analysis",
- xref="paper", yref="paper",
- x=0.5, y=0.5, showarrow=False)
-
- if button_id == 'find-typhoon-button' and typhoon_search:
- for trace in fig_tracks.data:
- if typhoon_search.lower() in trace.name.lower():
- trace.line.width = 5
- trace.line.color = 'yellow'
-
- filtered_data = merged_data[
- (merged_data['Year'] >= start_year) &
- (merged_data['Year'] <= end_year) &
- (merged_data['Month'].astype(int) >= start_month) &
- (merged_data['Month'].astype(int) <= end_month)
- ]
-
- wind_oni_scatter = px.scatter(filtered_data, x='ONI', y='USA_WIND', color='Category',
- hover_data=['NAME', 'Year','Category'],
- title='Wind Speed vs ONI',
- labels={'ONI': 'ONI Value', 'USA_WIND': 'Maximum Wind Speed (knots)'},
- color_discrete_map=color_map)
- wind_oni_scatter.update_traces(hovertemplate='%{customdata[0]} (%{customdata[1]})
Category: %{customdata[2]}
ONI: %{x}
Wind Speed: %{y} knots')
-
- pressure_oni_scatter = px.scatter(filtered_data, x='ONI', y='USA_PRES',color='Category',
- hover_data=['NAME', 'Year','Category'],
- title='Pressure vs ONI',
- labels={'ONI': 'ONI Value', 'USA_PRES': 'Minimum Pressure (hPa)'},
- color_discrete_map=color_map)
- pressure_oni_scatter.update_traces(hovertemplate='%{customdata[0]} (%{customdata[1]})
Category: %{customdata[2]}
ONI: %{x}
Pressure: %{y} hPa')
-
- if typhoon_search:
- for fig in [wind_oni_scatter, pressure_oni_scatter]:
- mask = filtered_data['NAME'].str.contains(typhoon_search, case=False, na=False)
- fig.add_trace(go.Scatter(
- x=filtered_data.loc[mask, 'ONI'],
- y=filtered_data.loc[mask, 'USA_WIND' if 'Wind' in fig.layout.title.text else 'USA_PRES'],
- mode='markers',
- marker=dict(size=10, color='red', symbol='star'),
- name=f'Matched: {typhoon_search}',
- hovertemplate='%{text}
Category: %{customdata}
ONI: %{x}
Value: %{y}',
- text=filtered_data.loc[mask, 'NAME'] + ' (' + filtered_data.loc[mask, 'Year'].astype(str) + ')',
- customdata=filtered_data.loc[mask, 'Category']
- ))
-
-
- start_date = datetime(start_year, start_month, 1)
- end_date = datetime(end_year, end_month, 28)
- typhoon_counts, concentrated_months = analyze_typhoon_generation(merged_data, start_date, end_date)
-
- month_names = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
- count_analysis = [html.P(f"{phase}: {count} typhoons") for phase, count in typhoon_counts.items()]
- month_analysis = [html.P(f"{phase}: Most concentrated in {month_names[month-1]}") for phase, month in concentrated_months.items()]
-
- max_wind_speed = filtered_data['USA_WIND'].max()
- min_pressure = typhoon_data[(typhoon_data['ISO_TIME'].dt.year >= start_year) &
- (typhoon_data['ISO_TIME'].dt.year <= end_year)]['WMO_PRES'].min()
-
- correlation_text = f"Logistic Regression Results: see below"
- max_wind_speed_text = f"Maximum Wind Speed: {max_wind_speed:.2f} knots"
- min_pressure_text = f"Minimum Pressure: {min_pressure:.2f} hPa"
-
-
- return (fig_tracks, all_years_fig, regression_figs, slopes,
- wind_oni_scatter, pressure_oni_scatter,
- correlation_text, max_wind_speed_text, min_pressure_text,
- "Wind-ONI correlation: See logistic regression results",
- "Pressure-ONI correlation: See logistic regression results",
- count_analysis, month_analysis)
-
-@app.callback(
- Output('logistic-regression-results', 'children'),
- [Input('wind-regression-button', 'n_clicks'),
- Input('pressure-regression-button', 'n_clicks'),
- Input('longitude-regression-button', 'n_clicks')],
- [State('start-year', 'value'),
- State('start-month', 'value'),
- State('end-year', 'value'),
- State('end-month', 'value')]
-)
-def update_logistic_regression(wind_clicks, pressure_clicks, longitude_clicks,
- start_year, start_month, end_year, end_month):
- ctx = dash.callback_context
- if not ctx.triggered:
- return "Click a button to see logistic regression results."
-
- button_id = ctx.triggered[0]['prop_id'].split('.')[0]
-
- start_date = datetime(start_year, start_month, 1)
- end_date = datetime(end_year, end_month, 28)
-
- filtered_data = merged_data[
- (merged_data['ISO_TIME'] >= start_date) &
- (merged_data['ISO_TIME'] <= end_date)
- ]
-
- if button_id == 'wind-regression-button':
- return calculate_wind_logistic_regression(filtered_data)
- elif button_id == 'pressure-regression-button':
- return calculate_pressure_logistic_regression(filtered_data)
- elif button_id == 'longitude-regression-button':
- return calculate_longitude_logistic_regression(filtered_data)
-
-def calculate_wind_logistic_regression(data):
- data['severe_typhoon'] = (data['USA_WIND'] >= 64).astype(int) # 64 knots threshold for severe typhoons
- X = sm.add_constant(data['ONI'])
- y = data['severe_typhoon']
- model = sm.Logit(y, X).fit()
-
- beta_1 = model.params['ONI']
- exp_beta_1 = np.exp(beta_1)
- p_value = model.pvalues['ONI']
-
- el_nino_data = data[data['ONI'] >= 0.5]
- la_nina_data = data[data['ONI'] <= -0.5]
- neutral_data = data[(data['ONI'] > -0.5) & (data['ONI'] < 0.5)]
-
- el_nino_severe = el_nino_data['severe_typhoon'].mean()
- la_nina_severe = la_nina_data['severe_typhoon'].mean()
- neutral_severe = neutral_data['severe_typhoon'].mean()
-
- return html.Div([
- html.H3("Wind Speed Logistic Regression Results"),
- html.P(f"β1 (ONI coefficient): {beta_1:.4f}"),
- html.P(f"exp(β1) (Odds Ratio): {exp_beta_1:.4f}"),
- html.P(f"P-value: {p_value:.4f}"),
- html.P("Interpretation:"),
- html.Ul([
- html.Li(f"For each unit increase in ONI, the odds of a severe typhoon are "
- f"{'increased' if exp_beta_1 > 1 else 'decreased'} by a factor of {exp_beta_1:.2f}."),
- html.Li(f"This effect is {'statistically significant' if p_value < 0.05 else 'not statistically significant'} "
- f"at the 0.05 level.")
- ]),
- html.P("Proportion of severe typhoons:"),
- html.Ul([
- html.Li(f"El Niño conditions: {el_nino_severe:.2%}"),
- html.Li(f"La Niña conditions: {la_nina_severe:.2%}"),
- html.Li(f"Neutral conditions: {neutral_severe:.2%}")
- ])
- ])
-
-def calculate_pressure_logistic_regression(data):
- data['intense_typhoon'] = (data['USA_PRES'] <= 950).astype(int) # 950 hPa threshold for intense typhoons
- X = sm.add_constant(data['ONI'])
- y = data['intense_typhoon']
- model = sm.Logit(y, X).fit()
-
- beta_1 = model.params['ONI']
- exp_beta_1 = np.exp(beta_1)
- p_value = model.pvalues['ONI']
-
- el_nino_data = data[data['ONI'] >= 0.5]
- la_nina_data = data[data['ONI'] <= -0.5]
- neutral_data = data[(data['ONI'] > -0.5) & (data['ONI'] < 0.5)]
-
- el_nino_intense = el_nino_data['intense_typhoon'].mean()
- la_nina_intense = la_nina_data['intense_typhoon'].mean()
- neutral_intense = neutral_data['intense_typhoon'].mean()
-
- return html.Div([
- html.H3("Pressure Logistic Regression Results"),
- html.P(f"β1 (ONI coefficient): {beta_1:.4f}"),
- html.P(f"exp(β1) (Odds Ratio): {exp_beta_1:.4f}"),
- html.P(f"P-value: {p_value:.4f}"),
- html.P("Interpretation:"),
- html.Ul([
- html.Li(f"For each unit increase in ONI, the odds of an intense typhoon (pressure <= 950 hPa) are "
- f"{'increased' if exp_beta_1 > 1 else 'decreased'} by a factor of {exp_beta_1:.2f}."),
- html.Li(f"This effect is {'statistically significant' if p_value < 0.05 else 'not statistically significant'} "
- f"at the 0.05 level.")
- ]),
- html.P("Proportion of intense typhoons:"),
- html.Ul([
- html.Li(f"El Niño conditions: {el_nino_intense:.2%}"),
- html.Li(f"La Niña conditions: {la_nina_intense:.2%}"),
- html.Li(f"Neutral conditions: {neutral_intense:.2%}")
- ])
- ])
-
-def calculate_longitude_logistic_regression(data):
- # Use only the data points where longitude is available
- data = data.dropna(subset=['LON'])
-
- if len(data) == 0:
- return html.Div("Insufficient data for longitude analysis")
-
- data['western_typhoon'] = (data['LON'] <= 140).astype(int) # 140°E as threshold for western typhoons
- X = sm.add_constant(data['ONI'])
- y = data['western_typhoon']
- model = sm.Logit(y, X).fit()
-
- beta_1 = model.params['ONI']
- exp_beta_1 = np.exp(beta_1)
- p_value = model.pvalues['ONI']
-
- el_nino_data = data[data['ONI'] >= 0.5]
- la_nina_data = data[data['ONI'] <= -0.5]
- neutral_data = data[(data['ONI'] > -0.5) & (data['ONI'] < 0.5)]
-
- el_nino_western = el_nino_data['western_typhoon'].mean()
- la_nina_western = la_nina_data['western_typhoon'].mean()
- neutral_western = neutral_data['western_typhoon'].mean()
-
- return html.Div([
- html.H3("Longitude Logistic Regression Results"),
- html.P(f"β1 (ONI coefficient): {beta_1:.4f}"),
- html.P(f"exp(β1) (Odds Ratio): {exp_beta_1:.4f}"),
- html.P(f"P-value: {p_value:.4f}"),
- html.P("Interpretation:"),
- html.Ul([
- html.Li(f"For each unit increase in ONI, the odds of a typhoon forming west of 140°E are "
- f"{'increased' if exp_beta_1 > 1 else 'decreased'} by a factor of {exp_beta_1:.2f}."),
- html.Li(f"This effect is {'statistically significant' if p_value < 0.05 else 'not statistically significant'} "
- f"at the 0.05 level.")
- ]),
- html.P("Proportion of typhoons forming west of 140°E:"),
- html.Ul([
- html.Li(f"El Niño conditions: {el_nino_western:.2%}"),
- html.Li(f"La Niña conditions: {la_nina_western:.2%}"),
- html.Li(f"Neutral conditions: {neutral_western:.2%}")
- ])
- ])
-
-def categorize_typhoon_by_standard(wind_speed, standard='atlantic'):
- """
- Categorize typhoon based on wind speed and chosen standard
- wind_speed is in knots
- """
- if standard == 'taiwan':
- # Convert knots to m/s for Taiwan standard
- wind_speed_ms = wind_speed * 0.514444
-
- if wind_speed_ms >= 51.0:
- return 'Strong Typhoon', taiwan_standard['Strong Typhoon']['color']
- elif wind_speed_ms >= 33.7:
- return 'Medium Typhoon', taiwan_standard['Medium Typhoon']['color']
- elif wind_speed_ms >= 17.2:
- return 'Mild Typhoon', taiwan_standard['Mild Typhoon']['color']
- else:
- return 'Tropical Depression', taiwan_standard['Tropical Depression']['color']
- else:
- # Atlantic standard uses knots
- if wind_speed >= 137:
- return 'C5 Super Typhoon', atlantic_standard['C5 Super Typhoon']['color']
- elif wind_speed >= 113:
- return 'C4 Very Strong Typhoon', atlantic_standard['C4 Very Strong Typhoon']['color']
- elif wind_speed >= 96:
- return 'C3 Strong Typhoon', atlantic_standard['C3 Strong Typhoon']['color']
- elif wind_speed >= 83:
- return 'C2 Typhoon', atlantic_standard['C2 Typhoon']['color']
- elif wind_speed >= 64:
- return 'C1 Typhoon', atlantic_standard['C1 Typhoon']['color']
- elif wind_speed >= 34:
- return 'Tropical Storm', atlantic_standard['Tropical Storm']['color']
- else:
- return 'Tropical Depression', atlantic_standard['Tropical Depression']['color']
-
-if __name__ == "__main__":
- print(f"Using data path: {DATA_PATH}")
- # Update ONI data before starting the application
- update_oni_data()
- oni_df = fetch_oni_data_from_csv(ONI_DATA_PATH)
- ibtracs = load_ibtracs_data()
- convert_typhoondata(LOCAL_iBtrace_PATH, TYPHOON_DATA_PATH)
- oni_data, typhoon_data = load_data(ONI_DATA_PATH, TYPHOON_DATA_PATH)
- oni_long = process_oni_data_with_cache(oni_data)
- typhoon_max = process_typhoon_data_with_cache(typhoon_data)
- merged_data = merge_data(oni_long, typhoon_max)
- data = preprocess_data(oni_data, typhoon_data)
- max_wind_speed, min_pressure = calculate_max_wind_min_pressure(typhoon_data)
-
-
- # Schedule IBTrACS data update daily
- schedule.every().day.at("01:00").do(update_ibtracs_data)
-
- # Schedule ONI data check daily, but only update on specified dates
- schedule.every().day.at("00:00").do(lambda: update_oni_data() if should_update_oni() else None)
-
- # Run the scheduler in a separate thread
- scheduler_thread = threading.Thread(target=run_schedule)
- scheduler_thread.start()
-
-
- app.run_server(debug=True, host='127.0.0.1', port=8050)
+import dash
+import plotly.graph_objects as go
+import plotly.express as px
+import pickle
+import tropycal.tracks as tracks
+import pandas as pd
+import numpy as np
+import cachetools
+import functools
+import hashlib
+import os
+import argparse
+from dash import dcc, html
+from dash.dependencies import Input, Output, State
+from dash.exceptions import PreventUpdate
+from plotly.subplots import make_subplots
+from datetime import datetime, timedelta
+from datetime import date, datetime
+from scipy import stats
+from scipy.optimize import minimize, curve_fit
+from sklearn.linear_model import LinearRegression
+from sklearn.cluster import KMeans
+from scipy.interpolate import interp1d
+from fractions import Fraction
+from concurrent.futures import ThreadPoolExecutor
+from sklearn.metrics import mean_squared_error
+import statsmodels.api as sm
+import schedule
+import time
+import threading
+import requests
+from io import StringIO
+import tempfile
+import csv
+from collections import defaultdict
+import shutil
+import filecmp
+
+# Add command-line argument parsing
+parser = argparse.ArgumentParser(description='Typhoon Analysis Dashboard')
+parser.add_argument('--data_path', type=str, default=os.getcwd(), help='Path to the data directory')
+args = parser.parse_args()
+
+# Use the command-line argument for data path
+DATA_PATH = args.data_path
+
+ONI_DATA_PATH = os.path.join(DATA_PATH, 'oni_data.csv')
+TYPHOON_DATA_PATH = os.path.join(DATA_PATH, 'processed_typhoon_data.csv')
+LOCAL_iBtrace_PATH = os.path.join(DATA_PATH, 'ibtracs.WP.list.v04r01.csv')
+iBtrace_uri = 'https://www.ncei.noaa.gov/data/international-best-track-archive-for-climate-stewardship-ibtracs/v04r01/access/csv/ibtracs.WP.list.v04r01.csv'
+
+CACHE_FILE = 'ibtracs_cache.pkl'
+CACHE_EXPIRY_DAYS = 1
+last_oni_update = None
+
+
+def should_update_oni():
+ today = datetime.now()
+ # Beginning of the month: 1st day
+ if today.day == 1:
+ return True
+ # Middle of the month: 15th day
+ if today.day == 15:
+ return True
+ # End of the month: last day
+ if today.day == (today.replace(day=1, month=today.month%12+1) - timedelta(days=1)).day:
+ return True
+ return False
+
+color_map = {
+ 'C5 Super Typhoon': 'rgb(255, 0, 0)', # Red
+ 'C4 Very Strong Typhoon': 'rgb(255, 63, 0)', # Red-Orange
+ 'C3 Strong Typhoon': 'rgb(255, 127, 0)', # Orange
+ 'C2 Typhoon': 'rgb(255, 191, 0)', # Orange-Yellow
+ 'C1 Typhoon': 'rgb(255, 255, 0)', # Yellow
+ 'Tropical Storm': 'rgb(0, 255, 255)', # Cyan
+ 'Tropical Depression': 'rgb(173, 216, 230)' # Light Blue
+}
+
+def convert_typhoondata(input_file, output_file):
+ with open(input_file, 'r') as infile:
+ # Skip the title and the unit line.
+ next(infile)
+ next(infile)
+
+ reader = csv.reader(infile)
+
+ # Used for storing data for each SID
+ sid_data = defaultdict(list)
+
+ for row in reader:
+ if not row: # Skip the blank lines
+ continue
+
+ sid = row[0]
+ iso_time = row[6]
+ sid_data[sid].append((row, iso_time))
+
+ with open(output_file, 'w', newline='') as outfile:
+ fieldnames = ['SID', 'ISO_TIME', 'LAT', 'LON', 'SEASON', 'NAME', 'WMO_WIND', 'WMO_PRES', 'USA_WIND', 'USA_PRES', 'START_DATE', 'END_DATE']
+ writer = csv.DictWriter(outfile, fieldnames=fieldnames)
+
+ writer.writeheader()
+
+ for sid, data in sid_data.items():
+ start_date = min(data, key=lambda x: x[1])[1]
+ end_date = max(data, key=lambda x: x[1])[1]
+
+ for row, iso_time in data:
+ writer.writerow({
+ 'SID': row[0],
+ 'ISO_TIME': iso_time,
+ 'LAT': row[8],
+ 'LON': row[9],
+ 'SEASON': row[1],
+ 'NAME': row[5],
+ 'WMO_WIND': row[10].strip() or ' ',
+ 'WMO_PRES': row[11].strip() or ' ',
+ 'USA_WIND': row[23].strip() or ' ',
+ 'USA_PRES': row[24].strip() or ' ',
+ 'START_DATE': start_date,
+ 'END_DATE': end_date
+ })
+
+
+def download_oni_file(url, filename):
+ print(f"Downloading file from {url}...")
+ try:
+ response = requests.get(url)
+ response.raise_for_status() # Raises an exception for non-200 status codes
+ with open(filename, 'wb') as f:
+ f.write(response.content)
+ print(f"File successfully downloaded and saved as {filename}")
+ return True
+ except requests.RequestException as e:
+ print(f"Download failed. Error: {e}")
+ return False
+
+
+def convert_oni_ascii_to_csv(input_file, output_file):
+ data = defaultdict(lambda: [''] * 12)
+ season_to_month = {
+ 'DJF': 12, 'JFM': 1, 'FMA': 2, 'MAM': 3, 'AMJ': 4, 'MJJ': 5,
+ 'JJA': 6, 'JAS': 7, 'ASO': 8, 'SON': 9, 'OND': 10, 'NDJ': 11
+ }
+
+ print(f"Attempting to read file: {input_file}")
+ try:
+ with open(input_file, 'r') as f:
+ lines = f.readlines()
+ print(f"Successfully read {len(lines)} lines")
+
+ if len(lines) <= 1:
+ print("Error: File is empty or contains only header")
+ return
+
+ for line in lines[1:]: # Skip header
+ parts = line.split()
+ if len(parts) >= 4:
+ season, year = parts[0], parts[1]
+ anom = parts[-1]
+
+ if season in season_to_month:
+ month = season_to_month[season]
+
+ if season == 'DJF':
+ year = str(int(year) - 1)
+
+ data[year][month-1] = anom
+ else:
+ print(f"Warning: Unknown season: {season}")
+ else:
+ print(f"Warning: Skipping invalid line: {line.strip()}")
+
+ print(f"Processed data for {len(data)} years")
+ except Exception as e:
+ print(f"Error reading file: {e}")
+ return
+
+ print(f"Attempting to write file: {output_file}")
+ try:
+ with open(output_file, 'w', newline='') as f:
+ writer = csv.writer(f)
+ writer.writerow(['Year', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'])
+
+ for year in sorted(data.keys()):
+ row = [year] + data[year]
+ writer.writerow(row)
+
+ print(f"Successfully wrote {len(data)} rows of data")
+ except Exception as e:
+ print(f"Error writing file: {e}")
+ return
+
+ print(f"Conversion complete. Data saved to {output_file}")
+
+def update_oni_data():
+ global last_oni_update
+ current_date = date.today()
+
+ # Check if already updated today
+ if last_oni_update == current_date:
+ print("ONI data already checked today. Skipping update.")
+ return
+
+ url = "https://www.cpc.ncep.noaa.gov/data/indices/oni.ascii.txt"
+ temp_file = os.path.join(DATA_PATH, "temp_oni.ascii.txt")
+ input_file = os.path.join(DATA_PATH, "oni.ascii.txt")
+ output_file = ONI_DATA_PATH
+
+ if download_oni_file(url, temp_file):
+ if not os.path.exists(input_file) or not filecmp.cmp(temp_file, input_file, shallow=False):
+ # File doesn't exist or has been updated
+ os.replace(temp_file, input_file)
+ print("New ONI data detected. Converting to CSV.")
+ convert_oni_ascii_to_csv(input_file, output_file)
+ print("ONI data updated successfully.")
+ else:
+ print("ONI data is up to date. No conversion needed.")
+ os.remove(temp_file) # Remove temporary file
+
+ last_oni_update = current_date
+ else:
+ print("Failed to download ONI data.")
+ if os.path.exists(temp_file):
+ os.remove(temp_file) # Ensure cleanup of temporary file
+
+def load_ibtracs_data():
+ if os.path.exists(CACHE_FILE):
+ cache_time = datetime.fromtimestamp(os.path.getmtime(CACHE_FILE))
+ if datetime.now() - cache_time < timedelta(days=CACHE_EXPIRY_DAYS):
+ print("Loading data from cache...")
+ with open(CACHE_FILE, 'rb') as f:
+ return pickle.load(f)
+
+ if os.path.exists(LOCAL_iBtrace_PATH):
+ print("Using local IBTrACS file...")
+ ibtracs = tracks.TrackDataset(basin='west_pacific', source='ibtracs', ibtracs_url=LOCAL_iBtrace_PATH)
+ else:
+ print("Local IBTrACS file not found. Fetching data from remote server...")
+ try:
+ response = requests.get(iBtrace_uri)
+ response.raise_for_status()
+
+ with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.csv') as temp_file:
+ temp_file.write(response.text)
+ temp_file_path = temp_file.name
+
+ # Save the downloaded data as the local file
+ shutil.move(temp_file_path, LOCAL_iBtrace_PATH)
+ print(f"Downloaded data saved to {LOCAL_iBtrace_PATH}")
+
+ ibtracs = tracks.TrackDataset(basin='west_pacific', source='ibtracs', ibtracs_url=LOCAL_iBtrace_PATH)
+ except requests.RequestException as e:
+ print(f"Error downloading data: {e}")
+ print("No local file available and download failed. Unable to load IBTrACS data.")
+ return None
+
+ with open(CACHE_FILE, 'wb') as f:
+ pickle.dump(ibtracs, f)
+
+ return ibtracs
+
+def update_ibtracs_data():
+ global ibtracs
+ print("Checking for IBTrACS data updates...")
+
+ try:
+ # Get the last-modified time of the remote file
+ response = requests.head(iBtrace_uri)
+ remote_last_modified = datetime.strptime(response.headers['Last-Modified'], '%a, %d %b %Y %H:%M:%S GMT')
+
+ # Get the last-modified time of the local file
+ if os.path.exists(LOCAL_iBtrace_PATH):
+ local_last_modified = datetime.fromtimestamp(os.path.getmtime(LOCAL_iBtrace_PATH))
+ else:
+ local_last_modified = datetime.min
+
+ # Compare the modification times
+ if remote_last_modified <= local_last_modified:
+ print("Local IBTrACS data is up to date. No update needed.")
+ if os.path.exists(CACHE_FILE):
+ # Update the cache file's timestamp to extend its validity
+ os.utime(CACHE_FILE, None)
+ print("Cache file timestamp updated.")
+ return
+
+ print("Remote data is newer. Updating IBTrACS data...")
+
+ # Download the new data
+ response = requests.get(iBtrace_uri)
+ response.raise_for_status()
+
+ with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.csv') as temp_file:
+ temp_file.write(response.text)
+ temp_file_path = temp_file.name
+
+ # Save the downloaded data as the local file
+ shutil.move(temp_file_path, LOCAL_iBtrace_PATH)
+ print(f"Downloaded data saved to {LOCAL_iBtrace_PATH}")
+
+ # Update the last modified time of the local file to match the remote file
+ os.utime(LOCAL_iBtrace_PATH, (remote_last_modified.timestamp(), remote_last_modified.timestamp()))
+
+ ibtracs = tracks.TrackDataset(basin='west_pacific', source='ibtracs', ibtracs_url=LOCAL_iBtrace_PATH)
+
+ with open(CACHE_FILE, 'wb') as f:
+ pickle.dump(ibtracs, f)
+ print("IBTrACS data updated and cache refreshed.")
+
+ except requests.RequestException as e:
+ print(f"Error checking or downloading data: {e}")
+ if os.path.exists(LOCAL_iBtrace_PATH):
+ print("Using existing local file.")
+ ibtracs = tracks.TrackDataset(basin='west_pacific', source='ibtracs', ibtracs_url=LOCAL_iBtrace_PATH)
+ if os.path.exists(CACHE_FILE):
+ # Update the cache file's timestamp even when using existing local file
+ os.utime(CACHE_FILE, None)
+ print("Cache file timestamp updated.")
+ else:
+ print("No local file available. Update failed.")
+
+def run_schedule():
+ while True:
+ schedule.run_pending()
+ time.sleep(1)
+
+def analyze_typhoon_generation(merged_data, start_date, end_date):
+ filtered_data = merged_data[
+ (merged_data['ISO_TIME'] >= start_date) &
+ (merged_data['ISO_TIME'] <= end_date)
+ ]
+
+ filtered_data['ENSO_Phase'] = filtered_data['ONI'].apply(classify_enso_phases)
+
+ typhoon_counts = filtered_data['ENSO_Phase'].value_counts().to_dict()
+
+ month_counts = filtered_data.groupby(['ENSO_Phase', filtered_data['ISO_TIME'].dt.month]).size().unstack(fill_value=0)
+ concentrated_months = month_counts.idxmax(axis=1).to_dict()
+
+ return typhoon_counts, concentrated_months
+
+def cache_key_generator(*args, **kwargs):
+ key = hashlib.md5()
+ for arg in args:
+ key.update(str(arg).encode())
+ for k, v in sorted(kwargs.items()):
+ key.update(str(k).encode())
+ key.update(str(v).encode())
+ return key.hexdigest()
+
+def categorize_typhoon(wind_speed):
+ wind_speed_kt = wind_speed / 2 # Convert kt to m/s
+
+ # Add category classification
+ if wind_speed_kt >= 137/2.35:
+ return 'C5 Super Typhoon'
+ elif wind_speed_kt >= 113/2.35:
+ return 'C4 Very Strong Typhoon'
+ elif wind_speed_kt >= 96/2.35:
+ return 'C3 Strong Typhoon'
+ elif wind_speed_kt >= 83/2.35:
+ return 'C2 Typhoon'
+ elif wind_speed_kt >= 64/2.35:
+ return 'C1 Typhoon'
+ elif wind_speed_kt >= 34/2.35:
+ return 'Tropical Storm'
+ else:
+ return 'Tropical Depression'
+
+@functools.lru_cache(maxsize=None)
+def process_oni_data_cached(oni_data_hash):
+ return process_oni_data(oni_data)
+
+def process_oni_data(oni_data):
+ oni_long = oni_data.melt(id_vars=['Year'], var_name='Month', value_name='ONI')
+ oni_long['Month'] = oni_long['Month'].map({
+ 'Jan': '01', 'Feb': '02', 'Mar': '03', 'Apr': '04', 'May': '05', 'Jun': '06',
+ 'Jul': '07', 'Aug': '08', 'Sep': '09', 'Oct': '10', 'Nov': '11', 'Dec': '12'
+ })
+ oni_long['Date'] = pd.to_datetime(oni_long['Year'].astype(str) + '-' + oni_long['Month'] + '-01')
+ oni_long['ONI'] = pd.to_numeric(oni_long['ONI'], errors='coerce')
+ return oni_long
+
+def process_oni_data_with_cache(oni_data):
+ oni_data_hash = cache_key_generator(oni_data.to_json())
+ return process_oni_data_cached(oni_data_hash)
+
+@functools.lru_cache(maxsize=None)
+def process_typhoon_data_cached(typhoon_data_hash):
+ return process_typhoon_data(typhoon_data)
+
+def process_typhoon_data(typhoon_data):
+ typhoon_data['ISO_TIME'] = pd.to_datetime(typhoon_data['ISO_TIME'], errors='coerce')
+ typhoon_data['USA_WIND'] = pd.to_numeric(typhoon_data['USA_WIND'], errors='coerce')
+ typhoon_data['USA_PRES'] = pd.to_numeric(typhoon_data['USA_PRES'], errors='coerce')
+ typhoon_data['LON'] = pd.to_numeric(typhoon_data['LON'], errors='coerce')
+
+ typhoon_max = typhoon_data.groupby('SID').agg({
+ 'USA_WIND': 'max',
+ 'USA_PRES': 'min',
+ 'ISO_TIME': 'first',
+ 'SEASON': 'first',
+ 'NAME': 'first',
+ 'LAT': 'first',
+ 'LON': 'first'
+ }).reset_index()
+
+ typhoon_max['Month'] = typhoon_max['ISO_TIME'].dt.strftime('%m')
+ typhoon_max['Year'] = typhoon_max['ISO_TIME'].dt.year
+ typhoon_max['Category'] = typhoon_max['USA_WIND'].apply(categorize_typhoon)
+ return typhoon_max
+
+def process_typhoon_data_with_cache(typhoon_data):
+ typhoon_data_hash = cache_key_generator(typhoon_data.to_json())
+ return process_typhoon_data_cached(typhoon_data_hash)
+
+def merge_data(oni_long, typhoon_max):
+ return pd.merge(typhoon_max, oni_long, on=['Year', 'Month'])
+
+def calculate_logistic_regression(merged_data):
+ data = merged_data.dropna(subset=['USA_WIND', 'ONI'])
+
+ # Create binary outcome for severe typhoons
+ data['severe_typhoon'] = (data['USA_WIND'] >= 51).astype(int)
+
+ # Create binary predictor for El Niño
+ data['el_nino'] = (data['ONI'] >= 0.5).astype(int)
+
+ X = data['el_nino']
+ X = sm.add_constant(X) # Add constant term
+ y = data['severe_typhoon']
+
+ model = sm.Logit(y, X).fit()
+
+ beta_1 = model.params['el_nino']
+ exp_beta_1 = np.exp(beta_1)
+ p_value = model.pvalues['el_nino']
+
+ return beta_1, exp_beta_1, p_value
+
+@cachetools.cached(cache={})
+def fetch_oni_data_from_csv(file_path):
+ df = pd.read_csv(file_path, sep=',', header=0, na_values='-99.90')
+ df.columns = ['Year', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
+ df = df.melt(id_vars=['Year'], var_name='Month', value_name='ONI')
+ df['Date'] = pd.to_datetime(df['Year'].astype(str) + df['Month'], format='%Y%b')
+ df = df.set_index('Date')
+ return df
+
+def classify_enso_phases(oni_value):
+ if isinstance(oni_value, pd.Series):
+ oni_value = oni_value.iloc[0]
+ if oni_value >= 0.5:
+ return 'El Nino'
+ elif oni_value <= -0.5:
+ return 'La Nina'
+ else:
+ return 'Neutral'
+
+def load_data(oni_data_path, typhoon_data_path):
+ oni_data = pd.read_csv(oni_data_path)
+ typhoon_data = pd.read_csv(typhoon_data_path, low_memory=False)
+
+ typhoon_data['ISO_TIME'] = pd.to_datetime(typhoon_data['ISO_TIME'], errors='coerce')
+
+ typhoon_data = typhoon_data.dropna(subset=['ISO_TIME'])
+
+ print(f"Typhoon data shape after cleaning: {typhoon_data.shape}")
+ print(f"Year range: {typhoon_data['ISO_TIME'].dt.year.min()} - {typhoon_data['ISO_TIME'].dt.year.max()}")
+
+ return oni_data, typhoon_data
+
+def preprocess_data(oni_data, typhoon_data):
+ typhoon_data['USA_WIND'] = pd.to_numeric(typhoon_data['USA_WIND'], errors='coerce')
+ typhoon_data['WMO_PRES'] = pd.to_numeric(typhoon_data['WMO_PRES'], errors='coerce')
+ typhoon_data['ISO_TIME'] = pd.to_datetime(typhoon_data['ISO_TIME'], errors='coerce')
+ typhoon_data['Year'] = typhoon_data['ISO_TIME'].dt.year
+ typhoon_data['Month'] = typhoon_data['ISO_TIME'].dt.month
+
+ monthly_max_wind_speed = typhoon_data.groupby(['Year', 'Month'])['USA_WIND'].max().reset_index()
+
+ oni_data_long = pd.melt(oni_data, id_vars=['Year'], var_name='Month', value_name='ONI')
+ oni_data_long['Month'] = oni_data_long['Month'].apply(lambda x: pd.to_datetime(x, format='%b').month)
+
+ merged_data = pd.merge(monthly_max_wind_speed, oni_data_long, on=['Year', 'Month'])
+
+ return merged_data
+
+def calculate_max_wind_min_pressure(typhoon_data):
+ max_wind_speed = typhoon_data['USA_WIND'].max()
+ min_pressure = typhoon_data['WMO_PRES'].min()
+ return max_wind_speed, min_pressure
+
+@functools.lru_cache(maxsize=None)
+def get_storm_data(storm_id):
+ return ibtracs.get_storm(storm_id)
+
+def filter_west_pacific_coordinates(lons, lats):
+ mask = (100 <= lons) & (lons <= 180) & (0 <= lats) & (lats <= 40)
+ return lons[mask], lats[mask]
+
+def polynomial_exp(x, a, b, c, d):
+ return a * x**2 + b * x + c + d * np.exp(x)
+
+def exponential(x, a, b, c):
+ return a * np.exp(b * x) + c
+
+def generate_cluster_equations(cluster_center):
+ X = cluster_center[:, 0] # Longitudes
+ y = cluster_center[:, 1] # Latitudes
+
+ x_min = X.min()
+ x_max = X.max()
+
+ equations = []
+
+ # Fourier Series (up to 4th order)
+ def fourier_series(x, a0, a1, b1, a2, b2, a3, b3, a4, b4):
+ return (a0 + a1*np.cos(x) + b1*np.sin(x) +
+ a2*np.cos(2*x) + b2*np.sin(2*x) +
+ a3*np.cos(3*x) + b3*np.sin(3*x) +
+ a4*np.cos(4*x) + b4*np.sin(4*x))
+
+ # Normalize X to the range [0, 2π]
+ X_normalized = 2 * np.pi * (X - x_min) / (x_max - x_min)
+
+ params, _ = curve_fit(fourier_series, X_normalized, y)
+ a0, a1, b1, a2, b2, a3, b3, a4, b4 = params
+
+ # Create the equation string
+ fourier_eq = (f"y = {a0:.4f} + {a1:.4f}*cos(x) + {b1:.4f}*sin(x) + "
+ f"{a2:.4f}*cos(2x) + {b2:.4f}*sin(2x) + "
+ f"{a3:.4f}*cos(3x) + {b3:.4f}*sin(3x) + "
+ f"{a4:.4f}*cos(4x) + {b4:.4f}*sin(4x)")
+
+ equations.append(("Fourier Series", fourier_eq))
+ equations.append(("X Range", f"x goes from 0 to {2*np.pi:.4f}"))
+ equations.append(("Longitude Range", f"Longitude goes from {x_min:.4f}°E to {x_max:.4f}°E"))
+
+ return equations, (x_min, x_max)
+
+#oni_df = fetch_oni_data_from_csv(ONI_DATA_PATH)
+#ibtracs = load_ibtracs_data()
+#oni_data, typhoon_data = load_data(ONI_DATA_PATH, TYPHOON_DATA_PATH)
+#oni_long = process_oni_data_with_cache(oni_data)
+#typhoon_max = process_typhoon_data_with_cache(typhoon_data)
+#merged_data = merge_data(oni_long, typhoon_max)
+#data = preprocess_data(oni_data, typhoon_data)
+#max_wind_speed, min_pressure = calculate_max_wind_min_pressure(typhoon_data)
+#
+## Schedule the update to run daily at 1:00 AM
+#schedule.every().day.at("01:00").do(update_ibtracs_data)
+#
+## Run the scheduler in a separate thread
+#scheduler_thread = threading.Thread(target=run_schedule)
+#scheduler_thread.start()
+
+
+app = dash.Dash(__name__)
+
+# First, add the classification standards
+atlantic_standard = {
+ 'C5 Super Typhoon': {'wind_speed': 137, 'color': 'rgb(255, 0, 0)'},
+ 'C4 Very Strong Typhoon': {'wind_speed': 113, 'color': 'rgb(255, 63, 0)'},
+ 'C3 Strong Typhoon': {'wind_speed': 96, 'color': 'rgb(255, 127, 0)'},
+ 'C2 Typhoon': {'wind_speed': 83, 'color': 'rgb(255, 191, 0)'},
+ 'C1 Typhoon': {'wind_speed': 64, 'color': 'rgb(255, 255, 0)'},
+ 'Tropical Storm': {'wind_speed': 34, 'color': 'rgb(0, 255, 255)'},
+ 'Tropical Depression': {'wind_speed': 0, 'color': 'rgb(173, 216, 230)'}
+}
+
+taiwan_standard = {
+ 'Strong Typhoon': {'wind_speed': 51.0, 'color': 'rgb(255, 0, 0)'}, # >= 51.0 m/s
+ 'Medium Typhoon': {'wind_speed': 33.7, 'color': 'rgb(255, 127, 0)'}, # 33.7-50.9 m/s
+ 'Mild Typhoon': {'wind_speed': 17.2, 'color': 'rgb(255, 255, 0)'}, # 17.2-33.6 m/s
+ 'Tropical Depression': {'wind_speed': 0, 'color': 'rgb(173, 216, 230)'} # < 17.2 m/s
+}
+
+app.layout = html.Div([
+ html.H1("Typhoon Analysis Dashboard"),
+
+ html.Div([
+ dcc.Input(id='start-year', type='number', placeholder='Start Year', value=2000, min=1900, max=2024, step=1),
+ dcc.Input(id='start-month', type='number', placeholder='Start Month', value=1, min=1, max=12, step=1),
+ dcc.Input(id='end-year', type='number', placeholder='End Year', value=2024, min=1900, max=2024, step=1),
+ dcc.Input(id='end-month', type='number', placeholder='End Month', value=6, min=1, max=12, step=1),
+ dcc.Dropdown(
+ id='enso-dropdown',
+ options=[
+ {'label': 'All Years', 'value': 'all'},
+ {'label': 'El Niño Years', 'value': 'el_nino'},
+ {'label': 'La Niña Years', 'value': 'la_nina'},
+ {'label': 'Neutral Years', 'value': 'neutral'}
+ ],
+ value='all'
+ ),
+ html.Button('Analyze', id='analyze-button', n_clicks=0),
+ ]),
+
+ html.Div([
+ dcc.Input(id='typhoon-search', type='text', placeholder='Search Typhoon Name'),
+ html.Button('Find Typhoon', id='find-typhoon-button', n_clicks=0),
+ ]),
+
+ html.Div([
+ html.Div(id='correlation-coefficient'),
+ html.Div(id='max-wind-speed'),
+ html.Div(id='min-pressure'),
+ ]),
+
+ dcc.Graph(id='typhoon-tracks-graph'),
+ html.Div([
+ html.P("Number of Clusters"),
+ dcc.Input(id='n-clusters', type='number', placeholder='Number of Clusters', value=5, min=1, max=20, step=1),
+ html.Button('Show Clusters', id='show-clusters-button', n_clicks=0),
+ html.Button('Show Typhoon Routes', id='show-routes-button', n_clicks=0),
+ ]),
+
+ dcc.Graph(id='typhoon-routes-graph'),
+
+ html.Div([
+ html.Button('Fourier Series', id='fourier-series-button', n_clicks=0),
+ ]),
+ html.Div(id='cluster-equation-results'),
+
+ html.Div([
+ html.Button('Wind Speed Logistic Regression', id='wind-regression-button', n_clicks=0),
+ html.Button('Pressure Logistic Regression', id='pressure-regression-button', n_clicks=0),
+ html.Button('Longitude Logistic Regression', id='longitude-regression-button', n_clicks=0),
+ ]),
+ html.Div(id='logistic-regression-results'),
+
+ html.H2("Typhoon Path Analysis"),
+ html.Div([
+ dcc.Dropdown(
+ id='year-dropdown',
+ options=[{'label': str(year), 'value': year} for year in range(1950, 2025)],
+ value=2024,
+ style={'width': '200px'}
+ ),
+ dcc.Dropdown(
+ id='typhoon-dropdown',
+ style={'width': '300px'}
+ ),
+ dcc.Dropdown(
+ id='classification-standard',
+ options=[
+ {'label': 'Atlantic Standard', 'value': 'atlantic'},
+ {'label': 'Taiwan Standard', 'value': 'taiwan'}
+ ],
+ value='atlantic',
+ style={'width': '200px'}
+ )
+ ], style={'display': 'flex', 'gap': '10px'}),
+
+ dcc.Graph(id='typhoon-path-animation'),
+ dcc.Graph(id='all-years-regression-graph'),
+ dcc.Graph(id='wind-oni-scatter-plot'),
+ dcc.Graph(id='pressure-oni-scatter'),
+
+ html.Div(id='regression-graphs'),
+ html.Div(id='slopes'),
+ html.Div([
+ html.H3("Correlation Analysis"),
+ html.Div(id='wind-oni-correlation'),
+ html.Div(id='pressure-oni-correlation'),
+ ]),
+ html.Div([
+ html.H3("Typhoon Generation Analysis"),
+ html.Div(id='typhoon-count-analysis'),
+ html.Div(id='concentrated-months-analysis'),
+ ]),
+ html.Div(id='cluster-info'),
+
+ html.Div([
+ dcc.Dropdown(
+ id='classification-standard',
+ options=[
+ {'label': 'Atlantic Standard', 'value': 'atlantic'},
+ {'label': 'Taiwan Standard', 'value': 'taiwan'}
+ ],
+ value='atlantic',
+ style={'width': '200px'}
+ )
+ ], style={'margin': '10px'}),
+
+], style={'font-family': 'Arial, sans-serif'})
+
+@app.callback(
+ Output('year-dropdown', 'options'),
+ Input('typhoon-tracks-graph', 'figure')
+)
+def initialize_year_dropdown(_):
+ try:
+ years = typhoon_data['ISO_TIME'].dt.year.unique()
+ years = years[~np.isnan(years)]
+ years = sorted(years)
+
+ options = [{'label': str(int(year)), 'value': int(year)} for year in years]
+ print(f"Generated options: {options[:5]}...")
+ return options
+ except Exception as e:
+ print(f"Error in initialize_year_dropdown: {str(e)}")
+ return [{'label': 'Error', 'value': 'error'}]
+
+@app.callback(
+ [Output('typhoon-dropdown', 'options'),
+ Output('typhoon-dropdown', 'value')],
+ [Input('year-dropdown', 'value')]
+)
+def update_typhoon_dropdown(selected_year):
+ if not selected_year:
+ raise PreventUpdate
+
+ selected_year = int(selected_year)
+
+ season = ibtracs.get_season(selected_year)
+ storm_summary = season.summary()
+
+ typhoon_options = []
+ for i in range(storm_summary['season_storms']):
+ storm_id = storm_summary['id'][i]
+ storm_name = storm_summary['name'][i]
+ typhoon_options.append({'label': f"{storm_name} ({storm_id})", 'value': storm_id})
+
+ selected_typhoon = typhoon_options[0]['value'] if typhoon_options else None
+ return typhoon_options, selected_typhoon
+
+@app.callback(
+ Output('typhoon-path-animation', 'figure'),
+ [Input('year-dropdown', 'value'),
+ Input('typhoon-dropdown', 'value'),
+ Input('classification-standard', 'value')]
+)
+def update_typhoon_path(selected_year, selected_sid, standard):
+ if not selected_year or not selected_sid:
+ raise PreventUpdate
+
+ storm = ibtracs.get_storm(selected_sid)
+ return create_typhoon_path_figure(storm, selected_year, standard)
+
+def create_typhoon_path_figure(storm, selected_year, standard='atlantic'):
+ fig = go.Figure()
+
+ fig.add_trace(
+ go.Scattergeo(
+ lon=storm.lon,
+ lat=storm.lat,
+ mode='lines',
+ line=dict(width=2, color='gray'),
+ name='Path',
+ showlegend=False,
+ )
+ )
+
+ fig.add_trace(
+ go.Scattergeo(
+ lon=[storm.lon[0]],
+ lat=[storm.lat[0]],
+ mode='markers',
+ marker=dict(size=10, color='green', symbol='star'),
+ name='Starting Point',
+ text=storm.time[0].strftime('%Y-%m-%d %H:%M'),
+ hoverinfo='text+name',
+ )
+ )
+
+ frames = []
+ for i in range(len(storm.time)):
+ category, color = categorize_typhoon_by_standard(storm.vmax[i], standard)
+
+ r34_ne = storm.dict['USA_R34_NE'][i] if 'USA_R34_NE' in storm.dict else None
+ r34_se = storm.dict['USA_R34_SE'][i] if 'USA_R34_SE' in storm.dict else None
+ r34_sw = storm.dict['USA_R34_SW'][i] if 'USA_R34_SW' in storm.dict else None
+ r34_nw = storm.dict['USA_R34_NW'][i] if 'USA_R34_NW' in storm.dict else None
+ rmw = storm.dict['USA_RMW'][i] if 'USA_RMW' in storm.dict else None
+ eye_diameter = storm.dict['USA_EYE'][i] if 'USA_EYE' in storm.dict else None
+
+ radius_info = f"R34: NE={r34_ne}, SE={r34_se}, SW={r34_sw}, NW={r34_nw}
"
+ radius_info += f"RMW: {rmw}
"
+ radius_info += f"Eye Diameter: {eye_diameter}"
+
+ frame_data = [
+ go.Scattergeo(
+ lon=storm.lon[:i+1],
+ lat=storm.lat[:i+1],
+ mode='lines',
+ line=dict(width=2, color='blue'),
+ name='Path Traveled',
+ showlegend=False,
+ ),
+ go.Scattergeo(
+ lon=[storm.lon[i]],
+ lat=[storm.lat[i]],
+ mode='markers+text',
+ marker=dict(size=10, color=color, symbol='star'),
+ text=category,
+ textposition="top center",
+ textfont=dict(size=12, color=color),
+ name='Current Location',
+ hovertext=f"{storm.time[i].strftime('%Y-%m-%d %H:%M')}
"
+ f"Category: {category}
"
+ f"Wind Speed: {storm.vmax[i]:.1f} m/s
"
+ f"{radius_info}",
+ hoverinfo='text',
+ ),
+ ]
+ frames.append(go.Frame(data=frame_data, name=f"frame{i}"))
+
+ fig.frames = frames
+
+ fig.update_layout(
+ title=f"{selected_year} Year {storm.name} Typhoon Path",
+ showlegend=False,
+ geo=dict(
+ projection_type='natural earth',
+ showland=True,
+ landcolor='rgb(243, 243, 243)',
+ countrycolor='rgb(204, 204, 204)',
+ coastlinecolor='rgb(100, 100, 100)',
+ showocean=True,
+ oceancolor='rgb(230, 250, 255)',
+ ),
+ updatemenus=[{
+ "buttons": [
+ {
+ "args": [None, {"frame": {"duration": 100, "redraw": True},
+ "fromcurrent": True,
+ "transition": {"duration": 0}}],
+ "label": "Play",
+ "method": "animate"
+ },
+ {
+ "args": [[None], {"frame": {"duration": 0, "redraw": True},
+ "mode": "immediate",
+ "transition": {"duration": 0}}],
+ "label": "Pause",
+ "method": "animate"
+ }
+ ],
+ "direction": "left",
+ "pad": {"r": 10, "t": 87},
+ "showactive": False,
+ "type": "buttons",
+ "x": 0.1,
+ "xanchor": "right",
+ "y": 0,
+ "yanchor": "top"
+ }],
+ sliders=[{
+ "active": 0,
+ "yanchor": "top",
+ "xanchor": "left",
+ "currentvalue": {
+ "font": {"size": 20},
+ "prefix": "Time: ",
+ "visible": True,
+ "xanchor": "right"
+ },
+ "transition": {"duration": 100, "easing": "cubic-in-out"},
+ "pad": {"b": 10, "t": 50},
+ "len": 0.9,
+ "x": 0.1,
+ "y": 0,
+ "steps": [
+ {
+ "args": [[f"frame{k}"],
+ {"frame": {"duration": 100, "redraw": True},
+ "mode": "immediate",
+ "transition": {"duration": 0}}
+ ],
+ "label": storm.time[k].strftime('%Y-%m-%d %H:%M'),
+ "method": "animate"
+ }
+ for k in range(len(storm.time))
+ ]
+ }]
+ )
+
+ return fig
+
+@app.callback(
+ [Output('typhoon-routes-graph', 'figure'),
+ Output('cluster-equation-results', 'children')],
+ [Input('analyze-button', 'n_clicks'),
+ Input('show-clusters-button', 'n_clicks'),
+ Input('show-routes-button', 'n_clicks'),
+ Input('fourier-series-button', 'n_clicks')],
+ [State('start-year', 'value'),
+ State('start-month', 'value'),
+ State('end-year', 'value'),
+ State('end-month', 'value'),
+ State('n-clusters', 'value'),
+ State('enso-dropdown', 'value')]
+)
+
+def update_route_clusters(analyze_clicks, show_clusters_clicks, show_routes_clicks,
+ fourier_clicks, start_year, start_month, end_year, end_month,
+ n_clusters, enso_value):
+ ctx = dash.callback_context
+ button_id = ctx.triggered[0]['prop_id'].split('.')[0]
+
+ start_date = datetime(start_year, start_month, 1)
+ end_date = datetime(end_year, end_month, 28)
+
+ filtered_oni_df = oni_df[(oni_df.index >= start_date) & (oni_df.index <= end_date)]
+
+ fig_routes = go.Figure()
+
+ clusters = np.array([]) # Initialize as empty NumPy array
+ cluster_equations = []
+
+ # Clustering analysis
+ west_pacific_storms = []
+ for year in range(start_year, end_year + 1):
+ season = ibtracs.get_season(year)
+ for storm_id in season.summary()['id']:
+ storm = get_storm_data(storm_id)
+ storm_date = storm.time[0]
+ storm_oni = oni_df.loc[storm_date.strftime('%Y-%b')]['ONI']
+ if isinstance(storm_oni, pd.Series):
+ storm_oni = storm_oni.iloc[0]
+ storm_phase = classify_enso_phases(storm_oni)
+
+ if enso_value == 'all' or \
+ (enso_value == 'el_nino' and storm_phase == 'El Nino') or \
+ (enso_value == 'la_nina' and storm_phase == 'La Nina') or \
+ (enso_value == 'neutral' and storm_phase == 'Neutral'):
+ lons, lats = filter_west_pacific_coordinates(np.array(storm.lon), np.array(storm.lat))
+ if len(lons) > 1: # Ensure the storm has a valid path in West Pacific
+ west_pacific_storms.append((lons, lats))
+
+ max_length = max(len(storm[0]) for storm in west_pacific_storms)
+ standardized_routes = []
+
+ for lons, lats in west_pacific_storms:
+ if len(lons) < 2: # Skip if not enough points
+ continue
+ t = np.linspace(0, 1, len(lons))
+ t_new = np.linspace(0, 1, max_length)
+ lon_interp = interp1d(t, lons, kind='linear')(t_new)
+ lat_interp = interp1d(t, lats, kind='linear')(t_new)
+ route_vector = np.column_stack((lon_interp, lat_interp)).flatten()
+ standardized_routes.append(route_vector)
+
+ kmeans = KMeans(n_clusters=n_clusters, random_state=42, n_init=10)
+ clusters = kmeans.fit_predict(standardized_routes)
+
+ # Count the number of typhoons in each cluster
+ cluster_counts = np.bincount(clusters)
+
+ for lons, lats in west_pacific_storms:
+ fig_routes.add_trace(go.Scattergeo(
+ lon=lons, lat=lats,
+ mode='lines',
+ line=dict(width=1, color='lightgray'),
+ showlegend=False,
+ hoverinfo='none',
+ visible=(button_id == 'show-routes-button')
+ ))
+
+ equations_output = []
+ for i in range(n_clusters):
+ cluster_center = kmeans.cluster_centers_[i].reshape(-1, 2)
+ cluster_equations, (lon_min, lon_max) = generate_cluster_equations(cluster_center)
+
+ #equations_output.append(html.H4(f"Cluster {i+1} (Typhoons: {cluster_counts[i]})"))
+ equations_output.append(html.H4([
+ f"Cluster {i+1} (Typhoons: ",
+ html.Span(f"{cluster_counts[i]}", style={'color': 'blue'}),
+ ")"
+ ]))
+ for name, eq in cluster_equations:
+ equations_output.append(html.P(f"{name}: {eq}"))
+
+ equations_output.append(html.P("To use in GeoGebra:"))
+ equations_output.append(html.P(f"1. Set x-axis from 0 to {2*np.pi:.4f}"))
+ equations_output.append(html.P(f"2. Use the equation as is"))
+ 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}"))
+ equations_output.append(html.Hr())
+
+ fig_routes.add_trace(go.Scattergeo(
+ lon=cluster_center[:, 0],
+ lat=cluster_center[:, 1],
+ mode='lines',
+ name=f'Cluster {i+1} (n={cluster_counts[i]})',
+ line=dict(width=3),
+ visible=(button_id == 'show-clusters-button')
+ ))
+
+ enso_phase_text = {
+ 'all': 'All Years',
+ 'el_nino': 'El Niño Years',
+ 'la_nina': 'La Niña Years',
+ 'neutral': 'Neutral Years'
+ }
+ fig_routes.update_layout(
+ title=f'Typhoon Routes Clustering in West Pacific ({start_year}-{end_year}) - {enso_phase_text[enso_value]}',
+ geo=dict(
+ projection_type='mercator',
+ showland=True,
+ landcolor='rgb(243, 243, 243)',
+ countrycolor='rgb(204, 204, 204)',
+ coastlinecolor='rgb(100, 100, 100)',
+ showocean=True,
+ oceancolor='rgb(230, 250, 255)',
+ lataxis={'range': [0, 40]},
+ lonaxis={'range': [100, 180]},
+ center={'lat': 20, 'lon': 140},
+ ),
+ legend_title='Clusters'
+ )
+
+ return fig_routes, html.Div(equations_output)
+
+@app.callback(
+ [Output('typhoon-tracks-graph', 'figure'),
+ Output('all-years-regression-graph', 'figure'),
+ Output('regression-graphs', 'children'),
+ Output('slopes', 'children'),
+ Output('wind-oni-scatter-plot', 'figure'),
+ Output('pressure-oni-scatter', 'figure'),
+ Output('correlation-coefficient', 'children'),
+ Output('max-wind-speed', 'children'),
+ Output('min-pressure', 'children'),
+ Output('wind-oni-correlation', 'children'),
+ Output('pressure-oni-correlation', 'children'),
+ Output('typhoon-count-analysis', 'children'),
+ Output('concentrated-months-analysis', 'children')],
+ [Input('analyze-button', 'n_clicks'),
+ Input('find-typhoon-button', 'n_clicks')],
+ [State('start-year', 'value'),
+ State('start-month', 'value'),
+ State('end-year', 'value'),
+ State('end-month', 'value'),
+ State('enso-dropdown', 'value'),
+ State('typhoon-search', 'value')]
+)
+
+def update_graphs(analyze_clicks, find_typhoon_clicks,
+ start_year, start_month, end_year, end_month,
+ enso_value, typhoon_search):
+ ctx = dash.callback_context
+ button_id = ctx.triggered[0]['prop_id'].split('.')[0]
+
+ start_date = datetime(start_year, start_month, 1)
+ end_date = datetime(end_year, end_month, 28)
+
+ filtered_oni_df = oni_df[(oni_df.index >= start_date) & (oni_df.index <= end_date)]
+
+
+ regression_data = {'El Nino': {'longitudes': [], 'oni_values': [], 'names': []},
+ 'La Nina': {'longitudes': [], 'oni_values': [], 'names': []},
+ 'Neutral': {'longitudes': [], 'oni_values': [], 'names': []},
+ 'All': {'longitudes': [], 'oni_values': [], 'names': []}}
+
+ fig_tracks = go.Figure()
+
+ def process_storm(year, storm_id):
+ storm = get_storm_data(storm_id)
+ storm_dates = storm.time
+ if any(start_date <= date <= end_date for date in storm_dates):
+ storm_oni = filtered_oni_df.loc[storm_dates[0].strftime('%Y-%b')]['ONI']
+ if isinstance(storm_oni, pd.Series):
+ storm_oni = storm_oni.iloc[0]
+ phase = classify_enso_phases(storm_oni)
+
+ regression_data[phase]['longitudes'].append(storm.lon[0])
+ regression_data[phase]['oni_values'].append(storm_oni)
+ regression_data[phase]['names'].append(f'{storm.name} ({year})')
+ regression_data['All']['longitudes'].append(storm.lon[0])
+ regression_data['All']['oni_values'].append(storm_oni)
+ regression_data['All']['names'].append(f'{storm.name} ({year})')
+
+ if (enso_value == 'all' or
+ (enso_value == 'el_nino' and phase == 'El Nino') or
+ (enso_value == 'la_nina' and phase == 'La Nina') or
+ (enso_value == 'neutral' and phase == 'Neutral')):
+ color = {'El Nino': 'red', 'La Nina': 'blue', 'Neutral': 'green'}[phase]
+ return go.Scattergeo(
+ lon=storm.lon,
+ lat=storm.lat,
+ mode='lines',
+ name=storm.name,
+ text=f'{storm.name} ({year})',
+ hoverinfo='text',
+ line=dict(width=2, color=color)
+ )
+ return None
+
+ with ThreadPoolExecutor() as executor:
+ futures = []
+ for year in range(start_year, end_year + 1):
+ season = ibtracs.get_season(year)
+ for storm_id in season.summary()['id']:
+ futures.append(executor.submit(process_storm, year, storm_id))
+
+ for future in futures:
+ result = future.result()
+ if result:
+ fig_tracks.add_trace(result)
+
+ fig_tracks.update_layout(
+ title=f'Typhoon Tracks from {start_year}-{start_month} to {end_year}-{end_month}',
+ geo=dict(
+ projection_type='natural earth',
+ showland=True,
+ )
+ )
+
+ regression_figs = []
+ slopes = []
+ all_years_fig = go.Figure() # Initialize with an empty figure
+
+ for phase in ['El Nino', 'La Nina', 'Neutral', 'All']:
+ df = pd.DataFrame({
+ 'Longitude': regression_data[phase]['longitudes'],
+ 'ONI': regression_data[phase]['oni_values'],
+ 'Name': regression_data[phase]['names']
+ })
+
+ if not df.empty and len(df) > 1: # Ensure there's enough data for regression
+ try:
+ fig = px.scatter(df, x='Longitude', y='ONI', hover_data=['Name'],
+ labels={'Longitude': 'Longitude of Typhoon Generation', 'ONI': 'ONI Value'},
+ title=f'Typhoon Generation Location vs. ONI ({phase})')
+
+ X = np.array(df['Longitude']).reshape(-1, 1)
+ y = df['ONI']
+ model = LinearRegression()
+ model.fit(X, y)
+ y_pred = model.predict(X)
+ slope = model.coef_[0]
+ intercept = model.intercept_
+ fraction_slope = Fraction(slope).limit_denominator()
+ equation = f'ONI = {fraction_slope} * Longitude + {Fraction(intercept).limit_denominator()}'
+
+ fig.add_trace(go.Scatter(x=df['Longitude'], y=y_pred, mode='lines', name='Regression Line'))
+ fig.add_annotation(x=df['Longitude'].mean(), y=y_pred.mean(),
+ text=equation, showarrow=False, yshift=10)
+
+ if phase == 'All':
+ all_years_fig = fig
+ else:
+ regression_figs.append(dcc.Graph(figure=fig))
+
+ correlation_coef = np.corrcoef(df['Longitude'], df['ONI'])[0, 1]
+ slopes.append(html.P(f'{phase} Regression Slope: {slope:.4f}, Correlation Coefficient: {correlation_coef:.4f}'))
+ except Exception as e:
+ print(f"Error in regression analysis for {phase}: {str(e)}")
+ if phase != 'All':
+ regression_figs.append(html.Div(f"Error in analysis for {phase}"))
+ slopes.append(html.P(f'{phase} Regression: Error in analysis'))
+ else:
+ if phase != 'All':
+ regression_figs.append(html.Div(f"Insufficient data for {phase}"))
+ slopes.append(html.P(f'{phase} Regression: Insufficient data'))
+
+ if all_years_fig.data == ():
+ all_years_fig = go.Figure()
+ all_years_fig.add_annotation(text="No data available for regression analysis",
+ xref="paper", yref="paper",
+ x=0.5, y=0.5, showarrow=False)
+
+ if button_id == 'find-typhoon-button' and typhoon_search:
+ for trace in fig_tracks.data:
+ if typhoon_search.lower() in trace.name.lower():
+ trace.line.width = 5
+ trace.line.color = 'yellow'
+
+ filtered_data = merged_data[
+ (merged_data['Year'] >= start_year) &
+ (merged_data['Year'] <= end_year) &
+ (merged_data['Month'].astype(int) >= start_month) &
+ (merged_data['Month'].astype(int) <= end_month)
+ ]
+
+ wind_oni_scatter = px.scatter(filtered_data, x='ONI', y='USA_WIND', color='Category',
+ hover_data=['NAME', 'Year','Category'],
+ title='Wind Speed vs ONI',
+ labels={'ONI': 'ONI Value', 'USA_WIND': 'Maximum Wind Speed (knots)'},
+ color_discrete_map=color_map)
+ wind_oni_scatter.update_traces(hovertemplate='%{customdata[0]} (%{customdata[1]})
Category: %{customdata[2]}
ONI: %{x}
Wind Speed: %{y} knots')
+
+ pressure_oni_scatter = px.scatter(filtered_data, x='ONI', y='USA_PRES',color='Category',
+ hover_data=['NAME', 'Year','Category'],
+ title='Pressure vs ONI',
+ labels={'ONI': 'ONI Value', 'USA_PRES': 'Minimum Pressure (hPa)'},
+ color_discrete_map=color_map)
+ pressure_oni_scatter.update_traces(hovertemplate='%{customdata[0]} (%{customdata[1]})
Category: %{customdata[2]}
ONI: %{x}
Pressure: %{y} hPa')
+
+ if typhoon_search:
+ for fig in [wind_oni_scatter, pressure_oni_scatter]:
+ mask = filtered_data['NAME'].str.contains(typhoon_search, case=False, na=False)
+ fig.add_trace(go.Scatter(
+ x=filtered_data.loc[mask, 'ONI'],
+ y=filtered_data.loc[mask, 'USA_WIND' if 'Wind' in fig.layout.title.text else 'USA_PRES'],
+ mode='markers',
+ marker=dict(size=10, color='red', symbol='star'),
+ name=f'Matched: {typhoon_search}',
+ hovertemplate='%{text}
Category: %{customdata}
ONI: %{x}
Value: %{y}',
+ text=filtered_data.loc[mask, 'NAME'] + ' (' + filtered_data.loc[mask, 'Year'].astype(str) + ')',
+ customdata=filtered_data.loc[mask, 'Category']
+ ))
+
+
+ start_date = datetime(start_year, start_month, 1)
+ end_date = datetime(end_year, end_month, 28)
+ typhoon_counts, concentrated_months = analyze_typhoon_generation(merged_data, start_date, end_date)
+
+ month_names = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
+ count_analysis = [html.P(f"{phase}: {count} typhoons") for phase, count in typhoon_counts.items()]
+ month_analysis = [html.P(f"{phase}: Most concentrated in {month_names[month-1]}") for phase, month in concentrated_months.items()]
+
+ max_wind_speed = filtered_data['USA_WIND'].max()
+ min_pressure = typhoon_data[(typhoon_data['ISO_TIME'].dt.year >= start_year) &
+ (typhoon_data['ISO_TIME'].dt.year <= end_year)]['WMO_PRES'].min()
+
+ correlation_text = f"Logistic Regression Results: see below"
+ max_wind_speed_text = f"Maximum Wind Speed: {max_wind_speed:.2f} knots"
+ min_pressure_text = f"Minimum Pressure: {min_pressure:.2f} hPa"
+
+
+ return (fig_tracks, all_years_fig, regression_figs, slopes,
+ wind_oni_scatter, pressure_oni_scatter,
+ correlation_text, max_wind_speed_text, min_pressure_text,
+ "Wind-ONI correlation: See logistic regression results",
+ "Pressure-ONI correlation: See logistic regression results",
+ count_analysis, month_analysis)
+
+@app.callback(
+ Output('logistic-regression-results', 'children'),
+ [Input('wind-regression-button', 'n_clicks'),
+ Input('pressure-regression-button', 'n_clicks'),
+ Input('longitude-regression-button', 'n_clicks')],
+ [State('start-year', 'value'),
+ State('start-month', 'value'),
+ State('end-year', 'value'),
+ State('end-month', 'value')]
+)
+def update_logistic_regression(wind_clicks, pressure_clicks, longitude_clicks,
+ start_year, start_month, end_year, end_month):
+ ctx = dash.callback_context
+ if not ctx.triggered:
+ return "Click a button to see logistic regression results."
+
+ button_id = ctx.triggered[0]['prop_id'].split('.')[0]
+
+ start_date = datetime(start_year, start_month, 1)
+ end_date = datetime(end_year, end_month, 28)
+
+ filtered_data = merged_data[
+ (merged_data['ISO_TIME'] >= start_date) &
+ (merged_data['ISO_TIME'] <= end_date)
+ ]
+
+ if button_id == 'wind-regression-button':
+ return calculate_wind_logistic_regression(filtered_data)
+ elif button_id == 'pressure-regression-button':
+ return calculate_pressure_logistic_regression(filtered_data)
+ elif button_id == 'longitude-regression-button':
+ return calculate_longitude_logistic_regression(filtered_data)
+
+def calculate_wind_logistic_regression(data):
+ data['severe_typhoon'] = (data['USA_WIND'] >= 64).astype(int) # 64 knots threshold for severe typhoons
+ X = sm.add_constant(data['ONI'])
+ y = data['severe_typhoon']
+ model = sm.Logit(y, X).fit()
+
+ beta_1 = model.params['ONI']
+ exp_beta_1 = np.exp(beta_1)
+ p_value = model.pvalues['ONI']
+
+ el_nino_data = data[data['ONI'] >= 0.5]
+ la_nina_data = data[data['ONI'] <= -0.5]
+ neutral_data = data[(data['ONI'] > -0.5) & (data['ONI'] < 0.5)]
+
+ el_nino_severe = el_nino_data['severe_typhoon'].mean()
+ la_nina_severe = la_nina_data['severe_typhoon'].mean()
+ neutral_severe = neutral_data['severe_typhoon'].mean()
+
+ return html.Div([
+ html.H3("Wind Speed Logistic Regression Results"),
+ html.P(f"β1 (ONI coefficient): {beta_1:.4f}"),
+ html.P(f"exp(β1) (Odds Ratio): {exp_beta_1:.4f}"),
+ html.P(f"P-value: {p_value:.4f}"),
+ html.P("Interpretation:"),
+ html.Ul([
+ html.Li(f"For each unit increase in ONI, the odds of a severe typhoon are "
+ f"{'increased' if exp_beta_1 > 1 else 'decreased'} by a factor of {exp_beta_1:.2f}."),
+ html.Li(f"This effect is {'statistically significant' if p_value < 0.05 else 'not statistically significant'} "
+ f"at the 0.05 level.")
+ ]),
+ html.P("Proportion of severe typhoons:"),
+ html.Ul([
+ html.Li(f"El Niño conditions: {el_nino_severe:.2%}"),
+ html.Li(f"La Niña conditions: {la_nina_severe:.2%}"),
+ html.Li(f"Neutral conditions: {neutral_severe:.2%}")
+ ])
+ ])
+
+def calculate_pressure_logistic_regression(data):
+ data['intense_typhoon'] = (data['USA_PRES'] <= 950).astype(int) # 950 hPa threshold for intense typhoons
+ X = sm.add_constant(data['ONI'])
+ y = data['intense_typhoon']
+ model = sm.Logit(y, X).fit()
+
+ beta_1 = model.params['ONI']
+ exp_beta_1 = np.exp(beta_1)
+ p_value = model.pvalues['ONI']
+
+ el_nino_data = data[data['ONI'] >= 0.5]
+ la_nina_data = data[data['ONI'] <= -0.5]
+ neutral_data = data[(data['ONI'] > -0.5) & (data['ONI'] < 0.5)]
+
+ el_nino_intense = el_nino_data['intense_typhoon'].mean()
+ la_nina_intense = la_nina_data['intense_typhoon'].mean()
+ neutral_intense = neutral_data['intense_typhoon'].mean()
+
+ return html.Div([
+ html.H3("Pressure Logistic Regression Results"),
+ html.P(f"β1 (ONI coefficient): {beta_1:.4f}"),
+ html.P(f"exp(β1) (Odds Ratio): {exp_beta_1:.4f}"),
+ html.P(f"P-value: {p_value:.4f}"),
+ html.P("Interpretation:"),
+ html.Ul([
+ html.Li(f"For each unit increase in ONI, the odds of an intense typhoon (pressure <= 950 hPa) are "
+ f"{'increased' if exp_beta_1 > 1 else 'decreased'} by a factor of {exp_beta_1:.2f}."),
+ html.Li(f"This effect is {'statistically significant' if p_value < 0.05 else 'not statistically significant'} "
+ f"at the 0.05 level.")
+ ]),
+ html.P("Proportion of intense typhoons:"),
+ html.Ul([
+ html.Li(f"El Niño conditions: {el_nino_intense:.2%}"),
+ html.Li(f"La Niña conditions: {la_nina_intense:.2%}"),
+ html.Li(f"Neutral conditions: {neutral_intense:.2%}")
+ ])
+ ])
+
+def calculate_longitude_logistic_regression(data):
+ # Use only the data points where longitude is available
+ data = data.dropna(subset=['LON'])
+
+ if len(data) == 0:
+ return html.Div("Insufficient data for longitude analysis")
+
+ data['western_typhoon'] = (data['LON'] <= 140).astype(int) # 140°E as threshold for western typhoons
+ X = sm.add_constant(data['ONI'])
+ y = data['western_typhoon']
+ model = sm.Logit(y, X).fit()
+
+ beta_1 = model.params['ONI']
+ exp_beta_1 = np.exp(beta_1)
+ p_value = model.pvalues['ONI']
+
+ el_nino_data = data[data['ONI'] >= 0.5]
+ la_nina_data = data[data['ONI'] <= -0.5]
+ neutral_data = data[(data['ONI'] > -0.5) & (data['ONI'] < 0.5)]
+
+ el_nino_western = el_nino_data['western_typhoon'].mean()
+ la_nina_western = la_nina_data['western_typhoon'].mean()
+ neutral_western = neutral_data['western_typhoon'].mean()
+
+ return html.Div([
+ html.H3("Longitude Logistic Regression Results"),
+ html.P(f"β1 (ONI coefficient): {beta_1:.4f}"),
+ html.P(f"exp(β1) (Odds Ratio): {exp_beta_1:.4f}"),
+ html.P(f"P-value: {p_value:.4f}"),
+ html.P("Interpretation:"),
+ html.Ul([
+ html.Li(f"For each unit increase in ONI, the odds of a typhoon forming west of 140°E are "
+ f"{'increased' if exp_beta_1 > 1 else 'decreased'} by a factor of {exp_beta_1:.2f}."),
+ html.Li(f"This effect is {'statistically significant' if p_value < 0.05 else 'not statistically significant'} "
+ f"at the 0.05 level.")
+ ]),
+ html.P("Proportion of typhoons forming west of 140°E:"),
+ html.Ul([
+ html.Li(f"El Niño conditions: {el_nino_western:.2%}"),
+ html.Li(f"La Niña conditions: {la_nina_western:.2%}"),
+ html.Li(f"Neutral conditions: {neutral_western:.2%}")
+ ])
+ ])
+
+def categorize_typhoon_by_standard(wind_speed, standard='atlantic'):
+ """
+ Categorize typhoon based on wind speed and chosen standard
+ wind_speed is in knots
+ """
+ if standard == 'taiwan':
+ # Convert knots to m/s for Taiwan standard
+ wind_speed_ms = wind_speed * 0.514444
+
+ if wind_speed_ms >= 51.0:
+ return 'Strong Typhoon', taiwan_standard['Strong Typhoon']['color']
+ elif wind_speed_ms >= 33.7:
+ return 'Medium Typhoon', taiwan_standard['Medium Typhoon']['color']
+ elif wind_speed_ms >= 17.2:
+ return 'Mild Typhoon', taiwan_standard['Mild Typhoon']['color']
+ else:
+ return 'Tropical Depression', taiwan_standard['Tropical Depression']['color']
+ else:
+ # Atlantic standard uses knots
+ if wind_speed >= 137:
+ return 'C5 Super Typhoon', atlantic_standard['C5 Super Typhoon']['color']
+ elif wind_speed >= 113:
+ return 'C4 Very Strong Typhoon', atlantic_standard['C4 Very Strong Typhoon']['color']
+ elif wind_speed >= 96:
+ return 'C3 Strong Typhoon', atlantic_standard['C3 Strong Typhoon']['color']
+ elif wind_speed >= 83:
+ return 'C2 Typhoon', atlantic_standard['C2 Typhoon']['color']
+ elif wind_speed >= 64:
+ return 'C1 Typhoon', atlantic_standard['C1 Typhoon']['color']
+ elif wind_speed >= 34:
+ return 'Tropical Storm', atlantic_standard['Tropical Storm']['color']
+ else:
+ return 'Tropical Depression', atlantic_standard['Tropical Depression']['color']
+
+if __name__ == "__main__":
+ print(f"Using data path: {DATA_PATH}")
+ # Update ONI data before starting the application
+ update_oni_data()
+ oni_df = fetch_oni_data_from_csv(ONI_DATA_PATH)
+ ibtracs = load_ibtracs_data()
+ convert_typhoondata(LOCAL_iBtrace_PATH, TYPHOON_DATA_PATH)
+ oni_data, typhoon_data = load_data(ONI_DATA_PATH, TYPHOON_DATA_PATH)
+ oni_long = process_oni_data_with_cache(oni_data)
+ typhoon_max = process_typhoon_data_with_cache(typhoon_data)
+ merged_data = merge_data(oni_long, typhoon_max)
+ data = preprocess_data(oni_data, typhoon_data)
+ max_wind_speed, min_pressure = calculate_max_wind_min_pressure(typhoon_data)
+
+
+ # Schedule IBTrACS data update daily
+ schedule.every().day.at("01:00").do(update_ibtracs_data)
+
+ # Schedule ONI data check daily, but only update on specified dates
+ schedule.every().day.at("00:00").do(lambda: update_oni_data() if should_update_oni() else None)
+
+ # Run the scheduler in a separate thread
+ scheduler_thread = threading.Thread(target=run_schedule)
+ scheduler_thread.start()
+
+
+ app.run_server(debug=True, host='127.0.0.1', port=7860)