PD03 commited on
Commit
7e0ae49
Β·
verified Β·
1 Parent(s): 4a1e4c5

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +671 -105
app.py CHANGED
@@ -1,10 +1,12 @@
 
1
  import streamlit as st
2
  import numpy as np
3
  import pandas as pd
4
  import plotly.express as px
 
 
5
  import shap
6
  import matplotlib.pyplot as plt
7
-
8
  from datetime import datetime, timedelta
9
  from sklearn.model_selection import train_test_split
10
  from sklearn.compose import ColumnTransformer
@@ -14,38 +16,108 @@ from sklearn.ensemble import RandomForestRegressor
14
  from sklearn.linear_model import LinearRegression
15
  from sklearn.metrics import r2_score, mean_absolute_error
16
 
17
- st.set_page_config(page_title="AI-Driven Daily Gross Margin", layout="wide")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
 
19
  # -----------------------------
20
- # 1) Synthetic data generation
21
  # -----------------------------
22
  @st.cache_data(show_spinner=False)
23
  def generate_synthetic_data(days=60, seed=42, rows_per_day=600):
24
  rng = np.random.default_rng(seed)
25
  start_date = datetime.today().date() - timedelta(days=days)
26
  dates = pd.date_range(start_date, periods=days, freq="D")
 
 
 
27
 
28
- products = ["A", "B", "C", "D"]
29
- regions = ["AMER", "EMEA", "APAC"]
30
- channels = ["Direct", "Distributor", "Online"]
31
-
32
- base_price = {"A": 120, "B": 135, "C": 110, "D": 150}
33
- base_cost = {"A": 70, "B": 88, "C": 60, "D": 95}
34
-
35
- region_price_bump = {"AMER": 1.00, "EMEA": 1.03, "APAC": 0.97}
36
- region_cost_bump = {"AMER": 1.00, "EMEA": 1.02, "APAC": 1.01}
37
-
38
- channel_discount_mean = {"Direct": 0.06, "Distributor": 0.12, "Online": 0.04}
39
- channel_discount_std = {"Direct": 0.02, "Distributor": 0.03, "Online": 0.02}
40
 
41
  seg_epsilon = {}
42
  for p in products:
43
  for r in regions:
44
  for c in channels:
45
  base_eps = rng.uniform(-0.9, -0.25)
46
- if c == "Distributor":
47
  base_eps -= rng.uniform(0.1, 0.3)
48
- if c == "Online":
49
  base_eps += rng.uniform(0.05, 0.15)
50
  seg_epsilon[(p, r, c)] = base_eps
51
 
@@ -57,16 +129,17 @@ def generate_synthetic_data(days=60, seed=42, rows_per_day=600):
57
 
58
  n = rows_per_day
59
  prod = rng.choice(products, size=n, p=[0.35, 0.3, 0.2, 0.15])
60
- reg = rng.choice(regions, size=n, p=[0.4, 0.35, 0.25])
61
- ch = rng.choice(channels, size=n, p=[0.45, 0.35, 0.20])
62
 
63
  base_p = np.array([base_price[x] for x in prod]) * np.array([region_price_bump[x] for x in reg])
64
- base_c = np.array([base_cost[x] for x in prod]) * np.array([region_cost_bump[x] for x in reg])
65
 
66
  discount = np.clip(
67
  np.array([channel_discount_mean[x] for x in ch]) +
68
  rng.normal(0, [channel_discount_std[x] for x in ch]), 0, 0.45
69
  )
 
70
  list_price = rng.normal(base_p, 5)
71
  net_price = np.clip(list_price * (1 - discount), 20, None)
72
  unit_cost = np.clip(rng.normal(base_c, 4), 10, None)
@@ -77,9 +150,9 @@ def generate_synthetic_data(days=60, seed=42, rows_per_day=600):
77
  qty = np.maximum(1, rng.poisson(8 * dow_mult * macro * qty_mu))
78
 
79
  revenue = net_price * qty
80
- cogs = unit_cost * qty
81
- gm_val = revenue - cogs
82
- gm_pct = np.where(revenue > 0, gm_val / revenue, 0.0)
83
 
84
  for i in range(n):
85
  records.append({
@@ -98,135 +171,628 @@ def generate_synthetic_data(days=60, seed=42, rows_per_day=600):
98
  "gm_pct": float(gm_pct[i]),
99
  "dow": dow
100
  })
101
- return pd.DataFrame(records)
102
 
103
- # -----------------------------
104
- # 2) Modeling utilities
105
- # -----------------------------
106
- def build_features(df):
107
  feats_num = ["net_price", "unit_cost", "qty", "discount_pct", "list_price", "dow"]
108
  feats_cat = ["product", "region", "channel"]
109
-
110
  df = df.sort_values("date").copy()
111
  seg = ["product", "region", "channel"]
112
  df["price_per_unit"] = df["net_price"]
113
- df["cost_per_unit"] = df["unit_cost"]
114
-
115
  df["roll7_qty"] = df.groupby(seg)["qty"].transform(lambda s: s.rolling(7, min_periods=1).median())
116
  df["roll7_price"] = df.groupby(seg)["price_per_unit"].transform(lambda s: s.rolling(7, min_periods=1).median())
117
- df["roll7_cost"] = df.groupby(seg)["cost_per_unit"].transform(lambda s: s.rolling(7, min_periods=1).median())
118
-
119
  feats_num += ["price_per_unit", "cost_per_unit", "roll7_qty", "roll7_price", "roll7_cost"]
120
- return df, feats_num, feats_cat, "gm_pct"
 
121
 
122
  @st.cache_resource(show_spinner=False)
123
- def train_model(df, feats_num, feats_cat, target, n_estimators=250):
124
  X = df[feats_num + feats_cat]
125
  y = df[target]
126
- pre = ColumnTransformer([
127
- ("cat", OneHotEncoder(handle_unknown="ignore"), feats_cat),
128
- ("num", "passthrough", feats_num),
129
- ])
130
- model = RandomForestRegressor(n_estimators=n_estimators, random_state=42, n_jobs=-1, min_samples_leaf=3)
 
 
131
  pipe = Pipeline([("pre", pre), ("rf", model)])
132
  X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, shuffle=False)
133
  pipe.fit(X_train, y_train)
