Spaces:
Sleeping
Sleeping
| # 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("<div style='margin-top:1.5rem'></div>", 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=( | |
| "<b>%{customdata[1]}</b> Β· %{customdata[0]}<br>" | |
| "Budget: βΉ%{customdata[2]:,.0f}L β %{customdata[3]:+.1f}%<br>" | |
| "PD Gain: <b>%{customdata[4]:+.2f}L</b><br>" | |
| "Efficiency: %{customdata[5]} PD/βΉL" | |
| "<extra></extra>" | |
| ), | |
| ) | |
| fig1.add_vline(x=0, line_dash="solid", line_color="#1C1917", line_width=1) | |
| l1 = {**PLOTLY_LAYOUT} | |
| l1.update(dict( | |
| height=520, | |
| title=dict(text="Person-Day Gain by District (Top 10 + Bottom 10)", | |
| font=dict(family="Fraunces, serif", size=14, color="#1C1917")), | |
| xaxis=dict(**PLOTLY_LAYOUT["xaxis"], title="Person-Day Gain (Lakh)"), | |
| yaxis=dict(**PLOTLY_LAYOUT["yaxis"]), | |
| showlegend=False, | |
| bargap=0.3, | |
| )) | |
| fig1.update_layout(**l1) | |
| st.plotly_chart(fig1, use_container_width=True, config={"displayModeBar": False}) | |
| # ββ Efficiency vs budget change scatter βββββββββββββββββββββββββββββββ | |
| section_label("Efficiency vs Budget Reallocation") | |
| if "persondays_per_lakh" in df.columns and "budget_change_pct" in df.columns: | |
| fig2 = go.Figure() | |
| fig2.add_scatter( | |
| x=df["persondays_per_lakh"], | |
| y=df["budget_change_pct"], | |
| mode="markers", | |
| marker=dict( | |
| color=df["persondays_gain"], | |
| colorscale=[[0, RED], [0.5, "#FED7AA"], [1, GREEN]], | |
| size=5, opacity=0.65, | |
| colorbar=dict( | |
| title=dict(text="PD Gain", font=dict(color="#78716C", size=9)), | |
| tickfont=dict(color="#78716C", size=8), | |
| thickness=8, len=0.5, | |
| ), | |
| ), | |
| customdata=list(zip( | |
| df["state"], df["district"], | |
| df["budget_change_pct"].round(1), | |
| df["persondays_gain"].round(2), | |
| )), | |
| hovertemplate=( | |
| "<b>%{customdata[1]}</b> Β· %{customdata[0]}<br>" | |
| "Budget Ξ: %{customdata[2]:+.1f}%<br>" | |
| "PD Gain: %{customdata[3]:+.2f}L" | |
| "<extra></extra>" | |
| ), | |
| ) | |
| fig2.add_hline(y=0, line_dash="dot", line_color="#1C1917", line_width=1) | |
| l2 = {**PLOTLY_LAYOUT} | |
| l2.update(dict( | |
| height=340, | |
| title=dict(text="Efficiency (PD/βΉ Lakh) vs Budget Change %", | |
| font=dict(family="Fraunces, serif", size=13, color="#1C1917")), | |
| xaxis=dict(**PLOTLY_LAYOUT["xaxis"], title="PD per βΉ Lakh"), | |
| yaxis=dict(**PLOTLY_LAYOUT["yaxis"], title="Budget Change (%)"), | |
| showlegend=False, | |
| )) | |
| fig2.update_layout(**l2) | |
| st.plotly_chart(fig2, use_container_width=True, config={"displayModeBar": False}) | |
| # ββ Full table ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| with st.expander("π Full Reallocation Table"): | |
| show_cols = [c for c in [ | |
| "state", "district", "budget_allocated_lakhs", "optimized_budget", | |
| "budget_change_pct", "sq_persondays", "opt_persondays", | |
| "persondays_gain", "persondays_gain_pct", "persondays_per_lakh", | |
| ] if c in df.columns] | |
| styled = df[show_cols].round(3).sort_values("persondays_gain", ascending=False) | |
| st.dataframe(styled, use_container_width=True, hide_index=True) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # TAB 2 β Live optimizer | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| with tab2: | |
| st.markdown(""" | |
| <p style="font-family:'Source Serif 4',serif; font-size:0.9rem; color:#57534E; | |
| line-height:1.7; margin-bottom:1.5rem;"> | |
| Run the SciPy linear-programming optimizer live with custom parameters. | |
| Results are computed in real-time using the latest district predictions from the database. | |
| </p> | |
| """, 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=( | |
| "<b>%{y}</b><br>PD Gain: <b>%{x:+.2f}L</b><extra></extra>" | |
| ), | |
| ) | |
| l_live = {**PLOTLY_LAYOUT} | |
| l_live.update(dict( | |
| height=380, showlegend=False, bargap=0.3, | |
| title=dict(text="Top 10 Districts to Increase", | |
| font=dict(family="Fraunces, serif", size=13, color="#1C1917")), | |
| xaxis=dict(**PLOTLY_LAYOUT["xaxis"], title="PD Gain (Lakh)"), | |
| yaxis=dict(**PLOTLY_LAYOUT["yaxis"]), | |
| )) | |
| fig_live.update_layout(**l_live) | |
| st.plotly_chart(fig_live, use_container_width=True, | |
| config={"displayModeBar": False}) | |
| with st.expander("π Full Live Results Table"): | |
| st.dataframe(dist_df.round(3), use_container_width=True, hide_index=True) | |
| from utils.ai_summary import render_ai_summary | |
| render_ai_summary("overview", state_param=state_param) | |