GaganMayuresh's picture
Fix Arrow.width error: replace width='stretch' with use_container_width=True and add try/except error handling for charts
76b0ae2
Raw
History Blame Contribute Delete
18.4 kB
# pyrefly: ignore [missing-import]
import streamlit as st
import numpy as np
import pandas as pd
from scipy.optimize import linprog
import plotly.graph_objects as go
import plotly.express as px
import json
from db_utils import get_connection, hash_password, verify_password, derive_key, encrypt_data, decrypt_data
# Set page configurations
st.set_page_config(
page_title="Marketing Budget Optimization System",
page_icon="📊",
layout="wide",
initial_sidebar_state="expanded"
)
# ---------- User Authentication ----------
if 'logged_in' not in st.session_state:
st.session_state['logged_in'] = False
st.session_state['user_id'] = None
st.session_state['username'] = ''
st.session_state['password'] = ''
def login_user(username, password):
conn = get_connection()
cur = conn.cursor()
cur.execute("SELECT id, password_hash, salt FROM users WHERE username = ?", (username,))
row = cur.fetchone()
conn.close()
if row and verify_password(row['password_hash'], row['salt'], password):
st.session_state['logged_in'] = True
st.session_state['user_id'] = row['id']
st.session_state['username'] = username
st.session_state['password'] = password
st.success(f"Logged in as {username}")
else:
st.error("Invalid username or password")
def register_user(username, password, full_name, email, company):
pwd_hash, salt = hash_password(password)
conn = get_connection()
cur = conn.cursor()
try:
cur.execute("INSERT INTO users (username, password_hash, salt) VALUES (?,?,?)", (username, pwd_hash, salt))
conn.commit()
st.success("Registration successful. You can now log in.")
except Exception as e:
st.error(f"Registration failed: {e}")
finally:
conn.close()
# Sidebar Account Section
with st.sidebar.expander("User Account", expanded=True):
if not st.session_state['logged_in']:
mode = st.radio("Choose action", ["Login", "Register"])
if mode == "Login":
login_user_name = st.text_input("Username")
login_password = st.text_input("Password", type="password")
if st.button("Login"):
login_user(login_user_name, login_password)
else:
reg_user_name = st.text_input("Choose Username")
reg_password = st.text_input("Choose Password", type="password")
reg_full_name = st.text_input("Full Name")
reg_email = st.text_input("Email")
reg_company = st.text_input("Company")
if st.button("Register"):
register_user(reg_user_name, reg_password, reg_full_name, reg_email, reg_company)
else:
st.write(f"Logged in as **{st.session_state['username']}**")
if st.button("Logout"):
st.session_state['logged_in'] = False
st.session_state['user_id'] = None
st.session_state['username'] = ''
st.session_state['password'] = ''
st.success("Logged out")
# Main Title and Executive Header
st.title("📊 Marketing Budget Optimization System")
st.markdown("##### *Enterprise-Grade Linear Programming Framework for Strategic Resource Allocation*")
# Introduction & LaTeX Math Model Section
with st.expander("📚 Mathematical Optimization Framework (LaTeX)", expanded=False):
st.markdown("""
### Linear Programming (LP) Formulation
To maximize our total brand impressions (Reach) across all social platforms while complying with budgetary boundaries and minimum operational limits, we construct a Linear Programming model.
Let:
* $x_1$ = Budget allocated to **Instagram** (₹)
* $x_2$ = Budget allocated to **Google Ads** (₹)
* $x_3$ = Budget allocated to **YouTube Ads** (₹)
Let $c_1, c_2, c_3$ denote the **Reach Efficiency Coefficients** (Impressions per unit spend) for each respective channel.
""")
st.latex(r"""
\begin{aligned}
\textbf{Maximize Reach (Objective Function):} \quad & Z = c_1 x_1 + c_2 x_2 + c_3 x_3 \\
\textbf{Subject to:} \quad & x_1 + x_2 + x_3 \le B \quad \text{(Total Budget Constraint)} \\
& L_1 \le x_1 \le U_1 \quad \text{(Instagram Limits)} \\
& L_2 \le x_2 \le U_2 \quad \text{(Google Ads Limits)} \\
& L_3 \le x_3 \le U_3 \quad \text{(YouTube Ads Limits)}
\end{aligned}
""")
st.markdown("""
#### Dual Problem Reformulation for Minimization (SciPy Engine)
Since `scipy.optimize.linprog` is built exclusively as a minimization solver, we translate our maximization objective function by negating all objective coefficients:
""")
st.latex(r"""
\textbf{Minimize:} \quad -Z = -c_1 x_1 - c_2 x_2 - c_3 x_3
""")
st.markdown("Upon completion, the solver returns $-Z$, which we negate back ($Z = -(-Z)$) to yield the optimal maximum reach.")
# ==========================================
# SIDEBAR CONTROLS & INPUT MATRIX
# ==========================================
st.sidebar.header("⚙️ Global Configurations")
# Total Budget Input
total_budget = st.sidebar.number_input(
"Total Available Budget (₹)",
min_value=1000.0,
max_value=1000000.0,
value=10000.0,
step=500.0,
help="Define the total maximum monetary cap for reallocation."
)
st.sidebar.markdown("---")
st.sidebar.header("📱 Platform Configurations")
# Instagram parameters
st.sidebar.subheader("📸 Instagram")
ig_c = st.sidebar.number_input("Reach Efficiency (Views/₹) - IG", min_value=1.0, value=50.0, key="ig_c_val")
ig_l = st.sidebar.number_input("Lower Bound Min Spend (₹) - IG", min_value=0.0, value=1000.0, key="ig_l_val")
ig_u = st.sidebar.number_input("Upper Bound Max Spend (₹) - IG", min_value=0.0, value=5000.0, key="ig_u_val")
st.sidebar.markdown("---")
# Google Ads parameters
st.sidebar.subheader("🔍 Google Ads")
go_c = st.sidebar.number_input("Reach Efficiency (Views/₹) - Google", min_value=1.0, value=40.0, key="go_c_val")
go_l = st.sidebar.number_input("Lower Bound Min Spend (₹) - Google", min_value=0.0, value=1500.0, key="go_l_val")
go_u = st.sidebar.number_input("Upper Bound Max Spend (₹) - Google", min_value=0.0, value=4000.0, key="go_u_val")
st.sidebar.markdown("---")
# YouTube Ads parameters
st.sidebar.subheader("🎥 YouTube Ads")
yt_c = st.sidebar.number_input("Reach Efficiency (Views/₹) - YouTube Ads", min_value=1.0, value=60.0, key="yt_c_val")
yt_l = st.sidebar.number_input("Lower Bound Min Spend (₹) - YouTube Ads", min_value=0.0, value=1000.0, key="yt_l_val")
yt_u = st.sidebar.number_input("Upper Bound Max Spend (₹) - YouTube Ads", min_value=0.0, value=6000.0, key="yt_u_val")
# ==========================================
# SOLVER ENGINE & EXCEPTION HANDLING
# ==========================================
# 1. Pre-solver Validation Checks
total_min_required = ig_l + go_l + yt_l
bounds_invalid = (ig_l > ig_u) or (go_l > go_u) or (yt_l > yt_u)
if bounds_invalid:
st.error("### ⚠️ Invalid Platform Bound Constraints")
st.markdown(
f"""
One or more of your platforms has a **Minimum Operational Spend (Lower Bound)** that exceeds its **Maximum Safety Cap (Upper Bound)**.
**Please verify your inputs:**
* **Instagram:** Min: ₹{ig_l:,.2f} | Max: ₹{ig_u:,.2f} {"❌ (Invalid)" if ig_l > ig_u else "✅"}
* **Google Ads:** Min: ₹{go_l:,.2f} | Max: ₹{go_u:,.2f} {"❌ (Invalid)" if go_l > go_u else "✅"}
* **YouTube Ads:** Min: ₹{yt_l:,.2f} | Max: ₹{yt_u:,.2f} {"❌ (Invalid)" if yt_l > yt_u else "✅"}
"""
)
elif total_min_required > total_budget:
st.error("### ⚠️ Budget Deficit - Infeasible Constraints")
st.markdown(
f"""
The sum of the minimum required operational spend for all platforms is greater than your total available budget!
* **Sum of Platform Lower Bounds ($L_1 + L_2 + L_3$):** **₹{total_min_required:,.2f}**
* **Your Total Allocated Budget ($B$):** **₹{total_budget:,.2f}**
* **Required Deficit:** **₹{total_min_required - total_budget:,.2f}**
*To solve this issue, please increase your **Total Available Budget** in the sidebar, or decrease the **Lower Bound Spend** limits of your platforms.*
"""
)
else:
# Build LP matrices
# Objective: Maximize Reach. Correct negation logic for minimization solver:
# linprog solves: min c^T x ===> to max c^T x, we solve: min -c^T x
c_coefficients = np.array([ig_c, go_c, yt_c])
c_negated = -c_coefficients
# Inequality constraints (A_ub * x <= b_ub)
# x1 + x2 + x3 <= Total Budget
A_ub = np.array([[1.0, 1.0, 1.0]])
b_ub = np.array([total_budget])
# Platform Bounds [(L1, U1), (L2, U2), (L3, U3)]
bounds = [
(ig_l, ig_u),
(go_l, go_u),
(yt_l, yt_u)
]
try:
# Solve using HiGHS method
result = linprog(
c=c_negated,
A_ub=A_ub,
b_ub=b_ub,
bounds=bounds,
method='highs'
)
if result.success:
x_optimal = result.x
# Guard against potential microscopic solver noise resulting in negative tiny floats
x_optimal = np.clip(x_optimal, a_min=[ig_l, go_l, yt_l], a_max=[ig_u, go_u, yt_u])
calculated_reach = -result.fun
total_allocated_capital = np.sum(x_optimal)
# Localized impressions calculations
ig_reach = x_optimal[0] * ig_c
go_reach = x_optimal[1] * go_c
yt_reach = x_optimal[2] * yt_c
# ==========================================
# DASHBOARD LAYOUT & ANALYTICS
# ==========================================
# 1. KPI Cards Row
kpi_col1, kpi_col2, kpi_col3 = st.columns(3)
with kpi_col1:
st.metric(
label="🎯 Maximum Calculated Reach",
value=f"{int(round(calculated_reach)):,} Impressions",
help="The maximum possible reach (impressions) calculated based on optimal spend allocation."
)
with kpi_col2:
st.metric(
label="💳 Total Allocated Capital",
value=f"₹{total_allocated_capital:,.2f}",
delta=f"{((total_allocated_capital / total_budget) * 100):.1f}% Budget Utilized",
delta_color="normal",
help="Total currency value allocated out of the available budget limit."
)
with kpi_col3:
slack = total_budget - total_allocated_capital
st.metric(
label="💰 Unallocated Capital (Slack)",
value=f"₹{slack:,.2f}",
delta=f"{((slack / total_budget) * 100):.1f}% Leftover",
delta_color="inverse",
help="Leftover unallocated capital due to maximum budget constraints."
)
# 2. DataFrame and Plots Section
tab1, tab2 = st.tabs(["📊 Optimization Analysis", "📋 Technical Model Execution Logs"])
with tab1:
# Construct clean DataFrame
platform_names = ["Instagram", "Google Ads", "YouTube Ads"]
spend_splits = [x_optimal[0], x_optimal[1], x_optimal[2]]
efficiencies = [ig_c, go_c, yt_c]
reaches = [ig_reach, go_reach, yt_reach]
percentages = [(s / total_allocated_capital) * 100 if total_allocated_capital > 0 else 0 for s in spend_splits]
df_profile = pd.DataFrame({
"Platform": platform_names,
"Optimal Budget Allocation (₹)": spend_splits,
"Budget Share (%)": percentages,
"Reach Efficiency (Views/₹)": efficiencies,
"Expected Impressions (Views)": reaches
})
# Render Clean DataFrame
st.markdown("### 📈 Optimal Allocation Data Profile Table")
try:
st.dataframe(
df_profile.style.format({
"Optimal Budget Allocation (₹)": "₹{:,.2f}",
"Budget Share (%)": "{:.2f}%",
"Reach Efficiency (Views/₹)": "{:,.1f}",
"Expected Impressions (Views)": "{:,.0f}"
}),
use_container_width=True,
hide_index=True
)
except Exception as chart_err:
st.warning(f"⚠️ Table rendering issue: {chart_err}")
st.write(df_profile)
# Graphical Visualizations in Columns
chart_col1, chart_col2 = st.columns(2)
with chart_col1:
st.markdown("### 📊 Optimal Spend Allocation per Platform")
try:
# Plotly Corporate Styled Bar Chart
fig_bar = go.Figure(data=[
go.Bar(
x=platform_names,
y=spend_splits,
text=[f"₹{s:,.0f}" for s in spend_splits],
textposition='auto',
marker=dict(
color=['#6c5ce7', '#0984e3', '#00cec9'],
line=dict(color='rgba(0, 0, 0, 0.1)', width=1)
)
)
])
fig_bar.update_layout(
yaxis_title="Budget Split (₹)",
xaxis_title="Platform",
template="plotly_white",
margin=dict(l=20, r=20, t=20, b=20),
height=400,
hovermode="x unified"
)
st.plotly_chart(fig_bar, use_container_width=True)
except Exception as chart_err:
st.warning(f"⚠️ Bar chart rendering failed: {chart_err}")
st.write("**Optimal Spend Allocation:**")
for name, spend in zip(platform_names, spend_splits):
st.write(f"- {name}: ₹{spend:,.2f}")
with chart_col2:
st.markdown("### 🍕 Percentage Budget Distribution")
try:
# Plotly Corporate Styled Pie Chart
fig_pie = go.Figure(data=[
go.Pie(
labels=platform_names,
values=spend_splits,
hole=0.4,
marker=dict(
colors=['#6c5ce7', '#0984e3', '#00cec9']
),
textinfo='percent+label',
insidetextorientation='radial'
)
])
fig_pie.update_layout(
template="plotly_white",
margin=dict(l=20, r=20, t=20, b=20),
height=400,
showlegend=False
)
st.plotly_chart(fig_pie, use_container_width=True)
except Exception as chart_err:
st.warning(f"⚠️ Pie chart rendering failed: {chart_err}")
st.write("**Budget Distribution:**")
for name, spend, pct in zip(platform_names, spend_splits, percentages):
st.write(f"- {name}: ₹{spend:,.2f} ({pct:.1f}%)")
with tab2:
# Technical Solver summary logs for reviews
st.markdown("### ⚙️ Solver Execution Diagnostics")
st.json({
"status_code": int(result.status),
"status_message": result.message,
"solver_method": "HiGHS",
"success": bool(result.success),
"number_of_iterations": int(result.nit),
"raw_slack_variables": list(result.slack),
"raw_optimal_x": list(result.x),
"raw_optimal_fun": float(result.fun)
})
st.info(
"""
**Methodology Note:**
The optimization utilizes the SciPy simplex/interior-point wrapper method `highs`,
a modern high-performance solver suite designed for large-scale linear and mixed-integer programming problems.
"""
)
else:
# Handle solver failure cases elegantly
st.error(f"### ⚠️ Optimization Failed")
st.markdown(
f"""
The optimization engine completed execution but could not find a mathematically feasible solution.
**Solver Reason:** `{result.message}`
*This typically occurs if upper bound limits are strictly smaller than the lower bound limits or if constraints create an empty feasibility region. Please check and adjust your platform constraints.*
"""
)
except Exception as e:
st.error(f"### ⚠️ Math Optimization Engine Exception")
st.markdown(
f"""
An unexpected error occurred in the Scipy Optimization Engine:
```python
Error: {str(e)}
```
Please verify that your input fields contain only valid numeric figures and do not present contradictory parameters.
"""
)