134
  pred = pipe.predict(X_test)
135
- return pipe, {"r2": r2_score(y_test, pred), "mae": mean_absolute_error(y_test, pred)}, X_test
 
 
136
 
137
  @st.cache_resource(show_spinner=False)
138
- def compute_shap(_pipe, X_sample, feats_num, feats_cat, shap_sample=800, seed=42):
139
- np.random.seed(seed)
140
- preproc = _pipe.named_steps["pre"]
141
- rf = _pipe.named_steps["rf"]
142
- feature_names = list(preproc.named_transformers_["cat"].get_feature_names_out(feats_cat)) + feats_num
143
 
144
  if len(X_sample) > shap_sample:
145
- X_sample = X_sample.sample(shap_sample, random_state=seed)
146
- X_t = preproc.transform(X_sample)
 
 
147
  try:
148
  X_t = X_t.toarray()
149
  except Exception:
150
  pass
 
151
  explainer = shap.TreeExplainer(rf)
152
  shap_values = explainer.shap_values(X_t)
153
- return pd.DataFrame(shap_values, columns=feature_names), explainer.expected_value, X_sample.reset_index(drop=True), feature_names
 
 
154
 
155
- def estimate_segment_elasticity(df, product, region, channel):
156
  seg_df = df[(df["product"]==product)&(df["region"]==region)&(df["channel"]==channel)]
157
- if len(seg_df) < 100 or seg_df["net_price"].std() < 1e-6:
158
  return -0.5, False
159
  x = np.log(np.clip(seg_df["net_price"].values, 1e-6, None)).reshape(-1,1)
160
  y = np.log(np.clip(seg_df["qty"].values, 1e-6, None))
161
  lin = LinearRegression().fit(x, y)
162
  return float(lin.coef_[0]), True
163
 
164
- def simulate_action(segment_df, elasticity, delta_discount=0.0, delta_unit_cost=0.0):
165
  if segment_df.empty:
166
  return None
167
  base = segment_df.iloc[-1]
168
- p0, c0, q0, d0 = base["net_price"], base["unit_cost"], base["qty"], base["discount_pct"]
169
- new_discount = np.clip(d0 + delta_discount, 0.0, 0.45)
 
 
 
 
170
  p1 = max(0.01, base["list_price"] * (1 - new_discount))
171
- c1 = max(0.01, c0 + delta_unit_cost)
172
- q1 = q0 if p0 <= 0 else max(0.0, q0 * (p1 / p0) ** elasticity)
173
- rev0, rev1 = p0*q0, p1*q1
174
- cogs0, cogs1 = c0*q0, c1*q1
175
- gm_delta = (rev1 - cogs1) - (rev0 - cogs0)
 
 
 
 
 
 
 
 
 
 
 
176
  return {
177
- "gm_delta_value": gm_delta,
178
- "gm0_pct": (rev0 - cogs0)/rev0 if rev0>0 else 0,
179
- "gm1_pct": (rev1 - cogs1)/rev1 if rev1>0 else 0,
180
- "new_discount": new_discount,
181
  "baseline_price": p0, "new_price": p1,
182
  "baseline_cost": c0, "new_cost": c1,
183
  "baseline_qty": q0, "new_qty": q1,
 
 
 
 
184
  }
185
 
186
  # -----------------------------
187
- # 3) UI
188
  # -----------------------------
189
- st.title("πŸ“ˆ AI-Driven Daily Gross Margin β€” Analysis & What-if Simulator")
190
- with st.sidebar:
191
- st.header("βš™οΈ Controls")
192
- fast_mode = st.toggle("Fast mode", value=True)
193
- days = 60 if fast_mode else 90
194
- rows_per_day = 600 if fast_mode else 1200
195
- seed = 42
196
- n_trees = 250 if fast_mode else 400
197
- shap_sample = 800 if fast_mode else 1800
198
-
199
- df = generate_synthetic_data(days, seed, rows_per_day)
200
- df_feat, feats_num, feats_cat, target = build_features(df)
201
-
202
- # KPI panel
203
- daily = df.groupby("date").agg(revenue=("revenue","sum"), cogs=("cogs","sum"), gm_value=("gm_value","sum")).reset_index()
204
- daily["gm_pct"] = daily["gm_value"]/daily["revenue"]
 
 
205
  today_row = daily.iloc[-1]
206
- st.metric("GM% (today)", f"{today_row['gm_pct']*100:.2f}%")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
 
208
  # Train model
209
- pipe, metrics, X_test = train_model(df_feat, feats_num, feats_cat, target, n_estimators=n_trees)
210
- st.success(f"Model trained RΒ²={metrics['r2']:.3f} β€’ MAE={metrics['mae']:.4f}")
211
-
212
- # SHAP compute
213
- if st.button("Compute / Refresh SHAP drivers"):
214
- shap_df, expected_value, X_test_sample, feature_names = compute_shap(pipe, X_test, feats_num, feats_cat, shap_sample)
215
- st.session_state["shap_df"] = shap_df
216
- st.session_state["X_test_sample"] = X_test_sample
217
- st.session_state["feature_names"] = feature_names
218
-
219
- if "shap_df" in st.session_state:
220
- shap_df = st.session_state["shap_df"]
221
- mean_abs = shap_df.abs().mean().sort_values(ascending=False)
222
- st.dataframe(pd.DataFrame({"feature": mean_abs.index, "mean_abs_shap": mean_abs.values}).head(15))
223
-
224
- # What-if simulation
225
- last_day = df["date"].max()
226
- seg = df[df["date"]==last_day][["product","region","channel"]].drop_duplicates().iloc[0]
227
- prod_sel, reg_sel, ch_sel = seg
228
- seg_hist = df[(df["product"]==prod_sel)&(df["region"]==reg_sel)&(df["channel"]==ch_sel)]
229
- elasticity, _ = estimate_segment_elasticity(seg_hist, prod_sel, reg_sel, ch_sel)
230
- res = simulate_action(seg_hist, elasticity, delta_discount=-0.015, delta_unit_cost=0.0)
231
- if res:
232
- st.write(f"Simulated GM%: {res['gm0_pct']*100:.2f}% β†’ {res['gm1_pct']*100:.2f}% (Ξ”GM: {res['gm_delta_value']:.2f})")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
  import streamlit as st
