# pages/optimizer.py — Budget reallocation optimizer results and live LP runner. 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 plotly.express as px import pandas as pd from theme import inject_theme, page_header, section_label, kpi_html, PLOTLY_LAYOUT, SAFFRON, GREEN, RED, AMBER from utils.api_client import fetch_states, fetch_optimizer_results, run_optimizer_live inject_theme() page_header( "◈ Module 04", "Budget Optimizer", "SciPy LP two-stage proportional reallocation — maximize employment at zero additional cost", ) # ── Tabs: pre-computed vs live ──────────────────────────────────────────────── tab1, tab2 = st.tabs(["Pre-Computed Results", "Run Live Optimizer"]) # ══════════════════════════════════════════════════════════════════════════════ # TAB 1 — Pre-computed results # ══════════════════════════════════════════════════════════════════════════════ with tab1: states = fetch_states() if not states: st.error("⚠️ API offline — run `uvicorn backend.main:app --port 8000`") st.stop() cs, _ = st.columns([1, 2]) with cs: scope = st.selectbox("State Filter", ["All-India"] + states, key="pre_scope") state_param = None if scope == "All-India" else scope df = fetch_optimizer_results(state_param) if df.empty: st.info("No optimizer results — run the pipeline first: `python main.py --stage 3`") else: # ── Summary KPIs ────────────────────────────────────────────────────── sq_total = df["sq_persondays"].sum() opt_total = df["opt_persondays"].sum() if "opt_persondays" in df.columns else sq_total + df["persondays_gain"].sum() gain = df["persondays_gain"].sum() gain_pct = gain / sq_total * 100 if sq_total else 0 tot_bud = df["budget_allocated_lakhs"].sum() if "budget_allocated_lakhs" in df.columns else 0 n_gain = int((df["persondays_gain"] > 0).sum()) n_cut = int((df["persondays_gain"] <= 0).sum()) kc1, kc2, kc3, kc4, kc5 = st.columns(5) with kc1: st.markdown(kpi_html(f"{sq_total:,.0f}L", "Status Quo PD", "#1C1917"), unsafe_allow_html=True) with kc2: st.markdown(kpi_html(f"{opt_total:,.0f}L", "Optimized PD", GREEN), unsafe_allow_html=True) with kc3: st.markdown(kpi_html(f"{gain:+,.1f}L", "Net Gain", GREEN, "lakh person-days"), unsafe_allow_html=True) with kc4: st.markdown(kpi_html(f"{gain_pct:+.2f}%", "% Uplift", GREEN, "budget-neutral"), unsafe_allow_html=True) with kc5: st.markdown(kpi_html(f"₹{tot_bud:,.0f}L", "Total Budget", "#1C1917", "unchanged"), unsafe_allow_html=True) st.markdown("
", unsafe_allow_html=True) # ── Budget change waterfall — top movers ────────────────────────────── section_label("Top Budget Movers") top_gain = df.nlargest(10, "persondays_gain").copy() top_cut = df.nsmallest(10, "persondays_gain").copy() show = pd.concat([top_gain, top_cut]).drop_duplicates().sort_values("persondays_gain") show["label"] = show["district"] + " · " + show["state"] fig1 = go.Figure() fig1.add_bar( x=show["persondays_gain"], y=show["label"], orientation="h", marker=dict( color=[GREEN if v > 0 else RED for v in show["persondays_gain"]], opacity=0.8, ), customdata=list(zip( show["state"], show["district"], show["budget_allocated_lakhs"].round(0) if "budget_allocated_lakhs" in show else [0]*len(show), show.get("budget_change_pct", pd.Series([0]*len(show))).round(1), show["persondays_gain"].round(2), show.get("persondays_per_lakh", pd.Series([0]*len(show))).round(4), )), hovertemplate=( "%{customdata[1]} · %{customdata[0]}Run the SciPy linear-programming optimizer live with custom parameters. Results are computed in real-time using the latest district predictions from the database.
""", unsafe_allow_html=True) ca, cb = st.columns(2) states2 = fetch_states() or [] with ca: scope2 = st.selectbox("State (or All-India)", ["All-India"] + states2, key="live_scope") budget_scale = st.slider("Budget Scale", 0.8, 1.5, 1.0, 0.05, help="1.0 = same total budget; 1.1 = +10% more funds") with cb: min_frac = st.slider("Min Allocation (floor)", 0.10, 0.60, 0.40, 0.05, help="No district drops below this fraction of its current budget") max_frac = st.slider("Max Allocation (cap)", 1.5, 3.0, 2.5, 0.1, help="No district exceeds this multiple of its current budget") if st.button("▶ Run Optimizer", type="primary"): with st.spinner("Running LP optimization…"): result = run_optimizer_live( state=None if scope2 == "All-India" else scope2, budget_scale=budget_scale, min_fraction=min_frac, max_fraction=max_frac, ) if result: st.success( f"✅ Optimization complete — " f"Gain: **{result['gain_lakhs']:+,.2f}L** person-days " f"({result['gain_pct']:+.2f}%) · " f"Total budget: ₹{result['total_budget_lakhs']:,.0f}L" ) # Summary metrics m1, m2, m3, m4 = st.columns(4) m1.metric("SQ Person-Days", f"{result['sq_persondays_total']:,.1f}L") m2.metric("Opt Person-Days", f"{result['opt_persondays_total']:,.1f}L") m3.metric("Net Gain", f"{result['gain_lakhs']:+,.2f}L") m4.metric("% Uplift", f"{result['gain_pct']:+.2f}%") # District breakdown if result.get("districts"): dist_df = pd.DataFrame(result["districts"]) section_label("District Reallocation Details") top10 = dist_df.nlargest(10, "persondays_gain") top10["label"] = top10["district"] + " · " + top10["state"] fig_live = go.Figure() fig_live.add_bar( x=top10["persondays_gain"], y=top10["label"], orientation="h", marker=dict(color=GREEN, opacity=0.8), hovertemplate=( "%{y}