import os import requests import numpy as np import pandas as pd import joblib import gradio as gr from huggingface_hub import hf_hub_download # ───────────────────────────────────────────────────────────────────────────── # CONFIG # ───────────────────────────────────────────────────────────────────────────── HF_REPO = "Sheshank2609/crop-recommendation-system" MANDI_API_KEY = os.getenv("MANDI_API_KEY", "579b464db66ec23bdd0000011718ca7e68464b7f48051c18eb346a7b") MANDI_BASE_URL = "https://api.data.gov.in/resource/35985678-0d79-46b4-9ed6-6f13308a1d24" # ───────────────────────────────────────────────────────────────────────────── # LOAD MODELS # ───────────────────────────────────────────────────────────────────────────── print("Loading Model 1 (soil/climate)…") model1 = joblib.load(hf_hub_download(HF_REPO, "model1_npk.pkl")) label_enc = joblib.load(hf_hub_download(HF_REPO, "model1_label_encoder.pkl")) print("Loading Model 2 (regional)…") model2_df = pd.read_csv(hf_hub_download(HF_REPO, "model2_full_scored.csv")) print("✅ Models loaded.") # ───────────────────────────────────────────────────────────────────────────── # CROP METADATA — per hectare figures, Maharashtra averages # ───────────────────────────────────────────────────────────────────────────── CROP_META = { "rice": {"input_cost": 30000, "yield_qtl": 25, "demand": "High", "risk": "Low", "days": 120, "best_season": "Kharif"}, "maize": {"input_cost": 20000, "yield_qtl": 45, "demand": "Medium", "risk": "Low", "days": 90, "best_season": "Kharif"}, "chickpea": {"input_cost": 20000, "yield_qtl": 12, "demand": "Medium", "risk": "Low", "days": 110, "best_season": "Rabi"}, "kidneybeans": {"input_cost": 22000, "yield_qtl": 14, "demand": "Low", "risk": "Medium", "days": 100, "best_season": "Kharif"}, "pigeonpeas": {"input_cost": 18000, "yield_qtl": 10, "demand": "Medium", "risk": "Low", "days": 180, "best_season": "Kharif"}, "mothbeans": {"input_cost": 15000, "yield_qtl": 8, "demand": "Low", "risk": "Low", "days": 80, "best_season": "Kharif"}, "mungbean": {"input_cost": 16000, "yield_qtl": 9, "demand": "Medium", "risk": "Low", "days": 70, "best_season": "Kharif"}, "blackgram": {"input_cost": 16000, "yield_qtl": 9, "demand": "Medium", "risk": "Low", "days": 80, "best_season": "Kharif"}, "lentil": {"input_cost": 17000, "yield_qtl": 10, "demand": "Medium", "risk": "Low", "days": 110, "best_season": "Rabi"}, "pomegranate": {"input_cost": 80000, "yield_qtl": 120, "demand": "High", "risk": "Low", "days": 365, "best_season": "Whole Year"}, "banana": {"input_cost": 80000, "yield_qtl": 400, "demand": "High", "risk": "Medium", "days": 300, "best_season": "Whole Year"}, "mango": {"input_cost": 55000, "yield_qtl": 80, "demand": "High", "risk": "Medium", "days": 365, "best_season": "Summer"}, "grapes": {"input_cost": 120000, "yield_qtl": 150, "demand": "High", "risk": "High", "days": 365, "best_season": "Rabi"}, "watermelon": {"input_cost": 35000, "yield_qtl": 200, "demand": "Medium", "risk": "High", "days": 80, "best_season": "Summer"}, "muskmelon": {"input_cost": 30000, "yield_qtl": 150, "demand": "Medium", "risk": "High", "days": 80, "best_season": "Summer"}, "apple": {"input_cost": 150000, "yield_qtl": 100, "demand": "High", "risk": "Low", "days": 365, "best_season": "Whole Year"}, "orange": {"input_cost": 60000, "yield_qtl": 100, "demand": "High", "risk": "Low", "days": 365, "best_season": "Rabi"}, "papaya": {"input_cost": 40000, "yield_qtl": 400, "demand": "Medium", "risk": "Medium", "days": 240, "best_season": "Whole Year"}, "coconut": {"input_cost": 45000, "yield_qtl": 50, "demand": "Medium", "risk": "Low", "days": 365, "best_season": "Whole Year"}, "cotton": {"input_cost": 35000, "yield_qtl": 18, "demand": "High", "risk": "Medium", "days": 180, "best_season": "Kharif"}, "jute": {"input_cost": 25000, "yield_qtl": 20, "demand": "Low", "risk": "Medium", "days": 120, "best_season": "Kharif"}, "coffee": {"input_cost": 90000, "yield_qtl": 12, "demand": "Medium", "risk": "Low", "days": 365, "best_season": "Whole Year"}, "arhar/tur": {"input_cost": 18000, "yield_qtl": 10, "demand": "Medium", "risk": "Low", "days": 180, "best_season": "Kharif"}, "bajra": {"input_cost": 14000, "yield_qtl": 20, "demand": "Low", "risk": "Low", "days": 80, "best_season": "Kharif"}, "castor seed": {"input_cost": 18000, "yield_qtl": 15, "demand": "Low", "risk": "Medium", "days": 180, "best_season": "Kharif"}, "gram": {"input_cost": 20000, "yield_qtl": 12, "demand": "Medium", "risk": "Low", "days": 110, "best_season": "Rabi"}, "groundnut": {"input_cost": 28000, "yield_qtl": 20, "demand": "Medium", "risk": "Medium", "days": 130, "best_season": "Kharif"}, "jowar": {"input_cost": 16000, "yield_qtl": 18, "demand": "Low", "risk": "Low", "days": 100, "best_season": "Kharif"}, "linseed": {"input_cost": 14000, "yield_qtl": 8, "demand": "Low", "risk": "Low", "days": 120, "best_season": "Rabi"}, "moong (green gram)": {"input_cost": 16000, "yield_qtl": 9, "demand": "Medium", "risk": "Low", "days": 70, "best_season": "Kharif"}, "niger seed": {"input_cost": 12000, "yield_qtl": 6, "demand": "Low", "risk": "Low", "days": 100, "best_season": "Kharif"}, "onion": {"input_cost": 40000, "yield_qtl": 180, "demand": "High", "risk": "High", "days": 120, "best_season": "Rabi"}, "other cereals": {"input_cost": 15000, "yield_qtl": 15, "demand": "Low", "risk": "Low", "days": 100, "best_season": "Kharif"}, "other kharif pulses":{"input_cost": 15000, "yield_qtl": 8, "demand": "Low", "risk": "Low", "days": 90, "best_season": "Kharif"}, "other rabi pulses": {"input_cost": 15000, "yield_qtl": 8, "demand": "Low", "risk": "Low", "days": 100, "best_season": "Rabi"}, "other summer pulses":{"input_cost": 15000, "yield_qtl": 8, "demand": "Low", "risk": "Low", "days": 80, "best_season": "Summer"}, "ragi": {"input_cost": 14000, "yield_qtl": 18, "demand": "Low", "risk": "Low", "days": 120, "best_season": "Kharif"}, "rapeseed & mustard": {"input_cost": 16000, "yield_qtl": 12, "demand": "Medium", "risk": "Low", "days": 110, "best_season": "Rabi"}, "safflower": {"input_cost": 15000, "yield_qtl": 10, "demand": "Low", "risk": "Low", "days": 130, "best_season": "Rabi"}, "sesamum": {"input_cost": 14000, "yield_qtl": 6, "demand": "Low", "risk": "Medium", "days": 90, "best_season": "Kharif"}, "small millets": {"input_cost": 12000, "yield_qtl": 10, "demand": "Low", "risk": "Low", "days": 90, "best_season": "Kharif"}, "soyabean": {"input_cost": 22000, "yield_qtl": 15, "demand": "High", "risk": "Medium", "days": 100, "best_season": "Kharif"}, "sugarcane": {"input_cost": 45000, "yield_qtl": 750, "demand": "High", "risk": "Low", "days": 365, "best_season": "Whole Year"}, "sunflower": {"input_cost": 18000, "yield_qtl": 12, "demand": "Medium", "risk": "Medium", "days": 100, "best_season": "Rabi"}, "tobacco": {"input_cost": 35000, "yield_qtl": 20, "demand": "Low", "risk": "Low", "days": 150, "best_season": "Rabi"}, "tomato": {"input_cost": 60000, "yield_qtl": 250, "demand": "High", "risk": "High", "days": 90, "best_season": "Rabi"}, "urad": {"input_cost": 16000, "yield_qtl": 9, "demand": "Medium", "risk": "Low", "days": 80, "best_season": "Kharif"}, "wheat": {"input_cost": 25000, "yield_qtl": 32, "demand": "Medium", "risk": "Low", "days": 120, "best_season": "Rabi"}, "other oilseeds": {"input_cost": 14000, "yield_qtl": 10, "demand": "Low", "risk": "Low", "days": 100, "best_season": "Kharif"}, "cotton(lint)": {"input_cost": 35000, "yield_qtl": 18, "demand": "High", "risk": "Medium", "days": 180, "best_season": "Kharif"}, } # Irrigation requirement per crop: "low"=rainfed OK, "medium"=seasonal OK, "high"=needs assured CROP_IRRIGATION = { "rice": "high", "sugarcane": "high", "banana": "high", "grapes": "high", "jute": "high", "apple": "high", "cotton": "medium", "maize": "medium", "soyabean": "medium", "onion": "medium", "tomato": "medium", "groundnut": "medium", "sunflower": "medium", "wheat": "medium", "pomegranate": "medium", "mango": "medium", "orange": "medium", "papaya": "medium", "coconut": "medium", "watermelon": "medium", "muskmelon": "medium", "coffee": "medium", "tobacco": "medium", "cotton(lint)": "medium", "other summer pulses": "medium", "chickpea": "low", "pigeonpeas": "low", "bajra": "low", "jowar": "low", "ragi": "low", "lentil": "low", "gram": "low", "arhar/tur": "low", "mungbean": "low", "urad": "low", "blackgram": "low", "mothbeans": "low", "kidneybeans": "low", "moong (green gram)": "low", "small millets": "low", "castor seed": "low", "linseed": "low", "sesamum": "low", "niger seed": "low", "safflower": "low", "rapeseed & mustard": "low", "other cereals": "low", "other kharif pulses": "low", "other rabi pulses": "low", "other oilseeds": "low", } # Farmer-selectable irrigation options → numeric availability level (0-3) IRRIGATION_LEVEL = { "Assured (Canal / River)": 3, "Borewell / Pump": 2, "Seasonal / Rain-fed": 1, "No Irrigation (Dryland)": 0, } # ───────────────────────────────────────────────────────────────────────────── # MARKET INTELLIGENCE — 3 cited, defensible sources # ───────────────────────────────────────────────────────────────────────────── # # Source 1 — MSP 2024-25 (GoI CCEA announcements) # Use: "Is the govt backing this crop?" High MSP = high govt demand signal # # Source 2 — Mandi price spread from data.gov.in live API # Use: "How volatile is the price?" Tight spread = stable demand # # Source 3 — Maharashtra agricultural export/consumption index # Proxy: mandi_frequency_score — how many districts regularly trade this crop # Derived from: model2_full_scored.csv (our own regional dataset) # Crops grown in >15 districts = national demand, <5 = niche local only # # Together these produce a COMPUTED demand_score (0-100) per crop per season, # replacing the hardcoded "High/Medium/Low" that has no backing. # ───────────────────────────────────────────────────────────────────────────── # MSP 2024-25 — ₹ per quintal (CCEA, Government of India) MSP = { "wheat": 2275, "rice": 2300, "maize": 2090, "jowar": 3371, "bajra": 2625, "ragi": 4290, "arhar/tur": 7550, "gram": 5440, "lentil": 6425, "mungbean": 8682, "urad": 7400, "blackgram": 7400, "groundnut": 6783, "sunflower": 7280, "soyabean": 4892, "sesamum": 9267, "safflower": 5800, "cotton": 7121, "cotton(lint)": 7121, "rapeseed & mustard": 5950, "sugarcane": 340, } # Source 3 proxy — district reach score (0-3): how widely traded in Maharashtra # Derived from model2_full_scored.csv crop frequency across districts # 3=traded 25-35 districts (staple/national), 2=15-24, 1=5-14, 0=<5 (niche) DISTRICT_REACH = { "rice": 3, "wheat": 3, "soyabean": 3, "cotton": 3, "cotton(lint)": 3, "sugarcane": 3, "jowar": 3, "bajra": 3, "gram": 3, "arhar/tur": 3, "groundnut": 3, "sunflower": 3, "onion": 3, "maize": 3, "tur": 3, "urad": 2, "mungbean": 2, "moong (green gram)": 2, "blackgram": 2, "rapeseed & mustard": 2, "sesamum": 2, "ragi": 2, "safflower": 2, "linseed": 2, "banana": 2, "mango": 2, "orange": 2, "pomegranate": 2, "tomato": 2, "grapes": 2, "chickpea": 2, "pigeonpeas": 2, "lentil": 1, "watermelon": 1, "muskmelon": 1, "papaya": 1, "coconut": 1, "mothbeans": 1, "kidneybeans": 1, "castor seed": 1, "sunflower": 2, "niger seed": 1, "small millets": 1, "other cereals": 1, "other kharif pulses": 1, "other rabi pulses": 1, "other summer pulses": 1, "other oilseeds": 1, "jute": 0, "tobacco": 0, "coffee": 0, "apple": 0, } # Seasonal price premium — crops command higher prices at harvest season end # (based on mandi price trends in Maharashtra, source: AGMARKNET historical data) SEASONAL_PREMIUM = { # crop: {season_when_premium_high: multiplier} "onion": {"Rabi": 1.3, "Kharif": 0.8}, # Rabi onion fetches more "tomato": {"Rabi": 1.25, "Summer": 0.9}, "soyabean": {"Kharif": 1.1}, "cotton": {"Kharif": 1.05}, "wheat": {"Rabi": 1.05}, "gram": {"Rabi": 1.1}, "groundnut": {"Kharif": 1.1}, "sugarcane": {"Whole Year": 1.0}, } def compute_market_score(crop_key: str, season: str, mandi: dict | None) -> dict: """ Returns a defensible market_score (0-100) built from 3 cited sources. Also returns a breakdown dict for transparency to judges / UI. Score components: 40% — MSP coverage (does govt back this crop?) 35% — Price stability (low mandi spread = stable demand) 25% — District reach (how many districts trade it = national vs niche) """ crop_key_lower = crop_key.lower() # ── Component 1: MSP coverage (0-40 pts) ───────────────────────────────── # Crops with MSP have guaranteed buyer (govt procurement) = demand floor msp_val = MSP.get(crop_key_lower) if msp_val: # Scale: high MSP relative to typical input cost = stronger backing meta_cost = 20000 # rough average input cost for normalisation msp_score = min(40, 20 + round((msp_val / 5000) * 8)) # ₹5000/qtl → 28pts else: msp_score = 10 # no MSP = no govt floor, still may have private demand # ── Component 2: Price stability from mandi data (0-35 pts) ───────────── # Tight price spread = many buyers competing = reliable demand # Source: data.gov.in live Mandi API if mandi and mandi.get("min") and mandi.get("max") and mandi.get("modal"): spread_pct = (mandi["max"] - mandi["min"]) / mandi["modal"] * 100 if spread_pct < 15: stability_score = 35 # very stable elif spread_pct < 30: stability_score = 25 elif spread_pct < 50: stability_score = 15 else: stability_score = 5 # highly volatile price_data_source = f"Live mandi data (spread: {round(spread_pct)}%)" else: # No live data — use district reach as fallback proxy reach = DISTRICT_REACH.get(crop_key_lower, 1) stability_score = {3: 25, 2: 18, 1: 12, 0: 5}[reach] price_data_source = "District reach proxy (no live mandi data)" # ── Component 3: District reach — Maharashtra trade breadth (0-25 pts) ── # Source: derived from model2_full_scored.csv crop frequency across districts reach = DISTRICT_REACH.get(crop_key_lower, 1) reach_score = {3: 25, 2: 18, 1: 10, 0: 4}[reach] reach_label = {3: "Traded 25-35 districts", 2: "Traded 15-24 districts", 1: "Traded 5-14 districts", 0: "Niche / <5 districts"}[reach] # ── Seasonal adjustment ─────────────────────────────────────────────────── premium = SEASONAL_PREMIUM.get(crop_key_lower, {}).get(season, 1.0) raw_score = msp_score + stability_score + reach_score final_score = round(min(100, raw_score * premium)) # ── Demand label ────────────────────────────────────────────────────────── if final_score >= 60: demand_label = "High" elif final_score >= 38: demand_label = "Medium" else: demand_label = "Low" # Market risk for risk engine: inverse of score market_risk = round(100 - final_score) return { "demand": demand_label, "demand_score": final_score, "market_risk": market_risk, "msp_val": msp_val, "msp_score": msp_score, "stability_score": stability_score, "reach_score": reach_score, "reach_label": reach_label, "price_data_src": price_data_source, "seasonal_premium": premium, } # MSP 2024-25 — ₹ per quintal (Cabinet Committee on Economic Affairs, GoI) MSP = { "wheat": 2275, "rice": 2300, "maize": 2090, "jowar": 3371, "bajra": 2625, "ragi": 4290, "arhar/tur": 7550, "gram": 5440, "lentil": 6425, "mungbean": 8682, "urad": 7400, "blackgram": 7400, "groundnut": 6783, "sunflower": 7280, "soyabean": 4892, "sesamum": 9267, "safflower": 5800, "cotton": 7121, "cotton(lint)": 7121, "rapeseed & mustard": 5950, "sugarcane": 340, } CROP_TO_MANDI = { "rice": "Rice", "maize": "Maize", "chickpea": "Gram", "kidneybeans": "Rajmash(Kidney Beans)", "pigeonpeas": "Arhar (Tur/Red Gram)(Whole)", "mothbeans": "Moth", "mungbean": "Green Gram (Whole)", "blackgram": "Black Gram (Urd Beans)(Whole)", "lentil": "Lentil", "pomegranate": "Pomegranate", "banana": "Banana", "mango": "Mango", "grapes": "Grapes", "watermelon": "Water Melon", "muskmelon": "Musk Melon", "apple": "Apple", "orange": "Orange", "papaya": "Papaya", "coconut": "Coconut", "cotton": "Cotton", "jute": "Jute", "coffee": "Coffee", "arhar/tur": "Arhar (Tur/Red Gram)(Whole)", "bajra": "Bajra(Pearl Millet/Cumbu)", "castor seed": "Castor Seed", "gram": "Gram", "groundnut": "Groundnut", "jowar": "Jowar(Sorghum)", "linseed": "Linseed", "moong (green gram)": "Green Gram (Whole)", "niger seed": "Niger Seed (Ramtil)", "onion": "Onion", "other cereals": None, "other kharif pulses": None, "other rabi pulses": None, "other summer pulses": None, "ragi": "Ragi (Finger Millet)", "rapeseed & mustard": "Mustard", "safflower": "Safflower", "sesamum": "Sesamum(Sesame,Gingelly,Til)", "small millets": None, "soyabean": "Soyabean", "sugarcane": "Sugarcane", "sunflower": "Sunflower", "tobacco": "Tobacco", "tomato": "Tomato", "urad": "Black Gram (Urd Beans)(Whole)", "wheat": "Wheat", "other oilseeds": None, "cotton(lint)": "Cotton", } DISTRICT_API_MAP = { "Ahilyanagar": "Ahmednagar", "Chhatrapati Sambhajinagar": "Chattrapati Sambhajinagar", "Dharashiv": "Dharashiv(Usmanabad)", "Mumbai suburban": "Mumbai", "Gondia": "Gondiya", "Jalna": "Jalana", "Solapur": "Sholapur", "Washim": "Vashim", "Amravati": "Amarawati", } DISTRICTS = [ "Ahilyanagar","Akola","Amravati","Beed","Bhandara","Buldhana", "Chandrapur","Chhatrapati Sambhajinagar","Dharashiv","Dhule", "Gadchiroli","Gondia","Hingoli","Jalgaon","Jalna","Kolhapur", "Latur","Mumbai suburban","Nagpur","Nanded","Nandurbar","Nashik", "Palghar","Parbhani","Pune","Raigad","Ratnagiri","Sangli","Satara", "Sindhudurg","Solapur","Thane","Wardha","Washim","Yavatmal", ] SEASONS = ["Kharif (Jun–Sep)", "Rabi (Oct–Mar)", "Summer (Apr–Jun)", "Whole Year"] SEASON_MAP = { "Kharif (Jun–Sep)": "Kharif", "Rabi (Oct–Mar)": "Rabi", "Summer (Apr–Jun)": "Summer", "Whole Year": "Whole Year", } # ───────────────────────────────────────────────────────────────────────────── # MANDI API # ───────────────────────────────────────────────────────────────────────────── def fetch_mandi_price(crop_key: str, district: str) -> dict | None: mandi_name = CROP_TO_MANDI.get(crop_key) if not mandi_name: return None api_district = DISTRICT_API_MAP.get(district, district) from datetime import date, timedelta def date_str(d): return d.strftime("%d/%m/%Y") def _call(with_district: bool, arrival_date: str = None) -> list: params = { "api-key": MANDI_API_KEY, "format": "json", "limit": 50, "filters[State]": "Maharashtra", "filters[Commodity]": mandi_name, } if with_district: params["filters[District]"] = api_district if arrival_date: params["filters[Arrival_Date]"] = arrival_date try: resp = requests.get(MANDI_BASE_URL, params=params, timeout=10) recs = resp.json().get("records", []) print(f"[Mandi] {mandi_name} | {'district ' if with_district else ''}date={arrival_date or 'any'} → {len(recs)} records") return recs except Exception as e: print(f"[Mandi error] {e}") return [] def parse_date(r): """Parse arrival date from record, return date object or None.""" raw = r.get("Arrival_Date") or r.get("arrival_date") or r.get("Arrival Date") or "" for fmt in ("%d/%m/%Y", "%Y-%m-%d", "%d-%m-%Y"): try: from datetime import datetime return datetime.strptime(str(raw).strip(), fmt).date() except: pass return None # Step 1: Try last 7 days with district → without district records = [] today = date.today() for days_back in range(0, 7): check_date = date_str(today - timedelta(days=days_back)) records = _call(True, check_date) or _call(False, check_date) if records: print(f"[Mandi] Found data for {check_date}") break # Step 2: If still nothing, fetch without date filter but sort by most recent if not records: records = _call(True) or _call(False) if not records: return None # Sort by arrival date descending — most recent first records.sort(key=lambda r: parse_date(r) or date(2000, 1, 1), reverse=True) # Keep only records within 90 days of the most recent record found most_recent = parse_date(records[0]) if most_recent: cutoff = most_recent - timedelta(days=90) records = [r for r in records if (parse_date(r) or date(2000,1,1)) >= cutoff] print(f"[Mandi] Most recent date: {most_recent}, using {len(records)} records within 90 days") def sf(v): try: return float(str(v).replace(",", "").strip()) except: return None def gf(rec, *keys): """Try multiple possible key names for the same field.""" for k in keys: v = rec.get(k) if v not in (None, "", "0", 0): val = sf(v) if val: return val return None # New API uses "Modal Price" / "Min Price" / "Max Price" (with spaces) # Old API used "Modal_Price" / "Min_Price" / "Max_Price" or "modal_price" etc. # Covers all variants: modals = [v for r in records for v in [gf(r, "Modal Price", "Modal_Price", "modal_price", "Modal_x0020_Price")] if v] mins = [v for r in records for v in [gf(r, "Min Price", "Min_Price", "min_price", "Min_x0020_Price")] if v] maxs = [v for r in records for v in [gf(r, "Max Price", "Max_Price", "max_price", "Max_x0020_Price")] if v] print(f"[Mandi parse] {mandi_name}: modals={modals[:2]}, mins={mins[:2]}, maxs={maxs[:2]}") if not modals: # Debug: print raw keys so we can see what the API actually returns if records: print(f"[Mandi DEBUG] Keys in record: {list(records[0].keys())}") print(f"[Mandi DEBUG] Sample record: {records[0]}") return None # Pick "best" as the record with highest modal price among the most recent date most_recent_date = parse_date(records[0]) if records else None recent_records = [r for r in records if parse_date(r) == most_recent_date] if most_recent_date else records best = max(recent_records, key=lambda r: gf(r, "Modal Price","Modal_Price","modal_price","Modal_x0020_Price") or 0) avg_modal = sum(modals) / len(modals) spread = (max(maxs) - min(mins)) / avg_modal * 100 if mins and maxs else 0 return { "modal": round(avg_modal), "min": round(min(mins)) if mins else None, "max": round(max(maxs)) if maxs else None, "market": best.get("Market") or best.get("market", "—"), "date": (best.get("Arrival Date") or best.get("Arrival_Date") or best.get("arrival_date") or "—"), "name": mandi_name, "spread_pct": round(spread), } # ───────────────────────────────────────────────────────────────────────────── # PREDICTION PIPELINE # ───────────────────────────────────────────────────────────────────────────── def compute_dynamic_risk(crop_key, meta, mandi, irrigation_label, rainfall, temp, humidity, budget_max, total_cost, computed_market_risk=None): """ Returns (risk_score 0-100, risk_label, breakdown_dict). Enhancement 2: fully dynamic risk across 4 dimensions. """ irr_level = IRRIGATION_LEVEL.get(irrigation_label, 1) irr_need = {"low": 0, "medium": 1, "high": 2}.get( CROP_IRRIGATION.get(crop_key, "medium"), 1) # ── 1. Weather risk (0-100) ────────────────────────────────────────────── weather_risk = 15.0 if temp > 42 or temp < 8: weather_risk += 35 elif temp > 38 or temp < 12: weather_risk += 18 if humidity > 92 or humidity < 18: weather_risk += 20 elif humidity > 82 or humidity < 28: weather_risk += 10 # Rainfall vs crop water need if irr_need == 2 and rainfall < 600: weather_risk += 25 # high-water crop, low rain elif irr_need == 0 and rainfall > 2000: weather_risk += 15 # dryland crop, too much rain weather_risk = min(weather_risk, 100) # ── 2. Market risk — uses computed 3-source score when available ──────── if computed_market_risk is not None: market_risk = float(computed_market_risk) # from compute_market_score() else: # Fallback: hardcoded (only used if called without market data) base = {"High": 20, "Medium": 40, "Low": 65}[meta["demand"]] spread_penalty = (min(25, mandi["spread_pct"] * 0.4) if mandi and mandi.get("spread_pct", 0) > 30 else 0) market_risk = min(base + spread_penalty, 100) # ── 3. Budget / financial risk (0-100) ────────────────────────────────── if budget_max <= 0: cost_risk = 50.0 else: ratio = total_cost / budget_max if ratio <= 0.5: cost_risk = 10.0 elif ratio <= 0.8: cost_risk = 28.0 elif ratio <= 1.0: cost_risk = 50.0 elif ratio <= 1.5: cost_risk = 72.0 else: cost_risk = 90.0 # ── 4. Water / irrigation risk (0-100) — NEW ──────────────────────────── irr_gap = irr_need - irr_level # >0 means crop needs more water than available if irr_gap <= 0: water_risk = 10.0 # fully covered elif irr_gap == 1: water_risk = 45.0 # one level short else: water_risk = 80.0 # seriously water-stressed # ── Overall weighted score ─────────────────────────────────────────────── overall = ( weather_risk * 0.20 + market_risk * 0.30 + cost_risk * 0.25 + water_risk * 0.25 ) overall = round(overall, 1) label = "Low" if overall < 35 else ("Medium" if overall < 65 else "High") return overall, label, { "weather": round(weather_risk, 1), "market": round(market_risk, 1), "budget": round(cost_risk, 1), "water": round(water_risk, 1), } def irrigation_verdict(crop_key, irrigation_label): """ Enhancement 1: plain-language irrigation fit message. Returns (icon, message, color) """ irr_level = IRRIGATION_LEVEL.get(irrigation_label, 1) irr_need = {"low": 0, "medium": 1, "high": 2}.get( CROP_IRRIGATION.get(crop_key, "medium"), 1) gap = irr_need - irr_level need_words = {0: "Rainfed (low water)", 1: "Seasonal irrigation", 2: "Assured irrigation"} need_txt = need_words.get(irr_need, "") if gap <= 0: return "💧", f"✅ Your water supply suits this crop ({need_txt} needed)", "#3fb950" elif gap == 1: return "💧", f"⚠️ This crop needs {need_txt} — your supply may be tight", "#d29922" else: return "🚱", f"❌ This crop needs {need_txt} — not enough water available", "#f85149" def predict(N, P, K, temp, humidity, ph, rainfall, district, season_display, land_ha, budget_min, budget_max, top_n, irrigation_label): season = SEASON_MAP[season_display] # Model 1: soil/climate probabilities features = np.array([[N, P, K, temp, humidity, ph, rainfall]]) proba = model1.predict_proba(features)[0] m1_scores = {c.lower(): float(p) for c, p in zip(label_enc.classes_, proba)} # Model 2: regional suitability region_df = model2_df[ (model2_df["District"].str.lower() == district.lower()) & (model2_df["Season"].str.lower() == season.lower()) ].copy() if not region_df.empty and region_df["Suitability_Score"].max() > 0: region_df["norm"] = region_df["Suitability_Score"] / region_df["Suitability_Score"].max() else: region_df["norm"] = 0.0 m2_scores = {row["Crop"].lower(): float(row["norm"]) for _, row in region_df.iterrows()} # Combined score all_crops = set(m1_scores) | set(m2_scores) combined = {c: round(0.6*m1_scores.get(c,0) + 0.4*m2_scores.get(c,0), 4) for c in all_crops} ranked = sorted(combined.items(), key=lambda x: x[1], reverse=True) results = [] for crop_key, score in ranked: if len(results) >= top_n: break meta = CROP_META.get(crop_key) if not meta: continue total_cost = meta["input_cost"] * land_ha # Budget fit if total_cost <= budget_min: budget_status = "well_within" affordable_ha = land_ha elif total_cost <= budget_max: budget_status = "within" affordable_ha = land_ha else: affordable_ha = budget_max / meta["input_cost"] if affordable_ha < 0.1: continue budget_status = "stretch" # Mandi price (Source 2 for market score) mandi = fetch_mandi_price(crop_key, district) modal_price = mandi["modal"] if mandi else None # Computed market score — 3 cited sources, not hardcoded market = compute_market_score(crop_key, season, mandi) # Profit on affordable area if modal_price: revenue = round(modal_price * meta["yield_qtl"] * affordable_ha) cost = round(meta["input_cost"] * affordable_ha) profit = revenue - cost roi = round((profit / cost) * 100) if cost > 0 else 0 else: revenue = profit = roi = None # Season fit season_match = (meta["best_season"].lower() == season.lower() or meta["best_season"] == "Whole Year") # Dynamic risk score — now uses computed market_risk not hardcoded demand risk_score, risk_label, risk_breakdown = compute_dynamic_risk( crop_key, meta, mandi, irrigation_label, rainfall, temp, humidity, budget_max, total_cost, market["market_risk"] ) # Enhancement 1: Irrigation verdict irr_icon, irr_msg, irr_col = irrigation_verdict(crop_key, irrigation_label) # Enhancement 3: Confidence labelling m1_val = m1_scores.get(crop_key, 0) m2_val = m2_scores.get(crop_key, 0) if m1_val >= 0.35: confidence_label = "High confidence" confidence_desc = f"Soil & climate model strongly matches ({round(m1_val*100)}% soil score)" confidence_col = "#3fb950" elif m1_val >= 0.12: confidence_label = "Moderate confidence" confidence_desc = f"Soil model partial match ({round(m1_val*100)}%) — regional history confirms" confidence_col = "#d29922" else: confidence_label = "Based on regional history" confidence_desc = f"Low soil match ({round(m1_val*100)}%) — recommended because local farmers grow it successfully" confidence_col = "#f0883e" # Enhancement 4: MSP comparison msp_val = MSP.get(crop_key) msp_signal = None if msp_val and modal_price: diff = modal_price - msp_val diff_pct = round(abs(diff) / msp_val * 100) if diff >= 0: msp_signal = ("above", diff, diff_pct, "#3fb950", f"₹{modal_price:,}/qtl is ₹{diff:,} ({diff_pct}%) ABOVE MSP of ₹{msp_val:,} — good selling conditions") else: msp_signal = ("below", abs(diff), diff_pct, "#f85149", f"₹{modal_price:,}/qtl is ₹{abs(diff):,} ({diff_pct}%) BELOW MSP of ₹{msp_val:,} — wait for better price or sell at govt centre") elif msp_val: msp_signal = ("no_price", 0, 0, "#7d8590", f"MSP for this crop is ₹{msp_val:,}/qtl — sell at govt procurement centre if mandi price is lower") results.append({ "rank": len(results) + 1, "crop": crop_key.title(), "score": round(score * 100, 1), "m1_score": round(m1_val * 100, 1), "m2_score": round(m2_val * 100, 1), "budget_status": budget_status, "total_cost": round(total_cost), "affordable_ha": round(affordable_ha, 1), "land_ha": land_ha, "input_cost_ha": meta["input_cost"], "yield_qtl_ha": meta["yield_qtl"], "days": meta["days"], "best_season": meta["best_season"], "season_match": season_match, # Market — computed from 3 sources, not hardcoded "demand": market["demand"], "demand_score": market["demand_score"], "msp_val": market["msp_val"], "msp_score": market["msp_score"], "stability_score": market["stability_score"], "reach_score": market["reach_score"], "reach_label": market["reach_label"], "price_data_src": market["price_data_src"], "seasonal_premium": market["seasonal_premium"], "risk": risk_label, "risk_score": risk_score, "risk_breakdown": risk_breakdown, "irr_icon": irr_icon, "irr_msg": irr_msg, "irr_col": irr_col, "confidence_label": confidence_label, "confidence_desc": confidence_desc, "confidence_col": confidence_col, "msp_signal": msp_signal, "mandi": mandi, "modal_price": modal_price, "revenue": revenue, "profit": profit, "roi": roi, }) return results # ───────────────────────────────────────────────────────────────────────────── # HTML — very basic farmer UI: big text, simple words, clear YES/NO # ───────────────────────────────────────────────────────────────────────────── def render_html(results, district, season_display, budget_min, budget_max, land_ha, irrigation_label): if not results: return """
{k}" for k in field_names)}⚠️ Minimum budget must be less than maximum budget.
" try: results = predict(N, P, K, temp, humidity, ph, rainfall, district, season, land_ha, budget_min, budget_max, int(top_n), irrigation) return render_html(results, district, season, budget_min, budget_max, land_ha, irrigation) except Exception as e: import traceback return f"❌ {e}\n{traceback.format_exc()}"
CSS = """
body, .gradio-container { background:#0d1117 !important; color:#e6edf3 !important; }
.gr-panel, .gr-box, .block { background:#161b22 !important; border-color:#30363d !important; }
label { color:#8b949e !important; font-size:0.82rem !important; }
.gr-button-primary {
background:linear-gradient(135deg,#238636,#2ea043) !important;
border:none !important; font-weight:800 !important;
font-size:1.05rem !important; letter-spacing:0.02em !important;
}
footer { display:none !important; }
.gr-markdown h1 { color:#e6edf3 !important; }
.gr-markdown h3 { color:#8b949e !important; font-size:0.78rem !important;
text-transform:uppercase; letter-spacing:0.1em; font-weight:600 !important; }
input[type=number] { font-size:1rem !important; font-weight:600 !important; }
"""
with gr.Blocks(
theme=gr.themes.Base(primary_hue="green", neutral_hue="slate",
font=gr.themes.GoogleFont("DM Sans")),
title="🌾 Maharashtra Crop Advisor",
css=CSS
) as demo:
gr.Markdown("# 🌾 Maharashtra Crop Advisor")
gr.Markdown("Tell us about your farm — we'll show which crops suit you best, what they cost, and how much profit you can make.")
with gr.Row():
# ── LEFT PANEL: Inputs ──────────────────────────────────────────────
with gr.Column(scale=1, min_width=300):
gr.Markdown("### 📍 Your Location")
dist_in = gr.Dropdown(DISTRICTS, value="Pune", label="Select your District")
seas_in = gr.Dropdown(SEASONS, value="Kharif (Jun–Sep)", label="Which season are you planning for?")
gr.Markdown("### 🌱 Your Farm")
land_in = gr.Number(value=1.0, label="How much land do you have? (in Hectares)", minimum=0.1, maximum=100)
gr.Markdown("### 💧 Water / Irrigation")
irr_in = gr.Radio(
choices=list(IRRIGATION_LEVEL.keys()),
value="Seasonal / Rain-fed",
label="What is your water source?",
)
gr.Markdown("### 💰 Your Budget Range")
gr.Markdown("Set the minimum and maximum amount you can spend")
bmin_in = gr.Number(value=20000, label="Minimum Budget (₹) — I can definitely spend this", minimum=1000)
bmax_in = gr.Number(value=60000, label="Maximum Budget (₹) — I can stretch up to this", minimum=1000)
gr.Markdown("### 🧪 Soil Test Results")
gr.Markdown("Available from your nearest Krishi Vigyan Kendra or soil lab")
N_in = gr.Slider(0, 300, value=90, step=1, label="Nitrogen — N (mg/kg)")
P_in = gr.Slider(0, 300, value=42, step=1, label="Phosphorus — P (mg/kg)")
K_in = gr.Slider(0, 300, value=43, step=1, label="Potassium — K (mg/kg)")
ph_in = gr.Slider(3.5, 9.5, value=6.5, step=0.1, label="Soil pH")
gr.Markdown("### 🌦 Local Weather")
tmp_in = gr.Slider(5, 50, value=28.0, step=0.5, label="Average Temperature (°C)")
hum_in = gr.Slider(10, 100, value=75.0, step=1, label="Average Humidity (%)")
rain_in = gr.Slider(20, 3000, value=800, step=10, label="Annual Rainfall (mm)")
gr.Markdown("### ⚙️ Show top")
topn_in = gr.Slider(3, 10, value=5, step=1, label="Number of crop recommendations")
btn = gr.Button("🚀 Find Best Crops For Me", variant="primary", size="lg")
# ── RIGHT PANEL: Output ─────────────────────────────────────────────
with gr.Column(scale=2):
output = gr.HTML(
value="""