3
  import numpy as np
4
  import pandas as pd
5
  import plotly.express as px
6
+ import plotly.graph_objects as go
7
+ from plotly.subplots import make_subplots
8
  import shap
9
  import matplotlib.pyplot as plt
 
10
  from datetime import datetime, timedelta
11
  from sklearn.model_selection import train_test_split
12
  from sklearn.compose import ColumnTransformer
 
16
  from sklearn.linear_model import LinearRegression
17
  from sklearn.metrics import r2_score, mean_absolute_error
18
 
19
+ # Enhanced page config
20
+ st.set_page_config(
21
+ page_title="Profitability Intelligence Suite",
22
+ page_icon="πŸ“Š",
23
+ layout="wide",
24
+ initial_sidebar_state="collapsed"
25
+ )
26
+
27
+ # Custom CSS for premium look
28
+ st.markdown("""
29
+ <style>
30
+ .main-header {
31
+ font-size: 2.8rem;
32
+ font-weight: 700;
33
+ color: #1f77b4;
34
+ text-align: center;
35
+ margin-bottom: 0.5rem;
36
+ text-shadow: 2px 2px 4px rgba(0,0,0,0.1);
37
+ }
38
+ .sub-header {
39
+ font-size: 1.2rem;
40
+ color: #666;
41
+ text-align: center;
42
+ margin-bottom: 2rem;
43
+ }
44
+ .metric-container {
45
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
46
+ padding: 1.5rem;
47
+ border-radius: 15px;
48
+ box-shadow: 0 8px 16px rgba(0,0,0,0.1);
49
+ color: white;
50
+ text-align: center;
51
+ }
52
+ .insight-box {
53
+ background: #f8f9fa;
54
+ border-left: 5px solid #1f77b4;
55
+ padding: 1.5rem;
56
+ margin: 1rem 0;
57
+ border-radius: 8px;
58
+ box-shadow: 0 4px 8px rgba(0,0,0,0.05);
59
+ }
60
+ .recommendation-card {
61
+ background: white;
62
+ border: 2px solid #e9ecef;
63
+ border-radius: 12px;
64
+ padding: 1.5rem;
65
+ margin: 1rem 0;
66
+ box-shadow: 0 4px 12px rgba(0,0,0,0.08);
67
+ transition: transform 0.2s;
68
+ }
69
+ .recommendation-card:hover {
70
+ transform: translateY(-5px);
71
+ box-shadow: 0 8px 20px rgba(0,0,0,0.12);
72
+ }
73
+ .positive-impact {
74
+ color: #28a745;
75
+ font-weight: 700;
76
+ font-size: 1.5rem;
77
+ }
78
+ .negative-impact {
79
+ color: #dc3545;
80
+ font-weight: 700;
81
+ font-size: 1.5rem;
82
+ }
83
+ .stTabs [data-baseweb="tab-list"] {
84
+ gap: 2rem;
85
+ }
86
+ .stTabs [data-baseweb="tab"] {
87
+ height: 3rem;
88
+ font-size: 1.1rem;
89
+ font-weight: 600;
90
+ }
91
+ </style>
92
+ """, unsafe_allow_html=True)
93
 
94
  # -----------------------------
95
+ # Data Generation (Same as original)
96
  # -----------------------------
97
  @st.cache_data(show_spinner=False)
98
  def generate_synthetic_data(days=60, seed=42, rows_per_day=600):
99
  rng = np.random.default_rng(seed)
100
  start_date = datetime.today().date() - timedelta(days=days)
101
  dates = pd.date_range(start_date, periods=days, freq="D")
102
+ products = ["Premium Widget", "Standard Widget", "Economy Widget", "Deluxe Widget"]
103
+ regions = ["Americas", "EMEA", "Asia Pacific"]
104
+ channels = ["Direct Sales", "Distribution Partners", "E-Commerce"]
105
 
106
+ base_price = {"Premium Widget": 120, "Standard Widget": 135, "Economy Widget": 110, "Deluxe Widget": 150}
107
+ base_cost = {"Premium Widget": 70, "Standard Widget": 88, "Economy Widget": 60, "Deluxe Widget": 95}
108
+ region_price_bump = {"Americas": 1.00, "EMEA": 1.03, "Asia Pacific": 0.97}
109
+ region_cost_bump = {"Americas": 1.00, "EMEA": 1.02, "Asia Pacific": 1.01}
110
+ channel_discount_mean = {"Direct Sales": 0.06, "Distribution Partners": 0.12, "E-Commerce": 0.04}
111
+ channel_discount_std = {"Direct Sales": 0.02, "Distribution Partners": 0.03, "E-Commerce": 0.02}
 
 
 
 
 
 
112
 
113
  seg_epsilon = {}
114
  for p in products:
115
  for r in regions:
116
  for c in channels:
117
  base_eps = rng.uniform(-0.9, -0.25)
118
+ if c == "Distribution Partners":
119
  base_eps -= rng.uniform(0.1, 0.3)
120
+ if c == "E-Commerce":
121
  base_eps += rng.uniform(0.05, 0.15)
122
  seg_epsilon[(p, r, c)] = base_eps
123
 
 
129
 
130
  n = rows_per_day
131
  prod = rng.choice(products, size=n, p=[0.35, 0.3, 0.2, 0.15])
132
+ reg = rng.choice(regions, size=n, p=[0.4, 0.35, 0.25])
133
+ ch = rng.choice(channels, size=n, p=[0.45, 0.35, 0.20])
134
 
135
  base_p = np.array([base_price[x] for x in prod]) * np.array([region_price_bump[x] for x in reg])
136
+ base_c = np.array([base_cost[x] for x in prod]) * np.array([region_cost_bump[x] for x in reg])
137
 
138
  discount = np.clip(
139
  np.array([channel_discount_mean[x] for x in ch]) +
140
  rng.normal(0, [channel_discount_std[x] for x in ch]), 0, 0.45
141
  )
142
+
143
  list_price = rng.normal(base_p, 5)
144
  net_price = np.clip(list_price * (1 - discount), 20, None)
145
  unit_cost = np.clip(rng.normal(base_c, 4), 10, None)
 
150
  qty = np.maximum(1, rng.poisson(8 * dow_mult * macro * qty_mu))
