sammeeer's picture
genai inclusion
90bfc25
# pages/spatial.py β€” Spatial Overview Map
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
import streamlit as st
import plotly.graph_objects as go
import pandas as pd
import numpy as np
from theme import inject_theme, page_header, section_label, kpi_html, PLOTLY_LAYOUT, SAFFRON, SAFFRON_SCALE, GREEN, RED, AMBER
from utils.api_client import fetch_states, fetch_predictions, fetch_optimizer_results, fetch_district_history
inject_theme()
page_header(
"β—ˆ Module 05",
"Spatial Overview",
"District-level employment prediction map β€” hover any bubble for full model details",
)
# ── District coordinates (approximate centroids for all major districts) ──────
# Covers all 36 states/UTs across India's 700+ districts.
# Format: "District|State": (lat, lon)
DISTRICT_COORDS: dict[str, tuple[float, float]] = {
# ── Andhra Pradesh ─────────────────────────────────────────────────────────
"Srikakulam|Andhra Pradesh": (18.30, 83.90), "Vizianagaram|Andhra Pradesh": (18.12, 83.41),
"Visakhapatnam|Andhra Pradesh": (17.69, 83.22), "East Godavari|Andhra Pradesh":(17.00, 82.00),
"West Godavari|Andhra Pradesh": (16.92, 81.34), "Krishna|Andhra Pradesh": (16.61, 80.83),
"Guntur|Andhra Pradesh": (16.31, 80.44), "Prakasam|Andhra Pradesh": (15.35, 79.57),
"Nellore|Andhra Pradesh": (14.44, 79.99), "Kurnool|Andhra Pradesh": (15.83, 78.05),
"Kadapa|Andhra Pradesh": (14.47, 78.82), "Anantapur|Andhra Pradesh": (14.68, 77.60),
"Chittoor|Andhra Pradesh": (13.22, 79.10),
# ── Assam ─────────────────────────────────────────────────────────────────
"Kamrup|Assam": (26.14, 91.77), "Barpeta|Assam": (26.32, 91.00),
"Dhubri|Assam": (26.02, 89.98), "Goalpara|Assam": (26.17, 90.62),
"Nagaon|Assam": (26.35, 92.68), "Cachar|Assam": (24.81, 92.86),
"Lakhimpur|Assam": (27.24, 94.10), "Dibrugarh|Assam": (27.49, 95.00),
"Sonitpur|Assam": (26.63, 92.80), "Jorhat|Assam": (26.75, 94.22),
# ── Bihar ─────────────────────────────────────────────────────────────────
"Patna|Bihar": (25.59, 85.13), "Gaya|Bihar": (24.80, 84.99),
"Muzaffarpur|Bihar": (26.12, 85.38), "Bhagalpur|Bihar": (25.24, 86.98),
"Darbhanga|Bihar": (26.16, 85.90), "Purnea|Bihar": (25.78, 87.47),
"Rohtas|Bihar": (24.98, 83.98), "Siwan|Bihar": (26.22, 84.36),
"Saran|Bihar": (25.92, 84.74), "Nalanda|Bihar": (25.10, 85.44),
"Madhubani|Bihar": (26.37, 86.07), "Champaran East|Bihar": (26.65, 84.92),
"Champaran West|Bihar": (27.02, 84.46),
# ── Chhattisgarh ──────────────────────────────────────────────────────────
"Raipur|Chhattisgarh": (21.25, 81.63), "Bilaspur|Chhattisgarh": (22.09, 82.15),
"Durg|Chhattisgarh": (21.19, 81.28), "Rajnandgaon|Chhattisgarh": (21.10, 81.03),
"Bastar|Chhattisgarh": (19.10, 81.95), "Sarguja|Chhattisgarh": (23.12, 83.19),
"Korba|Chhattisgarh": (22.35, 82.72), "Raigarh|Chhattisgarh": (21.90, 83.40),
# ── Gujarat ───────────────────────────────────────────────────────────────
"Ahmedabad|Gujarat": (23.03, 72.58), "Surat|Gujarat": (21.17, 72.83),
"Vadodara|Gujarat": (22.31, 73.18), "Rajkot|Gujarat": (22.30, 70.80),
"Bhavnagar|Gujarat": (21.77, 72.15), "Jamnagar|Gujarat": (22.47, 70.06),
"Junagadh|Gujarat": (21.52, 70.46), "Anand|Gujarat": (22.56, 72.93),
"Mehsana|Gujarat": (23.59, 72.37), "Banaskantha|Gujarat": (24.17, 72.42),
"Kutch|Gujarat": (23.73, 69.86), "Dahod|Gujarat": (22.83, 74.25),
"Narmada|Gujarat": (21.87, 73.49), "Valsad|Gujarat": (20.59, 72.93),
"Dang|Gujarat": (20.75, 73.69),
# ── Haryana ───────────────────────────────────────────────────────────────
"Hisar|Haryana": (29.15, 75.72), "Sirsa|Haryana": (29.53, 75.03),
"Bhiwani|Haryana": (28.79, 76.13), "Rohtak|Haryana": (28.89, 76.61),
"Sonipat|Haryana": (28.99, 77.01), "Karnal|Haryana": (29.68, 76.99),
"Ambala|Haryana": (30.37, 76.78), "Kurukshetra|Haryana": (29.97, 76.85),
"Mahendragarh|Haryana": (28.27, 76.15),
# ── Jharkhand ─────────────────────────────────────────────────────────────
"Ranchi|Jharkhand": (23.35, 85.33), "Dhanbad|Jharkhand": (23.80, 86.45),
"Bokaro|Jharkhand": (23.67, 86.15), "Giridih|Jharkhand": (24.19, 86.30),
"Hazaribagh|Jharkhand": (23.99, 85.36), "Dumka|Jharkhand": (24.27, 87.25),
"Palamu|Jharkhand": (24.03, 84.08), "Gumla|Jharkhand": (23.05, 84.54),
"Pakur|Jharkhand": (24.63, 87.84), "Lohardaga|Jharkhand": (23.44, 84.68),
# ── Karnataka ─────────────────────────────────────────────────────────────
"Bangalore Rural|Karnataka": (13.01, 77.57), "Tumkur|Karnataka": (13.34, 77.10),
"Kolar|Karnataka": (13.14, 78.13), "Mysore|Karnataka": (12.30, 76.65),
"Mandya|Karnataka": (12.52, 76.90), "Hassan|Karnataka": (13.00, 76.10),
"Chikmagalur|Karnataka": (13.32, 75.78), "Shimoga|Karnataka": (13.93, 75.57),
"Dakshina Kannada|Karnataka": (12.85, 75.24), "Uttara Kannada|Karnataka": (14.79, 74.68),
"Raichur|Karnataka": (16.21, 77.36), "Koppal|Karnataka": (15.35, 76.15),
"Gadag|Karnataka": (15.42, 75.62), "Dharwad|Karnataka": (15.46, 75.01),
"Bagalkot|Karnataka": (16.18, 75.70), "Bijapur|Karnataka": (16.83, 75.72),
"Gulbarga|Karnataka": (17.34, 76.82), "Bidar|Karnataka": (17.91, 77.52),
"Bellary|Karnataka": (15.14, 76.92), "Chitradurga|Karnataka": (14.23, 76.40),
"Davangere|Karnataka": (14.46, 75.92), "Udupi|Karnataka": (13.34, 74.75),
# ── Kerala ────────────────────────────────────────────────────────────────
"Thiruvananthapuram|Kerala": (8.52, 76.94), "Kollam|Kerala": (8.88, 76.61),
"Pathanamthitta|Kerala": (9.27, 76.77), "Alappuzha|Kerala": (9.49, 76.32),
"Kottayam|Kerala": (9.59, 76.52), "Idukki|Kerala": (9.85, 77.10),
"Ernakulam|Kerala": (10.01, 76.31), "Thrissur|Kerala": (10.52, 76.22),
"Palakkad|Kerala": (10.77, 76.65), "Malappuram|Kerala": (11.07, 76.07),
"Kozhikode|Kerala": (11.25, 75.78), "Wayanad|Kerala": (11.61, 76.08),
"Kannur|Kerala": (11.87, 75.37), "Kasaragod|Kerala": (12.50, 74.99),
# ── Madhya Pradesh ────────────────────────────────────────────────────────
"Bhopal|Madhya Pradesh": (23.26, 77.41), "Indore|Madhya Pradesh": (22.72, 75.86),
"Jabalpur|Madhya Pradesh": (23.18, 79.99), "Gwalior|Madhya Pradesh": (26.22, 78.18),
"Sagar|Madhya Pradesh": (23.84, 78.74), "Rewa|Madhya Pradesh": (24.53, 81.30),
"Satna|Madhya Pradesh": (24.60, 80.83), "Ujjain|Madhya Pradesh": (23.18, 75.78),
"Chhindwara|Madhya Pradesh": (22.06, 78.94), "Shivpuri|Madhya Pradesh": (25.42, 77.66),
"Morena|Madhya Pradesh": (26.50, 78.00), "Bhind|Madhya Pradesh": (26.56, 78.78),
"Datia|Madhya Pradesh": (25.67, 78.46), "Chhatarpur|Madhya Pradesh": (24.92, 79.58),
"Tikamgarh|Madhya Pradesh": (24.74, 78.83), "Raisen|Madhya Pradesh": (22.99, 77.79),
"Vidisha|Madhya Pradesh": (23.52, 77.81), "Hoshangabad|Madhya Pradesh": (22.75, 77.73),
"Harda|Madhya Pradesh": (22.34, 77.09), "Betul|Madhya Pradesh": (21.91, 77.90),
"Balaghat|Madhya Pradesh": (21.81, 80.19), "Seoni|Madhya Pradesh": (22.09, 79.55),
"Mandla|Madhya Pradesh": (22.60, 80.38), "Dindori|Madhya Pradesh": (22.95, 81.08),
"Shahdol|Madhya Pradesh": (23.30, 81.36), "Anuppur|Madhya Pradesh": (23.10, 81.69),
"Umaria|Madhya Pradesh": (23.53, 80.84), "Katni|Madhya Pradesh": (23.83, 80.39),
"Panna|Madhya Pradesh": (24.72, 80.19), "Damoh|Madhya Pradesh": (23.83, 79.45),
"Narsinghpur|Madhya Pradesh": (22.95, 79.19), "Niwari|Madhya Pradesh": (25.01, 78.76),
# ── Maharashtra ───────────────────────────────────────────────────────────
"Ahmednagar|Maharashtra": (19.10, 74.74), "Akola|Maharashtra": (20.71, 77.00),
"Amravati|Maharashtra": (20.93, 77.75), "Aurangabad|Maharashtra": (19.88, 75.34),
"Beed|Maharashtra": (18.99, 75.75), "Bhandara|Maharashtra": (21.17, 79.65),
"Buldhana|Maharashtra": (20.53, 76.18), "Chandrapur|Maharashtra": (19.96, 79.30),
"Dhule|Maharashtra": (20.90, 74.78), "Gadchiroli|Maharashtra": (20.18, 80.00),
"Gondia|Maharashtra": (21.46, 80.20), "Hingoli|Maharashtra": (19.72, 77.15),
"Jalgaon|Maharashtra": (21.00, 75.57), "Jalna|Maharashtra": (19.84, 75.89),
"Kolhapur|Maharashtra": (16.70, 74.24), "Latur|Maharashtra": (18.40, 76.57),
"Mumbai City|Maharashtra": (18.96, 72.82), "Mumbai Suburban|Maharashtra": (19.17, 72.96),
"Nagpur|Maharashtra": (21.15, 79.09), "Nanded|Maharashtra": (19.15, 77.32),
"Nandurbar|Maharashtra": (21.37, 74.24), "Nashik|Maharashtra": (19.99, 73.79),
"Osmanabad|Maharashtra": (18.18, 76.04), "Palghar|Maharashtra": (19.70, 72.77),
"Parbhani|Maharashtra": (19.27, 76.77), "Pune|Maharashtra": (18.52, 73.86),
"Raigad|Maharashtra": (18.52, 73.18), "Ratnagiri|Maharashtra": (16.99, 73.30),
"Sangli|Maharashtra": (16.86, 74.56), "Satara|Maharashtra": (17.69, 74.00),
"Sindhudurg|Maharashtra": (16.35, 73.74), "Solapur|Maharashtra": (17.69, 75.91),
"Thane|Maharashtra": (19.22, 72.98), "Wardha|Maharashtra": (20.75, 78.60),
"Washim|Maharashtra": (20.11, 77.15), "Yavatmal|Maharashtra": (20.39, 78.13),
# ── Odisha ────────────────────────────────────────────────────────────────
"Bhubaneswar|Odisha": (20.30, 85.84), "Cuttack|Odisha": (20.46, 85.88),
"Balasore|Odisha": (21.49, 86.93), "Mayurbhanj|Odisha": (21.92, 86.73),
"Keonjhar|Odisha": (21.63, 85.58), "Sundargarh|Odisha": (22.12, 84.03),
"Sambalpur|Odisha": (21.47, 83.97), "Bargarh|Odisha": (21.33, 83.62),
"Bolangir|Odisha": (20.71, 83.49), "Kalahandi|Odisha": (19.91, 83.17),
"Koraput|Odisha": (18.81, 82.71), "Rayagada|Odisha": (19.17, 83.41),
"Ganjam|Odisha": (19.39, 84.70), "Puri|Odisha": (19.81, 85.83),
"Khordha|Odisha": (20.18, 85.62), "Jagatsinghpur|Odisha": (20.25, 86.18),
"Kendrapara|Odisha": (20.50, 86.42), "Jajpur|Odisha": (20.85, 86.33),
# ── Rajasthan ─────────────────────────────────────────────────────────────
"Jaipur|Rajasthan": (26.92, 75.79), "Jodhpur|Rajasthan": (26.29, 73.03),
"Udaipur|Rajasthan": (24.58, 73.69), "Kota|Rajasthan": (25.18, 75.84),
"Ajmer|Rajasthan": (26.45, 74.64), "Bikaner|Rajasthan": (28.02, 73.31),
"Alwar|Rajasthan": (27.57, 76.61), "Bharatpur|Rajasthan": (27.22, 77.49),
"Sikar|Rajasthan": (27.61, 75.14), "Nagaur|Rajasthan": (27.21, 73.74),
"Pali|Rajasthan": (25.77, 73.33), "Barmer|Rajasthan": (25.75, 71.39),
"Jaisalmer|Rajasthan": (26.92, 70.91), "Churu|Rajasthan": (28.30, 74.96),
"Jhunjhunu|Rajasthan": (28.13, 75.40), "Sirohi|Rajasthan": (24.89, 72.86),
"Banswara|Rajasthan": (23.54, 74.44), "Dungarpur|Rajasthan": (23.84, 73.71),
"Baran|Rajasthan": (25.10, 76.52), "Jhalawar|Rajasthan": (24.60, 76.16),
"Tonk|Rajasthan": (26.17, 75.79), "Sawai Madhopur|Rajasthan": (26.01, 76.35),
"Dausa|Rajasthan": (26.89, 76.34), "Karauli|Rajasthan": (26.50, 77.02),
# ── Tamil Nadu ────────────────────────────────────────────────────────────
"Chennai|Tamil Nadu": (13.08, 80.27), "Coimbatore|Tamil Nadu": (11.02, 76.97),
"Madurai|Tamil Nadu": (9.93, 78.12), "Tiruchirappalli|Tamil Nadu": (10.80, 78.69),
"Salem|Tamil Nadu": (11.65, 78.16), "Tirunelveli|Tamil Nadu": (8.73, 77.70),
"Vellore|Tamil Nadu": (12.92, 79.13), "Erode|Tamil Nadu": (11.34, 77.73),
"Thanjavur|Tamil Nadu": (10.79, 79.14), "Virudhunagar|Tamil Nadu": (9.58, 77.96),
"Ramanathapuram|Tamil Nadu": (9.37, 78.83), "Pudukkottai|Tamil Nadu": (10.38, 78.82),
"Dindigul|Tamil Nadu": (10.36, 77.98), "Dharmapuri|Tamil Nadu": (12.13, 78.16),
"Krishnagiri|Tamil Nadu": (12.52, 78.21), "Namakkal|Tamil Nadu": (11.22, 78.17),
"Nilgiris|Tamil Nadu": (11.47, 76.73), "Tiruppur|Tamil Nadu": (11.11, 77.34),
"Cuddalore|Tamil Nadu": (11.75, 79.77), "Villupuram|Tamil Nadu": (11.94, 79.49),
"Kancheepuram|Tamil Nadu": (12.83, 79.70), "Thiruvallur|Tamil Nadu": (13.15, 79.91),
"Tiruvannamalai|Tamil Nadu": (12.23, 79.07),
# ── Telangana ─────────────────────────────────────────────────────────────
"Hyderabad|Telangana": (17.38, 78.47), "Medchal|Telangana": (17.62, 78.48),
"Rangareddy|Telangana": (17.25, 78.38), "Nalgonda|Telangana": (17.05, 79.27),
"Warangal|Telangana": (17.97, 79.59), "Karimnagar|Telangana": (18.44, 79.13),
"Khammam|Telangana": (17.25, 80.15), "Nizamabad|Telangana": (18.67, 78.10),
"Adilabad|Telangana": (19.67, 78.53), "Mahabubnagar|Telangana": (16.74, 77.99),
# ── Uttar Pradesh ─────────────────────────────────────────────────────────
"Lucknow|Uttar Pradesh": (26.85, 80.95), "Kanpur Nagar|Uttar Pradesh": (26.45, 80.35),
"Agra|Uttar Pradesh": (27.18, 78.02), "Varanasi|Uttar Pradesh": (25.32, 83.01),
"Allahabad|Uttar Pradesh": (25.44, 81.85), "Meerut|Uttar Pradesh": (28.98, 77.71),
"Bareilly|Uttar Pradesh": (28.35, 79.43), "Gorakhpur|Uttar Pradesh": (26.76, 83.37),
"Mathura|Uttar Pradesh": (27.49, 77.67), "Muzaffarnagar|Uttar Pradesh": (29.47, 77.70),
"Shahjahanpur|Uttar Pradesh": (27.88, 79.91), "Sitapur|Uttar Pradesh": (27.57, 80.68),
"Lakhimpur Kheri|Uttar Pradesh": (27.94, 80.78), "Hardoi|Uttar Pradesh": (27.40, 80.13),
"Unnao|Uttar Pradesh": (26.54, 80.49), "Rae Bareli|Uttar Pradesh": (26.22, 81.24),
"Pratapgarh|Uttar Pradesh": (25.89, 81.99), "Jaunpur|Uttar Pradesh": (25.73, 82.69),
"Ghazipur|Uttar Pradesh": (25.58, 83.57), "Ballia|Uttar Pradesh": (25.75, 84.15),
"Azamgarh|Uttar Pradesh": (26.07, 83.18), "Mau|Uttar Pradesh": (25.94, 83.56),
"Deoria|Uttar Pradesh": (26.50, 83.78), "Basti|Uttar Pradesh": (26.79, 82.73),
"Siddharthnagar|Uttar Pradesh": (27.29, 83.07), "Maharajganj|Uttar Pradesh": (27.15, 83.56),
"Gonda|Uttar Pradesh": (27.13, 81.97), "Bahraich|Uttar Pradesh": (27.57, 81.60),
"Shravasti|Uttar Pradesh": (27.72, 81.87), "Balrampur|Uttar Pradesh": (27.43, 82.19),
"Barabanki|Uttar Pradesh": (26.94, 81.19), "Faizabad|Uttar Pradesh": (26.77, 82.14),
"Ambedkar Nagar|Uttar Pradesh": (26.43, 82.62), "Sultanpur|Uttar Pradesh": (26.26, 82.06),
"Banda|Uttar Pradesh": (25.48, 80.34), "Chitrakoot|Uttar Pradesh": (25.20, 80.90),
"Hamirpur|Uttar Pradesh": (25.95, 80.15), "Mahoba|Uttar Pradesh": (25.29, 79.87),
"Lalitpur|Uttar Pradesh": (24.69, 78.41), "Jhansi|Uttar Pradesh": (25.45, 78.57),
"Jalaun|Uttar Pradesh": (26.14, 79.34), "Etawah|Uttar Pradesh": (26.78, 79.02),
"Auraiya|Uttar Pradesh": (26.47, 79.51), "Kannauj|Uttar Pradesh": (27.05, 79.92),
"Farrukhabad|Uttar Pradesh": (27.38, 79.57), "Mainpuri|Uttar Pradesh": (27.23, 79.02),
"Firozabad|Uttar Pradesh": (27.15, 78.39), "Etah|Uttar Pradesh": (27.65, 78.67),
"Kasganj|Uttar Pradesh": (27.81, 78.65), "Hathras|Uttar Pradesh": (27.60, 78.06),
"Aligarh|Uttar Pradesh": (27.88, 78.07), "Bulandshahr|Uttar Pradesh": (28.41, 77.85),
"Hapur|Uttar Pradesh": (28.72, 77.78), "Gautam Buddha Nagar|Uttar Pradesh": (28.54, 77.39),
"Ghaziabad|Uttar Pradesh": (28.67, 77.44), "Bagpat|Uttar Pradesh": (28.94, 77.22),
"Bijnor|Uttar Pradesh": (29.37, 78.13), "Amroha|Uttar Pradesh": (28.91, 78.47),
"Sambhal|Uttar Pradesh": (28.59, 78.56), "Moradabad|Uttar Pradesh": (28.84, 78.77),
"Rampur|Uttar Pradesh": (28.81, 79.03), "Pilibhit|Uttar Pradesh": (28.64, 79.81),
"Budaun|Uttar Pradesh": (28.04, 79.13),
# ── West Bengal ───────────────────────────────────────────────────────────
"Kolkata|West Bengal": (22.57, 88.37), "Howrah|West Bengal": (22.59, 88.31),
"North 24 Parganas|West Bengal": (22.86, 88.54), "South 24 Parganas|West Bengal":(22.15, 88.27),
"Bardhaman|West Bengal": (23.23, 87.86), "Birbhum|West Bengal": (23.90, 87.53),
"Murshidabad|West Bengal": (24.18, 88.27), "Nadia|West Bengal": (23.47, 88.55),
"Hooghly|West Bengal": (22.96, 88.38), "Midnapore West|West Bengal": (22.43, 86.92),
"Midnapore East|West Bengal": (22.11, 87.67), "Bankura|West Bengal": (23.23, 87.07),
"Purulia|West Bengal": (23.33, 86.36), "Malda|West Bengal": (25.00, 88.14),
"Dinajpur North|West Bengal": (25.62, 88.43), "Dinajpur South|West Bengal": (25.29, 88.68),
"Jalpaiguri|West Bengal": (26.54, 88.73), "Darjeeling|West Bengal": (27.04, 88.26),
"Cooch Behar|West Bengal": (26.32, 89.45),
# ── Himachal Pradesh ──────────────────────────────────────────────────────
"Shimla|Himachal Pradesh": (31.10, 77.17), "Kangra|Himachal Pradesh": (32.10, 76.27),
"Mandi|Himachal Pradesh": (31.71, 76.93), "Hamirpur|Himachal Pradesh": (31.69, 76.52),
"Una|Himachal Pradesh": (31.46, 76.27), "Chamba|Himachal Pradesh": (32.55, 76.13),
"Solan|Himachal Pradesh": (30.91, 77.10), "Sirmaur|Himachal Pradesh": (30.56, 77.46),
"Bilaspur|Himachal Pradesh": (31.34, 76.76), "Kinnaur|Himachal Pradesh": (31.59, 78.45),
"Kullu|Himachal Pradesh": (31.96, 77.11), "Lahul Spiti|Himachal Pradesh":(32.77, 77.67),
# ── Uttarakhand ───────────────────────────────────────────────────────────
"Dehradun|Uttarakhand": (30.32, 78.03), "Haridwar|Uttarakhand": (29.96, 78.16),
"Nainital|Uttarakhand": (29.38, 79.46), "Udham Singh Nagar|Uttarakhand":(29.00, 79.52),
"Almora|Uttarakhand": (29.60, 79.66), "Pauri Garhwal|Uttarakhand": (29.78, 79.01),
"Tehri Garhwal|Uttarakhand": (30.39, 78.48), "Chamoli|Uttarakhand": (30.41, 79.32),
"Rudraprayag|Uttarakhand": (30.28, 78.98), "Uttarkashi|Uttarakhand": (30.73, 78.44),
"Bageshwar|Uttarakhand": (29.84, 79.77), "Pithoragarh|Uttarakhand": (29.58, 80.22),
"Champawat|Uttarakhand": (29.33, 80.09),
# ── Punjab ────────────────────────────────────────────────────────────────
"Amritsar|Punjab": (31.63, 74.87), "Ludhiana|Punjab": (30.90, 75.85),
"Jalandhar|Punjab": (31.33, 75.58), "Patiala|Punjab": (30.34, 76.39),
"Bathinda|Punjab": (30.21, 74.95), "Gurdaspur|Punjab": (32.04, 75.41),
"Firozpur|Punjab": (30.93, 74.61), "Hoshiarpur|Punjab": (31.53, 75.91),
"Rupnagar|Punjab": (30.96, 76.53), "Sangrur|Punjab": (30.25, 75.84),
"Moga|Punjab": (30.82, 75.17), "Faridkot|Punjab": (30.67, 74.76),
"Muktsar|Punjab": (30.48, 74.52), "Fazilka|Punjab": (30.40, 74.02),
"Nawanshahr|Punjab": (31.12, 76.12), "Kapurthala|Punjab": (31.38, 75.38),
# ── Jharkhand extra ───────────────────────────────────────────────────────
"Chatra|Jharkhand": (24.21, 84.88), "Koderma|Jharkhand": (24.47, 85.60),
"Simdega|Jharkhand": (22.61, 84.51), "Khunti|Jharkhand": (23.07, 85.28),
"Ramgarh|Jharkhand": (23.63, 85.51), "Jamtara|Jharkhand": (23.96, 86.80),
"Sahibganj|Jharkhand": (24.96, 87.63), "Godda|Jharkhand": (24.83, 87.21),
"Deoghar|Jharkhand": (24.48, 86.70),
# ── Generic fallback centroids for states ─────────────────────────────────
"Unknown|Andhra Pradesh": (15.9, 79.7),
"Unknown|Assam": (26.2, 92.9),
"Unknown|Bihar": (25.1, 85.3),
"Unknown|Chhattisgarh": (21.3, 81.7),
"Unknown|Gujarat": (22.3, 71.2),
"Unknown|Haryana": (29.1, 76.1),
"Unknown|Jharkhand": (23.6, 85.3),
"Unknown|Karnataka": (15.3, 75.7),
"Unknown|Kerala": (10.9, 76.3),
"Unknown|Madhya Pradesh": (22.9, 78.7),
"Unknown|Maharashtra": (19.7, 75.7),
"Unknown|Odisha": (20.9, 85.1),
"Unknown|Rajasthan": (27.0, 74.2),
"Unknown|Tamil Nadu": (11.1, 78.7),
"Unknown|Telangana": (17.4, 79.1),
"Unknown|Uttar Pradesh": (26.8, 80.9),
"Unknown|West Bengal": (22.9, 87.9),
}
def get_coords(district: str, state: str) -> tuple[float, float]:
"""Return (lat, lon) for a district, with fallback to state centroid."""
rng = np.random.default_rng(abs(hash(f"{district}{state}")) % (2**31))
key = f"{district}|{state}"
if key in DISTRICT_COORDS:
lat, lon = DISTRICT_COORDS[key]
lat += rng.uniform(-0.08, 0.08)
lon += rng.uniform(-0.08, 0.08)
return lat, lon
# Fallback: state centroid + jitter
fb_key = f"Unknown|{state}"
lat, lon = DISTRICT_COORDS.get(fb_key, (22.0, 78.0))
lat += rng.uniform(-1.2, 1.2)
lon += rng.uniform(-1.2, 1.2)
return lat, lon
# ── Controls ──────────────────────────────────────────────────────────────────
states = fetch_states()
if not states:
st.error("⚠️ API offline β€” run `uvicorn backend.main:app --port 8000`")
st.stop()
cc1, cc2, cc3 = st.columns(3)
with cc1:
state_filter = st.selectbox("State Filter", ["All India"] + states)
with cc2:
map_metric = st.selectbox("Bubble Color / Size", [
"Predicted Person-Days",
"Prediction Error",
"Budget Gain (LP Optimizer)",
"Actual Person-Days",
])
with cc3:
year_opts = []
_df_raw = fetch_predictions()
if not _df_raw.empty:
year_opts = sorted(_df_raw["financial_year"].unique().tolist())
selected_year = st.selectbox("Financial Year", year_opts if year_opts else ["β€”"])
# ── Fetch & merge data ────────────────────────────────────────────────────────
pred_df = fetch_predictions(
state=None if state_filter == "All India" else state_filter,
year=int(selected_year) if selected_year != "β€”" else None,
)
opt_df = fetch_optimizer_results(
state=None if state_filter == "All India" else state_filter,
)
if pred_df.empty:
st.info("No prediction data for selected filters. Ensure the pipeline has run.")
st.stop()
# Merge optimizer results in if available
if not opt_df.empty:
merge_cols = ["state", "district"]
opt_sub = opt_df[merge_cols + [
c for c in ["persondays_gain", "budget_change_pct", "persondays_per_lakh",
"budget_allocated_lakhs", "optimized_budget"]
if c in opt_df.columns
]].drop_duplicates(subset=merge_cols)
pred_df = pred_df.merge(opt_sub, on=merge_cols, how="left")
# Pick what to color by
COLOR_MAP = {
"Predicted Person-Days": "predicted_persondays",
"Prediction Error": "prediction_error",
"Budget Gain (LP Optimizer)": "persondays_gain",
"Actual Person-Days": "person_days_lakhs",
}
color_col = COLOR_MAP[map_metric]
if color_col not in pred_df.columns:
color_col = "predicted_persondays"
# ── Build map data ────────────────────────────────────────────────────────────
lats, lons, colors, sizes = [], [], [], []
hover_data = []
for _, row in pred_df.iterrows():
lat, lon = get_coords(str(row["district"]), str(row["state"]))
lats.append(lat)
lons.append(lon)
colors.append(float(row.get(color_col, 0) or 0))
sizes.append(max(float(row.get("predicted_persondays", 1) or 1), 0.1))
hover_data.append(row)
# Normalize sizes for bubble radius
sz_arr = np.array(sizes)
sz_min, sz_max = sz_arr.min(), sz_arr.max()
norm_sz = np.clip((sz_arr - sz_min) / (sz_max - sz_min + 1e-9) * 13 + 4, 4, 17).tolist()
# ── Choose colorscale based on metric ────────────────────────────────────────
if color_col == "prediction_error":
cscale = [[0, RED], [0.5, "#FED7AA"], [1, "#FED7AA"]]
cscale = [[0, RED], [0.5, "#FAFAF9"], [1, GREEN]]
elif color_col == "persondays_gain":
cscale = [[0, RED], [0.5, "#FFF7ED"], [1, GREEN]]
else:
cscale = SAFFRON_SCALE
# ── Build hover template ──────────────────────────────────────────────────────
# customdata columns: 0=district, 1=state, 2=fy, 3=actual, 4=predicted,
# 5=error, 6=persondays_gain, 7=budget_chg_pct,
# 8=persondays_per_lakh, 9=budget_allocated
custom = []
for row in hover_data:
custom.append([
str(row.get("district", "")),
str(row.get("state", "")),
int(row.get("financial_year", 0)),
float(row.get("person_days_lakhs", 0) or 0),
float(row.get("predicted_persondays", 0) or 0),
float(row.get("prediction_error", 0) or 0),
float(row.get("persondays_gain", 0) or 0),
float(row.get("budget_change_pct", 0) or 0),
float(row.get("persondays_per_lakh", 0) or 0),
float(row.get("budget_allocated_lakhs", 0) or 0),
])
hover_tmpl = (
"<b>%{customdata[0]}</b><br>"
"<span style='color:#A8A29E'>%{customdata[1]}</span><br>"
"<br>"
"<b>FY:</b> %{customdata[2]}<br>"
"<b>Actual PD:</b> %{customdata[3]:.2f}L<br>"
"<b>Predicted PD:</b> %{customdata[4]:.2f}L<br>"
"<b>Model Error:</b> %{customdata[5]:+.2f}L<br>"
"<br>"
"<b>LP Optimizer</b><br>"
"<b>PD Gain:</b> %{customdata[6]:+.2f}L<br>"
"<b>Budget Ξ”:</b> %{customdata[7]:+.1f}%<br>"
"<b>Efficiency:</b> %{customdata[8]:.4f} PD/β‚ΉL<br>"
"<b>Budget:</b> β‚Ή%{customdata[9]:,.0f}L"
"<extra></extra>"
)
fig = go.Figure()
fig.add_scattergeo(
lat=lats, lon=lons,
mode="markers",
marker=dict(
size=norm_sz,
color=colors,
colorscale=cscale,
colorbar=dict(
title=dict(text=map_metric[:12], font=dict(color="#78716C", size=9)),
tickfont=dict(color="#78716C", size=8),
thickness=10, len=0.55,
bgcolor="rgba(255,255,255,0.88)",
),
opacity=0.80,
line=dict(width=0.8, color="rgba(255,255,255,0.7)"),
),
customdata=custom,
hovertemplate=hover_tmpl,
)
fig.update_geos(
scope="asia",
showland=True, landcolor="#F5F5F4",
showocean=True, oceancolor="#EFF6FF",
showcountries=True, countrycolor="#D6D3D1",
showsubunits=True, subunitcolor="#E7E5E4",
showrivers=True, rivercolor="#DBEAFE",
center=dict(lat=22, lon=80),
projection_scale=5.0,
bgcolor="rgba(0,0,0,0)",
)
fig.update_layout(
height=620,
paper_bgcolor="rgba(0,0,0,0)",
margin=dict(l=0, r=0, t=10, b=0),
font=dict(family="DM Mono, monospace", color="#1C1917"),
showlegend=False,
hoverlabel=dict(
bgcolor="#1C1917",
bordercolor="#1C1917",
font=dict(family="DM Mono, monospace", size=11, color="#FAF9F7"),
),
)
st.plotly_chart(fig, use_container_width=True, config={"displayModeBar": False})
# ── Caption ───────────────────────────────────────────────────────────────────
n_mapped = len([c for c in custom if c[0]])
year_label = selected_year if selected_year != "β€”" else "all years"
st.caption(
f"{n_mapped} districts Β· FY {year_label} Β· "
f"Bubble size ∝ predicted person-days · Hover for full model details"
)
# ── Summary cards below map ───────────────────────────────────────────────────
st.markdown("<div style='margin-top:1.5rem'></div>", unsafe_allow_html=True)
section_label("Prediction Summary for Filtered View")
c1, c2, c3, c4 = st.columns(4)
total_pred = pred_df["predicted_persondays"].sum()
total_act = pred_df["person_days_lakhs"].sum()
mean_err = pred_df["prediction_error"].mean()
gain_total = pred_df["persondays_gain"].sum() if "persondays_gain" in pred_df.columns else 0
c1.metric("Total Predicted PD", f"{total_pred:,.1f}L")
c2.metric("Total Actual PD", f"{total_act:,.1f}L")
c3.metric("Mean Model Error", f"{mean_err:+.3f}L")
c4.metric("Total LP Gain", f"{gain_total:+,.1f}L")
from utils.ai_summary import render_ai_summary
state_param = None if state_filter == "All India" else state_filter
render_ai_summary("spatial", state_param=state_param)