Abu-Sameer-66 commited on
Commit
dc70349
·
0 Parent(s):

init: ClimaShock FastAPI backend with LFS

Browse files
.gitattributes ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ *.pkl filter=lfs diff=lfs merge=lfs -text
2
+ *.zip filter=lfs diff=lfs merge=lfs -text
3
+ *.pt filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+ WORKDIR /app
3
+ COPY requirements.txt .
4
+ RUN pip install --no-cache-dir -r requirements.txt
5
+ COPY . .
6
+ EXPOSE 7860
7
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
app.py ADDED
@@ -0,0 +1,263 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, HTTPException
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from pydantic import BaseModel
4
+ import joblib
5
+ import json
6
+ import numpy as np
7
+ import pandas as pd
8
+ from pathlib import Path
9
+
10
+ app = FastAPI(
11
+ title="ClimaShock API",
12
+ description="Pakistan's first distributed causal climate-economic intelligence system",
13
+ version="1.0.0"
14
+ )
15
+
16
+ app.add_middleware(
17
+ CORSMiddleware,
18
+ allow_origins=["*"],
19
+ allow_methods=["*"],
20
+ allow_headers=["*"],
21
+ )
22
+
23
+ # load models on startup
24
+ BASE = Path(__file__).parent / "models"
25
+
26
+ xgb_inf = joblib.load(BASE / "xgb_inf.pkl")
27
+ xgb_crop = joblib.load(BASE / "xgb_crop.pkl")
28
+ feat_scaler = joblib.load(BASE / "feat_scaler.pkl")
29
+ target_scaler = joblib.load(BASE / "target_scaler.pkl")
30
+ district_metrics = joblib.load(BASE / "district_metrics.pkl")
31
+
32
+ with open(BASE / "ensemble_config.json") as f:
33
+ ensemble_config = json.load(f)
34
+
35
+ causal_links = pd.read_csv(BASE / "causal_links.csv")
36
+ climate_df = pd.read_csv(BASE / "pakistan_climate.csv")
37
+
38
+ # district-specific climate norms
39
+ district_stats = climate_df.groupby("district").agg(
40
+ rain_mean = ("PRECTOTCORR", "mean"),
41
+ rain_std = ("PRECTOTCORR", "std"),
42
+ temp_mean = ("T2M", "mean"),
43
+ temp_std = ("T2M", "std"),
44
+ ).to_dict("index")
45
+
46
+ # historical reference values for XGBoost features
47
+ climate_annual = climate_df.groupby("year").agg(
48
+ rain_mean = ("PRECTOTCORR", "mean"),
49
+ rain_std = ("PRECTOTCORR", "std"),
50
+ temp_mean = ("T2M", "mean"),
51
+ temp_max = ("T2M_MAX", "mean"),
52
+ humidity = ("RH2M", "mean"),
53
+ ).reset_index()
54
+
55
+ HIST_RAIN_MEAN = float(climate_annual["rain_mean"].mean())
56
+ HIST_RAIN_STD = float(climate_annual["rain_mean"].std())
57
+ HIST_TEMP_MEAN = float(climate_annual["temp_mean"].mean())
58
+ HIST_TEMP_STD = float(climate_annual["temp_mean"].std())
59
+
60
+ # granger coefficients from analysis
61
+ GRANGER_RAIN_INF = 0.347
62
+ GRANGER_TEMP_INF = 0.128
63
+ INF_MEAN = 12.8
64
+ INF_STD = 7.4
65
+
66
+
67
+ class PredictRequest(BaseModel):
68
+ district: str
69
+ rainfall_mm_day: float
70
+ temperature_c: float
71
+ gdp_growth_pct: float = 3.5
72
+ agri_gdp_pct: float = 22.0
73
+
74
+
75
+ class PredictResponse(BaseModel):
76
+ district: str
77
+ rain_zscore: float
78
+ temp_zscore: float
79
+ risk_level: str
80
+ risk_score: float
81
+ immediate_crop_pct: float
82
+ inflation_predicted: float
83
+ inflation_range_low: float
84
+ inflation_range_high: float
85
+ gdp_outlook: str
86
+ cascade_chain: list
87
+ model_confidence: str
88
+
89
+
90
+ @app.get("/")
91
+ def root():
92
+ return {
93
+ "system": "ClimaShock",
94
+ "version": "1.0.0",
95
+ "status": "online",
96
+ "endpoints": ["/predict", "/causal", "/districts", "/health"]
97
+ }
98
+
99
+
100
+ @app.get("/health")
101
+ def health():
102
+ return {"status": "ok", "models_loaded": True}
103
+
104
+
105
+ @app.post("/predict", response_model=PredictResponse)
106
+ def predict(req: PredictRequest):
107
+ district = req.district.strip().title()
108
+
109
+ if district not in district_stats:
110
+ raise HTTPException(
111
+ status_code=400,
112
+ detail=f"District '{district}' not found. Available: {list(district_stats.keys())}"
113
+ )
114
+
115
+ stats = district_stats[district]
116
+
117
+ # district-specific z-scores
118
+ rain_z = (req.rainfall_mm_day - stats["rain_mean"]) / max(stats["rain_std"], 0.01)
119
+ temp_z = (req.temperature_c - stats["temp_mean"]) / max(stats["temp_std"], 0.01)
120
+
121
+ # risk classification
122
+ risk_score = abs(rain_z) * 0.7 + abs(temp_z) * 0.3
123
+ if abs(rain_z) > 2.5: risk_level = "EXTREME"
124
+ elif abs(rain_z) > 1.5: risk_level = "HIGH"
125
+ elif abs(rain_z) > 0.5: risk_level = "MODERATE"
126
+ else: risk_level = "NORMAL"
127
+
128
+ # granger-calibrated inflation prediction
129
+ inflation_pred = INF_MEAN
130
+ inflation_pred += GRANGER_RAIN_INF * rain_z * INF_STD
131
+ inflation_pred += GRANGER_TEMP_INF * temp_z * INF_STD
132
+ inflation_pred = max(0, round(inflation_pred, 1))
133
+
134
+ # confidence interval — ±1 std scaled by risk
135
+ margin = INF_STD * 0.5 * (1 + abs(rain_z) * 0.1)
136
+ inf_low = round(max(0, inflation_pred - margin), 1)
137
+ inf_high = round(inflation_pred + margin, 1)
138
+
139
+ # immediate crop impact
140
+ immediate_crop = round(-abs(rain_z) * 5.2 if rain_z > 1.5 else rain_z * 1.8, 1)
141
+
142
+ # gdp outlook
143
+ if inflation_pred > 20: gdp_outlook = "contraction likely"
144
+ elif inflation_pred > 12: gdp_outlook = "slowdown possible"
145
+ else: gdp_outlook = "stable"
146
+
147
+ # cascade chain — from granger discoveries
148
+ cascade = []
149
+ if abs(rain_z) > 0.5:
150
+ cascade.append({
151
+ "step": 1,
152
+ "event": "Climate Anomaly Detected",
153
+ "detail": f"Rainfall {rain_z:+.2f}σ from district norm",
154
+ "timeframe": "Now",
155
+ "severity": risk_level,
156
+ })
157
+ if abs(rain_z) > 1.0:
158
+ cascade.append({
159
+ "step": 2,
160
+ "event": "Crop Stress",
161
+ "detail": f"Expected yield change: {immediate_crop:+.1f}%",
162
+ "timeframe": "4–8 weeks",
163
+ "severity": "HIGH" if immediate_crop < -10 else "MODERATE",
164
+ })
165
+ cascade.append({
166
+ "step": 3,
167
+ "event": "Inflation Pressure",
168
+ "detail": f"Predicted: {inflation_pred}% (range {inf_low}–{inf_high}%)",
169
+ "timeframe": "6–18 months (lag=2y Granger)",
170
+ "severity": "HIGH" if inflation_pred > 15 else "MODERATE",
171
+ })
172
+ cascade.append({
173
+ "step": 4,
174
+ "event": "GDP Outlook",
175
+ "detail": gdp_outlook.title(),
176
+ "timeframe": "12–24 months",
177
+ "severity": "HIGH" if gdp_outlook == "contraction likely" else "LOW",
178
+ })
179
+
180
+ confidence = (
181
+ "HIGH — well within training distribution" if abs(rain_z) < 2 else
182
+ "MEDIUM — near extreme historical values" if abs(rain_z) < 3 else
183
+ "LOW — beyond historical training range"
184
+ )
185
+
186
+ return PredictResponse(
187
+ district = district,
188
+ rain_zscore = round(rain_z, 3),
189
+ temp_zscore = round(temp_z, 3),
190
+ risk_level = risk_level,
191
+ risk_score = round(risk_score, 3),
192
+ immediate_crop_pct = immediate_crop,
193
+ inflation_predicted = inflation_pred,
194
+ inflation_range_low = inf_low,
195
+ inflation_range_high= inf_high,
196
+ gdp_outlook = gdp_outlook,
197
+ cascade_chain = cascade,
198
+ model_confidence = confidence,
199
+ )
200
+
201
+
202
+ @app.get("/causal")
203
+ def get_causal_links(min_strength: float = 0.08):
204
+ links = causal_links[causal_links["strength"] >= min_strength].copy()
205
+ return {
206
+ "total_links": len(links),
207
+ "links": links.sort_values("strength", ascending=False).to_dict("records"),
208
+ "top_discovery": {
209
+ "cause": "rain_anomaly",
210
+ "effect": "inflation_pct",
211
+ "lag_years": 2,
212
+ "strength": 0.347,
213
+ "meaning": "Extreme rainfall causes inflation spike after 2-year lag in Pakistan"
214
+ }
215
+ }
216
+
217
+
218
+ @app.get("/districts")
219
+ def get_districts():
220
+ out = []
221
+ for district, stats in district_stats.items():
222
+ met = district_metrics.get(district, {})
223
+ out.append({
224
+ "district": district,
225
+ "rain_mean": round(stats["rain_mean"], 3),
226
+ "rain_std": round(stats["rain_std"], 3),
227
+ "temp_mean": round(stats["temp_mean"], 3),
228
+ "model_inf_mae": met.get("inf_mae", None),
229
+ "model_crop_mae": met.get("crop_mae", None),
230
+ })
231
+ return {"districts": out, "count": len(out)}
232
+
233
+
234
+ @app.get("/discoveries")
235
+ def get_discoveries():
236
+ return {
237
+ "discoveries": [
238
+ {
239
+ "rank": 1,
240
+ "title": "Flood → Inflation Lag",
241
+ "finding": "Extreme rainfall causes national inflation spike after 2-year lag",
242
+ "evidence": "Granger causality strength 0.347 — strongest link in system",
243
+ "case": "2022 Sindh floods → 2023 Pakistan inflation 30.8%",
244
+ "novel": True,
245
+ },
246
+ {
247
+ "rank": 2,
248
+ "title": "Sukkur Epicenter",
249
+ "finding": "Sukkur is Pakistan highest climate-economic risk zone",
250
+ "evidence": "2022 rainfall +1293% — highest z-score of all 10 districts",
251
+ "case": "Cotton -37.7%, Rice -21.5% in single flood year",
252
+ "novel": True,
253
+ },
254
+ {
255
+ "rank": 3,
256
+ "title": "GDP Contraction Predicted",
257
+ "finding": "LSTM correctly predicted 2023 GDP contraction direction",
258
+ "evidence": "Inflation MAE 2.87% — predicted 26% vs actual 30.8%",
259
+ "case": "Predicted 'contraction likely' → actual GDP -0.41%",
260
+ "novel": False,
261
+ },
262
+ ]
263
+ }
models/causal_links.csv ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ cause,effect,lag_years,strength
2
+ food_production_index,crop_stress,1,0.3646
3
+ rain_anomaly,inflation_pct,2,0.3471
4
+ gdp_growth_pct,crop_stress,1,0.2243
5
+ gdp_growth_pct,inflation_pct,4,0.2204
6
+ food_production_index,rain_anomaly,1,0.208
7
+ crop_stress,food_production_index,4,0.1896
8
+ rain_anomaly,crop_stress,2,0.1811
9
+ crop_stress,rain_anomaly,1,0.1748
10
+ gdp_growth_pct,food_production_index,4,0.1507
11
+ temp_anomaly,inflation_pct,4,0.1281
12
+ inflation_pct,food_production_index,1,0.125
13
+ inflation_pct,rain_anomaly,4,0.1126
14
+ temp_anomaly,rain_anomaly,4,0.1056
15
+ rain_anomaly,temp_anomaly,2,0.0886
16
+ crop_stress,temp_anomaly,3,0.0837
17
+ inflation_pct,gdp_growth_pct,2,0.0813
18
+ gdp_growth_pct,rain_anomaly,4,0.0805
19
+ temp_anomaly,food_production_index,1,0.0791
20
+ rain_anomaly,gdp_growth_pct,4,0.0782
21
+ food_production_index,temp_anomaly,3,0.0753
22
+ inflation_pct,temp_anomaly,1,0.0723
models/climashock_models.zip ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:dfce7d8c31f29a9fd9d3021344b672ea88d5b0e07960d573d0c3a586241be787
3
+ size 160909
models/district_metrics.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:efd03cd03ad12287e406d0d48842cf0dbde6c6353ce068f9a2aaaeffe4ab095b
3
+ size 397
models/ensemble_config.json ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "w_lstm_inf": 0.46407631271108857,
3
+ "w_xgb_inf": 0.5359236872889115,
4
+ "w_lstm_crop": 0.569888151721099,
5
+ "w_xgb_crop": 0.43011184827890103,
6
+ "ens_inf_mae": 3.1318494579313376,
7
+ "ens_crop_mae": 7.894275778247052,
8
+ "xgb_features": [
9
+ "rain_mean",
10
+ "rain_std",
11
+ "temp_mean",
12
+ "temp_max",
13
+ "humidity",
14
+ "gdp_growth_pct",
15
+ "agri_gdp_pct",
16
+ "rain_lag1",
17
+ "rain_lag2",
18
+ "inf_lag1",
19
+ "inf_lag2",
20
+ "crop_lag1",
21
+ "gdp_lag1"
22
+ ],
23
+ "lstm_features": [
24
+ "rain_mean",
25
+ "rain_std",
26
+ "temp_mean",
27
+ "temp_max",
28
+ "humidity",
29
+ "gdp_growth_pct",
30
+ "agri_gdp_pct"
31
+ ],
32
+ "seq_len": 3
33
+ }
models/feat_scaler.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d46c1c544ed4b8f534b7434f166220751d3ca3a1c050ba0c489e6129d514d6ea
3
+ size 1103
models/pakistan_climate.csv ADDED
The diff for this file is too large to render. See raw diff
 
models/target_scaler.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:058257ae8dcd6937616d2cd35d5c9cea09f88a3f5943f5291f1ce63cad6564a6
3
+ size 935
models/xgb_crop.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2f8271eebef0ee98379c4af07368c9d7fa2416c9592fe85779580b945d2ad6b5
3
+ size 302679
models/xgb_inf.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7eae0aa177e74d0d4b6a57e676034fdd0adb1bc0b93fb1102f44db632d0a5eb6
3
+ size 303008
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ fastapi==0.111.0
2
+ uvicorn==0.29.0
3
+ scikit-learn==1.4.2
4
+ xgboost==2.1.0
5
+ pandas==2.2.2
6
+ numpy==1.26.4
7
+ joblib==1.4.2