151
 
152
  revenue = net_price * qty
153
+ cogs = unit_cost * qty
154
+ gm_val = revenue - cogs
155
+ gm_pct = np.where(revenue > 0, gm_val / revenue, 0.0)
156
 
157
  for i in range(n):
158
  records.append({
 
171
  "gm_pct": float(gm_pct[i]),
172
  "dow": dow
173
  })
 
174
 
175
+ df = pd.DataFrame(records)
176
+ return df
177
+
178
+ def build_features(df: pd.DataFrame):
179
  feats_num = ["net_price", "unit_cost", "qty", "discount_pct", "list_price", "dow"]
180
  feats_cat = ["product", "region", "channel"]
 
181
  df = df.sort_values("date").copy()
182
  seg = ["product", "region", "channel"]
183
  df["price_per_unit"] = df["net_price"]
184
+ df["cost_per_unit"] = df["unit_cost"]
 
185
  df["roll7_qty"] = df.groupby(seg)["qty"].transform(lambda s: s.rolling(7, min_periods=1).median())
186
  df["roll7_price"] = df.groupby(seg)["price_per_unit"].transform(lambda s: s.rolling(7, min_periods=1).median())
187
+ df["roll7_cost"] = df.groupby(seg)["cost_per_unit"].transform(lambda s: s.rolling(7, min_periods=1).median())
 
188
  feats_num += ["price_per_unit", "cost_per_unit", "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(df: pd.DataFrame, feats_num, feats_cat, target):
194
  X = df[feats_num + feats_cat]
195
  y = df[target]
196
+ pre = ColumnTransformer(
197
+ transformers=[
198
+ ("cat", OneHotEncoder(handle_unknown="ignore"), feats_cat),
199
+ ("num", "passthrough", feats_num),
200
+ ]
201
+ )
202
+ model = RandomForestRegressor(n_estimators=250, max_depth=None, random_state=42, n_jobs=-1, min_samples_leaf=3)
203
  pipe = Pipeline([("pre", pre), ("rf", model)])
204
  X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, shuffle=False)
205
  pipe.fit(X_train, y_train)
206
  pred = pipe.predict(X_test)
207
+ r2 = r2_score(y_test, pred)
208
+ mae = mean_absolute_error(y_test, pred)
209
+ return pipe, {"r2": r2, "mae": mae}, X_test
210
 
211
  @st.cache_resource(show_spinner=False)
212
+ def compute_shap(pipe, X_sample, feats_num, feats_cat, shap_sample=800):
213
+ np.random.seed(42)
214
+ preprocessor = pipe.named_steps["pre"]
215
+ rf = pipe.named_steps["rf"]
216
+ feature_names = list(preprocessor.named_transformers_["cat"].get_feature_names_out(feats_cat)) + feats_num
217
 
218
  if len(X_sample) > shap_sample:
219
+ sample_idx = np.random.choice(len(X_sample), size=shap_sample, replace=False)
220
+ X_sample = X_sample.iloc[sample_idx]
221
+
222
+ X_t = preprocessor.transform(X_sample)
223
  try:
224
  X_t = X_t.toarray()
225
  except Exception:
226
  pass
227
+
228
  explainer = shap.TreeExplainer(rf)
229
  shap_values = explainer.shap_values(X_t)
230
+ shap_df = pd.DataFrame(shap_values, columns=feature_names)
231
+
232
+ return shap_df, X_sample.reset_index(drop=True), feature_names
233
 
234
+ def estimate_segment_elasticity(df: pd.DataFrame, product, region, channel):
235
  seg_df = df[(df["product"]==product)&(df["region"]==region)&(df["channel"]==channel)]
236
+ if len(seg_df) < 100 or seg_df["net_price"].std() < 1e-6 or seg_df["qty"].std() < 1e-6:
237
  return -0.5, False
238
  x = np.log(np.clip(seg_df["net_price"].values, 1e-6, None)).reshape(-1,1)
239
  y = np.log(np.clip(seg_df["qty"].values, 1e-6, None))
240
  lin = LinearRegression().fit(x, y)
241
  return float(lin.coef_[0]), True
242
 
243
+ def simulate_pricing_action(segment_df: pd.DataFrame, elasticity, discount_reduction_pct):
244
  if segment_df.empty:
245
  return None
246
  base = segment_df.iloc[-1]
247
+ p0 = base["net_price"]
248
+ c0 = base["unit_cost"]
249
+ q0 = base["qty"]
250
+ d0 = base["discount_pct"]
251
+
252
+ new_discount = np.clip(d0 - (discount_reduction_pct/100), 0.0, 0.45)
253
  p1 = max(0.01, base["list_price"] * (1 - new_discount))
254
+ c1 = c0
255
+
256
+ if p0 <= 0:
257
+ q1 = q0
258
+ else:
259
+ q1 = max(0.0, q0 * (p1 / p0) ** elasticity)
260
+
261
+ rev0 = p0 * q0
262
+ cogs0 = c0 * q0
263
+ rev1 = p1 * q1
264
+ cogs1 = c1 * q1
265
+
266
+ gm_delta_value = (rev1 - cogs1) - (rev0 - cogs0)
267
+ gm0_pct = (rev0 - cogs0)/rev0 if rev0>0 else 0.0
268
+ gm1_pct = (rev1 - cogs1)/rev1 if rev1>0 else 0.0
269
+
270
  return {
 
 
 
 
271
  "baseline_price": p0, "new_price": p1,
272
  "baseline_cost": c0, "new_cost": c1,
273
  "baseline_qty": q0, "new_qty": q1,
274
+ "baseline_discount": d0*100, "new_discount": new_discount*100,
275
+ "gm_delta_value": gm_delta_value,
276
+ "gm0_pct": gm0_pct, "gm1_pct": gm1_pct,
277
+ "revenue_delta": rev1 - rev0
278
  }
279
 
280
  # -----------------------------
281
+ # Main App
282
  # -----------------------------
