KB-Infinity-Tech commited on
Commit
f1b5844
Β·
verified Β·
1 Parent(s): cb0cc10

Upload 3 files

Browse files
Files changed (3) hide show
  1. demand_model.pkl +3 -0
  2. demo.py +123 -0
  3. pricer.py +457 -0
demand_model.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:674fa4e2d5ed0b866b11dfaba84ca4d85e77313004580d66fef5f93dfc33982a
3
+ size 720425
demo.py ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ demo.py β€” Quick demo for hackathon judges / live defense
3
+ Runs everything in < 5 seconds. All output is self-explanatory.
4
+ """
5
+
6
+ from pricer import (
7
+ Product, suggest_price, simulate_7_days,
8
+ format_sms, print_comparison_report, freshness_factor
9
+ )
10
+
11
+
12
+ def demo_single_price():
13
+ """Show how one pricing call works, step by step."""
14
+ print("\n" + "━"*60)
15
+ print(" DEMO 1: Single Price Recommendation")
16
+ print("━"*60)
17
+
18
+ tomato = Product(
19
+ sku="TOMATO-A",
20
+ cost=1000,
21
+ shelf_life_days=7,
22
+ p_ref=1800,
23
+ Q0=50,
24
+ alpha=1.5,
25
+ )
26
+
27
+ competitors = [1600, 1700, 1900]
28
+
29
+ for age in [0, 2, 3.5, 5, 6]:
30
+ result = suggest_price(tomato, age, competitors)
31
+ freshness = result["freshness"]
32
+ price = result["suggested_price"]
33
+ label = result["freshness_label"]
34
+ print(f" Day {age:>3.1f} | Freshness: {freshness:.3f} ({label:<10}) "
35
+ f"β†’ Price: {price:>7.0f} UGX | "
36
+ f"Margin: {result['margin_pct']:>5.1f}%")
37
+
38
+
39
+ def demo_freshness_table():
40
+ """Show the sigmoid freshness curve β€” great for interview visuals."""
41
+ print("\n" + "━"*60)
42
+ print(" DEMO 2: Freshness Curve (why sigmoid beats linear)")
43
+ print("━"*60)
44
+ print(f" {'Day':<6} {'Sigmoid':>10} {'Linear':>10} {'Difference':>12}")
45
+ print(" " + "-"*42)
46
+
47
+ shelf = 7
48
+ for day in range(8):
49
+ sigmoid = freshness_factor(day, shelf)
50
+ linear = max(0, 1 - day / shelf)
51
+ diff = sigmoid - linear
52
+ bar = "β–ˆ" * int(sigmoid * 20)
53
+ print(f" {day:<6} {sigmoid:>10.3f} {linear:>10.3f} {diff:>+12.3f} {bar}")
54
+
55
+
56
+ def demo_what_at_half_life():
57
+ """Interview: what happens at half shelf life?"""
58
+ print("\n" + "━"*60)
59
+ print(" DEMO 3: The Half-Life Moment (key interview talking point)")
60
+ print("━"*60)
61
+
62
+ tomato = Product(
63
+ sku="TOMATO-A", cost=1000, shelf_life_days=7,
64
+ p_ref=1800, Q0=50, alpha=1.5,
65
+ )
66
+ half_life = tomato.shelf_life_days / 2 # Day 3.5
67
+
68
+ fresh_result = suggest_price(tomato, 0, [1600, 1700])
69
+ mid_result = suggest_price(tomato, half_life, [1600, 1700])
70
+ late_result = suggest_price(tomato, 6, [1600, 1700])
71
+
72
+ print(f" Day 0 (fresh): {fresh_result['suggested_price']:>7.0f} UGX β€” {fresh_result['freshness_label']}")
73
+ print(f" Day 3.5 (half-life): {mid_result['suggested_price']:>7.0f} UGX β€” {mid_result['freshness_label']}")
74
+ print(f" Day 6 (near-exp): {late_result['suggested_price']:>7.0f} UGX β€” {late_result['freshness_label']}")
75
+ print()
76
+ price_drop = (fresh_result['suggested_price'] - mid_result['suggested_price'])
77
+ print(f" β†’ At half-life, price drops {price_drop:.0f} UGX ({price_drop/fresh_result['suggested_price']*100:.0f}%)")
78
+ print(" β†’ This is the inflection point β€” aggressive discounting begins")
79
+ print(" β†’ Exactly where the sigmoid inflects: steepest rate of change")
80
+
81
+
82
+ def demo_sms():
83
+ """Show SMS output for different freshness levels."""
84
+ print("\n" + "━"*60)
85
+ print(" DEMO 4: SMS Output (African Market Feature)")
86
+ print("━"*60)
87
+
88
+ tomato = Product(
89
+ sku="TOM", cost=1000, shelf_life_days=7,
90
+ p_ref=1800, Q0=50, alpha=1.5,
91
+ )
92
+
93
+ for age in [0, 3, 5, 6]:
94
+ result = suggest_price(tomato, age, [1600, 1700, 1900])
95
+ sms = format_sms(result, "UGX")
96
+ print(f" Day {age}: [{len(sms):>3}chr] {sms}")
97
+
98
+
99
+ def demo_simulation():
100
+ """Full 7-day comparison across 3 strategies."""
101
+ tomato = Product(
102
+ sku="TOMATO", cost=1000, shelf_life_days=7,
103
+ p_ref=1800, Q0=50, alpha=1.5,
104
+ )
105
+ print_comparison_report(tomato, [1600, 1700, 1900])
106
+
107
+ # Second product: bread (shorter shelf life, different dynamics)
108
+ bread = Product(
109
+ sku="BREAD", cost=2500, shelf_life_days=3,
110
+ p_ref=4000, Q0=30, alpha=2.0,
111
+ k=10.0 # Sharper cliff for bread
112
+ )
113
+ print_comparison_report(bread, [3800, 3900])
114
+
115
+
116
+ if __name__ == "__main__":
117
+ demo_single_price()
118
+ demo_freshness_table()
119
+ demo_what_at_half_life()
120
+ demo_sms()
121
+ demo_simulation()
122
+
123
+ print("\nβœ… All demos complete. Ready for live defense.")
pricer.py ADDED
@@ -0,0 +1,457 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ pricer.py β€” Perishable Goods Dynamic Pricing Engine
3
+ AIMS KTT Hackathon Solution
4
+ Author: [Your Name]
5
+
6
+ CORE IDEA:
7
+ Price should fall as goods age β€” but not linearly.
8
+ Freshness drops like a sigmoid cliff near expiry.
9
+ We find the price that maximizes (profit per unit) Γ— (expected demand).
10
+ """
11
+
12
+ import math
13
+ import argparse
14
+ from dataclasses import dataclass
15
+ from typing import Optional
16
+
17
+
18
+ # ─────────────────────────────────────────────
19
+ # 1. DATA MODEL
20
+ # ─────────────────────────────────────────────
21
+
22
+ @dataclass
23
+ class Product:
24
+ """Everything the engine needs to know about one SKU."""
25
+ sku: str
26
+ cost: float # What you paid per unit (UGX / KES / local currency)
27
+ shelf_life_days: int # Days until unsellable
28
+ p_ref: float # "Normal" full-price when perfectly fresh
29
+ Q0: float # Expected daily demand at reference price & full freshness
30
+ alpha: float = 1.5 # Price sensitivity: higher = customers more price-sensitive
31
+ margin_floor: float = 1.10 # Never sell below cost Γ— this (e.g. 1.10 = 10% min margin)
32
+ k: float = 8.0 # Sigmoid sharpness: higher = sharper freshness cliff
33
+
34
+
35
+ # ─────────────────────────────────────────────
36
+ # 2. FRESHNESS FUNCTION (The Heart of the Engine)
37
+ # ─────────────────────────────────────────────
38
+
39
+ def freshness_factor(age_days: float, shelf_life: int, k: float = 8.0) -> float:
40
+ """
41
+ Returns a value in (0, 1] representing how 'fresh' the product is.
42
+
43
+ FORMULA:
44
+ freshness = 1 / (1 + exp(k * (age - shelf_life/2) / shelf_life))
45
+
46
+ WHY SIGMOID, NOT LINEAR?
47
+ - Linear decay: tomato loses equal value every day. Unrealistic.
48
+ - Sigmoid: product stays near full value for the first half of shelf life,
49
+ then VALUE COLLAPSES near expiry β€” matching real buyer psychology.
50
+ - At age=0: freshness β‰ˆ 1.0 (perfectly fresh)
51
+ - At age=half-life: freshness = 0.5 (midpoint β€” important for interviews!)
52
+ - At age=shelf_life: freshness β‰ˆ 0.02 (near-expired, almost worthless)
53
+
54
+ INTERVIEW ANSWER for "What happens at half shelf life?":
55
+ "The price is roughly halfway between full price and minimum floor.
56
+ This is intentional β€” it's the inflection point where we start aggressive
57
+ discounting to clear stock before waste occurs."
58
+ """
59
+ if age_days >= shelf_life:
60
+ return 0.001 # Expired: unsellable (near-zero, not zero, to avoid log errors)
61
+
62
+ ratio = (age_days - shelf_life / 2.0) / shelf_life
63
+ return 1.0 / (1.0 + math.exp(k * ratio))
64
+
65
+
66
+ # ─────────────────────────────────────────────
67
+ # 3. DEMAND MODEL
68
+ # ─────────────────────────────────────────────
69
+
70
+ def expected_demand(price: float, product: Product, freshness: float) -> float:
71
+ """
72
+ Q(p) = Q0 Γ— exp(-Ξ± Γ— (p - p_ref) / p_ref) Γ— freshness
73
+
74
+ HOW TO READ THIS:
75
+ - If p = p_ref (reference price): demand = Q0 Γ— freshness
76
+ - If p > p_ref: demand falls exponentially (customers walk away)
77
+ - If p < p_ref: demand rises (bargain hunters come in)
78
+ - freshness scales everything: near-expired goods sell less regardless of price
79
+
80
+ This is a standard "constant elasticity" demand model β€” simple, defensible,
81
+ and widely used in revenue management literature.
82
+ """
83
+ price_effect = math.exp(-product.alpha * (price - product.p_ref) / product.p_ref)
84
+ return product.Q0 * price_effect * freshness
85
+
86
+
87
+ # ─────────────────────────────────────────────
88
+ # 4. PROFIT FUNCTION
89
+ # ─────────────────────────────────────────────
90
+
91
+ def expected_profit(price: float, product: Product, freshness: float) -> float:
92
+ """
93
+ Profit = (price - cost) Γ— expected_demand(price)
94
+
95
+ We maximise this over a grid of candidate prices.
96
+ Simple grid search β€” fast, transparent, no black box.
97
+ """
98
+ margin = price - product.cost
99
+ if margin <= 0:
100
+ return 0.0 # Never sell at a loss
101
+ return margin * expected_demand(price, product, freshness)
102
+
103
+
104
+ # ─────────────────────────────────────────────
105
+ # 5. COMPETITOR ADJUSTMENT
106
+ # ─────────────────────────────────────────────
107
+
108
+ def apply_competitor_pressure(
109
+ optimal_price: float,
110
+ competitor_prices: list[float],
111
+ beta: float = 0.20
112
+ ) -> float:
113
+ """
114
+ Nudge price down if we're significantly more expensive than competitors.
115
+
116
+ FORMULA:
117
+ if p* > min_competitor:
118
+ discount = Ξ² Γ— (p* - min_competitor) / p*
119
+ p_final = p* Γ— (1 - discount)
120
+
121
+ WHY NOT JUST MATCH LOWEST PRICE?
122
+ Because undercutting on price is a race to the bottom.
123
+ We only adjust partially (Ξ² = 0.20 = 20% of the gap).
124
+ This maintains margin while staying competitive.
125
+ """
126
+ if not competitor_prices:
127
+ return optimal_price
128
+
129
+ min_comp = min(competitor_prices)
130
+
131
+ if optimal_price <= min_comp:
132
+ return optimal_price # Already competitive, no change needed
133
+
134
+ gap_ratio = (optimal_price - min_comp) / optimal_price
135
+ discount_fraction = beta * gap_ratio
136
+ adjusted = optimal_price * (1.0 - discount_fraction)
137
+ return adjusted
138
+
139
+
140
+ # ─────────────────────────────────────────────
141
+ # 6. THE MAIN PRICING FUNCTION
142
+ # ─────────────────────────────────────────────
143
+
144
+ def suggest_price(
145
+ product: Product,
146
+ age_days: float,
147
+ competitor_prices: Optional[list[float]] = None,
148
+ grid_steps: int = 200,
149
+ beta: float = 0.20
150
+ ) -> dict:
151
+ """
152
+ THE CORE ENGINE.
153
+
154
+ Given a product, its age, and competitor prices:
155
+ β†’ Find the price that maximises expected profit
156
+ β†’ Enforce minimum margin floor
157
+ β†’ Adjust for competition
158
+ β†’ Return price + explanation
159
+
160
+ STEPS:
161
+ 1. Compute freshness
162
+ 2. Build price grid from floor to ceiling
163
+ 3. Score each price by expected profit
164
+ 4. Pick winner
165
+ 5. Competitor adjustment
166
+ 6. Enforce margin floor (safety net)
167
+ 7. Return result + reasoning
168
+ """
169
+ competitor_prices = competitor_prices or []
170
+
171
+ # Step 1: How fresh is this product right now?
172
+ freshness = freshness_factor(age_days, product.shelf_life_days, product.k)
173
+
174
+ # Step 2: Build candidate price range
175
+ min_price = product.cost * product.margin_floor # Hard floor: must cover cost
176
+ max_price = product.p_ref * 1.5 # Ceiling: don't be absurd
177
+
178
+ if min_price >= max_price:
179
+ # Edge case: cost is too high relative to market β€” flag it
180
+ return {
181
+ "sku": product.sku,
182
+ "suggested_price": min_price,
183
+ "freshness": round(freshness, 3),
184
+ "expected_demand": 0.0,
185
+ "expected_profit": 0.0,
186
+ "note": "WARNING: Cost floor exceeds max price. Check cost data.",
187
+ "age_days": age_days,
188
+ }
189
+
190
+ # Step 3: Grid search β€” test 200 candidate prices
191
+ # KEY INSIGHT: as freshness drops, the "effective" demand ceiling drops,
192
+ # so the profit-maximising price shifts LEFT (lower).
193
+ # We also shrink max_price by freshness so we search a relevant range.
194
+ effective_max = min_price + (max_price - min_price) * max(freshness, 0.05)
195
+
196
+ step = (effective_max - min_price) / grid_steps
197
+ best_price = min_price
198
+ best_profit = 0.0
199
+
200
+ for i in range(grid_steps + 1):
201
+ candidate = min_price + i * step
202
+ profit = expected_profit(candidate, product, freshness)
203
+ if profit > best_profit:
204
+ best_profit = profit
205
+ best_price = candidate
206
+
207
+ # Step 4: Apply competitor pressure
208
+ adjusted_price = apply_competitor_pressure(best_price, competitor_prices, beta)
209
+
210
+ # Step 5: Enforce margin floor (safety net β€” can never go below this)
211
+ final_price = max(adjusted_price, product.cost * product.margin_floor)
212
+
213
+ # Step 6: Build explanation (critical for interviews + SMS output)
214
+ days_left = product.shelf_life_days - age_days
215
+ demand_at_price = expected_demand(final_price, product, freshness)
216
+
217
+ if freshness > 0.8:
218
+ freshness_label = "FRESH"
219
+ elif freshness > 0.5:
220
+ freshness_label = "GOOD"
221
+ elif freshness > 0.2:
222
+ freshness_label = "AGING"
223
+ else:
224
+ freshness_label = "CLEAR NOW"
225
+
226
+ return {
227
+ "sku": product.sku,
228
+ "suggested_price": round(final_price, 2),
229
+ "freshness": round(freshness, 3),
230
+ "freshness_label": freshness_label,
231
+ "days_left": round(days_left, 1),
232
+ "expected_demand_units": round(demand_at_price, 1),
233
+ "expected_daily_profit": round((final_price - product.cost) * demand_at_price, 2),
234
+ "margin_pct": round((final_price - product.cost) / product.cost * 100, 1),
235
+ "competitor_min": round(min(competitor_prices), 2) if competitor_prices else None,
236
+ "note": freshness_label,
237
+ }
238
+
239
+
240
+ # ─────────────────────────────────────────────
241
+ # 7. SIMULATION ENGINE
242
+ # ────────────────────���────────────────────────
243
+
244
+ def simulate_7_days(product: Product, competitor_prices: list[float], strategy: str = "engine") -> dict:
245
+ """
246
+ Run a 7-day simulation for one product under a given strategy.
247
+
248
+ STRATEGIES:
249
+ "engine" β€” Our dynamic pricing engine
250
+ "cost_plus" β€” Naive: always sell at cost Γ— 1.30
251
+ "cheapest" β€” Always match the cheapest competitor
252
+ """
253
+ total_profit = 0.0
254
+ total_units_sold = 0.0
255
+ total_waste = 0.0
256
+ daily_log = []
257
+
258
+ opening_stock = product.Q0 * product.shelf_life_days * 0.8 # Realistic starting stock
259
+ stock = opening_stock
260
+
261
+ for day in range(product.shelf_life_days):
262
+ age = day # Age in days (0 = just stocked)
263
+ freshness = freshness_factor(age, product.shelf_life_days, product.k)
264
+
265
+ # Determine price by strategy
266
+ if strategy == "engine":
267
+ result = suggest_price(product, age, competitor_prices)
268
+ price = result["suggested_price"]
269
+
270
+ elif strategy == "cost_plus":
271
+ price = product.cost * 1.30 # Simple 30% markup, never changes
272
+
273
+ elif strategy == "cheapest":
274
+ comp_min = min(competitor_prices) if competitor_prices else product.p_ref
275
+ price = max(comp_min, product.cost * product.margin_floor)
276
+
277
+ # Demand realisation: add small randomness to simulate real market
278
+ demand = expected_demand(price, product, freshness)
279
+ # Slight noise: Β±10%
280
+ import random
281
+ random.seed(day * 7 + hash(strategy) % 100)
282
+ noise = random.uniform(0.90, 1.10)
283
+ actual_demand = demand * noise
284
+
285
+ # Units sold = min(demand, available stock)
286
+ units_sold = min(actual_demand, stock)
287
+ revenue = units_sold * price
288
+ cost_of_goods = units_sold * product.cost
289
+ profit = revenue - cost_of_goods
290
+
291
+ stock -= units_sold
292
+
293
+ # Track waste on last day
294
+ if day == product.shelf_life_days - 1:
295
+ waste = max(stock, 0)
296
+ total_waste += waste
297
+ stock = 0
298
+
299
+ total_profit += profit
300
+ total_units_sold += units_sold
301
+
302
+ daily_log.append({
303
+ "day": day + 1,
304
+ "price": round(price, 2),
305
+ "freshness": round(freshness, 3),
306
+ "units_sold": round(units_sold, 1),
307
+ "profit": round(profit, 2),
308
+ "stock_remaining": round(stock, 1),
309
+ })
310
+
311
+ # Final waste: any remaining stock
312
+ total_waste += max(stock, 0)
313
+
314
+ return {
315
+ "strategy": strategy,
316
+ "total_profit": round(total_profit, 2),
317
+ "total_units_sold": round(total_units_sold, 1),
318
+ "waste_units": round(total_waste, 1),
319
+ "waste_pct": round(total_waste / opening_stock * 100, 1),
320
+ "avg_daily_profit": round(total_profit / product.shelf_life_days, 2),
321
+ "daily_log": daily_log,
322
+ }
323
+
324
+
325
+ # ─────────────────────────────────────────────
326
+ # 8. SMS FORMATTER (African Market Feature)
327
+ # ─────────────────────────────────────────────
328
+
329
+ def format_sms(result: dict, currency: str = "UGX") -> str:
330
+ """
331
+ Format pricing recommendation as an SMS under 160 characters.
332
+
333
+ DESIGN PRINCIPLE:
334
+ Vendors in low-bandwidth markets use feature phones.
335
+ 160 chars = one SMS = one price recommendation.
336
+ No app needed. No smartphone needed. No internet needed.
337
+ """
338
+ sku = result["sku"][:6].upper()
339
+ price = int(result["suggested_price"])
340
+ label = result.get("freshness_label", "OK")
341
+ days = result.get("days_left", "?")
342
+ margin = result.get("margin_pct", 0)
343
+
344
+ msg = (
345
+ f"PRICE:{sku} {currency}{price} [{label}] "
346
+ f"{days}d left. Margin:{margin}%. "
347
+ f"Reply HELP for options."
348
+ )
349
+
350
+ # Truncate to 160 if needed (shouldn't happen with this template)
351
+ return msg[:160]
352
+
353
+
354
+ # ─────────────────────────────────────────────
355
+ # 9. REPORT GENERATOR
356
+ # ─────────────────────────────────────────────
357
+
358
+ def print_comparison_report(product: Product, competitor_prices: list[float]):
359
+ """Run all 3 strategies and print a clean comparison table."""
360
+ print("\n" + "="*60)
361
+ print(f" 7-DAY SIMULATION REPORT: {product.sku}")
362
+ print("="*60)
363
+ print(f" Cost/unit: {product.cost} | Shelf life: {product.shelf_life_days}d")
364
+ print(f" Ref price: {product.p_ref} | Competitors: {competitor_prices}")
365
+ print("-"*60)
366
+
367
+ strategies = ["engine", "cost_plus", "cheapest"]
368
+ results = {}
369
+
370
+ for strat in strategies:
371
+ r = simulate_7_days(product, competitor_prices, strat)
372
+ results[strat] = r
373
+
374
+ # Header
375
+ print(f"{'Strategy':<18} {'Profit':>10} {'Units':>8} {'Waste%':>8} {'Avg/Day':>10}")
376
+ print("-"*60)
377
+
378
+ for strat, r in results.items():
379
+ label = {
380
+ "engine": "Dynamic Engine",
381
+ "cost_plus": "Cost+30% (Naive)",
382
+ "cheapest": "Match Cheapest",
383
+ }[strat]
384
+ print(
385
+ f"{label:<18} "
386
+ f"{r['total_profit']:>10.2f} "
387
+ f"{r['total_units_sold']:>8.1f} "
388
+ f"{r['waste_pct']:>7.1f}% "
389
+ f"{r['avg_daily_profit']:>10.2f}"
390
+ )
391
+
392
+ # Lift calculation
393
+ engine_profit = results["engine"]["total_profit"]
394
+ baseline_profit = results["cost_plus"]["total_profit"]
395
+ if baseline_profit > 0:
396
+ lift = (engine_profit - baseline_profit) / baseline_profit * 100
397
+ print(f"\n βœ… Engine vs Naive: {lift:+.1f}% profit improvement")
398
+ print(f" βœ… Waste reduction: "
399
+ f"{results['cost_plus']['waste_pct'] - results['engine']['waste_pct']:.1f}% less waste")
400
+
401
+ print("="*60)
402
+
403
+ # Sample SMS output
404
+ print("\n πŸ“± SAMPLE SMS OUTPUT (Day 3):")
405
+ day3_result = suggest_price(product, 3.0, competitor_prices)
406
+ sms = format_sms(day3_result)
407
+ print(f" [{len(sms)} chars] {sms}")
408
+ print("="*60)
409
+
410
+ return results
411
+
412
+
413
+ # ─────────────────────────────────────────────
414
+ # 10. CLI INTERFACE
415
+ # ─────────────────────────────────────────────
416
+
417
+ def main():
418
+ parser = argparse.ArgumentParser(
419
+ description="Perishable Goods Dynamic Pricing Engine β€” AIMS KTT Hackathon"
420
+ )
421
+ parser.add_argument("--sku", default="TOMATO", help="Product SKU")
422
+ parser.add_argument("--cost", type=float, default=1000, help="Cost per unit (local currency)")
423
+ parser.add_argument("--shelf-life", type=int, default=7, help="Shelf life in days")
424
+ parser.add_argument("--ref-price", type=float, default=1800, help="Reference (full) price")
425
+ parser.add_argument("--q0", type=float, default=50, help="Base daily demand at ref price")
426
+ parser.add_argument("--alpha", type=float, default=1.5, help="Price sensitivity (demand model)")
427
+ parser.add_argument("--age", type=float, default=0, help="Current age in days (for single query)")
428
+ parser.add_argument("--competitors", type=float, nargs="*", default=[1600, 1700, 1900],
429
+ help="Competitor prices (space-separated)")
430
+ parser.add_argument("--simulate", action="store_true", help="Run 7-day simulation")
431
+ parser.add_argument("--currency", default="UGX", help="Currency label for SMS output")
432
+
433
+ args = parser.parse_args()
434
+
435
+ product = Product(
436
+ sku=args.sku,
437
+ cost=args.cost,
438
+ shelf_life_days=args.shelf_life,
439
+ p_ref=args.ref_price,
440
+ Q0=args.q0,
441
+ alpha=args.alpha,
442
+ )
443
+
444
+ if args.simulate:
445
+ print_comparison_report(product, args.competitors)
446
+ else:
447
+ result = suggest_price(product, args.age, args.competitors)
448
+ print("\nπŸ“¦ PRICING RECOMMENDATION")
449
+ print("-" * 40)
450
+ for k, v in result.items():
451
+ print(f" {k:<25}: {v}")
452
+ print("\nπŸ“± SMS FORMAT:")
453
+ print(f" {format_sms(result, args.currency)}")
454
+
455
+
456
+ if __name__ == "__main__":
457
+ main()