causalscience's picture
Update app.py
637d86b verified
import gradio as gr
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from io import BytesIO
from PIL import Image
###############################################################################
# GLOBAL STORAGE OF PARAMETERS
###############################################################################
PARAMS = {}
###############################################################################
# DERIVED RATES + PUSH/PULL HELPER FUNCTIONS
###############################################################################
def compute_derived_rates(
# Product A (replaces Smart phone)
product_value_A, salvage_rate_A, life_cycle_A, capital_return_A,
# Product B (replaces Feature phone)
product_value_B, salvage_rate_B, life_cycle_B, capital_return_B,
# Freight
overnight_rate, standard_ratio, volume_discount,
# Picking
pick_first, pick_follow,
# Product mapping - NEW
productA_keyword, productB_keyword
):
"""
Reads the user inputs, calculates weekly inventory cost, freight rates, etc.
Stores results in PARAMS + returns summary text.
"""
try:
# Convert everything to float
pv_A = float(product_value_A)
sr_A = float(salvage_rate_A)
lc_A = float(life_cycle_A)
cr_A = float(capital_return_A)
pv_B = float(product_value_B)
sr_B = float(salvage_rate_B)
lc_B = float(life_cycle_B)
cr_B = float(capital_return_B)
on_rate = float(overnight_rate)
std_ratio = float(standard_ratio)
vol_disc = float(volume_discount)
pk_first = float(pick_first)
pk_follow = float(pick_follow)
# Liquidation values
liq_A = pv_A * sr_A
liq_B = pv_B * sr_B
# Weekly inventory cost
inv_A = (pv_A * cr_A / 52.0) + ((pv_A - liq_A) / lc_A)
inv_B = (pv_B * cr_B / 52.0) + ((pv_B - liq_B) / lc_B)
# Standard freight
std_freight = (on_rate / std_ratio) * vol_disc
# Store in global dict
PARAMS["productA_inventory_cost"] = inv_A
PARAMS["productB_inventory_cost"] = inv_B
PARAMS["overnight_rate"] = on_rate
PARAMS["standard_freight"] = std_freight
PARAMS["pick_first"] = pk_first
PARAMS["pick_follow"] = pk_follow
# Store product mapping keywords
PARAMS["productA_keyword"] = productA_keyword if productA_keyword else "Smart"
PARAMS["productB_keyword"] = productB_keyword if productB_keyword else "Feature"
summary = [
"## Computed Derived Rates:",
f"- **Product A weekly inventory**: ${inv_A:.2f}/unit",
f"- **Product B weekly inventory**: ${inv_B:.2f}/unit",
f"- **Overnight freight rate**: ${on_rate:.2f}",
f"- **Standard freight rate**: ${std_freight:.2f}",
f"- **Pick cost (first)**: ${pk_first:.2f}, (follow-up): ${pk_follow:.2f}",
"",
"## Product Mapping:",
f"- **Product A stores**: containing '{PARAMS['productA_keyword']}'",
f"- **Product B stores**: containing '{PARAMS['productB_keyword']}'"
]
return "\n".join(summary)
except ValueError as e:
return f"Error converting to floats: {str(e)}"
def compute_inventory_cost(row):
"""
Flexible method to determine product type based on store name or Product_Type column.
Uses the keyword mappings defined by the user when setting up parameters.
"""
# First check if there's a dedicated Product_Type column
if 'Product_Type' in row:
product_type = str(row['Product_Type']).strip().upper()
if product_type == 'A':
rate = PARAMS.get("productA_inventory_cost", 0.0)
else:
rate = PARAMS.get("productB_inventory_cost", 0.0)
else:
# Fall back to using the configured keywords for matching store names
store_name = str(row["Store"])
productA_keyword = PARAMS.get("productA_keyword", "Smart")
productB_keyword = PARAMS.get("productB_keyword", "Feature")
# Check if store name contains the product A keyword
if productA_keyword in store_name:
rate = PARAMS.get("productA_inventory_cost", 0.0)
# Check if store name contains the product B keyword
elif productB_keyword in store_name:
rate = PARAMS.get("productB_inventory_cost", 0.0)
else:
# If no match, log warning and default to product B cost
print(f"Warning: Store '{store_name}' doesn't match any product keyword. Defaulting to Product B.")
rate = PARAMS.get("productB_inventory_cost", 0.0)
return row["Onhand_Inventory"] * rate
def compute_shipping_cost(row):
"""If strategy=Pull => overnight, else => standard freight."""
if row["Strategy"] == "Pull":
return row["Weekly_Sales"] * PARAMS.get("overnight_rate", 0.0)
else:
return row["Weekly_Sales"] * PARAMS.get("standard_freight", 0.0)
def compute_picking_cost(row):
"""Pull => usage * pick_first; Push => pick_first + (usage-1)*pick_follow (if usage>1)."""
sales = row["Weekly_Sales"]
first = PARAMS.get("pick_first", 1.0)
follow = PARAMS.get("pick_follow", 0.1)
if row["Strategy"] == "Pull":
return sales * first
else:
if sales > 1:
return first + (sales - 1)*follow
elif sales == 1:
return first
else:
return 0.0
###############################################################################
# PUSH/PULL ANALYSIS FUNCTION
###############################################################################
def run_push_pull_analysis(df):
"""
Expects columns: Store, Strategy, Onhand_Inventory, Weekly_Sales
Returns:
- text summary
- cost breakdown chart
- savings chart
- and a DataFrame with new cost columns
"""
# Convert relevant columns to numeric
for col in ["Onhand_Inventory","Weekly_Sales"]:
df[col] = pd.to_numeric(df[col], errors="coerce").fillna(0)
# Compute cost columns
df["Inventory_Cost"] = df.apply(compute_inventory_cost, axis=1)
df["Shipping_Cost"] = df.apply(compute_shipping_cost, axis=1)
df["PickPack_Cost"] = df.apply(compute_picking_cost, axis=1)
df["Total_Cost"] = df["Inventory_Cost"] + df["Shipping_Cost"] + df["PickPack_Cost"]
# Summation text
lines = []
strategies = df["Strategy"].unique()
for s in strategies:
sub = df[df["Strategy"]==s]
inv_sum = sub["Inventory_Cost"].sum()
shp_sum = sub["Shipping_Cost"].sum()
pkp_sum = sub["PickPack_Cost"].sum()
ttl_sum = sub["Total_Cost"].sum()
lines.append(f"**{s}** Strategy:")
lines.append(f"- Inventory: ${inv_sum:,.2f}")
lines.append(f"- Shipping: ${shp_sum:,.2f}")
lines.append(f"- PickPack: ${pkp_sum:,.2f}")
lines.append(f"- **Total**: ${ttl_sum:,.2f}")
lines.append("")
# If we have both Pull and Push, compute overall % saving
if len(strategies)==2 and set(strategies)=={"Pull","Push"}:
push_total = df.loc[df["Strategy"]=="Push","Total_Cost"].sum()
pull_total = df.loc[df["Strategy"]=="Pull","Total_Cost"].sum()
if push_total>0:
saving = (push_total - pull_total)/push_total*100
lines.append(f"**Overall % saving from Pull**: {saving:.2f}%")
text_summary = "\n".join(lines)
# Chart 1: Stacked cost breakdown
group_sums = df.groupby("Strategy")[["Inventory_Cost","Shipping_Cost","PickPack_Cost"]].sum()
fig1, ax1 = plt.subplots(figsize=(6,4))
group_sums.plot.bar(stacked=True, ax=ax1)
ax1.set_title("Cost Breakdown by Strategy")
ax1.set_ylabel("Cost ($)")
# Move legend so it doesn't cover the bars
ax1.legend(loc='upper left', bbox_to_anchor=(1,1))
plt.tight_layout()
buf1 = BytesIO()
plt.savefig(buf1, format="png", bbox_inches="tight")
buf1.seek(0)
cost_img = Image.open(buf1)
plt.close(fig1)
# Chart 2: Savings by store ( (Push - Pull)/Push )
stores = df["Store"].unique()
savings_dict = {}
for store in stores:
sub = df[df["Store"]==store]
if sub["Strategy"].nunique()==2:
push_val = sub.loc[sub["Strategy"]=="Push","Total_Cost"].values[0]
pull_val = sub.loc[sub["Strategy"]=="Pull","Total_Cost"].values[0]
if push_val>0:
pct = (push_val - pull_val)/push_val*100
else:
pct = 0
savings_dict[store] = pct
fig2, ax2 = plt.subplots(figsize=(6,4))
labels = list(savings_dict.keys())
values = [savings_dict[x] for x in labels]
bars = ax2.bar(labels, values, color="skyblue")
ax2.axhline(y=0, color="black", linewidth=1)
ax2.set_ylabel("% Savings (Push vs Pull)")
ax2.set_title("Savings by Store (Pull vs. Push)")
plt.xticks(rotation=45)
for bar, val in zip(bars, values):
ax2.text(bar.get_x()+bar.get_width()/2,
(val*0.5 if val>0 else val-5),
f"{val:.2f}%", ha="center")
plt.tight_layout()
buf2 = BytesIO()
plt.savefig(buf2, format="png", bbox_inches="tight")
buf2.seek(0)
savings_img = Image.open(buf2)
plt.close(fig2)
return text_summary, cost_img, savings_img, df
###############################################################################
# CATEGORY ANALYSIS (STORES AS ITEMS, similar to "ABC")
###############################################################################
def run_category_analysis_on_stores(df, multiplier=26):
"""
Takes the push/pull result DataFrame (with columns Inventory_Cost, Shipping_Cost,
PickPack_Cost, Weekly_Sales, Store, Strategy).
1) We create "Category_Cost" = sum of Inventory + Shipping + PickPack.
2) "Usage" = Weekly_Sales
3) "Annual_Spend" = Usage * Category_Cost * multiplier
4) Sort desc, cumulative %, classify => A/B/C
Returns text summary + Pareto chart.
"""
# Build the cost
df["Category_Cost"] = df["Inventory_Cost"] + df["Shipping_Cost"] + df["PickPack_Cost"]
# MODIFIED: Create a combined Store-Strategy identifier
df["StoreStrategy"] = df["Store"] + "-" + df["Strategy"]
# Build a smaller Category df
# MODIFIED: Use StoreStrategy instead of just Store
cat_df = df[["StoreStrategy","Weekly_Sales","Category_Cost"]].copy()
cat_df.rename(columns={
"StoreStrategy":"ItemName", # MODIFIED: Using the combined identifier
"Weekly_Sales":"Usage",
"Category_Cost":"UnitCost"
}, inplace=True)
# Annual spend
cat_df["Annual_Spend"] = cat_df["Usage"] * cat_df["UnitCost"] * multiplier
# Sort desc
cat_df = cat_df.sort_values("Annual_Spend", ascending=False).reset_index(drop=True)
total_spend = cat_df["Annual_Spend"].sum()
cat_df["Cumulative_Spend"] = cat_df["Annual_Spend"].cumsum()
if total_spend>0:
cat_df["Cumulative_Percent"] = cat_df["Cumulative_Spend"]/total_spend*100
else:
cat_df["Cumulative_Percent"] = 0
# Classify each item as A/B/C
def classify_abc(pct):
if pct <= 70:
return "A"
elif pct <= 90:
return "B"
else:
return "C"
cat_df["Category"] = cat_df["Cumulative_Percent"].apply(classify_abc)
# Text summary
lines = ["## Category Analysis (Store-Strategy as Item)"] # MODIFIED: Updated title
cat_grp = cat_df.groupby("Category")["Annual_Spend"].agg(["sum","count"])
for cat in ["A","B","C"]:
if cat in cat_grp.index:
s = cat_grp.loc[cat,"sum"]
c = cat_grp.loc[cat,"count"]
pct_spend = (s/total_spend*100 if total_spend else 0)
lines.append(f"\n**Category {cat}**:")
lines.append(f" - Items: {c}")
lines.append(f" - Spend: ${s:,.2f} ({pct_spend:.1f}%)")
txt_summary = "\n".join(lines)
# Pareto chart
fig, ax1 = plt.subplots(figsize=(8,5)) # MODIFIED: Increased figure width for longer labels
ranks = cat_df.index + 1
# MODIFIED: Store the actual labels for plotting
store_strategy_labels = cat_df["ItemName"].values
ax1.bar(ranks, cat_df["Annual_Spend"], color="lightblue", label="Annual Spend")
ax1.set_xlabel("Store-Strategy Combinations (Sorted by Descending Spend)") # MODIFIED: Updated label
ax1.set_ylabel("Annual Spend", color="blue")
ax1.tick_params(axis='y', labelcolor='blue')
# MODIFIED: Set x-tick labels to store-strategy combinations
ax1.set_xticks(ranks)
ax1.set_xticklabels(store_strategy_labels, rotation=45, ha='right') # Rotate labels for readability
ax2 = ax1.twinx()
ax2.plot(ranks, cat_df["Cumulative_Percent"], color="red", linewidth=2, label="Cumulative %")
ax2.set_ylabel("Cumulative %", color="red")
ax2.tick_params(axis='y', labelcolor='red')
ax2.axhline(y=70, color="gray", linestyle="--", alpha=0.7)
ax2.axhline(y=90, color="gray", linestyle="--", alpha=0.7)
plt.title("Category Pareto Chart - (Store-Strategy as Item)") # MODIFIED: Updated title
plt.tight_layout()
buf = BytesIO()
plt.savefig(buf, format="png", bbox_inches="tight")
buf.seek(0)
chart_img = Image.open(buf)
plt.close(fig)
return txt_summary, chart_img
###############################################################################
# SINGLE FUNCTION TO RUN BOTH ANALYSES ON ONE CSV
###############################################################################
def run_both_analyses(file):
"""
1) Read user CSV (with columns: Store, Strategy, Onhand_Inventory, Weekly_Sales).
2) Do push/pull => text + 2 images + resulting DF.
3) Do category analysis => text + 1 image (on that resulting DF).
4) Return everything to Gradio.
"""
if file is None:
return (
"No file uploaded!", None, None,
"No file => Category Analysis not run!", None
)
# Read CSV
try:
df = pd.read_csv(file.name)
except Exception as e:
return (
f"Error reading CSV: {e}", None, None,
"Category Analysis not run", None
)
# Run push/pull
try:
pp_text, cost_img, savings_img, df_res = run_push_pull_analysis(df)
except Exception as e:
return (
f"Push/Pull error: {e}", None, None,
"Category Analysis not run", None
)
# Run category analysis
try:
cat_text, cat_chart = run_category_analysis_on_stores(df_res, multiplier=26)
except Exception as e:
cat_text = f"Category Analysis error: {e}"
cat_chart = None
return (
pp_text, cost_img, savings_img,
cat_text, cat_chart
)
###############################################################################
# BUILD GRADIO APP
###############################################################################
def build_app():
with gr.Blocks(theme="JohnSmith9982/small_and_pretty") as app:
gr.Markdown("# Push/Pull and Category Analysis")
gr.Markdown("**Instructions**:")
gr.Markdown(
"1. Fill in the parameter inputs and click **Compute Derived Rates**.\n"
"2. Upload **one CSV** with columns: `Store, Strategy, Onhand_Inventory, Weekly_Sales`.\n"
"3. Click **Run Both Analyses** to see Push/Pull results (2 charts) + Category Analysis results (Pareto chart)."
)
# Parameter inputs
with gr.Tab("Parameter Inputs"):
gr.Markdown("Enter your product/freight/pick parameters. Then click the button.")
with gr.Row():
with gr.Column():
gr.Markdown("### Product A")
product_value_A = gr.Textbox(label="Product Value ($)", value="500")
salvage_rate_A = gr.Textbox(label="Salvage Rate (0-1)", value="0.0")
life_cycle_A = gr.Textbox(label="Life cycle (weeks)", value="26")
capital_return_A = gr.Textbox(label="Capital return rate (0-1)", value="0.08")
with gr.Column():
gr.Markdown("### Product B")
product_value_B = gr.Textbox(label="Product Value ($)", value="200")
salvage_rate_B = gr.Textbox(label="Salvage Rate (0-1)", value="0.0")
life_cycle_B = gr.Textbox(label="Life cycle (weeks)", value="26")
capital_return_B = gr.Textbox(label="Capital return rate (0-1)", value="0.08")
with gr.Row():
with gr.Column():
gr.Markdown("### Freight")
overnight_rate = gr.Textbox(label="Overnight Rate ($)", value="12")
standard_ratio = gr.Textbox(label="Overnight/Standard Ratio", value="2.5")
volume_discount = gr.Textbox(label="Volume Discount (0-1)", value="0.5")
with gr.Column():
gr.Markdown("### Picking Costs")
pick_first = gr.Textbox(label="First Pick Cost ($)", value="1")
pick_follow = gr.Textbox(label="Follow-up Pick Cost ($)", value="0.1")
# Add Product mapping section
with gr.Row():
with gr.Column():
gr.Markdown("### Product Mapping")
productA_keyword = gr.Textbox(label="Product A Keyword (in store name)", value="Smart")
productB_keyword = gr.Textbox(label="Product B Keyword (in store name)", value="Feature")
compute_btn = gr.Button("Compute Derived Rates")
param_output = gr.Markdown()
# When user clicks, compute derived rates
compute_btn.click(
fn=compute_derived_rates,
inputs=[
product_value_A,
salvage_rate_A,
life_cycle_A,
capital_return_A,
product_value_B,
salvage_rate_B,
life_cycle_B,
capital_return_B,
overnight_rate,
standard_ratio,
volume_discount,
pick_first,
pick_follow,
productA_keyword,
productB_keyword
],
outputs=param_output
)
# Single CSV => run both analyses
with gr.Tab("Upload & Run"):
file_input = gr.File(label="Single CSV for both analyses")
run_btn = gr.Button("Run Both Analyses")
# 5 outputs from run_both_analyses
pushpull_text = gr.Markdown(label="Push/Pull Summary")
cost_chart_img = gr.Image(label="Cost Breakdown by Strategy")
savings_chart_img = gr.Image(label="Savings by Store")
cat_text = gr.Markdown(label="Category Analysis Summary")
cat_chart_img = gr.Image(label="Category Pareto Chart")
run_btn.click(
fn=run_both_analyses,
inputs=[file_input],
outputs=[
pushpull_text,
cost_chart_img,
savings_chart_img,
cat_text,
cat_chart_img
]
)
return app
###############################################################################
# MAIN
###############################################################################
if __name__ == "__main__":
app = build_app()
app.launch(debug=True)