rajkhanke commited on
Commit
df54699
·
verified ·
1 Parent(s): 73729f3

Upload 9 files

Browse files
app.py ADDED
@@ -0,0 +1,678 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, request, render_template
2
+ import requests
3
+ from datetime import datetime, date, timedelta
4
+ import joblib
5
+ import numpy as np
6
+ import shap
7
+ import google.generativeai as genai
8
+ import json
9
+ import logging
10
+ import os
11
+ import pandas as pd
12
+
13
+ # Attempt to import dice_ml and set a flag
14
+ try:
15
+ import dice_ml
16
+
17
+ dice_ml_available = True
18
+ logging.info("dice_ml library found and imported successfully.")
19
+ except ImportError as e_import: # Catch the specific ImportError
20
+ dice_ml_available = False
21
+ # Log the actual import error, which can be very helpful for debugging
22
+ logging.warning(f"IMPORTANT: dice_ml library FAILED TO IMPORT: {e_import}. "
23
+ f"DICE explanations will be unavailable. Ensure 'dice-ml' is installed in your Python environment (e.g., 'pip install dice-ml').")
24
+
25
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
26
+
27
+ app = Flask(__name__)
28
+
29
+ # IMPORTANT: Set this environment variable or replace the placeholder
30
+ GEMINI_API_KEY = "AIzaSyDkiYr-eSkqIXpZ1fHlik_YFsFtfQoFi0w"
31
+ if GEMINI_API_KEY == "YOUR_GEMINI_API_KEY_HERE":
32
+ logging.warning(
33
+ "Using a placeholder Gemini API Key. AI Analysis will likely fail. Please set the GEMINI_API_KEY environment variable or update app.py.")
34
+
35
+ NOMINATIM_USER_AGENT = 'CloudburstPredictorApp/1.0 ([email protected])' # Update with your contact
36
+
37
+ # Model and explainer globals
38
+ reg = None # Regressor (for probability score)
39
+ scaler = None
40
+ explainer = None # SHAP explainer
41
+ scaled_background_data = None
42
+ dice_explainer = None # DICE explainer
43
+ dice_data_object = None # DICE Data object
44
+
45
+ feature_names = [
46
+ 'Min Temp (°C)', 'Max Temp (°C)', 'Humidity (2m %)', 'Pressure (hPa)',
47
+ 'Precipitation (mm)', 'Rain (mm)', 'Precipitation Probability (%)',
48
+ 'Cloud Cover (%)', 'Wind Speed (km/h)', 'Wind Gust (km/h)',
49
+ 'Wind Direction (Encoded)', 'Is Day (Encoded)', 'Temp (2m °C)',
50
+ 'Weather Description (Encoded)'
51
+ ]
52
+
53
+ weather_code_mapping = {
54
+ 0: {"desc": "Clear sky", "icon": "fa-sun"}, 1: {"desc": "Mainly clear", "icon": "fa-cloud-sun"},
55
+ 2: {"desc": "Partly cloudy", "icon": "fa-cloud"}, 3: {"desc": "Overcast", "icon": "fa-smog"},
56
+ 45: {"desc": "Fog", "icon": "fa-smog"}, 48: {"desc": "Depositing rime fog", "icon": "fa-smog"},
57
+ 51: {"desc": "Light drizzle", "icon": "fa-cloud-rain"}, 53: {"desc": "Moderate drizzle", "icon": "fa-cloud-rain"},
58
+ 55: {"desc": "Dense drizzle", "icon": "fa-cloud-showers-heavy"},
59
+ 56: {"desc": "Light freezing drizzle", "icon": "fa-snowflake"},
60
+ 57: {"desc": "Dense freezing drizzle", "icon": "fa-snowflake"},
61
+ 61: {"desc": "Slight rain", "icon": "fa-cloud-rain"},
62
+ 63: {"desc": "Moderate rain", "icon": "fa-cloud-showers-heavy"},
63
+ 65: {"desc": "Heavy rain", "icon": "fa-cloud-pour"},
64
+ 66: {"desc": "Light freezing rain", "icon": "fa-cloud-meatball"},
65
+ 67: {"desc": "Heavy freezing rain", "icon": "fa-cloud-meatball"},
66
+ 71: {"desc": "Slight snow fall", "icon": "fa-snowflake"},
67
+ 73: {"desc": "Moderate snow fall", "icon": "fa-snowflake"},
68
+ 75: {"desc": "Heavy snow fall", "icon": "fa-snowflake"}, 77: {"desc": "Snow grains", "icon": "fa-snowflake"},
69
+ 80: {"desc": "Slight rain showers", "icon": "fa-cloud-sun-rain"},
70
+ 81: {"desc": "Moderate rain showers", "icon": "fa-cloud-showers-heavy"},
71
+ 82: {"desc": "Violent rain showers", "icon": "fa-cloud-pour"},
72
+ 85: {"desc": "Slight snow showers", "icon": "fa-cloud-meatball"},
73
+ 86: {"desc": "Heavy snow showers", "icon": "fa-cloud-meatball"},
74
+ 95: {"desc": "Thunderstorm", "icon": "fa-bolt-lightning"},
75
+ 96: {"desc": "Thunderstorm with slight hail", "icon": "fa-cloud-bolt"},
76
+ 99: {"desc": "Thunderstorm with heavy hail", "icon": "fa-cloud-bolt"}
77
+ }
78
+
79
+
80
+ def get_weather_detail(code, detail_type="desc", default_desc="Unknown", default_icon="fa-question-circle"):
81
+ mapping = weather_code_mapping.get(code)
82
+ if mapping: return mapping.get(detail_type, default_desc if detail_type == "desc" else default_icon)
83
+ return default_desc if detail_type == "desc" else default_icon
84
+
85
+
86
+ def load_models():
87
+ global reg, scaler, explainer, scaled_background_data, dice_explainer, dice_data_object, dice_ml_available, feature_names
88
+ try:
89
+ logging.info("Attempting to load models and scaler...")
90
+ reg_path, scaler_path, background_data_path = 'cloudburst_regressor.pkl', 'scaler.pkl', 'scaled_background_data_sample.npy'
91
+
92
+ if os.path.exists(reg_path):
93
+ reg = joblib.load(reg_path); logging.info("Regressor loaded.")
94
+ else:
95
+ logging.warning(f"Regressor model not found at {reg_path}. Regression disabled."); reg = None
96
+ if os.path.exists(scaler_path):
97
+ scaler = joblib.load(scaler_path); logging.info("Scaler loaded.")
98
+ else:
99
+ logging.warning(f"Scaler not found at {scaler_path}. Predictions disabled."); scaler = None
100
+ if os.path.exists(background_data_path):
101
+ scaled_background_data = np.load(background_data_path, allow_pickle=True)
102
+ logging.info(f"Background data for explainers loaded. Shape: {scaled_background_data.shape}")
103
+ if scaled_background_data.ndim == 1 and scaler and hasattr(scaler, 'n_features_in_') and \
104
+ scaled_background_data.shape[0] == scaler.n_features_in_:
105
+ scaled_background_data = scaled_background_data.reshape(1, -1);
106
+ logging.info(f"Reshaped 1D background data to: {scaled_background_data.shape}")
107
+ else:
108
+ logging.warning(f"Background data not found at {background_data_path}. Explainers may be affected.");
109
+ scaled_background_data = None
110
+
111
+ model_ready = reg and scaler and hasattr(reg, 'n_features_in_') and hasattr(scaler, 'n_features_in_') and \
112
+ reg.n_features_in_ == scaler.n_features_in_ == len(feature_names)
113
+
114
+ if model_ready:
115
+ # SHAP Explainer
116
+ is_tree_model = any(hasattr(reg, attr) for attr in ['tree_', 'booster_', 'estimators_']) or \
117
+ reg.__class__.__name__ in ['RandomForestRegressor', 'GradientBoostingRegressor',
118
+ 'XGBRegressor', 'LGBMRegressor']
119
+ if is_tree_model:
120
+ explainer = shap.TreeExplainer(reg); logging.info("SHAP TreeExplainer initialized.")
121
+ elif scaled_background_data is not None and scaled_background_data.shape[1] == scaler.n_features_in_:
122
+ summary_data = shap.kmeans(scaled_background_data, min(10, scaled_background_data.shape[0])) if \
123
+ scaled_background_data.shape[0] > 10 else scaled_background_data
124
+ explainer = shap.KernelExplainer(reg.predict, summary_data);
125
+ logging.info(f"SHAP KernelExplainer initialized with background summary shape {summary_data.shape}.")
126
+ else:
127
+ logging.warning(
128
+ "SHAP explainer could not be initialized (non-tree model and no suitable background data)."); explainer = None
129
+
130
+ # DICE Explainer
131
+ if dice_ml_available and scaled_background_data is not None and scaled_background_data.shape[1] == len(
132
+ feature_names):
133
+ try:
134
+ logging.info("Attempting to initialize DICE explainer...")
135
+ df_background_for_dice = pd.DataFrame(scaled_background_data, columns=feature_names)
136
+ df_dice_data_constructor = df_background_for_dice.copy()
137
+ df_dice_data_constructor['Cloudburst_Probability'] = reg.predict(scaled_background_data)
138
+
139
+ dice_data_object = dice_ml.Data(dataframe=df_dice_data_constructor,
140
+ continuous_features=feature_names,
141
+ outcome_name='Cloudburst_Probability')
142
+ dice_model_wrapper = dice_ml.Model(model=reg, backend='sklearn', model_type='regressor')
143
+ dice_explainer = dice_ml.Dice(dice_data_object, dice_model_wrapper, method="random")
144
+ logging.info("DICE explainer initialized successfully.")
145
+ except Exception as e_dice_init:
146
+ logging.error(f"Error initializing DICE explainer: {e_dice_init}", exc_info=True)
147
+ dice_explainer = None # Keep dice_ml_available as True, but explainer object is None
148
+ elif not dice_ml_available:
149
+ logging.warning(
150
+ "DICE explainer not initialized: dice_ml library not available.") # Already logged at import
151
+ else:
152
+ logging.warning(
153
+ "DICE explainer not initialized: background data for DICE missing or mismatched."); dice_explainer = None
154
+ else:
155
+ logging.warning(
156
+ "SHAP and DICE explainers disabled due to missing models, scaler, or feature count mismatch.")
157
+ explainer = None;
158
+ dice_explainer = None
159
+
160
+ except Exception as e_load:
161
+ logging.error(f"Error during model loading: {e_load}", exc_info=True)
162
+ finally:
163
+ if not model_ready: logging.critical(
164
+ "CRITICAL: Model/Scaler/Feature_names integrity check failed. Predictions unreliable.")
165
+
166
+
167
+ load_models()
168
+
169
+ wind_direction_mapping = {"E": 0, "N": 1, "NE": 2, "NW": 3, "S": 4, "SE": 5, "SW": 6, "W": 7}
170
+ is_day_mapping = {1: 0, 0: 1} # API: 1=Day (model:0), 0=Night (model:1)
171
+ wind_direction_full_names = {"N": "North", "NE": "Northeast", "E": "East", "SE": "Southeast", "S": "South",
172
+ "SW": "Southwest", "W": "West", "NW": "Northwest"}
173
+
174
+
175
+ def map_weather_description_to_encoding(code):
176
+ if code is None: return 0
177
+ try:
178
+ code = int(code)
179
+ except (ValueError, TypeError):
180
+ return 0
181
+ weather_encoding_map = {0: 0, 1: 0, 2: 5, 3: 4, 45: 2, 48: 2, 51: 1, 53: 1, 55: 1, 56: 1, 57: 1, 61: 6, 63: 6,
182
+ 65: 3, 66: 6, 67: 3, 71: 6, 73: 6, 75: 6, 77: 6, 80: 6, 81: 6, 82: 3, 85: 6, 86: 6, 95: 7,
183
+ 96: 7, 99: 7}
184
+ return weather_encoding_map.get(code, 0)
185
+
186
+
187
+ def get_previous_week_data(lat, lon):
188
+ today = date.today();
189
+ start_date, end_date = today - timedelta(days=8), today - timedelta(days=1)
190
+ url = "https://api.open-meteo.com/v1/archive"
191
+ params = {"latitude": lat, "longitude": lon, "daily": 'precipitation_sum,rain_sum',
192
+ "hourly": 'temperature_2m,relativehumidity_2m,pressure_msl,cloudcover,windspeed_10m,windgusts_10m,precipitation_probability',
193
+ "timezone": "auto", "start_date": start_date.strftime("%Y-%m-%d"),
194
+ "end_date": end_date.strftime("%Y-%m-%d")}
195
+ hist_avgs = {"avg_precipitation_sum": 0.1, "avg_rain_sum": 0.1, "avg_relativehumidity_2m": 65.0,
196
+ "avg_pressure_msl": 1012.0,
197
+ "avg_cloudcover": 40.0, "avg_temp": 22.0, "avg_wind_speed": 8.0, "avg_wind_gust": 12.0,
198
+ "avg_precip_prob": 15.0}
199
+ try:
200
+ response = requests.get(url, params=params, timeout=15);
201
+ response.raise_for_status();
202
+ data_hist = response.json()
203
+ key_map = [(("daily", "precipitation_sum"), "avg_precipitation_sum"), (("daily", "rain_sum"), "avg_rain_sum"),
204
+ (("hourly", "temperature_2m"), "avg_temp"),
205
+ (("hourly", "relativehumidity_2m"), "avg_relativehumidity_2m"),
206
+ (("hourly", "pressure_msl"), "avg_pressure_msl"), (("hourly", "cloudcover"), "avg_cloudcover"),
207
+ (("hourly", "windspeed_10m"), "avg_wind_speed"), (("hourly", "windgusts_10m"), "avg_wind_gust"),
208
+ (("hourly", "precipitation_probability"), "avg_precip_prob")]
209
+ for (data_type, param_name), avg_key in key_map:
210
+ values = [x for x in data_hist.get(data_type, {}).get(param_name, []) if
211
+ x is not None and not (isinstance(x, float) and np.isnan(x))]
212
+ if values: hist_avgs[avg_key] = float(np.mean(values))
213
+ except Exception as e:
214
+ logging.error(f"Error fetching/processing historical data: {e}. Using defaults.")
215
+ return hist_avgs
216
+
217
+
218
+ def degrees_to_cardinal(deg):
219
+ if deg is None: return "N/A"
220
+ try:
221
+ deg = float(deg)
222
+ except:
223
+ return "N/A"
224
+ return ["N", "NE", "E", "SE", "S", "SW", "W", "NW"][int((deg + 22.5) / 45) % 8]
225
+
226
+
227
+ def reverse_geocode(lat, lon):
228
+ url, headers = f"https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat={lat}&lon={lon}", {
229
+ 'User-Agent': NOMINATIM_USER_AGENT}
230
+ try:
231
+ r = requests.get(url, headers=headers, timeout=10);
232
+ r.raise_for_status();
233
+ data = r.json();
234
+ addr = data.get('address', {})
235
+ name = addr.get('city') or addr.get('town') or addr.get('village') or data.get('display_name')
236
+ country = addr.get('country')
237
+ return f"{name}, {country}" if name and country else data.get('display_name', f'Lat: {lat:.3f}, Lon: {lon:.3f}')
238
+ except Exception as e:
239
+ logging.error(f"Geocoding failed: {e}"); return f'Lat: {lat:.3f}, Lon: {lon:.3f} (No Address)'
240
+
241
+
242
+ def map_daily_to_model_features(daily_data_point, historical_avgs, expected_n_features):
243
+ try:
244
+ min_temp, max_temp = daily_data_point.get('temperature_2m_min'), daily_data_point.get('temperature_2m_max')
245
+ precip_sum, rain_sum_val = daily_data_point.get('precipitation_sum', 0.0), daily_data_point.get('rain_sum', 0.0)
246
+ precip_prob_max = daily_data_point.get('precipitation_probability_max', 0.0)
247
+ wind_speed_max, wind_gust_max = daily_data_point.get('windspeed_10m_max', 0.0), daily_data_point.get(
248
+ 'windgusts_10m_max', 0.0)
249
+ wind_dir_deg, weathercode = daily_data_point.get('winddirection_10m_dominant'), daily_data_point.get(
250
+ 'weathercode')
251
+ humidity, pressure, cloudcover = historical_avgs['avg_relativehumidity_2m'], historical_avgs[
252
+ 'avg_pressure_msl'], historical_avgs['avg_cloudcover']
253
+ temp_avg_day = (min_temp + max_temp) / 2 if min_temp is not None and max_temp is not None else historical_avgs[
254
+ 'avg_temp']
255
+ is_day_enc = is_day_mapping.get(1, 0)
256
+
257
+ features_raw = [min_temp, max_temp, humidity, pressure, precip_sum, rain_sum_val, precip_prob_max, cloudcover,
258
+ wind_speed_max, wind_gust_max, wind_direction_mapping.get(degrees_to_cardinal(wind_dir_deg), 1),
259
+ is_day_enc, temp_avg_day, map_weather_description_to_encoding(weathercode)]
260
+
261
+ default_map = {'Min Temp (°C)': historical_avgs['avg_temp'] - 5,
262
+ 'Max Temp (°C)': historical_avgs['avg_temp'] + 5,
263
+ 'Humidity (2m %)': historical_avgs['avg_relativehumidity_2m'],
264
+ 'Pressure (hPa)': historical_avgs['avg_pressure_msl'],
265
+ 'Precipitation (mm)': historical_avgs['avg_precipitation_sum'],
266
+ 'Rain (mm)': historical_avgs['avg_rain_sum'],
267
+ 'Precipitation Probability (%)': historical_avgs['avg_precip_prob'],
268
+ 'Cloud Cover (%)': historical_avgs['avg_cloudcover'],
269
+ 'Wind Speed (km/h)': historical_avgs['avg_wind_speed'],
270
+ 'Wind Gust (km/h)': historical_avgs['avg_wind_gust'],
271
+ 'Wind Direction (Encoded)': 1, 'Is Day (Encoded)': 0,
272
+ 'Temp (2m °C)': historical_avgs['avg_temp'],
273
+ 'Weather Description (Encoded)': 0}
274
+
275
+ proc_features = [float(val) if val is not None and not (isinstance(val, float) and np.isnan(val)) else float(
276
+ default_map[feature_names[i]]) for i, val in enumerate(features_raw)]
277
+
278
+ features_arr = np.array([proc_features])
279
+ if features_arr.shape[1] != expected_n_features: logging.error(
280
+ f"Daily mapped features count ({features_arr.shape[1]}) != expected ({expected_n_features})."); return None
281
+ return features_arr
282
+ except Exception as e:
283
+ logging.error(f"Error mapping daily to features: {e}", exc_info=True); return None
284
+
285
+
286
+ def get_gemini_analysis(current_weather, current_prediction, future_daily_predictions, shap_explanation, location,
287
+ dice_explanation=None):
288
+ if not GEMINI_API_KEY or "YOUR_GEMINI_API_KEY_HERE" in GEMINI_API_KEY: return {
289
+ "error": "AI analysis key not configured or is placeholder."}
290
+ try:
291
+ genai.configure(api_key=GEMINI_API_KEY)
292
+ model = genai.GenerativeModel('gemini-1.5-flash',
293
+ generation_config={"temperature": 0.5, "top_p": 0.9, "max_output_tokens": 3500})
294
+ cw = {k: (v if v is not None else 'N/A') for k, v in (current_weather or {}).items()}
295
+ cp = {k: (v if v is not None else 'N/A') for k, v in (current_prediction or {}).items()}
296
+ future_preds_text = [
297
+ f"- **{p.get('date', 'N/A')}**: Risk {p.get('probability', 'N/A')}% ({p.get('status', 'N/A')}). Weather: {p.get('weather_description', 'N/A')}, Temp: {p.get('min_temp', 'N/A')} to {p.get('max_temp', 'N/A')}. Max Precip Prob: {p.get('precip_prob_max', 'N/A')}."
298
+ for p in future_daily_predictions or []]
299
+ future_summary = "\n".join(future_preds_text) if future_preds_text else "No detailed future forecast available."
300
+
301
+ base_val_text = "Base model prediction (average probability): Not available."
302
+ shap_text_parts = []
303
+ if shap_explanation and not any(
304
+ item.get('feature', '').lower().startswith(('shap error', 'prediction disabled', 'shap n/a')) for item
305
+ in shap_explanation):
306
+ for item in shap_explanation:
307
+ if item['feature'] == 'Base Value (Average Prediction)':
308
+ base_val_text = f"Base model prediction (average probability): {item['impact']:.1f}%"
309
+ elif isinstance(item['impact'], float) and abs(item['impact']) > 0.01:
310
+ shap_text_parts.append(f" - {item['feature']}: influence of {item['impact']:.1f}% on probability")
311
+ shap_summary = "Key factors influencing *current* prediction (SHAP values show % change from base):\n" + "\n".join(
312
+ shap_text_parts[:5]) if shap_text_parts else "SHAP analysis not available or not significant."
313
+
314
+ dice_summary_text = "Counterfactual analysis (what-if scenarios to lower risk): Not available or not run."
315
+ if dice_explanation and dice_explanation.get("counterfactuals"):
316
+ dice_parts = ["**Insights from Counterfactual Analysis (What could lower the risk?):**"]
317
+ for i, cf in enumerate(dice_explanation["counterfactuals"][:2]):
318
+ dice_parts.append(
319
+ f" *Scenario {i + 1} (to achieve ~{cf.get('achieved_probability', 'target')}% risk):*") # Added % to achieved_probability
320
+ for change in cf["changes"][:3]:
321
+ orig_val, cf_val = change.get('original_value_unscaled',
322
+ change.get('original_value_scaled')), change.get('cf_value_unscaled',
323
+ change.get(
324
+ 'cf_value_scaled'))
325
+ dice_parts.append(f" - **{change['feature']}**: change from `{orig_val}` to `{cf_val}`")
326
+ dice_summary_text = "\n".join(dice_parts)
327
+ elif dice_explanation and (dice_explanation.get("message") or dice_explanation.get("error")):
328
+ dice_summary_text = f"Counterfactual analysis: {dice_explanation.get('message') or dice_explanation.get('error')}"
329
+
330
+ prompt = f"""
331
+ You are an expert meteorologist. Analyze the following cloudburst risk data for **{location}** and provide a comprehensive summary.
332
+ Use Markdown for all formatting (e.g., `## Heading 2`, `### Heading 3`, `* list item`, `**bold text**`, `_italic text_`).
333
+
334
+ **Current Weather Snapshot (as of {cw.get('Current Time', 'N/A')}):**
335
+ - Conditions: {cw.get('Weather Description (Current)', 'N/A')}
336
+ - Temperature: {cw.get('Temp (2m °C)', 'N/A')}°C (Today's Range: {cw.get('Min Temp (°C)', 'N/A')}°C - {cw.get('Max Temp (°C)', 'N/A')}°C)
337
+ - Humidity (Recent Avg): {cw.get('Humidity (Past Week Avg %)', 'N/A')}%
338
+ - Wind: {cw.get('Wind Speed (Current km/h)', 'N/A')} km/h from {cw.get('Wind Direction (Current)', 'N/A')}
339
+ - Today's Precipitation: {cw.get('Precipitation Today (Accumulated mm)', 'N/A')} mm
340
+ - Current Hour Precip. Chance: {cw.get('Precipitation Probability (Current Hour %)', 'N/A')}%
341
+
342
+ **Immediate Cloudburst Risk Assessment (Now):**
343
+ - Predicted Likelihood: **{cp.get('Predicted Cloudburst', 'N/A')}**
344
+ - Probability Score: **{cp.get('Predicted Cloudburst (%)', 'N/A')}%**
345
+ - {base_val_text}
346
+ {shap_summary}
347
+
348
+ {dice_summary_text}
349
+
350
+ **Cloudburst Risk Outlook (Next ~{len(future_daily_predictions) if future_daily_predictions else 0} Days):**
351
+ {future_summary}
352
+
353
+ ---
354
+ **YOUR DETAILED ANALYSIS & ADVICE (Use Markdown formatting as specified above):**
355
+
356
+ ## Executive Summary
357
+ _(A concise overview: current cloudburst risk level at {location}, the trend for upcoming days, and critical factors. Subtly weave in SHAP/DICE insights if available for the *immediate* forecast.)_
358
+
359
+ ## Detailed Risk Breakdown
360
+ _(Elaborate on the current situation. For the future outlook, if any days show moderate or high risk (e.g., > 40-50% probability), create sub-sections like `### Tuesday: Elevated Risk` and explain the contributing factors for that day.)_
361
+
362
+ ## Actionable Recommendations & Safety Tips
363
+ _(Provide 3-5 clear, practical bullet points based on the overall risk. E.g., preparations, travel advice, monitoring official alerts.)_
364
+
365
+ ## Understanding the Forecast
366
+ _(Briefly explain that these are model-based predictions with inherent uncertainties and encourage users to stay updated with official meteorological sources.)_
367
+
368
+ **Important:** Maintain a factual, clear, and safety-conscious tone. Avoid sensationalism. Ensure all structured text (headings, lists) uses Markdown.
369
+ gie response in html for proper rendering on webpage.
370
+ """
371
+ logging.info("Sending refined prompt to Gemini API...")
372
+ response = model.generate_content(prompt)
373
+ if response.prompt_feedback and response.prompt_feedback.block_reason:
374
+ reason = response.prompt_feedback.block_reason_message or response.prompt_feedback.block_reason.name
375
+ logging.warning(f"Gemini API call blocked. Reason: {reason}")
376
+ return {"error": f"AI analysis blocked by content policy ({reason})."}
377
+ analysis_text = "".join(part.text for part in response.candidates[0].content.parts) if response.candidates and \
378
+ response.candidates[
379
+ 0].content else None
380
+ if analysis_text: return {"analysis": analysis_text}
381
+ logging.warning(f"Gemini API returned empty or unexpected response. Full response: {response}")
382
+ return {"error": "AI analysis response was empty or malformed."}
383
+ except Exception as e:
384
+ logging.error(f"Error calling Gemini API: {e}", exc_info=True);
385
+ err_msg = str(e).lower()
386
+ if any(s in err_msg for s in ["api_key_invalid", "permission_denied", "authentication"]): return {
387
+ "error": "AI analysis failed: Invalid API Key or auth issue."}
388
+ if "quota" in err_msg: return {"error": "AI analysis failed: API quota exceeded."}
389
+ if "rate limit" in err_msg: return {"error": "AI analysis failed: Rate limit. Try again later."}
390
+ if "Deadline" in str(e) or "timeout" in err_msg: return {"error": "AI analysis failed: Request timed out."}
391
+ return {"error": f"Failed to get AI analysis: Unexpected error ({type(e).__name__})."}
392
+
393
+
394
+ @app.route('/', methods=['GET'])
395
+ def index():
396
+ return render_template('index.html', show_results=False, current_year=datetime.now().year,
397
+ lat_initial=20.5937, lon_initial=78.9629,
398
+ weather_code_mapping_json=json.dumps(weather_code_mapping))
399
+
400
+
401
+ @app.route('/forecast', methods=['GET'])
402
+ def forecast():
403
+ lat_str, lon_str = request.args.get('lat'), request.args.get('lon')
404
+ render_args = {'show_results': True, 'current_year': datetime.now().year, 'lat_initial': lat_str,
405
+ 'lon_initial': lon_str,
406
+ 'current_weather': None,
407
+ 'current_prediction': {"Predicted Cloudburst": "Error", "Predicted Cloudburst (%)": "Error"},
408
+ 'future_predictions': [],
409
+ 'shap_explanation': [{"feature": "SHAP N/A", "impact": "Not run or error."}],
410
+ 'dice_explanation': {"error": "DICE N/A"}, 'gemini_analysis': {"error": "Analysis pending."},
411
+ 'future_prob_chart_data_json': "{}", 'shap_chart_data_json': "{}", 'prediction_error': None,
412
+ 'error': None,
413
+ 'weather_code_mapping_json': json.dumps(weather_code_mapping)}
414
+
415
+ if not lat_str or not lon_str: render_args.update(
416
+ {'error': "Latitude and longitude are required.", 'show_results': False}); return render_template('index.html',
417
+ **render_args)
418
+ try:
419
+ lat, lon = float(lat_str), float(
420
+ lon_str); assert -90 <= lat <= 90 and -180 <= lon <= 180, "Coords out of range."
421
+ except (ValueError, AssertionError) as e:
422
+ render_args.update({'error': f"Invalid coordinates: {e}", 'show_results': False}); return render_template(
423
+ 'index.html', **render_args)
424
+
425
+ if not (scaler and reg and hasattr(scaler, 'n_features_in_') and hasattr(reg,
426
+ 'n_features_in_') and scaler.n_features_in_ == reg.n_features_in_ == len(
427
+ feature_names)):
428
+ err_msg = "Core prediction models/config missing/mismatched. Cannot forecast.";
429
+ logging.critical(err_msg)
430
+ render_args.update({'prediction_error': err_msg, 'gemini_analysis': {"error": err_msg}});
431
+ return render_template('index.html', **render_args)
432
+
433
+ weather_api_url = "https://api.open-meteo.com/v1/forecast"
434
+ api_params_curr = {'latitude': lat, 'longitude': lon, 'current_weather': True, 'timezone': 'auto',
435
+ 'forecast_days': 1,
436
+ 'hourly': 'temperature_2m,relativehumidity_2m,pressure_msl,precipitation,rain,cloudcover,windspeed_10m,windgusts_10m,winddirection_10m,is_day,weathercode,precipitation_probability',
437
+ 'daily': 'temperature_2m_max,temperature_2m_min,precipitation_sum,rain_sum,precipitation_probability_max,weathercode,windspeed_10m_max,windgusts_10m_max,winddirection_10m_dominant'}
438
+ try:
439
+ r_curr = requests.get(weather_api_url, params=api_params_curr,
440
+ timeout=15); r_curr.raise_for_status(); data_curr_api = r_curr.json()
441
+ except requests.exceptions.RequestException as e:
442
+ render_args['prediction_error'] = f"Weather API error (current): {e}"; return render_template('index.html',
443
+ **render_args)
444
+
445
+ api_cw, api_hrly, api_dly_today = data_curr_api.get("current_weather", {}), data_curr_api.get("hourly",
446
+ {}), data_curr_api.get(
447
+ "daily", {})
448
+ if not (api_cw and api_hrly.get("time") and api_dly_today.get("time")): render_args[
449
+ 'prediction_error'] = "Incomplete current weather data from API."; return render_template('index.html',
450
+ **render_args)
451
+
452
+ curr_time_api = api_cw.get("time");
453
+ curr_idx = api_hrly["time"].index(curr_time_api) if curr_time_api and curr_time_api in api_hrly["time"] else 0
454
+
455
+ def get_val(src, key, idx, default=None):
456
+ vals = src.get(key); return vals[idx] if vals and idx < len(vals) and vals[idx] is not None else default
457
+
458
+ hist_avgs = get_previous_week_data(lat, lon)
459
+ min_T, max_T = get_val(api_dly_today, "temperature_2m_min", 0, hist_avgs['avg_temp'] - 5), get_val(api_dly_today,
460
+ "temperature_2m_max",
461
+ 0, hist_avgs[
462
+ 'avg_temp'] + 5)
463
+ curr_T = get_val(api_hrly, "temperature_2m", curr_idx, hist_avgs['avg_temp'])
464
+ is_day_api = get_val(api_hrly, "is_day", curr_idx, 1)
465
+
466
+ curr_feat_vals = [min_T, max_T,
467
+ get_val(api_hrly, "relativehumidity_2m", curr_idx, hist_avgs['avg_relativehumidity_2m']),
468
+ get_val(api_hrly, "pressure_msl", curr_idx, hist_avgs['avg_pressure_msl']),
469
+ get_val(api_dly_today, "precipitation_sum", 0, hist_avgs['avg_precipitation_sum']),
470
+ get_val(api_dly_today, "rain_sum", 0, hist_avgs['avg_rain_sum']),
471
+ get_val(api_dly_today, "precipitation_probability_max", 0,
472
+ get_val(api_hrly, "precipitation_probability", curr_idx, hist_avgs['avg_precip_prob'])),
473
+ get_val(api_hrly, "cloudcover", curr_idx, hist_avgs['avg_cloudcover']),
474
+ get_val(api_dly_today, "windspeed_10m_max", 0,
475
+ get_val(api_hrly, "windspeed_10m", curr_idx, hist_avgs['avg_wind_speed'])),
476
+ get_val(api_dly_today, "windgusts_10m_max", 0,
477
+ get_val(api_hrly, "windgusts_10m", curr_idx, hist_avgs['avg_wind_gust'])),
478
+ wind_direction_mapping.get(degrees_to_cardinal(
479
+ get_val(api_dly_today, "winddirection_10m_dominant", 0,
480
+ get_val(api_hrly, "winddirection_10m", curr_idx, 0))), 1),
481
+ is_day_mapping.get(is_day_api, 0), curr_T,
482
+ map_weather_description_to_encoding(
483
+ get_val(api_dly_today, "weathercode", 0, get_val(api_hrly, "weathercode", curr_idx, 0)))]
484
+ curr_feat_proc = [float(x) if x is not None and not (isinstance(x, float) and np.isnan(x)) else 0.0 for x in
485
+ curr_feat_vals]
486
+ curr_feat_np = np.array([curr_feat_proc])
487
+
488
+ # Current Prediction & Explainability
489
+ if curr_feat_np.shape[1] == scaler.n_features_in_:
490
+ try:
491
+ curr_feat_scaled = scaler.transform(curr_feat_np)
492
+ prob_raw = reg.predict(curr_feat_scaled)[0]
493
+ prob_clmp = max(0, min(100, int(round(prob_raw))))
494
+ render_args['current_prediction'] = {"Predicted Cloudburst (%)": prob_clmp,
495
+ "Predicted Cloudburst": "Yes" if prob_clmp > 50 else "No"}
496
+
497
+ # SHAP Explanations
498
+ if explainer:
499
+ try:
500
+ shap_vals_raw = explainer.shap_values(curr_feat_scaled);
501
+ shap_vals = np.asarray(
502
+ shap_vals_raw[0] if isinstance(shap_vals_raw, list) else shap_vals_raw).squeeze()
503
+ if shap_vals.ndim > 1 and shap_vals.shape[0] == 1: shap_vals = shap_vals[0]
504
+ if len(shap_vals) == len(feature_names):
505
+ shap_pairs = sorted(zip(feature_names, shap_vals * 100), key=lambda x: abs(x[1]), reverse=True)
506
+ render_args['shap_explanation'] = [{"feature": name, "impact": float(val)} for name, val in
507
+ shap_pairs]
508
+ if hasattr(explainer, 'expected_value'):
509
+ base_val = explainer.expected_value;
510
+ base_val = base_val.mean() if isinstance(base_val, np.ndarray) else base_val
511
+ render_args['shap_explanation'].insert(0, {"feature": "Base Value (Average Prediction)",
512
+ "impact": float(base_val * 100)})
513
+ else:
514
+ render_args['shap_explanation'] = [
515
+ {"feature": "SHAP Error", "impact": "SHAP values length mismatch."}]
516
+ except Exception as e_s:
517
+ logging.error(f"SHAP error: {e_s}", exc_info=True); render_args['shap_explanation'] = [
518
+ {"feature": "SHAP Error", "impact": str(e_s)}]
519
+ else:
520
+ render_args['shap_explanation'] = [{"feature": "SHAP Disabled", "impact": "Explainer not initialized."}]
521
+
522
+ # DICE Counterfactuals
523
+ if dice_explainer and dice_ml_available: # Check both the explainer object and the import flag
524
+ try:
525
+ query_instance_df = pd.DataFrame(curr_feat_scaled, columns=feature_names)
526
+ desired_prob_range = [0, max(0, prob_clmp - 30)]
527
+
528
+ if prob_clmp < 30:
529
+ render_args['dice_explanation'] = {
530
+ "message": "Current risk is already low. Counterfactuals for further reduction may not be very distinct or meaningful."}
531
+ else:
532
+ cfs_object = dice_explainer.generate_counterfactuals(
533
+ query_instance_df, total_CFs=3, desired_range=desired_prob_range,
534
+ features_to_vary='all'
535
+ )
536
+ if cfs_object and cfs_object.cf_examples_list:
537
+ processed_cfs = []
538
+ original_unscaled_features = pd.Series(curr_feat_np[0], index=feature_names)
539
+
540
+ for cf_example in cfs_object.cf_examples_list:
541
+ cf_df_final = cf_example.final_cfs_df
542
+ if cf_df_final is not None and not cf_df_final.empty:
543
+ for _, cf_row_scaled_series in cf_df_final.iterrows():
544
+ achieved_prob = cf_row_scaled_series['Cloudburst_Probability']
545
+ cf_scaled_values = cf_row_scaled_series.drop(
546
+ 'Cloudburst_Probability').values.reshape(1, -1)
547
+ cf_unscaled_values = scaler.inverse_transform(cf_scaled_values)[0]
548
+ cf_unscaled_series = pd.Series(cf_unscaled_values, index=feature_names)
549
+
550
+ changes_list = []
551
+ for feat_name in feature_names:
552
+ original_val_display = f"{original_unscaled_features[feat_name]:.2f}"
553
+ cf_val_display = f"{cf_unscaled_series[feat_name]:.2f}"
554
+ if not np.isclose(original_unscaled_features[feat_name],
555
+ cf_unscaled_series[feat_name], atol=1e-2):
556
+ changes_list.append({
557
+ "feature": feat_name,
558
+ "original_value_unscaled": original_val_display,
559
+ "cf_value_unscaled": cf_val_display
560
+ })
561
+ if changes_list:
562
+ processed_cfs.append({
563
+ "target_probability_range": f"{desired_prob_range[0]}-{desired_prob_range[1]}%",
564
+ "achieved_probability": f"{achieved_prob:.1f}",
565
+ "changes": changes_list
566
+ })
567
+ render_args['dice_explanation'] = {"counterfactuals": processed_cfs} if processed_cfs else {
568
+ "message": "No distinct counterfactuals found to significantly lower the risk."}
569
+ else:
570
+ render_args['dice_explanation'] = {"message": "No counterfactuals generated by DiCE."}
571
+ except Exception as e_d:
572
+ logging.error(f"DICE error during generation: {e_d}", exc_info=True)
573
+ render_args['dice_explanation'] = {"error": f"DICE generation failed: {str(e_d)}"}
574
+ elif not dice_ml_available: # Condition for "dice-ml library not loaded"
575
+ render_args['dice_explanation'] = {
576
+ "error": "The 'dice-ml' library (for counterfactuals) failed to import. "
577
+ "Please ensure it is installed correctly in your active Python environment. "
578
+ "You can typically install it using: pip install dice-ml. "
579
+ "Check the server console/logs for specific import error messages that occurred at startup."
580
+ }
581
+ else: # dice_ml_available is True, but dice_explainer object is None (init failed for other reasons)
582
+ render_args['dice_explanation'] = {
583
+ "error": "DICE explainer could not be initialized. This might be due to issues with "
584
+ "background data ('scaled_background_data_sample.npy'), model compatibility, "
585
+ "or other setup problems. Check server console/logs for detailed initialization errors that occurred at startup."
586
+ }
587
+
588
+ except Exception as e_p:
589
+ logging.error(f"Prediction engine error: {e_p}", exc_info=True)
590
+ render_args.update({'prediction_error': f"Prediction engine error: {e_p}",
591
+ 'shap_explanation': [{"feature": "Prediction Error", "impact": str(e_p)}],
592
+ 'dice_explanation': {"error": "Prediction failed, so DICE analysis was not run."}})
593
+ else:
594
+ render_args[
595
+ 'prediction_error'] = "Feature mismatch for current prediction. Expected {} features, got {}.".format(
596
+ scaler.n_features_in_, curr_feat_np.shape[1])
597
+
598
+ render_args['current_weather'] = {
599
+ "Location Address": reverse_geocode(lat, lon),
600
+ "Current Time": datetime.fromisoformat(curr_time_api).strftime("%Y-%m-%d %H:%M %Z") if curr_time_api else "N/A",
601
+ "Min Temp (°C)": f"{min_T:.1f}" if min_T is not None else "N/A",
602
+ "Max Temp (°C)": f"{max_T:.1f}" if max_T is not None else "N/A",
603
+ "Temp (2m °C)": f"{curr_T:.1f}" if curr_T is not None else "N/A",
604
+ "Weather Code": get_val(api_hrly, "weathercode", curr_idx, 0),
605
+ "Weather Description (Current)": get_weather_detail(get_val(api_hrly, "weathercode", curr_idx, 0), "desc"),
606
+ "Humidity (Past Week Avg %)": f"{hist_avgs['avg_relativehumidity_2m']:.0f}",
607
+ "Wind Speed (Current km/h)": f"{get_val(api_hrly, 'windspeed_10m', curr_idx, 0):.1f}",
608
+ "Wind Direction (Current)": wind_direction_full_names.get(
609
+ degrees_to_cardinal(get_val(api_hrly, 'winddirection_10m', curr_idx)), "N/A"),
610
+ "Precipitation Today (Accumulated mm)": f"{get_val(api_dly_today, 'precipitation_sum', 0, 0.0):.1f}",
611
+ "Precipitation Probability (Current Hour %)": f"{get_val(api_hrly, 'precipitation_probability', curr_idx, 0):.0f}"}
612
+
613
+ # Future Predictions
614
+ api_params_fut = {'latitude': lat, 'longitude': lon, 'timezone': 'auto', 'forecast_days': 8,
615
+ 'daily': 'weathercode,temperature_2m_max,temperature_2m_min,precipitation_sum,rain_sum,precipitation_probability_max,windspeed_10m_max,windgusts_10m_max,winddirection_10m_dominant'}
616
+ try:
617
+ r_fut = requests.get(weather_api_url, params=api_params_fut, timeout=15);
618
+ r_fut.raise_for_status();
619
+ data_fut_api_dly = r_fut.json().get("daily", {})
620
+ if data_fut_api_dly.get("time") and len(data_fut_api_dly["time"]) > 1:
621
+ for i in range(1, len(data_fut_api_dly["time"])): # Skip today
622
+ day_data = {key: get_val(data_fut_api_dly, key, i) for key in data_fut_api_dly.keys()}
623
+ day_feat_np = map_daily_to_model_features(day_data, hist_avgs, scaler.n_features_in_)
624
+ day_pred = {"date": day_data.get('time'),
625
+ "min_temp": f"{day_data.get('temperature_2m_min'):.1f}" if day_data.get(
626
+ 'temperature_2m_min') is not None else "N/A",
627
+ "max_temp": f"{day_data.get('temperature_2m_max'):.1f}" if day_data.get(
628
+ 'temperature_2m_max') is not None else "N/A",
629
+ "weather_code": day_data.get('weathercode'),
630
+ "weather_description": get_weather_detail(day_data.get('weathercode'), "desc"),
631
+ "precip_prob_max": f"{day_data.get('precipitation_probability_max'):.0f}%" if day_data.get(
632
+ 'precipitation_probability_max') is not None else "N/A",
633
+ "probability": "N/A", "status": "Error"}
634
+ if day_feat_np is not None:
635
+ try:
636
+ day_prob_raw = reg.predict(scaler.transform(day_feat_np))[0]; day_prob_clmp = max(0, min(100,
637
+ int(round(
638
+ day_prob_raw))))
639
+ except Exception as e_fut_pred:
640
+ day_prob_clmp = "N/A"; day_pred["status"] = "Pred. Err"; logging.error(
641
+ f"Future day pred err: {e_fut_pred}")
642
+ day_pred.update({"probability": day_prob_clmp, "status": "Yes" if isinstance(day_prob_clmp,
643
+ int) and day_prob_clmp > 50 else "No" if isinstance(
644
+ day_prob_clmp, int) else day_pred["status"]})
645
+ else:
646
+ day_pred["status"] = "Data Err"
647
+ render_args['future_predictions'].append(day_pred)
648
+ except Exception as e_fut_api:
649
+ logging.error(f"Future forecast API/processing error: {e_fut_api}", exc_info=True)
650
+
651
+ # Gemini AI Analysis
652
+ render_args['gemini_analysis'] = get_gemini_analysis(render_args['current_weather'],
653
+ render_args['current_prediction'],
654
+ render_args['future_predictions'],
655
+ render_args['shap_explanation'],
656
+ render_args['current_weather'].get('Location Address',
657
+ f'Lat: {lat:.2f}, Lon: {lon:.2f}') if
658
+ render_args[
659
+ 'current_weather'] else f'Lat: {lat:.2f}, Lon: {lon:.2f}',
660
+ render_args['dice_explanation'])
661
+
662
+ # Chart Data
663
+ plottable_future = [p for p in render_args['future_predictions'] if isinstance(p.get('probability'), int)]
664
+ if plottable_future: render_args['future_prob_chart_data_json'] = json.dumps(
665
+ {"labels": [p['date'] for p in plottable_future], "data": [p['probability'] for p in plottable_future]})
666
+
667
+ plottable_shap = [s for s in render_args['shap_explanation'] if
668
+ s.get('feature') != 'Base Value (Average Prediction)' and isinstance(s.get('impact'),
669
+ float) and not s.get(
670
+ 'feature', '').lower().startswith(('shap', 'error', 'prediction', 'disabled', 'n/a'))]
671
+ if plottable_shap: render_args['shap_chart_data_json'] = json.dumps(
672
+ {"labels": [s['feature'] for s in plottable_shap], "data": [s['impact'] for s in plottable_shap]})
673
+
674
+ return render_template('index.html', **render_args)
675
+
676
+
677
+ if __name__ == '__main__':
678
+ app.run(debug=True, port=os.environ.get("PORT", 5000))
cloudburst_classifier.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:954c6209cf075faa720dc875c2d1011c6bd11b22ee53c9194d013bd1136f8857
3
+ size 1959545
cloudburst_regressor.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e96a532ddedaf840692b96d9b138eb8aa21d48f8fa1dbfd0511fccb2ea0a3616
3
+ size 16435729
finalclouddata.csv ADDED
The diff for this file is too large to render. See raw diff
 
