mirror of
https://github.com/nethunterzist/trendyol-analiz
synced 2026-07-01 17:37:04 +00:00
- FastAPI backend with Python - React + Vite admin panel - PostgreSQL database - Trendyol marketplace analytics - GitHub Actions CI/CD workflow Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
314 lines
13 KiB
Python
314 lines
13 KiB
Python
"""
|
||
Hidden Champion Finder - Gizli şampiyon bulucu
|
||
Özelleştirilmiş filtreler ile fırsat ürünlerini bulur
|
||
"""
|
||
from typing import Dict, List, Any, Optional
|
||
from collections import defaultdict
|
||
from .metrics import (
|
||
get_rating_value,
|
||
get_review_count,
|
||
calculate_potential_score
|
||
)
|
||
|
||
|
||
class HiddenChampionFinder:
|
||
"""
|
||
Gizli şampiyonları bulan sınıf
|
||
Parçalı pazarlarda (düşük HHI) özelleştirilmiş filtreler kullanır
|
||
"""
|
||
|
||
def find(
|
||
self,
|
||
products: List[Dict],
|
||
social_data: Dict,
|
||
filters: Optional[Dict] = None
|
||
) -> Dict[str, Any]:
|
||
"""
|
||
Gizli şampiyonları bul (async)
|
||
|
||
Özelleştirilmiş Filtreler:
|
||
- Rating >= 4.6 (yüksek kalite)
|
||
- Review count < 30 (henüz keşfedilmemiş)
|
||
- Social proof (views/baskets) kategorinin 2 katı üzerinde
|
||
|
||
Args:
|
||
products: Ürün listesi
|
||
social_data: Sosyal kanıt verileri
|
||
filters: Filtreleme kriterleri
|
||
|
||
Returns:
|
||
Gizli şampiyon listesi
|
||
"""
|
||
if filters is None:
|
||
filters = {
|
||
"min_rating": 4.6,
|
||
"max_review_count": 30,
|
||
"social_multiplier": 2.0, # Kategori ortalamasının 2 katı
|
||
"min_score": 70,
|
||
"limit": 50
|
||
}
|
||
|
||
# Kategori bazlı ürün sayıları (competition level için)
|
||
category_counts = defaultdict(int)
|
||
category_products = defaultdict(list)
|
||
|
||
for p in products:
|
||
category = p.get("category", {}).get("name", "Unknown")
|
||
if isinstance(category, dict):
|
||
category = category.get("name", "Unknown")
|
||
category_counts[category] += 1
|
||
category_products[category].append(p)
|
||
|
||
# Kategori bazlı ortalama social proof hesapla
|
||
category_avg_social = {}
|
||
social_details = social_data.get("details", {})
|
||
|
||
for category, cat_products in category_products.items():
|
||
total_views = 0
|
||
total_baskets = 0
|
||
count = 0
|
||
|
||
for product in cat_products:
|
||
pid = str(product.get("id"))
|
||
if pid in social_details:
|
||
views = social_details[pid].get("page_views", 0) or 0
|
||
baskets = social_details[pid].get("baskets", 0) or 0
|
||
if views > 0:
|
||
total_views += views
|
||
total_baskets += baskets
|
||
count += 1
|
||
|
||
if count > 0:
|
||
category_avg_social[category] = {
|
||
"avg_views": total_views / count,
|
||
"avg_baskets": total_baskets / count
|
||
}
|
||
else:
|
||
category_avg_social[category] = {
|
||
"avg_views": 0,
|
||
"avg_baskets": 0
|
||
}
|
||
|
||
hidden_champions = []
|
||
|
||
for product in products:
|
||
# Temel veriler
|
||
rating = get_rating_value(product)
|
||
review_count = get_review_count(product)
|
||
pid = str(product.get("id"))
|
||
social = social_details.get(pid, {})
|
||
|
||
page_views = social.get("page_views", 0) or 0
|
||
orders = social.get("orders", 0) or 0
|
||
baskets = social.get("baskets", 0) or 0
|
||
favorites = social.get("favorites", 0) or 0
|
||
|
||
conversion_rate = (orders / page_views * 100) if page_views > 0 else 0
|
||
|
||
# Kategori bilgisi
|
||
category = product.get("category", {})
|
||
if isinstance(category, dict):
|
||
category_name = category.get("name", "Unknown")
|
||
else:
|
||
category_name = category if category else "Unknown"
|
||
|
||
# Competition level
|
||
category_count = category_counts.get(category_name, 0)
|
||
if category_count < 100:
|
||
competition_level = "low"
|
||
elif category_count < 500:
|
||
competition_level = "medium"
|
||
else:
|
||
competition_level = "high"
|
||
|
||
# Kategori ortalaması ile karşılaştır
|
||
cat_avg = category_avg_social.get(category_name, {"avg_views": 0, "avg_baskets": 0})
|
||
threshold_views = cat_avg["avg_views"] * filters.get("social_multiplier", 2.0)
|
||
threshold_baskets = cat_avg["avg_baskets"] * filters.get("social_multiplier", 2.0)
|
||
|
||
# Eğer kategori ortalaması 0 veya çok düşükse, minimum threshold kullan
|
||
min_views_threshold = 100 # Minimum görüntülenme
|
||
min_baskets_threshold = 5 # Minimum sepet
|
||
|
||
# Threshold'ları ayarla
|
||
if threshold_views < min_views_threshold:
|
||
threshold_views = min_views_threshold
|
||
if threshold_baskets < min_baskets_threshold:
|
||
threshold_baskets = min_baskets_threshold
|
||
|
||
# Minimum Orders kontrolü (satış verisi çok önemli)
|
||
min_orders = filters.get("min_orders", 1) # Varsayılan: en az 1 satış
|
||
|
||
# Özelleştirilmiş Filtreleme (daha esnek)
|
||
passes_filter = (
|
||
rating >= filters.get("min_rating", 4.6) and
|
||
review_count < filters.get("max_review_count", 30) and
|
||
review_count >= 1 and # En az 1 yorum olmalı
|
||
orders >= min_orders and # EN AZ 1 SATIŞ OLMALI (satış verisi çok önemli)
|
||
(page_views >= threshold_views or page_views >= min_views_threshold) and # Kategori ortalamasının üzerinde VEYA minimum threshold
|
||
(baskets >= threshold_baskets or baskets >= min_baskets_threshold) and # Sepet de kategori ortalamasının üzerinde VEYA minimum
|
||
(conversion_rate >= 1.0 or page_views >= 500) # Minimum %1 conversion VEYA yüksek görüntülenme
|
||
)
|
||
|
||
if passes_filter:
|
||
# Potential score hesapla
|
||
potential_score = calculate_potential_score(
|
||
page_views, orders, review_count, conversion_rate, competition_level
|
||
)
|
||
|
||
# Hidden champion score hesapla
|
||
hidden_champion_score = self._calculate_hidden_champion_score(
|
||
rating, potential_score, conversion_rate, competition_level,
|
||
page_views, threshold_views, baskets, threshold_baskets
|
||
)
|
||
|
||
# Min score kontrolü
|
||
if hidden_champion_score >= filters.get("min_score", 70):
|
||
# Trendyol linki oluştur
|
||
product_url = product.get("url", "")
|
||
if product_url:
|
||
# URL relative ise (örn: /kumtel/urun-p-123), başına domain ekle
|
||
if product_url.startswith("/"):
|
||
product_url = f"https://www.trendyol.com{product_url}"
|
||
elif not product_url.startswith("http"):
|
||
product_url = f"https://www.trendyol.com{product_url}"
|
||
elif product.get("id"):
|
||
# Eğer url yoksa, product_id'den oluştur
|
||
product_url = f"https://www.trendyol.com/urun/{product.get('id')}"
|
||
else:
|
||
product_url = ""
|
||
|
||
# Görsel URL'i al (imageUrl veya images array'inden ilk eleman)
|
||
image_url = product.get("imageUrl", "")
|
||
if not image_url:
|
||
# images array'inden ilk elemanı al
|
||
images = product.get("images", [])
|
||
if images and len(images) > 0:
|
||
image_url = images[0] if isinstance(images[0], str) else str(images[0])
|
||
# Eğer hala boşsa, boş string olarak bırak
|
||
if not image_url:
|
||
image_url = ""
|
||
|
||
hidden_champions.append({
|
||
"product_id": product.get("id"),
|
||
"name": product.get("name", ""),
|
||
"brand": product.get("brand", {}).get("name", "Unknown"),
|
||
"category": category_name,
|
||
"rating": round(rating, 2),
|
||
"review_count": review_count,
|
||
"price": product.get("price", {}).get("sellingPrice", 0),
|
||
"page_views": page_views,
|
||
"orders": orders,
|
||
"baskets": baskets,
|
||
"favorites": favorites,
|
||
"conversion_rate": round(conversion_rate, 2),
|
||
"competition_level": competition_level,
|
||
"potential_score": potential_score,
|
||
"hidden_champion_score": hidden_champion_score,
|
||
"image": image_url, # Sosyal kanıt sekmesiyle uyumlu olması için "image" kullan
|
||
"image_url": image_url, # Geriye dönük uyumluluk için
|
||
"url": product_url, # Sosyal kanıt sekmesiyle uyumlu olması için "url" kullan
|
||
"product_url": product_url, # Geriye dönük uyumluluk için
|
||
"social_performance": {
|
||
"views_vs_category_avg": round((page_views / cat_avg["avg_views"]) if cat_avg["avg_views"] > 0 else 0, 2),
|
||
"baskets_vs_category_avg": round((baskets / cat_avg["avg_baskets"]) if cat_avg["avg_baskets"] > 0 else 0, 2),
|
||
"category_avg_views": round(cat_avg["avg_views"], 0),
|
||
"category_avg_baskets": round(cat_avg["avg_baskets"], 0)
|
||
}
|
||
})
|
||
|
||
# Skora göre sırala
|
||
hidden_champions.sort(key=lambda x: x["hidden_champion_score"], reverse=True)
|
||
|
||
# Limit
|
||
result = hidden_champions[:filters.get("limit", 50)]
|
||
|
||
return {
|
||
"total_found": len(hidden_champions),
|
||
"hidden_champions": result,
|
||
"summary": {
|
||
"avg_score": round(sum(hc["hidden_champion_score"] for hc in result) / len(result), 2) if result else 0,
|
||
"avg_rating": round(sum(hc["rating"] for hc in result) / len(result), 2) if result else 0,
|
||
"avg_conversion": round(sum(hc["conversion_rate"] for hc in result) / len(result), 2) if result else 0,
|
||
"low_competition_count": len([hc for hc in result if hc["competition_level"] == "low"]),
|
||
"avg_social_performance": round(
|
||
sum(hc["social_performance"]["views_vs_category_avg"] for hc in result) / len(result), 2
|
||
) if result else 0
|
||
},
|
||
"filters_applied": filters
|
||
}
|
||
|
||
def _calculate_hidden_champion_score(
|
||
self,
|
||
rating: float,
|
||
potential_score: float,
|
||
conversion_rate: float,
|
||
competition_level: str,
|
||
page_views: int,
|
||
threshold_views: float,
|
||
baskets: int,
|
||
threshold_baskets: float
|
||
) -> float:
|
||
"""
|
||
Gizli şampiyon skoru hesapla (özelleştirilmiş)
|
||
|
||
Formül:
|
||
- Rating skoru: 30 puan (4.6+ = 30, 4.8+ = 35)
|
||
- Potential score: 25 puan
|
||
- Conversion rate: 20 puan
|
||
- Social performance bonus: 15 puan (kategori ortalamasının üzerinde)
|
||
- Competition level: 10 puan
|
||
"""
|
||
score = 0
|
||
|
||
# 1. Rating skoru (30 puan)
|
||
if rating >= 4.8:
|
||
score += 35
|
||
elif rating >= 4.6:
|
||
score += 30
|
||
elif rating >= 4.5:
|
||
score += 25
|
||
elif rating >= 4.0:
|
||
score += 15
|
||
else:
|
||
score += 0
|
||
|
||
# 2. Potansiyel skoru (25 puan)
|
||
score += (potential_score / 100) * 25
|
||
|
||
# 3. Conversion rate skoru (20 puan)
|
||
if conversion_rate >= 5:
|
||
score += 20
|
||
elif conversion_rate >= 3:
|
||
score += 15
|
||
elif conversion_rate >= 2:
|
||
score += 10
|
||
else:
|
||
score += 5
|
||
|
||
# 4. Social performance bonus (15 puan)
|
||
# Kategori ortalamasının ne kadar üzerinde?
|
||
views_multiplier = (page_views / threshold_views) if threshold_views > 0 else 0
|
||
baskets_multiplier = (baskets / threshold_baskets) if threshold_baskets > 0 else 0
|
||
avg_multiplier = (views_multiplier + baskets_multiplier) / 2
|
||
|
||
if avg_multiplier >= 3.0:
|
||
score += 15 # Kategori ortalamasının 3+ katı
|
||
elif avg_multiplier >= 2.5:
|
||
score += 12
|
||
elif avg_multiplier >= 2.0:
|
||
score += 10
|
||
else:
|
||
score += 5
|
||
|
||
# 5. Rekabet seviyesi skoru (10 puan)
|
||
if competition_level == "low":
|
||
score += 10
|
||
elif competition_level == "medium":
|
||
score += 7
|
||
else:
|
||
score += 3
|
||
|
||
return min(100, round(score, 2))
|
||
|