krishy commited on
Commit
2d723e9
·
1 Parent(s): fa08517

Refine scoring calibration, Kisan recommendations, and stress-test edge cases

Browse files
Files changed (5) hide show
  1. app.py +104 -32
  2. climaiq_engine.py +44 -19
  3. climaiq_gemma.py +11 -0
  4. climaiq_model.pkl +0 -0
  5. climaiq_scaler.pkl +0 -0
app.py CHANGED
@@ -623,8 +623,8 @@ def build_farmer_input(prefix: str, compact: bool = False, label_lang: Optional[
623
  prev_default = st.radio(
624
  text["default_hist"], ["No", "Yes"], horizontal=True, key=f"{prefix}_default"
625
  )
626
- rain_def = st.slider(text["rain_def"], 0, 60, 20, 1, key=f"{prefix}_rain")
627
- spi = st.slider(text["spi"], -3.0, 2.0, -1.0, 0.1, key=f"{prefix}_spi")
628
  drought = st.selectbox(
629
  text["drought_years"],
630
  DROUGHT_OPTIONS,
@@ -643,8 +643,8 @@ def build_farmer_input(prefix: str, compact: bool = False, label_lang: Optional[
643
  )
644
  st.markdown("---")
645
  st.markdown(f"<div class='section-label'>{html.escape(text['section_climate'])}</div>", unsafe_allow_html=True)
646
- rain_def = st.slider(text["rain_def"], 0, 60, 20, 1, key=f"{prefix}_rain")
647
- spi = st.slider(text["spi"], -3.0, 2.0, -1.0, 0.1, key=f"{prefix}_spi")
648
  spi_label_idx = 0 if spi <= -1.5 else 1 if spi <= -0.5 else 2
649
  st.caption(SPI_LABELS[lang][spi_label_idx])
650
  drought = st.selectbox(
@@ -708,19 +708,64 @@ def build_score_gauge(score: int, risk_band: str):
708
  return plotly_theme(fig)
709
 
710
 
711
- def extract_action_cards(text: str, language: str) -> List[str]:
712
  fallback = (
713
- ["🌱 Fasal Badlein", "💧 Sinchai Sudharen", "🛡️ Bima Karwain"]
714
  if language == "Hindi"
715
- else ["🌱 Shift Crop Mix", "💧 Improve Irrigation", "🛡️ Take Insurance"]
716
  )
717
- if not text:
718
- return fallback
719
- lines = [ln.strip(" -•\t") for ln in text.splitlines() if ln.strip()]
720
- picks = [
721
- ln for ln in lines if any(k in ln.lower() for k in ["insurance", "crop", "irrig", "बीमा", "फसल", "सिंच"])
722
- ]
723
- return (picks[:3] if len(picks) >= 3 else fallback)[:3]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
724
 
725
 
726
  def try_farmer_explanation(client, farmer_input, result, language: str) -> str:
@@ -821,7 +866,7 @@ def tab_kisan(client):
821
  stored.setdefault("explanation_sources", {})[lang] = get_inference_config()[0]
822
  explanation = stored["explanations"][lang]
823
  expl_mode = stored.get("explanation_sources", {}).get(lang, get_inference_config()[0])
824
- cards = extract_action_cards(explanation, lang)
825
 
826
  st.plotly_chart(build_score_gauge(result["credit_score"], result["risk_band"]), use_container_width=True)
827
  st.markdown(
@@ -962,6 +1007,11 @@ def tab_officer(client):
962
 
963
 
964
  def build_portfolio(n_loans: int, avg_loan: float, dominant_crop: str):
 
 
 
 
 
965
  crops = list(CROP_WATER_MAP.keys())
966
  probs_map = {
967
  "Cotton": [0.5 if c == "Cotton" else 0.5 / (len(crops) - 1) for c in crops],
@@ -972,16 +1022,16 @@ def build_portfolio(n_loans: int, avg_loan: float, dominant_crop: str):
972
  crop_probs = probs_map[dominant_crop]
973
  portfolio = []
974
  for _ in range(n_loans):
975
- crop = np.random.choice(crops, p=crop_probs)
976
  portfolio.append(
977
  {
978
- "age": int(np.random.randint(28, 62)),
979
- "land_size_acres": round(np.random.uniform(1, 10), 2),
980
- "annual_income_lakhs": round(np.random.uniform(1.5, 10), 2),
981
- "loan_amount_lakhs": round(max(0.5, np.random.normal(avg_loan, 0.6)), 2),
982
- "previous_defaults": int(np.random.choice([0, 1], p=[0.82, 0.18])),
983
  "crop_type": crop,
984
- "state": str(np.random.choice(["Maharashtra", "Punjab"], p=[0.6, 0.4])),
985
  "rainfall_deficit_pct": 0.0,
986
  "spi": 0.0,
987
  "consecutive_drought_years": 0,
@@ -1014,17 +1064,39 @@ def tab_portfolio(client):
1014
  if run:
1015
  with st.spinner("Simulating portfolio across climate scenarios…"):
1016
  portfolio = build_portfolio(int(n_loans), float(avg_loan), dominant_crop)
1017
- stress = run_stress_test(st.session_state.model, st.session_state.scaler, portfolio)
1018
- rr = recovery_rate / 100
1019
- for row in stress:
1020
- row["total_loss_lakhs"] = round(row["avg_default_pct"] / 100 * avg_loan * (1 - rr) * n_loans, 2)
1021
- st.session_state.last_stress_result = {"stress": stress, "narratives": {}, "narrative_sources": {}}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1022
  st.success("Stress test complete. Charts are below.")
1023
 
1024
  if not st.session_state.last_stress_result:
1025
  st.markdown(narr_html(text["empty_portfolio"]), unsafe_allow_html=True)
1026
  return
1027
 
 
 
 
 
 
 
 
 
1028
  stress = st.session_state.last_stress_result["stress"]
1029
  df = pd.DataFrame(stress)
1030
  order = ["Normal Monsoon", "Moderate Drought", "Severe Drought", "Back-to-Back Drought"]
@@ -1049,6 +1121,7 @@ def tab_portfolio(client):
1049
  "Back-to-Back Drought": "#7F0000",
1050
  },
1051
  )
 
1052
  bar.update_layout(showlegend=False, yaxis_title="", xaxis_title="Estimated loss (₹ lakhs)", height=330)
1053
  st.plotly_chart(plotly_theme(bar), use_container_width=True)
1054
 
@@ -1098,13 +1171,12 @@ def tab_portfolio(client):
1098
 
1099
  normal_loss = float(df[df["scenario"] == "Normal Monsoon"]["total_loss_lakhs"].iloc[0])
1100
  worst_loss = float(df["total_loss_lakhs"].max())
1101
- gap = round(worst_loss - normal_loss, 2)
1102
- buf = int(gap // 100 * 100)
1103
  cap_lines = (
1104
  f"<b>Capital buffer indicator</b><br><br>"
1105
- f"Baseline (normal monsoon) loss estimate: ₹{normal_loss} lakhs<br>"
1106
- f"Incremental stress to worst scenario shown: ₹{gap} lakhs<br>"
1107
- f"Rule-of-thumb buffer to discuss with risk: ₹{buf} lakhs+"
1108
  )
1109
  st.markdown(f"<div class='soft-note narr-body'>{cap_lines}</div>", unsafe_allow_html=True)
1110
 
 
623
  prev_default = st.radio(
624
  text["default_hist"], ["No", "Yes"], horizontal=True, key=f"{prefix}_default"
625
  )
626
+ rain_def = st.slider(text["rain_def"], 0, 60, 0, 1, key=f"{prefix}_rain")
627
+ spi = st.slider(text["spi"], -3.0, 2.0, 0.3, 0.1, key=f"{prefix}_spi")
628
  drought = st.selectbox(
629
  text["drought_years"],
630
  DROUGHT_OPTIONS,
 
643
  )
644
  st.markdown("---")
645
  st.markdown(f"<div class='section-label'>{html.escape(text['section_climate'])}</div>", unsafe_allow_html=True)
646
+ rain_def = st.slider(text["rain_def"], 0, 60, 0, 1, key=f"{prefix}_rain")
647
+ spi = st.slider(text["spi"], -3.0, 2.0, 0.3, 0.1, key=f"{prefix}_spi")
648
  spi_label_idx = 0 if spi <= -1.5 else 1 if spi <= -0.5 else 2
649
  st.caption(SPI_LABELS[lang][spi_label_idx])
650
  drought = st.selectbox(
 
708
  return plotly_theme(fig)
709
 
710
 
711
+ def extract_action_cards(text: str, language: str, farmer_input: Dict, result: Dict) -> List[str]:
712
  fallback = (
713
+ ["🌱 फसल विविधीकरण करें", "💧 सिंचाई दक्षता बढ़ाएँ", "🛡️ फसल बीमा लें"]
714
  if language == "Hindi"
715
+ else ["🌱 Diversify crop mix", "💧 Improve irrigation efficiency", "🛡️ Take crop insurance"]
716
  )
717
+
718
+ def _uniq(items: List[str]) -> List[str]:
719
+ out: List[str] = []
720
+ for it in items:
721
+ clean = it.strip()
722
+ if clean and clean not in out:
723
+ out.append(clean)
724
+ return out
725
+
726
+ picked_from_text: List[str] = []
727
+ if text:
728
+ lines = [ln.strip(" -•\t") for ln in text.splitlines() if ln.strip()]
729
+ picked_from_text = [
730
+ ln
731
+ for ln in lines
732
+ if any(k in ln.lower() for k in ["insurance", "crop", "irrig", "बीमा", "फसल", "सिंच"])
733
+ ][:3]
734
+
735
+ rf = float(farmer_input.get("rainfall_deficit_pct", 0.0))
736
+ spi = float(farmer_input.get("spi", 0.0))
737
+ dyears = int(farmer_input.get("consecutive_drought_years", 0))
738
+ prev = int(farmer_input.get("previous_defaults", 0))
739
+ dti = float(farmer_input.get("loan_amount_lakhs", 0.0)) / max(float(farmer_input.get("annual_income_lakhs", 1.0)), 0.1)
740
+ crop = str(farmer_input.get("crop_type", ""))
741
+ risk_band = str(result.get("risk_band", ""))
742
+
743
+ dynamic: List[str] = []
744
+ if language == "Hindi":
745
+ if rf <= -20 or spi <= -1.2 or dyears >= 1:
746
+ dynamic.append("💧 तुरंत जल-संरक्षण योजना अपनाएँ (ड्रिप/मल्च/लाइन सिंचाई)")
747
+ if crop in {"Rice", "Sugarcane"} and rf <= -15:
748
+ dynamic.append("🌱 कम पानी वाली फसल/मिश्रित खेती पर विचार करें")
749
+ if dti > 0.5:
750
+ dynamic.append("📉 ऋण राशि या किश्त संरचना को आय के अनुसार पुनर्संतुलित करें")
751
+ if prev == 1:
752
+ dynamic.append("🧾 पिछले बकाये के लिए समयबद्ध चुकौती योजना बनाएं")
753
+ if "High" in risk_band or "Medium" in risk_band:
754
+ dynamic.append("🛡️ मौसम-आधारित फसल बीमा और नुकसान कवर सक्रिय करें")
755
+ else:
756
+ if rf <= -20 or spi <= -1.2 or dyears >= 1:
757
+ dynamic.append("💧 Start an immediate water-conservation plan (drip/mulch/scheduling)")
758
+ if crop in {"Rice", "Sugarcane"} and rf <= -15:
759
+ dynamic.append("🌱 Consider a lower water-intensity crop mix for the next cycle")
760
+ if dti > 0.5:
761
+ dynamic.append("📉 Rebalance loan size/instalments to better match farm income")
762
+ if prev == 1:
763
+ dynamic.append("🧾 Create a time-bound plan to clear past overdues")
764
+ if "High" in risk_band or "Medium" in risk_band:
765
+ dynamic.append("🛡️ Activate weather-index crop insurance and loss cover")
766
+
767
+ cards = _uniq(picked_from_text + dynamic + fallback)
768
+ return cards[:3]
769
 
770
 
771
  def try_farmer_explanation(client, farmer_input, result, language: str) -> str:
 
866
  stored.setdefault("explanation_sources", {})[lang] = get_inference_config()[0]
867
  explanation = stored["explanations"][lang]
868
  expl_mode = stored.get("explanation_sources", {}).get(lang, get_inference_config()[0])
869
+ cards = extract_action_cards(explanation, lang, stored["input"], result)
870
 
871
  st.plotly_chart(build_score_gauge(result["credit_score"], result["risk_band"]), use_container_width=True)
872
  st.markdown(
 
1007
 
1008
 
1009
  def build_portfolio(n_loans: int, avg_loan: float, dominant_crop: str):
1010
+ """
1011
+ Synthetic portfolio draws. Uses an isolated RNG so each Run produces a fresh book even if
1012
+ global np.random was seeded elsewhere (e.g. training helpers).
1013
+ """
1014
+ rng = np.random.default_rng()
1015
  crops = list(CROP_WATER_MAP.keys())
1016
  probs_map = {
1017
  "Cotton": [0.5 if c == "Cotton" else 0.5 / (len(crops) - 1) for c in crops],
 
1022
  crop_probs = probs_map[dominant_crop]
1023
  portfolio = []
1024
  for _ in range(n_loans):
1025
+ crop = str(rng.choice(crops, p=crop_probs))
1026
  portfolio.append(
1027
  {
1028
+ "age": int(rng.integers(28, 62)),
1029
+ "land_size_acres": round(float(rng.uniform(1, 10)), 2),
1030
+ "annual_income_lakhs": round(float(rng.uniform(1.5, 10)), 2),
1031
+ "loan_amount_lakhs": round(max(0.5, float(rng.normal(avg_loan, 0.6))), 2),
1032
+ "previous_defaults": int(rng.choice([0, 1], p=[0.82, 0.18])),
1033
  "crop_type": crop,
1034
+ "state": str(rng.choice(["Maharashtra", "Punjab"], p=[0.6, 0.4])),
1035
  "rainfall_deficit_pct": 0.0,
1036
  "spi": 0.0,
1037
  "consecutive_drought_years": 0,
 
1064
  if run:
1065
  with st.spinner("Simulating portfolio across climate scenarios…"):
1066
  portfolio = build_portfolio(int(n_loans), float(avg_loan), dominant_crop)
1067
+ rr = recovery_rate / 100
1068
+ stress = run_stress_test(
1069
+ st.session_state.model,
1070
+ st.session_state.scaler,
1071
+ portfolio,
1072
+ avg_loan_lakhs=float(avg_loan),
1073
+ recovery_rate=float(rr),
1074
+ )
1075
+ st.session_state.last_stress_result = {
1076
+ "stress": stress,
1077
+ "narratives": {},
1078
+ "narrative_sources": {},
1079
+ "portfolio_meta": {
1080
+ "n_loans": int(n_loans),
1081
+ "avg_loan": float(avg_loan),
1082
+ "recovery_pct": int(recovery_rate),
1083
+ "dominant_crop": dominant_crop,
1084
+ },
1085
+ }
1086
  st.success("Stress test complete. Charts are below.")
1087
 
1088
  if not st.session_state.last_stress_result:
1089
  st.markdown(narr_html(text["empty_portfolio"]), unsafe_allow_html=True)
1090
  return
1091
 
1092
+ meta = st.session_state.last_stress_result.get("portfolio_meta")
1093
+ if meta:
1094
+ st.caption(
1095
+ f"Charts reflect your last Run: {meta['n_loans']} loans, avg ticket ₹{meta['avg_loan']} lakhs, "
1096
+ f"{meta['recovery_pct']}% recovery, dominant crop mix {meta['dominant_crop']}. "
1097
+ "Change inputs and tap Run stress test again for a new simulated book."
1098
+ )
1099
+
1100
  stress = st.session_state.last_stress_result["stress"]
1101
  df = pd.DataFrame(stress)
1102
  order = ["Normal Monsoon", "Moderate Drought", "Severe Drought", "Back-to-Back Drought"]
 
1121
  "Back-to-Back Drought": "#7F0000",
1122
  },
1123
  )
1124
+ bar.update_traces(texttemplate="%{x:.2f}", textposition="outside", cliponaxis=False)
1125
  bar.update_layout(showlegend=False, yaxis_title="", xaxis_title="Estimated loss (₹ lakhs)", height=330)
1126
  st.plotly_chart(plotly_theme(bar), use_container_width=True)
1127
 
 
1171
 
1172
  normal_loss = float(df[df["scenario"] == "Normal Monsoon"]["total_loss_lakhs"].iloc[0])
1173
  worst_loss = float(df["total_loss_lakhs"].max())
1174
+ gap = round(max(worst_loss - normal_loss, 0), 2)
 
1175
  cap_lines = (
1176
  f"<b>Capital buffer indicator</b><br><br>"
1177
+ f"Baseline (normal monsoon) loss estimate: ₹{round(normal_loss, 2)} lakhs<br>"
1178
+ f"Worst scenario loss in this run: ₹{round(worst_loss, 2)} lakhs<br>"
1179
+ f"<b>Incremental climate tail</b> (worst minus baseline): ₹{gap} lakhs — use this gap when discussing extra provisioning vs normal weather."
1180
  )
1181
  st.markdown(f"<div class='soft-note narr-body'>{cap_lines}</div>", unsafe_allow_html=True)
1182
 
climaiq_engine.py CHANGED
@@ -188,24 +188,38 @@ def engineer_single_input(data: dict) -> pd.DataFrame:
188
 
189
  # ─── Credit Score Calculation ──────────────────────────────────────────────────
190
 
191
- def compute_credit_score(model, scaler, feature_row: pd.DataFrame,
192
- base_score=650, pdo=50) -> int:
193
- """Convert default probability into 300–850 credit score."""
194
- coef = model.coef_[0]
195
- factor = pdo / np.log(2)
196
- offset = base_score - factor * np.log(20)
 
 
 
 
197
 
198
- z = scaler.transform(feature_row)[0]
199
- score = offset + np.sum(z * (-factor * coef))
 
 
 
 
 
 
 
 
 
 
200
  return int(np.clip(score, 300, 850))
201
 
202
 
203
  def get_risk_band(score: int) -> str:
204
  if score >= 700:
205
  return "Very Low Risk"
206
- elif score >= 650:
207
  return "Low Risk"
208
- elif score >= 600:
209
  return "Medium Risk"
210
  else:
211
  return "High Risk"
@@ -286,20 +300,31 @@ def predict_single(data: dict, model, scaler) -> dict:
286
 
287
  # ─── Stress Test ───────────────────────────────────────────────────────────────
288
 
289
- def run_stress_test(model, scaler, base_portfolio: list) -> list:
 
 
 
 
 
 
290
  """
291
  Run 4 climate scenarios across a list of farmer input dicts.
292
- Returns list of scenario result dicts.
293
  """
294
  scenarios = {
295
- "Normal Monsoon": {"rainfall_deficit_pct": 0, "spi": 0, "consecutive_drought_years": 0},
296
- "Moderate Drought": {"rainfall_deficit_pct": -20, "spi": -1.2, "consecutive_drought_years": 1},
297
- "Severe Drought": {"rainfall_deficit_pct": -35, "spi": -1.8, "consecutive_drought_years": 1},
298
  "Back-to-Back Drought": {"rainfall_deficit_pct": -25, "spi": -1.5, "consecutive_drought_years": 2},
299
  }
300
 
301
- avg_loan = 3.0
302
- recovery = 0.30
 
 
 
 
 
303
  results = []
304
 
305
  for scenario_name, overrides in scenarios.items():
@@ -310,13 +335,13 @@ def run_stress_test(model, scaler, base_portfolio: list) -> list:
310
  probs.append(result["default_probability"] / 100)
311
 
312
  avg_default_pct = np.mean(probs) * 100
313
- total_loss = np.mean(probs) * avg_loan * (1 - recovery) * len(base_portfolio)
314
 
315
  results.append({
316
  "scenario": scenario_name,
317
  "avg_default_pct": round(avg_default_pct, 2),
318
  "total_loss_lakhs": round(total_loss, 2),
319
- "portfolio_size": len(base_portfolio)
320
  })
321
 
322
  return results
 
188
 
189
  # ─── Credit Score Calculation ──────────────────────────────────────────────────
190
 
191
+ def compute_credit_score(
192
+ model,
193
+ scaler,
194
+ feature_row: pd.DataFrame,
195
+ base_score: int = 650,
196
+ pdo: int = 50,
197
+ p_ref: float = 0.20,
198
+ ) -> int:
199
+ """
200
+ Convert model PD to score using a log-odds mapping.
201
 
202
+ Calibration anchor:
203
+ - score = base_score when PD = p_ref
204
+ - +pdo points for each halving of default odds
205
+ """
206
+ scaled = scaler.transform(feature_row)
207
+ pd_raw = float(model.predict_proba(scaled)[0][1])
208
+ pd_safe = float(np.clip(pd_raw, 1e-4, 1 - 1e-4))
209
+
210
+ factor = pdo / np.log(2)
211
+ logit = np.log(pd_safe / (1 - pd_safe))
212
+ logit_ref = np.log(p_ref / (1 - p_ref))
213
+ score = base_score - factor * (logit - logit_ref)
214
  return int(np.clip(score, 300, 850))
215
 
216
 
217
  def get_risk_band(score: int) -> str:
218
  if score >= 700:
219
  return "Very Low Risk"
220
+ elif score >= 640:
221
  return "Low Risk"
222
+ elif score >= 560:
223
  return "Medium Risk"
224
  else:
225
  return "High Risk"
 
300
 
301
  # ─── Stress Test ───────────────────────────────────────────────────────────────
302
 
303
+ def run_stress_test(
304
+ model,
305
+ scaler,
306
+ base_portfolio: list,
307
+ avg_loan_lakhs: float = 3.0,
308
+ recovery_rate: float = 0.30,
309
+ ) -> list:
310
  """
311
  Run 4 climate scenarios across a list of farmer input dicts.
312
+ Expected portfolio loss uses the supplied average ticket (₹ lakhs) and recovery_rate (0–1).
313
  """
314
  scenarios = {
315
+ "Normal Monsoon": {"rainfall_deficit_pct": 0, "spi": 0, "consecutive_drought_years": 0},
316
+ "Moderate Drought": {"rainfall_deficit_pct": -20, "spi": -1.2, "consecutive_drought_years": 1},
317
+ "Severe Drought": {"rainfall_deficit_pct": -35, "spi": -1.8, "consecutive_drought_years": 1},
318
  "Back-to-Back Drought": {"rainfall_deficit_pct": -25, "spi": -1.5, "consecutive_drought_years": 2},
319
  }
320
 
321
+ n = len(base_portfolio)
322
+ rm = float(recovery_rate)
323
+ if rm < 0:
324
+ rm = 0.0
325
+ elif rm > 1:
326
+ rm = 1.0
327
+
328
  results = []
329
 
330
  for scenario_name, overrides in scenarios.items():
 
335
  probs.append(result["default_probability"] / 100)
336
 
337
  avg_default_pct = np.mean(probs) * 100
338
+ total_loss = np.mean(probs) * float(avg_loan_lakhs) * (1 - rm) * n
339
 
340
  results.append({
341
  "scenario": scenario_name,
342
  "avg_default_pct": round(avg_default_pct, 2),
343
  "total_loss_lakhs": round(total_loss, 2),
344
+ "portfolio_size": n,
345
  })
346
 
347
  return results
climaiq_gemma.py CHANGED
@@ -31,6 +31,13 @@ GEMMA_MODEL = GEMMA_MODEL_CLOUD
31
 
32
  DEFAULT_OLLAMA_BASE = os.environ.get("OLLAMA_BASE_URL", "http://127.0.0.1:11434").rstrip("/")
33
  DEFAULT_OLLAMA_MODEL = os.environ.get("OLLAMA_MODEL", "gemma3:4b")
 
 
 
 
 
 
 
34
 
35
 
36
  def _ollama_installed_name_matches(installed: str, requested: str) -> bool:
@@ -153,6 +160,8 @@ def generate_explanation_text(
153
 
154
  def _build_farmer_prompt(farmer_data: dict, result: dict, language: str) -> str:
155
  drivers = "\n".join([f"- {d['display_name']}" for d in result["top_risk_drivers"]])
 
 
156
 
157
  if language == "Hindi":
158
  return f"""
@@ -178,6 +187,7 @@ ClimaIQ परिणाम:
178
  1. किसान को सरल भाषा में बताएं कि उनका स्कोर क्यों कम/अधिक है
179
  2. मौसम और फसल का जोखिम पर क्या असर हुआ, यह समझाएं
180
  3. किसान 3 व्यावहारिक कदम क्या उठा सकते हैं (बीमा, वैकल्पिक फसल, सिंचाई आदि)
 
181
  केवल हिंदी में उत्तर दें। जटिल शब्दों से बचें।
182
  """
183
  else:
@@ -204,6 +214,7 @@ In 4-5 simple sentences:
204
  1. Explain why their score is what it is
205
  2. Explain how the weather and crop type has affected their risk
206
  3. Give 3 practical steps they can take (insurance, alternate crops, irrigation, etc.)
 
207
  Use simple, non-technical language. Be empathetic and constructive.
208
  """
209
 
 
31
 
32
  DEFAULT_OLLAMA_BASE = os.environ.get("OLLAMA_BASE_URL", "http://127.0.0.1:11434").rstrip("/")
33
  DEFAULT_OLLAMA_MODEL = os.environ.get("OLLAMA_MODEL", "gemma3:4b")
34
+ CROP_WATER_CLASS = {
35
+ "Rice": "very high",
36
+ "Sugarcane": "very high",
37
+ "Cotton": "moderate",
38
+ "Wheat": "low",
39
+ "Millets": "very low",
40
+ }
41
 
42
 
43
  def _ollama_installed_name_matches(installed: str, requested: str) -> bool:
 
160
 
161
  def _build_farmer_prompt(farmer_data: dict, result: dict, language: str) -> str:
162
  drivers = "\n".join([f"- {d['display_name']}" for d in result["top_risk_drivers"]])
163
+ crop = str(farmer_data["crop_type"])
164
+ water_class = CROP_WATER_CLASS.get(crop, "moderate")
165
 
166
  if language == "Hindi":
167
  return f"""
 
187
  1. किसान को सरल भाषा में बताएं कि उनका स्कोर क्यों कम/अधिक है
188
  2. मौसम और फसल का जोखिम पर क्या असर हुआ, यह समझाएं
189
  3. किसान 3 व्यावहारिक कदम क्या उठा सकते हैं (बीमा, वैकल्पिक फसल, सिंचाई आदि)
190
+ महत्वपूर्ण तथ्य: इस इंजन में {crop} की पानी-आधारित श्रेणी "{water_class}" मानी जाती है। पानी-आवश्यकता के बारे में इससे उल्टा दावा न करें।
191
  केवल हिंदी में उत्तर दें। जटिल शब्दों से बचें।
192
  """
193
  else:
 
214
  1. Explain why their score is what it is
215
  2. Explain how the weather and crop type has affected their risk
216
  3. Give 3 practical steps they can take (insurance, alternate crops, irrigation, etc.)
217
+ Important fact guardrail: in this engine, {crop} is classified as "{water_class}" water-intensity. Do not claim the opposite.
218
  Use simple, non-technical language. Be empathetic and constructive.
219
  """
220
 
climaiq_model.pkl CHANGED
Binary files a/climaiq_model.pkl and b/climaiq_model.pkl differ
 
climaiq_scaler.pkl CHANGED
Binary files a/climaiq_scaler.pkl and b/climaiq_scaler.pkl differ