PD03 commited on
Commit
10a6fd4
Β·
verified Β·
1 Parent(s): 7c10250

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +363 -515
app.py CHANGED
@@ -4,8 +4,6 @@ import pandas as pd
4
  import plotly.express as px
5
  import plotly.graph_objects as go
6
  from plotly.subplots import make_subplots
7
- import shap
8
- import matplotlib.pyplot as plt
9
  from datetime import datetime, timedelta
10
  from sklearn.model_selection import train_test_split
11
  from sklearn.compose import ColumnTransformer
@@ -17,7 +15,6 @@ from sklearn.metrics import r2_score, mean_absolute_error
17
  import warnings
18
  warnings.filterwarnings('ignore')
19
 
20
- # Enhanced page config
21
  st.set_page_config(
22
  page_title="Profitability Intelligence Suite",
23
  page_icon="πŸ“Š",
@@ -25,7 +22,7 @@ st.set_page_config(
25
  initial_sidebar_state="collapsed"
26
  )
27
 
28
- # Custom CSS for premium look
29
  st.markdown("""
30
  <style>
31
  .main-header {
@@ -34,7 +31,6 @@ st.markdown("""
34
  color: #1f77b4;
35
  text-align: center;
36
  margin-bottom: 0.5rem;
37
- text-shadow: 2px 2px 4px rgba(0,0,0,0.1);
38
  }
39
  .sub-header {
40
  font-size: 1.2rem;
@@ -56,7 +52,6 @@ st.markdown("""
56
  padding: 1.5rem;
57
  margin: 1rem 0;
58
  border-radius: 8px;
59
- box-shadow: 0 4px 8px rgba(0,0,0,0.05);
60
  }
61
  .recommendation-card {
62
  background: white;
@@ -65,31 +60,20 @@ st.markdown("""
65
  padding: 1.5rem;
66
  margin: 1rem 0;
67
  box-shadow: 0 4px 12px rgba(0,0,0,0.08);
68
- transition: transform 0.2s;
69
- }
70
- .recommendation-card:hover {
71
- transform: translateY(-5px);
72
- box-shadow: 0 8px 20px rgba(0,0,0,0.12);
73
  }
74
  .positive-impact {
75
  color: #28a745;
76
  font-weight: 700;
77
  font-size: 1.5rem;
78
  }
79
- .stTabs [data-baseweb="tab-list"] {
80
- gap: 2rem;
81
- }
82
- .stTabs [data-baseweb="tab"] {
83
- height: 3rem;
84
- font-size: 1.1rem;
85
- font-weight: 600;
86
  }
87
  </style>
88
  """, unsafe_allow_html=True)
89
 
90
- # -----------------------------
91
- # Data Generation
92
- # -----------------------------
93
  @st.cache_data(show_spinner=False)
94
  def generate_synthetic_data(days=60, seed=42, rows_per_day=600):
95
  rng = np.random.default_rng(seed)
@@ -171,67 +155,82 @@ def generate_synthetic_data(days=60, seed=42, rows_per_day=600):
171
  df = pd.DataFrame(records)
172
  return df
173
 
174
- @st.cache_data(show_spinner=False)
175
- def build_features(_df):
176
- df = _df.copy()
177
- # FIXED: Remove duplicate features
178
- feats_num = ["net_price", "unit_cost", "qty", "discount_pct", "list_price", "dow"]
179
- feats_cat = ["product", "region", "channel"]
180
- df = df.sort_values("date").copy()
181
- seg = ["product", "region", "channel"]
182
-
183
- # Add rolling features
184
- df["roll7_qty"] = df.groupby(seg)["qty"].transform(lambda s: s.rolling(7, min_periods=1).median())
185
- df["roll7_price"] = df.groupby(seg)["net_price"].transform(lambda s: s.rolling(7, min_periods=1).median())
186
- df["roll7_cost"] = df.groupby(seg)["unit_cost"].transform(lambda s: s.rolling(7, min_periods=1).median())
187
-
188
- feats_num += ["roll7_qty", "roll7_price", "roll7_cost"]
189
- target = "gm_pct"
190
- return df, feats_num, feats_cat, target
191
-
192
- @st.cache_resource(show_spinner=False)
193
- def train_model(feats_num, feats_cat, target, _X, _y):
194
- pre = ColumnTransformer(
195
- transformers=[
196
- ("cat", OneHotEncoder(handle_unknown="ignore"), feats_cat),
197
- ("num", "passthrough", feats_num),
198
- ]
199
- )
200
- model = RandomForestRegressor(n_estimators=80, max_depth=8, random_state=42, n_jobs=-1)
201
- pipe = Pipeline([("pre", pre), ("rf", model)])
202
- X_train, X_test, y_train, y_test = train_test_split(_X, _y, test_size=0.25, shuffle=False, random_state=42)
203
- pipe.fit(X_train, y_train)
204
- pred = pipe.predict(X_test)
205
- r2 = r2_score(y_test, pred)
206
- mae = mean_absolute_error(y_test, pred)
207
- return pipe, {"r2": r2, "mae": mae}, X_test
208
-
209
- @st.cache_data(show_spinner=False)
210
- def compute_shap_values(_pipe, _X_sample, feats_num, feats_cat, shap_sample=400):
211
- try:
212
- np.random.seed(42)
213
- X_sample = _X_sample.copy() if hasattr(_X_sample, 'copy') else pd.DataFrame(_X_sample)
214
-
215
- if len(X_sample) > shap_sample:
216
- sample_idx = np.random.choice(len(X_sample), size=shap_sample, replace=False)
217
- X_sample = X_sample.iloc[sample_idx]
218
-
219
- X_t = _pipe.named_steps["pre"].transform(X_sample)
220
- if hasattr(X_t, 'toarray'):
221
- X_t = X_t.toarray()
222
-
223
- cat_features = list(_pipe.named_steps["pre"].named_transformers_["cat"].get_feature_names_out(feats_cat))
224
- feature_names = cat_features + feats_num
225
-
226
- explainer = shap.TreeExplainer(_pipe.named_steps["rf"])
227
- shap_values = explainer.shap_values(X_t)
228
-
229
- shap_df = pd.DataFrame(shap_values, columns=feature_names)
230
-
231
- return shap_df, X_sample.reset_index(drop=True), feature_names
232
- except Exception as e:
233
- st.error(f"Error computing SHAP: {str(e)}")
234
- return None, None, None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
235
 
236
  def estimate_segment_elasticity(df, product, region, channel):
237
  seg_df = df[(df["product"]==product)&(df["region"]==region)&(df["channel"]==channel)]
@@ -285,516 +284,365 @@ def simulate_pricing_action(segment_df, elasticity, discount_reduction_pct):
285
  except:
286
  return None
287
 
288
- # -----------------------------
289
  # Main App
290
- # -----------------------------
291
-
292
- st.markdown('<h1 class="main-header">🎯 Profitability Intelligence Suite</h1>', unsafe_allow_html=True)
293
- st.markdown('<p class="sub-header">AI-Powered Margin Analysis & Strategic Recommendations</p>', unsafe_allow_html=True)
294
 
295
  # Generate data
296
  with st.spinner("πŸ”„ Loading business data..."):
297
  df = generate_synthetic_data(days=60, seed=42, rows_per_day=600)
298
- df_feat, feats_num, feats_cat, target = build_features(df)
299
 
300
- # Calculate KPIs
301
  daily = df.groupby("date").agg(
302
  revenue=("revenue","sum"),
303
  cogs=("cogs","sum"),
304
- gm_value=("gm_value","sum")
 
305
  ).reset_index()
306
  daily["gm_pct"] = np.where(daily["revenue"]>0, daily["gm_value"]/daily["revenue"], 0.0)
307
 
308
- today_row = daily.iloc[-1]
309
- yesterday_row = daily.iloc[-2] if len(daily) > 1 else today_row
310
- week_ago_row = daily.iloc[-8] if len(daily) > 7 else today_row
 
 
311
  roll7 = daily["gm_pct"].tail(7).mean()
312
 
313
- # Executive Dashboard Section
314
- st.markdown("### πŸ“Š Executive Performance Dashboard")
 
 
 
315
 
316
  col1, col2, col3, col4 = st.columns(4)
317
 
318
  with col1:
319
- delta_gm = (today_row["gm_pct"] - yesterday_row["gm_pct"]) * 100
320
  st.metric(
321
  label="Gross Margin %",
322
- value=f"{today_row['gm_pct']*100:.1f}%",
323
- delta=f"{delta_gm:+.2f}pp vs yesterday",
324
  delta_color="normal"
325
  )
326
 
327
  with col2:
328
- delta_rev = ((today_row["revenue"] - yesterday_row["revenue"]) / yesterday_row["revenue"] * 100) if yesterday_row["revenue"] > 0 else 0
329
  st.metric(
330
- label="Revenue (Today)",
331
- value=f"${today_row['revenue']/1e6:.2f}M",
332
- delta=f"{delta_rev:+.1f}% DoD",
333
  delta_color="normal"
334
  )
335
 
336
  with col3:
 
337
  st.metric(
338
- label="Gross Margin $ (Today)",
339
- value=f"${today_row['gm_value']/1e6:.2f}M",
340
- delta=f"${(today_row['gm_value'] - yesterday_row['gm_value'])/1e6:+.2f}M",
341
  delta_color="normal"
342
  )
343
 
344
  with col4:
345
- avg_gm_vs_week = (today_row["gm_pct"] - week_ago_row["gm_pct"]) * 100
346
  st.metric(
347
- label="7-Day Avg GM%",
348
- value=f"{roll7*100:.1f}%",
349
- delta=f"{avg_gm_vs_week:+.2f}pp WoW",
350
  delta_color="normal"
351
  )
352
 
353
- # Trend visualization
354
- st.markdown("#### πŸ“ˆ Performance Trend Analysis")
355
-
356
- fig_trends = make_subplots(
357
- rows=1, cols=2,
358
- subplot_titles=("Gross Margin % Trend", "Revenue & Margin $ Trend"),
359
- specs=[[{"secondary_y": False}, {"secondary_y": True}]]
360
- )
361
-
362
- fig_trends.add_trace(
363
- go.Scatter(
364
- x=daily["date"],
365
- y=daily["gm_pct"]*100,
366
- name="GM%",
367
- line=dict(color="#1f77b4", width=3),
368
- fill='tozeroy',
369
- fillcolor="rgba(31, 119, 180, 0.1)"
370
- ),
371
- row=1, col=1
372
- )
373
-
374
- fig_trends.add_trace(
375
- go.Scatter(
376
- x=daily["date"],
377
- y=daily["revenue"]/1e6,
378
- name="Revenue",
379
- line=dict(color="#2ca02c", width=2)
380
- ),
381
- row=1, col=2
382
  )
383
-
384
- fig_trends.add_trace(
385
- go.Scatter(
386
- x=daily["date"],
387
- y=daily["gm_value"]/1e6,
388
- name="GM Value",
389
- line=dict(color="#ff7f0e", width=2, dash="dash")
390
- ),
391
- row=1, col=2, secondary_y=True
392
- )
393
-
394
- fig_trends.update_xaxes(title_text="Date", row=1, col=1)
395
- fig_trends.update_xaxes(title_text="Date", row=1, col=2)
396
- fig_trends.update_yaxes(title_text="Gross Margin %", row=1, col=1)
397
- fig_trends.update_yaxes(title_text="Revenue ($M)", row=1, col=2)
398
- fig_trends.update_yaxes(title_text="GM Value ($M)", row=1, col=2, secondary_y=True)
399
-
400
- fig_trends.update_layout(height=400, showlegend=True, hovermode="x unified")
401
- st.plotly_chart(fig_trends, use_container_width=True)
402
 
403
  st.markdown("---")
404
 
405
- # Train model
406
- with st.spinner("πŸ€– Training AI model..."):
407
- X = df_feat[feats_num + feats_cat].copy()
408
- y = df_feat[target].copy()
409
- pipe, metrics, X_test = train_model(feats_num, feats_cat, target, X, y)
410
- st.success(f"βœ… Model trained: RΒ² = {metrics['r2']:.3f}, MAE = {metrics['mae']:.4f}")
411
-
412
- # Compute SHAP once for all tabs
413
- with st.spinner("πŸ”¬ Analyzing profitability drivers..."):
414
- shap_df, X_test_sample, feature_names = compute_shap_values(pipe, X_test, feats_num, feats_cat, shap_sample=400)
415
 
416
- # Tabs for different sections
417
- tab1, tab2, tab3 = st.tabs(["πŸ” Key Drivers Analysis", "🎯 Strategic Recommendations", "πŸ§ͺ What-If Simulator"])
418
 
419
  with tab1:
420
- st.markdown("### Understanding What Drives Your Profitability")
421
- st.markdown("""
 
422
  <div class="insight-box">
423
- <b>πŸŽ“ Business Insight:</b> This analysis reveals which business factors and segment combinations have the strongest impact on gross margin.
424
- Understanding these drivers helps prioritize strategic initiatives and operational improvements.
 
425
  </div>
426
  """, unsafe_allow_html=True)
427
 
428
- if shap_df is not None and X_test_sample is not None:
429
- # Calculate mean absolute SHAP
430
- mean_abs = shap_df.abs().mean().sort_values(ascending=False)
431
-
432
- # Map technical names to business names
433
- business_name_map = {
434
- "discount_pct": "Discount Level",
435
- "unit_cost": "Unit Cost",
436
- "net_price": "Net Selling Price",
437
- "list_price": "List Price",
438
- "qty": "Order Quantity",
439
- "roll7_qty": "7-Day Avg Quantity",
440
- "roll7_price": "7-Day Avg Price",
441
- "roll7_cost": "7-Day Avg Cost",
442
- "dow": "Day of Week"
443
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
444
 
445
- # Get top drivers with business names
446
- top_drivers = []
447
- for feat, val in mean_abs.head(10).items():
448
- bus_name = feat
449
- for key, name in business_name_map.items():
450
- if key == feat:
451
- bus_name = name
452
- break
453
- if feat.startswith("cat__"):
454
- parts = feat.replace("cat__", "").replace("product_", "").replace("region_", "").replace("channel_", "")
455
- if "product" in feat.lower():
456
- bus_name = f"Product: {parts}"
457
- elif "region" in feat.lower():
458
- bus_name = f"Region: {parts}"
459
- elif "channel" in feat.lower():
460
- bus_name = f"Channel: {parts}"
461
- top_drivers.append({"Driver": bus_name, "Impact Score": val})
462
-
463
- drivers_df = pd.DataFrame(top_drivers)
464
-
465
- col_a, col_b = st.columns([1, 1])
466
-
467
- with col_a:
468
- st.markdown("#### Top 10 Profitability Drivers")
469
-
470
- fig_drivers = go.Figure()
471
- fig_drivers.add_trace(go.Bar(
472
- y=drivers_df["Driver"][::-1],
473
- x=drivers_df["Impact Score"][::-1],
474
- orientation='h',
475
- marker=dict(
476
- color=drivers_df["Impact Score"][::-1],
477
- colorscale='Blues',
478
- line=dict(color='rgb(8,48,107)', width=1.5)
479
- ),
480
- text=[f"{v:.4f}" for v in drivers_df["Impact Score"][::-1]],
481
- textposition='outside',
482
- ))
483
-
484
- fig_drivers.update_layout(
485
- title="Ranked by Average Impact on Gross Margin",
486
- xaxis_title="Impact Score",
487
- yaxis_title="",
488
- height=500,
489
- showlegend=False
490
- )
491
- st.plotly_chart(fig_drivers, use_container_width=True)
492
-
493
- with col_b:
494
- st.markdown("#### Key Insights")
495
-
496
- top_3 = drivers_df.head(3)
497
 
498
- st.markdown(f"""
499
- <div class="insight-box">
500
- <b>πŸ₯‡ Primary Driver:</b> {top_3.iloc[0]['Driver']}<br>
501
- <small>Impact Score: {top_3.iloc[0]['Impact Score']:.4f}</small>
502
- </div>
503
- """, unsafe_allow_html=True)
 
 
504
 
505
- st.markdown(f"""
506
- <div class="insight-box">
507
- <b>πŸ₯ˆ Secondary Driver:</b> {top_3.iloc[1]['Driver']}<br>
508
- <small>Impact Score: {top_3.iloc[1]['Impact Score']:.4f}</small>
509
- </div>
510
- """, unsafe_allow_html=True)
 
511
 
512
- st.markdown(f"""
513
- <div class="insight-box">
514
- <b>πŸ₯‰ Tertiary Driver:</b> {top_3.iloc[2]['Driver']}<br>
515
- <small>Impact Score: {top_3.iloc[2]['Impact Score']:.4f}</small>
516
- </div>
517
- """, unsafe_allow_html=True)
 
 
518
 
519
- st.markdown("#### Segment Performance Analysis")
520
-
521
- try:
522
- cat_cols = ["product", "region", "channel"]
523
- joined = pd.concat([X_test_sample[cat_cols].reset_index(drop=True),
524
- shap_df.reset_index(drop=True)], axis=1)
525
-
526
- grp = joined.groupby(cat_cols, as_index=False).mean(numeric_only=True)
527
- key_shap_cols = [c for c in shap_df.columns if c in grp.columns]
528
- grp["net_impact"] = grp[key_shap_cols].sum(axis=1)
529
-
530
- top_negative = grp.nsmallest(8, "net_impact")
531
- top_positive = grp.nlargest(8, "net_impact")
532
-
533
- st.markdown("**⚠️ Product-Region-Channel Combinations Reducing Margin:**")
534
- for _, row in top_negative.head(5).iterrows():
535
- st.markdown(f"""
536
- <div class="recommendation-card" style="border-left: 4px solid #dc3545; padding: 0.8rem; margin: 0.5rem 0;">
537
- <b>{row['product']}</b> β€’ {row['region']} β€’ {row['channel']}<br>
538
- <small style="color: #dc3545;">Cumulative Impact: {row['net_impact']:.4f}</small>
539
- </div>
540
- """, unsafe_allow_html=True)
541
-
542
- st.markdown("**βœ… Product-Region-Channel Combinations Boosting Margin:**")
543
- for _, row in top_positive.head(5).iterrows():
544
- st.markdown(f"""
545
- <div class="recommendation-card" style="border-left: 4px solid #28a745; padding: 0.8rem; margin: 0.5rem 0;">
546
- <b>{row['product']}</b> β€’ {row['region']} β€’ {row['channel']}<br>
547
- <small style="color: #28a745;">Cumulative Impact: {row['net_impact']:.4f}</small>
548
- </div>
549
- """, unsafe_allow_html=True)
550
-
551
- # Visualization
552
- st.markdown("---")
553
- st.markdown("#### Segment Impact Visualization")
554
-
555
- fig_segments = px.treemap(
556
- grp,
557
- path=['product', 'region', 'channel'],
558
- values=grp['net_impact'].abs(),
559
- color='net_impact',
560
- color_continuous_scale='RdYlGn',
561
- title="Product-Region-Channel Combinations Impact on Margin"
562
- )
563
- fig_segments.update_layout(height=500)
564
- st.plotly_chart(fig_segments, use_container_width=True)
565
-
566
- except Exception as e:
567
- st.warning(f"Unable to compute detailed segment analysis: {str(e)}")
568
- else:
569
- st.error("Unable to compute driver analysis. Please check your data.")
570
 
571
  with tab2:
572
- st.markdown("### AI-Generated Strategic Recommendations")
573
  st.markdown("""
574
  <div class="insight-box">
575
- <b>πŸ’‘ How This Works:</b> The AI identifies segments with margin pressure and suggests specific pricing actions
576
- to improve profitability. Recommendations are ranked by expected financial impact.
577
  </div>
578
  """, unsafe_allow_html=True)
579
 
580
- if shap_df is not None and X_test_sample is not None:
581
- with st.spinner("🧠 Generating strategic recommendations..."):
582
- try:
583
- joined = pd.concat([X_test_sample.reset_index(drop=True), shap_df.reset_index(drop=True)], axis=1)
584
- joined["key"] = joined["product"] + "|" + joined["region"] + "|" + joined["channel"]
585
-
586
- cand_cols = [c for c in joined.columns if ("discount" in c.lower() or "cost" in c.lower() or "price" in c.lower()) and c in shap_df.columns]
587
- seg_scores = joined.groupby("key")[cand_cols].mean().sum(axis=1)
588
- worst_keys = seg_scores.sort_values().head(15).index.tolist()
589
-
590
- recs = []
591
- for key in worst_keys:
592
- p, r, c = key.split("|")
593
- hist = df[(df["product"]==p)&(df["region"]==r)&(df["channel"]==c)].sort_values("date")
594
- if hist.empty or len(hist) < 50:
595
- continue
596
-
597
- eps, _ = estimate_segment_elasticity(hist, p, r, c)
598
- prop_disc_pts = np.clip(abs(seg_scores[key])*10, 1.0, 3.0)
599
- sim = simulate_pricing_action(hist, eps, prop_disc_pts)
600
-
601
- if sim is None or sim["gm_delta_value"] <= 0:
602
- continue
603
-
604
- daily_transactions = len(hist) / ((hist["date"].max() - hist["date"].min()).days + 1)
605
- annual_impact = sim["gm_delta_value"] * daily_transactions * 365
606
-
607
- recs.append({
608
- "Segment": f"{p}",
609
- "Region": r,
610
- "Channel": c,
611
- "Current Discount": f"{sim['baseline_discount']:.1f}%",
612
- "Recommended Discount": f"{sim['new_discount']:.1f}%",
613
- "Expected GM Uplift": sim["gm_delta_value"],
614
- "Annual Impact Estimate": annual_impact,
615
- "Current GM%": sim["gm0_pct"]*100,
616
- "Projected GM%": sim["gm1_pct"]*100
617
- })
618
-
619
- recs_df = pd.DataFrame(recs).sort_values("Expected GM Uplift", ascending=False)
620
-
621
- if len(recs_df) > 0:
622
- st.markdown("#### πŸ† Top 3 Priority Actions")
623
-
624
- for i, (idx, rec) in enumerate(recs_df.head(3).iterrows()):
625
- st.markdown(f"""
626
- <div class="recommendation-card">
627
- <h4>#{i+1}: {rec['Segment']} β€’ {rec['Region']} β€’ {rec['Channel']}</h4>
628
- <p style="font-size: 1.1rem; margin: 0.5rem 0;">
629
- <b>Recommended Action:</b> Reduce discount from <b>{rec['Current Discount']}</b> to <b>{rec['Recommended Discount']}</b>
630
- </p>
631
- <p style="font-size: 1rem; color: #666; margin: 0.5rem 0;">
632
- Current GM: <b>{rec['Current GM%']:.1f}%</b> β†’ Projected GM: <b style="color: #28a745;">{rec['Projected GM%']:.1f}%</b>
633
- </p>
634
- <p class="positive-impact">
635
- πŸ’° Expected Daily Impact: ${rec['Expected GM Uplift']:.2f}
636
- </p>
637
- <p style="font-size: 0.95rem; color: #666;">
638
- πŸ“Š Estimated Annual Impact: <b>${rec['Annual Impact Estimate']/1e3:.1f}K</b>
639
- </p>
640
- </div>
641
- """, unsafe_allow_html=True)
642
-
643
- st.markdown("---")
644
- st.markdown("#### πŸ“‹ Complete Recommendations List")
645
- st.dataframe(recs_df, use_container_width=True, height=400)
646
-
647
- total_daily_impact = recs_df["Expected GM Uplift"].sum()
648
- total_annual_impact = recs_df["Annual Impact Estimate"].sum()
649
-
650
- st.markdown(f"""
651
- <div class="insight-box" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none;">
652
- <h3 style="color: white; margin-top: 0;">πŸ’Ž Total Opportunity</h3>
653
- <p style="font-size: 1.3rem; margin: 0.5rem 0;">
654
- <b>Daily GM Impact:</b> ${total_daily_impact:.2f}
655
- </p>
656
- <p style="font-size: 1.6rem; margin: 0.5rem 0;">
657
- <b>Estimated Annual Impact:</b> ${total_annual_impact/1e6:.2f}M
658
- </p>
659
- </div>
660
- """, unsafe_allow_html=True)
661
- else:
662
- st.info("No significant optimization opportunities detected in current data.")
663
- except Exception as e:
664
- st.error(f"Error generating recommendations: {str(e)}")
665
- else:
666
- st.error("Unable to generate recommendations. Please check your data.")
667
 
668
  with tab3:
669
- st.markdown("### Custom What-If Analysis")
670
  st.markdown("""
671
  <div class="insight-box">
672
- <b>πŸ§ͺ Interactive Simulation:</b> Test different pricing strategies for specific segments to understand
673
- the potential impact on revenue, volume, and profitability.
674
  </div>
675
  """, unsafe_allow_html=True)
676
 
677
- last_day = df["date"].max()
678
- seg_today = df[df["date"]==last_day][["product","region","channel"]].drop_duplicates()
679
-
680
- col_sim1, col_sim2, col_sim3 = st.columns(3)
681
-
682
- with col_sim1:
683
- selected_product = st.selectbox("πŸ“¦ Select Product", sorted(seg_today["product"].unique()))
684
- with col_sim2:
685
- selected_region = st.selectbox("🌍 Select Region", sorted(seg_today["region"].unique()))
686
- with col_sim3:
687
- selected_channel = st.selectbox("πŸ›’ Select Channel", sorted(seg_today["channel"].unique()))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
688
 
689
- seg_hist = df[
690
- (df["product"]==selected_product) &
691
- (df["region"]==selected_region) &
692
- (df["channel"]==selected_channel)
693
- ].sort_values("date")
694
 
695
- if not seg_hist.empty and len(seg_hist) >= 50:
696
- elasticity, _ = estimate_segment_elasticity(seg_hist, selected_product, selected_region, selected_channel)
697
- current = seg_hist.iloc[-1]
698
 
699
- st.markdown(f"""
700
- <div class="insight-box">
701
- <b>πŸ“Š Current State for {selected_product} β€’ {selected_region} β€’ {selected_channel}:</b><br>
702
- β€’ Current Discount: <b>{current['discount_pct']*100:.1f}%</b><br>
703
- β€’ Net Price: <b>${current['net_price']:.2f}</b><br>
704
- β€’ Unit Cost: <b>${current['unit_cost']:.2f}</b><br>
705
- β€’ Avg Daily Volume: <b>{seg_hist.tail(7)['qty'].mean():.0f} units</b><br>
706
- β€’ Current GM%: <b>{current['gm_pct']*100:.1f}%</b><br>
707
- β€’ Price Elasticity: <b>{elasticity:.2f}</b>
708
- </div>
709
- """, unsafe_allow_html=True)
710
 
711
- st.markdown("#### 🎯 Test Pricing Strategy")
 
 
712
 
713
- discount_change = st.slider(
714
- "Adjust Discount Level (percentage points)",
715
- min_value=-10.0,
716
- max_value=5.0,
717
- value=0.0,
718
- step=0.5,
719
- help="Negative values reduce discount (increase price)"
720
  )
721
-
722
- if discount_change != 0:
723
- sim = simulate_pricing_action(seg_hist, elasticity, -discount_change)
724
-
725
- if sim:
726
- col_res1, col_res2 = st.columns(2)
727
-
728
- with col_res1:
729
- comparison_data = pd.DataFrame({
730
- 'Metric': ['Price', 'Volume', 'GM%'],
731
- 'Current': [sim['baseline_price'], sim['baseline_qty'], sim['gm0_pct']*100],
732
- 'Projected': [sim['new_price'], sim['new_qty'], sim['gm1_pct']*100]
733
- })
734
-
735
- fig_comp = go.Figure()
736
- fig_comp.add_trace(go.Bar(
737
- name='Current',
738
- x=comparison_data['Metric'],
739
- y=comparison_data['Current'],
740
- marker_color='#94a3b8'
741
- ))
742
- fig_comp.add_trace(go.Bar(
743
- name='Projected',
744
- x=comparison_data['Metric'],
745
- y=comparison_data['Projected'],
746
- marker_color='#3b82f6'
747
- ))
748
-
749
- fig_comp.update_layout(
750
- title="Current vs. Projected Performance",
751
- barmode='group',
752
- height=350
753
- )
754
- st.plotly_chart(fig_comp, use_container_width=True)
755
-
756
- with col_res2:
757
- st.markdown("#### πŸ“ˆ Simulation Results")
758
-
759
- gm_change = sim['gm1_pct'] - sim['gm0_pct']
760
-
761
- st.metric(
762
- "Gross Margin Impact",
763
- f"{sim['gm1_pct']*100:.1f}%",
764
- f"{gm_change*100:+.1f}pp"
765
- )
766
-
767
- st.metric(
768
- "Revenue Impact",
769
- f"${sim['new_price'] * sim['new_qty']:.2f}",
770
- f"${sim['revenue_delta']:+.2f}"
771
- )
772
-
773
- vol_change = sim['new_qty'] - sim['baseline_qty']
774
- st.metric(
775
- "Volume Impact",
776
- f"{sim['new_qty']:.0f} units",
777
- f"{vol_change:+.0f} units"
778
- )
779
-
780
- st.markdown(f"""
781
- <div class="insight-box" style="margin-top: 1rem;">
782
- <b>πŸ’° Daily P&L Impact:</b><br>
783
- <span style="font-size: 1.5rem; {'color: #28a745' if sim['gm_delta_value'] > 0 else 'color: #dc3545'}">
784
- ${sim['gm_delta_value']:+.2f}
785
- </span>
786
- </div>
787
- """, unsafe_allow_html=True)
788
- else:
789
- st.info("πŸ‘† Adjust the discount slider above to simulate different pricing strategies")
790
  else:
791
- st.warning("⚠️ Insufficient data for selected segment. Please choose a different combination.")
792
 
793
  st.markdown("---")
794
  st.markdown("""
795
- <div style="text-align: center; color: #666; padding: 2rem 0;">
796
- <small>
797
- πŸ”’ Demo Mode: Using synthetic SAP-style data for illustration purposes
798
- </small>
799
  </div>
800
  """, unsafe_allow_html=True)
 
4
  import plotly.express as px
5
  import plotly.graph_objects as go
6
  from plotly.subplots import make_subplots
 
 
7
  from datetime import datetime, timedelta
8
  from sklearn.model_selection import train_test_split
9
  from sklearn.compose import ColumnTransformer
 
15
  import warnings
16
  warnings.filterwarnings('ignore')
17
 
 
18
  st.set_page_config(
19
  page_title="Profitability Intelligence Suite",
20
  page_icon="πŸ“Š",
 
22
  initial_sidebar_state="collapsed"
23
  )
24
 
25
+ # Custom CSS
26
  st.markdown("""
27
  <style>
28
  .main-header {
 
31
  color: #1f77b4;
32
  text-align: center;
33
  margin-bottom: 0.5rem;
 
34
  }
35
  .sub-header {
36
  font-size: 1.2rem;
 
52
  padding: 1.5rem;
53
  margin: 1rem 0;
54
  border-radius: 8px;
 
55
  }
56
  .recommendation-card {
57
  background: white;
 
60
  padding: 1.5rem;
61
  margin: 1rem 0;
62
  box-shadow: 0 4px 12px rgba(0,0,0,0.08);
 
 
 
 
 
63
  }
64
  .positive-impact {
65
  color: #28a745;
66
  font-weight: 700;
67
  font-size: 1.5rem;
68
  }
69
+ .negative-impact {
70
+ color: #dc3545;
71
+ font-weight: 700;
72
+ font-size: 1.5rem;
 
 
 
73
  }
74
  </style>
75
  """, unsafe_allow_html=True)
76
 
 
 
 
77
  @st.cache_data(show_spinner=False)
78
  def generate_synthetic_data(days=60, seed=42, rows_per_day=600):
79
  rng = np.random.default_rng(seed)
 
155
  df = pd.DataFrame(records)
156
  return df
157
 
158
+ def analyze_margin_bridge(df, current_date, prior_date):
159
+ """
160
+ Professional Price-Volume-Mix (PVM) analysis following FP&A best practices
161
+ Breaks down GM variance into: Price Effect, Volume Effect, Mix Effect, Cost Effect
162
+ """
163
+ current_data = df[df["date"] == current_date].copy()
164
+ prior_data = df[df["date"] == prior_date].copy()
165
+
166
+ # Calculate totals for both periods
167
+ current_total_revenue = current_data["revenue"].sum()
168
+ current_total_cogs = current_data["cogs"].sum()
169
+ current_total_gm = current_total_revenue - current_total_cogs
170
+ current_gm_pct = current_total_gm / current_total_revenue if current_total_revenue > 0 else 0
171
+
172
+ prior_total_revenue = prior_data["revenue"].sum()
173
+ prior_total_cogs = prior_data["cogs"].sum()
174
+ prior_total_gm = prior_total_revenue - prior_total_cogs
175
+ prior_gm_pct = prior_total_gm / prior_total_revenue if prior_total_revenue > 0 else 0
176
+
177
+ total_gm_variance = current_total_gm - prior_total_gm
178
+
179
+ # Aggregate by segment
180
+ current_seg = current_data.groupby(["product", "region", "channel"]).agg({
181
+ "revenue": "sum",
182
+ "cogs": "sum",
183
+ "qty": "sum",
184
+ "net_price": "mean",
185
+ "unit_cost": "mean"
186
+ }).reset_index()
187
+ current_seg["gm"] = current_seg["revenue"] - current_seg["cogs"]
188
+ current_seg["gm_pct"] = current_seg["gm"] / current_seg["revenue"]
189
+
190
+ prior_seg = prior_data.groupby(["product", "region", "channel"]).agg({
191
+ "revenue": "sum",
192
+ "cogs": "sum",
193
+ "qty": "sum",
194
+ "net_price": "mean",
195
+ "unit_cost": "mean"
196
+ }).reset_index()
197
+ prior_seg["gm"] = prior_seg["revenue"] - prior_seg["cogs"]
198
+ prior_seg["gm_pct"] = prior_seg["gm"] / prior_seg["revenue"]
199
+
200
+ # Merge segments
201
+ merged = pd.merge(
202
+ current_seg,
203
+ prior_seg,
204
+ on=["product", "region", "channel"],
205
+ suffixes=("_curr", "_prior"),
206
+ how="outer"
207
+ ).fillna(0)
208
+
209
+ # Price-Volume-Mix Decomposition (industry standard method)
210
+ # Price Effect: (Current Price - Prior Price) Γ— Current Volume
211
+ merged["price_effect"] = (merged["net_price_curr"] - merged["net_price_prior"]) * merged["qty_curr"]
212
+
213
+ # Volume Effect: (Current Volume - Prior Volume) Γ— Prior Price Γ— Prior GM%
214
+ merged["volume_effect"] = (merged["qty_curr"] - merged["qty_prior"]) * merged["net_price_prior"] * merged["gm_pct_prior"]
215
+
216
+ # Cost Effect: -(Current Cost - Prior Cost) Γ— Current Volume
217
+ merged["cost_effect"] = -(merged["unit_cost_curr"] - merged["unit_cost_prior"]) * merged["qty_curr"]
218
+
219
+ # Mix Effect: Residual (actual GM change minus price/volume/cost effects)
220
+ merged["gm_variance"] = merged["gm_curr"] - merged["gm_prior"]
221
+ merged["mix_effect"] = merged["gm_variance"] - (merged["price_effect"] + merged["volume_effect"] + merged["cost_effect"])
222
+
223
+ return merged, {
224
+ "total_gm_variance": total_gm_variance,
225
+ "price_effect_total": merged["price_effect"].sum(),
226
+ "volume_effect_total": merged["volume_effect"].sum(),
227
+ "cost_effect_total": merged["cost_effect"].sum(),
228
+ "mix_effect_total": merged["mix_effect"].sum(),
229
+ "current_gm": current_total_gm,
230
+ "prior_gm": prior_total_gm,
231
+ "current_gm_pct": current_gm_pct,
232
+ "prior_gm_pct": prior_gm_pct
233
+ }
234
 
235
  def estimate_segment_elasticity(df, product, region, channel):
236
  seg_df = df[(df["product"]==product)&(df["region"]==region)&(df["channel"]==channel)]
 
284
  except:
285
  return None
286
 
 
287
  # Main App
288
+ st.markdown('<h1 class="main-header">🎯 Daily Profitability Variance Analysis</h1>', unsafe_allow_html=True)
289
+ st.markdown('<p class="sub-header">Understanding What Drives Daily Margin Changes</p>', unsafe_allow_html=True)
 
 
290
 
291
  # Generate data
292
  with st.spinner("πŸ”„ Loading business data..."):
293
  df = generate_synthetic_data(days=60, seed=42, rows_per_day=600)
 
294
 
295
+ # Calculate daily aggregates
296
  daily = df.groupby("date").agg(
297
  revenue=("revenue","sum"),
298
  cogs=("cogs","sum"),
299
+ gm_value=("gm_value","sum"),
300
+ qty=("qty","sum")
301
  ).reset_index()
302
  daily["gm_pct"] = np.where(daily["revenue"]>0, daily["gm_value"]/daily["revenue"], 0.0)
303
 
304
+ current_date = daily["date"].max()
305
+ prior_date = current_date - timedelta(days=1)
306
+ current_row = daily[daily["date"]==current_date].iloc[0]
307
+ prior_row = daily[daily["date"]==prior_date].iloc[0]
308
+ week_ago_row = daily.iloc[-8] if len(daily) > 7 else current_row
309
  roll7 = daily["gm_pct"].tail(7).mean()
310
 
311
+ gm_variance_pp = (current_row["gm_pct"] - prior_row["gm_pct"]) * 100
312
+ gm_variance_dollar = current_row["gm_value"] - prior_row["gm_value"]
313
+
314
+ # Executive Dashboard
315
+ st.markdown("### πŸ“Š Executive Summary")
316
 
317
  col1, col2, col3, col4 = st.columns(4)
318
 
319
  with col1:
 
320
  st.metric(
321
  label="Gross Margin %",
322
+ value=f"{current_row['gm_pct']*100:.2f}%",
323
+ delta=f"{gm_variance_pp:+.2f}pp",
324
  delta_color="normal"
325
  )
326
 
327
  with col2:
 
328
  st.metric(
329
+ label="Gross Margin $",
330
+ value=f"${current_row['gm_value']/1e6:.2f}M",
331
+ delta=f"${gm_variance_dollar/1e6:+.2f}M",
332
  delta_color="normal"
333
  )
334
 
335
  with col3:
336
+ revenue_var_pct = ((current_row["revenue"] - prior_row["revenue"]) / prior_row["revenue"] * 100) if prior_row["revenue"] > 0 else 0
337
  st.metric(
338
+ label="Revenue",
339
+ value=f"${current_row['revenue']/1e6:.2f}M",
340
+ delta=f"{revenue_var_pct:+.1f}%",
341
  delta_color="normal"
342
  )
343
 
344
  with col4:
345
+ volume_var_pct = ((current_row["qty"] - prior_row["qty"]) / prior_row["qty"] * 100) if prior_row["qty"] > 0 else 0
346
  st.metric(
347
+ label="Volume (Units)",
348
+ value=f"{current_row['qty']:,.0f}",
349
+ delta=f"{volume_var_pct:+.1f}%",
350
  delta_color="normal"
351
  )
352
 
353
+ # Trend chart
354
+ st.markdown("#### πŸ“ˆ Gross Margin Trend (Last 30 Days)")
355
+ recent_daily = daily.tail(30)
356
+
357
+ fig_trend = go.Figure()
358
+ fig_trend.add_trace(go.Scatter(
359
+ x=recent_daily["date"],
360
+ y=recent_daily["gm_pct"]*100,
361
+ mode='lines+markers',
362
+ name="GM%",
363
+ line=dict(color="#1f77b4", width=3),
364
+ fill='tozeroy',
365
+ fillcolor="rgba(31, 119, 180, 0.1)"
366
+ ))
367
+ fig_trend.add_hline(y=roll7*100, line_dash="dash", line_color="red",
368
+ annotation_text="7-Day Average", annotation_position="right")
369
+ fig_trend.update_layout(
370
+ xaxis_title="Date",
371
+ yaxis_title="Gross Margin %",
372
+ height=350,
373
+ hovermode="x unified"
 
 
 
 
 
 
 
 
374
  )
375
+ st.plotly_chart(fig_trend, use_container_width=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
376
 
377
  st.markdown("---")
378
 
379
+ # Perform margin bridge analysis
380
+ with st.spinner("πŸ”¬ Performing Price-Volume-Mix analysis..."):
381
+ variance_detail, summary = analyze_margin_bridge(df, current_date, prior_date)
 
 
 
 
 
 
 
382
 
383
+ # Main Analysis Tabs
384
+ tab1, tab2, tab3 = st.tabs(["πŸ“Š Margin Bridge (PVM)", "πŸ” Segment Deep Dive", "πŸ’‘ Pricing Opportunities"])
385
 
386
  with tab1:
387
+ st.markdown(f"### Gross Margin Bridge: {prior_date.strftime('%b %d')} β†’ {current_date.strftime('%b %d')}")
388
+
389
+ st.markdown(f"""
390
  <div class="insight-box">
391
+ <b>πŸ“‹ Variance Summary:</b><br>
392
+ Gross margin changed by <b>${gm_variance_dollar/1000:+.1f}K</b> ({gm_variance_pp:+.2f} percentage points)<br>
393
+ from {prior_row['gm_pct']*100:.2f}% to {current_row['gm_pct']*100:.2f}%
394
  </div>
395
  """, unsafe_allow_html=True)
396
 
397
+ # Waterfall Chart - Professional PVM Analysis
398
+ st.markdown("#### Price-Volume-Mix (PVM) Waterfall Analysis")
399
+
400
+ waterfall_data = pd.DataFrame({
401
+ "Category": [
402
+ f"{prior_date.strftime('%b %d')}<br>Gross Margin",
403
+ "Price<br>Effect",
404
+ "Volume<br>Effect",
405
+ "Cost<br>Effect",
406
+ "Mix<br>Effect",
407
+ f"{current_date.strftime('%b %d')}<br>Gross Margin"
408
+ ],
409
+ "Value": [
410
+ summary["prior_gm"],
411
+ summary["price_effect_total"],
412
+ summary["volume_effect_total"],
413
+ summary["cost_effect_total"],
414
+ summary["mix_effect_total"],
415
+ summary["current_gm"]
416
+ ],
417
+ "Type": ["absolute", "relative", "relative", "relative", "relative", "total"]
418
+ })
419
+
420
+ fig_waterfall = go.Figure(go.Waterfall(
421
+ orientation="v",
422
+ measure=waterfall_data["Type"],
423
+ x=waterfall_data["Category"],
424
+ y=waterfall_data["Value"],
425
+ text=[f"${v/1000:.1f}K" if abs(v) > 100 else f"${v:.0f}" for v in waterfall_data["Value"]],
426
+ textposition="outside",
427
+ connector={"line": {"color": "rgb(63, 63, 63)"}},
428
+ increasing={"marker": {"color": "#28a745"}},
429
+ decreasing={"marker": {"color": "#dc3545"}},
430
+ totals={"marker": {"color": "#1f77b4"}}
431
+ ))
432
+
433
+ fig_waterfall.update_layout(
434
+ title="Gross Margin Variance Breakdown",
435
+ showlegend=False,
436
+ height=450,
437
+ yaxis_title="Gross Margin ($)"
438
+ )
439
+ st.plotly_chart(fig_waterfall, use_container_width=True)
440
 
441
+ # Explanation of each component
442
+ col_exp1, col_exp2 = st.columns(2)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
443
 
444
+ with col_exp1:
445
+ st.markdown(f"""
446
+ <div class="insight-box">
447
+ <b>πŸ’° Price Effect:</b> ${summary['price_effect_total']/1000:+.1f}K<br>
448
+ <small>Impact of changes in average selling prices across all transactions.
449
+ Positive = higher prices captured, Negative = price erosion or higher discounts.</small>
450
+ </div>
451
+ """, unsafe_allow_html=True)
452
 
453
+ st.markdown(f"""
454
+ <div class="insight-box">
455
+ <b>πŸ“¦ Volume Effect:</b> ${summary['volume_effect_total']/1000:+.1f}K<br>
456
+ <small>Impact of selling more or fewer units at prior period margins.
457
+ Positive = higher volumes, Negative = volume decline.</small>
458
+ </div>
459
+ """, unsafe_allow_html=True)
460
 
461
+ with col_exp2:
462
+ st.markdown(f"""
463
+ <div class="insight-box">
464
+ <b>🏭 Cost Effect:</b> ${summary['cost_effect_total']/1000:+.1f}K<br>
465
+ <small>Impact of changes in unit costs (COGS).
466
+ Positive = cost reduction, Negative = cost inflation.</small>
467
+ </div>
468
+ """, unsafe_allow_html=True)
469
 
470
+ st.markdown(f"""
471
+ <div class="insight-box">
472
+ <b>πŸ”€ Mix Effect:</b> ${summary['mix_effect_total']/1000:+.1f}K<br>
473
+ <small>Impact of shifts in product, channel, or customer mix.
474
+ Reflects selling relatively more/less of high-margin items.</small>
475
+ </div>
476
+ """, unsafe_allow_html=True)
477
+
478
+ # Key Insight
479
+ dominant_effect = max([
480
+ ("Price changes", summary['price_effect_total']),
481
+ ("Volume changes", summary['volume_effect_total']),
482
+ ("Cost changes", summary['cost_effect_total']),
483
+ ("Mix shifts", summary['mix_effect_total'])
484
+ ], key=lambda x: abs(x[1]))
485
+
486
+ st.markdown(f"""
487
+ <div class="{'insight-box' if gm_variance_dollar > 0 else 'warning-box'}">
488
+ <b>🎯 Key Takeaway:</b><br>
489
+ The primary driver of today's margin {'improvement' if gm_variance_dollar > 0 else 'decline'} was
490
+ <b>{dominant_effect[0]}</b>, contributing ${dominant_effect[1]/1000:+.1f}K to the overall variance.
491
+ </div>
492
+ """, unsafe_allow_html=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
493
 
494
  with tab2:
495
+ st.markdown("### Segment-Level Variance Analysis")
496
  st.markdown("""
497
  <div class="insight-box">
498
+ <b>πŸ” Detailed Breakdown:</b> Which specific product-region-channel combinations drove the margin change?
 
499
  </div>
500
  """, unsafe_allow_html=True)
501
 
502
+ # Top positive and negative contributors
503
+ variance_detail_sorted = variance_detail.sort_values("gm_variance", ascending=False)
504
+
505
+ col_seg1, col_seg2 = st.columns(2)
506
+
507
+ with col_seg1:
508
+ st.markdown("#### πŸ“ˆ Top 5 Margin Gainers")
509
+ top_gainers = variance_detail_sorted.head(5)
510
+
511
+ for idx, row in top_gainers.iterrows():
512
+ if row["gm_variance"] > 0:
513
+ st.markdown(f"""
514
+ <div class="recommendation-card" style="border-left: 4px solid #28a745;">
515
+ <b>{row['product']}</b><br>
516
+ <small>{row['region']} β€’ {row['channel']}</small><br>
517
+ <span class="positive-impact">+${row['gm_variance']:.2f}</span><br>
518
+ <small>
519
+ β€’ Price Effect: ${row['price_effect']:+.2f}<br>
520
+ β€’ Volume Effect: ${row['volume_effect']:+.2f}<br>
521
+ β€’ Cost Effect: ${row['cost_effect']:+.2f}<br>
522
+ β€’ Mix Effect: ${row['mix_effect']:+.2f}
523
+ </small>
524
+ </div>
525
+ """, unsafe_allow_html=True)
526
+
527
+ with col_seg2:
528
+ st.markdown("#### πŸ“‰ Top 5 Margin Losers")
529
+ top_losers = variance_detail_sorted.tail(5)
530
+
531
+ for idx, row in top_losers.iterrows():
532
+ if row["gm_variance"] < 0:
533
+ st.markdown(f"""
534
+ <div class="recommendation-card" style="border-left: 4px solid #dc3545;">
535
+ <b>{row['product']}</b><br>
536
+ <small>{row['region']} β€’ {row['channel']}</small><br>
537
+ <span class="negative-impact">${row['gm_variance']:.2f}</span><br>
538
+ <small>
539
+ β€’ Price Effect: ${row['price_effect']:+.2f}<br>
540
+ β€’ Volume Effect: ${row['volume_effect']:+.2f}<br>
541
+ β€’ Cost Effect: ${row['cost_effect']:+.2f}<br>
542
+ β€’ Mix Effect: ${row['mix_effect']:+.2f}
543
+ </small>
544
+ </div>
545
+ """, unsafe_allow_html=True)
546
+
547
+ # Detailed table
548
+ st.markdown("---")
549
+ st.markdown("#### Complete Segment Variance Table")
550
+
551
+ display_variance = variance_detail[[
552
+ "product", "region", "channel", "gm_variance",
553
+ "price_effect", "volume_effect", "cost_effect", "mix_effect"
554
+ ]].sort_values("gm_variance", ascending=False)
555
+
556
+ display_variance.columns = [
557
+ "Product", "Region", "Channel", "GM Variance",
558
+ "Price Effect", "Volume Effect", "Cost Effect", "Mix Effect"
559
+ ]
560
+
561
+ st.dataframe(display_variance.style.format({
562
+ "GM Variance": "${:,.2f}",
563
+ "Price Effect": "${:,.2f}",
564
+ "Volume Effect": "${:,.2f}",
565
+ "Cost Effect": "${:,.2f}",
566
+ "Mix Effect": "${:,.2f}"
567
+ }).background_gradient(subset=["GM Variance"], cmap="RdYlGn", vmin=-1000, vmax=1000),
568
+ use_container_width=True, height=400)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
569
 
570
  with tab3:
571
+ st.markdown("### Pricing Optimization Opportunities")
572
  st.markdown("""
573
  <div class="insight-box">
574
+ <b>πŸ’‘ AI Recommendations:</b> Based on segments with declining margins, here are pricing actions to consider.
 
575
  </div>
576
  """, unsafe_allow_html=True)
577
 
578
+ # Focus on segments with negative GM variance and negative price effects
579
+ problem_segments = variance_detail[
580
+ (variance_detail["gm_variance"] < -50) |
581
+ (variance_detail["price_effect"] < -50)
582
+ ].copy()
583
+ problem_segments["priority_score"] = problem_segments["gm_variance"]
584
+ problem_segments = problem_segments.sort_values("priority_score")
585
+
586
+ recs = []
587
+ for _, seg in problem_segments.head(15).iterrows():
588
+ p, r, c = seg["product"], seg["region"], seg["channel"]
589
+ hist = df[(df["product"]==p)&(df["region"]==r)&(df["channel"]==c)].sort_values("date")
590
+
591
+ if hist.empty or len(hist) < 50:
592
+ continue
593
+
594
+ eps, _ = estimate_segment_elasticity(hist, p, r, c)
595
+ discount_reduction = 2.0 # Standard 2pp reduction
596
+ sim = simulate_pricing_action(hist, eps, discount_reduction)
597
+
598
+ if sim and sim["gm_delta_value"] > 0:
599
+ daily_txns = len(hist) / ((hist["date"].max() - hist["date"].min()).days + 1)
600
+ annual_impact = sim["gm_delta_value"] * daily_txns * 365
601
+
602
+ recs.append({
603
+ "Segment": p,
604
+ "Region": r,
605
+ "Channel": c,
606
+ "Yesterday GM Loss": seg["gm_variance"],
607
+ "Root Cause": "Price erosion" if seg["price_effect"] < -30 else "Volume decline" if seg["volume_effect"] < -30 else "Cost increase",
608
+ "Recommended Action": f"Reduce discount from {sim['baseline_discount']:.1f}% to {sim['new_discount']:.1f}%",
609
+ "Expected Daily GM Uplift": sim["gm_delta_value"],
610
+ "Estimated Annual Impact": annual_impact
611
+ })
612
 
613
+ recs_df = pd.DataFrame(recs).sort_values("Expected Daily GM Uplift", ascending=False)
 
 
 
 
614
 
615
+ if len(recs_df) > 0:
616
+ st.markdown("#### πŸ† Top 3 Priority Actions")
 
617
 
618
+ for i, (_, rec) in enumerate(recs_df.head(3).iterrows()):
619
+ st.markdown(f"""
620
+ <div class="recommendation-card">
621
+ <h4>#{i+1}: {rec['Segment']} β€’ {rec['Region']} β€’ {rec['Channel']}</h4>
622
+ <p><b>Yesterday's Performance:</b> Lost ${abs(rec['Yesterday GM Loss']):.2f} in gross margin</p>
623
+ <p><b>Root Cause:</b> {rec['Root Cause']}</p>
624
+ <p><b>Recommended Action:</b> {rec['Recommended Action']}</p>
625
+ <p class="positive-impact">πŸ’° Expected Daily Recovery: ${rec['Expected Daily GM Uplift']:.2f}</p>
626
+ <p><small>πŸ“Š Annual Impact Estimate: ${rec['Estimated Annual Impact']/1e3:.1f}K</small></p>
627
+ </div>
628
+ """, unsafe_allow_html=True)
629
 
630
+ st.markdown("---")
631
+ st.markdown("#### Complete Action Plan")
632
+ st.dataframe(recs_df, use_container_width=True)
633
 
634
+ st.download_button(
635
+ label="πŸ“₯ Download Recommendations (CSV)",
636
+ data=recs_df.to_csv(index=False).encode("utf-8"),
637
+ file_name=f"margin_recovery_plan_{current_date.strftime('%Y%m%d')}.csv",
638
+ mime="text/csv"
 
 
639
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
640
  else:
641
+ st.success("βœ… All segments performing well. No immediate pricing interventions needed.")
642
 
643
  st.markdown("---")
644
  st.markdown("""
645
+ <div style="text-align: center; color: #666; padding: 1rem;">
646
+ <small>πŸ”’ Demo Mode: Using synthetic SAP-style transaction data for illustration</small>
 
 
647
  </div>
648
  """, unsafe_allow_html=True)