283
+
284
+ # Header
285
+ st.markdown('<h1 class="main-header">🎯 Profitability Intelligence Suite</h1>', unsafe_allow_html=True)
286
+ st.markdown('<p class="sub-header">AI-Powered Margin Analysis & Strategic Recommendations</p>', unsafe_allow_html=True)
287
+
288
+ # Generate data
289
+ with st.spinner("πŸ”„ Loading business data..."):
290
+ df = generate_synthetic_data(days=60, seed=42, rows_per_day=600)
291
+ df_feat, feats_num, feats_cat, target = build_features(df)
292
+
293
+ # Calculate KPIs
294
+ daily = df.groupby("date").agg(
295
+ revenue=("revenue","sum"),
296
+ cogs=("cogs","sum"),
297
+ gm_value=("gm_value","sum")
298
+ ).reset_index()
299
+ daily["gm_pct"] = np.where(daily["revenue"]>0, daily["gm_value"]/daily["revenue"], 0.0)
300
+
301
  today_row = daily.iloc[-1]
302
+ yesterday_row = daily.iloc[-2] if len(daily) > 1 else today_row
303
+ week_ago_row = daily.iloc[-8] if len(daily) > 7 else today_row
304
+ roll7 = daily["gm_pct"].tail(7).mean()
305
+
306
+ # Executive Dashboard Section
307
+ st.markdown("### πŸ“Š Executive Performance Dashboard")
308
+
309
+ col1, col2, col3, col4 = st.columns(4)
310
+
311
+ with col1:
312
+ delta_gm = (today_row["gm_pct"] - yesterday_row["gm_pct"]) * 100
313
+ st.metric(
314
+ label="Gross Margin %",
315
+ value=f"{today_row['gm_pct']*100:.1f}%",
316
+ delta=f"{delta_gm:+.2f}pp vs yesterday",
317
+ delta_color="normal"
318
+ )
319
+
320
+ with col2:
321
+ delta_rev = ((today_row["revenue"] - yesterday_row["revenue"]) / yesterday_row["revenue"] * 100) if yesterday_row["revenue"] > 0 else 0
322
+ st.metric(
323
+ label="Revenue (Today)",
324
+ value=f"${today_row['revenue']/1e6:.2f}M",
325
+ delta=f"{delta_rev:+.1f}% DoD",
326
+ delta_color="normal"
327
+ )
328
+
329
+ with col3:
330
+ st.metric(
331
+ label="Gross Margin $ (Today)",
332
+ value=f"${today_row['gm_value']/1e6:.2f}M",
333
+ delta=f"${(today_row['gm_value'] - yesterday_row['gm_value'])/1e6:+.2f}M",
334
+ delta_color="normal"
335
+ )
336
+
337
+ with col4:
338
+ avg_gm_vs_week = (today_row["gm_pct"] - week_ago_row["gm_pct"]) * 100
339
+ st.metric(
340
+ label="7-Day Avg GM%",
341
+ value=f"{roll7*100:.1f}%",
342
+ delta=f"{avg_gm_vs_week:+.2f}pp WoW",
343
+ delta_color="normal"
344
+ )
345
+
346
+ # Trend visualization
347
+ st.markdown("#### πŸ“ˆ Performance Trend Analysis")
348
+
349
+ fig_trends = make_subplots(
350
+ rows=1, cols=2,
351
+ subplot_titles=("Gross Margin % Trend", "Revenue & Margin $ Trend"),
352
+ specs=[[{"secondary_y": False}, {"secondary_y": True}]]
353
+ )
354
+
355
+ # GM% trend
356
+ fig_trends.add_trace(
357
+ go.Scatter(
358
+ x=daily["date"],
359
+ y=daily["gm_pct"]*100,
360
+ name="GM%",
361
+ line=dict(color="#1f77b4", width=3),
362
+ fill='tozeroy',
363
+ fillcolor="rgba(31, 119, 180, 0.1)"
364
+ ),
365
+ row=1, col=1
366
+ )
367
+
368
+ # Revenue and GM$ trend
369
+ fig_trends.add_trace(
370
+ go.Scatter(
371
+ x=daily["date"],
372
+ y=daily["revenue"]/1e6,
373
+ name="Revenue",
374
+ line=dict(color="#2ca02c", width=2)
375
+ ),
376
+ row=1, col=2
377
+ )
378
+
379
+ fig_trends.add_trace(
380
+ go.Scatter(
381
+ x=daily["date"],
382
+ y=daily["gm_value"]/1e6,
383
+ name="GM Value",
384
+ line=dict(color="#ff7f0e", width=2, dash="dash")
385
+ ),
386
+ row=1, col=2, secondary_y=True
387
+ )
388
+
389
+ fig_trends.update_xaxes(title_text="Date", row=1, col=1)
390
+ fig_trends.update_xaxes(title_text="Date", row=1, col=2)
391
+ fig_trends.update_yaxes(title_text="Gross Margin %", row=1, col=1)
392
+ fig_trends.update_yaxes(title_text="Revenue ($M)", row=1, col=2)
393
+ fig_trends.update_yaxes(title_text="GM Value ($M)", row=1, col=2, secondary_y=True)
394
+
395
+ fig_trends.update_layout(height=400, showlegend=True, hovermode="x unified")
396
+ st.plotly_chart(fig_trends, use_container_width=True)
397
+
398
+ st.markdown("---")
399
 
400
  # Train model
