├── disaster_assets_dataset.xlsx ├── README.md ├── file └── real_time_disaster_platform.py /disaster_assets_dataset.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Otutu11/Real-Time-Disaster-Impact-Analytics-Platform/main/disaster_assets_dataset.xlsx -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Real-Time Disaster Impact Analytics Platform 2 | 📌 Overview 3 | 4 | This project is a synthetic data demonstration of a real-time disaster impact analytics system. It simulates natural hazard events (e.g., floods and windstorms) and computes their spatiotemporal impacts on vulnerable assets within the Niger Delta region. 5 | 6 | It integrates: 7 | 8 | 🛰️ Geospatial data modeling 9 | 10 | 🤖 Impact estimation using vulnerability, population, and criticality factors 11 | 12 | 📈 Rolling real-time analytics and anomaly detection 13 | 14 | 🗺️ Visualization of disaster impact hotspots 15 | 16 | This framework serves as a foundation for building operational disaster monitoring systems for climate resilience, early warning, and risk mitigation. 17 | 18 | ⚙️ Features 19 | 20 | Generate >1,000 synthetic asset points with attributes (location, population, vulnerability). 21 | 22 | Simulate real-time disaster event streams (flood/wind). 23 | 24 | Compute impact severity scores based on exposure and vulnerability. 25 | 26 | Perform rolling-minute analytics and z-score anomaly detection. 27 | 28 | Output: 29 | 30 | assets.csv — synthetic assets database 31 | 32 | hazard_events.csv — simulated disaster events 33 | 34 | impacts_stream.csv — asset-level event impacts 35 | 36 | impact_summary_by_minute.csv — aggregated rolling analytics 37 | 38 | latest_impact_map.png — visualization of recent impacts 39 | 40 | 🧠 Methodology 41 | 42 | Impact Score Formula 43 | 44 | Impact = hazard_intensity × sqrt(population) × vulnerability × critical_boost 45 | 46 | 47 | hazard_intensity: Decays with distance from event center (Gaussian-like) 48 | 49 | critical_boost: +25% for critical infrastructure 50 | 51 | Normalized to 0–100 and categorized into severity bands: Minimal, Minor, Moderate, Severe, Extreme 52 | 53 | Anomaly Detection 54 | 55 | Rolling 15-minute window 56 | 57 | Z-score ≥ 2.5 triggers anomaly flags on: 58 | 59 | Number of affected assets 60 | 61 | Extreme events 62 | 63 | 90th percentile impact 64 | 65 | 📂 Project Structure 66 | real_time_disaster_platform.py 67 | outputs/ 68 | ├─ assets.csv 69 | ├─ hazard_events.csv 70 | ├─ impacts_stream.csv 71 | ├─ impact_summary_by_minute.csv 72 | └─ latest_impact_map.png 73 | README.md 74 | 75 | 🚀 Usage 76 | 1. Requirements 77 | 78 | Python 3.9+ 79 | 80 | Libraries: numpy, pandas, matplotlib 81 | 82 | Install dependencies: 83 | 84 | pip install numpy pandas matplotlib 85 | 86 | 2. Run the Platform 87 | python real_time_disaster_platform.py 88 | 89 | 3. View Outputs 90 | 91 | All generated files will be stored in the outputs/ folder. 92 | 93 | Open latest_impact_map.png to view the most recent impact hotspots. 94 | 95 | 📊 Example Outputs 96 | 97 | Assets Dataset: 1,500 synthetic points across the Niger Delta region. 98 | 99 | Events: 240 simulated events (~4 hours at 1 per minute). 100 | 101 | Impact Scores: Severity-labeled, normalized 0–100. 102 | 103 | Anomalies: Automatic detection of unusual surges in impact metrics. 104 | 105 | 📌 Future Extensions 106 | 107 | Integration of real hazard feeds (satellite + sensor data) 108 | 109 | Streamlit-based real-time dashboard 110 | 111 | Multi-hazard modeling (flood, wind, wildfire, landslide) 112 | 113 | Asset economic valuation for cost–benefit analysis 114 | 115 | 📜 License 116 | 117 | MIT License — you are free to use, modify, and distribute this code with attribution. 118 | 119 | 📧 Contact 120 | 121 | Developer: Otutu Anslem 122 | github: @Otutu11 123 | -------------------------------------------------------------------------------- /file: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Real-Time-Disaster-Impact-Analytics-Platform (Synthetic Data Demo) 4 | ------------------------------------------------------------------ 5 | This standalone script simulates a streaming disaster feed and computes 6 | real-time impact analytics on a synthetic asset inventory (>100 points). 7 | 8 | Features 9 | - Generates > 1,000 synthetic assets (lat/lon, population, vulnerability). 10 | - Simulates a stream of hazard events (e.g., flood/wind) with spatial footprints. 11 | - Computes per-asset impact in (near) real time with decay by distance. 12 | - Rolling analytics (per-minute aggregate, anomaly detection via z-score). 13 | - Saves CSV artifacts and a PNG map of the latest impacts. 14 | 15 | USAGE (no dependencies beyond numpy/pandas/matplotlib): 16 | python real_time_disaster_platform.py 17 | 18 | Outputs created in ./outputs/ : 19 | - assets.csv 20 | - hazard_events.csv 21 | - impacts_stream.csv (long format per event-asset impact) 22 | - impact_summary_by_minute.csv 23 | - latest_impact_map.png 24 | 25 | This demo uses a Niger Delta–like bounding box for geospatial realism. 26 | Author: ChatGPT 27 | """ 28 | 29 | from __future__ import annotations 30 | import os 31 | import math 32 | import time 33 | import uuid 34 | import random 35 | from dataclasses import dataclass 36 | from typing import List, Tuple 37 | 38 | import numpy as np 39 | import pandas as pd 40 | import matplotlib.pyplot as plt 41 | 42 | # ------------------------------- Configuration ------------------------------- 43 | 44 | RNG_SEED = 42 45 | ASSET_COUNT = 1500 # > 100 points (synthetic assets) 46 | EVENT_COUNT = 240 # ~4 hours at 1/min if you like to imagine 47 | ASSET_BOUNDS = { # Niger Delta-ish bbox (approx) 48 | "lat_min": 4.3, "lat_max": 6.2, 49 | "lon_min": 5.0, "lon_max": 7.5 50 | } 51 | HAZARD_TYPES = ["flood", "wind"] 52 | EVENT_RADIUS_RANGE_KM = (10, 80) # spatial footprint radius 53 | EVENT_INTENSITY_RANGE = (0.4, 1.0) # base intensity multiplier 54 | CRITICAL_PROB = 0.1 # % of assets as critical infrastructure 55 | ROLLING_WINDOW_MIN = 15 # analytics window for anomaly detection 56 | 57 | OUTPUT_DIR = os.path.join(os.path.dirname(__file__), "outputs") 58 | os.makedirs(OUTPUT_DIR, exist_ok=True) 59 | 60 | np.random.seed(RNG_SEED) 61 | random.seed(RNG_SEED) 62 | 63 | 64 | # ------------------------------- Data Classes -------------------------------- 65 | 66 | @dataclass 67 | class HazardEvent: 68 | event_id: str 69 | timestamp: pd.Timestamp 70 | hazard_type: str 71 | center_lat: float 72 | center_lon: float 73 | radius_km: float 74 | base_intensity: float 75 | 76 | 77 | # ------------------------------- Generators ---------------------------------- 78 | 79 | def generate_assets(n: int) -> pd.DataFrame: 80 | """Generate synthetic assets within a bounding box.""" 81 | lat = np.random.uniform(ASSET_BOUNDS["lat_min"], ASSET_BOUNDS["lat_max"], n) 82 | lon = np.random.uniform(ASSET_BOUNDS["lon_min"], ASSET_BOUNDS["lon_max"], n) 83 | 84 | # Population and vulnerability 85 | population = np.random.lognormal(mean=6.5, sigma=0.6, size=n).astype(int) # ~exp(6.5) ~ 665 86 | vulnerability = np.clip(np.random.normal(0.5, 0.15, n), 0.05, 0.95) 87 | 88 | # Infrastructure criticality 89 | critical = np.random.rand(n) < CRITICAL_PROB 90 | 91 | assets = pd.DataFrame({ 92 | "asset_id": [f"A{i:05d}" for i in range(n)], 93 | "lat": lat, 94 | "lon": lon, 95 | "population": population, 96 | "vulnerability": vulnerability, 97 | "is_critical": critical 98 | }) 99 | return assets 100 | 101 | 102 | def simulate_event(t0: pd.Timestamp, minute_offset: int) -> HazardEvent: 103 | """Create a single hazard event at t0 + minute_offset minutes.""" 104 | hazard_type = random.choice(HAZARD_TYPES) 105 | 106 | # Centers biased toward clusters (hotspots) for realism 107 | hotspot_centers = [ 108 | (5.5, 6.7), # near Port Harcourt-ish 109 | (5.3, 5.5), # coastal area 110 | (6.0, 7.2) # inland 111 | ] 112 | if random.random() < 0.7: 113 | base_lat, base_lon = random.choice(hotspot_centers) 114 | jitter = np.random.normal(0, 0.15, 2) 115 | center_lat = float(np.clip(base_lat + jitter[0], ASSET_BOUNDS["lat_min"], ASSET_BOUNDS["lat_max"])) 116 | center_lon = float(np.clip(base_lon + jitter[1], ASSET_BOUNDS["lon_min"], ASSET_BOUNDS["lon_max"])) 117 | else: 118 | center_lat = float(np.random.uniform(ASSET_BOUNDS["lat_min"], ASSET_BOUNDS["lat_max"])) 119 | center_lon = float(np.random.uniform(ASSET_BOUNDS["lon_min"], ASSET_BOUNDS["lon_max"])) 120 | 121 | radius_km = float(np.random.uniform(*EVENT_RADIUS_RANGE_KM)) 122 | base_intensity = float(np.random.uniform(*EVENT_INTENSITY_RANGE)) 123 | 124 | return HazardEvent( 125 | event_id=str(uuid.uuid4())[:8], 126 | timestamp=t0 + pd.Timedelta(minutes=minute_offset), 127 | hazard_type=hazard_type, 128 | center_lat=center_lat, 129 | center_lon=center_lon, 130 | radius_km=radius_km, 131 | base_intensity=base_intensity 132 | ) 133 | 134 | 135 | def haversine_km(lat1, lon1, lat2, lon2): 136 | """Calculate great-circle distance between two points on Earth (km).""" 137 | R = 6371.0 138 | phi1, phi2 = np.radians(lat1), np.radians(lat2) 139 | dphi = np.radians(lat2 - lat1) 140 | dlambda = np.radians(lon2 - lon1) 141 | a = np.sin(dphi/2.0)**2 + np.cos(phi1) * np.cos(phi2) * np.sin(dlambda/2.0)**2 142 | return R * 2 * np.arctan2(np.sqrt(a), np.sqrt(1 - a)) 143 | 144 | 145 | def event_impact_on_assets(event: HazardEvent, assets: pd.DataFrame) -> pd.DataFrame: 146 | """Compute impact of a single event on all assets with radial decay.""" 147 | # Vectorized distance calc 148 | d_km = haversine_km(event.center_lat, event.center_lon, assets["lat"].values, assets["lon"].values) 149 | 150 | # Only assets within ~3 * radius feel some impact (soft cutoff) 151 | influence_mask = d_km <= (event.radius_km * 3) 152 | affected = assets.loc[influence_mask].copy() 153 | if affected.empty: 154 | return pd.DataFrame(columns=[ 155 | "event_id","asset_id","timestamp","hazard_type","distance_km", 156 | "hazard_intensity","impact_score","impact_severity" 157 | ]) 158 | 159 | # Intensity decays with distance (Gaussian-like), clipping small values 160 | intensity = event.base_intensity * np.exp(-(d_km[influence_mask] / event.radius_km)**2) 161 | intensity = np.clip(intensity, 0, None) 162 | 163 | # Impact combines intensity, population exposure, and vulnerability 164 | exposure = affected["population"].values ** 0.5 # diminishing returns on population 165 | vulnerability = affected["vulnerability"].values 166 | critical_boost = np.where(affected["is_critical"].values, 1.25, 1.0) 167 | 168 | impact = intensity * exposure * vulnerability * critical_boost 169 | 170 | # Normalize impact to a 0..100 scale for interpretability 171 | if impact.max() > 0: 172 | impact_norm = 100 * (impact / impact.max()) 173 | else: 174 | impact_norm = impact 175 | 176 | # Severity bands 177 | bins = [-np.inf, 5, 20, 40, 70, np.inf] 178 | labels = ["Minimal", "Minor", "Moderate", "Severe", "Extreme"] 179 | severity = pd.cut(impact_norm, bins=bins, labels=labels) 180 | 181 | out = pd.DataFrame({ 182 | "event_id": event.event_id, 183 | "asset_id": affected["asset_id"].values, 184 | "timestamp": event.timestamp, 185 | "hazard_type": event.hazard_type, 186 | "distance_km": np.round(d_km[influence_mask], 2), 187 | "hazard_intensity": np.round(intensity, 4), 188 | "impact_score": np.round(impact_norm, 2), 189 | "impact_severity": severity.astype(str) 190 | }) 191 | return out 192 | 193 | 194 | # ------------------------------ Analytics Utils ------------------------------ 195 | 196 | def zscore_anomalies(series: pd.Series, window: int = ROLLING_WINDOW_MIN, z_thresh: float = 2.5) -> pd.Series: 197 | """Return a boolean Series where values are anomalous by rolling z-score.""" 198 | roll_mean = series.rolling(window=window, min_periods=max(3, window//3)).mean() 199 | roll_std = series.rolling(window=window, min_periods=max(3, window//3)).std(ddof=0) 200 | z = (series - roll_mean) / (roll_std.replace(0, np.nan)) 201 | return (z.abs() >= z_thresh).fillna(False) 202 | 203 | 204 | def summarize_per_minute(impacts: pd.DataFrame) -> pd.DataFrame: 205 | """Aggregate impacts per minute and compute anomaly flags.""" 206 | g = impacts.groupby(pd.Grouper(key="timestamp", freq="1min")) 207 | summary = g.agg( 208 | events=("event_id", "nunique"), 209 | affected_assets=("asset_id", "nunique"), 210 | mean_impact=("impact_score", "mean"), 211 | p90_impact=("impact_score", lambda x: np.nanpercentile(x, 90)), 212 | extreme_hits=("impact_severity", lambda s: (s == "Extreme").sum()), 213 | severe_hits=("impact_severity", lambda s: (s == "Severe").sum()), 214 | ).reset_index() 215 | 216 | # Fill NA for early windows 217 | summary[["mean_impact","p90_impact"]] = summary[["mean_impact","p90_impact"]].fillna(0) 218 | 219 | # Anomaly flags 220 | summary["anomaly_affected_assets"] = zscore_anomalies(summary["affected_assets"]) 221 | summary["anomaly_extreme"] = zscore_anomalies(summary["extreme_hits"]) 222 | summary["anomaly_p90"] = zscore_anomalies(summary["p90_impact"]) 223 | 224 | return summary 225 | 226 | 227 | # --------------------------------- Plotting ---------------------------------- 228 | 229 | def plot_latest_map(assets: pd.DataFrame, impacts: pd.DataFrame, outpath: str): 230 | """Scatter plot of latest-minute impacts over asset locations.""" 231 | if impacts.empty: 232 | return 233 | 234 | latest_minute = impacts["timestamp"].max().floor("min") 235 | latest = impacts[impacts["timestamp"].dt.floor("min") == latest_minute] 236 | 237 | # Join for coordinates 238 | latest = latest.merge(assets[["asset_id","lat","lon"]], on="asset_id", how="left") 239 | 240 | fig, ax = plt.subplots(figsize=(8, 7)) 241 | sc = ax.scatter(latest["lon"], latest["lat"], 242 | s=10 + latest["impact_score"] * 0.8, 243 | c=latest["impact_score"], 244 | alpha=0.8) 245 | plt.colorbar(sc, ax=ax, label="Impact Score (0-100)") 246 | 247 | ax.set_title(f"Latest Impacts @ {latest_minute} (n={len(latest)})") 248 | ax.set_xlabel("Longitude") 249 | ax.set_ylabel("Latitude") 250 | ax.set_xlim(ASSET_BOUNDS["lon_min"]-0.1, ASSET_BOUNDS["lon_max"]+0.1) 251 | ax.set_ylim(ASSET_BOUNDS["lat_min"]-0.1, ASSET_BOUNDS["lat_max"]+0.1) 252 | ax.grid(True, linestyle="--", alpha=0.3) 253 | 254 | # Add a rough bbox frame label 255 | ax.text(0.02, 0.98, "Niger Delta (approx bbox)", transform=ax.transAxes, 256 | va="top", ha="left", fontsize=9, alpha=0.7) 257 | 258 | plt.tight_layout() 259 | fig.savefig(outpath, dpi=150) 260 | plt.close(fig) 261 | 262 | 263 | # --------------------------------- Pipeline ---------------------------------- 264 | 265 | def main(): 266 | # 1) Assets 267 | assets = generate_assets(ASSET_COUNT) 268 | assets_path = os.path.join(OUTPUT_DIR, "assets.csv") 269 | assets.to_csv(assets_path, index=False) 270 | 271 | # 2) Generate event stream 272 | start_time = pd.Timestamp.utcnow().floor("min") 273 | events: List[HazardEvent] = [simulate_event(start_time, i) for i in range(EVENT_COUNT)] 274 | events_df = pd.DataFrame([e.__dict__ for e in events]) 275 | events_path = os.path.join(OUTPUT_DIR, "hazard_events.csv") 276 | events_df.to_csv(events_path, index=False) 277 | 278 | # 3) Stream processing (no sleeps in demo; iterate quickly) 279 | impact_rows = [] 280 | for ev in events: 281 | impacts_ev = event_impact_on_assets(ev, assets) 282 | if not impacts_ev.empty: 283 | impact_rows.append(impacts_ev) 284 | 285 | if impact_rows: 286 | impacts = pd.concat(impact_rows, ignore_index=True) 287 | else: 288 | impacts = pd.DataFrame(columns=[ 289 | "event_id","asset_id","timestamp","hazard_type","distance_km", 290 | "hazard_intensity","impact_score","impact_severity" 291 | ]) 292 | 293 | impacts["timestamp"] = pd.to_datetime(impacts["timestamp"]) 294 | impacts_path = os.path.join(OUTPUT_DIR, "impacts_stream.csv") 295 | impacts.to_csv(impacts_path, index=False) 296 | 297 | # 4) Minute-level analytics + anomaly detection 298 | summary = summarize_per_minute(impacts) 299 | summary_path = os.path.join(OUTPUT_DIR, "impact_summary_by_minute.csv") 300 | summary.to_csv(summary_path, index=False) 301 | 302 | # 5) Map of latest-minute impacts 303 | map_path = os.path.join(OUTPUT_DIR, "latest_impact_map.png") 304 | plot_latest_map(assets, impacts, map_path) 305 | 306 | # 6) Console recap 307 | print("Real-Time-Disaster-Impact-Analytics-Platform (Synthetic Demo)") 308 | print(f"Assets: {len(assets):>6} -> {assets_path}") 309 | print(f"Hazard events: {len(events):>6} -> {events_path}") 310 | print(f"Impact records: {len(impacts):>6} -> {impacts_path}") 311 | print(f"Minute summary: {len(summary):>6} -> {summary_path}") 312 | print(f"Latest map PNG: -> {map_path}") 313 | 314 | # 7) Simple alert snapshot (top 5 minutes by p90 impact) 315 | top5 = summary.sort_values("p90_impact", ascending=False).head(5) 316 | print("\nTop 5 minutes by p90 impact:") 317 | print(top5[["timestamp","events","affected_assets","p90_impact","extreme_hits","anomaly_p90"]].to_string(index=False)) 318 | 319 | if __name__ == "__main__": 320 | main() 321 | -------------------------------------------------------------------------------- /real_time_disaster_platform.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Real-Time-Disaster-Impact-Analytics-Platform (Synthetic Data Demo) 4 | ------------------------------------------------------------------ 5 | This standalone script simulates a streaming disaster feed and computes 6 | real-time impact analytics on a synthetic asset inventory (>100 points). 7 | 8 | Features 9 | - Generates > 1,000 synthetic assets (lat/lon, population, vulnerability). 10 | - Simulates a stream of hazard events (e.g., flood/wind) with spatial footprints. 11 | - Computes per-asset impact in (near) real time with decay by distance. 12 | - Rolling analytics (per-minute aggregate, anomaly detection via z-score). 13 | - Saves CSV artifacts and a PNG map of the latest impacts. 14 | 15 | USAGE (no dependencies beyond numpy/pandas/matplotlib): 16 | python real_time_disaster_platform.py 17 | 18 | Outputs created in ./outputs/ : 19 | - assets.csv 20 | - hazard_events.csv 21 | - impacts_stream.csv (long format per event-asset impact) 22 | - impact_summary_by_minute.csv 23 | - latest_impact_map.png 24 | 25 | This demo uses a Niger Delta–like bounding box for geospatial realism. 26 | Author: ChatGPT 27 | """ 28 | 29 | from __future__ import annotations 30 | import os 31 | import math 32 | import time 33 | import uuid 34 | import random 35 | from dataclasses import dataclass 36 | from typing import List, Tuple 37 | 38 | import numpy as np 39 | import pandas as pd 40 | import matplotlib.pyplot as plt 41 | 42 | # ------------------------------- Configuration ------------------------------- 43 | 44 | RNG_SEED = 42 45 | ASSET_COUNT = 1500 # > 100 points (synthetic assets) 46 | EVENT_COUNT = 240 # ~4 hours at 1/min if you like to imagine 47 | ASSET_BOUNDS = { # Niger Delta-ish bbox (approx) 48 | "lat_min": 4.3, "lat_max": 6.2, 49 | "lon_min": 5.0, "lon_max": 7.5 50 | } 51 | HAZARD_TYPES = ["flood", "wind"] 52 | EVENT_RADIUS_RANGE_KM = (10, 80) # spatial footprint radius 53 | EVENT_INTENSITY_RANGE = (0.4, 1.0) # base intensity multiplier 54 | CRITICAL_PROB = 0.1 # % of assets as critical infrastructure 55 | ROLLING_WINDOW_MIN = 15 # analytics window for anomaly detection 56 | 57 | OUTPUT_DIR = os.path.join(os.path.dirname(__file__), "outputs") 58 | os.makedirs(OUTPUT_DIR, exist_ok=True) 59 | 60 | np.random.seed(RNG_SEED) 61 | random.seed(RNG_SEED) 62 | 63 | 64 | # ------------------------------- Data Classes -------------------------------- 65 | 66 | @dataclass 67 | class HazardEvent: 68 | event_id: str 69 | timestamp: pd.Timestamp 70 | hazard_type: str 71 | center_lat: float 72 | center_lon: float 73 | radius_km: float 74 | base_intensity: float 75 | 76 | 77 | # ------------------------------- Generators ---------------------------------- 78 | 79 | def generate_assets(n: int) -> pd.DataFrame: 80 | """Generate synthetic assets within a bounding box.""" 81 | lat = np.random.uniform(ASSET_BOUNDS["lat_min"], ASSET_BOUNDS["lat_max"], n) 82 | lon = np.random.uniform(ASSET_BOUNDS["lon_min"], ASSET_BOUNDS["lon_max"], n) 83 | 84 | # Population and vulnerability 85 | population = np.random.lognormal(mean=6.5, sigma=0.6, size=n).astype(int) # ~exp(6.5) ~ 665 86 | vulnerability = np.clip(np.random.normal(0.5, 0.15, n), 0.05, 0.95) 87 | 88 | # Infrastructure criticality 89 | critical = np.random.rand(n) < CRITICAL_PROB 90 | 91 | assets = pd.DataFrame({ 92 | "asset_id": [f"A{i:05d}" for i in range(n)], 93 | "lat": lat, 94 | "lon": lon, 95 | "population": population, 96 | "vulnerability": vulnerability, 97 | "is_critical": critical 98 | }) 99 | return assets 100 | 101 | 102 | def simulate_event(t0: pd.Timestamp, minute_offset: int) -> HazardEvent: 103 | """Create a single hazard event at t0 + minute_offset minutes.""" 104 | hazard_type = random.choice(HAZARD_TYPES) 105 | 106 | # Centers biased toward clusters (hotspots) for realism 107 | hotspot_centers = [ 108 | (5.5, 6.7), # near Port Harcourt-ish 109 | (5.3, 5.5), # coastal area 110 | (6.0, 7.2) # inland 111 | ] 112 | if random.random() < 0.7: 113 | base_lat, base_lon = random.choice(hotspot_centers) 114 | jitter = np.random.normal(0, 0.15, 2) 115 | center_lat = float(np.clip(base_lat + jitter[0], ASSET_BOUNDS["lat_min"], ASSET_BOUNDS["lat_max"])) 116 | center_lon = float(np.clip(base_lon + jitter[1], ASSET_BOUNDS["lon_min"], ASSET_BOUNDS["lon_max"])) 117 | else: 118 | center_lat = float(np.random.uniform(ASSET_BOUNDS["lat_min"], ASSET_BOUNDS["lat_max"])) 119 | center_lon = float(np.random.uniform(ASSET_BOUNDS["lon_min"], ASSET_BOUNDS["lon_max"])) 120 | 121 | radius_km = float(np.random.uniform(*EVENT_RADIUS_RANGE_KM)) 122 | base_intensity = float(np.random.uniform(*EVENT_INTENSITY_RANGE)) 123 | 124 | return HazardEvent( 125 | event_id=str(uuid.uuid4())[:8], 126 | timestamp=t0 + pd.Timedelta(minutes=minute_offset), 127 | hazard_type=hazard_type, 128 | center_lat=center_lat, 129 | center_lon=center_lon, 130 | radius_km=radius_km, 131 | base_intensity=base_intensity 132 | ) 133 | 134 | 135 | def haversine_km(lat1, lon1, lat2, lon2): 136 | """Calculate great-circle distance between two points on Earth (km).""" 137 | R = 6371.0 138 | phi1, phi2 = np.radians(lat1), np.radians(lat2) 139 | dphi = np.radians(lat2 - lat1) 140 | dlambda = np.radians(lon2 - lon1) 141 | a = np.sin(dphi/2.0)**2 + np.cos(phi1) * np.cos(phi2) * np.sin(dlambda/2.0)**2 142 | return R * 2 * np.arctan2(np.sqrt(a), np.sqrt(1 - a)) 143 | 144 | 145 | def event_impact_on_assets(event: HazardEvent, assets: pd.DataFrame) -> pd.DataFrame: 146 | """Compute impact of a single event on all assets with radial decay.""" 147 | # Vectorized distance calc 148 | d_km = haversine_km(event.center_lat, event.center_lon, assets["lat"].values, assets["lon"].values) 149 | 150 | # Only assets within ~3 * radius feel some impact (soft cutoff) 151 | influence_mask = d_km <= (event.radius_km * 3) 152 | affected = assets.loc[influence_mask].copy() 153 | if affected.empty: 154 | return pd.DataFrame(columns=[ 155 | "event_id","asset_id","timestamp","hazard_type","distance_km", 156 | "hazard_intensity","impact_score","impact_severity" 157 | ]) 158 | 159 | # Intensity decays with distance (Gaussian-like), clipping small values 160 | intensity = event.base_intensity * np.exp(-(d_km[influence_mask] / event.radius_km)**2) 161 | intensity = np.clip(intensity, 0, None) 162 | 163 | # Impact combines intensity, population exposure, and vulnerability 164 | exposure = affected["population"].values ** 0.5 # diminishing returns on population 165 | vulnerability = affected["vulnerability"].values 166 | critical_boost = np.where(affected["is_critical"].values, 1.25, 1.0) 167 | 168 | impact = intensity * exposure * vulnerability * critical_boost 169 | 170 | # Normalize impact to a 0..100 scale for interpretability 171 | if impact.max() > 0: 172 | impact_norm = 100 * (impact / impact.max()) 173 | else: 174 | impact_norm = impact 175 | 176 | # Severity bands 177 | bins = [-np.inf, 5, 20, 40, 70, np.inf] 178 | labels = ["Minimal", "Minor", "Moderate", "Severe", "Extreme"] 179 | severity = pd.cut(impact_norm, bins=bins, labels=labels) 180 | 181 | out = pd.DataFrame({ 182 | "event_id": event.event_id, 183 | "asset_id": affected["asset_id"].values, 184 | "timestamp": event.timestamp, 185 | "hazard_type": event.hazard_type, 186 | "distance_km": np.round(d_km[influence_mask], 2), 187 | "hazard_intensity": np.round(intensity, 4), 188 | "impact_score": np.round(impact_norm, 2), 189 | "impact_severity": severity.astype(str) 190 | }) 191 | return out 192 | 193 | 194 | # ------------------------------ Analytics Utils ------------------------------ 195 | 196 | def zscore_anomalies(series: pd.Series, window: int = ROLLING_WINDOW_MIN, z_thresh: float = 2.5) -> pd.Series: 197 | """Return a boolean Series where values are anomalous by rolling z-score.""" 198 | roll_mean = series.rolling(window=window, min_periods=max(3, window//3)).mean() 199 | roll_std = series.rolling(window=window, min_periods=max(3, window//3)).std(ddof=0) 200 | z = (series - roll_mean) / (roll_std.replace(0, np.nan)) 201 | return (z.abs() >= z_thresh).fillna(False) 202 | 203 | 204 | def summarize_per_minute(impacts: pd.DataFrame) -> pd.DataFrame: 205 | """Aggregate impacts per minute and compute anomaly flags.""" 206 | g = impacts.groupby(pd.Grouper(key="timestamp", freq="1min")) 207 | summary = g.agg( 208 | events=("event_id", "nunique"), 209 | affected_assets=("asset_id", "nunique"), 210 | mean_impact=("impact_score", "mean"), 211 | p90_impact=("impact_score", lambda x: np.nanpercentile(x, 90)), 212 | extreme_hits=("impact_severity", lambda s: (s == "Extreme").sum()), 213 | severe_hits=("impact_severity", lambda s: (s == "Severe").sum()), 214 | ).reset_index() 215 | 216 | # Fill NA for early windows 217 | summary[["mean_impact","p90_impact"]] = summary[["mean_impact","p90_impact"]].fillna(0) 218 | 219 | # Anomaly flags 220 | summary["anomaly_affected_assets"] = zscore_anomalies(summary["affected_assets"]) 221 | summary["anomaly_extreme"] = zscore_anomalies(summary["extreme_hits"]) 222 | summary["anomaly_p90"] = zscore_anomalies(summary["p90_impact"]) 223 | 224 | return summary 225 | 226 | 227 | # --------------------------------- Plotting ---------------------------------- 228 | 229 | def plot_latest_map(assets: pd.DataFrame, impacts: pd.DataFrame, outpath: str): 230 | """Scatter plot of latest-minute impacts over asset locations.""" 231 | if impacts.empty: 232 | return 233 | 234 | latest_minute = impacts["timestamp"].max().floor("min") 235 | latest = impacts[impacts["timestamp"].dt.floor("min") == latest_minute] 236 | 237 | # Join for coordinates 238 | latest = latest.merge(assets[["asset_id","lat","lon"]], on="asset_id", how="left") 239 | 240 | fig, ax = plt.subplots(figsize=(8, 7)) 241 | sc = ax.scatter(latest["lon"], latest["lat"], 242 | s=10 + latest["impact_score"] * 0.8, 243 | c=latest["impact_score"], 244 | alpha=0.8) 245 | plt.colorbar(sc, ax=ax, label="Impact Score (0-100)") 246 | 247 | ax.set_title(f"Latest Impacts @ {latest_minute} (n={len(latest)})") 248 | ax.set_xlabel("Longitude") 249 | ax.set_ylabel("Latitude") 250 | ax.set_xlim(ASSET_BOUNDS["lon_min"]-0.1, ASSET_BOUNDS["lon_max"]+0.1) 251 | ax.set_ylim(ASSET_BOUNDS["lat_min"]-0.1, ASSET_BOUNDS["lat_max"]+0.1) 252 | ax.grid(True, linestyle="--", alpha=0.3) 253 | 254 | # Add a rough bbox frame label 255 | ax.text(0.02, 0.98, "Niger Delta (approx bbox)", transform=ax.transAxes, 256 | va="top", ha="left", fontsize=9, alpha=0.7) 257 | 258 | plt.tight_layout() 259 | fig.savefig(outpath, dpi=150) 260 | plt.close(fig) 261 | 262 | 263 | # --------------------------------- Pipeline ---------------------------------- 264 | 265 | def main(): 266 | # 1) Assets 267 | assets = generate_assets(ASSET_COUNT) 268 | assets_path = os.path.join(OUTPUT_DIR, "assets.csv") 269 | assets.to_csv(assets_path, index=False) 270 | 271 | # 2) Generate event stream 272 | start_time = pd.Timestamp.utcnow().floor("min") 273 | events: List[HazardEvent] = [simulate_event(start_time, i) for i in range(EVENT_COUNT)] 274 | events_df = pd.DataFrame([e.__dict__ for e in events]) 275 | events_path = os.path.join(OUTPUT_DIR, "hazard_events.csv") 276 | events_df.to_csv(events_path, index=False) 277 | 278 | # 3) Stream processing (no sleeps in demo; iterate quickly) 279 | impact_rows = [] 280 | for ev in events: 281 | impacts_ev = event_impact_on_assets(ev, assets) 282 | if not impacts_ev.empty: 283 | impact_rows.append(impacts_ev) 284 | 285 | if impact_rows: 286 | impacts = pd.concat(impact_rows, ignore_index=True) 287 | else: 288 | impacts = pd.DataFrame(columns=[ 289 | "event_id","asset_id","timestamp","hazard_type","distance_km", 290 | "hazard_intensity","impact_score","impact_severity" 291 | ]) 292 | 293 | impacts["timestamp"] = pd.to_datetime(impacts["timestamp"]) 294 | impacts_path = os.path.join(OUTPUT_DIR, "impacts_stream.csv") 295 | impacts.to_csv(impacts_path, index=False) 296 | 297 | # 4) Minute-level analytics + anomaly detection 298 | summary = summarize_per_minute(impacts) 299 | summary_path = os.path.join(OUTPUT_DIR, "impact_summary_by_minute.csv") 300 | summary.to_csv(summary_path, index=False) 301 | 302 | # 5) Map of latest-minute impacts 303 | map_path = os.path.join(OUTPUT_DIR, "latest_impact_map.png") 304 | plot_latest_map(assets, impacts, map_path) 305 | 306 | # 6) Console recap 307 | print("Real-Time-Disaster-Impact-Analytics-Platform (Synthetic Demo)") 308 | print(f"Assets: {len(assets):>6} -> {assets_path}") 309 | print(f"Hazard events: {len(events):>6} -> {events_path}") 310 | print(f"Impact records: {len(impacts):>6} -> {impacts_path}") 311 | print(f"Minute summary: {len(summary):>6} -> {summary_path}") 312 | print(f"Latest map PNG: -> {map_path}") 313 | 314 | # 7) Simple alert snapshot (top 5 minutes by p90 impact) 315 | top5 = summary.sort_values("p90_impact", ascending=False).head(5) 316 | print("\nTop 5 minutes by p90 impact:") 317 | print(top5[["timestamp","events","affected_assets","p90_impact","extreme_hits","anomaly_p90"]].to_string(index=False)) 318 | 319 | if __name__ == "__main__": 320 | main() 321 | --------------------------------------------------------------------------------