File size: 19,738 Bytes
6b770b4
 
 
 
 
 
 
 
637d86b
6b770b4
 
 
 
637d86b
6b770b4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c3d09ab
6b770b4
 
 
 
 
 
 
 
c3d09ab
 
 
 
6b770b4
c3d09ab
 
6b770b4
c3d09ab
6b770b4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c3d09ab
6b770b4
 
 
 
 
 
 
 
 
 
 
 
 
c3d09ab
6b770b4
c3d09ab
 
 
 
6b770b4
c3d09ab
6b770b4
 
c3d09ab
 
 
 
6b770b4
 
 
 
 
 
 
 
 
c3d09ab
6b770b4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c3d09ab
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
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)