floodfloodlfodd.csv ADDED
The diff for this file is too large to render. See raw diff
 
scaler.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1f08430c98dd31536548df99063137e63ded259525a4ecf1fb3e1b0c48d70cbf
3
+ size 1559
synthetic_cloudburst_data.csv ADDED
The diff for this file is too large to render. See raw diff
 
templates/index.html ADDED
@@ -0,0 +1,397 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Cloudburst Predictor Pro</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
9
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css">
10
+ <link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css"/>
11
+ <script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>
12
+ <link rel="stylesheet" href="https://unpkg.com/leaflet-control-geocoder/dist/Control.Geocoder.css" />
13
+ <script src="https://unpkg.com/leaflet-control-geocoder/dist/Control.Geocoder.js"></script>
14
+
15
+ <style>
16
+ body {
17
+ font-family: 'Roboto', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
18
+ background: linear-gradient(135deg, #E0F7FA 0%, #B2EBF2 50%, #80DEEA 100%);
19
+ color: #374151; /* Tailwind gray-700 */
20
+ overflow-x: hidden;
21
+ }
22
+ .content-card {
23
+ background-color: rgba(255, 255, 255, 0.97);
24
+ backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
25
+ border: 1px solid rgba(209, 213, 219, 0.4);
26
+ box-shadow: 0 12px 30px -8px rgba(0, 0, 0, 0.1), 0 8px 15px -8px rgba(0, 0, 0, 0.06);
27
+ }
28
+ .header-title { color: #0D47A1; }
29
+ .section-title { color: #0277BD; }
30
+ .button-primary {
31
+ background-color: #0288D1;
32
+ transition: background-color 0.3s ease, transform 0.2s ease, box-shadow 0.2s ease;
33
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
34
+ }
35
+ .button-primary:hover {
36
+ background-color: #0277BD; transform: translateY(-2px);
37
+ box-shadow: 0 5px 10px rgba(0,0,0,0.15);
38
+ }
39
+ #map { height: 320px; border-radius: 0.5rem; border: 1px solid #CFD8DC; box-shadow: 0 2px 5px rgba(0,0,0,0.05); }
40
+
41
+ /* Gemini AI Analysis Content Styling - CRITICAL FOR MARKDOWN */
42
+ .gemini-analysis-content { line-height: 1.65; }
43
+ .gemini-analysis-content h1, .gemini-analysis-content h2, .gemini-analysis-content h3, .gemini-analysis-content h4, .gemini-analysis-content h5, .gemini-analysis-content h6 {
44
+ color: #0277BD; margin-top: 1.5em; margin-bottom: 0.75em; font-weight: 600;
45
+ }
46
+ .gemini-analysis-content h1 { font-size: 2em; border-bottom: 2px solid #80DEEA; padding-bottom: 0.3em;}
47
+ .gemini-analysis-content h2 { font-size: 1.6em; border-bottom: 1px solid #B2EBF2; padding-bottom: 0.2em;}
48
+ .gemini-analysis-content h3 { font-size: 1.3em; }
49
+ .gemini-analysis-content h4 { font-size: 1.15em; color: #0288D1; }
50
+ .gemini-analysis-content p { margin-bottom: 1em; }
51
+ .gemini-analysis-content ul, .gemini-analysis-content ol { margin-left: 1.8em; margin-bottom: 1em; list-style-position: outside; }
52
+ .gemini-analysis-content ul li { list-style-type: disc; }
53
+ .gemini-analysis-content ol li { list-style-type: decimal; }
54
+ .gemini-analysis-content li { margin-bottom: 0.5em; }
55
+ .gemini-analysis-content strong, .gemini-analysis-content b { color: #0D47A1; font-weight: 700; }
56
+ .gemini-analysis-content em, .gemini-analysis-content i { color: #0277BD; font-style: italic; }
57
+ .gemini-analysis-content code {
58
+ background-color: #E0F2F7; color: #01579B;
59
+ padding: 0.25em 0.5em; border-radius: 4px;
60
+ font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
61
+ font-size: 0.9em; border: 1px solid #B2EBF2;
62
+ }
63
+ .gemini-analysis-content pre {
64
+ background-color: #E8F5E9; border: 1px solid #A5D6A7;
65
+ padding: 1em; margin-bottom: 1em; border-radius: 0.375rem;
66
+ overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;
67
+ }
68
+ .gemini-analysis-content pre code { background-color: transparent; border: none; padding: 0; }
69
+ .gemini-analysis-content blockquote {
70
+ border-left: 4px solid #80DEEA; margin-left: 0; padding-left: 1em;
71
+ color: #455A64; font-style: italic; margin-bottom: 1em;
72
+ }
73
+ .gemini-analysis-content hr {
74
+ border: none; border-top: 2px dashed #B0BEC5; margin-top: 2em; margin-bottom: 2em;
75
+ }
76
+ .gemini-analysis-content table { width: 100%; border-collapse: collapse; margin-bottom: 1em; }
77
+ .gemini-analysis-content th, .gemini-analysis-content td { border: 1px solid #B0BEC5; padding: 0.6em; text-align: left; }
78
+ .gemini-analysis-content th { background-color: #E1F5FE; color: #0277BD; }
79
+
80
+
81
+ .risk-yes { color: #D32F2F; } .risk-no { color: #388E3C; }
82
+ .status-error { color: #F57C00; }
83
+
84
+ .table-auto th { background-color: #E1F5FE; color: #0277BD; }
85
+ .table-auto td, .table-auto th { border: 1px solid #B0BEC5; }
86
+
87
+ .future-day-card { border: 1px solid #B0E0E6; transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out; }
88
+ .future-day-card:hover { transform: translateY(-4px); box-shadow: 0 10px 20px rgba(0,0,0,0.12); }
89
+ .future-day-card .risk-high { background-color: #FFEBEE; border-left: 5px solid #D32F2F; }
90
+ .future-day-card .risk-moderate { background-color: #FFF9C4; border-left: 5px solid #FBC02D; }
91
+ .future-day-card .risk-low { background-color: #E8F5E9; border-left: 5px solid #388E3C; }
92
+ .future-day-card .risk-error { background-color: #ECEFF1; border-left: 5px solid #78909C; }
93
+
94
+ input[type="text"]:focus, input[type="number"]:focus { border-color: #0288D1; box-shadow: 0 0 0 2px rgba(2, 136, 209, 0.25); }
95
+ .scrollable-content { max-height: 450px; overflow-y: auto; padding-right: 10px;}
96
+ .scrollable-content::-webkit-scrollbar { width: 8px; }
97
+ .scrollable-content::-webkit-scrollbar-track { background: #E0F7FA; border-radius: 4px;}
98
+ .scrollable-content::-webkit-scrollbar-thumb { background: #80DEEA; border-radius: 4px;}
99
+ .scrollable-content::-webkit-scrollbar-thumb:hover { background: #4DD0E1; }
100
+ </style>
101
+ </head>
102
+ <body class="text-gray-700">
103
+ <div class="container mx-auto p-4 md:p-6 lg:p-8 min-h-screen">
104
+ <header class="text-center mb-10">
105
+ <h1 class="text-4xl md:text-5xl font-bold header-title drop-shadow-lg">
106
+ <i class="fas fa-cloud-bolt mr-3"></i>Cloudburst Prediction System
107
+ </h1>
108
+ <p class="text-cyan-700 text-lg mt-2">Advanced Weather Insights & Risk Assessment</p>
109
+ </header>
110
+
111
+ <section id="input-section" class="mb-10 p-6 rounded-xl shadow-xl content-card">
112
+ <form action="{{ url_for('forecast') }}" method="GET" class="space-y-5">
113
+ <h2 class="text-3xl font-semibold text-center section-title mb-6">Location Input</h2>
114
+ <p class="text-center text-gray-600 text-sm mb-5">Pinpoint on map, search, or enter coordinates manually.</p>
115
+ <div id="map"></div>
116
+ <div class="grid md:grid-cols-2 gap-5">
117
+ <div>
118
+ <label for="lat" class="block text-sm font-medium text-gray-700 mb-1">Latitude:</label>
119
+ <input type="text" id="lat" name="lat" required class="w-full px-4 py-2.5 border border-gray-300 rounded-lg shadow-sm focus:ring-2 focus:ring-cyan-500 focus:border-cyan-500 transition" placeholder="e.g., 28.6139" value="{{ lat_initial if lat_initial else '' }}">
120
+ </div>
121
+ <div>
122
+ <label for="lon" class="block text-sm font-medium text-gray-700 mb-1">Longitude:</label>
123
+ <input type="text" id="lon" name="lon" required class="w-full px-4 py-2.5 border border-gray-300 rounded-lg shadow-sm focus:ring-2 focus:ring-cyan-500 focus:border-cyan-500 transition" placeholder="e.g., 77.2090" value="{{ lon_initial if lon_initial else '' }}">
124
+ </div>
125
+ </div>
126
+ <button type="submit" class="w-full button-primary text-white font-bold py-3.5 px-6 rounded-lg shadow-md text-lg">
127
+ <i class="fas fa-magnifying-glass-chart mr-2"></i>Analyze Cloudburst Risk
128
+ </button>
129
+ {% if error %}
130
+ <div class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 rounded-md mt-4 text-sm" role="alert">
131
+ <p><i class="fas fa-exclamation-triangle mr-2"></i>{{ error }}</p>
132
+ </div>
133
+ {% endif %}
134
+ </form>
135
+ </section>
136
+
137
+ {% if show_results %}
138
+ <div id="results-section" class="space-y-10">
139
+ {% if prediction_error %}
140
+ <div class="p-6 rounded-xl shadow-xl content-card bg-red-50 border-l-4 border-red-600">
141
+ <h3 class="text-xl font-semibold text-red-700 mb-2"><i class="fas fa-shield-virus mr-2"></i>System Alert</h3>
142
+ <p class="text-red-600">{{ prediction_error }}</p>
143
+ </div>
144
+ {% endif %}
145
+
146
+ {% if current_weather and current_prediction and not prediction_error %}
147
+ <section class="p-6 rounded-xl shadow-xl content-card">
148
+ <h2 class="text-3xl font-semibold section-title mb-5 text-center"><i class="fas fa-clock mr-2"></i>Current Conditions & Immediate Risk</h2>
149
+ <div class="grid md:grid-cols-2 gap-6 items-start">
150
+ <div>
151
+ <h3 class="text-xl font-medium text-gray-800 mb-3"><i class="fas fa-map-marker-alt text-cyan-600 mr-2"></i>{{ current_weather['Location Address'] }}</h3>
152
+ <p class="text-sm text-gray-500 mb-3">{{ current_weather['Current Time'] }}</p>
153
+ <div class="bg-sky-50 p-4 rounded-lg space-y-2 text-sm">
154
+ <p class="flex items-center"><i class="fas {{ weather_code_mapping[current_weather['Weather Code']]['icon'] if current_weather['Weather Code'] in weather_code_mapping else 'fa-question-circle' }} text-xl text-cyan-600 w-6 mr-2"></i> <strong class="w-36">Weather:</strong> {{ current_weather['Weather Description (Current)'] }}</p>
155
+ <p><i class="fas fa-temperature-half w-6 mr-2 text-cyan-600"></i><strong class="w-36">Temperature:</strong> {{ current_weather['Temp (2m °C)'] }}°C (Min: {{ current_weather['Min Temp (°C)'] }}°C / Max: {{ current_weather['Max Temp (°C)'] }}°C)</p>
156
+ <p><i class="fas fa-wind w-6 mr-2 text-cyan-600"></i><strong class="w-36">Wind:</strong> {{ current_weather['Wind Speed (Current km/h)'] }} km/h from {{ current_weather['Wind Direction (Current)'] }}</p>
157
+ <p><i class="fas fa-droplet w-6 mr-2 text-cyan-600"></i><strong class="w-36">Humidity (Avg):</strong> {{ current_weather['Humidity (Past Week Avg %)'] }}%</p>
158
+ <p><i class="fas fa-cloud-showers-heavy w-6 mr-2 text-cyan-600"></i><strong class="w-36">Precip. Today:</strong> {{ current_weather['Precipitation Today (Accumulated mm)'] }} mm</p>
159
+ <p><i class="fas fa-umbrella w-6 mr-2 text-cyan-600"></i><strong class="w-36">Precip. Prob (Now):</strong> {{ current_weather['Precipitation Probability (Current Hour %)'] }}%</p>
160
+ </div>
161
+ </div>
162
+ <div class="text-center p-4 rounded-lg {% if current_prediction['Predicted Cloudburst'] == 'Yes' %}bg-red-50 border border-red-200{% elif current_prediction['Predicted Cloudburst'] == 'No' %}bg-green-50 border border-green-200{% else %}bg-gray-100 border border-gray-200{% endif %}">
163
+ <h3 class="text-xl font-medium text-gray-800 mb-3">Immediate Cloudburst Risk</h3>
164
+ {% if current_prediction['Predicted Cloudburst'] == "Error" %}
165
+ <p class="text-3xl font-bold status-error my-4">Prediction Error</p>
166
+ {% else %}
167
+ <p class="text-6xl font-bold my-4 {% if current_prediction['Predicted Cloudburst'] == 'Yes' %}risk-yes{% else %}risk-no{% endif %}">
168
+ {{ current_prediction['Predicted Cloudburst'] }}
169
+ </p>
170
+ <p class="text-3xl text-gray-700">
171
+ Risk Score: <strong class="{% if current_prediction['Predicted Cloudburst (%)'] > 70 %}risk-yes{% elif current_prediction['Predicted Cloudburst (%)'] > 40 %}text-orange-500{% else %}risk-no{% endif %}">{{ current_prediction['Predicted Cloudburst (%)'] }}%</strong>
172
+ </p>
173
+ {% endif %}
174
+ <p class="mt-3 text-xs text-gray-500">A "Yes" indicates conditions are favorable for potential cloudburst activity.</p>
175
+ </div>
176
+ </div>
177
+ </section>
178
+ {% endif %}
179
+
180
+ {% if shap_explanation and shap_explanation[0].feature not in ["SHAP N/A", "SHAP Disabled", "Prediction Disabled", "SHAP Error", "Prediction Error"] and not prediction_error and current_prediction and current_prediction['Predicted Cloudburst'] != "Error" %}
181
+ <section class="p-6 rounded-xl shadow-xl content-card">
182
+ <h2 class="text-3xl font-semibold section-title mb-5 text-center"><i class="fas fa-magnifying-glass-plus mr-2"></i>Key Factors Driving Current Risk (SHAP)</h2>
183
+ <div class="grid md:grid-cols-2 gap-6 items-center">
184
+ <div class="min-h-[350px] md:min-h-[400px]">
185
+ <canvas id="shapChart"></canvas>
186
+ </div>
187
+ <div class="scrollable-content pr-2">
188
+ {% set base_value_item = shap_explanation | selectattr('feature', 'equalto', 'Base Value (Average Prediction)') | first %}
189
+ {% if base_value_item and base_value_item.impact is number %}
190
+ <p class="text-sm text-gray-600 mb-3 p-3 bg-sky-50 rounded-md border border-sky-200">
191
+ <i class="fas fa-calculator mr-1 text-cyan-600"></i> The model's average (base) prediction for cloudburst probability is around <strong class="text-cyan-700">{{ "%.1f" | format(base_value_item.impact) }}%</strong>.
192
+ The features below show how current conditions shift the prediction from this base.
193
+ </p>
194
+ {% endif %}
195
+ <ul class="space-y-1.5 text-sm">
196
+ {% for item in shap_explanation if item.feature != 'Base Value (Average Prediction)' and item.impact is number %}
197
+ <li class="flex justify-between items-center p-3 rounded-md {% if item.impact > 0.1 %}bg-red-50 border border-red-100{% elif item.impact < -0.1 %}bg-green-50 border border-green-100{% else %}bg-gray-50 border border-gray-100{% endif %}">
198
+ <span class="truncate pr-2" title="{{ item.feature }}"><i class="fas {% if item.impact > 0.1 %}fa-arrow-trend-up text-red-500{% elif item.impact < -0.1 %}fa-arrow-trend-down text-green-500{% else %}fa-minus text-gray-400{% endif %} mr-2"></i>{{ item.feature }}</span>
199
+ <span class="font-semibold whitespace-nowrap {% if item.impact > 0.1 %}text-red-600{% elif item.impact < -0.1 %}text-green-600{% else %}text-gray-600{% endif %}">
200
+ {{ "%+.1f" | format(item.impact) }}%
201
+ </span>
202
+ </li>
203
+ {% endfor %}
204
+ </ul>
205
+ <p class="text-xs text-gray-500 mt-3">Positive values increase risk; negative values decrease it. Impacts are % change in probability.</p>
206
+ </div>
207
+ </div>
208
+ </section>
209
+ {% elif shap_explanation and (shap_explanation[0].feature in ["SHAP N/A", "SHAP Disabled", "SHAP Error", "Prediction Error"] or (current_prediction and current_prediction['Predicted Cloudburst'] == "Error")) %}
210
+ <section class="p-6 rounded-xl shadow-xl content-card">
211
+ <h2 class="text-3xl font-semibold section-title mb-2 text-center">Prediction Factors Analysis (SHAP)</h2>
212
+ <div class="bg-amber-50 border-l-4 border-amber-500 text-amber-700 p-4 rounded" role="alert">
213
+ <p class="font-bold"><i class="fas fa-triangle-exclamation mr-2"></i>SHAP Explanation Unavailable</p>
214
+ <p>{{ shap_explanation[0].impact }} {% if current_prediction and current_prediction['Predicted Cloudburst'] == "Error"%}(Prediction failed){%endif%} Check server logs for details if applicable.</p>
215
+ </div>
216
+ </section>
217
+ {% endif %}
218
+
219
+ {% if future_predictions and future_predictions|length > 0 %}
220
+ <section class="p-6 rounded-xl shadow-xl content-card">
221
+ <h2 class="text-3xl font-semibold section-title mb-5 text-center"><i class="fas fa-calendar-alt mr-2"></i>Cloudburst Risk Outlook: Next {{ future_predictions|length }} Days</h2>
222
+ <div class="mb-6 min-h-[300px] md:min-h-[350px]">
223
+ <canvas id="futureProbChart"></canvas>
224
+ </div>
225
+ <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
226
+ {% for pred in future_predictions %}
227
+ {% set risk_level = 'low' %}
228
+ {% if pred.probability is number %}
229
+ {% if pred.probability > 70 %}{% set risk_level = 'high' %}
230
+ {% elif pred.probability > 40 %}{% set risk_level = 'moderate' %}
231
+ {% endif %}
232
+ {% elif pred.status in ['Pred. Err', 'Data Err'] %}{% set risk_level = 'error' %}
233
+ {% endif %}
234
+ <div class="future-day-card p-4 rounded-lg shadow risk-{{risk_level}}">
235
+ <p class="font-bold text-gray-700 text-md">{{ pred.date }}</p>
236
+ <div class="flex items-center my-2">
237
+ <i class="fas {{ weather_code_mapping[pred.weather_code]['icon'] if pred.weather_code in weather_code_mapping else 'fa-question-circle' }} text-3xl mr-3
238
+ {% if risk_level == 'high' %}text-red-500{% elif risk_level == 'moderate' %}text-yellow-600{% elif risk_level == 'low' %}text-cyan-600{% else %}text-gray-500{% endif %}"></i>
239
+ <div>
240
+ <p class="text-sm text-gray-600">{{ pred.weather_description }}</p>
241
+ <p class="text-xs text-gray-500">{{ pred.min_temp }} / {{ pred.max_temp }}</p>
242
+ </div>
243
+ </div>
244
+ <div class="mt-1 text-center">
245
+ {% if pred.probability is number %}
246
+ <p class="text-lg font-semibold {% if risk_level == 'high' %}text-red-700{% elif risk_level == 'moderate' %}text-amber-700{% else %}text-green-700{% endif %}">
247
+ Risk: {{ pred.probability }}%
248
+ </p>
249
+ {% else %}
250
+ <p class="text-sm font-semibold status-error">{{ pred.status }}</p>
251
+ {% endif %}
252
+ </div>
253
+ </div>
254
+ {% endfor %}
255
+ </div>
256
+ </section>
257
+ {% endif %}
258
+
259
+ {% if gemini_analysis %}
260
+ <section class="p-6 rounded-xl shadow-xl content-card">
261
+ <h2 class="text-3xl font-semibold section-title mb-5 text-center"><i class="fas fa-brain text-cyan-600 mr-2"></i>AI Expert Analysis & Recommendations</h2>
262
+ {% if gemini_analysis.error %}
263
+ <div class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 rounded" role="alert">
264
+ <p class="font-bold"><i class="fas fa-exclamation-circle mr-2"></i>AI Analysis Error</p>
265
+ <p>{{ gemini_analysis.error }}</p>
266
+ </div>
267
+ {% elif gemini_analysis.analysis %}
268
+ <div class="gemini-analysis-content scrollable-content p-3 bg-white border border-gray-200 rounded-md shadow-inner">
269
+ {{ gemini_analysis.analysis | safe }} {# CRITICAL: | safe filter renders HTML from Markdown #}
270
+ </div>
271
+ <p class="text-xs text-center text-gray-500 mt-4">Analysis by Google Gemini. May require interpretation.</p>
272
+ {% else %}
273
+ <p class="text-gray-600 text-center py-4">AI analysis is currently unavailable.</p>
274
+ {% endif %}
275
+ </section>
276
+ {% endif %}
277
+ </div>
278
+ {% endif %} {# end show_results #}
279
+
280
+ <footer class="text-center mt-12 py-6 border-t border-cyan-200">
281
+ <p class="text-sm text-cyan-800">© {{ current_year }} Cloudburst Predictor Pro. Advanced Weather Intelligence.</p>
282
+ <p class="text-xs text-cyan-700 mt-1">Data: Open-Meteo. AI: Google Gemini. Explainability: SHAP & DiCE. Map: Leaflet & OpenStreetMap.</p>
283
+ </footer>
284
+ </div>
285
+
286
+ <script>
287
+ const weatherCodeMapping = JSON.parse('{{ weather_code_mapping_json | safe }}');
288
+ document.addEventListener('DOMContentLoaded', function () {
289
+ const latInput = document.getElementById('lat');
290
+ const lonInput = document.getElementById('lon');
291
+ let initialLat = parseFloat(latInput.value) || {{ lat_initial|default(20.5937) }};
292
+ let initialLon = parseFloat(lonInput.value) || {{ lon_initial|default(78.9629) }};
293
+ let initialZoom = latInput.value && lonInput.value && !isNaN(initialLat) && !isNaN(initialLon) ? 10 : 5;
294
+
295
+ const map = L.map('map').setView([initialLat, initialLon], initialZoom);
296
+ L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
297
+ attribution: '© OpenStreetMap contributors', maxZoom: 18
298
+ }).addTo(map);
299
+ let marker = null;
300
+ if (latInput.value && lonInput.value && !isNaN(initialLat) && !isNaN(initialLon)) {
301
+ marker = L.marker([initialLat, initialLon], {draggable: true}).addTo(map);
302
+ marker.on('dragend', updateInputsFromMarker);
303
+ }
304
+
305
+ L.Control.geocoder({ defaultMarkGeocode: false, placeholder: "Search city or address..." })
306
+ .on('markgeocode', function(e) {
307
+ const latlng = e.geocode.center;
308
+ map.setView(latlng, 11);
309
+ if (marker) { marker.setLatLng(latlng); }
310
+ else { marker = L.marker(latlng, {draggable: true}).addTo(map); marker.on('dragend', updateInputsFromMarker); }
311
+ latInput.value = latlng.lat.toFixed(5);
312
+ lonInput.value = latlng.lng.toFixed(5);
313
+ }).addTo(map);
314
+
315
+ function updateInputsFromMarker(e) {
316
+ const latlng = e.target.getLatLng();
317
+ latInput.value = latlng.lat.toFixed(5);
318
+ lonInput.value = latlng.lng.toFixed(5);
319
+ }
320
+
321
+ map.on('click', function(e) {
322
+ if (marker) { marker.setLatLng(e.latlng); }
323
+ else { marker = L.marker(e.latlng, {draggable: true}).addTo(map); marker.on('dragend', updateInputsFromMarker); }
324
+ latInput.value = e.latlng.lat.toFixed(5);
325
+ lonInput.value = e.latlng.lng.toFixed(5);
326
+ });
327
+
328
+ {% if show_results %}
329
+ const chartOptions = { responsive: true, maintainAspectRatio: false, animation: {duration: 600, easing: 'easeInOutQuart'} };
330
+
331
+ {% if shap_chart_data_json and shap_chart_data_json != "{}" %}
332
+ try {
333
+ const shapData = JSON.parse('{{ shap_chart_data_json | safe }}');
334
+ if (shapData.labels && shapData.labels.length > 0) {
335
+ const shapCtx = document.getElementById('shapChart')?.getContext('2d');
336
+ if (shapCtx) {
337
+ new Chart(shapCtx, {
338
+ type: 'bar',
339
+ data: {
340
+ labels: shapData.labels,
341
+ datasets: [{
342
+ label: 'SHAP Impact (% Change in Prob.)',
343
+ data: shapData.data,
344
+ backgroundColor: shapData.data.map(v => v >= 0 ? 'rgba(255, 99, 132, 0.75)' : 'rgba(75, 192, 192, 0.75)'),
345
+ borderColor: shapData.data.map(v => v >= 0 ? 'rgba(255, 99, 132, 1)' : 'rgba(75, 192, 192, 1)'),
346
+ borderWidth: 1
347
+ }]
348
+ },
349
+ options: { ...chartOptions, indexAxis: 'y',
350
+ scales: { x: { beginAtZero: true, title: { display: true, text: 'Contribution to Cloudburst Probability (%)', font: {size: 12, weight: '500'}}, grid: { color: 'rgba(0,0,0,0.05)'} } ,
351
+ y: { ticks: { font: {size: 10} }, grid: { display: false } }
352
+ },
353
+ plugins: { legend: { display: false }, tooltip: {callbacks: {label: (c) => `${c.dataset.label}: ${c.raw.toFixed(1)}%`}} }
354
+ }
355
+ });
356
+ }
357
+ } else {
358
+ document.getElementById('shapChart')?.parentElement?.insertAdjacentHTML('beforeend', '<p class="text-center text-sm text-gray-500 py-4">No SHAP data to display or data is invalid.</p>');
359
+ }
360
+ } catch (e) { console.error("SHAP chart error:", e); document.getElementById('shapChart')?.parentElement?.insertAdjacentHTML('beforeend', '<p class="text-center text-sm text-red-500 py-4">Error loading SHAP chart. See console.</p>'); }
361
+ {% endif %}
362
+
363
+ {% if future_prob_chart_data_json and future_prob_chart_data_json != "{}" %}
364
+ try {
365
+ const futureProbData = JSON.parse('{{ future_prob_chart_data_json | safe }}');
366
+ if (futureProbData.labels && futureProbData.labels.length > 0) {
367
+ const futureProbCtx = document.getElementById('futureProbChart')?.getContext('2d');
368
+ if (futureProbCtx) {
369
+ new Chart(futureProbCtx, {
370
+ type: 'line',
371
+ data: {
372
+ labels: futureProbData.labels,
373
+ datasets: [{
374
+ label: 'Cloudburst Risk (%)',
375
+ data: futureProbData.data,
376
+ borderColor: '#0288D1', backgroundColor: 'rgba(2, 136, 209, 0.2)',
377
+ tension: 0.35, fill: true, pointRadius: 5, pointHoverRadius: 7, pointBackgroundColor: '#01579B', borderWidth: 2
378
+ }]
379
+ },
380
+ options: { ...chartOptions,
381
+ scales: { y: { beginAtZero: true, max: 100, title: { display: true, text: 'Risk Probability (%)', font: {size: 12, weight: '500'}}, grid: { color: 'rgba(0,0,0,0.05)'} },
382
+ x: { ticks: { font: {size: 10} }, grid: { display: false } }
383
+ },
384
+ plugins: { legend: { display: true, position: 'top', labels:{boxWidth:15, padding:15, font:{size:11, weight: '500'}} }, tooltip: {callbacks: {label: (c) => `${c.dataset.label}: ${c.raw}%`}} }
385
+ }
386
+ });
387
+ }
388
+ } else {
389
+ document.getElementById('futureProbChart')?.parentElement?.insertAdjacentHTML('beforeend', '<p class="text-center text-sm text-gray-500 py-4">No future probability data to display or data is invalid.</p>');
390
+ }
391
+ } catch (e) { console.error("Future prob chart error:", e); document.getElementById('futureProbChart')?.parentElement?.insertAdjacentHTML('beforeend', '<p class="text-center text-sm text-red-500 py-4">Error loading future risk chart. See console.</p>');}
392
+ {% endif %}
393
+ {% endif %}
394
+ });
395
+ </script>
396
+ </body>
397
+ </html>
templates/index2.html ADDED
@@ -0,0 +1,219 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Cloudburst Prediction System</title>
7
+ <!-- Tailwind CSS -->
8
+ <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/tailwind.min.css" rel="stylesheet">
9
+ <!-- Leaflet CSS -->
10
+ <link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css" />
11
+ <!-- Font Awesome -->
12
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css" />
13
+ <style>
14
+ /* Custom Animations and Styles */
15
+ body {
16
+ background: linear-gradient(135deg, #e6f2ff 0%, #ffffff 100%);
17
+ }
18
+ .card-glow {
19
+ transition: all 0.3s ease;
20
+ box-shadow: 0 4px 6px rgba(0,0,0,0.1);
21
+ }
22
+ .card-glow:hover {
23
+ transform: translateY(-10px);
24
+ box-shadow: 0 15px 25px rgba(0,0,0,0.15);
25
+ }
26
+ .shine-effect {
27
+ position: relative;
28
+ overflow: hidden;
29
+ }
30
+ .shine-effect::before {
31
+ content: '';
32
+ position: absolute;
33
+ top: 0;
34
+ left: -100%;
35
+ width: 100%;
36
+ height: 100%;
37
+ background: linear-gradient(120deg, transparent, rgba(255,255,255,0.3), transparent);
38
+ transition: all 0.6s;
39
+ }
40
+ .shine-effect:hover::before {
41
+ left: 100%;
42
+ }
43
+ #map {
44
+ height: 400px;
45
+ width: 100%;
46
+ border-radius: 12px;
47
+ box-shadow: 0 10px 25px rgba(0,0,0,0.1);
48
+ }
49
+ .animate-float {
50
+ animation: float 3s ease-in-out infinite;
51
+ }
52
+ @keyframes float {
53
+ 0%, 100% { transform: translateY(0); }
54
+ 50% { transform: translateY(-10px); }
55
+ }
56
+ .cloud-bg {
57
+ background: linear-gradient(135deg, #87CEEB 0%, #B0E0E6 100%);
58
+ }
59
+ </style>
60
+ </head>
61
+ <body class="font-sans antialiased">
62
+ <!-- Header with Animated Elements -->
63
+ <header class="cloud-bg py-6 shadow-lg relative overflow-hidden">
64
+ <div class="absolute left-4 top-4 animate-float">
65
+ <i class="fas fa-cloud text-white text-5xl opacity-80"></i>
66
+ </div>
67
+ <div class="container mx-auto text-center relative z-10">
68
+ <h1 class="text-5xl font-extrabold text-white drop-shadow-lg mb-2 animate-pulse">
69
+ Cloudburst Prediction System
70
+ </h1>
71
+ <p class="text-white text-lg opacity-80 animate-bounce">
72
+ Advanced Weather Forecasting Technology
73
+ </p>
74
+ </div>
75
+ </header>
76
+
77
+ <!-- Main Content Container -->
78
+ <div class="container mx-auto px-4 py-8">
79
+ <!-- Input and Map Section -->
80
+ <div class="grid md:grid-cols-2 gap-8">
81
+ <!-- Map Section -->
82
+ <div class="bg-white rounded-2xl p-6 shadow-xl card-glow shine-effect">
83
+ <div class="mb-4">
84
+ <input type="text" id="searchInput" placeholder="Search location"
85
+ class="w-full border-2 border-blue-200 rounded-lg px-4 py-3 focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all">
86
+ <button id="searchBtn" class="mt-3 w-full bg-blue-500 text-white py-3 rounded-lg hover:bg-blue-600 transition-all">
87
+ <i class="fas fa-search mr-2"></i>Search Location
88
+ </button>
89
+ </div>
90
+ <div id="map" class="rounded-xl"></div>
91
+ </div>
92
+
93
+ <!-- Coordinates Input Section -->
94
+ <div class="bg-white rounded-2xl p-6 shadow-xl card-glow shine-effect">
95
+ <form action="/forecast" method="get" class="space-y-4">
96
+ <div>
97
+ <label class="block text-gray-700 font-semibold mb-2">
98
+ <i class="fas fa-map-marker-alt mr-2"></i>Latitude
99
+ </label>
100
+ <input type="text" id="lat" name="lat" required
101
+ class="w-full border-2 border-blue-200 rounded-lg px-4 py-3 focus:outline-none focus:ring-2 focus:ring-blue-500">
102
+ </div>
103
+ <div>
104
+ <label class="block text-gray-700 font-semibold mb-2">
105
+ <i class="fas fa-map-marker-alt mr-2"></i>Longitude
106
+ </label>
107
+ <input type="text" id="lon" name="lon" required
108
+ class="w-full border-2 border-blue-200 rounded-lg px-4 py-3 focus:outline-none focus:ring-2 focus:ring-blue-500">
109
+ </div>
110
+ <button type="submit" class="w-full bg-green-500 text-white py-3 rounded-lg hover:bg-green-600 transition-all">
111
+ <i class="fas fa-paper-plane mr-2"></i>Get Forecast
112
+ </button>
113
+ </form>
114
+ </div>
115
+ </div>
116
+
117
+ <!-- Forecast Results (Conditionally Rendered) -->
118
+ {% if result %}
119
+ <div class="mt-8 space-y-8">
120
+ <!-- Weather Parameters Grid -->
121
+ <div class="bg-white rounded-2xl p-6 shadow-xl card-glow">
122
+ <h2 class="text-3xl font-bold text-center mb-6 text-blue-600">
123
+ Weather Parameters
124
+ </h2>
125
+ <div class="grid md:grid-cols-3 gap-6">
126
+ {% for key, value in result.items() %}
127
+ {% if key not in ['Predicted Cloudburst (%)', 'Predicted Cloudburst'] %}
128
+ <div class="bg-blue-50 border border-blue-100 rounded-xl p-4 transform transition hover:scale-105 hover:shadow-lg">
129
+ <h3 class="text-lg font-semibold text-blue-700 mb-2">{{ key }}</h3>
130
+ <p class="text-gray-700 font-medium">{{ value }}</p>
131
+ </div>
132
+ {% endif %}
133
+ {% endfor %}
134
+ </div>
135
+ </div>
136
+
137
+ <!-- Prediction Visualization (Left/Right Layout) -->
138
+ <div class="bg-white rounded-2xl p-6 shadow-xl">
139
+ <div class="grid md:grid-cols-2 gap-8 items-center">
140
+ <!-- Left: Cloudburst Probability -->
141
+ <div class="text-center">
142
+ <h2 class="text-2xl font-semibold text-gray-700 mb-4">
143
+ Cloudburst Probability
144
+ </h2>
145
+ <div class="text-6xl font-bold text-blue-600">
146
+ {{ result["Predicted Cloudburst (%)"] }}%
147
+ </div>
148
+ </div>
149
+
150
+ <!-- Right: Cloudburst Prediction Status -->
151
+ <div class="text-center">
152
+ <h2 class="text-2xl font-semibold text-gray-700 mb-4">
153
+ Cloudburst Prediction Status
154
+ </h2>
155
+ {% if result['Predicted Cloudburst'] == "Yes" %}
156
+ <div class="text-6xl font-bold text-red-600">
157
+ YES
158
+ </div>
159
+ {% else %}
160
+ <div class="text-6xl font-bold text-green-600">
161
+ NO
162
+ </div>
163
+ {% endif %}
164
+ </div>
165
+ </div>
166
+ </div>
167
+ </div>
168
+ {% endif %}
169
+ </div>
170
+
171
+ <!-- Scripts -->
172
+ <script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
173
+ <script>
174
+ // Leaflet Map Initialization
175
+ var map = L.map('map').setView([20.5937, 78.9629], 5);
176
+ L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
177
+ attribution: '&copy; OpenStreetMap contributors'
178
+ }).addTo(map);
179
+
180
+ var marker;
181
+
182
+ // Map Click Event
183
+ map.on('click', function(e) {
184
+ var lat = e.latlng.lat.toFixed(6);
185
+ var lon = e.latlng.lng.toFixed(6);
186
+ document.getElementById('lat').value = lat;
187
+ document.getElementById('lon').value = lon;
188
+ if (marker) {
189
+ marker.setLatLng(e.latlng);
190
+ } else {
191
+ marker = L.marker(e.latlng).addTo(map);
192
+ }
193
+ });
194
+
195
+ // Search Functionality
196
+ document.getElementById("searchBtn").addEventListener("click", function() {
197
+ var address = document.getElementById("searchInput").value;
198
+ if(address) {
199
+ fetch(`https://nominatim.openstreetmap.org/search?format=json&q=${address}`)
200
+ .then(response => response.json())
201
+ .then(data => {
202
+ if(data && data.length > 0) {
203
+ var lat = data[0].lat;
204
+ var lon = data[0].lon;
205
+ map.setView([lat, lon], 13);
206
+ if(marker) {
207
+ marker.setLatLng([lat, lon]);
208
+ } else {
209
+ marker = L.marker([lat, lon]).addTo(map);
210
+ }
211
+ document.getElementById('lat').value = parseFloat(lat).toFixed(6);
212
+ document.getElementById('lon').value = parseFloat(lon).toFixed(6);
213
+ }
214
+ });
215
+ }
216
+ });
217
+ </script>
218
+ </body>
219
+ </html>