-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp.py
More file actions
813 lines (668 loc) · 35.7 KB
/
Copy pathapp.py
File metadata and controls
813 lines (668 loc) · 35.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
import streamlit as st
import os
import joblib
import pandas as pd
import numpy as np
from datetime import datetime
import subprocess
import json
import sys
import time
import random
from urllib.parse import urlparse
# No longer an async app, so direct imports are fine.
# We will call the scraper via a subprocess.
from src.matcher import get_image_hash, are_images_similar, are_titles_similar
from src.optimizer import load_model_and_features, optimize_price, COST_PER_UNIT
from src.search import find_competitor_urls
from thefuzz import fuzz
from typing import Optional # New import for Optional type hint
# --- CURRENCY CONVERSION ---
# Comprehensive exchange rates to USD (updated January 2025)
DEFAULT_RATES_TO_USD = {
# Major World Currencies
"EUR": 1.08, # Euro
"GBP": 1.26, # British Pound
"JPY": 0.0067, # Japanese Yen
"CHF": 1.14, # Swiss Franc
"CAD": 0.73, # Canadian Dollar
"AUD": 0.65, # Australian Dollar
"NZD": 0.60, # New Zealand Dollar
# Asian Currencies
"CNY": 0.14, # Chinese Yuan
"INR": 0.012, # Indian Rupee
"KRW": 0.00073, # South Korean Won
"SGD": 0.74, # Singapore Dollar
"HKD": 0.13, # Hong Kong Dollar
"TWD": 0.031, # Taiwan Dollar
"THB": 0.029, # Thai Baht
"MYR": 0.22, # Malaysian Ringgit
"IDR": 0.000063, # Indonesian Rupiah
"PHP": 0.017, # Philippine Peso
"VND": 0.000040, # Vietnamese Dong
# European Currencies (Non-Euro)
"SEK": 0.096, # Swedish Krona
"NOK": 0.093, # Norwegian Krone
"DKK": 0.14, # Danish Krone
"PLN": 0.25, # Polish Zloty
"CZK": 0.043, # Czech Koruna
"HUF": 0.0027, # Hungarian Forint
"RON": 0.22, # Romanian Leu
# Middle Eastern & African Currencies
"AED": 0.27, # UAE Dirham
"SAR": 0.27, # Saudi Riyal
"ILS": 0.28, # Israeli Shekel
"ZAR": 0.055, # South African Rand
"EGP": 0.020, # Egyptian Pound
"TRY": 0.029, # Turkish Lira
# Eastern European & CIS
"RUB": 0.010, # Russian Ruble
"UAH": 0.024, # Ukrainian Hryvnia
"GEL": 0.37, # Georgian Lari
# Latin American Currencies
"MXN": 0.050, # Mexican Peso
"BRL": 0.17, # Brazilian Real
"ARS": 0.0010, # Argentine Peso
"CLP": 0.0010, # Chilean Peso
"COP": 0.00024, # Colombian Peso
}
def convert_to_usd(price: float, currency: str) -> Optional[float]:
"""
Converts a given price to USD using fixed exchange rates from DEFAULT_RATES_TO_USD.
Supports 40+ major world currencies with rates updated for January 2025.
"""
if price is None:
return None
if currency.upper() in ['$', 'USD']:
return price
# Check if currency is in our supported list
if currency.upper() in DEFAULT_RATES_TO_USD:
rate = DEFAULT_RATES_TO_USD[currency.upper()]
return price * rate
# If currency not found, warn and use 1:1 conversion
st.warning(f"⚠️ Exchange rate for '{currency}' not found in our database of 40+ currencies. Assuming 1:1 conversion to USD. Supported currencies: {', '.join(sorted(DEFAULT_RATES_TO_USD.keys())[:10])}...")
return price # Assume 1:1 conversion as fallback
# --- Hide Streamlit's default UI elements ---
hide_streamlit_style = """
<style>
#MainMenu {visibility: hidden;}
footer {visibility: hidden;}
.stDeployButton {display: none;}
.stAppDeployButton {display: none;}
body {
background: linear-gradient(to bottom, #003300, #000033);
}
.stProgress > div > div > div > div {
background-color: #dff9ba; /* Tea Green for filled portion */
}
.stProgress > div > div > div {
background-color: #2C4F4F; /* Darker grey for empty track */
}
</style>
"""
st.markdown(hide_streamlit_style, unsafe_allow_html=True)
# Set page config
st.set_page_config(layout="wide", page_title="Dynamic Competitor Intelligence & Price Elasticity Engine")
def run_scraper_subprocess(url: str) -> dict:
"""
Runs the scraper.py script as an external process to avoid asyncio conflicts.
Args:
url (str): The URL to scrape.
Returns:
dict: The scraped product information as a dictionary.
"""
python_executable = sys.executable # Use the same python that runs streamlit
scraper_script_path = os.path.join(os.path.dirname(__file__), "src", "scraper.py")
command = [python_executable, scraper_script_path, url]
try:
result = subprocess.run(command, capture_output=True, text=True, check=True, timeout=60)
return json.loads(result.stdout)
except subprocess.CalledProcessError as e:
return {"error": f"Scraper subprocess failed with exit code {e.returncode}: {e.stderr}"}
except subprocess.TimeoutExpired:
return {"error": "Scraper timed out after 60 seconds. The website may be slow or blocking access."}
except json.JSONDecodeError as e:
return {"error": f"Failed to decode JSON output from scraper. Raw output: {result.stdout[:200] if 'result' in locals() else 'N/A'}"}
except Exception as e:
return {"error": f"An unexpected error occurred while running the scraper subprocess: {e}"}
# --- Global Data/Model Loading ---
# Ensure models and features are loaded only once
@st.cache_resource
def get_loaded_model_data():
current_dir = os.path.dirname(os.path.abspath(__file__))
model_full_path = os.path.join(current_dir, "demand_model.joblib")
features_full_path = os.path.join(current_dir, "model_features.joblib")
return load_model_and_features(model_full_path, features_full_path)
model, features = get_loaded_model_data()
if model is None or features is None:
st.error("🚨 Error: Could not load the demand prediction model or its features. Please ensure 'demand_model.joblib' and 'model_features.joblib' are in the root directory and try again.")
st.stop()
# --- Streamlit App Layout ---
st.title("Dynamic Competitor Intelligence & Price Elasticity Engine")
st.markdown("""
This AI agent automatically scrapes competitor websites, understands product images/descriptions,
and suggests the optimal price for your product to maximize profit.
""")
st.divider()
# --- User Inputs ---
st.subheader("1. Enter Your Product & Financials")
col1, col2, col3 = st.columns(3)
with col1:
your_product_url = st.text_input(
"Your Product URL",
value="", # Removed default value
placeholder="https://www.amazon.com/your-product-url",
help="Enter the URL of the product you want to price."
)
url_error_placeholder = st.empty()
with col2:
user_cost_per_unit = st.number_input(
"Your Cost Per Unit ($)",
min_value=0.0,
value=None, # Use None for placeholder to show
format="%.2f",
placeholder="e.g. 25.00",
help="Enter your cost to acquire one unit of this product."
)
cost_error_placeholder = st.empty()
with col3:
user_inventory = st.number_input(
"Available Inventory",
min_value=0,
value=None, # Use None for placeholder to show
placeholder="e.g. 50",
step=1,
help="Enter your available stock. Must be greater than 0."
)
inventory_error_placeholder = st.empty()
st.markdown("---")
if st.button("Analyze Market & Recommend Price", use_container_width=True, type="primary"):
# --- Input Validation ---
# Clear previous errors
url_error_placeholder.empty()
cost_error_placeholder.empty()
inventory_error_placeholder.empty()
# Check for errors
has_error = False
if not your_product_url:
url_error_placeholder.error("👆 Please provide a Product URL.")
has_error = True
if user_cost_per_unit is None or user_cost_per_unit <= 0.0:
cost_error_placeholder.error("👆 Cost must be > $0.00.")
has_error = True
elif user_cost_per_unit > 50000.0:
cost_error_placeholder.error("👆 Cost seems unusually high ($50,000+). Please verify this is correct.")
has_error = True
if user_inventory is None or user_inventory <= 0:
inventory_error_placeholder.error("👆 Inventory must be > 0.")
has_error = True
elif user_inventory > 1000000:
inventory_error_placeholder.error("👆 Inventory seems unusually high (1M+). Please verify this is correct.")
has_error = True
if has_error:
st.stop()
else: # Only proceed if validation passes
st.subheader("Market Analysis in Progress...")
progress_bar = st.progress(0)
status_text = st.empty()
# --- Step 1: Scrape your product (with retry) ---
status_text.text("1/5: Scraping your product details... (this may take up to 30 seconds)")
progress_bar.progress(10)
max_retries = 2
your_product_info = None
for attempt in range(max_retries):
if attempt > 0:
status_text.text(f"1/5: Retry {attempt}/{max_retries-1} - Scraping your product...")
import time
time.sleep(3) # Wait 3 seconds before retry
your_product_info = run_scraper_subprocess(your_product_url)
if "error" not in your_product_info:
break # Success!
if attempt == max_retries - 1:
# Final attempt failed
st.error(f"""
Failed to scrape your product after {max_retries} attempts: {your_product_info['error']}
**Common fixes:**
- Wait a few minutes and try again (Amazon may be rate-limiting)
- Try a different product URL
- Ensure the URL is a direct product page (contains /dp/ for Amazon or /itm/ for eBay)
""")
st.stop()
if your_product_info.get('name') == 'Title not found':
st.error("Could not find the product title on your page. The website may be blocking the scraper or has changed its layout.")
st.stop()
# Validate essential product data
if not your_product_info.get('price') or your_product_info.get('price') <= 0:
st.error("⚠️ Could not extract a valid price from your product page. The scraper may have been blocked or the page layout is unusual.")
st.stop()
if not your_product_info.get('image_url') or your_product_info.get('image_url') == 'Image not found':
st.warning("⚠️ Could not extract product image. Competitor matching will rely only on title similarity, which may be less accurate.")
st.success("Your product scraped successfully!")
# --- Step 2: Dynamically Find Competitors ---
progress_bar.progress(20)
status_text.text("2/5: Searching for competitors online...")
sites_to_search = ['ebay.com', 'amazon.com']
found_urls = find_competitor_urls(your_product_info.get('name'), sites_to_search, max_results_per_site=10)
competitor_urls = list(set([url for url in found_urls if any(site in url for site in sites_to_search)])) # De-duplicate
# Limit to max 15 URLs to keep scraping fast
if len(competitor_urls) > 15:
competitor_urls = competitor_urls[:15]
if not competitor_urls:
st.warning("Could not dynamically find any valid competitor URLs. Cannot perform optimization.")
st.stop()
status_text.text(f"2/5: Found {len(competitor_urls)} potential competitors. Now scraping and matching...")
# --- Step 3: Scrape & Match Competitors ---
progress_bar.progress(30)
raw_competitors = []
scraping_log = []
your_product_image_hash = get_image_hash(your_product_info.get('image_url'))
st.write(f"*(Found {len(competitor_urls)} potential competitor links. Proceeding to scrape...)*")
for i, comp_url in enumerate(competitor_urls):
progress_bar.progress(30 + int((i + 1) / len(competitor_urls) * 30))
status_text.text(f"3/5: Scraping competitor {i+1}/{len(competitor_urls)}...")
if comp_url == your_product_url:
scraping_log.append({'url': comp_url, 'status': 'Skipped', 'reason': 'Same as input URL'})
continue
# Add delay to be less aggressive
time.sleep(random.uniform(1, 2))
comp_product_info = run_scraper_subprocess(comp_url)
if "error" in comp_product_info or not comp_product_info.get('price') or comp_product_info.get('name') == 'Title not found':
reason = comp_product_info.get('error', 'Could not find critical details (name/price)')
scraping_log.append({'url': comp_url, 'status': 'Failed', 'reason': reason})
continue
comp_product_info['url'] = comp_url
scraping_log.append({'url': comp_url, 'status': 'Success', 'reason': ''})
# --- Matching Logic ---
comp_product_image_hash = get_image_hash(comp_product_info.get('image_url'))
title_sim_score = fuzz.token_set_ratio(your_product_info.get('name', ''), comp_product_info.get('name', ''))
image_sim_score = 0
# Check if we have valid image hashes for comparison
has_images = your_product_image_hash and comp_product_image_hash
if has_images:
image_sim_score = 100 - (your_product_image_hash - comp_product_image_hash)
# Determine matching strategy based on image availability
if has_images:
# Both images available - use combined scoring
# If images are clearly different, disqualify the competitor
if image_sim_score < 60:
scraping_log.append({'url': comp_url, 'status': 'Rejected', 'reason': f'Image similarity too low ({image_sim_score:.0f}%)'})
continue
# Balanced weighting between title and image similarity
combined_similarity_score = (title_sim_score * 0.6) + (image_sim_score * 0.4)
threshold = 65
else:
# Fallback to title-only matching if images unavailable
combined_similarity_score = title_sim_score
threshold = 75 # Higher threshold for title-only matching
# Accept competitor if similarity meets threshold
if combined_similarity_score >= threshold:
comp_product_info['similarity_score'] = combined_similarity_score
raw_competitors.append(comp_product_info)
else:
scraping_log.append({'url': comp_url, 'status': 'Rejected', 'reason': f'Combined similarity too low ({combined_similarity_score:.0f}%)'})
# --- "Fail Smarter" Check ---
successful_scrapes = sum(1 for entry in scraping_log if entry['status'] == 'Success')
total_attempts = len(competitor_urls)
# Avoid division by zero if no attempts were made (should be caught earlier, but for safety)
if total_attempts > 0:
success_rate = successful_scrapes / total_attempts
MIN_SUCCESS_RATE = 0.25 # If less than 25% of initial links were successfully scraped
if success_rate < MIN_SUCCESS_RATE:
st.error(f"🚨 Widespread Scraping Failure: Only {successful_scrapes}/{total_attempts} ({success_rate:.0%}) competitor links were successfully scraped. Amazon's anti-bot measures are actively blocking most requests. Please try again later, use a different product, or consider a dedicated scraping service.")
with st.expander("View Full Scraping Log"):
st.dataframe(pd.DataFrame(scraping_log))
st.stop()
# --- Step 4: Data Cleaning & Outlier Removal ---
status_text.text("4/5: Cleaning data and removing outliers...")
progress_bar.progress(70)
if not raw_competitors:
st.warning("No matching competitors found after scraping and filtering.")
st.dataframe(pd.DataFrame(scraping_log))
st.stop()
# 1. Absurd Price Filter - now dynamic
your_product_price_usd = convert_to_usd(your_product_info.get('price'), your_product_info.get('currency'))
if your_product_price_usd:
MIN_PRICE_THRESHOLD = your_product_price_usd * 0.5
else:
MIN_PRICE_THRESHOLD = 5.0 # A safe, low default if own price isn't found
competitors_after_min_price_filter = []
for comp in raw_competitors:
price_usd = convert_to_usd(comp.get('price'), comp.get('currency'))
if price_usd is None:
scraping_log.append({'url': comp['url'], 'status': 'Rejected', 'reason': 'Currency conversion failed'})
elif price_usd < MIN_PRICE_THRESHOLD:
scraping_log.append({'url': comp['url'], 'status': 'Rejected', 'reason': f'Price ${price_usd:.2f} is below dynamic threshold of ${MIN_PRICE_THRESHOLD:.2f}'})
else:
competitors_after_min_price_filter.append(comp)
# 2. Statistical Outlier Removal
valid_competitors = []
if len(competitors_after_min_price_filter) > 2:
prices = [convert_to_usd(c.get('price'), c.get('currency')) for c in competitors_after_min_price_filter]
prices = [p for p in prices if p is not None]
if prices:
mean_price = np.mean(prices)
std_dev = np.std(prices)
# Keep prices within 2 standard deviations of the mean
price_floor = mean_price - 2 * std_dev
price_ceiling = mean_price + 2 * std_dev
for comp in competitors_after_min_price_filter:
price_usd = convert_to_usd(comp.get('price'), comp.get('currency'))
if price_floor <= price_usd <= price_ceiling:
valid_competitors.append(comp)
else:
scraping_log.append({'url': comp['url'], 'status': 'Rejected', 'reason': f'Price ${price_usd:.2f} is a statistical outlier'})
else:
valid_competitors = competitors_after_min_price_filter
if not valid_competitors:
st.warning("No valid competitors remained after data cleaning and outlier removal.")
st.dataframe(pd.DataFrame(scraping_log))
st.stop()
if len(valid_competitors) < 6:
st.warning(f"Only {len(valid_competitors)} top competitors remained after filtering. For more robust recommendations, aim for at least 6 valid competitors.")
# --- Step 5: Market Analysis & "Smart" Price Recommendation ---
status_text.text("5/5: Analyzing market and generating smart recommendation...")
progress_bar.progress(80)
# --- Calculate Market Prices ---
total_sales_volume = sum(p.get('sales_volume', 0) for p in valid_competitors)
# Convert all competitor prices to USD, filtering out None values
competitor_prices_usd = []
for p in valid_competitors:
if p.get('price'):
price_usd = convert_to_usd(p.get('price'), p.get('currency'))
if price_usd is not None:
competitor_prices_usd.append(price_usd)
# Check if we have any valid prices
if not competitor_prices_usd:
st.error("⚠️ Could not convert any competitor prices to USD. All currency conversions failed. Cannot generate a recommendation.")
st.stop()
simple_avg_market_price = sum(competitor_prices_usd) / len(competitor_prices_usd)
weighted_avg_market_price = 0
if total_sales_volume > 0:
total_weighted_price = 0
for p in valid_competitors:
if p.get('price'):
price_usd = convert_to_usd(p.get('price'), p.get('currency'))
if price_usd is not None:
total_weighted_price += price_usd * p.get('sales_volume', 0)
weighted_avg_market_price = total_weighted_price / total_sales_volume
else:
weighted_avg_market_price = simple_avg_market_price
if weighted_avg_market_price <= 0:
st.error("⚠️ Could not determine a valid market price from competitors. The calculated average is zero or negative.")
st.stop()
# --- VIABILITY CHECK ---
profit_margin_percent = ((weighted_avg_market_price - user_cost_per_unit) / weighted_avg_market_price) * 100
if user_cost_per_unit > weighted_avg_market_price:
st.error(f"""
🚨 **UNPROFITABLE PRODUCT ALERT**
Your cost per unit (${user_cost_per_unit:.2f}) is **HIGHER** than the market average price (${weighted_avg_market_price:.2f}).
**This means you cannot sell this product profitably at current market prices.**
**Options:**
1. Reduce your cost per unit
2. Find a different market/platform with higher prices
3. Add unique value to justify premium pricing
The analysis will continue, but keep in mind that recommended prices may still result in losses.
""")
elif profit_margin_percent < 10:
st.warning(f"""
⚠️ **Low Profit Margin Warning**
Your cost (${user_cost_per_unit:.2f}) leaves only a **{profit_margin_percent:.1f}% profit margin** at market average (${weighted_avg_market_price:.2f}).
This is very thin. Consider if this product is worth selling.
""")
progress_bar.progress(100)
status_text.empty()
# --- Display Results ---
st.balloons()
st.markdown("---")
# --- NEW "SMART RECOMMENDATION" LOGIC ---
# This is where the magic happens. We use our trained model to find the optimal price.
icon_html = """
<link rel="stylesheet" href="https://cdn-uicons.flaticon.com/uicons-regular-rounded/css/uicons-regular-rounded.css">
<h3><i class="fi fi-rr-chart-histogram" style="font-size: 0.8em;"></i> AI Price Optimization</h3>
"""
st.markdown(icon_html, unsafe_allow_html=True)
with st.spinner("🧠 Running pricing model to find the sweet spot..."):
# 1. COST-BASED PRICING: Try to get 50% profit margin (Cost × 1.5)
ideal_price = user_cost_per_unit * 1.5 # 50% profit margin
# Check if ideal price crosses market average
if ideal_price <= weighted_avg_market_price:
# GOOD SCENARIO: Your ideal price is below market
max_test_price = ideal_price
pricing_scenario = "good"
else:
# MEDIUM SCENARIO: Ideal price would be above market, cap at market average
max_test_price = weighted_avg_market_price
pricing_scenario = "capped"
st.warning(f"""
⚠️ **Profit Margin Constraint**
- Your ideal price: ${ideal_price:.2f} (50% profit)
- Market average: ${weighted_avg_market_price:.2f}
To stay competitive, we'll cap at market average. Your profit margin will be lower than 50%.
""")
# Ensure we have a valid range (test from cost to max_test_price)
price_range = np.arange(user_cost_per_unit, max_test_price + 1.0, 1.0)
if len(price_range) < 10:
# Expand range if too narrow
max_test_price = user_cost_per_unit + 20.0
price_range = np.arange(user_cost_per_unit, max_test_price, 1.0)
# 2. Get current day of the week
current_day_of_week = datetime.today().weekday()
# 3. Call the optimizer function
optimal_price, max_profit, predicted_demand = optimize_price(
model=model,
features=features,
current_competitor_price=weighted_avg_market_price,
day_of_week=current_day_of_week,
our_price_range=price_range,
cost_per_unit=user_cost_per_unit
)
if optimal_price is None or max_profit <= 0:
st.error(f"""
⚠️ **No Profitable Price Found**
The AI model could not find a price point that generates profit within the tested range
(${user_cost_per_unit:.2f} to ${max_test_price:.2f}).
**Possible reasons:**
- Your cost (${user_cost_per_unit:.2f}) is too high relative to market prices (${weighted_avg_market_price:.2f})
- The model predicts very low demand at all profitable price points
- Market conditions make this product difficult to sell profitably
**Recommendations:**
1. Review your cost structure
2. Consider a different product
3. Look for alternative markets with higher price tolerance
""")
else:
# Calculate multiple pricing strategies
strategies = []
# Strategy 1: Target Price (Cost × 1.5 = 50% profit)
target_price = user_cost_per_unit * 1.5
if target_price <= weighted_avg_market_price:
target_demand = model.predict(pd.DataFrame([[target_price, weighted_avg_market_price, current_day_of_week]], columns=features))[0]
target_units = min(max(0, target_demand), float(user_inventory))
target_profit = (target_price - user_cost_per_unit) * target_units
target_margin = ((target_price - user_cost_per_unit) / target_price) * 100
strategies.append({
'name': '🎯 Target (50% Profit)',
'price': target_price,
'units': target_units,
'profit': target_profit,
'margin': target_margin,
'description': f'Your goal: Cost × 1.5 = ${target_price:.2f}',
'recommended': True
})
# Strategy 2: AI Optimal
ai_units = min(float(predicted_demand), float(user_inventory))
ai_profit = (optimal_price - user_cost_per_unit) * ai_units
ai_margin = ((optimal_price - user_cost_per_unit) / optimal_price) * 100
strategies.append({
'name': '🏆 AI Optimal',
'price': optimal_price,
'units': ai_units,
'profit': ai_profit,
'margin': ai_margin,
'description': 'AI-recommended based on demand prediction',
'recommended': target_price > weighted_avg_market_price # Recommend if target not achievable
})
# Strategy 3: Market Match
market_price = weighted_avg_market_price
if market_price > user_cost_per_unit:
market_demand = model.predict(pd.DataFrame([[market_price, weighted_avg_market_price, current_day_of_week]], columns=features))[0]
market_units = min(max(0, market_demand), float(user_inventory))
market_profit = (market_price - user_cost_per_unit) * market_units
market_margin = ((market_price - user_cost_per_unit) / market_price) * 100
strategies.append({
'name': '⚡ Market Match',
'price': market_price,
'units': market_units,
'profit': market_profit,
'margin': market_margin,
'description': 'Match market average for direct competition',
'recommended': False
})
# Strategy 4: Budget (Cost × 1.2 = 20% profit, fast sales)
budget_price = user_cost_per_unit * 1.2
if budget_price < weighted_avg_market_price:
budget_demand = model.predict(pd.DataFrame([[budget_price, weighted_avg_market_price, current_day_of_week]], columns=features))[0]
budget_units = min(max(0, budget_demand), float(user_inventory))
budget_profit = (budget_price - user_cost_per_unit) * budget_units
budget_margin = ((budget_price - user_cost_per_unit) / budget_price) * 100
strategies.append({
'name': '💨 Quick Sale',
'price': budget_price,
'units': budget_units,
'profit': budget_profit,
'margin': budget_margin,
'description': 'Low price for maximum volume',
'recommended': False
})
# Display strategies
st.subheader("💡 Pricing Strategies")
# Calculate what the user can achieve
ideal_price = user_cost_per_unit * 1.5
achievable = ideal_price <= weighted_avg_market_price
if achievable:
st.success(f"""
✅ **Great News!** You can achieve 50% profit margin and stay competitive.
- Your Cost: ${user_cost_per_unit:.2f}
- Your Target (Cost × 1.5): ${ideal_price:.2f}
- Market Average: ${weighted_avg_market_price:.2f}
""")
else:
st.warning(f"""
⚠️ **Tight Margins** - Your target price exceeds market average.
- Your Cost: ${user_cost_per_unit:.2f}
- Your Target (Cost × 1.5): ${ideal_price:.2f}
- Market Average: ${weighted_avg_market_price:.2f}
Consider lower margins to stay competitive.
""")
for i, strategy in enumerate(strategies):
with st.container(border=True):
# Add recommended badge
title = f"### {strategy['name']} - ${strategy['price']:.2f}"
if strategy.get('recommended'):
title += " ⭐ **RECOMMENDED**"
st.markdown(title)
st.caption(strategy['description'])
col1, col2, col3, col4 = st.columns(4)
with col1:
st.metric("Price", f"${strategy['price']:.2f}")
with col2:
st.metric("Profit Margin", f"{strategy['margin']:.1f}%")
with col3:
st.metric("Est. Units Sold", f"{strategy['units']:.0f}")
with col4:
st.metric("Total Profit", f"${strategy['profit']:.2f}")
st.markdown("---")
# --- Detailed Breakdown in Tabs ---
st.header("📄 Details")
tab1, tab2, tab3, tab4 = st.tabs(["📦 Your Product", "📊 Market", "🏆 Top 10 Competitors", "📋 Log"])
with tab1:
col1, col2 = st.columns([1, 2])
with col1:
if your_product_info.get('image_url') and your_product_info.get('image_url') != 'Image not found':
st.image(your_product_info.get('image_url'), width=200)
else:
st.info("No image available")
with col2:
st.markdown("**Product Name:**")
st.write(your_product_info.get('name'))
st.markdown("**Your Details:**")
st.metric("Cost per Unit", f"${user_cost_per_unit:.2f}")
st.metric("Available Inventory", f"{user_inventory:,} units")
with tab2:
st.subheader("Market Snapshot")
col1, col2, col3 = st.columns(3)
with col1:
st.metric("Weighted Average Price", f"${weighted_avg_market_price:,.2f}", help="Prices from competitors are weighted by their sales volume to better reflect true market positioning.")
with col2:
st.metric("Simple Average Price", f"${simple_avg_market_price:,.2f}", help="A simple average of all competitor prices, without considering their sales volume.")
with col3:
st.metric("Total Sales Volume Found", f"{int(total_sales_volume):,}", help="The total number of sales or ratings found across all valid competitors. Used for weighting.")
st.markdown(f"Based on **{len(valid_competitors)}** valid competitor products found after cleaning.")
with tab3:
st.subheader(f"Top 10 Competitors (from {len(valid_competitors)} found)")
competitor_df = pd.DataFrame(valid_competitors)
if not competitor_df.empty:
# Convert all prices to USD for display
competitor_df['price_usd'] = competitor_df.apply(
lambda row: convert_to_usd(row['price'], row['currency']), axis=1
)
# Shorten product names to first 60 characters
competitor_df['short_name'] = competitor_df['name'].str[:60] + '...'
# Sort by similarity and take top 10
top_10 = competitor_df.sort_values(by='similarity_score', ascending=False).head(10)
st.dataframe(
top_10[['short_name', 'price_usd', 'sales_volume', 'similarity_score', 'url']],
hide_index=True,
column_config={
"short_name": st.column_config.TextColumn("Product", width="medium"),
"price_usd": st.column_config.NumberColumn("Price (USD)", format="$%.2f"),
"sales_volume": st.column_config.NumberColumn("Sales", help="Number of ratings/reviews"),
"similarity_score": st.column_config.ProgressColumn("Match", format="%d%%", min_value=0, max_value=100),
"url": st.column_config.LinkColumn("Link", display_text="View"),
},
use_container_width=True
)
else:
st.info("No competitors remained after advanced filtering.")
with tab4:
st.subheader("Scraping Summary")
log_df = pd.DataFrame(scraping_log)
if not log_df.empty:
# Show summary stats
status_counts = log_df['status'].value_counts()
col1, col2, col3, col4 = st.columns(4)
with col1:
st.metric("✅ Success", status_counts.get('Success', 0))
with col2:
st.metric("❌ Failed", status_counts.get('Failed', 0))
with col3:
st.metric("🚫 Rejected", status_counts.get('Rejected', 0))
with col4:
st.metric("⏭️ Skipped", status_counts.get('Skipped', 0))
# Show only failed and rejected entries (not successful ones - those are in competitors tab)
st.markdown("**Issues Found:**")
issues_df = log_df[log_df['status'].isin(['Failed', 'Rejected'])].tail(20) # Last 20 issues only
if not issues_df.empty:
st.dataframe(
issues_df[['url', 'status', 'reason']],
hide_index=True,
column_config={
"url": st.column_config.TextColumn("URL", width="medium"),
"status": st.column_config.TextColumn("Status", width="small"),
"reason": st.column_config.TextColumn("Reason", width="large"),
},
use_container_width=True
)
else:
st.success("No issues! All competitors scraped successfully.")
# Optional: Download full log
with st.expander("📥 Download Full Log (All Entries)"):
csv = log_df.to_csv(index=False)
st.download_button(
label="Download CSV",
data=csv,
file_name="scraping_log.csv",
mime="text/csv"
)
else:
st.info("No scraping log available.")