401
+ with st.spinner("πŸ€– Training AI model..."):
402
+ pipe, metrics, X_test = train_model(df_feat, feats_num, feats_cat, target)
403
+
404
+ # Tabs for different sections
405
+ tab1, tab2, tab3 = st.tabs(["πŸ” Key Drivers Analysis", "🎯 Strategic Recommendations", "πŸ§ͺ What-If Simulator"])
406
+
407
+ with tab1:
408
+ st.markdown("### Understanding What Drives Your Profitability")
409
+ st.markdown("""
410
+ <div class="insight-box">
411
+ <b>πŸŽ“ Business Insight:</b> This analysis reveals which business factors have the strongest impact on gross margin.
412
+ Understanding these drivers helps prioritize strategic initiatives and operational improvements.
413
+ </div>
414
+ """, unsafe_allow_html=True)
415
+
416
+ # Compute SHAP
417
+ with st.spinner("πŸ”¬ Analyzing profitability drivers..."):
418
+ shap_df, X_test_sample, feature_names = compute_shap(pipe, X_test, feats_num, feats_cat, shap_sample=800)
419
+
420
+ # Calculate mean absolute SHAP
421
+ mean_abs = shap_df.abs().mean().sort_values(ascending=False)
422
+
423
+ # Map technical names to business names
424
+ business_name_map = {
425
+ "discount_pct": "Discount Level",
426
+ "unit_cost": "Unit Cost",
427
+ "net_price": "Net Selling Price",
428
+ "list_price": "List Price",
429
+ "qty": "Order Quantity",
430
+ "price_per_unit": "Price per Unit",
431
+ "cost_per_unit": "Cost per Unit",
432
+ "roll7_qty": "7-Day Avg Quantity",
433
+ "roll7_price": "7-Day Avg Price",
434
+ "roll7_cost": "7-Day Avg Cost",
435
+ "dow": "Day of Week"
436
+ }
437
+
438
+ # Get top drivers with business names
439
+ top_drivers = []
440
+ for feat, val in mean_abs.head(10).items():
441
+ bus_name = feat
442
+ for key, name in business_name_map.items():
443
+ if key in feat.lower():
444
+ bus_name = name
445
+ break
446
+ # Check for product/region/channel encoding
447
+ if feat.startswith("cat__"):
448
+ parts = feat.replace("cat__", "").split("_")
449
+ if "product" in feat.lower():
450
+ bus_name = f"Product: {parts[-1] if parts else feat}"
451
+ elif "region" in feat.lower():
452
+ bus_name = f"Region: {parts[-1] if parts else feat}"
453
+ elif "channel" in feat.lower():
454
+ bus_name = f"Channel: {parts[-1] if parts else feat}"
455
+ top_drivers.append({"Driver": bus_name, "Impact Score": val})
456
+
457
+ drivers_df = pd.DataFrame(top_drivers)
458
+
459
+ col_a, col_b = st.columns([1, 1])
460
+
461
+ with col_a:
462
+ st.markdown("#### Top 10 Profitability Drivers")
463
+
464
+ # Create horizontal bar chart
465
+ fig_drivers = go.Figure()
466
+ fig_drivers.add_trace(go.Bar(
467
+ y=drivers_df["Driver"][::-1],
468
+ x=drivers_df["Impact Score"][::-1],
469
+ orientation='h',
470
+ marker=dict(
471
+ color=drivers_df["Impact Score"][::-1],
472
+ colorscale='Blues',
473
+ line=dict(color='rgb(8,48,107)', width=1.5)
474
+ ),
475
+ text=drivers_df["Impact Score"][::-1].round(4),
476
+ textposition='outside',
477
+ ))
478
+
479
+ fig_drivers.update_layout(
480
+ title="Ranked by Average Impact on Gross Margin",
481
+ xaxis_title="Impact Score (higher = stronger influence)",
482
+ yaxis_title="",
483
+ height=500,
484
+ showlegend=False
485
+ )
486
+ st.plotly_chart(fig_drivers, use_container_width=True)
487
+
488
+ with col_b:
489
+ st.markdown("#### Key Insights")
490
+
491
+ # Generate business insights
492
+ top_3 = drivers_df.head(3)
493
+
494
+ st.markdown(f"""
495
+ <div class="insight-box">
496
+ <b>πŸ₯‡ Primary Driver:</b> {top_3.iloc[0]['Driver']}<br>
497
+ <small>This factor has the strongest influence on margin performance</small>
498
+ </div>
499
+ """, unsafe_allow_html=True)
500
+
501
+ st.markdown(f"""
502
+ <div class="insight-box">
503
+ <b>πŸ₯ˆ Secondary Driver:</b> {top_3.iloc[1]['Driver']}<br>
504
+ <small>Second most important factor affecting profitability</small>
505
+ </div>
506
+ """, unsafe_allow_html=True)
507
+
508
+ st.markdown(f"""
509
+ <div class="insight-box">
510
+ <b>πŸ₯‰ Tertiary Driver:</b> {top_3.iloc[2]['Driver']}<br>
511
+ <small>Third key factor with significant margin impact</small>
512
+ </div>
513
+ """, unsafe_allow_html=True)
514
+
515
+ # Segment-level insights
516
+ st.markdown("#### Segment Performance")
517
+
518
+ # Join SHAP with original data
519
+ cat_cols = ["product", "region", "channel"]
520
+ joined = pd.concat([X_test_sample.reset_index(drop=True), shap_df.reset_index(drop=True)], axis=1)
521
+
522
+ # Find segments with biggest impact
523
+ grp = joined.groupby(cat_cols).mean(numeric_only=True)
524
+ key_shap_cols = [c for c in grp.columns if c in shap_df.columns]
525
+ grp["net_impact"] = grp[key_shap_cols].sum(axis=1)
526
+
527
+ top_negative = grp.nsmallest(5, "net_impact")
528
+ top_positive = grp.nlargest(5, "net_impact")
529
+
530
+ st.markdown("**⚠️ Segments Reducing Margin:**")
531
+ for idx, row in top_negative.head(3).iterrows():
532
+ st.markdown(f"β€’ **{idx[0]}** β€’ {idx[1]} β€’ {idx[2]} *(Impact: {row['net_impact']:.4f})*")
533
+
534
+ st.markdown("**βœ… Segments Boosting Margin:**")
535
+ for idx, row in top_positive.head(3).iterrows():
536
+ st.markdown(f"β€’ **{idx[0]}** β€’ {idx[1]} β€’ {idx[2]} *(Impact: {row['net_impact']:.4f})*")
537
+
538
+ with tab2:
539
+ st.markdown("### AI-Generated Strategic Recommendations")
540
+ st.markdown("""
541
+ <div class="insight-box">
542
+ <b>πŸ’‘ How This Works:</b> The AI identifies segments with margin pressure and suggests specific pricing actions
543
+ to improve profitability. Recommendations are ranked by expected financial impact.
544
+ </div>
545
+ """, unsafe_allow_html=True)
546
+
547
+ # Generate recommendations
548
+ with st.spinner("🧠 Generating strategic recommendations..."):
549
+ joined = pd.concat([X_test_sample.reset_index(drop=True), shap_df.reset_index(drop=True)], axis=1)
550
+ joined["key"] = joined["product"] + "|" + joined["region"] + "|" + joined["channel"]
551
+
552
+ 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]
553
+ seg_scores = joined.groupby("key")[cand_cols].mean().sum(axis=1)
554
+ worst_keys = seg_scores.sort_values().head(15).index.tolist()
555
+
556
+ recs = []
557
+ for key in worst_keys:
558
+ p, r, c = key.split("|")
559
+ hist = df[(df["product"]==p)&(df["region"]==r)&(df["channel"]==c)].sort_values("date")
560
+ if hist.empty or len(hist) < 50:
561
+ continue
562
+
563
+ eps, _ = estimate_segment_elasticity(hist, p, r, c)
564
+
565
+ # Suggest discount reduction between 1-3 percentage points
566
+ prop_disc_pts = np.clip(abs(seg_scores[key])*10, 1.0, 3.0)
567
+ sim = simulate_pricing_action(hist, eps, prop_disc_pts)
568
+
569
+ if sim is None or sim["gm_delta_value"] <= 0:
570
+ continue
571
+
572
+ # Calculate annualized impact (rough estimate)
573
+ daily_transactions = len(hist) / ((hist["date"].max() - hist["date"].min()).days + 1)
574
+ annual_impact = sim["gm_delta_value"] * daily_transactions * 365
575
+
576
+ recs.append({
577
+ "Segment": f"{p}",
578
+ "Region": r,
579
+ "Channel": c,
580
+ "Current Discount": f"{sim['baseline_discount']:.1f}%",
581
+ "Recommended Discount": f"{sim['new_discount']:.1f}%",
582
+ "Expected GM Uplift": sim["gm_delta_value"],
583
+ "Annual Impact Estimate": annual_impact,
584
+ "Current GM%": sim["gm0_pct"]*100,
585
+ "Projected GM%": sim["gm1_pct"]*100,
586
+ "Price Elasticity": eps
587
+ })
588
+
589
+ recs_df = pd.DataFrame(recs).sort_values("Expected GM Uplift", ascending=False)
590
+
591
+ if len(recs_df) > 0:
592
+ # Show top 3 recommendations in cards
593
+ st.markdown("#### πŸ† Top 3 Priority Actions")
594
+
595
+ for i, (idx, rec) in enumerate(recs_df.head(3).iterrows()):
596
+ with st.container():
597
+ st.markdown(f"""
598
+ <div class="recommendation-card">
599
+ <h4>#{i+1}: {rec['Segment']} β€’ {rec['Region']} β€’ {rec['Channel']}</h4>
600
+ <p style="font-size: 1.1rem; margin: 0.5rem 0;">
601
+ <b>Recommended Action:</b> Reduce discount from <b>{rec['Current Discount']}</b> to <b>{rec['Recommended Discount']}</b>
602
+ </p>
603
+ <p style="font-size: 1rem; color: #666; margin: 0.5rem 0;">
604
+ Current GM: <b>{rec['Current GM%']:.1f}%</b> β†’ Projected GM: <b style="color: #28a745;">{rec['Projected GM%']:.1f}%</b>
605
+ </p>
606
+ <p class="positive-impact">
607
+ πŸ’° Expected Daily Impact: ${rec['Expected GM Uplift']:.2f}
608
+ </p>
609
+ <p style="font-size: 0.95rem; color: #666;">
610
+ πŸ“Š Estimated Annual Impact: <b>${rec['Annual Impact Estimate']/1e3:.1f}K</b>
611
+ </p>
612
+ </div>
613
+ """, unsafe_allow_html=True)
614
+
615
+ st.markdown("---")
616
+ st.markdown("#### πŸ“‹ Complete Recommendations List")
617
+
618
+ # Format for display
619
+ display_df = recs_df.copy()
620
+ display_df["Expected GM Uplift"] = display_df["Expected GM Uplift"].apply(lambda x: f"${x:.2f}")
621
+ display_df["Annual Impact Estimate"] = display_df["Annual Impact Estimate"].apply(lambda x: f"${x/1e3:.1f}K")
622
+ display_df["Current GM%"] = display_df["Current GM%"].apply(lambda x: f"{x:.1f}%")
623
+ display_df["Projected GM%"] = display_df["Projected GM%"].apply(lambda x: f"{x:.1f}%")
624
+ display_df["Price Elasticity"] = display_df["Price Elasticity"].apply(lambda x: f"{x:.2f}")
625
+
626
+ st.dataframe(display_df, use_container_width=True, height=400)
627
+
628
+ # Download button
629
+ st.download_button(
630
+ label="πŸ“₯ Download Full Recommendations (CSV)",
631
+ data=recs_df.to_csv(index=False).encode("utf-8"),
632
+ file_name=f"profitability_recommendations_{datetime.today().strftime('%Y%m%d')}.csv",
633
+ mime="text/csv"
634
+ )
635
+
636
+ # Aggregate impact
637
+ total_daily_impact = recs_df["Expected GM Uplift"].sum()
638
+ total_annual_impact = recs_df["Annual Impact Estimate"].sum()
639
+
640
+ st.markdown(f"""
641
+ <div class="insight-box" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none;">
642
+ <h3 style="color: white; margin-top: 0;">πŸ’Ž Total Opportunity</h3>
643
+ <p style="font-size: 1.3rem; margin: 0.5rem 0;">
644
+ <b>Daily GM Impact:</b> ${total_daily_impact:.2f}
645
+ </p>
646
+ <p style="font-size: 1.6rem; margin: 0.5rem 0;">
647
+ <b>Estimated Annual Impact:</b> ${total_annual_impact/1e6:.2f}M
648
+ </p>
649
+ <small>Based on current transaction volumes and assuming consistent implementation</small>
650
+ </div>
651
+ """, unsafe_allow_html=True)
652
+ else:
653
+ st.info("No significant optimization opportunities detected in current data.")
654
+
655
+ with tab3:
656
+ st.markdown("### Custom What-If Analysis")
657
+ st.markdown("""
658
+ <div class="insight-box">
659
+ <b>πŸ§ͺ Interactive Simulation:</b> Test different pricing strategies for specific segments to understand
660
+ the potential impact on revenue, volume, and profitability.
661
+ </div>
662
+ """, unsafe_allow_html=True)
663
+
664
+ # Segment selector
665
+ last_day = df["date"].max()
666
+ seg_today = df[df["date"]==last_day][["product","region","channel"]].drop_duplicates()
667
+
668
+ col_sim1, col_sim2, col_sim3 = st.columns(3)
669
+
670
+ with col_sim1:
671
+ selected_product = st.selectbox("πŸ“¦ Select Product", sorted(seg_today["product"].unique()))
672
+ with col_sim2:
673
+ selected_region = st.selectbox("🌍 Select Region", sorted(seg_today["region"].unique()))
674
+ with col_sim3:
675
+ selected_channel = st.selectbox("πŸ›’ Select Channel", sorted(seg_today["channel"].unique()))
676
+
677
+ # Get segment history
678
+ seg_hist = df[
679
+ (df["product"]==selected_product) &
680
+ (df["region"]==selected_region) &
681
+ (df["channel"]==selected_channel)
682
+ ].sort_values("date")
683
+
684
+ if not seg_hist.empty and len(seg_hist) >= 50:
685
+ elasticity, _ = estimate_segment_elasticity(seg_hist, selected_product, selected_region, selected_channel)
686
+
687
+ # Current state
688
+ current = seg_hist.iloc[-1]
689
+
690
+ st.markdown(f"""
691
+ <div class="insight-box">
692
+ <b>πŸ“Š Current State:</b><br>
693
+ β€’ Current Discount: <b>{current['discount_pct']*100:.1f}%</b><br>
694
+ β€’ Net Price: <b>${current['net_price']:.2f}</b><br>
695
+ β€’ Unit Cost: <b>${current['unit_cost']:.2f}</b><br>
696
+ β€’ Avg Daily Volume: <b>{seg_hist.tail(7)['qty'].mean():.0f} units</b><br>
697
+ β€’ Current GM%: <b>{current['gm_pct']*100:.1f}%</b><br>
698
+ β€’ Price Elasticity: <b>{elasticity:.2f}</b> <small>(% change in volume per 1% price change)</small>
699
+ </div>
700
+ """, unsafe_allow_html=True)
701
+
702
+ st.markdown("#### 🎯 Test Pricing Strategy")
703
+
704
+ # Pricing strategy slider
705
+ discount_change = st.slider(
706
+ "Adjust Discount Level (percentage points)",
707
+ min_value=-10.0,
708
+ max_value=5.0,
709
+ value=0.0,
710
+ step=0.5,
711
+ help="Negative values reduce discount (increase price), positive values increase discount"
712
+ )
713
+
714
+ if discount_change != 0:
715
+ sim = simulate_pricing_action(seg_hist, elasticity, -discount_change)
716
+
717
+ if sim:
718
+ # Visualization
719
+ col_res1, col_res2 = st.columns(2)
720
+
721
+ with col_res1:
722
+ # Create comparison chart
723
+ comparison_data = pd.DataFrame({
724
+ 'Metric': ['Price', 'Volume', 'GM%'],
725
+ 'Current': [sim['baseline_price'], sim['baseline_qty'], sim['gm0_pct']*100],
726
+ 'Projected': [sim['new_price'], sim['new_qty'], sim['gm1_pct']*100]
727
+ })
728
+
729
+ fig_comp = go.Figure()
730
+ fig_comp.add_trace(go.Bar(
731
+ name='Current',
732
+ x=comparison_data['Metric'],
733
+ y=comparison_data['Current'],
734
+ marker_color='#94a3b8'
735
+ ))
736
+ fig_comp.add_trace(go.Bar(
737
+ name='Projected',
738
+ x=comparison_data['Metric'],
739
+ y=comparison_data['Projected'],
740
+ marker_color='#3b82f6'
741
+ ))
742
+
743
+ fig_comp.update_layout(
744
+ title="Current vs. Projected Performance",
745
+ barmode='group',
746
+ height=350
747
+ )
748
+ st.plotly_chart(fig_comp, use_container_width=True)
749
+
750
+ with col_res2:
751
+ st.markdown("#### πŸ“ˆ Simulation Results")
752
+
753
+ gm_change = sim['gm1_pct'] - sim['gm0_pct']
754
+ rev_change_pct = (sim['revenue_delta'] / (sim['baseline_price'] * sim['baseline_qty'])) * 100 if sim['baseline_price'] * sim['baseline_qty'] > 0 else 0
755
+ vol_change_pct = ((sim['new_qty'] - sim['baseline_qty']) / sim['baseline_qty']) * 100 if sim['baseline_qty'] > 0 else 0
756
+
757
+ st.metric(
758
+ "Gross Margin Impact",
759
+ f"{sim['gm1_pct']*100:.1f}%",
760
+ f"{gm_change*100:+.1f}pp"
761
+ )
762
+
763
+ st.metric(
764
+ "Revenue Impact",
765
+ f"${sim['new_price'] * sim['new_qty']:.2f}",
766
+ f"{rev_change_pct:+.1f}%"
767
+ )
768
+
769
+ st.metric(
770
+ "Volume Impact",
771
+ f"{sim['new_qty']:.0f} units",
772
+ f"{vol_change_pct:+.1f}%"
773
+ )
774
+
775
+ # Daily P&L impact
776
+ st.markdown(f"""
777
+ <div class="insight-box" style="margin-top: 1rem;">
778
+ <b>πŸ’° Daily P&L Impact:</b><br>
779
+ <span style="font-size: 1.5rem; {'color: #28a745' if sim['gm_delta_value'] > 0 else 'color: #dc3545'}">
780
+ ${sim['gm_delta_value']:+.2f}
781
+ </span>
782
+ </div>
783
+ """, unsafe_allow_html=True)
784
+ else:
785
+ st.info("πŸ‘† Adjust the discount slider above to simulate different pricing strategies")
786
+
787
+ else:
788
+ st.warning("⚠️ Insufficient data for selected segment. Please choose a different combination.")
789
+
790
+ st.markdown("---")
791
+ st.markdown("""
792
+ <div style="text-align: center; color: #666; padding: 2rem 0;">
793
+ <small>
794
+ πŸ”’ Demo Mode: Using synthetic SAP-style data for illustration purposes<br>
795
+ For production deployment, connect to live SAP S/4HANA CDS views or data warehouse
796
+ </small>
797
+ </div>
798
+ """, unsafe_allow_html=True)