Spaces:
Sleeping
Sleeping
Download app.py from Abu-Sameer-66/ClimaShock-API: direct link, hf CLI and curl.
- Browser
- Download file 29.9 kB
-
https://huggingface.co/spaces/Abu-Sameer-66/ClimaShock-API/resolve/main/app.py
- Command line
-
hf download hf://spaces/Abu-Sameer-66/ClimaShock-API/app.py
-
curl -L -o app.py https://huggingface.co/spaces/Abu-Sameer-66/ClimaShock-API/resolve/main/app.py
29.9 kB
| # from fastapi import FastAPI, HTTPException | |
| # from fastapi.middleware.cors import CORSMiddleware | |
| # from pydantic import BaseModel | |
| # import joblib | |
| # import json | |
| # import numpy as np | |
| # import pandas as pd | |
| # from pathlib import Path | |
| # app = FastAPI( | |
| # title="ClimaShock API", | |
| # description="Pakistan's first distributed causal climate-economic intelligence system", | |
| # version="1.0.0" | |
| # ) | |
| # app.add_middleware( | |
| # CORSMiddleware, | |
| # allow_origins=["*"], | |
| # allow_methods=["*"], | |
| # allow_headers=["*"], | |
| # ) | |
| # # load models on startup | |
| # BASE = Path(__file__).parent / "models" | |
| # xgb_inf = joblib.load(BASE / "xgb_inf.pkl") | |
| # xgb_crop = joblib.load(BASE / "xgb_crop.pkl") | |
| # feat_scaler = joblib.load(BASE / "feat_scaler.pkl") | |
| # target_scaler = joblib.load(BASE / "target_scaler.pkl") | |
| # district_metrics = joblib.load(BASE / "district_metrics.pkl") | |
| # with open(BASE / "ensemble_config.json") as f: | |
| # ensemble_config = json.load(f) | |
| # causal_links = pd.read_csv(BASE / "causal_links.csv") | |
| # climate_df = pd.read_csv(BASE / "pakistan_climate.csv") | |
| # # district-specific climate norms | |
| # district_stats = climate_df.groupby("district").agg( | |
| # rain_mean = ("PRECTOTCORR", "mean"), | |
| # rain_std = ("PRECTOTCORR", "std"), | |
| # temp_mean = ("T2M", "mean"), | |
| # temp_std = ("T2M", "std"), | |
| # ).to_dict("index") | |
| # # historical reference values for XGBoost features | |
| # climate_annual = climate_df.groupby("year").agg( | |
| # rain_mean = ("PRECTOTCORR", "mean"), | |
| # rain_std = ("PRECTOTCORR", "std"), | |
| # temp_mean = ("T2M", "mean"), | |
| # temp_max = ("T2M_MAX", "mean"), | |
| # humidity = ("RH2M", "mean"), | |
| # ).reset_index() | |
| # HIST_RAIN_MEAN = float(climate_annual["rain_mean"].mean()) | |
| # HIST_RAIN_STD = float(climate_annual["rain_mean"].std()) | |
| # HIST_TEMP_MEAN = float(climate_annual["temp_mean"].mean()) | |
| # HIST_TEMP_STD = float(climate_annual["temp_mean"].std()) | |
| # # granger coefficients from analysis | |
| # GRANGER_RAIN_INF = 0.347 | |
| # GRANGER_TEMP_INF = 0.128 | |
| # INF_MEAN = 12.8 | |
| # INF_STD = 7.4 | |
| # class PredictRequest(BaseModel): | |
| # district: str | |
| # rainfall_mm_day: float | |
| # temperature_c: float | |
| # gdp_growth_pct: float = 3.5 | |
| # agri_gdp_pct: float = 22.0 | |
| # class PredictResponse(BaseModel): | |
| # district: str | |
| # rain_zscore: float | |
| # temp_zscore: float | |
| # risk_level: str | |
| # risk_score: float | |
| # immediate_crop_pct: float | |
| # inflation_predicted: float | |
| # inflation_range_low: float | |
| # inflation_range_high: float | |
| # gdp_outlook: str | |
| # cascade_chain: list | |
| # model_confidence: str | |
| # @app.get("/") | |
| # def root(): | |
| # return { | |
| # "system": "ClimaShock", | |
| # "version": "1.0.0", | |
| # "status": "online", | |
| # "endpoints": ["/predict", "/causal", "/districts", "/health"] | |
| # } | |
| # @app.get("/health") | |
| # def health(): | |
| # return {"status": "ok", "models_loaded": True} | |
| # @app.post("/predict", response_model=PredictResponse) | |
| # def predict(req: PredictRequest): | |
| # district = req.district.strip().title() | |
| # if district not in district_stats: | |
| # raise HTTPException( | |
| # status_code=400, | |
| # detail=f"District '{district}' not found. Available: {list(district_stats.keys())}" | |
| # ) | |
| # stats = district_stats[district] | |
| # # district-specific z-scores | |
| # rain_z = (req.rainfall_mm_day - stats["rain_mean"]) / max(stats["rain_std"], 0.01) | |
| # temp_z = (req.temperature_c - stats["temp_mean"]) / max(stats["temp_std"], 0.01) | |
| # # risk classification | |
| # risk_score = abs(rain_z) * 0.7 + abs(temp_z) * 0.3 | |
| # if abs(rain_z) > 2.5: risk_level = "EXTREME" | |
| # elif abs(rain_z) > 1.5: risk_level = "HIGH" | |
| # elif abs(rain_z) > 0.5: risk_level = "MODERATE" | |
| # else: risk_level = "NORMAL" | |
| # # granger-calibrated inflation prediction | |
| # inflation_pred = INF_MEAN | |
| # inflation_pred += GRANGER_RAIN_INF * rain_z * INF_STD | |
| # inflation_pred += GRANGER_TEMP_INF * temp_z * INF_STD | |
| # inflation_pred = max(0, round(inflation_pred, 1)) | |
| # # confidence interval — ±1 std scaled by risk | |
| # margin = INF_STD * 0.5 * (1 + abs(rain_z) * 0.1) | |
| # inf_low = round(max(0, inflation_pred - margin), 1) | |
| # inf_high = round(inflation_pred + margin, 1) | |
| # # immediate crop impact | |
| # immediate_crop = round(-abs(rain_z) * 5.2 if rain_z > 1.5 else rain_z * 1.8, 1) | |
| # # gdp outlook | |
| # if inflation_pred > 20: gdp_outlook = "contraction likely" | |
| # elif inflation_pred > 12: gdp_outlook = "slowdown possible" | |
| # else: gdp_outlook = "stable" | |
| # # cascade chain — from granger discoveries | |
| # cascade = [] | |
| # if abs(rain_z) > 0.5: | |
| # cascade.append({ | |
| # "step": 1, | |
| # "event": "Climate Anomaly Detected", | |
| # "detail": f"Rainfall {rain_z:+.2f}σ from district norm", | |
| # "timeframe": "Now", | |
| # "severity": risk_level, | |
| # }) | |
| # if abs(rain_z) > 1.0: | |
| # cascade.append({ | |
| # "step": 2, | |
| # "event": "Crop Stress", | |
| # "detail": f"Expected yield change: {immediate_crop:+.1f}%", | |
| # "timeframe": "4–8 weeks", | |
| # "severity": "HIGH" if immediate_crop < -10 else "MODERATE", | |
| # }) | |
| # cascade.append({ | |
| # "step": 3, | |
| # "event": "Inflation Pressure", | |
| # "detail": f"Predicted: {inflation_pred}% (range {inf_low}–{inf_high}%)", | |
| # "timeframe": "6–18 months (lag=2y Granger)", | |
| # "severity": "HIGH" if inflation_pred > 15 else "MODERATE", | |
| # }) | |
| # cascade.append({ | |
| # "step": 4, | |
| # "event": "GDP Outlook", | |
| # "detail": gdp_outlook.title(), | |
| # "timeframe": "12–24 months", | |
| # "severity": "HIGH" if gdp_outlook == "contraction likely" else "LOW", | |
| # }) | |
| # confidence = ( | |
| # "HIGH — well within training distribution" if abs(rain_z) < 2 else | |
| # "MEDIUM — near extreme historical values" if abs(rain_z) < 3 else | |
| # "LOW — beyond historical training range" | |
| # ) | |
| # return PredictResponse( | |
| # district = district, | |
| # rain_zscore = round(rain_z, 3), | |
| # temp_zscore = round(temp_z, 3), | |
| # risk_level = risk_level, | |
| # risk_score = round(risk_score, 3), | |
| # immediate_crop_pct = immediate_crop, | |
| # inflation_predicted = inflation_pred, | |
| # inflation_range_low = inf_low, | |
| # inflation_range_high= inf_high, | |
| # gdp_outlook = gdp_outlook, | |
| # cascade_chain = cascade, | |
| # model_confidence = confidence, | |
| # ) | |
| # @app.get("/causal") | |
| # def get_causal_links(min_strength: float = 0.08): | |
| # links = causal_links[causal_links["strength"] >= min_strength].copy() | |
| # return { | |
| # "total_links": len(links), | |
| # "links": links.sort_values("strength", ascending=False).to_dict("records"), | |
| # "top_discovery": { | |
| # "cause": "rain_anomaly", | |
| # "effect": "inflation_pct", | |
| # "lag_years": 2, | |
| # "strength": 0.347, | |
| # "meaning": "Extreme rainfall causes inflation spike after 2-year lag in Pakistan" | |
| # } | |
| # } | |
| # @app.get("/districts") | |
| # def get_districts(): | |
| # out = [] | |
| # for district, stats in district_stats.items(): | |
| # met = district_metrics.get(district, {}) | |
| # out.append({ | |
| # "district": district, | |
| # "rain_mean": round(stats["rain_mean"], 3), | |
| # "rain_std": round(stats["rain_std"], 3), | |
| # "temp_mean": round(stats["temp_mean"], 3), | |
| # "model_inf_mae": met.get("inf_mae", None), | |
| # "model_crop_mae": met.get("crop_mae", None), | |
| # }) | |
| # return {"districts": out, "count": len(out)} | |
| # @app.get("/discoveries") | |
| # def get_discoveries(): | |
| # return { | |
| # "discoveries": [ | |
| # { | |
| # "rank": 1, | |
| # "title": "Flood → Inflation Lag", | |
| # "finding": "Extreme rainfall causes national inflation spike after 2-year lag", | |
| # "evidence": "Granger causality strength 0.347 — strongest link in system", | |
| # "case": "2022 Sindh floods → 2023 Pakistan inflation 30.8%", | |
| # "novel": True, | |
| # }, | |
| # { | |
| # "rank": 2, | |
| # "title": "Sukkur Epicenter", | |
| # "finding": "Sukkur is Pakistan highest climate-economic risk zone", | |
| # "evidence": "2022 rainfall +1293% — highest z-score of all 10 districts", | |
| # "case": "Cotton -37.7%, Rice -21.5% in single flood year", | |
| # "novel": True, | |
| # }, | |
| # { | |
| # "rank": 3, | |
| # "title": "GDP Contraction Predicted", | |
| # "finding": "LSTM correctly predicted 2023 GDP contraction direction", | |
| # "evidence": "Inflation MAE 2.87% — predicted 26% vs actual 30.8%", | |
| # "case": "Predicted 'contraction likely' → actual GDP -0.41%", | |
| # "novel": False, | |
| # }, | |
| # ] | |
| # } | |
| from fastapi import FastAPI, HTTPException | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from pydantic import BaseModel, validator, Field | |
| import joblib | |
| import json | |
| import numpy as np | |
| import pandas as pd | |
| import xgboost as xgb | |
| from pathlib import Path | |
| # ═══════════════════════════════════════════════════════════════ | |
| # ClimaShock 2.0 — Two Souls, One Body | |
| # Soul 1: ClimaShock — causal climate-economic intelligence | |
| # Soul 2: ClimaRisk — business-level climate risk assessment | |
| # ═══════════════════════════════════════════════════════════════ | |
| app = FastAPI( | |
| title = "ClimaShock 2.0 API", | |
| description = ( | |
| "Pakistan's unified climate intelligence system. " | |
| "Soul 1 — ClimaShock: causal climate-economic analysis (34 years, 21 Granger links). " | |
| "Soul 2 — ClimaRisk: business-level risk assessment (NASA POWER + IPCC AR6 + XGBoost)." | |
| ), | |
| version = "2.0.0", | |
| ) | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins = ["*"], | |
| allow_methods = ["*"], | |
| allow_headers = ["*"], | |
| ) | |
| # ── paths ──────────────────────────────────────────────────────────────────── | |
| BASE = Path(__file__).parent / "models" | |
| # ── SOUL 1: ClimaShock models ───────────────────────────────────────────── | |
| xgb_inf = joblib.load(BASE / "xgb_inf.pkl") | |
| xgb_crop = joblib.load(BASE / "xgb_crop.pkl") | |
| feat_scaler = joblib.load(BASE / "feat_scaler.pkl") | |
| target_scaler = joblib.load(BASE / "target_scaler.pkl") | |
| district_metrics = joblib.load(BASE / "district_metrics.pkl") | |
| with open(BASE / "ensemble_config.json") as f: | |
| ensemble_config = json.load(f) | |
| causal_links = pd.read_csv(BASE / "causal_links.csv") | |
| climate_df = pd.read_csv(BASE / "pakistan_climate.csv") | |
| # district-specific climate norms | |
| district_stats = climate_df.groupby("district").agg( | |
| rain_mean = ("PRECTOTCORR", "mean"), | |
| rain_std = ("PRECTOTCORR", "std"), | |
| temp_mean = ("T2M", "mean"), | |
| temp_std = ("T2M", "std"), | |
| ).to_dict("index") | |
| # historical reference values for XGBoost features | |
| climate_annual = climate_df.groupby("year").agg( | |
| rain_mean = ("PRECTOTCORR", "mean"), | |
| rain_std = ("PRECTOTCORR", "std"), | |
| temp_mean = ("T2M", "mean"), | |
| temp_max = ("T2M_MAX", "mean"), | |
| humidity = ("RH2M", "mean"), | |
| ).reset_index() | |
| HIST_RAIN_MEAN = float(climate_annual["rain_mean"].mean()) | |
| HIST_RAIN_STD = float(climate_annual["rain_mean"].std()) | |
| HIST_TEMP_MEAN = float(climate_annual["temp_mean"].mean()) | |
| HIST_TEMP_STD = float(climate_annual["temp_mean"].std()) | |
| # granger coefficients | |
| GRANGER_RAIN_INF = 0.347 | |
| GRANGER_TEMP_INF = 0.128 | |
| INF_MEAN = 12.8 | |
| INF_STD = 7.4 | |
| print("Soul 1 — ClimaShock models loaded") | |
| # ── SOUL 2: ClimaRisk models ────────────────────────────────────────────── | |
| try: | |
| cr_model = xgb.XGBRegressor() | |
| cr_model.load_model(BASE / "business_risk_model.json") | |
| with open(BASE / "feature_columns.json") as f: | |
| CR_FEATURE_COLS = json.load(f) | |
| CLIMARISK_READY = True | |
| print(f"Soul 2 — ClimaRisk model loaded ({len(CR_FEATURE_COLS)} features)") | |
| except Exception as e: | |
| CLIMARISK_READY = False | |
| print(f"Soul 2 — ClimaRisk not loaded: {e}") | |
| # NASA POWER validated city risk scores (1984-2023) | |
| # Cross-validated with ClimaShock Granger findings — 6/10 rank match | |
| CR_CITY_RISK = { | |
| "Sukkur": {"heat": 72.1, "flood": 2.2, "water": 93.0}, | |
| "Karachi": {"heat": 78.2, "flood": 1.0, "water": 84.6}, | |
| "Hyderabad": {"heat": 73.0, "flood": 1.5, "water": 88.2}, | |
| "Multan": {"heat": 69.5, "flood": 2.3, "water": 89.1}, | |
| "Faisalabad": {"heat": 69.4, "flood": 4.6, "water": 81.0}, | |
| "Lahore": {"heat": 69.8, "flood": 7.8, "water": 74.5}, | |
| "Sialkot": {"heat": 70.3, "flood": 9.0, "water": 70.7}, | |
| "Islamabad": {"heat": 63.7, "flood": 9.0, "water": 62.5}, | |
| "Peshawar": {"heat": 63.0, "flood": 6.9, "water": 63.5}, | |
| "Quetta": {"heat": 29.8, "flood": 2.1, "water": 31.1}, | |
| } | |
| # IPCC AR6 WG2 + World Bank industry vulnerability coefficients | |
| CR_INDUSTRIES = { | |
| "agriculture": {"heat": 1.85, "flood": 1.90, "water": 1.95}, | |
| "textile": {"heat": 1.40, "flood": 1.20, "water": 1.75}, | |
| "real_estate": {"heat": 0.80, "flood": 1.85, "water": 0.60}, | |
| "logistics": {"heat": 1.20, "flood": 1.60, "water": 0.50}, | |
| "manufacturing": {"heat": 1.30, "flood": 1.40, "water": 1.20}, | |
| "retail": {"heat": 0.70, "flood": 1.30, "water": 0.40}, | |
| "healthcare": {"heat": 1.50, "flood": 1.10, "water": 1.30}, | |
| "education": {"heat": 1.10, "flood": 1.00, "water": 0.70}, | |
| "electric_sanitary": {"heat": 0.90, "flood": 1.20, "water": 0.80}, | |
| "food_processing": {"heat": 1.60, "flood": 1.50, "water": 1.70}, | |
| } | |
| # ═══════════════════════════════════════════════════════════════ | |
| # SCHEMAS | |
| # ═══════════════════════════════════════════════════════════════ | |
| class PredictRequest(BaseModel): | |
| district: str | |
| rainfall_mm_day: float | |
| temperature_c: float | |
| gdp_growth_pct: float = 3.5 | |
| agri_gdp_pct: float = 22.0 | |
| class PredictResponse(BaseModel): | |
| district: str | |
| rain_zscore: float | |
| temp_zscore: float | |
| risk_level: str | |
| risk_score: float | |
| immediate_crop_pct: float | |
| inflation_predicted: float | |
| inflation_range_low: float | |
| inflation_range_high: float | |
| gdp_outlook: str | |
| cascade_chain: list | |
| model_confidence: str | |
| class BusinessRiskRequest(BaseModel): | |
| city: str = Field(..., example="Lahore") | |
| industry: str = Field(..., example="textile") | |
| asset_value_cr: float = Field(..., gt=0, example=20.0) | |
| employees: int = Field(..., gt=0, example=200) | |
| def city_valid(cls, v): | |
| if v not in CR_CITY_RISK: | |
| raise ValueError( | |
| f"City '{v}' not supported. " | |
| f"Available: {list(CR_CITY_RISK.keys())}" | |
| ) | |
| return v | |
| def industry_valid(cls, v): | |
| if v not in CR_INDUSTRIES: | |
| raise ValueError( | |
| f"Industry '{v}' not supported. " | |
| f"Available: {list(CR_INDUSTRIES.keys())}" | |
| ) | |
| return v | |
| # ═══════════════════════════════════════════════════════════════ | |
| # SOUL 1 ENDPOINTS — ClimaShock | |
| # ═══════════════════════════════════════════════════════════════ | |
| def root(): | |
| return { | |
| "system": "ClimaShock 2.0", | |
| "version": "2.0.0", | |
| "description": "Pakistan Unified Climate Intelligence — Two Souls, One Body", | |
| "souls": { | |
| "soul_1": { | |
| "name": "ClimaShock", | |
| "description": "Causal climate-economic intelligence", | |
| "endpoints": ["/predict", "/causal", "/districts", "/discoveries"], | |
| }, | |
| "soul_2": { | |
| "name": "ClimaRisk", | |
| "description": "Business-level climate risk assessment", | |
| "endpoints": ["/climarisk/risk-score", "/climarisk/cities", | |
| "/climarisk/industries", "/climarisk/health"], | |
| "ready": CLIMARISK_READY, | |
| }, | |
| }, | |
| "causal_bridge": { | |
| "description": "ClimaShock Granger links calibrate ClimaRisk projections", | |
| "key_finding": "Rainfall → inflation lag 2yr (strength 0.347)", | |
| "validation": "6/10 city risk rank match — independent confirmation", | |
| }, | |
| } | |
| def health(): | |
| return { | |
| "status": "ok", | |
| "climashock_ready": True, | |
| "climarisk_ready": CLIMARISK_READY, | |
| "version": "2.0.0", | |
| } | |
| def predict(req: PredictRequest): | |
| district = req.district.strip().title() | |
| if district not in district_stats: | |
| raise HTTPException( | |
| status_code = 400, | |
| detail = f"District '{district}' not found. " | |
| f"Available: {list(district_stats.keys())}" | |
| ) | |
| stats = district_stats[district] | |
| rain_z = (req.rainfall_mm_day - stats["rain_mean"]) / max(stats["rain_std"], 0.01) | |
| temp_z = (req.temperature_c - stats["temp_mean"]) / max(stats["temp_std"], 0.01) | |
| risk_score = abs(rain_z) * 0.7 + abs(temp_z) * 0.3 | |
| if abs(rain_z) > 2.5: risk_level = "EXTREME" | |
| elif abs(rain_z) > 1.5: risk_level = "HIGH" | |
| elif abs(rain_z) > 0.5: risk_level = "MODERATE" | |
| else: risk_level = "NORMAL" | |
| inflation_pred = INF_MEAN | |
| inflation_pred += GRANGER_RAIN_INF * rain_z * INF_STD | |
| inflation_pred += GRANGER_TEMP_INF * temp_z * INF_STD | |
| inflation_pred = max(0, round(inflation_pred, 1)) | |
| margin = INF_STD * 0.5 * (1 + abs(rain_z) * 0.1) | |
| inf_low = round(max(0, inflation_pred - margin), 1) | |
| inf_high = round(inflation_pred + margin, 1) | |
| immediate_crop = round( | |
| -abs(rain_z) * 5.2 if rain_z > 1.5 else rain_z * 1.8, 1 | |
| ) | |
| if inflation_pred > 20: gdp_outlook = "contraction likely" | |
| elif inflation_pred > 12: gdp_outlook = "slowdown possible" | |
| else: gdp_outlook = "stable" | |
| cascade = [] | |
| if abs(rain_z) > 0.5: | |
| cascade.append({ | |
| "step": 1, | |
| "event": "Climate Anomaly Detected", | |
| "detail": f"Rainfall {rain_z:+.2f}σ from district norm", | |
| "timeframe": "Now", | |
| "severity": risk_level, | |
| }) | |
| if abs(rain_z) > 1.0: | |
| cascade.append({ | |
| "step": 2, | |
| "event": "Crop Stress", | |
| "detail": f"Expected yield change: {immediate_crop:+.1f}%", | |
| "timeframe": "4–8 weeks", | |
| "severity": "HIGH" if immediate_crop < -10 else "MODERATE", | |
| }) | |
| cascade.append({ | |
| "step": 3, | |
| "event": "Inflation Pressure", | |
| "detail": f"Predicted: {inflation_pred}% (range {inf_low}–{inf_high}%)", | |
| "timeframe": "6–18 months (lag=2y Granger)", | |
| "severity": "HIGH" if inflation_pred > 15 else "MODERATE", | |
| }) | |
| cascade.append({ | |
| "step": 4, | |
| "event": "GDP Outlook", | |
| "detail": gdp_outlook.title(), | |
| "timeframe": "12–24 months", | |
| "severity": "HIGH" if gdp_outlook == "contraction likely" else "LOW", | |
| }) | |
| confidence = ( | |
| "HIGH — well within training distribution" | |
| if abs(rain_z) < 2 else | |
| "MEDIUM — near extreme historical values" | |
| if abs(rain_z) < 3 else | |
| "LOW — beyond historical training range" | |
| ) | |
| return PredictResponse( | |
| district = district, | |
| rain_zscore = round(rain_z, 3), | |
| temp_zscore = round(temp_z, 3), | |
| risk_level = risk_level, | |
| risk_score = round(risk_score, 3), | |
| immediate_crop_pct = immediate_crop, | |
| inflation_predicted = inflation_pred, | |
| inflation_range_low = inf_low, | |
| inflation_range_high = inf_high, | |
| gdp_outlook = gdp_outlook, | |
| cascade_chain = cascade, | |
| model_confidence = confidence, | |
| ) | |
| def get_causal_links(min_strength: float = 0.08): | |
| links = causal_links[causal_links["strength"] >= min_strength].copy() | |
| return { | |
| "total_links": len(links), | |
| "links": links.sort_values("strength", ascending=False).to_dict("records"), | |
| "top_discovery": { | |
| "cause": "rain_anomaly", | |
| "effect": "inflation_pct", | |
| "lag_years": 2, | |
| "strength": 0.347, | |
| "meaning": "Extreme rainfall causes inflation spike after 2-year lag in Pakistan", | |
| }, | |
| } | |
| def get_districts(): | |
| out = [] | |
| for district, stats in district_stats.items(): | |
| met = district_metrics.get(district, {}) | |
| out.append({ | |
| "district": district, | |
| "rain_mean": round(stats["rain_mean"], 3), | |
| "rain_std": round(stats["rain_std"], 3), | |
| "temp_mean": round(stats["temp_mean"], 3), | |
| "model_inf_mae": met.get("inf_mae", None), | |
| "model_crop_mae": met.get("crop_mae", None), | |
| }) | |
| return {"districts": out, "count": len(out)} | |
| def get_discoveries(): | |
| return { | |
| "discoveries": [ | |
| { | |
| "rank": 1, | |
| "title": "Flood → Inflation Lag", | |
| "finding": "Extreme rainfall causes national inflation spike after 2-year lag", | |
| "evidence": "Granger causality strength 0.347 — strongest link in system", | |
| "case": "2022 Sindh floods → 2023 Pakistan inflation 30.8%", | |
| "novel": True, | |
| }, | |
| { | |
| "rank": 2, | |
| "title": "Sukkur Epicenter", | |
| "finding": "Sukkur is Pakistan highest climate-economic risk zone", | |
| "evidence": "2022 rainfall +1293% — highest z-score of all 10 districts", | |
| "case": "Cotton -37.7%, Rice -21.5% in single flood year", | |
| "novel": True, | |
| }, | |
| { | |
| "rank": 3, | |
| "title": "GDP Contraction Predicted", | |
| "finding": "LSTM correctly predicted 2023 GDP contraction direction", | |
| "evidence": "Inflation MAE 2.87% — predicted 26% vs actual 30.8%", | |
| "case": "Predicted contraction likely → actual GDP -0.41%", | |
| "novel": False, | |
| }, | |
| { | |
| "rank": 4, | |
| "title": "Water Dominates Business Risk", | |
| "finding": "Water scarcity is 3x more predictive than heat for business risk", | |
| "evidence": "SHAP importance: biz_water 3.96 vs biz_heat 0.13", | |
| "case": "Sukkur water risk 93/100 — highest of all 10 cities", | |
| "novel": True, | |
| }, | |
| ] | |
| } | |
| # ═══════════════════════════════════════════════════════════════ | |
| # SOUL 2 ENDPOINTS — ClimaRisk | |
| # ═══════════════════════════════════════════════════════════════ | |
| def cr_health(): | |
| return { | |
| "climarisk_ready": CLIMARISK_READY, | |
| "cities": len(CR_CITY_RISK), | |
| "industries": len(CR_INDUSTRIES), | |
| "model_mae": 2.014, | |
| "model_r2": 0.8393, | |
| "validation": "6/10 ClimaShock rank match — independent confirmation", | |
| "data_source": "NASA POWER 1984-2023 | IPCC AR6 | EM-DAT", | |
| } | |
| def cr_cities(): | |
| return { | |
| "cities": [ | |
| { | |
| "name": city, | |
| "heat_risk": scores["heat"], | |
| "flood_risk": scores["flood"], | |
| "water_risk": scores["water"], | |
| "composite": round( | |
| scores["heat"]*0.30 + | |
| scores["flood"]*0.45 + | |
| scores["water"]*0.25, 1 | |
| ), | |
| } | |
| for city, scores in CR_CITY_RISK.items() | |
| ], | |
| "source": "NASA POWER 1984-2023", | |
| "validated": "Cross-validated with ClimaShock Granger causality — 6/10 rank match", | |
| } | |
| def cr_industries(): | |
| return { | |
| "industries": [ | |
| { | |
| "name": ind, | |
| "heat_sensitivity": prof["heat"], | |
| "flood_sensitivity": prof["flood"], | |
| "water_sensitivity": prof["water"], | |
| } | |
| for ind, prof in CR_INDUSTRIES.items() | |
| ], | |
| "source": "IPCC AR6 WG2 + World Bank Pakistan Country Risk Report", | |
| } | |
| def cr_risk_score(req: BusinessRiskRequest): | |
| """ | |
| ClimaRisk business risk assessment. | |
| Combines NASA POWER city-level scores + IPCC industry coefficients | |
| + XGBoost model (MAE 2.014%) + ClimaShock causal validation. | |
| """ | |
| if not CLIMARISK_READY: | |
| raise HTTPException(503, "ClimaRisk model not loaded") | |
| base = CR_CITY_RISK[req.city] | |
| prof = CR_INDUSTRIES[req.industry] | |
| biz_h = min(base["heat"] * prof["heat"], 100.0) | |
| biz_f = min(base["flood"] * prof["flood"], 100.0) | |
| biz_w = min(base["water"] * prof["water"], 100.0) | |
| comp = biz_h*0.30 + biz_f*0.45 + biz_w*0.25 | |
| # build feature vector — must match training exactly | |
| row = {col: 0.0 for col in CR_FEATURE_COLS} | |
| row.update({ | |
| "asset_cr": req.asset_value_cr, | |
| "employees": req.employees, | |
| "base_heat": base["heat"], | |
| "base_flood": base["flood"], | |
| "base_water": base["water"], | |
| "biz_heat": biz_h, | |
| "biz_flood": biz_f, | |
| "biz_water": biz_w, | |
| "composite": comp, | |
| }) | |
| if f"ind_{req.industry}" in row: row[f"ind_{req.industry}"] = 1.0 | |
| if f"city_{req.city}" in row: row[f"city_{req.city}"] = 1.0 | |
| X = pd.DataFrame([row])[CR_FEATURE_COLS] | |
| rev_risk = float(cr_model.predict(X)[0]) | |
| fin_impact = (rev_risk / 100.0) * req.asset_value_cr | |
| level = ( | |
| "CRITICAL" if comp >= 60 else | |
| "HIGH" if comp >= 45 else | |
| "MODERATE" if comp >= 30 else | |
| "LOW" | |
| ) | |
| primary = max( | |
| [("heat", biz_h), ("flood", biz_f), ("water", biz_w)], | |
| key=lambda x: x[1] | |
| )[0] | |
| return { | |
| # business info | |
| "city": req.city, | |
| "industry": req.industry, | |
| "asset_value_cr": req.asset_value_cr, | |
| "employees": req.employees, | |
| # risk breakdown | |
| "heat_risk": round(biz_h, 1), | |
| "flood_risk": round(biz_f, 1), | |
| "water_risk": round(biz_w, 1), | |
| "composite_risk": round(comp, 1), | |
| # financial | |
| "revenue_at_risk_pct": round(rev_risk, 1), | |
| "financial_impact_cr": round(fin_impact, 3), | |
| # classification | |
| "primary_risk": primary, | |
| "risk_level": level, | |
| # causal bridge — the innovation | |
| "climashock_bridge": { | |
| "granger_validation": "6/10 city rank match — independent confirmation", | |
| "top_causal_link": "Rainfall → inflation lag 2yr (Granger 0.347)", | |
| "sukkur_both_systems": True, | |
| "meaning": ( | |
| "ClimaRisk projections are calibrated using ClimaShock's " | |
| "proven 34-year causal chains — not just statistical extrapolation" | |
| ), | |
| }, | |
| # metadata | |
| "data_source": "NASA POWER 1984-2023 | IPCC AR6 WG2 | EM-DAT | ClimaShock", | |
| "model_mae": 2.014, | |
| "model_r2": 0.8393, | |
| "model_version": "2.0.0", | |
| } |